0

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 = "foo@bar.com",
                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.

Marco
  • 22,856
  • 9
  • 75
  • 124
  • is tenant part of the data to be seeded? ie does seeding have to happen for the first tenant to exist? – Joe Audette Aug 26 '16 at 19:27
  • That's not going to work. in the example, you instantiate the `ApplicationDbContext` manually using the `new` keyword. But in the next line you resolve `UserManager` which depends on the `ApplicationDbContext`. The user manage will receive a different instance of `ApplicationDbContext`, where the `user` is not created yet (because the `SaveChanges`method is called after `CreateAsync` – Tseng Aug 26 '16 at 19:49
  • You need to resolve your``ApplicationDbContext` from the IoC container too, but be careful. When you do this during startup with the app.ApplicationServices you create a singleton (there is no request during app boot). You first need to create a scope, then resolve from the scope and dispose the scope at the end. – Tseng Aug 26 '16 at 19:50
  • That is posing a Problem, because the the tenant is resolved per request, but the userstore will call find in any case. Maybe creating the user without the store could be a valid work around. – Marco Aug 26 '16 at 20:07
  • How is `AppTenant` being injected in the context in the first place? Do you register it via factory method to the IoC container? – Tseng Aug 26 '16 at 22:19

1 Answers1

2

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.

Sock
  • 5,323
  • 1
  • 21
  • 32
  • Hello Andrew. I've created a git repository for this. This should make it easier to sturdy the code, instead of just fragments here on stackoverflow. https://github.com/Servellia/Saas-Demo/tree/master/src/RecruitmentPortal.Web – Marco Aug 27 '16 at 13:50
  • I've taken a look at your code, and I think that fundamentally what you are trying to achieve won't be possible when not in the context of a request. You just won't be able to resolve the AppTenant through DI in your SeedData class as it doesn't really make sense - what if you want to insert users for 2 different AppTenants? I think the edit you provide is probably the best workaround. – Sock Aug 28 '16 at 00:30