From 00dc5c40d675c136c8537d7bb6f463f4ff4bc9d6 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 8 Nov 2024 17:44:14 -0700 Subject: [PATCH 1/6] feat: added MultiTenantDbContext.Create() and tests BREAKING CHANGE: MultiTenantDbContext constructors accepting ITenantInfo removed, use Create factory instead --- .../MultiTenantDbContext.cs | 39 +++-- .../StaticMultiTenantContextAccessor.cs | 13 ++ .../EntityTypeBuilderExtensionsShould.cs | 151 +++++++++--------- .../TestDbContext.cs | 40 ++--- .../TestDbContext.cs | 3 +- .../TestDbContext.cs | 32 ++-- .../MultiTenantDbContextShould.cs | 50 +++++- .../MultiTenantDbContext/TestEntitities.cs | 8 - .../TestDbContext.cs | 24 +-- 9 files changed, 215 insertions(+), 145 deletions(-) create mode 100644 src/Finbuckle.MultiTenant/Internal/StaticMultiTenantContextAccessor.cs diff --git a/src/Finbuckle.MultiTenant.EntityFrameworkCore/MultiTenantDbContext.cs b/src/Finbuckle.MultiTenant.EntityFrameworkCore/MultiTenantDbContext.cs index 2e5e6f30..aed3304a 100644 --- a/src/Finbuckle.MultiTenant.EntityFrameworkCore/MultiTenantDbContext.cs +++ b/src/Finbuckle.MultiTenant.EntityFrameworkCore/MultiTenantDbContext.cs @@ -1,9 +1,8 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System.Threading; -using System.Threading.Tasks; using Finbuckle.MultiTenant.Abstractions; +using Finbuckle.MultiTenant.Internal; using Microsoft.EntityFrameworkCore; namespace Finbuckle.MultiTenant.EntityFrameworkCore; @@ -22,9 +21,33 @@ public abstract class MultiTenantDbContext : DbContext, IMultiTenantDbContext /// public TenantNotSetMode TenantNotSetMode { get; set; } = TenantNotSetMode.Throw; - protected MultiTenantDbContext(ITenantInfo? tenantInfo) + /// + /// Creates a new instance of a multitenant context that accepts a IMultiTenantContextAccessor instance and an optional DbContextOptions instance. + /// + /// The tenant information to bind to the context. + /// The database options instance. + /// The TContext implementation type. + /// The ITenantInfo implementation type. + /// + public static TContext Create(TTenantInfo? tenantInfo, DbContextOptions? options = null) + where TContext : DbContext + where TTenantInfo : class, ITenantInfo, new() { - TenantInfo = tenantInfo; + try + { + var mca = new StaticMultiTenantContextAccessor(tenantInfo); + var context = options switch + { + null => (TContext)Activator.CreateInstance(typeof(TContext), mca)!, + not null => (TContext)Activator.CreateInstance(typeof(TContext), mca, options)! + }; + + return context; + } + catch (MissingMethodException) + { + throw new ArgumentException("The provided DbContext type does not have a constructor that accepts the required parameters."); + } } /// @@ -36,17 +59,13 @@ protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAcc TenantInfo = multiTenantContextAccessor.MultiTenantContext.TenantInfo; } - protected MultiTenantDbContext(ITenantInfo? tenantInfo, DbContextOptions options) : base(options) - { - TenantInfo = tenantInfo; - } - /// /// Constructs the database context instance and binds to the current tenant. /// /// The MultiTenantContextAccessor instance used to bind the context instance to a tenant. /// The database options instance. - protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(options) + protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : + base(options) { TenantInfo = multiTenantContextAccessor.MultiTenantContext.TenantInfo; } diff --git a/src/Finbuckle.MultiTenant/Internal/StaticMultiTenantContextAccessor.cs b/src/Finbuckle.MultiTenant/Internal/StaticMultiTenantContextAccessor.cs new file mode 100644 index 00000000..c8ef19c0 --- /dev/null +++ b/src/Finbuckle.MultiTenant/Internal/StaticMultiTenantContextAccessor.cs @@ -0,0 +1,13 @@ +using Finbuckle.MultiTenant.Abstractions; + +namespace Finbuckle.MultiTenant.Internal; + +internal class StaticMultiTenantContextAccessor(TTenantInfo? tenantInfo) + : IMultiTenantContextAccessor + where TTenantInfo : class, ITenantInfo, new() +{ + IMultiTenantContext IMultiTenantContextAccessor.MultiTenantContext => MultiTenantContext; + + public IMultiTenantContext MultiTenantContext { get; } = + new MultiTenantContext { TenantInfo = tenantInfo }; +} \ No newline at end of file diff --git a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/EntityTypeBuilderExtensionsShould.cs b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/EntityTypeBuilderExtensionsShould.cs index afa9caaf..9c66e4ce 100644 --- a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/EntityTypeBuilderExtensionsShould.cs +++ b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/EntityTypeBuilderExtensionsShould.cs @@ -1,9 +1,6 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. -using System; -using System.Linq; -using Finbuckle.MultiTenant.Abstractions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -17,115 +14,115 @@ public class EntityTypeBuilderExtensionsShould : IDisposable public EntityTypeBuilderExtensionsShould() { - _connection = new SqliteConnection("DataSource=:memory:"); - _connection.Open(); - } + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + } public void Dispose() { - _connection?.Dispose(); - } + _connection?.Dispose(); + } - private TestDbContext GetDbContext(Action? config = null, ITenantInfo? tenant = null) + private TestDbContext GetDbContext(Action? config = null, TenantInfo? tenant = null) { - var options = new DbContextOptionsBuilder() - .ReplaceService() // needed for testing only - .UseSqlite(_connection) - .Options; - return new TestDbContext(config, tenant ?? new TenantInfo(), options); - } + var options = new DbContextOptionsBuilder() + .ReplaceService() // needed for testing only + .UseSqlite(_connection) + .Options; + return new TestDbContext(config, tenant ?? new TenantInfo(), options); + } [Fact] public void SetMultiTenantAnnotation() { - using var db = GetDbContext(); - var annotation = db.Model.FindEntityType(typeof(MyMultiTenantThing))? - .FindAnnotation(Constants.MultiTenantAnnotationName); + using var db = GetDbContext(); + var annotation = db.Model.FindEntityType(typeof(MyMultiTenantThing))? + .FindAnnotation(Constants.MultiTenantAnnotationName); - Assert.True((bool)annotation!.Value!); - } + Assert.True((bool)annotation!.Value!); + } [Fact] public void AddTenantIdStringShadowProperty() { - using var db = GetDbContext(); - var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId"); + using var db = GetDbContext(); + var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId"); - Assert.Equal(typeof(string), prop?.ClrType); - Assert.True(prop?.IsShadowProperty()); - Assert.Null(prop?.FieldInfo); - } + Assert.Equal(typeof(string), prop?.ClrType); + Assert.True(prop?.IsShadowProperty()); + Assert.Null(prop?.FieldInfo); + } [Fact] public void RespectExistingTenantIdStringProperty() { - using var db = GetDbContext(); - var prop = db.Model.FindEntityType(typeof(MyThingWithTenantId))?.FindProperty("TenantId"); + using var db = GetDbContext(); + var prop = db.Model.FindEntityType(typeof(MyThingWithTenantId))?.FindProperty("TenantId"); - Assert.Equal(typeof(string), prop!.ClrType); - Assert.False(prop.IsShadowProperty()); - Assert.NotNull(prop.FieldInfo); - } + Assert.Equal(typeof(string), prop!.ClrType); + Assert.False(prop.IsShadowProperty()); + Assert.NotNull(prop.FieldInfo); + } [Fact] public void ThrowOnNonStringExistingTenantIdProperty() { - using var db = GetDbContext(b => b.Entity().IsMultiTenant()); - Assert.Throws(() => db.Model); - } + using var db = GetDbContext(b => b.Entity().IsMultiTenant()); + Assert.Throws(() => db.Model); + } [Fact] public void SetsTenantIdStringMaxLength() { - using var db = GetDbContext(); - var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId"); + using var db = GetDbContext(); + var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId"); - Assert.Equal(Internal.Constants.TenantIdMaxLength, prop!.GetMaxLength()); - } + Assert.Equal(Internal.Constants.TenantIdMaxLength, prop!.GetMaxLength()); + } [Fact] public void SetGlobalFilterQuery() { - // Doesn't appear to be a way to test this except to try it out... - var tenant1 = new TenantInfo - { - Id = "abc" - }; - - var tenant2 = new TenantInfo - { - Id = "123" - }; - - using var db = GetDbContext(null, tenant1); - db.Database.EnsureCreated(); - db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 }); - db.SaveChanges(); - - Assert.Equal(1, db.MyMultiTenantThings!.Count()); - db.TenantInfo = tenant2; - Assert.Equal(0, db.MyMultiTenantThings!.Count()); - } + // Doesn't appear to be a way to test this except to try it out... + var tenant1 = new TenantInfo + { + Id = "abc" + }; + + var tenant2 = new TenantInfo + { + Id = "123" + }; + + using var db = GetDbContext(null, tenant1); + db.Database.EnsureCreated(); + db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 }); + db.SaveChanges(); + + Assert.Equal(1, db.MyMultiTenantThings!.Count()); + db.TenantInfo = tenant2; + Assert.Equal(0, db.MyMultiTenantThings!.Count()); + } [Fact] public void RespectExistingQueryFilter() { - // Doesn't appear to be a way to test this except to try it out... - var tenant1 = new TenantInfo - { - Id = "abc" - }; - - using var db = GetDbContext(config => - { - config.Entity().HasQueryFilter(e => e.Id == 1); - config.Entity().IsMultiTenant(); - }, tenant1); - db.Database.EnsureCreated(); - db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 }); - db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 2 }); - db.SaveChanges(); - - Assert.Equal(1, db.MyMultiTenantThings!.Count()); - } + // Doesn't appear to be a way to test this except to try it out... + var tenant1 = new TenantInfo + { + Id = "abc" + }; + + using var db = GetDbContext(config => + { + config.Entity().HasQueryFilter(e => e.Id == 1); + config.Entity().IsMultiTenant(); + }, tenant1); + db.Database.EnsureCreated(); + db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 }); + db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 2 }); + db.SaveChanges(); + + Assert.Equal(1, db.MyMultiTenantThings!.Count()); + } } \ No newline at end of file diff --git a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/TestDbContext.cs b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/TestDbContext.cs index f4b64e31..e4ddb646 100644 --- a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/TestDbContext.cs +++ b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/TestDbContext.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Finbuckle.MultiTenant.Abstractions; +using Finbuckle.MultiTenant.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -14,10 +15,11 @@ public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext { private readonly Action? _config; - public TestDbContext(Action? config, ITenantInfo tenantInfo, DbContextOptions options) : base(tenantInfo, options) + public TestDbContext(Action? config, TenantInfo tenantInfo, DbContextOptions options) : + base(new StaticMultiTenantContextAccessor(tenantInfo), options) { - this._config = config; - } + this._config = config; + } public DbSet? MyMultiTenantThings { get; set; } public DbSet? MyThingsWithTenantIds { get; set; } @@ -26,18 +28,18 @@ public TestDbContext(Action? config, ITenantInfo tenantInfo, DbCon protected override void OnModelCreating(ModelBuilder modelBuilder) { - // If the test passed in a custom builder use it - if (_config != null) - _config(modelBuilder); - // Of use the standard builder configuration - else - { - modelBuilder.Entity().IsMultiTenant(); - modelBuilder.Entity().IsMultiTenant(); - } - - base.OnModelCreating(modelBuilder); + // If the test passed in a custom builder use it + if (_config != null) + _config(modelBuilder); + // Of use the standard builder configuration + else + { + modelBuilder.Entity().IsMultiTenant(); + modelBuilder.Entity().IsMultiTenant(); } + + base.OnModelCreating(modelBuilder); + } } // ReSharper disable once ClassNeverInstantiated.Global @@ -45,13 +47,13 @@ public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory { public object Create(DbContext context) { - return new object(); - } - + return new object(); + } + public object Create(DbContext context, bool designTime) { - return new object(); - } + return new object(); + } } public class MyMultiTenantThing diff --git a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantDbContextExtensions/TestDbContext.cs b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantDbContextExtensions/TestDbContext.cs index 17ae125d..aef6c98e 100644 --- a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantDbContextExtensions/TestDbContext.cs +++ b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantDbContextExtensions/TestDbContext.cs @@ -2,6 +2,7 @@ // Refer to the solution LICENSE file for more information. using System.Collections.Generic; +using Finbuckle.MultiTenant.Internal; using Microsoft.EntityFrameworkCore; namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.MultiTenantDbContextExtensions; @@ -13,7 +14,7 @@ public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext public TestDbContext(TenantInfo tenantInfo, DbContextOptions options) : - base(tenantInfo, options) + base(new StaticMultiTenantContextAccessor(tenantInfo), options) { } } diff --git a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantEntityTypeBuilderExtensions/TestDbContext.cs b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantEntityTypeBuilderExtensions/TestDbContext.cs index 4e5d7343..666380b0 100644 --- a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantEntityTypeBuilderExtensions/TestDbContext.cs +++ b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantEntityTypeBuilderExtensions/TestDbContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Finbuckle.MultiTenant.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -12,26 +13,25 @@ public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext { private readonly Action _config; - public TestDbContext(Action config, DbContextOptions options) : base( - new TenantInfo {Id = "dummy"}, - options) + public TestDbContext(Action config, DbContextOptions options) : + base(new StaticMultiTenantContextAccessor(new TenantInfo { Id = "dummy" }), options) { - this._config = config; - } + this._config = config; + } public DbSet? Blogs { get; set; } public DbSet? Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - optionsBuilder.UseSqlite("DataSource=:memory:"); - base.OnConfiguring(optionsBuilder); - } + optionsBuilder.UseSqlite("DataSource=:memory:"); + base.OnConfiguring(optionsBuilder); + } protected override void OnModelCreating(ModelBuilder modelBuilder) { - _config(modelBuilder); - } + _config(modelBuilder); + } } public class Blog @@ -55,12 +55,12 @@ public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory { public object Create(DbContext context) { - return new object(); - } - + return new object(); + } + public object Create(DbContext context, bool designTime) { - // Needed for tests that change the model. - return new object(); - } + // Needed for tests that change the model. + return new object(); + } } \ No newline at end of file diff --git a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs index 8253fd29..9a09521f 100644 --- a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs +++ b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs @@ -1,6 +1,7 @@ // Copyright Finbuckle LLC, Andrew White, and Contributors. // Refer to the solution LICENSE file for more information. +using Finbuckle.MultiTenant.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -33,7 +34,8 @@ public void WorkWithSingleParamCtor() Identifier = "abc", Name = "abc" }; - var c = new TestBlogDbContext(tenant1); + var mca = new StaticMultiTenantContextAccessor(tenant1); + var c = new TestBlogDbContext(mca); Assert.NotNull(c); } @@ -47,7 +49,51 @@ public void WorkWithTwoParamCtor() Identifier = "abc", Name = "abc" }; - var c = new TestBlogDbContext(tenant1, new DbContextOptions()); + var mca = new StaticMultiTenantContextAccessor(tenant1); + var c = new TestBlogDbContext(mca, new DbContextOptions()); + + Assert.NotNull(c); + } + + [Fact] + public void WorkWithCreate() + { + var tenant1 = new TenantInfo + { + Id = "abc", + Identifier = "abc", + Name = "abc" + }; + var c = + EntityFrameworkCore.MultiTenantDbContext.Create(tenant1, new DbContextOptions()); + + Assert.NotNull(c); + } + + [Fact] + public void WorkWithCreateNoOptions() + { + var tenant1 = new TenantInfo + { + Id = "abc", + Identifier = "abc", + Name = "abc" + }; + var c = EntityFrameworkCore.MultiTenantDbContext.Create(tenant1); + + Assert.NotNull(c); + } + + [Fact] + public void CreateArbitraryDbContext() + { + var tenant1 = new TenantInfo + { + Id = "abc", + Identifier = "abc", + Name = "abc" + }; + var c = EntityFrameworkCore.MultiTenantDbContext.Create(tenant1); Assert.NotNull(c); } diff --git a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/TestEntitities.cs b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/TestEntitities.cs index ae0b4841..7b00ed13 100644 --- a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/TestEntitities.cs +++ b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/TestEntitities.cs @@ -12,18 +12,10 @@ public class TestBlogDbContext : EntityFrameworkCore.MultiTenantDbContext public DbSet? Blogs { get; set; } public DbSet? Posts { get; set; } - public TestBlogDbContext(ITenantInfo? tenantInfo) : base(tenantInfo) - { - } - public TestBlogDbContext(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor) { } - public TestBlogDbContext(ITenantInfo? tenantInfo, DbContextOptions options) : base(tenantInfo, options) - { - } - public TestBlogDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(multiTenantContextAccessor, options) { } diff --git a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantEntityTypeBuilder/TestDbContext.cs b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantEntityTypeBuilder/TestDbContext.cs index f24fdcdd..4fb7a51f 100644 --- a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantEntityTypeBuilder/TestDbContext.cs +++ b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantEntityTypeBuilder/TestDbContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Finbuckle.MultiTenant.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -12,20 +13,19 @@ public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext { private readonly Action _config; - public TestDbContext(Action config, DbContextOptions options) : base( - new TenantInfo { Id = "dummy" }, - options) + public TestDbContext(Action config, DbContextOptions options) : + base(new StaticMultiTenantContextAccessor(new TenantInfo { Id = "dummy" }), options) { - this._config = config; - } + this._config = config; + } public DbSet? Blogs { get; set; } public DbSet? Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { - _config(modelBuilder); - } + _config(modelBuilder); + } } public class Blog @@ -50,11 +50,11 @@ public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory { public object Create(DbContext context) { - return new object(); - } - + return new object(); + } + public object Create(DbContext context, bool designTime) { - return new Object(); // Never cache! - } + return new Object(); // Never cache! + } } \ No newline at end of file From 5611c60d18c48bac55b12ccc96e862d99afb9106 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 8 Nov 2024 18:13:21 -0700 Subject: [PATCH 2/6] chore: add docs --- docs/EFCore.md | 101 +++++++++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 36 deletions(-) diff --git a/docs/EFCore.md b/docs/EFCore.md index d9a3ade1..eb30e005 100644 --- a/docs/EFCore.md +++ b/docs/EFCore.md @@ -54,19 +54,28 @@ null or mismatched tenants. Finbuckle.MultiTenant provides two different ways to utilize this behavior in a database context class: -1. Implement `IMultiTenantDbContext` and used the helper methods as +1. Implement `IMultiTenantDbContext` and use the provided helper methods as [described below](#adding-multitenant-functionality-to-an-existing-dbcontext), or -2. Derive from `MultiTenantDbContext` which handles the details for you. +2. Derive from `MultiTenantDbContext` which handles the details for + you, [also described below](#deriving-from-multitenantdbcontext). The first option is more complex, but provides enhanced flexibility and allows existing database context classes (which may derive from a base class) to utilize per-tenant data isolation. The second option is easier, but provides less -flexibility. These approaches are both explained further below. +flexibility. These approaches are both explained in detail further below. -Regardless of how the database context is configured, the context will need to know which entity types should be treated -as multi-tenant (i.e. which entity types are to be isolated per tenant) When the database context is initialized, a -shadow property named `TenantId` is added to the data model for designated entity types. This property is used -internally to filter all requests and commands. If there already is a defined string property named `TenantId` then it -will be used. +## Hybrid Per-tenant and Shared Databases + +When using a shared database context based on `IMultiTenantDbContext` it is simple extend into a hybrid approach simply +by assigning some tenants to a separate shared database (or its own completely isolated database) via a tenant info +connection string property as [described above](#separate-databases). + +## Configuring and Using a Shared Database + +Whether implementing `IMultiTenantDbContext` directly or deriving from `MultiTenantDbContext`, the context will need to +know which entity types should be treated as multi-tenant (i.e. which entity types are to be isolated per tenant) When +the database context is initialized, a shadow property named `TenantId` is added to the data model for designated entity +types. This property is used internally to filter all requests and commands. If there already is a defined string +property named `TenantId` then it will be used. There are two ways to designate an entity type as multi-tenant: @@ -190,14 +199,14 @@ default values. > injection, but this was removed in v7.0.0 for consistency. Instead, inject the `IMultiTenantContextAccessor` and use > it to set the `TenantInfo` property in the database context constructor. -Finally, call the library extension methods as described below. This requires overriding -the `OnModelCreating`, `SaveChanges`, and `SaveChangesAsync` methods. +Finally, call the library extension methods as described below. This requires overriding the `OnModelCreating`, +`SaveChanges`, and `SaveChangesAsync` methods. In `OnModelCreating` use the `EntityTypeBuilder` fluent API extension method `IsMultiTenant` to designate entity types -as multi-tenant. Call `ConfigureMultiTenant` on the `ModelBuilder` to configure each entity type marked with -the `[MultiTenant]` data attribute. This is only needed if using the attribute and internally uses the `IsMultiTenant` -fluent API. Make sure to call the base class `OnModelCreating` method if necessary, such as if inheriting -from `IdentityDbContext`. +as multi-tenant. Call `ConfigureMultiTenant` on the `ModelBuilder` to configure each entity type marked with the +`[MultiTenant]` data attribute. This is only needed if using the attribute and internally uses the `IsMultiTenant` +fluent API. Make sure to call the base class `OnModelCreating` method if necessary, such as if inheriting from +`IdentityDbContext`. ```csharp protected override void OnModelCreating(ModelBuilder builder) @@ -233,7 +242,7 @@ public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, } ``` -Now, whenever this database context is used it will only set and query records for the current tenant. +Now whenever this database context is used it will only set and query records for the current tenant. ## Deriving from `MultiTenantDbContext` @@ -249,8 +258,6 @@ dotnet add package Finbuckle.MultiTenant.EntityFrameworkCore The `MultiTenantDbContext` has two constructors which should be called from any derived database context. Make sure to forward the `IMultiTenatContextAccessor` and, if applicable the `DbContextOptions` into the base constructor. -Variants of these constructors that pass `ITenantInfo` to the base constructor are also available, but these will not be -used for dependency injection. ```csharp public class BloggingDbContext : MultiTenantDbContext @@ -297,31 +304,53 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Now, whenever this database context is used it will only set and query records for the current tenant. -## Hybrid Per-tenant and Shared Databases +## Dependency Injection -When using a shared database context based on `IMultiTenantDbContext` it is simple extend into a hybrid approach simply -by assigning some tenants to a separate shared database (or its own completely isolated database) via the tenant info -connection string property. +For many cases, such as typical ASP.NET Core apps, the normaly dependency injection registration of a database context +is sufficient. The `AddDbContext` will register the context as a service and provide the necessary dependencies. +Injected instances will automatically be associated with the current tenant. + +When registering the database context as a service for use with depency injection it is important to take into account +whether the connection string and/or provider will vary per-tenant. If so, it is recommended to set the connection +string and provider in the `OnConfiguring` database context method as described above rather than in the `AddDbContext` +service registration method. + +## Factory Instantiation + +In some cases it may be necessary to create a database context instance without dependency injection, such as in code +that loops through tenants. In this case, the `MultiTenantDbContext.Create` factory method can be used to create a +database context instance for a specific tenant. + +```csharp +// create or otherwise obtain a tenant info instance +var tenantInfo = new MyTenantInfo(...); + +// create a database context instance for the tenant +var tenantDbContext = MultiTenantDbContext.Create(tenantInfo); + +// create a database context instance for the tenant with an instance of DbOptions +var tenantDbContextWithOptions = MultiTenantDbContext.Create(tenantInfo, +dbOptions); +``` + +Make sure to dispose of the database context instance when it is no longer needed, or better yet use a `using` block. +This method will work for any database context class expecting a `IMultiTenantContextAccessor` in its constructor and an +options DbContextOptions in its constructor. ## Design Time Instantiation Given that a multi-tenant database context usually requires a tenant to function, design time instantiation can be challenging. By default, for things like migrations and command line tools Entity Framework core attempts to create an instance of the context using dependency injection, however usually no valid tenant exists in these cases and DI fails. -For this reason it is recommended to use a [design time factory](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dbcontext-creation#from-a-design-time-factory) wherein a dummy `ITenantInfo` is -constructed with the desired connection string and passed to the database context constructor. - -## Registering with ASP.NET Core - -When registering the database context as a service in ASP.NET Core it is important to take into account whether the -connection string and/or provider will vary per-tenant. If so, it is recommended to set the connection string and -provider in the `OnConfiguring` database context method as described above rather than in the `AddDbContext` service -registration method. +For this reason it is recommended to use +a [design time factory](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dbcontext-creation#from-a-design-time-factory) +wherein a dummy `ITenantInfo` with the desired connection string and passed to the database context +creation factory described above. ## Adding Data -Added entities are automatically associated with the current `TenantInfo`. If an entity is associated with a -different `TenantInfo` then a `MultiTenantException` is thrown in `SaveChanges` or `SaveChangesAsync`. +Added entities are automatically associated with the current `TenantInfo`. If an entity is associated with a different +`TenantInfo` then a `MultiTenantException` is thrown in `SaveChanges` or `SaveChangesAsync`. ```csharp // Add a blog for a tenant. @@ -417,8 +446,8 @@ property on the database context: * `TenantMismatchMode.Throw` - A `MultiTenantException` is thrown (default). * `TenantMismatchMode.Ignore` - The entity is added or updated without modifying its `TenantId`. -* `TenantMismatchMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's - current `TenantInfo`. +* `TenantMismatchMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's current + `TenantInfo`. ## Tenant Not Set Mode @@ -428,5 +457,5 @@ or `SaveChangesAsync`. This behavior can be changed by setting the `TenantNotSet * `TenantNotSetMode.Throw` - For added entities the null `TenantId` will be overwritten to match the database context's current `TenantInfo`. For updated entities a `MultiTenantException` is thrown (default). -* `TenantNotSetMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's - current `TenantInfo`. +* `TenantNotSetMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's current + `TenantInfo`. From c0dc259835a54dda156d352a46fc26d0a0756726 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 8 Nov 2024 18:16:21 -0700 Subject: [PATCH 3/6] chore: docs tweak --- docs/EFCore.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/EFCore.md b/docs/EFCore.md index eb30e005..7154574f 100644 --- a/docs/EFCore.md +++ b/docs/EFCore.md @@ -323,17 +323,25 @@ database context instance for a specific tenant. ```csharp // create or otherwise obtain a tenant info instance -var tenantInfo = new MyTenantInfo(...); +using var tenantInfo = new MyTenantInfo(...); // create a database context instance for the tenant -var tenantDbContext = MultiTenantDbContext.Create(tenantInfo); +using var tenantDbContext = MultiTenantDbContext.Create(tenantInfo); // create a database context instance for the tenant with an instance of DbOptions var tenantDbContextWithOptions = MultiTenantDbContext.Create(tenantInfo, dbOptions); + +// loop through a bunch of tenant instances +foreach (var tenant in tenants) +{ + using var tenantDbContext = MultiTenantDbContext.Create(tenant); + // do something with the database context +} ``` -Make sure to dispose of the database context instance when it is no longer needed, or better yet use a `using` block. +Make sure to dispose of the database context instance when it is no longer needed, or better yet use a `using` block +or variable. This method will work for any database context class expecting a `IMultiTenantContextAccessor` in its constructor and an options DbContextOptions in its constructor. From e7a56deac0132e7cd2a223ccc547ed3732e65759 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 8 Nov 2024 18:17:18 -0700 Subject: [PATCH 4/6] chore: docs tweak --- docs/EFCore.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/EFCore.md b/docs/EFCore.md index 7154574f..c0c8da24 100644 --- a/docs/EFCore.md +++ b/docs/EFCore.md @@ -340,10 +340,9 @@ foreach (var tenant in tenants) } ``` -Make sure to dispose of the database context instance when it is no longer needed, or better yet use a `using` block -or variable. -This method will work for any database context class expecting a `IMultiTenantContextAccessor` in its constructor and an -options DbContextOptions in its constructor. +Make sure to dispose of the database context instance when it is no longer needed, or better yet use a `using` block or +variable. This method will work for any database context class expecting a `IMultiTenantContextAccessor` in its +constructor and an options DbContextOptions in its constructor. ## Design Time Instantiation @@ -352,8 +351,8 @@ challenging. By default, for things like migrations and command line tools Entit instance of the context using dependency injection, however usually no valid tenant exists in these cases and DI fails. For this reason it is recommended to use a [design time factory](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dbcontext-creation#from-a-design-time-factory) -wherein a dummy `ITenantInfo` with the desired connection string and passed to the database context -creation factory described above. +wherein a dummy `ITenantInfo` with the desired connection string and passed to the database context creation factory +described above. ## Adding Data From df1f0c16316a6c25aa7d9d546e21be5769e605bb Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 8 Nov 2024 18:18:13 -0700 Subject: [PATCH 5/6] chore: docs tweak --- docs/EFCore.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/EFCore.md b/docs/EFCore.md index c0c8da24..b1a3ae71 100644 --- a/docs/EFCore.md +++ b/docs/EFCore.md @@ -306,13 +306,14 @@ Now, whenever this database context is used it will only set and query records f ## Dependency Injection -For many cases, such as typical ASP.NET Core apps, the normaly dependency injection registration of a database context -is sufficient. The `AddDbContext` will register the context as a service and provide the necessary dependencies. -Injected instances will automatically be associated with the current tenant. - -When registering the database context as a service for use with depency injection it is important to take into account -whether the connection string and/or provider will vary per-tenant. If so, it is recommended to set the connection -string and provider in the `OnConfiguring` database context method as described above rather than in the `AddDbContext` +For many cases, such as typical ASP.NET Core apps, the normal dependency injection registration of a database context is +sufficient. The `AddDbContext` will register the context as a service and provide the necessary dependencies. Injected +instances will automatically be associated with the current tenant. + +When registering the database context as a service for use with dependency injection it is important to take into +account whether the connection string and/or provider will vary per-tenant. If so, it is recommended to set the +connection string and provider in the `OnConfiguring` database context method as described above rather than in the +`AddDbContext` service registration method. ## Factory Instantiation From 1ca99824c0856462726169a0d81a0ee1c6866f06 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Fri, 8 Nov 2024 18:22:30 -0700 Subject: [PATCH 6/6] chore: added test --- .../MultiTenantDbContextShould.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs index 9a09521f..6b2acf17 100644 --- a/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs +++ b/test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs @@ -97,4 +97,17 @@ public void CreateArbitraryDbContext() Assert.NotNull(c); } + + [Fact] + public void ThrowOnInvalidDbContext() + { + var tenant1 = new TenantInfo + { + Id = "abc", + Identifier = "abc", + Name = "abc" + }; + + Assert.Throws(() => EntityFrameworkCore.MultiTenantDbContext.Create(tenant1)); + } } \ No newline at end of file