From 3eb45960963789762b3e70c15876995fc4054d7f Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sun, 10 Nov 2024 15:59:36 -0700 Subject: [PATCH] docs updates --- docs/Authentication.md | 2 +- docs/ConfigurationAndUsage.md | 79 ++++++++++++------- docs/CoreConcepts.md | 2 +- docs/GettingStarted.md | 20 +++-- docs/Identity.md | 2 +- docs/Options.md | 2 +- docs/Stores.md | 19 ++--- docs/Strategies.md | 14 ++-- .../Strategies/StaticStrategy.cs | 4 +- src/Finbuckle.MultiTenant/TenantResolver.cs | 32 ++++---- 10 files changed, 100 insertions(+), 76 deletions(-) diff --git a/docs/Authentication.md b/docs/Authentication.md index 908873e4..3eb0fda9 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -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() diff --git a/docs/ConfigurationAndUsage.md b/docs/ConfigurationAndUsage.md index 47e1eb29..69d792af 100644 --- a/docs/ConfigurationAndUsage.md +++ b/docs/ConfigurationAndUsage.md @@ -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; @@ -30,10 +30,10 @@ app.Run(); ## Adding the Finbuckle.MultiTenant Service -Use the `AddMultiTenant` extension method on `IServiceCollection` to register the basic dependencies -needed by the library. It returns a `MultiTenantBuilder` instance on which the methods below can be called -for further configuration. Each of these methods returns the same `MultiTenantBuilder` instance allowing -for chaining method calls. +Use the `AddMultiTenant` extension method on `IServiceCollection` to register the basic dependencies needed +by the library. It returns a `MultiTenantBuilder` instance on which the methods below can be called for +further configuration. Each of these methods returns the same `MultiTenantBuilder` instance allowing for +chaining method calls. ## Configuring the Service @@ -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` 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 @@ -95,8 +120,8 @@ There are several ways an app can see the current tenant: extension `GetMultiTenantContext` 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` in use cases where `HttpContext` is available. + useful in advanced scenarios and should be used with caution. Prefer using the `HttpContext` extension method + `TrySetTenantInfo` 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 @@ -109,9 +134,8 @@ For web apps these convenience methods are also available: * `GetMultiTenantContext` - Use this `HttpContext` extension method to get the `MultiTenantContext` instance for the current - request. This should be preferred to `IMultiTenantContextAccessor` or `IMultiTenantContextAccessor` when - possible. + Use this `HttpContext` extension method to get the `MultiTenantContext` instance for the current request. + This should be preferred to `IMultiTenantContextAccessor` or `IMultiTenantContextAccessor` when possible. ```csharp var tenantInfo = HttpContext.GetMultiTenantContext().TenantInfo; @@ -125,17 +149,16 @@ For web apps these convenience methods are also available: } ``` -* `TrySetTenantInfo` +* `SetTenantInfo` - 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` 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` to `null`. ```csharp var newTenantInfo = new TenantInfo(...); diff --git a/docs/CoreConcepts.md b/docs/CoreConcepts.md index b3750f65..2e19f4d2 100644 --- a/docs/CoreConcepts.md +++ b/docs/CoreConcepts.md @@ -29,7 +29,7 @@ when needed via the tenant `Id`. The `MultiTenantContext` contains information about the current tenant. -* Implements `IMultiTenantContext` and `IMultiTenantContext` which can be obtained from depdency injection. +* Implements `IMultiTenantContext` and `IMultiTenantContext` 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` diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index f29f57f3..56e17e54 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -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; @@ -51,9 +50,8 @@ This line registers the base services and designates `TenantInfo` as the class t runtime. The type parameter for `AddMultiTenant` 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`. @@ -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 @@ -100,8 +98,8 @@ if(tenantInfo != null) The type of the `TenantInfo` property depends on the type passed when calling `AddMultiTenant` 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` interface which is available via dependency injection. +The `ITenantInfo` instance and the typed instance are also available using the +`IMultiTenantContextAccessor` interface which is available via dependency injection. See [Configuration and Usage](ConfigurationAndUsage) for more information. diff --git a/docs/Identity.md b/docs/Identity.md index 3583d1af..da740b29 100644 --- a/docs/Identity.md +++ b/docs/Identity.md @@ -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 diff --git a/docs/Options.md b/docs/Options.md index 03e4b889..22397240 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -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 diff --git a/docs/Stores.md b/docs/Stores.md index 3a956ddf..3f82e4ac 100644 --- a/docs/Stores.md +++ b/docs/Stores.md @@ -67,8 +67,8 @@ Currently `InMemoryStore`, `ConfigurationStore`, and `EFCoreStore` implement `Ge Uses a `ConcurrentDictionary` as the underlying store. -Configure by calling `WithInMemoryStore` after `AddMultiTenant`. 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`. 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` delegate to configure the store further: ```csharp @@ -93,14 +93,14 @@ builder.Services.AddMultiTenant() 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`. By default it will use the root +Configure by calling `WithConfigurationStore` after `AddMultiTenant`. 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. @@ -182,7 +182,8 @@ builder.Services.AddMultiTenant() .WithEFCoreStore()... ``` -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 @@ -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`. @@ -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`. By default entries do not expire, +Configure by calling `WithDistributedCacheStore` after `AddMultiTenant`. By default, entries do not expire, but a `TimeSpan` can be passed to be used as a sliding expiration: diff --git a/docs/Strategies.md b/docs/Strategies.md index d098a3a4..473f36f9 100644 --- a/docs/Strategies.md +++ b/docs/Strategies.md @@ -45,7 +45,7 @@ type is for several instances of `DelegateStrategy` utilizing distinct logic or > NuGet package: Finbuckle.MultiTenant Always uses the same identifier to resolve the tenant. Often useful in testing or to resolve to a fallback or default -tenant by registering the strategy last. +tenant. This strategy will run last no matter where it is configured. Configure by calling `WithStaticStrategy` after `AddMultiTenant` and passing in the identifier to use for tenant resolution: @@ -126,15 +126,15 @@ builder.Services.AddMultiTenant() Be aware that relative links to static files will be impacted so css files and other static resources may need to be referenced using absolute urls. Alternatively, you can place the `UseStaticFiles` middleware after -the `UseMultiTenant` middware in the app pipeline configuration. +the `UseMultiTenant` middleware in the app pipeline configuration. ## Claim Strategy > NuGet package: Finbuckle.MultiTenant.AspNetCore -Uses a claim to determine the tenant identifier. By default the first claim value with type `__tenant__` is used, but a +Uses a claim to determine the tenant identifier. By default, the first claim value with type `__tenant__` is used, but a custom type name can also be used. This strategy uses the default authentication scheme, which is usually cookie based, -but does not go so far as to set `HttpContext.User`. Thus the ASP.NET Core authentication middleware should still be +but does not go so far as to set `HttpContext.User`. Thus, the ASP.NET Core authentication middleware should still be used as normal, and in most use cases should come after `UseMultiTenant`. Note that this strategy is does not work well with per-tenant cookie names since it must know the cookie name before the @@ -185,7 +185,7 @@ tenant without invoking the expensive strategy. Uses the `__tenant__` route parameter (or a specified route parameter) to determine the tenant. For example, a request to "https://www.example.com/initech/home/" and a route configuration of `{__tenant__}/{controller=Home}/{action=Index}` would use "initech" as the identifier when resolving the tenant. The `__tenant__` parameter can be placed anywhere in -the route path configuration. If explicity calling `UseRouting` in your app pipline make sure to place it +the route path configuration. If explicitly calling `UseRouting` in your app pipeline make sure to place it before `WithRouteStrategy`. Configure by calling `WithRouteStrategy` after `AddMultiTenant`. A custom route parameter can also be @@ -209,7 +209,7 @@ app.UseMultiTenant(); > NuGet package: Finbuckle.MultiTenant.AspNetCore -Uses request's host value to determine the tenant. By default the first host segment is used. For example, a request +Uses request's host value to determine the tenant. By default, the first host segment is used. For example, a request to "https://initech.example.com/abc123" would use "initech" as the identifier when resolving the tenant. This strategy can be difficult to use in a development environment. Make sure the development system is configured properly to allow subdomains on `localhost`. This strategy is configured as a singleton. @@ -246,7 +246,7 @@ builder.Services.AddMultiTenant() > NuGet package: Finbuckle.MultiTenant.AspNetCore -Uses an HTTP request header to determine the tenant identifier. By default the header with key `__tenant__` is used, but +Uses an HTTP request header to determine the tenant identifier. By default, the header with key `__tenant__` is used, but a custom key can also be used. Configure by calling `WithHeaderStrategy` after `AddMultiTenant`. An overload to accept a custom claim type diff --git a/src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs b/src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs index 56f7ad0a..a30b023a 100644 --- a/src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs +++ b/src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs @@ -7,9 +7,11 @@ namespace Finbuckle.MultiTenant.Strategies; public class StaticStrategy : IMultiTenantStrategy { + // internal for testing + // ReSharper disable once MemberCanBePrivate.Global internal readonly string Identifier; - public int Priority { get => -1000; } + public int Priority => -1000; public StaticStrategy(string identifier) { diff --git a/src/Finbuckle.MultiTenant/TenantResolver.cs b/src/Finbuckle.MultiTenant/TenantResolver.cs index 3b187538..f9122096 100644 --- a/src/Finbuckle.MultiTenant/TenantResolver.cs +++ b/src/Finbuckle.MultiTenant/TenantResolver.cs @@ -58,11 +58,11 @@ public async Task> ResolveAsync(object context) var wrappedStrategy = new MultiTenantStrategyWrapper(strategy, strategyLogger); identifier = await wrappedStrategy.GetIdentifierAsync(context); - var strategyRanContext = new StrategyResolveCompletedContext { Context = context, Strategy = strategy, Identifier = identifier }; - await options.CurrentValue.Events.OnStrategyResolveCompleted(strategyRanContext); - if(identifier is not null && strategyRanContext.Identifier is null) - tenantResoloverLogger.LogDebug("OnStrategyCompleted set non-null Identifier to null"); - identifier = strategyRanContext.Identifier; + var strategyResolveCompletedContext = new StrategyResolveCompletedContext { Context = context, Strategy = strategy, Identifier = identifier }; + await options.CurrentValue.Events.OnStrategyResolveCompleted(strategyResolveCompletedContext); + if(identifier is not null && strategyResolveCompletedContext.Identifier is null) + tenantResoloverLogger.LogDebug("OnStrategyResolveCompleted set non-null Identifier to null"); + identifier = strategyResolveCompletedContext.Identifier; if (options.CurrentValue.IgnoredIdentifiers.Contains(identifier, StringComparer.OrdinalIgnoreCase)) { @@ -80,18 +80,18 @@ public async Task> ResolveAsync(object context) var wrappedStore = new MultiTenantStoreWrapper(store, storeLogger); var tenantInfo = await wrappedStore.TryGetByIdentifierAsync(identifier); - var storeLookupCompletedContext = new StoreResolveCompletedContext { Store = store, Identifier = identifier, TenantInfo = tenantInfo }; - await options.CurrentValue.Events.OnStoreResolveCompleted(storeLookupCompletedContext); - if(tenantInfo is not null && storeLookupCompletedContext.TenantInfo is null) - tenantResoloverLogger.LogDebug("OnStoreLookupCompleted set non-null TenantInfo to null"); - tenantInfo = storeLookupCompletedContext.TenantInfo; + var storeResolveCompletedContext = new StoreResolveCompletedContext { Store = store, Identifier = identifier, TenantInfo = tenantInfo }; + await options.CurrentValue.Events.OnStoreResolveCompleted(storeResolveCompletedContext); + if(tenantInfo is not null && storeResolveCompletedContext.TenantInfo is null) + tenantResoloverLogger.LogDebug("OnStoreResolveCompleted set non-null TenantInfo to null"); + tenantInfo = storeResolveCompletedContext.TenantInfo; - if (tenantInfo == null) - continue; - - mtc.StoreInfo = new StoreInfo { Store = store, StoreType = store.GetType() }; - mtc.StrategyInfo = new StrategyInfo { Strategy = strategy, StrategyType = strategy.GetType() }; - mtc.TenantInfo = tenantInfo; + if (tenantInfo != null) + { + mtc.StoreInfo = new StoreInfo { Store = store, StoreType = store.GetType() }; + mtc.StrategyInfo = new StrategyInfo { Strategy = strategy, StrategyType = strategy.GetType() }; + mtc.TenantInfo = tenantInfo; + } // no longer check stores if tenant is resolved if(mtc.IsResolved)