Search code examples
c#entity-frameworklambda

Conditional Tenancy Global Filtering with a Run-time Evaluation in EF


I am trying to build a DbContext base class that automagically implements support for tenancy awareness, audit logs, etc. so that my team can inherit from it, add their entities, and with very little manual work, have an operable tenancy-enforced and managed database context. This is to reduce the level of effort and customization in a retrofit to something slightly less insane (we have like 30+ services, each with their own db contexts).

I have an interface ITenantAwareEntity that resembles this:

public interface ITenantAwareEntity
{
    /// <summary>
    /// Specifies the customer that this record belongs to.
    /// </summary>
    Guid? CustomerId { get; set; }
}

I built out a SaveChangesInterceptor to ensure that the CustomerId is always populated prior to saving using an injected IContextProvider that offers the current DbContext information like current user, current customer, that sort of thing.

On the query side, I'm trying to enforce tenancy with a global filter that adds a filter for CustomerId, using the aforementioned IContextProvider. There's a fair number of examples out there on how to do this, but they all seem different enough from my implementation and I'm breaking my brain on how to make this work. Lambda expressions are apparently my nemesis.

The context provider I inject into my DbContext (and expose as a protected get/ private set property) has a property on it called IsServiceContext which indicates that the identity using the DbContext does not belong to a specific tenant, but rather is a service account where I do not want to apply the global filter, as it may be managing records from several tenants at a time.

I am at a complete loss as to how I can write a lambda expression that basically says:

  • If the requesting user is a service account as defined in the IContextProvider, apply no filter.
  • Otherwise, filter any result sets asked for by the CustomerId property in the IContextProvider.

How do I write that in such a way that I can apply it to all entities that implement ITenantAwareEntity and have it dynamically evaluated at runtime for each DB operation?


Solution

  • EF Core allows in Query Filter only DbContext's properties usage. So define them:

    public class MyDbContext : DbConext
    {
        ...
    
        protected bool IsServiceUser => _contextProvider.IsServiceUser;
        protected Guid? CustomerId   => _contextProvider.CustomerId;
    
        ...
    }
    

    There is no way to dynamically change query filter - it is defined in model and cannot be changed later. Combine IsServiceUser and CustomerId in one filter:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        ... // configuring classes
    
        modelBuilder.Entity<Some>().HasQueryFilter(e => IsServiceUser || CustomerId == e.CustomerId);
    }    
    

    Also you can use the following custom extension ApplyQueryFilter to define query filters for all entities:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        ... // configuring classes
    
        modelBuilder.ApplyQueryFilter<ITenantAwareEntity>(e => IsServiceUser || CustomerId == e.CustomerId);
    }