Finbuckle / Finbuckle.MultiTenant

Finbuckle.MultiTenant is an open-source multitenancy middleware library for .NET. It enables tenant resolution, per-tenant app behavior, and per-tenant data isolation.
https://www.finbuckle.com/multitenant
Apache License 2.0
1.3k stars 265 forks source link

Cannot create a DbSet for 'MultiTenantIdentityUser' because this type is not included in the model for the context. #187

Closed unencode closed 4 years ago

unencode commented 4 years ago

I have an ASP.NET Core 3.0 application using Razor Pages for Identity and I'm getting stuck trying to register a new user receiving this exception


System.InvalidOperationException: Cannot create a DbSet for 'MultiTenantIdentityUser' because this type is not included in the model for the context.
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.get_EntityType()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.CheckState()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.get_EntityQueryable()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.System.Linq.IQueryable.get_Provider()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, LambdaExpression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync[TSource](IQueryable`1 source, Expression`1 predicate, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.FindByNameAsync(String normalizedUserName, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.UserManager`1.FindByNameAsync(String userName)
   at Microsoft.AspNetCore.Identity.UserValidator`1.ValidateUserName(UserManager`1 manager, TUser user, ICollection`1 errors)
   at Microsoft.AspNetCore.Identity.UserValidator`1.ValidateAsync(UserManager`1 manager, TUser user)
   at Microsoft.AspNetCore.Identity.UserManager`1.ValidateUserAsync(TUser user)
   at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user)
   at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user, String password)
   at Portal.ServerSide.Areas.Identity.Pages.Account.RegisterModel.OnPostAsync(String returnUrl)

Perhaps it's not wired up properly. At the moment it has two DBContexts and two databases.

ConfigureServices:


   public void ConfigureServices(IServiceCollection services)
        {

            services.AddSingleton<IConfiguration>(Configuration);

            //Tenants Store
            services.AddDbContext<TenantDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("TenantDbConnection")));

            //Per-Tenant Data Store (Identity is added below)
            services.AddDbContext<ApplicationDbContext>();

            services.AddIdentity<MultiTenantIdentityUser, MultiTenantIdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddMultiTenant()
               .WithHostStrategy("__tenant__.*") //https://www.finbuckle.com/MultiTenant/Docs/Strategies#static-strategy
                .WithEFCoreStore<TenantDbContext, AppTenantInfo>();

            //For Finbuckle .... To use SignInManager
            // services.AddAuthentication(IdentityConstants.ApplicationScheme);

            //2019-10-06 For Finbuckle
            //services.AddScoped<AuthenticationStateProvider, RevalidatingAuthenticationStateProvider<MultiTenantIdentityUser>>();

            services.AddRazorPages();
            services.AddServerSideBlazor();  ...

ApplicationDBContext:

public class ApplicationDbContext : MultiTenantIdentityDbContext<AppUser>
    {
        public IConfiguration Configuration { get; }
        public virtual DbSet<VeeamLookup> VeeamLookups { get; set; }
        public virtual DbSet<PaymentProvider> PaymentProviders { get; set; }

        public ApplicationDbContext(TenantInfo tenantInfo, DbContextOptions<ApplicationDbContext> options)
           : base(tenantInfo, options)
        {
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {

            optionsBuilder.UseSqlServer(ConnectionString);
            base.OnConfiguring(optionsBuilder);
        }
    }

TenantDbContext:

public class TenantDbContext : EFCoreStoreDbContext<AppTenantInfo>
    {
        public IConfiguration Configuration { get; }
        public TenantDbContext( DbContextOptions<TenantDbContext> options, IConfiguration configuration) : base( options)
        {
            Configuration = configuration;
        }
...

In Register.cshtml.cs excerpt:


 if (ModelState.IsValid)
            {
                var user = new MultiTenantIdentityUser { UserName = Input.Email, Email = Input.Email };

EXCEPTION HERE>>                var result = await _userManager.CreateAsync(user, Input.Password);

I've also reconfigured it where it looks like it will add the user at _userManager.CreateAsync but then crashes saying the user's Id is null.

AndrewTriesToCode commented 4 years ago

@unencode Hi I'm taking a look into this. The issue is with the identity context and not the EFCoreStore context - so that will help narrow it down. Can you post your AppUser class?

AndrewTriesToCode commented 4 years ago

I think I see the problem. In AddIdentity you pass MultiTenantUser, but later in the ApplicationDbContext you designate AppUser as the user entity class:

public class ApplicationDbContext : MultiTenantIdentityDbContext<AppUser>

If you just want the default Identity stuff you can just use MultiTenantIdentityUser everywhere and you don't need AppUser. If you add your own properties to the user model, you'd do so in AppUser (which inherits from MultitenantIdentityUser) so then make sure when you add identity you specify AppUser as the user entity.

Let me know how it goes.

unencode commented 4 years ago

Thanks for your help! My AppUser class is posted below, as you can see it's empty. I am going to work through your second answer in just a bit here and report back. Thanks!

[MultiTenant]
    public class AppUser : MultiTenantIdentityUser
    {

    }
unencode commented 4 years ago

Thanks again. As you suggested I removed AppUser as it's not really being used.

So the only line of code that changed is class declaration for ApplicationDbContext which now reads:

public class ApplicationDbContext : MultiTenantIdentityDbContext<MultiTenantIdentityUser>

That's where I see the other exception I mentioned which also happens at _userManager.CreateAsync in Register.cshtml.cs. Here's the exception:

System.InvalidOperationException: Unable to track an entity of type 'MultiTenantIdentityUser' because primary key property 'Id' is null.
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NullableKeyIdentityMap`1.Add(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey)
   at Microsoft.EntityFrameworkCore.DbContext.SetEntityState(InternalEntityEntry entry, EntityState entityState)
   at Microsoft.EntityFrameworkCore.DbContext.SetEntityState[TEntity](TEntity entity, EntityState entityState)
   at Microsoft.EntityFrameworkCore.DbContext.Add[TEntity](TEntity entity)
   at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.CreateAsync(TUser user, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user)
   at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user, String password)

I'm going to keep working on it. If you have any insight or ideas I would super appreciate it. At first I was thinking I had my stores mixed up or something.

Thanks!

unencode commented 4 years ago

In testing things out, if I add my own Id things work. From Register.cshtml.cs OnPostAsync:

var user = new MultiTenantIdentityUser { UserName = Input.Email, Email = Input.Email };
 ADDED THIS>>>     user.Id = Guid.NewGuid().ToString();
                   var result = await _userManager.CreateAsync(user, Input.Password);

After doing this Finbuckle adds the TenantId properly to Identity and custom entities and the host routing works.

AndrewTriesToCode commented 4 years ago

I'm glad you got it working. FYI MultiTenantIdentityDbContext derives from MultiTenantIdentityDbContext<MultiTenantIdentityUser> so your app db context can just derive from MultiTenantIdentityDbContext if you want to simplify--no need to though.

I thought that the user ID would automatically create a GUID for you -- does regular Identity do that? It has been a while I will want to check on that. Ideally you wouldn't have to set the user ID yourself.

unencode commented 4 years ago

Great, thank you, I will simplify it.

That's right, normally regular Identity's UserManager will create the user and it will be assigned a GUID.

AndrewTriesToCode commented 4 years ago

I found out why the Id null issue is happening--I will have a fix in the next release.

unencode commented 4 years ago

I found out why the Id null issue is happening--I will have a fix in the next release.

Great! I'm looking forward to the fix and am super curious about it. Thanks!

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.