Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions TinyHelpers.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</Folder>
<Folder Name="/Tests/">
<Project Path="tests/TinyHelpers.Tests/TinyHelpers.Tests.csproj" />
<Project Path="tests/TinyHelpers.EntityFrameworkCore.Tests/TinyHelpers.EntityFrameworkCore.Tests.csproj" />
</Folder>
<Project Path="src/TinyHelpers.AspNetCore.Swashbuckle/TinyHelpers.AspNetCore.Swashbuckle.csproj" />
<Project Path="src/TinyHelpers.AspNetCore/TinyHelpers.AspNetCore.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,122 @@

namespace TinyHelpers.EntityFrameworkCore.Extensions;

/// <summary>
/// Provides extension methods for <see cref="ModelBuilder"/> to apply query filters and retrieve entity types.
/// </summary>
public static class ModelBuilderExtensions
{
/// <summary>
/// Applies a global query filter to all entity types assignable to <typeparamref name="TEntity"/>.
/// </summary>
/// <typeparam name="TEntity">The base type or interface to match entity types against.</typeparam>
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
/// <param name="expression">The filter expression to apply.</param>
public static void ApplyQueryFilter<TEntity>(this ModelBuilder modelBuilder, Expression<Func<TEntity, bool>> expression)
{
foreach (var clrType in modelBuilder.GetEntityTypes<TEntity>())
{
var parameter = Expression.Parameter(clrType);
var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body);
modelBuilder.Entity(clrType).HasQueryFilter(Expression.Lambda(body, parameter));
}
}

/// <summary>
/// Applies a global query filter to all entity types that have a property with the specified name and type.
/// </summary>
/// <typeparam name="TType">The type of the property to filter on.</typeparam>
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
/// <param name="propertyName">The name of the property to filter on.</param>
/// <param name="value">The value to compare the property against.</param>
public static void ApplyQueryFilter<TType>(this ModelBuilder modelBuilder, string propertyName, TType value)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(TEntity).IsAssignableFrom(entityType.ClrType))
var property = entityType.FindProperty(propertyName);
if (property?.ClrType == typeof(TType))
{
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 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);
}
}
}

public static void ApplyQueryFilter<TType>(this ModelBuilder modelBuilder, string propertyName, TType value)
#if NET10_0_OR_GREATER
/// <summary>
/// Applies a named query filter to all entity types assignable to <typeparamref name="TEntity"/>.
/// Named query filters can be selectively disabled at query time using <c>IgnoreQueryFilters</c>.
/// </summary>
/// <typeparam name="TEntity">The base type or interface to match entity types against.</typeparam>
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
/// <param name="filterName">The name to assign to the query filter.</param>
/// <param name="expression">The filter expression to apply.</param>
/// <remarks>
/// This feature requires .NET 10 or greater. Named query filters allow multiple filters per entity type
/// and selective disabling via <c>IgnoreQueryFilters(["filterName"])</c>.
/// See <see href="https://learn.microsoft.com/ef/core/querying/filters">EF Core Query Filters</see> for more information.
/// </remarks>
public static void ApplyQueryFilter<TEntity>(this ModelBuilder modelBuilder, string filterName, Expression<Func<TEntity, bool>> expression)
{
foreach (var clrType in modelBuilder.GetEntityTypes<TEntity>())
{
var parameter = Expression.Parameter(clrType);
var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameter, expression.Body);
modelBuilder.Entity(clrType).HasQueryFilter(filterName, Expression.Lambda(body, parameter));
}
}

/// <summary>
/// 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 <c>IgnoreQueryFilters</c>.
/// </summary>
/// <typeparam name="TType">The type of the property to filter on.</typeparam>
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to apply the filter to.</param>
/// <param name="filterName">The name to assign to the query filter.</param>
/// <param name="propertyName">The name of the property to filter on.</param>
/// <param name="value">The value to compare the property against.</param>
/// <remarks>
/// This feature requires .NET 10 or greater. Named query filters allow multiple filters per entity type
/// and selective disabling via <c>IgnoreQueryFilters(["filterName"])</c>.
/// See <see href="https://learn.microsoft.com/ef/core/querying/filters">EF Core Query Filters</see> for more information.
/// </remarks>
public static void ApplyQueryFilter<TType>(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(filter);
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);
}
}
}
#endif

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

/// <summary>
/// Gets all entity types in the model that are assignable to the specified <paramref name="baseType"/>.
/// </summary>
/// <param name="modelBuilder">The <see cref="ModelBuilder"/> to query.</param>
/// <param name="baseType">The base type or interface to match entity types against.</param>
/// <returns>An enumerable of CLR types that are assignable to <paramref name="baseType"/>.</returns>
public static IEnumerable<Type> 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);
Expand Down
49 changes: 49 additions & 0 deletions src/TinyHelpers.EntityFrameworkCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ISoftDeletable>(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<ISoftDeletable>("SoftDelete", e => !e.IsDeleted);
modelBuilder.ApplyQueryFilter<ITenantEntity>("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.
Expand Down
Loading
Loading