public partial class AppDbContext(DbContextOptions<AppDbContext> options, ILegalEntityProvider legalEntityProvider) : DbContext(options)
{
public Guid? LegalEntityId { get; } = legalEntityProvider.LegalEntity;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
// Apply a global query filter to all entities implementing IMultiLegalEntity.
// This ensures that queries only return records associated with the current LegalEntityId,
// which is determined from the 'extension_LegalEntityId' claim in the user's identity.
// The LegalEntityId is retrieved through the LegalEntityProvider, and only records with
// matching LegalEntityIds will be included in the query results.
// Global query filters are compiled and cannot be “switched off” on the fly unless you design them to be conditional.
modelBuilder.SetQueryFilterOnAllEntities<IMultiLegalEntity>(
x => LegalEntityId.HasValue && x.LegalEntities.Any(le => le.Id == LegalEntityId.Value)
);
}
}
I'm using EF Core global query filters to filter records based on LegalEntityId
. Essentially, if there is a LegalEntityId in the JWT token, the filter ensures that only records associated with the current LegalEntityId are returned. The LegalEntityId is retrieved from a provider and is applied in the global query filter like above.
The problem is when LegalEntityId is null
. Since global query filters in EF Core are compiled once during startup, they don't dynamically adjust to changes in the value of LegalEntityId at runtime. This happens because the filter is evaluated when the model is created and remains static for subsequent queries.
There are similar topics like EF Core global query filter based on nullable property resulting in NullReferenceException, however, I couldn't think of a solution.
It's essentially the same thing as a multi-tenant implementation, however, LegalEntityId can be null. Any idea how to tackle this?
public static class ModelBuilderExtensions
{
/// <summary>
/// Applies a global query filter to all entities of the specified type in the <see cref="ModelBuilder"/>.
/// </summary>
/// <typeparam name="TEntity">The type of the entity to which the filter will be applied. It must be a class type or interface type.</typeparam>
/// <param name="builder">The <see cref="ModelBuilder"/> instance to which the global filter will be applied.</param>
/// <param name="filter">An <see cref="Expression{Func{TEntity, bool}}"/> representing the filter to be applied to the entities.</param>
/// <returns>The <see cref="ModelBuilder"/> instance with the global query filter applied.</returns>
/// <remarks>
/// This method applies the specified filter to all entities that are assignable to the <typeparamref name="TEntity"/> type.
/// If an entity already has a query filter, the new filter is combined with the existing filter using an AND operation.
/// </remarks>
public static ModelBuilder SetQueryFilterOnAllEntities<TEntity>(
this ModelBuilder builder,
Expression<Func<TEntity, bool>> filter)
{
// Entities that are assignable to the TEntity generic
var entities = builder.Model.GetEntityTypes()
.Where(entity => typeof(TEntity).IsAssignableFrom(entity.ClrType))
.ToList();
foreach (var entity in entities)
{
// Creating a ParameterExpression with the actual type of the current entity
var entityParam = Expression.Parameter(entity.ClrType, "entity");
// Replacing the parameter with the actual entity parameter / (e.g => (entityParameter => ....))
var filterBody = ReplacingExpressionVisitor.Replace(filter.Parameters[0], entityParam, filter.Body);
// Getting the current query filters of the actual entity
var existingFilter = entity.GetQueryFilter();
if (existingFilter is not null)
{
// Other filter already present, combine them
filterBody = ReplacingExpressionVisitor.Replace(entityParam, existingFilter.Parameters[0], filterBody);
filterBody = Expression.AndAlso(existingFilter.Body, filterBody);
// Create a new lambda expression for the combined filter
existingFilter = Expression.Lambda(filterBody, existingFilter.Parameters);
}
else
{
// Create a new lambda expression for the current filter
existingFilter = Expression.Lambda(filterBody, entityParam);
}
// Setting the query filter to the entity
entity.SetQueryFilter(existingFilter);
}
return builder;
}
}