Queryfilter on ApplicationUser in OnModelCreating in ApplicationDbContext creates StackOverflowException

asp.net-core asp.net-core-2.1 asp.net-identity c# entity-framework-core

Question

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?

1
0
8/17/2019 2:36:18 PM

Accepted Answer

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

1
8/18/2019 8:36:16 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow