Skip to content

Commit ab826e0

Browse files
Copilotmarcominerva
andcommitted
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>
1 parent 895620c commit ab826e0

File tree

6 files changed

+423
-0
lines changed

6 files changed

+423
-0
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: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,17 @@
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)
1019
{
1120
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
@@ -19,6 +28,13 @@ public static void ApplyQueryFilter<TEntity>(this ModelBuilder modelBuilder, Exp
1928
}
2029
}
2130

31+
/// <summary>
32+
/// Applies a global query filter to all entity types that have a property with the specified name and type.
33+
/// </summary>
34+
/// <typeparam name="TType">The type of the property to filter on.</typeparam>
35+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
36+
/// <param name="propertyName">The name of the property to filter on.</param>
37+
/// <param name="value">The value to compare the property against.</param>
2238
public static void ApplyQueryFilter<TType>(this ModelBuilder modelBuilder, string propertyName, TType value)
2339
{
2440
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
@@ -33,9 +49,77 @@ public static void ApplyQueryFilter<TType>(this ModelBuilder modelBuilder, strin
3349
}
3450
}
3551

52+
#if NET10_0_OR_GREATER
53+
/// <summary>
54+
/// Applies a named query filter to all entity types assignable to <typeparamref name="TEntity"/>.
55+
/// Named query filters can be selectively disabled at query time using <c>IgnoreQueryFilters</c>.
56+
/// </summary>
57+
/// <typeparam name="TEntity">The base type or interface to match entity types against.</typeparam>
58+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
59+
/// <param name="filterName">The name to assign to the query filter.</param>
60+
/// <param name="expression">The filter expression to apply.</param>
61+
/// <remarks>
62+
/// This feature requires .NET 10 or greater. Named query filters allow multiple filters per entity type
63+
/// and selective disabling via <c>IgnoreQueryFilters(["filterName"])</c>.
64+
/// See <see href="https://learn.microsoft.com/ef/core/querying/filters">EF Core Query Filters</see> for more information.
65+
/// </remarks>
66+
public static void ApplyQueryFilter<TEntity>(this ModelBuilder modelBuilder, string filterName, Expression<Func<TEntity, bool>> expression)
67+
{
68+
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
69+
{
70+
if (typeof(TEntity).IsAssignableFrom(entityType.ClrType))
71+
{
72+
var parameter = Expression.Parameter(entityType.ClrType);
73+
var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body);
74+
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filterName, Expression.Lambda(body, parameter));
75+
}
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Applies a named query filter to all entity types that have a property with the specified name and type.
81+
/// Named query filters can be selectively disabled at query time using <c>IgnoreQueryFilters</c>.
82+
/// </summary>
83+
/// <typeparam name="TType">The type of the property to filter on.</typeparam>
84+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
85+
/// <param name="filterName">The name to assign to the query filter.</param>
86+
/// <param name="propertyName">The name of the property to filter on.</param>
87+
/// <param name="value">The value to compare the property against.</param>
88+
/// <remarks>
89+
/// This feature requires .NET 10 or greater. Named query filters allow multiple filters per entity type
90+
/// and selective disabling via <c>IgnoreQueryFilters(["filterName"])</c>.
91+
/// See <see href="https://learn.microsoft.com/ef/core/querying/filters">EF Core Query Filters</see> for more information.
92+
/// </remarks>
93+
public static void ApplyQueryFilter<TType>(this ModelBuilder modelBuilder, string filterName, string propertyName, TType value)
94+
{
95+
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
96+
{
97+
var property = entityType.FindProperty(propertyName);
98+
if (property?.ClrType == typeof(TType))
99+
{
100+
var parameter = Expression.Parameter(entityType.ClrType);
101+
var filter = Expression.Lambda(Expression.Equal(Expression.Property(parameter, propertyName), Expression.Constant(value)), parameter);
102+
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filterName, filter);
103+
}
104+
}
105+
}
106+
#endif
107+
108+
/// <summary>
109+
/// Gets all entity types in the model that are assignable to <typeparamref name="TType"/>.
110+
/// </summary>
111+
/// <typeparam name="TType">The base type or interface to match entity types against.</typeparam>
112+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to query.</param>
113+
/// <returns>An enumerable of CLR types that are assignable to <typeparamref name="TType"/>.</returns>
36114
public static IEnumerable<Type> GetEntityTypes<TType>(this ModelBuilder modelBuilder)
37115
=> GetEntityTypes(modelBuilder, typeof(TType));
38116

117+
/// <summary>
118+
/// Gets all entity types in the model that are assignable to the specified <paramref name="baseType"/>.
119+
/// </summary>
120+
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to query.</param>
121+
/// <param name="baseType">The base type or interface to match entity types against.</param>
122+
/// <returns>An enumerable of CLR types that are assignable to <paramref name="baseType"/>.</returns>
39123
public static IEnumerable<Type> GetEntityTypes(this ModelBuilder modelBuilder, Type baseType)
40124
{
41125
var entityTypes = modelBuilder.Model.GetEntityTypes()

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)