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:
IContextProvider
, apply no filter.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?
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);
}