Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Finbuckle.MultiTenant.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<Project Path="src\Finbuckle.MultiTenant\Finbuckle.MultiTenant.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test\Finbuckle.MultiTenant.Abstractions.Test\Finbuckle.MultiTenant.Abstractions.Test.csproj" />
<Project Path="test\Finbuckle.MultiTenant.AspNetCore.Test\Finbuckle.MultiTenant.AspNetCore.Test.csproj" />
<Project Path="test\Finbuckle.MultiTenant.EntityFrameworkCore.Test\Finbuckle.MultiTenant.EntityFrameworkCore.Test.csproj" />
<Project Path="test\Finbuckle.MultiTenant.Identity.EntityFrameworkCore.Test\Finbuckle.MultiTenant.Identity.EntityFrameworkCore.Test.csproj" />
Expand Down
24 changes: 11 additions & 13 deletions docs/ConfigurationAndUsage.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,15 @@ There are several ways your app can see the current tenant:

### Dependency Injection

* `IMultiTenantContextAccessor` and `IMultiTenantContextAccessor<TTeenantInfo>` are available via dependency injection
* `IMultiTenantContextAccessor` and `IMultiTenantContextAccessor<TTenantInfo>` are available via dependency injection
and behave similar to `IHttpContextAccessor`. Internally an `AsyncLocal<T>` is used to track state and in parent async
contexts any changes in tenant will not be reflected. For example, the accessor will not reflect a tenant in the
post-endpoint processing in ASP.NET Core middleware registered prior to `UseMultiTenant`. Use the `HttpContext`
extension `GetMultiTenantContext<TTenantInfo>` to avoid this caveat.

* `IMultiTenantContextSetter` is available via dependency injection and can be used to set the current tenant. This is
useful in advanced scenarios and should be used with caution. Prefer using the `HttpContext` extension method
`TrySetTenantInfo<TTenantInfo>` in use cases where `HttpContext` is available.
`SetTenantInfo<TTenantInfo>` in use cases where `HttpContext` is available.

> Prior versions of MultiTenant also exposed `IMultiTenantContext`, `TenantInfo`, and their implementations
> via dependency injection. This was removed as these are not actual services, similar to
Expand All @@ -251,7 +251,6 @@ For web apps these convenience methods are also available:
var tenantId = tenantInfo.Id;
var identifier = tenantInfo.Identifier;
var name = tenantInfo.Name;
var something = tenantInfo.Items["something"];
}
```

Expand All @@ -260,21 +259,20 @@ For web apps these convenience methods are also available:
For most cases the middleware sets the `TenantInfo` and this method is not needed. Use only if explicitly overriding
the `TenantInfo` set by the middleware.

Use this 'HttpContext' extension method to the current tenant to the provided `TenantInfo`. Returns true if
successful. Optionally it can also reset the service provider scope so that any scoped services already resolved will
Use this 'HttpContext' extension method to set the current tenant to the provided `TenantInfo`.
Optionally it can also reset the service provider scope so that any scoped services already resolved will
be resolved again under the current tenant when needed. This has no effect on singleton or transient services. Setting
the `TenantInfo` with this method sets both the `StoreInfo` and `StrategyInfo` properties on the
`MultiTenantContext<TTenantInfo>` to `null`.

```csharp
var newTenantInfo = new TenantInfo(...);
var newTenantInfo = new TenantInfo { Id = "new-id", Identifier = "new-identifier" };

if(HttpContext.TrySetTenantInfo(newTenantInfo, resetServiceProvider: true))
{
// This will be the new tenant.
var tenant = HttpContext.GetMultiTenantContext().TenantInfo;
HttpContext.SetTenantInfo(newTenantInfo, resetServiceProviderScope: true);

// This will regenerate the options class.
var optionsProvider = HttpContext.RequestServices.GetService<IOptions<MyScopedOptions>>();
}
// This will be the new tenant.
var tenant = HttpContext.GetMultiTenantContext<TenantInfo>().TenantInfo;

// This will regenerate the options class.
var optionsProvider = HttpContext.RequestServices.GetService<IOptions<MyScopedOptions>>();
```
26 changes: 14 additions & 12 deletions docs/CoreConcepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
The library uses standard .NET Core conventions and most of the internal details are abstracted away from app code.
However, there are a few important specifics to be aware of. The items below make up the foundation of the library.

## `TenantInfo`
## `ITenantInfo` and `TenantInfo`

A `TenantInfo` record instance contains information about a tenant. Often this will be the "current" tenant in the context an
app. These instances' type use or inherit from `TenantInfo` which defines properties
for `Id`, `Identifier`, `Name`. When calling `AddMultiTenant<TTenantInfo>` the type passed into the
type parameter defines the `TenantInfo` derived class used throughout the library and app. TenantInfo instances are
intended to be immutable and the `with` expression should be used to create modified copies.
A `TenantInfo` instance contains information about a tenant. Often this will be the "current" tenant in the context an
app. These instances' type must implement `ITenantInfo` which defines properties
for `Id` and `Identifier`. `TenantInfo` is a basic implementation provided by the library which also includes a `Name` property.

When calling `AddMultiTenant<TTenantInfo>` the type passed into the
type parameter defines the `ITenantInfo` implementation used throughout the library and app.

* `Id` is a unique id for a tenant in your app and should never change.
* `Identifier` is the value used to actually resolve a tenant and should have a syntax compatible for your app (i.e. no
crazy symbols in a web app where the identifier will be part of the URL). Unlike `Id`, `Identifier` can be changed if
necessary.
* `Name` is a display name for the tenant.

`TenantInfo` is a base implementation. Your app can and should define a custom `TenantInfo` and add custom
The library provides `TenantInfo` as a base implementation. Your app can and should define a custom class implementing `ITenantInfo` (or inheriting from `TenantInfo`) and add custom
properties as needed. It is recommended to keep these
classes lightweight since they are often queried. Keep heavier associated data in an external area that can be pulled in
when needed via the tenant `Id`.
Expand All @@ -32,13 +32,15 @@ The `MultiTenantContext<TTenantInfo>` contains information about the current ten
* Implements `IMultiTenantContext` and `IMultiTenantContext<TTenantInfo>` which can be obtained from dependency injection.
* Includes `TenantInfo`, `StrategyInfo`, and `StoreInfo` properties with details on the current tenant, how it was
determined, and from where its information was retrieved.
* The TenantInfo property gets and sets copies of the `TenantInfo` instance to help ensure immutability.
* The `HasTenant` property indicates whether a tenant was successfully resolved for the current context.
* The `IsResolved` property indicates whether a tenant was successfully resolved for the current context.
* Can be obtained in ASP.NET Core by calling the `GetMultiTenantContext()` method on the current request's `HttpContext`
object. The implementation used with ASP.NET Core middleware has read only properties. The `HttpContext` extension
method `SetTenantInfo` can be used to manually set the current tenant, but normally the middleware handles this.
object.
* The `HttpContext` extension method `SetTenantInfo` can be used to manually set the current tenant, but normally the middleware handles this.
* A custom implementation can be defined for advanced use cases.

> In the original v10 release the `TenantInfo` property was immutable, but this change was reverted. It is
> recommended that you only mutate the `TenantInfo` property with extreme care.

## MultiTenant Strategies

Responsible for determining and returning a tenant identifier string for the current request.
Expand Down
41 changes: 20 additions & 21 deletions docs/EFCore.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,28 @@ implementation. and use it in the `OnConfiguring` method of the database context
by injecting a `IMultiTenantContextAccessor<TTenantInfo>` into the database context class constructor.

```csharp
public class AppTenantInfo : TenantInfo
public class AppTenantInfo : ITenantInfo
{
public string Id { get; set; }
public string Identifier { get; set; }
public string Name { get; set; }
public string ConnectionString { get; set; }
public required string Id { get; init; }
public required string Identifier { get; init; }
public string? Name { get; init; }
public string? ConnectionString { get; init; }
}

public class MyAppDbContext : DbContext
{
// AppTenantInfo is the app's custom implementation of TenantInfo which
private AppTenantInfo TenantInfo { get; set; }
private AppTenantInfo? TenantInfo { get; set; }

public MyAppDbContext(IMultiTenantContextAccessor<AppTenantInfo> multiTenantContextAccessor)
{
// get the current tenant info at the time of construction
TenantInfo = multiTenantContextAccessor.tenantInfo;
TenantInfo = multiTenantContextAccessor.MultiTenantContext.TenantInfo;
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// use the connection string to connect to the per-tenant database
optionsBuilder.UseSqlServer(TenantInfo.ConnectionString);
optionsBuilder.UseSqlServer(TenantInfo?.ConnectionString);
}
...
}
Expand Down Expand Up @@ -210,7 +209,7 @@ will have the information needed to provide proper data isolation.
public class MyDbContext : DbContext, IMultiTenantDbContext
{
...
public TenantInfo TenantInfo { get; }
public ITenantInfo? TenantInfo { get; }
public TenantMismatchMode TenantMismatchMode { get; }
public TenantNotSetMode TenantNotSetMode { get; }
...
Expand Down Expand Up @@ -294,9 +293,9 @@ public class BloggingDbContext : MultiTenantDbContext
}

// these constructors are useful for testing or other use cases where depdenency injection is not used
public BloggingDbContext(TenantInfo tenantInfo) : base(tenantInfo) { }
public BloggingDbContext(ITenantInfo tenantInfo) : base(tenantInfo) { }

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

public DbSet<Blog> Blogs { get; set; }
Expand Down Expand Up @@ -331,7 +330,7 @@ database context instance for a specific tenant.

```csharp
// create or otherwise obtain a tenant info instance
var tenantInfo = new MyTenantInfo(...);
var tenantInfo = new MyTenantInfo { Id = "id", Identifier = "identifier" };

// create a database context instance for the tenant
var tenantDbContext = MultiTenantDbContext.Create<AppMultiTenantDbContext, AppTenantInfo>(tenantInfo);
Expand Down Expand Up @@ -392,13 +391,13 @@ altered by changing the values of [TenantMisMatchMode](#tenant-mismatch-mode) an
Blog myBlog = new Blog{ TenantId = "1", Title = "My Blog" };

// Add the blog to a db context for a tenant.
var myTenantInfo = ...;
var myTenantInfo = new TenantInfo { Id = "1", Identifier = "tenant-1" };
var myDbContext = MultiTenantDbContext.Create<BloggingDbContext, TenantInfo>(myTenantInfo);
myDbContext.Blogs.Add(myBlog));
myDbContext.Blogs.Add(myBlog);
myDbContext.SaveChanges();

// Try to add the same blog to a different tenant.
var yourTenantInfo = ...;
var yourTenantInfo = new TenantInfo { Id = "2", Identifier = "tenant-2" };
var yourDbContext = MultiTenantDbContext.Create<BloggingDbContext, TenantInfo>(yourTenantInfo);
yourDbContext.Blogs.Add(myBlog);
await yourDbContext.SaveChangesAsync(); // Throws MultiTenantException.
Expand All @@ -410,12 +409,12 @@ EF Core Queries will only return results associated to the `TenantInfo`.

```csharp
// Will only return "My Blog".
var myTenantInfo = ...;
var myTenantInfo = new TenantInfo { Id = "1", Identifier = "tenant-1" };
var myDbContext = MultiTenantDbContext.Create<BloggingDbContext, TenantInfo>(myTenantInfo);
var tenantBlog = myDbContext.Blogs.First();

// Will only return "Your Blog".
var yourTenantInfo = ...;
var yourTenantInfo = new TenantInfo { Id = "2", Identifier = "tenant-2" };
var yourDbContext = MultiTenantDbContext.Create<BloggingDbContext, TenantInfo>(yourTenantInfo);
var yourBlogs = yourDbContext.Blogs.First();
```
Expand Down Expand Up @@ -447,13 +446,13 @@ This behavior can be altered by changing the values of [TenantMisMatchMode](#ten
```csharp
// Add a blog for a tenant.
Blog myBlog = new Blog{ TenantId = "1", Title = "My Blog" };
var myTenantInfo = ...;
var myTenantInfo = new TenantInfo { Id = "1", Identifier = "tenant-1" };
var myDbContext = MultiTenantDbContext.Create<BloggingDbContext, TenantInfo>(myTenantInfo);
myDbContext.Blogs.Add(myBlog));
myDbContext.Blogs.Add(myBlog);
myDbContext.SaveChanges();

// Modify and attach the same blog to a different tenant.
var yourTenantInfo = ...;
var yourTenantInfo = new TenantInfo { Id = "2", Identifier = "tenant-2" };
var yourDbContext = MultiTenantDbContext.Create<BloggingDbContext, TenantInfo>(yourTenantInfo);
yourDbContext.Blogs.Attach(myBlog);
myBlog.Title = "My Changed Blog";
Expand Down
6 changes: 3 additions & 3 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ That's all that is needed to get going. Let's break down each line:
This line registers the base services and designates `TenantInfo` as the class that will hold tenant information at
runtime.

The type parameter for `AddMultiTenant<TTenantInfo>` must be an instance of `TenantInfo` or a derived class and holds
basic information about the tenant such as its name and an identifier. `TenantInfo` is provided as a basic
implementation record, but a derived record can be used if more properties are needed.
The type parameter for `AddMultiTenant<TTenantInfo>` must implement `ITenantInfo` and holds
basic information about the tenant such as its id and an identifier. `TenantInfo` is provided as a basic
implementation class, but any implementation of `ITenantInfo` can be used if more properties are needed.

See [Core Concepts](CoreConcepts) for more information on `TenantInfo`.

Expand Down
8 changes: 4 additions & 4 deletions docs/Stores.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# MultiTenant Stores

A MultiTenant store is responsible for retrieving information about a tenant based on an identifier string determined
by [MultiTenant strategies](Strategies). The retrieved information is then used to create a `TenantInfo` object which
by [MultiTenant strategies](Strategies). The retrieved information is then used to create an `ITenantInfo` object which
provides the current tenant information to your app.

MultiTenant supports several "out-of-the-box" stores for resolving the tenant. Custom stores can be created by
implementing `IMultiTenantStore`.

## Custom TenantInfo Support

MultiTenant stores support custom `TenantInfo` derived classes, but complex implementations may require special
MultiTenant stores support custom `ITenantInfo` implementations, but complex implementations may require special
handling. For best results ensure the class works well with the underlying store approach—for example, that it can be
serialized from JSON for the configuration store if using JSON file configuration sources.

Expand Down Expand Up @@ -168,8 +168,8 @@ Uses an Entity Framework Core database context as the backing store.

Case sensitivity is determined by the underlying EF Core database provider.

The database context must derive from `EFCoreStoreDbContext`. Note that `TTenantInfo` is a record type and EF Core
does not support tracking for record types. The `EFCoreStore` carefully avoids tracking issues, but if your app uses the
The database context must derive from `EFCoreStoreDbContext`. The `EFCoreStore` carefully avoids tracking issues by
using no-tracking queries and detaching entities after store operations. If your app uses the
`EFCoreStoreDbContext` directly it should be aware of these issues.

This database context is not itself multi-tenant, but rather contains the details of all tenants.
Expand Down
4 changes: 2 additions & 2 deletions docs/Strategies.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# MultiTenant Strategies

A multi-tenant strategy is responsible for defining how the tenant is determined. It ultimately produces an identifier
string which is used to create a `TenantInfo` object with information from the [MultiTenant store](Stores).
string which is used to resolve an `ITenantInfo` object with information from the [MultiTenant store](Stores).

MultiTenant supports several "out-of-the-box" strategies for resolving the tenant. Custom strategies can be
created by implementing `IMultiTenantStrategy` or using `DelegateStrategy`.
Expand All @@ -20,7 +20,7 @@ created by implementing `IMultiTenantStrategy` or using `DelegateStrategy`.
All MultiTenant strategies derive from `IMultiTenantStrategy` and must implement the `GetIdentifierAsync` method.

If an identifier can't be determined, `GetIdentifierAsync` should return null which will ultimately result in a
null `TenantInfo`.
null `ITenantInfo`.

Configure a custom implementation of `IMultiTenantStrategy` by calling `WithStrategy<TStrategy>`
after `AddMultiTenant<TTenantInfo>` in your app configuration. There are several
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ namespace Finbuckle.MultiTenant.Abstractions;
/// <summary>
/// Provides access to the current <see cref="IMultiTenantContext{TTenantInfo}"/> via an <see cref="AsyncLocal{T}"/> variable.
/// </summary>
/// <typeparam name="TTenantInfo">The <see cref="TenantInfo"/> derived type.</typeparam>
/// <typeparam name="TTenantInfo">The <see cref="ITenantInfo"/> implementation type.</typeparam>
public class AsyncLocalMultiTenantContextAccessor<TTenantInfo> : IMultiTenantContextSetter,
IMultiTenantContextAccessor<TTenantInfo>
where TTenantInfo : TenantInfo
where TTenantInfo : ITenantInfo
{
private static readonly AsyncLocal<IMultiTenantContext<TTenantInfo>> AsyncLocalContext = new();

/// <inheritdoc />
public IMultiTenantContext<TTenantInfo> MultiTenantContext
{
get => AsyncLocalContext.Value ?? (AsyncLocalContext.Value = new MultiTenantContext<TTenantInfo>(null));
get => AsyncLocalContext.Value ?? (AsyncLocalContext.Value = new MultiTenantContext<TTenantInfo>(default));
private set => AsyncLocalContext.Value = value;
}

Expand Down
14 changes: 7 additions & 7 deletions src/Finbuckle.MultiTenant.Abstractions/IMultiTenantContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ public interface IMultiTenantContext
/// <summary>
/// Information about the tenant for this context.
/// </summary>
TenantInfo? TenantInfo { get; init; }
ITenantInfo? TenantInfo { get; init; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It likely doesn't concern any code I'd author, but do you need init here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, I have removed the initial accessors here


/// <summary>
/// True if a tenant has been resolved and <see cref="TenantInfo"/> is not null.
/// True if a tenant has been resolved and <see cref="ITenantInfo"/> is not null.
/// </summary>
bool IsResolved { get; }

Expand All @@ -27,17 +27,17 @@ public interface IMultiTenantContext
/// <summary>
/// Generic interface for the multi-tenant context.
/// </summary>
/// <typeparam name="TTenantInfo">The <see cref="TenantInfo"/> derived type.</typeparam>
/// <typeparam name="TTenantInfo">The <see cref="ITenantInfo"/> implementation type.</typeparam>
public interface IMultiTenantContext<TTenantInfo> : IMultiTenantContext
where TTenantInfo : TenantInfo
where TTenantInfo : ITenantInfo
{
/// <summary>
/// Information about the tenant for this context.
/// </summary>
new TTenantInfo? TenantInfo { get; init; }
new TTenantInfo? TenantInfo { get; }

/// <summary>
/// Information about the <see cref="IMultiTenantStore{TTenantInfo}"/> for this context.
/// Information about the <see cref="IMultiTenantStore{ITenantInfo}"/> for this context.
/// </summary>
StoreInfo<TTenantInfo>? StoreInfo { get; init; }
StoreInfo<TTenantInfo>? StoreInfo { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public interface IMultiTenantContextAccessor
/// <summary>
/// Provides access to the current <see cref="IMultiTenantContext{TTenantInfo}"/>.
/// </summary>
/// <typeparam name="TTenantInfo">The <see cref="TenantInfo"/> derived type.</typeparam>
/// <typeparam name="TTenantInfo">The <see cref="ITenantInfo"/> implementation type.</typeparam>
public interface IMultiTenantContextAccessor<TTenantInfo> : IMultiTenantContextAccessor
where TTenantInfo : TenantInfo
where TTenantInfo : ITenantInfo
{
/// <summary>
/// Gets the current <see cref="IMultiTenantContext{TTenantInfo}"/>.
Expand Down
Loading
Loading