Search code examples
c#asp.netasp.net-coreasp.net-identitysaaskit

Why is AppTenant null only while seeding data?


I'm currently fiddeling around with Ben Fosters Saaskit.

I have extended the ApplicationUser with a AppTenantId property and created a custom UserStore, which uses the AppTenant to identify the user:

public class TenantEnabledUserStore : IUserStore<ApplicationUser>, IUserLoginStore<ApplicationUser>,
    IUserPasswordStore<ApplicationUser>, IUserSecurityStampStore<ApplicationUser>
{
    private bool _disposed;
    private AppTenant _tenant;
    private readonly ApplicationDbContext _context;

    public TenantEnabledUserStore(ApplicationDbContext context, AppTenant tenant)
    {
        _context = context;
        _tenant = tenant;
    }
    /*... implementation omitted for brevity*/
}

If a user registers or logs in, this works fine. The AppTenant is set correctly. The problem occurs, when SeedData.Initialize(app.ApplicationServices); is called at the end of my Statup.Configure() method:

public static class SeedData
{
    public async static void Initialize(IServiceProvider provider)
    {
        using (var context = new ApplicationDbContext(
            provider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
        {
            var admin = new ApplicationUser
            {
                AppTenantId = 1,
                Email = "[email protected]",
                UserName = "Administrator",
                EmailConfirmed = true
            };

            if(!context.Users.Any(u => u.Email == admin.Email))
            {
                var userManager = provider.GetRequiredService<UserManager<ApplicationUser>>();
                await userManager.CreateAsync(admin, "Penis123#");
            }
            context.SaveChanges();
        }
    }
}

The usermanager is is calling the custom userstore, but now AppTenant is null. When the code finally reaches

public Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
    return _context.Users.FirstOrDefaultAsync(u => u.NormalizedUserName == normalizedUserName && u.AppTenantId == _tenant.AppTenantId, cancellationToken);
}

I am facing a System.InvalidoperationException, because AppTenant is passed as null in the constructor of above mentioned userstore.

What am I doing wrong? Am I seeding the wrong way or do I forget something fundamental here?

Update: For now I have taken the crowbar-approach, avoided the usermanager and created my own instance of a userstore with a mock AppTenant:

if (!context.Users.Any(u => u.Email == admin.Email))
{
    var userStore = new TenantEnabledUserStore(context, new AppTenant
    {
        AppTenantId = 1
    });
    await userStore.SetPasswordHashAsync(admin, new PasswordHasher<ApplicationUser>().HashPassword(admin, "VeryStrongPassword123#"), default(CancellationToken));
    await userStore.SetSecurityStampAsync(admin, Guid.NewGuid().ToString("D"), default(CancellationToken));
    await userStore.CreateAsync(admin, default(CancellationToken));
}

Nontheless, I'm still interested in a more clean approach, that doesn't feel that hacky.


Solution

  • When using Saaskit, you configure an AppTenantResolver that determines how to set the TenantContext<T> based on the provided HttpContext. It then stores the retrieved TenantContext<T> in the Items property of the HttpContext. This is a Scope level cache, so the tenant is only stored there for the duration of the request.

    When you inject an AppTenant into a class it attempts to resolve it from HttpContext.Items. If no tenant is found, then it injects null instead.

    When you call SeedData.Initialize(app.ApplicationServices), you are not in the context of a request, and so the AppTenantResolver middleware has not run, and will not have resolved an AppTenant.

    Unfortunately, not having the full details of your code, it's hard to say exactly how to fix your issue. You would need to make sure you create a new Scope in your SeedData method and resolve an AppTenant within that scope so that subsequent calls to the IoC will allow it to be inserted.