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

Mark entities as MultiTenant without using an Attribute #184

Closed GordonBlahut closed 4 years ago

GordonBlahut commented 4 years ago

If entity classes are in a class library (e.g. MyApp.Core) and the EF configuration in another class library (e.g. MyApp.Data), the MyApp.Core class library has to take on Finbuckle.MultiTenant.EntityFrameworkCore (and therefore EntityFrameworkCore itself) in order to use [MultiTenant] on classes.

A solution to mark entities as MultiTenant without an attribute would be great. It would make a lot of sense (to me anyway) to make this part of model building API. Maybe an extension method on EntityTypeBuilder and EntityTypeBuilder<T> so you could do modelBuilder.Entity<MyEntity>().HasMultiTenant().

That way the MyApp.Core library doesn't need to depend on Finbuckle at all.

For example, the HasMultiTenant extension method could add an annotation or something to the entity type:

public static EntityTypeBuilder<TEntity> HasMultiTenant<TEntity>(this EntityTypeBuilder<TEntity> builder) where TEntity : class
{
    HasMultiTenant((EntityTypeBuilder)builder)

    return builder;
}  

public static EntityTypeBuilder HasMultiTenant(this EntityTypeBuilder builder)
{
    builder.HasAnnotation("Finbuckle:MultiTenant", true);

    return builder;
}

Shared could add Shared.IsMultiTenant and look for the annotation instead of the attribute:

public static bool IsMultiTenant(IEntityType t)
{
    return (bool?)t.FindAnnotation("Finbuckle:MultiTenant")?.Value ?? false
}

(or since it's a static method anyway, it could be an extension method of IEntityType).

MultiTenantDbContext.MultiTenantEntityTypes and MultiTenantIdentityDbContext.MultiTenantEntityTypes would call Shared.IsMultiTenant instead of Shared.HasMultiTenantAttribute to build the list of MultiTenant entity types.

public IImmutableList<IEntityType> MultiTenantEntityTypes
{
    get
    {
        if (multiTenantEntityTypes == null)
        {
            multiTenantEntityTypes = Model.GetEntityTypes().
                Where(t => Shared.IsMultiTenant(t)).
                ToImmutableList();
        }

        return multiTenantEntityTypes;
    }
}

MultiTenantIdentityDbContext.OnModelCreating would also have to call Shared.IsMultiTenant when modifying indexes on identity tables.

MultiTenantAttribute could remain as well for backwards compatibility and for those who prefer attribtues to annotate models. Shared.SetupModel would simply get a list of entities with the attribute and call HasMultiTenant on them to add the annotation and then loop through entity types with the annotation instead:

public static void SetupModel(ModelBuilder modelBuilder, Expression<Func<TenantInfo>> currentTenantInfoExpression)
{
    foreach (var t in modelBuilder.Model.GetEntityTypes().
        Where(t => HasMultiTenantAttribute(t.ClrType)))
    {
        modelBuilder.Entity(t.ClrType).HasMultiTenant();
    }

    foreach (var t in moduleBuilder.Model.GetEntityTypes().
        Where(t => IsMultiTenant(t))
    {
        var r = modelBuilder.Entity(t.ClrType);

        // ... original logic for setting up model
    }
}

That way both the fluent model builder API as well as the attribute could be used to mark entities as MultiTenant.

Caveat: MultiTenantDbContext.MultiTenantEntityTypes and MultiTenantIdentityDbContext.MultiTenantEntityTypes would not be able to return a list of results until after OnModelCreating is called but I'm not sure how much that would end up being an issue.

Let me know if you're interested in this concept. Maybe I could put together a PR but I don't currently have .NET Core 3.0.

GordonBlahut commented 4 years ago

Oops, Shared.EnforceTenantId would also need to be updated to identify MultiTenant entities via the annotation:

public static void EnforceTenantId(TenantInfo tenantInfo,
    ChangeTracker changeTracker,
    TenantNotSetMode tenantNotSetMode,
    TenantMismatchMode tenantMismatchMode)
{
    var changedMultiTenantEntities = changeTracker.Entries().
        Where(e => e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted).
        Where(e => Shared.IsMultiTenant(e.Metadata));

    // ... rest of method
AndrewTriesToCode commented 4 years ago

Hi @GordonBlahut

I had the same thought about using the model builder as I started looking into IdentityServer support. I think this adds a lot of value as well.

If you start a PR that would be great, otherwise maybe I can get you to review mine? Not sure when I will get to it--a few other items on the plate as well.

GordonBlahut commented 4 years ago

Ok, I'll look into putting together a PR. I likely won't be able to get to it until the weekend or maybe even next week.

AndrewTriesToCode commented 4 years ago

hi @GordonBlahut, I just submitted PR #191

Thoughts?

GordonBlahut commented 4 years ago

No joke I was just about to start my implementation. I will take a look at the PR.