Search code examples
c#asp.net-coreentity-framework-coreasp.net-identityasp.net-core-2.1

Queryfilter on ApplicationUser in OnModelCreating in ApplicationDbContext creates StackOverflowException


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?


Solution

  • 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