I need to create a Global Query Filter that filters only those Users who belong belong to a certain Tenant.
However, I get a stackoverflow when adding the queryfilter to OnModelCreating.
I get the TenantId from the current logged in user, using IHttpContextAccessor. This works quite fine with other Entities, but ApplicationUser creates the error. Is this perhaps a problem of circular code?
My ApplicationDbContext is as follows (abbreviated for clarity purposes)
public class ApplicationDbContext
: IdentityDbContext<ApplicationUser, ApplicationRole, string, IdentityUserClaim<string>,
ApplicationUserRole, IdentityUserLogin<string>,
IdentityRoleClaim<string>, IdentityUserToken<string>>
{
private readonly IHttpContextAccessor _contextAccessor;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IHttpContextAccessor contextAccessor)
: base(options)
{
_contextAccessor = contextAccessor;
}
public virtual Guid? CurrentTenantId
{
get
{
return Users.FirstOrDefault(u => u.UserName == _contextAccessor.HttpContext.User.Identity.Name)?.TenantId;
}
}
public virtual string CurrentUserName
{
get
{
return Users.FirstOrDefault(u => u.UserName == _contextAccessor.HttpContext.User.Identity.Name)?.UserName;
}
}
public DbSet<ApplicationUser> ApplicationUser { get; set; }
public DbSet<Tenant> Tenant { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Tenant>().HasQueryFilter(e => e.TenantId == CurrentTenantId);
builder.Entity<ApplicationUser>().HasQueryFilter(e => e.TenantId == CurrentTenantId);
}
}
}
I have added services.AddHttpContextAccessor()
to the ConfifureServices section in my startup.
Any suggestions on how to resolve this?
There are many ways to solve this issue but I'll just give you my prefered way.
First, you have to create a second DbContext class that has access to your Users database set, and that is not going to have a Query Filter applied to it.
public class UserDbContext : DbContext
{
public DbSet<ApplicationUser> Users { get; set; }
}
Which you register in startup in the same way as you current DbContext class and that uses the same connection string.
Then you can go on to create a service that will supply you with your TenantID.
public class TenantProvider : ITenantProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDbContext _userDbContext;
private Guid _tenantId;
public TenantProvider(UserDbContext userDbContext,
IHttpContextAccessor httpContextAccessor)
{
_userDbContext = userDbContext;
_httpContextAccessor = httpContextAccessor;
}
private void SetTenantId()
{
if (_httpContextAccessor.HttpContext == null)
{
// Whatever you would like to return if there is no request (eg. on startup of app).
_tenantId = new Guid();
return;
}
_tenantId = _userDbContext.Users.FirstOrDefault(u => u.UserName == _httpContextAccessor.HttpContext.User.Identity.Name)?.TenantId;
return;
}
public Guid GetTenantId()
{
SetTenantId();
return _tenantId;
}
And of course an interface
public interface ITenantProvider
{
Guid GetTenantId();
}
You register this service in startup as well.
services.AddScoped<ITenantProvider, TenantProvider>();
Then you modify your ApplicationDbContext:
public class ApplicationDbContext
: IdentityDbContext<ApplicationUser, ApplicationRole, string, IdentityUserClaim<string>,
ApplicationUserRole, IdentityUserLogin<string>,
IdentityRoleClaim<string>, IdentityUserToken<string>>
{
private readonly IHttpContextAccessor _contextAccessor;
private Guid _tenantId;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IHttpContextAccessor contextAccessor, ITenantProvider _tenantProvider)
: base(options)
{
_contextAccessor = contextAccessor;
_tenantId = _tenantProvider.GetTenantId();
}
public DbSet<ApplicationUser> ApplicationUser { get; set; }
public DbSet<Tenant> Tenant { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Tenant>().HasQueryFilter(e => e.TenantId == _tenantId);
builder.Entity<ApplicationUser>().HasQueryFilter(e => e.TenantId == _tenantId);
}
}
And that should be it, no more infinite loops :) Good luck