Skip to content

Commit

Permalink
feat: better tenant resolution events (#897)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `OnTenantResolved` and `OnTenantNotResolved` are no longer used. Use the `OnStrategyResolveCompleted`, `OnStoreResolveCompleted`, and `OnTenantResolveCompleted` events instead.
  • Loading branch information
AndrewTriesToCode authored Nov 10, 2024
1 parent 8728447 commit 956ca36
Show file tree
Hide file tree
Showing 22 changed files with 312 additions and 194 deletions.
2 changes: 1 addition & 1 deletion docs/Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Internally `WithPerTenantAuthentication()` makes use of
For example, if you want to configure JWT tokens so that each tenant has a
different recognized authority for token validation we can add a field to the
`ITenantInfo` implementation and configure the option per-tenant. Any options configured will overwrite earlier
configureations:
configurations:

```csharp
builder.Services.AddMultiTenant<TenantInfo>()
Expand Down
79 changes: 51 additions & 28 deletions docs/ConfigurationAndUsage.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Finbuckle.MultiTenant uses the standard application builder pattern for its configuration. In addition to adding the
services, configuration for one or more [MultiTenant Stores](Stores) and [MultiTenant Strategies](Strategies) are
required:
required. A typical configuration for an ASP.NET Core application might look like this:

```csharp
using Finbuckle.MultiTenant;
Expand All @@ -30,10 +30,10 @@ app.Run();

## Adding the Finbuckle.MultiTenant Service

Use the `AddMultiTenant<TTenantInfo>` extension method on `IServiceCollection` to register the basic dependencies
needed by the library. It returns a `MultiTenantBuilder<TTenantInfo>` instance on which the methods below can be called
for further configuration. Each of these methods returns the same `MultiTenantBuilder<TTenantInfo>` instance allowing
for chaining method calls.
Use the `AddMultiTenant<TTenantInfo>` extension method on `IServiceCollection` to register the basic dependencies needed
by the library. It returns a `MultiTenantBuilder<TTenantInfo>` instance on which the methods below can be called for
further configuration. Each of these methods returns the same `MultiTenantBuilder<TTenantInfo>` instance allowing for
chaining method calls.

## Configuring the Service

Expand Down Expand Up @@ -70,17 +70,42 @@ Configures support for per-tenant authentication. See [Per-Tenant Authentication

## Per-Tenant Options

Finbuckle.MultiTenant integrates with the
standard [.NET Options pattern](https://learn.microsoft.com/en-us/dotnet/core/extensions/options) (see also the [ASP.NET
Core Options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) and lets apps
customize options distinctly for each tenant. See [Per-Tenant Options](Options) for more details.

## Tenant Resolution

Most of the capability enabled by Finbuckle.MultiTenant is utilized through its middleware and use
the [Options pattern with per-tenant options](Options). For web applications the middleware will resolve the app's
current tenant on each request using the configured strategies and stores, and the per-tenant
options will alter the app's behavior as dependency injection passes the options to app components.
Finbuckle.MultiTenant id designed to integrate with the
standard [.NET Options pattern](https://learn.microsoft.com/en-us/dotnet/core/extensions/options) (see also
the [ASP.NET Core Options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options)) and
lets apps customize options distinctly for each tenant. See [Per-Tenant Options](Options) for more details.

## Tenant Resolution and Usage

Finbuckle.MultiTenant will perform tenant resolution using the context, strategies, and stores as configured.

The context will determine on the type of app. For an ASP.NET Core web app the context is the `HttpContext` for each
request and a tenant will be resolved for each request. For other types of apps the context will be different. For
example, a console app might resolve the tenant once at startup or a background service monitoring a queue might resolve
the tenant for each message it receives.

Tenant resolution is performed by the `TenantResolver` class. The class requires a list of strategies and a list of
stores as well as some options. The class will try each strategy generally in the order added, but static and per-tenant
authentication strategies will run at a lower priority. If a strategy returns a tenant identifier then each store will
be queried in the order they were added. The first store to return a `TenantInfo`
object will determine the resolved tenant. If no store returns a `TenantInfo` object then the next strategy will be
tried and so on. The `UseMultiTenant` middleware for ASP.NET Core uses `TenantResolver`
internally.

The `TenantResolver` options are configured in the `AddMultiTenant<TTenantInfo>` method with the following properties:

- `IgnoredIdentifiers` - A list of tenant identifiers that should be ignored by the resolver.
- `Events` - A set of events that can be used to hook into the resolution process:
- `OnStrategyResolveCompleted` - Called after each strategy has attempted to resolve a tenant identifier. The
`IdentifierFound` property will be `true` if the strategy resolved a tenant identifier. The `Identifier` property
contains the resolved tenant identifier and can be changed by the event handler to override the strategy's result.
- `OnStoreResolveCompleted` - Called after each store has attempted to resolve a tenant. The `TenantFound` property
will be `true` if the store resolved a tenant. The `TenantInfo` property contains the resolved tenant and can be
changed by the event handler to override the store's result. A non-null `TenantInfo` object will stop the resolver
from trying additional strategies and stores.
- `OnTenantResolveCompleted` - Called once after a tenant has been resolved. The `MultiTenantContext` property
contains the resolved multi-tenant context and can be changed by the event handler to override the resolver's
result.

## Getting the Current Tenant

Expand All @@ -95,8 +120,8 @@ There are several ways an app can see the current tenant:
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.
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.

> Prior versions of Finbuckle.MultiTenant also exposed `IMultiTenantContext`, `ITenantInfo`, and their implementations
> via dependency injection. This was removed as these are not actual services, similar to
Expand All @@ -109,9 +134,8 @@ For web apps these convenience methods are also available:

* `GetMultiTenantContext<TTenantInfo>`

Use this `HttpContext` extension method to get the `MultiTenantContext<TTenantInfo>` instance for the current
request. This should be preferred to `IMultiTenantContextAccessor` or `IMultiTenantContextAccessor<TTenantInfo>` when
possible.
Use this `HttpContext` extension method to get the `MultiTenantContext<TTenantInfo>` instance for the current request.
This should be preferred to `IMultiTenantContextAccessor` or `IMultiTenantContextAccessor<TTenantInfo>` when possible.

```csharp
var tenantInfo = HttpContext.GetMultiTenantContext<TenantInfo>().TenantInfo;
Expand All @@ -125,17 +149,16 @@ For web apps these convenience methods are also available:
}
```

* `TrySetTenantInfo<TTenantInfo>`
* `SetTenantInfo<TTenantInfo>`

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.
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
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`.
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(...);
Expand Down
2 changes: 1 addition & 1 deletion docs/CoreConcepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ when needed via the tenant `Id`.

The `MultiTenantContext<TTenantInfo>` contains information about the current tenant.

* Implements `IMultiTenantContext` and `IMultiTenantContext<TTenantInfo>` which can be obtained from depdency injection.
* 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.
* Can be obtained in ASP.NET Core by calling the `GetMultiTenantContext()` method on the current request's `HttpContext`
Expand Down
2 changes: 1 addition & 1 deletion docs/EFCore.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ public override async Task<int> 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`

Expand Down
20 changes: 9 additions & 11 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ $ dotnet add package Finbuckle.MultiTenant.AspNetCore

## Basic Configuration

Finbuckle.MultiTenant is simple to get started with. Below is a sample app that configured to use the subdomain as the
tenant identifier and the
app's [configuration](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) (most likely from
a `appsettings.json` file)' as the source of tenant details.
Finbuckle.MultiTenant is simple to get started with. Below is a sample app configured to use the subdomain as the tenant
identifier and the app's [configuration](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) (
most likely from a`appsettings.json` file) as the source of tenant details.

```csharp
using Finbuckle.MultiTenant;
Expand Down Expand Up @@ -51,9 +50,8 @@ This line registers the base services and designates `TenantInfo` as the class t
runtime.

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

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

Expand All @@ -78,8 +76,8 @@ ways.
`app.UseMultiTenant()`

This line configures the middleware which resolves the tenant using the registered strategies, stores, and other
settings. Be sure to call it before other middleware which will use per-tenant
functionality, such as `UseAuthentication()`.
settings. Be sure to call it before other middleware which will use per-tenant functionality, such as
`UseAuthentication()`.

## Basic Usage

Expand All @@ -100,8 +98,8 @@ if(tenantInfo != null)
The type of the `TenantInfo` property depends on the type passed when calling `AddMultiTenant<TTenantInfo>` during
configuration. If the current tenant could not be determined then `TenantInfo` will be null.

The `ITenantInfo` instance and the typed instance are also available using
the `IMultiTenantContextAccessor<TTenantinfo>` interface which is available via dependency injection.
The `ITenantInfo` instance and the typed instance are also available using the
`IMultiTenantContextAccessor<TTenantinfo>` interface which is available via dependency injection.

See [Configuration and Usage](ConfigurationAndUsage) for more information.

Expand Down
2 changes: 1 addition & 1 deletion docs/Identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ calls into the database instead of your own code.

See the Identity data isolation sample projects in
the [GitHub repository](https://github.com/Finbuckle/Finbuckle.MultiTenant/tree/master/samples) for examples on how to
use Finbuckle.MultiTenant with ASP.NET Core Identity. These samples illustrates how to isolate the tenant Identity data
use Finbuckle.MultiTenant with ASP.NET Core Identity. These samples illustrate how to isolate the tenant Identity data
and integrate the Identity UI to work with a route multi-tenant strategy.

## Configuration
Expand Down
2 changes: 1 addition & 1 deletion docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ multi-tenant capability with minimal code changes.

Finbuckle.MultiTenant integrates with the
standard [.NET Options pattern](https://learn.microsoft.com/en-us/dotnet/core/extensions/options) (see also the [ASP.NET
Core Options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options) and lets apps
Core Options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options)) and lets apps
customize options distinctly for each tenant.

Note: For authentication options, Finbuckle.MultiTenant provides special support
Expand Down
19 changes: 10 additions & 9 deletions docs/Stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ Currently `InMemoryStore`, `ConfigurationStore`, and `EFCoreStore` implement `Ge
Uses a `ConcurrentDictionary<string, TenantInfo>` as the underlying store.

Configure by calling `WithInMemoryStore` after `AddMultiTenant<TTenantInfo>`. By default the store is empty and the
tenant identifier matching is case insensitive. Case insensitive is generally preferred. An overload
Configure by calling `WithInMemoryStore` after `AddMultiTenant<TTenantInfo>`. By default, the store is empty and the
tenant identifier matching is case-insensitive. Case-insensitive is generally preferred. An overload
of `WithInMemoryStore` accepts an `Action<InMemoryStoreOptions>` delegate to configure the store further:

```csharp
Expand All @@ -93,14 +93,14 @@ builder.Services.AddMultiTenant<TenantInfo>()
Uses an
app's [configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) as
the underlying store. Most of the sample projects use this store for simplicity. This store is case insensitive when
the underlying store. Most of the sample projects use this store for simplicity. This store is case-insensitive when
retrieving tenant information by tenant identifier.

This store is read-only and calls to `TryAddAsync`, `TryUpdateAsync`, and `TryRemoveAsync` will throw
a `NotImplementedException`. However, if the app is configured to reload its configuration if the source changes,
e.g. `appsettings.json` is updated, then the MultiTenant store will reflect the change.

Configure by calling `WithConfigurationStore` after `AddMultiTenant<TTenantInfo>`. By default it will use the root
Configure by calling `WithConfigurationStore` after `AddMultiTenant<TTenantInfo>`. By default, it will use the root
configuration object and search for a section named "Finbuckle:MultiTenant:Stores:ConfigurationStore". An overload
of `WithConfigurationStore` allows for a different base
configuration object or section name if needed.
Expand Down Expand Up @@ -182,7 +182,8 @@ builder.Services.AddMultiTenant<TenantInfo>()
.WithEFCoreStore<MultiTenantStoreDbContext,TenantInfo>()...
```

In addition the `IMultiTenantStore` interface methods, the database context can be used to modify data in the same way
In addition to the `IMultiTenantStore` interface methods, the database context can be used to modify data in the
same way
Entity Framework Core works with any database context which can offer richer functionality.

## Http Remote Store
Expand All @@ -193,12 +194,12 @@ Sends the tenant identifier, provided by the multitenant strategy, to an http(s)
in return.

The [Http Remote Store Sample](https://github.com/Finbuckle/Finbuckle.MultiTenant/tree/v6.9.1/samples/ASP.NET%20Core%203/HttpRemoteStoreSample)
projects demonstrate this store. This store is usually case insensitive when retrieving tenant information by tenant identifier, but the remote server might be more restrictive.
projects demonstrate this store. This store is usually case-insensitive when retrieving tenant information by tenant identifier, but the remote server might be more restrictive.

Make sure the tenant info type will support basic JSON serialization and deserialization via `System.Text.Json`.
This strategy will attempt to deserialize the tenant using the [System.Text.Json web defaults](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-configure-options?pivots=dotnet-6-0#web-defaults-for-jsonserializeroptions).
For a successfully request, the store expects a 200 response code and a json body with properties `Id`, `Identifier`
For a successful request, the store expects a 200 response code and a json body with properties `Id`, `Identifier`
, `Name`, and other properties which will be mapped into a `TenantInfo` object with the type
passed to `AddMultiTenant<TTenantInfo>`.

Expand Down Expand Up @@ -253,13 +254,13 @@ implementation. A sliding expiration is also supported. The store does not inter
Make sure the tenant info type will support basic JSON serialization and deserialization via `System.Text.Json`.
This strategy will attempt to deserialize the tenant using the [System.Text.Json web defaults](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-configure-options?pivots=dotnet-6-0#web-defaults-for-jsonserializeroptions).
Each tenant info instance is actually stored twice in the cache, once using the Tenant Id as the key and another using
Each tenant info instance is actually stored twice in the cache, once using the Tenant ID as the key and another using
the Tenant Identifier as the key. Calls to `TryAddAsync`, `TryUpdateAsync`, and `TryRemoveAsync` will keep these dual
cache entries synced.

This store does not implement `GetAllAsync`.

Configure by calling `WithDistributedCacheStore` after `AddMultiTenant<TTenantInfo>`. By default entries do not expire,
Configure by calling `WithDistributedCacheStore` after `AddMultiTenant<TTenantInfo>`. By default, entries do not expire,
but a `TimeSpan` can be passed to be used as a sliding
expiration:

Expand Down
Loading

0 comments on commit 956ca36

Please sign in to comment.