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.32k stars 266 forks source link

System.InvalidOperationException: 'Unable to track an entity of type 'IdentityUserLogin<string>' because its primary key property 'TenantId' is null.' #881

Open MayurPatel-BlobStation opened 1 week ago

MayurPatel-BlobStation commented 1 week ago

Discussed in https://github.com/Finbuckle/Finbuckle.MultiTenant/discussions/879

Originally posted by **MayurPatel-BlobStation** October 4, 2024 "When I attempt to insert data into the AspNetUserLogin table, I encounter the following error: System.InvalidOperationException: 'Unable to track an entity of type 'IdentityUserLogin' because its primary key property 'TenantId' is null.'. However, inserting data into the AspNetUser table works correctly." var userResult = await _userManager.FindByEmailAsync("abc@gmail.com"); var result = await _userManager.AddLoginAsync(userResult, externalLoginInfo);
AndrewTriesToCode commented 1 week ago

Thanks I am looking into it. Does this happen during the third party sign in login flow or are you inserting into the table somewhere else in your code?

MayurPatel-BlobStation commented 1 week ago

Thanks I am looking into it. Does this happen during the third party sign in login flow or are you inserting into the table somewhere else in your code?

Yes, In application I am implementing google and Microsoft SSO login, From UI side I have received Google token, so first I have check token is verified or not, if token is verified then check Login exist or not with

var result = await _userManager.FindByLoginAsync(loginProvider,providerKey);

If no login exist then inserting data in AspNetUser table then AspNetUserLogin table. but in AspNetUser data inserted successfully but AspNetUserLogin cause issue.

AndrewTriesToCode commented 1 week ago

Are you using Asp.net Identity? Also can you please post what your Finbuckle setup looks like in your program.cs file?

MayurPatel-BlobStation commented 1 week ago

Yes I have use Asp.net identity.

program.cs file code :

----------Program.cs----------

builder.Services.AddAppServices(configuration);

var app = builder.Build();
app.UseMultiTenant();

----------ServiceCollectionExtensions.cs----------

  public static class ServiceCollectionExtensions
  {

        public static void AddAppServices(this IServiceCollection services, ApiConfiguration configuration)
        {
              services.AddSubscriptionManagmentDbContext(configuration!.DbSettings);
              services.AddMultiTenantStoreDbContext(configuration!.DbSettings);
              services.AddMultiTenant<TenantInfo>()
              .WithRouteStrategy("TenantId")
              .WithHeaderStrategy("TenantIdKey")
              .WithPerTenantAuthentication()
              .WithEFCoreStore<MultiTenantStoreDbContext, TenantInfo>();
        }

        public static void AddSubscriptionManagmentDbContext(this IServiceCollection services, DbSettings dbSettings)
        {
              NpgsqlConnection.GlobalTypeMapper.EnableDynamicJson();
              services.AddDbContext<SubscriptionManagmentDbContext>(opts =>
              {
              opts.UseNpgsql(Convert.ToString(dbSettings.ConnectionString));
              opts.ConfigureWarnings(w => w.Ignore(RelationalEventId.MultipleCollectionIncludeWarning));
              });
        }

        public static void AddMultiTenantStoreDbContext(this IServiceCollection services, DbSettings dbSettings)
        {
              services.AddDbContext<MultiTenantStoreDbContext>(opts =>
              {
                    opts.UseNpgsql(Convert.ToString(dbSettings.ConnectionString));
                    opts.ConfigureWarnings(w => w.Ignore(RelationalEventId.MultipleCollectionIncludeWarning));
              });
        }
  }

----------MultiTenantStoreDbContext.cs----------

public class MultiTenantStoreDbContext : EFCoreStoreDbContext<TenantInfo> { public MultiTenantStoreDbContext(DbContextOptions options) : base(options){} }

----------SubscriptionManagmentDbContext.cs----------

  public class SubscriptionManagmentDbContext : MultiTenantIdentityDbContext
  {
      private readonly IMultiTenantContextAccessor _multiTenantContextAccessor;
      protected readonly IHttpContextAccessor _httpContextAccessor;
      public SubscriptionManagmentDbContext(DbContextOptions<SubscriptionManagmentDbContext> options, 
                         IMultiTenantContextAccessor multiTenantContextAccessor
                  , IHttpContextAccessor httpContextAccessor) : base(multiTenantContextAccessor, options)
      {
          _multiTenantContextAccessor = multiTenantContextAccessor;
          _httpContextAccessor = httpContextAccessor;
      }

      public SubscriptionManagmentDbContext(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor)
      {}

      public SubscriptionManagmentDbContext(ITenantInfo tenantInfo, DbContextOptions<SubscriptionManagmentDbContext> options) :
          base(tenantInfo, options){}

      protected override void OnModelCreating(ModelBuilder modelBuilder)
      {
          base.OnModelCreating(modelBuilder);
      }
  }
AndrewTriesToCode commented 1 week ago

Thanks. I don’t see any issues with this code. Where exactly in your app is AddLoginAsync getting called?

MayurPatel-BlobStation commented 1 week ago

I have created a repository called IdentityRepository, which I am currently using.

Repository Code:

   public class IdentityRepository : IIdentityRepository
   {
       private readonly UserManager<IdentityUser> _userManager;
       private readonly SignInManager<IdentityUser> _signInManager;
       public IdentityRepository(
           UserManager<IdentityUser> userManager,
           SignInManager<IdentityUser> signInManager,
           )
      {
           _userManager = userManager;
           _signInManager = signInManager;
      }

      public async Domains.IdentityUser AddLoginAsync(Domains.IdentityUser users, UserLoginInfo externalLoginInfo)
      {
          try
          {
            var insertUserModel = _mapper.Map<IdentityUser>(users);
            var result = await _userManager.AddLoginAsync(insertUserModel, externalLoginInfo);
            return _mapper.Map<Domains.IdentityUser>(insertUserModel);
          }
          catch(){}
      }
  }
AndrewTriesToCode commented 1 week ago

Is your identity repository registered in DI? What is the lifetime? The user manager internally will have a user store which internally uses your dbcontext which was instantiated for the current tenant. If you also inject IMultitenantAccessor into this class does it resolve the correct tenant right before the add user login call?

MayurPatel-BlobStation commented 3 days ago

I have registered the identity repository in dependency injection with a scoped lifetime. However, I cannot find IMultitenantAccessor, which I believe refers to IMultiTenantContextAccessor. Even after using this, it is still not working.