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.26k stars 256 forks source link

Tenant resolved in first request resolved for second request and so son #840

Closed iAmBipinPaul closed 1 week ago

iAmBipinPaul commented 3 weeks ago

Hi @AndrewTriesToCode I'm new for this library and I've facing issue, where it for the first request it resolved the tenant correctly and after that all other request resolved to tenant which was resolved in first request. I'm not sure, what I'm doing wrong.

here are details of code follow

Tenant Db context

public class TenantDbContext(DbContextOptions<TenantDbContext> options)
    : EFCoreStoreDbContext<Tenant>(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Tenant>(entity =>
        {
            entity.HasQueryFilter(c=>c.IsDeleted==false);
            entity.Property(b => b.CreatedOn)
                .HasDefaultValueSql("getdate()");
            entity.ToTable(nameof(Tenant), b => b.IsTemporal());
            entity.HasData( new List<Tenant>()
            {
                new Tenant()
                {
                    Id = ApplicationInitialConstants.RootTenant.Id,
                    Identifier =ApplicationInitialConstants.RootTenant.Identifier,
                    IsActive = true,
                    Name = ApplicationInitialConstants.RootTenant.Name,
                    CreatedOn = DateTime.UtcNow,
                    ValidUpto = DateTime.MaxValue,
                    AdminEmail = ApplicationInitialConstants.RootUser.EmailAddress,
                    ChangeId = Guid.NewGuid().ToString()
                }
            });
        });
    }
}

Service registration

var connectionString
    = builder.Configuration["ConnectionStrings:DbContext"];

builder.Services
    .AddDbContext<TenantDbContext>(
        opts => { opts.UseSqlServer(connectionString); })
    .AddMultiTenant<Tenant>()
    .WithHostStrategy("{tenant}.api.*")
    .WithHeaderStrategy("x-tenant-identifier")
     .WithStaticStrategy("xyz")
    .WithEFCoreStore<TenantDbContext, Tenant>();

builder.Services.AddDbContext<ApplicationDbContext>(
    opts => { opts.UseSqlServer(connectionString); });

Middleware

app.UseCors(b => b.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
app.UseMultiTenant(); //addended to the middleware  
app.UseSwagger();

Service to get current Tenant

public class CurrentTenantService(IMultiTenantContextAccessor<Tenant> multiTenantContextAccessor)
    : ICurrentTenantService
{
    public TenantDto? GetCurrent()
    {
        if (multiTenantContextAccessor?.MultiTenantContext is null ||
            multiTenantContextAccessor.MultiTenantContext.TenantInfo is null)
        {
            return new TenantDto()
            {
                Id = ApplicationInitialConstants.RootTenant.Id,
                Identifier = ApplicationtInitialConstants.RootTenant.Identifier,
                IsActive = true,
                Name = ApplicationInitialConstants.RootTenant.Name,
                CreatedOn = DateTime.UtcNow,
                ValidUpto = DateTime.MaxValue,
                AdminEmail = ApplicationInitialConstants.RootUser.EmailAddress
            };
        }

        var res = multiTenantContextAccessor.MultiTenantContext.TenantInfo;
        return res?.Adapt<TenantDto>();
    }
}

if I call end point with

iAmBipinPaul commented 2 weeks ago

I have found the issue. I will detail of the issue here later.

iAmBipinPaul commented 1 week ago

I was getting all entity that implements ITenant then looping through each of them and I was calling static method where I was passing currentTenantService.

var tenantEntities = allEntities
                .Where(e => typeof(ITenant).IsAssignableFrom(e.ClrType));
            foreach (var entityType in tenantEntities)
            {
                builder.Entity(entityType.ClrType).Property("TenantId").IsRequired();
                entityType.AddTenantQueryFilter(currentTenantService);
            }

static extension metho for filter

public static class TenantFilterQueryExtension
    {

        public static void AddTenantQueryFilter(
            this IMutableEntityType entityData, ICurrentTenantService currentTenantService)
        {
            var methodToCall = typeof(SoftDeleteQueryExtension)
                .GetMethod(nameof(GetTenantFilter),
                    BindingFlags.NonPublic | BindingFlags.Static)!
                .MakeGenericMethod(entityData.ClrType);

            var filter = methodToCall.Invoke(null, new object[] { currentTenantService });

            entityData.SetQueryFilter((LambdaExpression)filter!);

            entityData.AddIndex(entityData.FindProperty(nameof(ITenant.TenantId))!);
        }

        private static LambdaExpression GetTenantFilter<TEntity>(CurrentTenantService currentTenantService)
            where TEntity : class, ITenant
        {
            Expression<Func<TEntity, bool>> filter = x => x.TenantId == currentTenantService.GetCurrent().Id;
            return filter;
        }
}

when requesting was coming for that first time it was setting query filter with that tenant and it was stating their till, we restart the application

AndrewTriesToCode commented 1 week ago

Sorry I was late to reply. I see you ran into the model cache.

The tenant query filter is implemented in MultiTenantDbContext and its related extension methods in such a way to avoid this. Did these not work for your use case?

in other words you reimplemented the logic Finbuckle provides in its EFCore support but that’s ok if it works for you!

iAmBipinPaul commented 3 days ago

I was getting all entity that implements ITenant then looping through each of them and I was calling static method where I was passing currentTenantService.

var tenantEntities = allEntities
                .Where(e => typeof(ITenant).IsAssignableFrom(e.ClrType));
            foreach (var entityType in tenantEntities)
            {
                builder.Entity(entityType.ClrType).Property("TenantId").IsRequired();
                entityType.AddTenantQueryFilter(currentTenantService);
            }

static extension metho for filter

public static class TenantFilterQueryExtension
    {

        public static void AddTenantQueryFilter(
            this IMutableEntityType entityData, ICurrentTenantService currentTenantService)
        {
            var methodToCall = typeof(SoftDeleteQueryExtension)
                .GetMethod(nameof(GetTenantFilter),
                    BindingFlags.NonPublic | BindingFlags.Static)!
                .MakeGenericMethod(entityData.ClrType);

            var filter = methodToCall.Invoke(null, new object[] { currentTenantService });

            entityData.SetQueryFilter((LambdaExpression)filter!);

            entityData.AddIndex(entityData.FindProperty(nameof(ITenant.TenantId))!);
        }

        private static LambdaExpression GetTenantFilter<TEntity>(CurrentTenantService currentTenantService)
            where TEntity : class, ITenant
        {
            Expression<Func<TEntity, bool>> filter = x => x.TenantId == currentTenantService.GetCurrent().Id;
            return filter;
        }
}

when requesting was coming for that first time it was setting query filter with that tenant and it was stating their till, we restart the application

Hi @AndrewTriesToCode np ta all The other main db context I have does not inherit form MultiTenantDbContext instead it just inherits form IdentityDbContext.

I was not able to write code which will loop through each of the Entity that implements ITenant and add Global query filter to filter result by current tenant so I have ended up writing one statement for each of the entity in main db context OnModelCreating.

like this

  builder.Entity<Product>(entity =>
            {
                entity.HasQueryFilter(c => c.IsDeleted == false && c.TenantId == tenantGetterService.Tenant.Id);
            });
            builder.Entity<EntityVersion>(entity =>
            {
                entity
                    .HasKey(p => new { p.EntityName, p.Timestamp });
                entity.HasQueryFilter(c => c.TenantId == tenantGetterService.Tenant.Id);
            });

Can you please point me to that part of code where I can take a look.

The tenant query filter is implemented in MultiTenantDbContext and its related extension methods in such a way to avoid this. Did these not work for your use case?

in other words you reimplemented the logic Finbuckle provides in its EFCore support but that’s ok if it works for you!