Skip to content

Commit 223cb4a

Browse files
authored
Add named query filter support for .NET 10+ and verify interface compatibility (#305)
2 parents 4af600e + b02d386 commit 223cb4a

File tree

6 files changed

+426
-7
lines changed

6 files changed

+426
-7
lines changed

TinyHelpers.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
</Folder>
1414
<Folder Name="/Tests/">
1515
<Project Path="tests/TinyHelpers.Tests/TinyHelpers.Tests.csproj" />
16+
<Project Path="tests/TinyHelpers.EntityFrameworkCore.Tests/TinyHelpers.EntityFrameworkCore.Tests.csproj" />
1617
</Folder>
1718
<Project Path="src/TinyHelpers.AspNetCore.Swashbuckle/TinyHelpers.AspNetCore.Swashbuckle.csproj" />
1819
<Project Path="src/TinyHelpers.AspNetCore/TinyHelpers.AspNetCore.csproj" />

src/TinyHelpers.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,122 @@
44

55
namespace TinyHelpers.EntityFrameworkCore.Extensions;
66

7+
/// <summary>
8+
/// Provides extension methods for <see cref="ModelBuilder"/> to apply query filters and retrieve entity types.
9+
/// </summary>
710
public static class ModelBuilderExtensions
811
{
12+
/// <summary>
13+
/// Applies a global query filter to all entity types assignable to <typeparamref name="TEntity"/>.
14+
/// </summary>
15+
/// <typeparam name="TEntity">The base type or interface to match entity types against.</typeparam>
16+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
17+
/// <param name="expression">The filter expression to apply.</param>
918
public static void ApplyQueryFilter<TEntity>(this ModelBuilder modelBuilder, Expression<Func<TEntity, bool>> expression)
19+
{
20+
foreach (var clrType in modelBuilder.GetEntityTypes<TEntity>())
21+
{
22+
var parameter = Expression.Parameter(clrType);
23+
var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body);
24+
modelBuilder.Entity(clrType).HasQueryFilter(Expression.Lambda(body, parameter));
25+
}
26+
}
27+
28+
/// <summary>
29+
/// Applies a global query filter to all entity types that have a property with the specified name and type.
30+
/// </summary>
31+
/// <typeparam name="TType">The type of the property to filter on.</typeparam>
32+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
33+
/// <param name="propertyName">The name of the property to filter on.</param>
34+
/// <param name="value">The value to compare the property against.</param>
35+
public static void ApplyQueryFilter<TType>(this ModelBuilder modelBuilder, string propertyName, TType value)
1036
{
1137
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
1238
{
13-
if (typeof(TEntity).IsAssignableFrom(entityType.ClrType))
39+
var property = entityType.FindProperty(propertyName);
40+
if (property?.ClrType == typeof(TType))
1441
{
1542
var parameter = Expression.Parameter(entityType.ClrType);
16-
var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body);
17-
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(Expression.Lambda(body, parameter));
43+
var propertyAccess = Expression.Call(typeof(EF), nameof(EF.Property), [typeof(TType)], parameter, Expression.Constant(propertyName));
44+
var filter = Expression.Lambda(Expression.Equal(propertyAccess, Expression.Constant(value, typeof(TType))), parameter);
45+
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
1846
}
1947
}
2048
}
2149

22-
public static void ApplyQueryFilter<TType>(this ModelBuilder modelBuilder, string propertyName, TType value)
50+
#if NET10_0_OR_GREATER
51+
/// <summary>
52+
/// Applies a named query filter to all entity types assignable to <typeparamref name="TEntity"/>.
53+
/// Named query filters can be selectively disabled at query time using <c>IgnoreQueryFilters</c>.
54+
/// </summary>
55+
/// <typeparam name="TEntity">The base type or interface to match entity types against.</typeparam>
56+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
57+
/// <param name="filterName">The name to assign to the query filter.</param>
58+
/// <param name="expression">The filter expression to apply.</param>
59+
/// <remarks>
60+
/// This feature requires .NET 10 or greater. Named query filters allow multiple filters per entity type
61+
/// and selective disabling via <c>IgnoreQueryFilters(["filterName"])</c>.
62+
/// See <see href="https://learn.microsoft.com/ef/core/querying/filters">EF Core Query Filters</see> for more information.
63+
/// </remarks>
64+
public static void ApplyQueryFilter<TEntity>(this ModelBuilder modelBuilder, string filterName, Expression<Func<TEntity, bool>> expression)
65+
{
66+
foreach (var clrType in modelBuilder.GetEntityTypes<TEntity>())
67+
{
68+
var parameter = Expression.Parameter(clrType);
69+
var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body);
70+
modelBuilder.Entity(clrType).HasQueryFilter(filterName, Expression.Lambda(body, parameter));
71+
}
72+
}
73+
74+
/// <summary>
75+
/// Applies a named query filter to all entity types that have a property with the specified name and type.
76+
/// Named query filters can be selectively disabled at query time using <c>IgnoreQueryFilters</c>.
77+
/// </summary>
78+
/// <typeparam name="TType">The type of the property to filter on.</typeparam>
79+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
80+
/// <param name="filterName">The name to assign to the query filter.</param>
81+
/// <param name="propertyName">The name of the property to filter on.</param>
82+
/// <param name="value">The value to compare the property against.</param>
83+
/// <remarks>
84+
/// This feature requires .NET 10 or greater. Named query filters allow multiple filters per entity type
85+
/// and selective disabling via <c>IgnoreQueryFilters(["filterName"])</c>.
86+
/// See <see href="https://learn.microsoft.com/ef/core/querying/filters">EF Core Query Filters</see> for more information.
87+
/// </remarks>
88+
public static void ApplyQueryFilter<TType>(this ModelBuilder modelBuilder, string filterName, string propertyName, TType value)
2389
{
2490
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
2591
{
2692
var property = entityType.FindProperty(propertyName);
2793
if (property?.ClrType == typeof(TType))
2894
{
2995
var parameter = Expression.Parameter(entityType.ClrType);
30-
var filter = Expression.Lambda(Expression.Equal(Expression.Property(parameter, propertyName), Expression.Constant(value)), parameter);
31-
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
96+
var propertyAccess = Expression.Call(typeof(EF), nameof(EF.Property), [typeof(TType)], parameter, Expression.Constant(propertyName));
97+
var filter = Expression.Lambda(Expression.Equal(propertyAccess, Expression.Constant(value, typeof(TType))), parameter);
98+
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filterName, filter);
3299
}
33100
}
34101
}
102+
#endif
35103

104+
/// <summary>
105+
/// Gets all entity types in the model that are assignable to <typeparamref name="TType"/>.
106+
/// </summary>
107+
/// <typeparam name="TType">The base type or interface to match entity types against.</typeparam>
108+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to query.</param>
109+
/// <returns>An enumerable of CLR types that are assignable to <typeparamref name="TType"/>.</returns>
36110
public static IEnumerable<Type> GetEntityTypes<TType>(this ModelBuilder modelBuilder)
37111
=> GetEntityTypes(modelBuilder, typeof(TType));
38112

113+
/// <summary>
114+
/// Gets all entity types in the model that are assignable to the specified <paramref name="baseType"/>.
115+
/// </summary>
116+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to query.</param>
117+
/// <param name="baseType">The base type or interface to match entity types against.</param>
118+
/// <returns>An enumerable of CLR types that are assignable to <paramref name="baseType"/>.</returns>
39119
public static IEnumerable<Type> GetEntityTypes(this ModelBuilder modelBuilder, Type baseType)
40120
{
41121
var entityTypes = modelBuilder.Model.GetEntityTypes()
42-
.Where(t => baseType.IsAssignableFrom(t.ClrType))
122+
.Where(t => t.ClrType.IsAssignableTo(baseType))
43123
.ToList();
44124

45125
return entityTypes.Select(t => t.ClrType);

src/TinyHelpers.EntityFrameworkCore/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,55 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
8282
}
8383
```
8484

85+
It also works with interfaces:
86+
87+
```csharp
88+
public interface ISoftDeletable
89+
{
90+
bool IsDeleted { get; set; }
91+
}
92+
93+
public class Person : ISoftDeletable
94+
{
95+
public int Id { get; set; }
96+
public bool IsDeleted { get; set; }
97+
}
98+
99+
public class City : ISoftDeletable
100+
{
101+
public int Id { get; set; }
102+
public bool IsDeleted { get; set; }
103+
}
104+
105+
// using TinyHelpers.EntityFrameworkCore.Extensions;
106+
protected override void OnModelCreating(ModelBuilder modelBuilder)
107+
{
108+
// Apply a filter to all the entities that implement ISoftDeletable.
109+
modelBuilder.ApplyQueryFilter<ISoftDeletable>(e => !e.IsDeleted);
110+
}
111+
```
112+
113+
#### Named Query Filters (.NET 10+)
114+
115+
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:
116+
117+
```csharp
118+
// using TinyHelpers.EntityFrameworkCore.Extensions;
119+
protected override void OnModelCreating(ModelBuilder modelBuilder)
120+
{
121+
// Apply named filters to all entities that implement the corresponding interfaces.
122+
modelBuilder.ApplyQueryFilter<ISoftDeletable>("SoftDelete", e => !e.IsDeleted);
123+
modelBuilder.ApplyQueryFilter<ITenantEntity>("TenantFilter", e => e.TenantId == currentTenantId);
124+
}
125+
```
126+
127+
Named filters can then be selectively disabled in specific queries:
128+
129+
```csharp
130+
// Disable only the soft-delete filter, keeping the tenant filter active.
131+
var allItems = await context.People.IgnoreQueryFilters(["SoftDelete"]).ToListAsync();
132+
```
133+
85134
### Contributing
86135

87136
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.

0 commit comments

Comments
 (0)