Skip to content

Commit ea216ff

Browse files
authoredNov 9, 2024··
multitenant db factory (#894)
feat: add MultiTenantDbContext.Create factory method BREAKING CHANGE: `MultiTenantDbContext` constructors accepting ITenantInfo removed, use `MultiTenantDbContext .Create` factory method instead
1 parent ebf6d86 commit ea216ff

File tree

10 files changed

+301
-181
lines changed

10 files changed

+301
-181
lines changed
 

‎docs/EFCore.md

+73-36
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,28 @@ null or mismatched tenants.
5454

5555
Finbuckle.MultiTenant provides two different ways to utilize this behavior in a database context class:
5656

57-
1. Implement `IMultiTenantDbContext` and used the helper methods as
57+
1. Implement `IMultiTenantDbContext` and use the provided helper methods as
5858
[described below](#adding-multitenant-functionality-to-an-existing-dbcontext), or
59-
2. Derive from `MultiTenantDbContext` which handles the details for you.
59+
2. Derive from `MultiTenantDbContext` which handles the details for
60+
you, [also described below](#deriving-from-multitenantdbcontext).
6061

6162
The first option is more complex, but provides enhanced flexibility and allows existing database context classes (which
6263
may derive from a base class) to utilize per-tenant data isolation. The second option is easier, but provides less
63-
flexibility. These approaches are both explained further below.
64+
flexibility. These approaches are both explained in detail further below.
6465

65-
Regardless of how the database context is configured, the context will need to know which entity types should be treated
66-
as multi-tenant (i.e. which entity types are to be isolated per tenant) When the database context is initialized, a
67-
shadow property named `TenantId` is added to the data model for designated entity types. This property is used
68-
internally to filter all requests and commands. If there already is a defined string property named `TenantId` then it
69-
will be used.
66+
## Hybrid Per-tenant and Shared Databases
67+
68+
When using a shared database context based on `IMultiTenantDbContext` it is simple extend into a hybrid approach simply
69+
by assigning some tenants to a separate shared database (or its own completely isolated database) via a tenant info
70+
connection string property as [described above](#separate-databases).
71+
72+
## Configuring and Using a Shared Database
73+
74+
Whether implementing `IMultiTenantDbContext` directly or deriving from `MultiTenantDbContext`, the context will need to
75+
know which entity types should be treated as multi-tenant (i.e. which entity types are to be isolated per tenant) When
76+
the database context is initialized, a shadow property named `TenantId` is added to the data model for designated entity
77+
types. This property is used internally to filter all requests and commands. If there already is a defined string
78+
property named `TenantId` then it will be used.
7079

7180
There are two ways to designate an entity type as multi-tenant:
7281

@@ -190,14 +199,14 @@ default values.
190199
> injection, but this was removed in v7.0.0 for consistency. Instead, inject the `IMultiTenantContextAccessor` and use
191200
> it to set the `TenantInfo` property in the database context constructor.
192201
193-
Finally, call the library extension methods as described below. This requires overriding
194-
the `OnModelCreating`, `SaveChanges`, and `SaveChangesAsync` methods.
202+
Finally, call the library extension methods as described below. This requires overriding the `OnModelCreating`,
203+
`SaveChanges`, and `SaveChangesAsync` methods.
195204

196205
In `OnModelCreating` use the `EntityTypeBuilder` fluent API extension method `IsMultiTenant` to designate entity types
197-
as multi-tenant. Call `ConfigureMultiTenant` on the `ModelBuilder` to configure each entity type marked with
198-
the `[MultiTenant]` data attribute. This is only needed if using the attribute and internally uses the `IsMultiTenant`
199-
fluent API. Make sure to call the base class `OnModelCreating` method if necessary, such as if inheriting
200-
from `IdentityDbContext`.
206+
as multi-tenant. Call `ConfigureMultiTenant` on the `ModelBuilder` to configure each entity type marked with the
207+
`[MultiTenant]` data attribute. This is only needed if using the attribute and internally uses the `IsMultiTenant`
208+
fluent API. Make sure to call the base class `OnModelCreating` method if necessary, such as if inheriting from
209+
`IdentityDbContext`.
201210

202211
```csharp
203212
protected override void OnModelCreating(ModelBuilder builder)
@@ -233,7 +242,7 @@ public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
233242
}
234243
```
235244

236-
Now, whenever this database context is used it will only set and query records for the current tenant.
245+
Now whenever this database context is used it will only set and query records for the current tenant.
237246

238247
## Deriving from `MultiTenantDbContext`
239248

@@ -249,8 +258,6 @@ dotnet add package Finbuckle.MultiTenant.EntityFrameworkCore
249258

250259
The `MultiTenantDbContext` has two constructors which should be called from any derived database context. Make sure to
251260
forward the `IMultiTenatContextAccessor` and, if applicable the `DbContextOptions<T>` into the base constructor.
252-
Variants of these constructors that pass `ITenantInfo` to the base constructor are also available, but these will not be
253-
used for dependency injection.
254261

255262
```csharp
256263
public class BloggingDbContext : MultiTenantDbContext
@@ -297,31 +304,61 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
297304

298305
Now, whenever this database context is used it will only set and query records for the current tenant.
299306

300-
## Hybrid Per-tenant and Shared Databases
307+
## Dependency Injection
301308

302-
When using a shared database context based on `IMultiTenantDbContext` it is simple extend into a hybrid approach simply
303-
by assigning some tenants to a separate shared database (or its own completely isolated database) via the tenant info
304-
connection string property.
309+
For many cases, such as typical ASP.NET Core apps, the normal dependency injection registration of a database context is
310+
sufficient. The `AddDbContext` will register the context as a service and provide the necessary dependencies. Injected
311+
instances will automatically be associated with the current tenant.
312+
313+
When registering the database context as a service for use with dependency injection it is important to take into
314+
account whether the connection string and/or provider will vary per-tenant. If so, it is recommended to set the
315+
connection string and provider in the `OnConfiguring` database context method as described above rather than in the
316+
`AddDbContext`
317+
service registration method.
318+
319+
## Factory Instantiation
320+
321+
In some cases it may be necessary to create a database context instance without dependency injection, such as in code
322+
that loops through tenants. In this case, the `MultiTenantDbContext.Create` factory method can be used to create a
323+
database context instance for a specific tenant.
324+
325+
```csharp
326+
// create or otherwise obtain a tenant info instance
327+
using var tenantInfo = new MyTenantInfo(...);
328+
329+
// create a database context instance for the tenant
330+
using var tenantDbContext = MultiTenantDbContext.Create<AppMultiTenantDbContext, AppTenantInfo>(tenantInfo);
331+
332+
// create a database context instance for the tenant with an instance of DbOptions<AppMultiTenantDbContext>
333+
var tenantDbContextWithOptions = MultiTenantDbContext.Create<AppMultiTenantDbContext, AppTenantInfo>(tenantInfo,
334+
dbOptions);
335+
336+
// loop through a bunch of tenant instances
337+
foreach (var tenant in tenants)
338+
{
339+
using var tenantDbContext = MultiTenantDbContext.Create<AppMultiTenantDbContext, AppTenantInfo>(tenant);
340+
// do something with the database context
341+
}
342+
```
343+
344+
Make sure to dispose of the database context instance when it is no longer needed, or better yet use a `using` block or
345+
variable. This method will work for any database context class expecting a `IMultiTenantContextAccessor` in its
346+
constructor and an options DbContextOptions<T> in its constructor.
305347

306348
## Design Time Instantiation
307349

308350
Given that a multi-tenant database context usually requires a tenant to function, design time instantiation can be
309351
challenging. By default, for things like migrations and command line tools Entity Framework core attempts to create an
310352
instance of the context using dependency injection, however usually no valid tenant exists in these cases and DI fails.
311-
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
312-
constructed with the desired connection string and passed to the database context constructor.
313-
314-
## Registering with ASP.NET Core
315-
316-
When registering the database context as a service in ASP.NET Core it is important to take into account whether the
317-
connection string and/or provider will vary per-tenant. If so, it is recommended to set the connection string and
318-
provider in the `OnConfiguring` database context method as described above rather than in the `AddDbContext` service
319-
registration method.
353+
For this reason it is recommended to use
354+
a [design time factory](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dbcontext-creation#from-a-design-time-factory)
355+
wherein a dummy `ITenantInfo` with the desired connection string and passed to the database context creation factory
356+
described above.
320357

321358
## Adding Data
322359

323-
Added entities are automatically associated with the current `TenantInfo`. If an entity is associated with a
324-
different `TenantInfo` then a `MultiTenantException` is thrown in `SaveChanges` or `SaveChangesAsync`.
360+
Added entities are automatically associated with the current `TenantInfo`. If an entity is associated with a different
361+
`TenantInfo` then a `MultiTenantException` is thrown in `SaveChanges` or `SaveChangesAsync`.
325362

326363
```csharp
327364
// Add a blog for a tenant.
@@ -417,8 +454,8 @@ property on the database context:
417454

418455
* `TenantMismatchMode.Throw` - A `MultiTenantException` is thrown (default).
419456
* `TenantMismatchMode.Ignore` - The entity is added or updated without modifying its `TenantId`.
420-
* `TenantMismatchMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's
421-
current `TenantInfo`.
457+
* `TenantMismatchMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's current
458+
`TenantInfo`.
422459

423460
## Tenant Not Set Mode
424461

@@ -428,5 +465,5 @@ or `SaveChangesAsync`. This behavior can be changed by setting the `TenantNotSet
428465

429466
* `TenantNotSetMode.Throw` - For added entities the null `TenantId` will be overwritten to match the database context's
430467
current `TenantInfo`. For updated entities a `MultiTenantException` is thrown (default).
431-
* `TenantNotSetMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's
432-
current `TenantInfo`.
468+
* `TenantNotSetMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's current
469+
`TenantInfo`.

‎src/Finbuckle.MultiTenant.EntityFrameworkCore/MultiTenantDbContext.cs

+29-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
// Copyright Finbuckle LLC, Andrew White, and Contributors.
22
// Refer to the solution LICENSE file for more information.
33

4-
using System.Threading;
5-
using System.Threading.Tasks;
64
using Finbuckle.MultiTenant.Abstractions;
5+
using Finbuckle.MultiTenant.Internal;
76
using Microsoft.EntityFrameworkCore;
87

98
namespace Finbuckle.MultiTenant.EntityFrameworkCore;
@@ -22,9 +21,33 @@ public abstract class MultiTenantDbContext : DbContext, IMultiTenantDbContext
2221
/// <inheritdoc />
2322
public TenantNotSetMode TenantNotSetMode { get; set; } = TenantNotSetMode.Throw;
2423

25-
protected MultiTenantDbContext(ITenantInfo? tenantInfo)
24+
/// <summary>
25+
/// Creates a new instance of a multitenant context that accepts a IMultiTenantContextAccessor instance and an optional DbContextOptions instance.
26+
/// </summary>
27+
/// <param name="tenantInfo">The tenant information to bind to the context.</param>
28+
/// <param name="options">The database options instance.</param>
29+
/// <typeparam name="TContext">The TContext implementation type.</typeparam>
30+
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
31+
/// <returns></returns>
32+
public static TContext Create<TContext, TTenantInfo>(TTenantInfo? tenantInfo, DbContextOptions? options = null)
33+
where TContext : DbContext
34+
where TTenantInfo : class, ITenantInfo, new()
2635
{
27-
TenantInfo = tenantInfo;
36+
try
37+
{
38+
var mca = new StaticMultiTenantContextAccessor<TTenantInfo>(tenantInfo);
39+
var context = options switch
40+
{
41+
null => (TContext)Activator.CreateInstance(typeof(TContext), mca)!,
42+
not null => (TContext)Activator.CreateInstance(typeof(TContext), mca, options)!
43+
};
44+
45+
return context;
46+
}
47+
catch (MissingMethodException)
48+
{
49+
throw new ArgumentException("The provided DbContext type does not have a constructor that accepts the required parameters.");
50+
}
2851
}
2952

3053
/// <summary>
@@ -36,17 +59,13 @@ protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAcc
3659
TenantInfo = multiTenantContextAccessor.MultiTenantContext.TenantInfo;
3760
}
3861

39-
protected MultiTenantDbContext(ITenantInfo? tenantInfo, DbContextOptions options) : base(options)
40-
{
41-
TenantInfo = tenantInfo;
42-
}
43-
4462
/// <summary>
4563
/// Constructs the database context instance and binds to the current tenant.
4664
/// </summary>
4765
/// <param name="multiTenantContextAccessor">The MultiTenantContextAccessor instance used to bind the context instance to a tenant.</param>
4866
/// <param name="options">The database options instance.</param>
49-
protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(options)
67+
protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) :
68+
base(options)
5069
{
5170
TenantInfo = multiTenantContextAccessor.MultiTenantContext.TenantInfo;
5271
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Finbuckle.MultiTenant.Abstractions;
2+
3+
namespace Finbuckle.MultiTenant.Internal;
4+
5+
internal class StaticMultiTenantContextAccessor<TTenantInfo>(TTenantInfo? tenantInfo)
6+
: IMultiTenantContextAccessor<TTenantInfo>
7+
where TTenantInfo : class, ITenantInfo, new()
8+
{
9+
IMultiTenantContext IMultiTenantContextAccessor.MultiTenantContext => MultiTenantContext;
10+
11+
public IMultiTenantContext<TTenantInfo> MultiTenantContext { get; } =
12+
new MultiTenantContext<TTenantInfo> { TenantInfo = tenantInfo };
13+
}

0 commit comments

Comments
 (0)
Please sign in to comment.