Skip to content

Instantly share code, notes, and snippets.

@dukesteen
Last active November 15, 2025 23:32
Show Gist options
  • Select an option

  • Save dukesteen/f6b3b9935fae2202bba48f03c4d44c5f to your computer and use it in GitHub Desktop.

Select an option

Save dukesteen/f6b3b9935fae2202bba48f03c4d44c5f to your computer and use it in GitHub Desktop.
EFCore Named Query Filter on interface that is applied to entity
using System.Linq.Expressions;
namespace Timespace.Api.Database.Extensions;
internal sealed class ReplacingExpressionVisitor(Expression oldExpression, Expression newExpression) : ExpressionVisitor
{
public override Expression? Visit(Expression? node)
{
// If the current node is the one we're looking for, return the new expression.
return node == oldExpression ? newExpression : base.Visit(node);
}
}
internal sealed class QueryFilterExtensions
{
/// <summary>
/// Creates a new LambdaExpression for a specific entity type from a template expression based on an interface.
/// This is used to adapt a generic query filter to a concrete entity type.
/// </summary>
/// <typeparam name="TInterface">The interface type used in the template expression.</typeparam>
/// <param name="filterExpression">The template expression (e.g., entity => entity.TenantId == ...).</param>
/// <param name="entityType">The concrete entity type to apply the filter to (e.g., typeof(Contract)).</param>
/// <returns>A new LambdaExpression with its parameter replaced with the concrete entity type.</returns>
public static LambdaExpression CreateEntityTypeSpecificFilter<TInterface>(
Expression<Func<TInterface, bool>> filterExpression,
Type entityType)
{
// Get the parameter from the original interface-based expression (e.g., "entity" of type ITenantScoped).
var originalParameter = filterExpression.Parameters.Single();
// Create a new parameter for the concrete entity type (e.g., "entity" of type Contract).
// We use the same name for clarity.
var newParameter = Expression.Parameter(entityType, originalParameter.Name);
// Create a visitor that will replace all occurrences of the original parameter with the new one.
var visitor = new ReplacingExpressionVisitor(originalParameter, newParameter);
// Visit the body of the original expression to create the new body with the replaced parameter.
var newBody = visitor.Visit(filterExpression.Body);
// Create and return the new lambda expression with the new body and new parameter.
return Expression.Lambda(newBody!, newParameter);
}
}
protected override void OnModelCreating(ModelBuilder builder)
{
_ = builder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
foreach (var entityType in builder.Model.GetEntityTypes())
{
if (entityType.ClrType.IsAssignableTo(typeof(ITenantScoped)))
{
var tenantFilter = QueryFilterExtensions.CreateEntityTypeSpecificFilter<ITenantScoped>(
entity => entity.TenantId == usageContext.TenantId || usageContext.TenantId == null,
entityType.ClrType);
_ = builder.Entity(entityType.ClrType).HasQueryFilter(GlobalQueryFilters.Tenant, tenantFilter);
}
if (entityType.ClrType.IsAssignableTo(typeof(ISoftDeletableEntity)))
{
var softDeleteFilter = QueryFilterExtensions.CreateEntityTypeSpecificFilter<ISoftDeletableEntity>(
entity => entity.DeletedAt == null,
entityType.ClrType);
_ = builder.Entity(entityType.ClrType).HasQueryFilter(GlobalQueryFilters.SoftDelete, softDeleteFilter);
}
}
base.OnModelCreating(builder);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment