We're working on building a multitenant application, and we're using Entity Framework Core as our base data provider. With EF Core, you can use Global Query Filters to define custom filters that apply to all queries generated by EF.
These filters work great and appear to serve most of our purposes, but we begin running into issues when we attempt to add our multitenancy filters to the application. To better separate our concerns, we've created various classes to serve as our tenant identifiers and handles. However, this begins to cause problems when we attempt to use our tenant identification/security classes as the whole project breaks down as these security classes have a dependency on the data context itself.
A simplified model of our application would look something like the following.
In our application project, you'd find:
public class TenantService : ITenantService
{
private IHttpContextAccessor _accessor;
private ITenantIdentifier _identifier;
private ITenantSecurityService _security;
public TenantService(IHttpContextAccessor accessor,
ITenantIdentifier identifier,
ITenantSecurityService security)
{
_accessor = accessor;
_identifier = identifier;
_security = security;
}
public Guid GetCurrentTenant() {
var tenant = _identifier.GetTenant(_accessor.HttpContext);
if (!_security.CanAccess(tenant))
throw new TenantAuthorizationException("User cannot access tenant", tenant);
return tenant.TenantId;
}
}
public class TenantSecurityService : ITenantSecurityService
{
private readonly IDbContext _dbContext;
private readonly ICurrentUserService _userService
public TenantSecurityService(IDbContext dbContext,
ICurrentUserService userService)
{
_dbContext = dbContext;
_userService = userService;
}
public bool CanAccess(Tenant tenant)
{
var currentUser = _userService.GetCurrentUser();
// do various checks with dbContext data...
return result;
}
}
And in our persistence project, you'd see something like:
public class ApplicationDbContext : DbContext, IDbContext
{
private readonly ITenantService _tenantService;
public ApplicationDbContext(ITenantService tenantService)
{
_tenantService = tenantService;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ConfigureTenantFilter(modelBuilder);
}
private void ConfigureTenantFilter(ModelBuilder modelBuilder)
{
var tenant = _tenantService?.GetCurrentTenant();
foreach (var entity in modelBuilder.Model.GetEntityTypes().Where(x =>
typeof(ITenantEntity).IsAssignableFrom(x.ClrType)))
{
entity.AddProperty(nameof(Tenant), typeof(Guid));
if (!string.IsNullOrEmpty(tenant))
modelBuilder
.Entity(entity.ClrType)
.HasQueryFilter(IsTenantRestriction(entity.ClrType, tenant));
}
}
}
As you may note, the above code actually fails to run due to the circular dependency, but I cannot seem to find any better ways to structure the application aside from having 2 separate dbContexts (which seems a bit hacky).
What would be a better approach to handling these query filters without needing to tightly couple my security to the dbContext?