From 895620ccabdfcf38db7b9d55a523152e5ece3dc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:00:18 +0000 Subject: [PATCH 1/3] Initial plan From ab826e092189b8b5b39f945008f40d0691033b08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:10:06 +0000 Subject: [PATCH 2/3] Add named query filter support for .NET 10+ and verify interface compatibility - Add ApplyQueryFilter overloads with filterName parameter (NET10_0_OR_GREATER) - Add XML documentation to all ModelBuilderExtensions methods - Create TinyHelpers.EntityFrameworkCore.Tests project with 12 unit tests - Verify ApplyQueryFilter works with both base classes and interfaces - Update README with interface usage and named query filter documentation Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- TinyHelpers.slnx | 1 + .../Extensions/ModelBuilderExtensions.cs | 84 ++++++ src/TinyHelpers.EntityFrameworkCore/README.md | 49 ++++ .../Extensions/ModelBuilderExtensionsTests.cs | 261 ++++++++++++++++++ ...nyHelpers.EntityFrameworkCore.Tests.csproj | 27 ++ .../Usings.cs | 1 + 6 files changed, 423 insertions(+) create mode 100644 tests/TinyHelpers.EntityFrameworkCore.Tests/Extensions/ModelBuilderExtensionsTests.cs create mode 100644 tests/TinyHelpers.EntityFrameworkCore.Tests/TinyHelpers.EntityFrameworkCore.Tests.csproj create mode 100644 tests/TinyHelpers.EntityFrameworkCore.Tests/Usings.cs diff --git a/TinyHelpers.slnx b/TinyHelpers.slnx index 34ca223..da10b11 100644 --- a/TinyHelpers.slnx +++ b/TinyHelpers.slnx @@ -13,6 +13,7 @@ + diff --git a/src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs b/src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs index 28133a0..9add07f 100644 --- a/src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs +++ b/src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs @@ -4,8 +4,17 @@ namespace TinyHelpers.EntityFrameworkCore.Extensions; +/// +/// Provides extension methods for to apply query filters and retrieve entity types. +/// public static class ModelBuilderExtensions { + /// + /// Applies a global query filter to all entity types assignable to . + /// + /// The base type or interface to match entity types against. + /// The to apply the filter to. + /// The filter expression to apply. public static void ApplyQueryFilter(this ModelBuilder modelBuilder, Expression> expression) { foreach (var entityType in modelBuilder.Model.GetEntityTypes()) @@ -19,6 +28,13 @@ public static void ApplyQueryFilter(this ModelBuilder modelBuilder, Exp } } + /// + /// Applies a global query filter to all entity types that have a property with the specified name and type. + /// + /// The type of the property to filter on. + /// The to apply the filter to. + /// The name of the property to filter on. + /// The value to compare the property against. public static void ApplyQueryFilter(this ModelBuilder modelBuilder, string propertyName, TType value) { foreach (var entityType in modelBuilder.Model.GetEntityTypes()) @@ -33,9 +49,77 @@ public static void ApplyQueryFilter(this ModelBuilder modelBuilder, strin } } +#if NET10_0_OR_GREATER + /// + /// Applies a named query filter to all entity types assignable to . + /// Named query filters can be selectively disabled at query time using IgnoreQueryFilters. + /// + /// The base type or interface to match entity types against. + /// The to apply the filter to. + /// The name to assign to the query filter. + /// The filter expression to apply. + /// + /// This feature requires .NET 10 or greater. Named query filters allow multiple filters per entity type + /// and selective disabling via IgnoreQueryFilters(["filterName"]). + /// See EF Core Query Filters for more information. + /// + public static void ApplyQueryFilter(this ModelBuilder modelBuilder, string filterName, Expression> expression) + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + if (typeof(TEntity).IsAssignableFrom(entityType.ClrType)) + { + var parameter = Expression.Parameter(entityType.ClrType); + var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body); + modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filterName, Expression.Lambda(body, parameter)); + } + } + } + + /// + /// Applies a named query filter to all entity types that have a property with the specified name and type. + /// Named query filters can be selectively disabled at query time using IgnoreQueryFilters. + /// + /// The type of the property to filter on. + /// The to apply the filter to. + /// The name to assign to the query filter. + /// The name of the property to filter on. + /// The value to compare the property against. + /// + /// This feature requires .NET 10 or greater. Named query filters allow multiple filters per entity type + /// and selective disabling via IgnoreQueryFilters(["filterName"]). + /// See EF Core Query Filters for more information. + /// + public static void ApplyQueryFilter(this ModelBuilder modelBuilder, string filterName, string propertyName, TType value) + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var property = entityType.FindProperty(propertyName); + if (property?.ClrType == typeof(TType)) + { + var parameter = Expression.Parameter(entityType.ClrType); + var filter = Expression.Lambda(Expression.Equal(Expression.Property(parameter, propertyName), Expression.Constant(value)), parameter); + modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filterName, filter); + } + } + } +#endif + + /// + /// Gets all entity types in the model that are assignable to . + /// + /// The base type or interface to match entity types against. + /// The to query. + /// An enumerable of CLR types that are assignable to . public static IEnumerable GetEntityTypes(this ModelBuilder modelBuilder) => GetEntityTypes(modelBuilder, typeof(TType)); + /// + /// Gets all entity types in the model that are assignable to the specified . + /// + /// The to query. + /// The base type or interface to match entity types against. + /// An enumerable of CLR types that are assignable to . public static IEnumerable GetEntityTypes(this ModelBuilder modelBuilder, Type baseType) { var entityTypes = modelBuilder.Model.GetEntityTypes() diff --git a/src/TinyHelpers.EntityFrameworkCore/README.md b/src/TinyHelpers.EntityFrameworkCore/README.md index 828ac38..5f4f94c 100644 --- a/src/TinyHelpers.EntityFrameworkCore/README.md +++ b/src/TinyHelpers.EntityFrameworkCore/README.md @@ -82,6 +82,55 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } ``` +It also works with interfaces: + +```csharp +public interface ISoftDeletable +{ + bool IsDeleted { get; set; } +} + +public class Person : ISoftDeletable +{ + public int Id { get; set; } + public bool IsDeleted { get; set; } +} + +public class City : ISoftDeletable +{ + public int Id { get; set; } + public bool IsDeleted { get; set; } +} + +// using TinyHelpers.EntityFrameworkCore.Extensions; +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + // Apply a filter to all the entities that implement ISoftDeletable. + modelBuilder.ApplyQueryFilter(e => !e.IsDeleted); +} +``` + +#### Named Query Filters (.NET 10+) + +Starting from .NET 10, the library supports [named query filters](https://learn.microsoft.com/ef/core/querying/filters), which allow attaching names to query filters and managing each one separately. This is useful when you need multiple filters per entity type and want to selectively disable specific filters at query time: + +```csharp +// using TinyHelpers.EntityFrameworkCore.Extensions; +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + // Apply named filters to all entities that implement the corresponding interfaces. + modelBuilder.ApplyQueryFilter("SoftDelete", e => !e.IsDeleted); + modelBuilder.ApplyQueryFilter("TenantFilter", e => e.TenantId == currentTenantId); +} +``` + +Named filters can then be selectively disabled in specific queries: + +```csharp +// Disable only the soft-delete filter, keeping the tenant filter active. +var allItems = await context.People.IgnoreQueryFilters(["SoftDelete"]).ToListAsync(); +``` + ### Contributing The project is constantly evolving. Contributions are welcome. Feel free to file issues and pull requests on the repo and we'll address them as we can. diff --git a/tests/TinyHelpers.EntityFrameworkCore.Tests/Extensions/ModelBuilderExtensionsTests.cs b/tests/TinyHelpers.EntityFrameworkCore.Tests/Extensions/ModelBuilderExtensionsTests.cs new file mode 100644 index 0000000..1c2ad43 --- /dev/null +++ b/tests/TinyHelpers.EntityFrameworkCore.Tests/Extensions/ModelBuilderExtensionsTests.cs @@ -0,0 +1,261 @@ +using Microsoft.EntityFrameworkCore; +using TinyHelpers.EntityFrameworkCore.Extensions; + +namespace TinyHelpers.EntityFrameworkCore.Tests.Extensions; + +public interface ISoftDeletable +{ + bool IsDeleted { get; set; } +} + +public interface ITenantEntity +{ + int TenantId { get; set; } +} + +public abstract class DeletableEntity +{ + public bool IsDeleted { get; set; } +} + +public class Person : DeletableEntity, ISoftDeletable, ITenantEntity +{ + public int Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public int TenantId { get; set; } +} + +public class City : DeletableEntity, ISoftDeletable +{ + public int Id { get; set; } + + public string Name { get; set; } = string.Empty; +} + +public class Country +{ + public int Id { get; set; } + + public string Name { get; set; } = string.Empty; +} + +public class ModelBuilderExtensionsTests +{ + [Fact] + public void ApplyQueryFilter_WithBaseClass_AppliesFilterToInheritingEntities() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter(e => !e.IsDeleted); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Single(personFilters); + Assert.Single(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyQueryFilter_WithInterface_AppliesFilterToImplementingEntities() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter(e => !e.IsDeleted); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Single(personFilters); + Assert.Single(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyQueryFilter_WithInterface_OnlyAppliesFilterToEntitiesThatImplementInterface() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter(e => e.TenantId == 1); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Single(personFilters); + Assert.Empty(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyQueryFilter_ByPropertyName_AppliesFilterToEntitiesWithMatchingProperty() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter("IsDeleted", false); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Single(personFilters); + Assert.Single(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyNamedQueryFilter_WithBaseClass_AppliesNamedFilterToInheritingEntities() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter("SoftDelete", e => !e.IsDeleted); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Single(personFilters); + Assert.Single(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyNamedQueryFilter_WithInterface_AppliesNamedFilterToImplementingEntities() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter("SoftDelete", e => !e.IsDeleted); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Single(personFilters); + Assert.Single(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyNamedQueryFilter_WithInterface_OnlyAppliesFilterToEntitiesThatImplementInterface() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter("TenantFilter", e => e.TenantId == 1); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Single(personFilters); + Assert.Empty(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyNamedQueryFilter_MultipleFilters_AllFiltersAreApplied() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter("SoftDelete", e => !e.IsDeleted); + modelBuilder.ApplyQueryFilter("TenantFilter", e => e.TenantId == 1); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Equal(2, personFilters.Count); + Assert.Single(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyNamedQueryFilter_ByPropertyName_AppliesNamedFilterToEntitiesWithMatchingProperty() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter("SoftDelete", "IsDeleted", false); + + var personFilters = modelBuilder.Model.FindEntityType(typeof(Person))!.GetDeclaredQueryFilters(); + var cityFilters = modelBuilder.Model.FindEntityType(typeof(City))!.GetDeclaredQueryFilters(); + var countryFilters = modelBuilder.Model.FindEntityType(typeof(Country))!.GetDeclaredQueryFilters(); + + Assert.Single(personFilters); + Assert.Single(cityFilters); + Assert.Empty(countryFilters); + } + + [Fact] + public void ApplyNamedQueryFilter_FindByName_ReturnsCorrectFilter() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.ApplyQueryFilter("SoftDelete", e => !e.IsDeleted); + modelBuilder.ApplyQueryFilter("TenantFilter", e => e.TenantId == 1); + + var personType = modelBuilder.Model.FindEntityType(typeof(Person))!; + + var softDeleteFilter = personType.FindDeclaredQueryFilter("SoftDelete"); + var tenantFilter = personType.FindDeclaredQueryFilter("TenantFilter"); + var nonExistentFilter = personType.FindDeclaredQueryFilter("NonExistent"); + + Assert.NotNull(softDeleteFilter); + Assert.NotNull(tenantFilter); + Assert.Null(nonExistentFilter); + } + + [Fact] + public void GetEntityTypes_WithBaseClass_ReturnsInheritingTypes() + { + var modelBuilder = CreateModelBuilder(); + + var types = modelBuilder.GetEntityTypes().ToList(); + + Assert.Contains(typeof(Person), types); + Assert.Contains(typeof(City), types); + Assert.DoesNotContain(typeof(Country), types); + } + + [Fact] + public void GetEntityTypes_WithInterface_ReturnsImplementingTypes() + { + var modelBuilder = CreateModelBuilder(); + + var types = modelBuilder.GetEntityTypes().ToList(); + + Assert.Contains(typeof(Person), types); + Assert.Contains(typeof(City), types); + Assert.DoesNotContain(typeof(Country), types); + } + + private static ModelBuilder CreateModelBuilder() + { + var modelBuilder = new ModelBuilder(); + + modelBuilder.Entity(b => + { + b.Property(p => p.Id); + b.Property(p => p.Name); + b.Property(p => p.IsDeleted); + b.Property(p => p.TenantId); + }); + + modelBuilder.Entity(b => + { + b.Property(c => c.Id); + b.Property(c => c.Name); + b.Property(c => c.IsDeleted); + }); + + modelBuilder.Entity(b => + { + b.Property(c => c.Id); + b.Property(c => c.Name); + }); + + return modelBuilder; + } +} diff --git a/tests/TinyHelpers.EntityFrameworkCore.Tests/TinyHelpers.EntityFrameworkCore.Tests.csproj b/tests/TinyHelpers.EntityFrameworkCore.Tests/TinyHelpers.EntityFrameworkCore.Tests.csproj new file mode 100644 index 0000000..d9a4996 --- /dev/null +++ b/tests/TinyHelpers.EntityFrameworkCore.Tests/TinyHelpers.EntityFrameworkCore.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + false + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/TinyHelpers.EntityFrameworkCore.Tests/Usings.cs b/tests/TinyHelpers.EntityFrameworkCore.Tests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/tests/TinyHelpers.EntityFrameworkCore.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; From b02d38608d96b227d10c2ca2416433f03f7e16f8 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Thu, 12 Mar 2026 09:48:36 +0100 Subject: [PATCH 3/3] Refactor query filter logic for EF Core compatibility Refactored entity type filtering to use GetEntityTypes and IsAssignableTo for improved robustness. Updated query filter property access to use EF.Property for better EF Core query translation support. These changes enhance maintainability and align with EF Core best practices. --- .../Extensions/ModelBuilderExtensions.cs | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs b/src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs index 9add07f..81be186 100644 --- a/src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs +++ b/src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs @@ -17,14 +17,11 @@ public static class ModelBuilderExtensions /// The filter expression to apply. public static void ApplyQueryFilter(this ModelBuilder modelBuilder, Expression> expression) { - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + foreach (var clrType in modelBuilder.GetEntityTypes()) { - if (typeof(TEntity).IsAssignableFrom(entityType.ClrType)) - { - var parameter = Expression.Parameter(entityType.ClrType); - var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body); - modelBuilder.Entity(entityType.ClrType).HasQueryFilter(Expression.Lambda(body, parameter)); - } + var parameter = Expression.Parameter(clrType); + var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body); + modelBuilder.Entity(clrType).HasQueryFilter(Expression.Lambda(body, parameter)); } } @@ -43,7 +40,8 @@ public static void ApplyQueryFilter(this ModelBuilder modelBuilder, strin if (property?.ClrType == typeof(TType)) { var parameter = Expression.Parameter(entityType.ClrType); - var filter = Expression.Lambda(Expression.Equal(Expression.Property(parameter, propertyName), Expression.Constant(value)), parameter); + var propertyAccess = Expression.Call(typeof(EF), nameof(EF.Property), [typeof(TType)], parameter, Expression.Constant(propertyName)); + var filter = Expression.Lambda(Expression.Equal(propertyAccess, Expression.Constant(value, typeof(TType))), parameter); modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter); } } @@ -65,14 +63,11 @@ public static void ApplyQueryFilter(this ModelBuilder modelBuilder, strin /// public static void ApplyQueryFilter(this ModelBuilder modelBuilder, string filterName, Expression> expression) { - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + foreach (var clrType in modelBuilder.GetEntityTypes()) { - if (typeof(TEntity).IsAssignableFrom(entityType.ClrType)) - { - var parameter = Expression.Parameter(entityType.ClrType); - var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body); - modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filterName, Expression.Lambda(body, parameter)); - } + var parameter = Expression.Parameter(clrType); + var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body); + modelBuilder.Entity(clrType).HasQueryFilter(filterName, Expression.Lambda(body, parameter)); } } @@ -98,7 +93,8 @@ public static void ApplyQueryFilter(this ModelBuilder modelBuilder, strin if (property?.ClrType == typeof(TType)) { var parameter = Expression.Parameter(entityType.ClrType); - var filter = Expression.Lambda(Expression.Equal(Expression.Property(parameter, propertyName), Expression.Constant(value)), parameter); + var propertyAccess = Expression.Call(typeof(EF), nameof(EF.Property), [typeof(TType)], parameter, Expression.Constant(propertyName)); + var filter = Expression.Lambda(Expression.Equal(propertyAccess, Expression.Constant(value, typeof(TType))), parameter); modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filterName, filter); } } @@ -123,7 +119,7 @@ public static IEnumerable GetEntityTypes(this ModelBuilder modelBui public static IEnumerable GetEntityTypes(this ModelBuilder modelBuilder, Type baseType) { var entityTypes = modelBuilder.Model.GetEntityTypes() - .Where(t => baseType.IsAssignableFrom(t.ClrType)) + .Where(t => t.ClrType.IsAssignableTo(baseType)) .ToList(); return entityTypes.Select(t => t.ClrType);