diff --git a/README.md b/README.md index ef6c31e..844306e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Backend ToolkitOrleans.Multitenant +# Backend ToolkitOrleans.Multitenant Secure, flexible tenant separation for Microsoft Orleans 8 > [![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/Orleans.Multitenant?color=gold&label=NuGet:%20Orleans.Multitenant&style=plastic)](https://www.nuget.org/packages/Orleans.Multitenant)
@@ -49,6 +49,26 @@ siloBuilder ) ``` +#### Customize storage provider constructor parameters +By default, the parameters passed into the storage provider instance for a tenant are the tenant provider name (which contains the tenant Id) and the tenant options. Some storage providers may expect a different (wrapper) type for the options, or you may want to pass in additional parameters (e.g. `ClusterOptions`). + +To do this, you can pass in an optional `GrainStorageProviderParametersFactory? getProviderParameters` parameter. + +E.g. the Orleans ADO.NET storage provider constructor expects an `IOptions` instead of an `AdoNetGrainStorageOptions`. You can use `getProviderParameters` to wrap the `AdoNetGrainStorageOptions` in an `IOptions`: + +```csharp +.AddMultitenantGrainStorageAsDefault( + (silo, name) => silo.AddAdoNetGrainStorage(name, options => options.ConnectionString = sqlConnectionString), + + configureTenantOptions: (options, tenantId) => options.ConnectionString = sqlConnectionString.Replace("[DatabaseName]", tenantId, StringComparison.Ordinal), + + getProviderParameters: (services, providerName, tenantProviderName, options) => [Options.Create(options)] +) +``` +Note that you do not need to include the `tenantProviderName` in the returned provider parameters; it is added automatically. + +The parameters passed to `getProviderParameters` allow to access relevant services from DI to retrieve additional provider parameters, if needed. + ### Add multitenant streams To configure a silo to use a specific stream provider type as a named stream provider with tenant separation, use `AddMultitenantStreams`. Any Orleans stream provider can be used: ```csharp diff --git a/src/Example/Apis/Apis.csproj b/src/Example/Apis/Apis.csproj index 3d9efc4..093f493 100644 --- a/src/Example/Apis/Apis.csproj +++ b/src/Example/Apis/Apis.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Example/Apis/Foundation/Program.cs b/src/Example/Apis/Foundation/Program.cs index 03747e5..895c253 100644 --- a/src/Example/Apis/Foundation/Program.cs +++ b/src/Example/Apis/Foundation/Program.cs @@ -1,6 +1,6 @@ -using Microsoft.OpenApi.Models; -using Azure.Data.Tables; +using Azure.Data.Tables; using Orleans.Configuration; +using Microsoft.OpenApi.Models; using Orleans.Multitenant; using Orleans.Storage; using Orleans4Multitenant.Apis; @@ -12,19 +12,19 @@ .UseLocalhostClustering() .AddMultitenantCommunicationSeparation() .AddMultitenantGrainStorageAsDefault( - (silo, name) => silo.AddAzureTableGrainStorage(name, options => - options.TableServiceClient = new TableServiceClient(tableStorageConnectionString)), - // Called during silo startup, to ensure that any common dependencies - // needed for tenant-specific provider instances are initialized - - configureTenantOptions: (options, tenantId) => - { - options.TableServiceClient = new TableServiceClient(tableStorageConnectionString); - options.TableName = $"OrleansGrainState{tenantId}"; - } // Called on the first grain state access for a tenant in a silo, - // to initialize the options for the tenant-specific provider instance - // just before it is instantiated - ) + (silo, name) => silo.AddAzureTableGrainStorage(name, options => + options.TableServiceClient = new TableServiceClient(tableStorageConnectionString)), + // Called during silo startup, to ensure that any common dependencies + // needed for tenant-specific provider instances are initialized + + configureTenantOptions: (options, tenantId) => + { + options.TableServiceClient = new TableServiceClient(tableStorageConnectionString); + options.TableName = $"OrleansGrainState{tenantId}"; + } // Called on the first grain state access for a tenant in a silo, + // to initialize the options for the tenant-specific provider instance + // just before it is instantiated + ) ); // Add services to the container. diff --git a/src/Example/Services.Tenant/Services.Tenant.csproj b/src/Example/Services.Tenant/Services.Tenant.csproj index d3d4007..a0de802 100644 --- a/src/Example/Services.Tenant/Services.Tenant.csproj +++ b/src/Example/Services.Tenant/Services.Tenant.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Orleans.Multitenant/Extensions.cs b/src/Orleans.Multitenant/Extensions.cs index e84a7b9..412e1af 100644 --- a/src/Orleans.Multitenant/Extensions.cs +++ b/src/Orleans.Multitenant/Extensions.cs @@ -39,12 +39,14 @@ public static ISiloBuilder AddMultitenantCommunicationSeparation( /// This storage provider instance will not be used to store state - it is only called at silo initialization, to ensure that any shared dependencies needed by the tenant-specific storage provider instances are initialized /// /// Action to configure the supplied based on the supplied tenant ID (e.g. use the tenant ID in a storage table name to realize separate storage per tenant) + /// Optional factory to transform or add constructor parameters for tenant grain storage providers; for details see
When omitted, only is passed in /// Action to configure the /// The same instance of the for chaining public static ISiloBuilder AddMultitenantGrainStorageAsDefault( this ISiloBuilder builder, Func addStorageProvider, Action? configureTenantOptions = null, + GrainStorageProviderParametersFactory? getProviderParameters = null, Action>? configureOptions = null) where TGrainStorage : IGrainStorage where TGrainStorageOptions : class, new() @@ -53,6 +55,7 @@ public static ISiloBuilder AddMultitenantGrainStorageAsDefault { _ = addStorageProvider(builder, name); return services; }, configureTenantOptions, + getProviderParameters, configureOptions); /// @@ -70,6 +73,7 @@ public static ISiloBuilder AddMultitenantGrainStorageAsDefault /// Action to configure the supplied based on the supplied tenant ID (e.g. use the tenant ID in a storage table name to realize separate storage per tenant) + /// Optional factory to transform or add constructor parameters for tenant grain storage providers; for details see
When omitted, only is passed in /// Action to configure the /// The same instance of the for chaining public static ISiloBuilder AddMultitenantGrainStorage( @@ -77,6 +81,7 @@ public static ISiloBuilder AddMultitenantGrainStorage addStorageProvider, Action? configureTenantOptions = null, + GrainStorageProviderParametersFactory? getProviderParameters = null, Action>? configureOptions = null) where TGrainStorage : IGrainStorage where TGrainStorageOptions : class, new() @@ -85,6 +90,7 @@ public static ISiloBuilder AddMultitenantGrainStorage { _ = addStorageProvider(builder, name); return sevices; }, configureTenantOptions, + getProviderParameters, configureOptions)); /// Configure silo to use a specific stream provider type as a named stream provider, with tenant separation @@ -121,12 +127,14 @@ public static class ServiceCollectionExtensions /// This storage provider instance will not be used to store state - it is only called at silo initialization, to ensure that any shared dependencies needed by the tenant-specific storage provider instances are initialized /// /// Action to configure the supplied based on the supplied tenant ID (e.g. use the tenant ID in a storage table name to realize separate storage per tenant) + /// Optional factory to transform or add constructor parameters for tenant grain storage providers; for details see
When omitted, only is passed in /// Action to configure the /// The same instance of the for chaining public static IServiceCollection AddMultitenantGrainStorageAsDefault( this IServiceCollection services, Func addStorageProvider, Action? configureTenantOptions = null, + GrainStorageProviderParametersFactory? getProviderParameters = null, Action>? configureOptions = null) where TGrainStorage : IGrainStorage where TGrainStorageOptions : class, new() @@ -135,6 +143,7 @@ public static IServiceCollection AddMultitenantGrainStorageAsDefault @@ -152,6 +161,7 @@ public static IServiceCollection AddMultitenantGrainStorageAsDefault /// Action to configure the supplied based on the supplied tenant ID (e.g. use the tenant ID in a storage table name to realize separate storage per tenant) + /// Optional factory to transform or add constructor parameters for tenant grain storage providers; for details see
When omitted, only is passed in /// Action to configure the /// The same instance of the for chaining public static IServiceCollection AddMultitenantGrainStorage( @@ -159,6 +169,7 @@ public static IServiceCollection AddMultitenantGrainStorage addStorageProvider, Action? configureTenantOptions = null, + GrainStorageProviderParametersFactory? getProviderParameters = null, Action>? configureOptions = null) where TGrainStorage : IGrainStorage where TGrainStorageOptions : class, new() @@ -167,11 +178,29 @@ public static IServiceCollection AddMultitenantGrainStorage TenantGrainStorageFactoryFactory.Create(services, name, configureTenantOptions), + (services, name) => TenantGrainStorageFactoryFactory.Create(services, name, configureTenantOptions, getProviderParameters), configureOptions); } } +/// +/// Factory delegate, used to create parameters for a tenant grain storage provider constructor.
+/// Allows to e.g. transform the options if the provider expects a different type than ,
+/// or to retrieve an add extra parameters like +///
+/// The provider-specific grain storage options type, e.g. Orleans.Storage.MemoryGrainStorageOptions or Orleans.Storage.AzureTableStorageOptions +/// The silo services +/// The name - without the tenant id - of the provider; can be used to access named provider services that are not tenant specific +/// The name - including the tenant id - of the tenant provider; can be used to access named provider services that are tenant specific +/// The options to pass to the provider. Note that configureTenantOptions and options validation have already been executed on this +/// The tenant storage provider construction parameters to pass to DI. Don't include in these; it is added automatically +public delegate object[] GrainStorageProviderParametersFactory( + IServiceProvider services, + string providerName, + string tenantProviderName, + TGrainStorageOptions options +); + public static class GrainExtensions { /// Get a tenant stream provider from within a , for the tenant that this grain belongs to diff --git a/src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs b/src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs index 0cef3fb..7209668 100644 --- a/src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs +++ b/src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs @@ -6,16 +6,23 @@ namespace Orleans.Multitenant.Internal; static class TenantGrainStorageFactoryFactory { - public static ITenantGrainStorageFactory Create(IServiceProvider services, string name, Action? configureTenantOptions = null) - where TGrainStorage : IGrainStorage + public static ITenantGrainStorageFactory Create( + IServiceProvider services, + string name, + Action? configureTenantOptions = null, + GrainStorageProviderParametersFactory? getProviderParameters = null + ) where TGrainStorage : IGrainStorage where TGrainStorageOptions : class, new() where TGrainStorageOptionsValidator : class, IConfigurationValidator - => configureTenantOptions is null ? - ActivatorUtilities.CreateInstance>(services, name) : - ActivatorUtilities.CreateInstance>(services, name, configureTenantOptions); + { + List parameters = [name]; + if (configureTenantOptions is not null) parameters.Add(configureTenantOptions); + if (getProviderParameters is not null) parameters.Add(getProviderParameters); + return ActivatorUtilities.CreateInstance>(services, parameters: [.. parameters]); + } } -[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Class is instantiated through DI")] +[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Class is instantiated through DI")] sealed class TenantGrainStorageFactory : ITenantGrainStorageFactory where TGrainStorage : IGrainStorage where TGrainStorageOptions : class, new() @@ -23,6 +30,7 @@ sealed class TenantGrainStorageFactory? configureTenantOptions; + readonly GrainStorageProviderParametersFactory? getProviderParameters; readonly IServiceProvider services; readonly ILogger logger; @@ -41,6 +49,31 @@ public TenantGrainStorageFactory(string name, Action getProviderParameters, IServiceProvider services, ILogger logger) + { + this.name = name; + this.getProviderParameters = getProviderParameters; + this.services = services; + this.logger = logger; + } + + public TenantGrainStorageFactory(string name, Action configureTenantOptions, GrainStorageProviderParametersFactory getProviderParameters, IServiceProvider services, ILogger logger) + { + this.name = name; + this.configureTenantOptions = configureTenantOptions; + this.getProviderParameters = getProviderParameters; + this.services = services; + this.logger = logger; + } + + // The common Orleans grain storage provider implementation pattern uses a static Create method which takes a provider name parameter, + // e.g. AzureTableGrainStorageFactory.Create and AdoNetGrainStorageFactory.Create. + // These Create methods both retrieve the provider options and create the provider instance with those options. + // We need to change the provider name to include the tenant ID, + // and we need to call configureTenantOptions after retrieving the options and before the provider instance is created. + // In addition, we need a way to support providers that take other parameters than tenantProviderName and options + // To do this, we use below method instead of those Create methods, and offer optional configureTenantOptions and getProviderParameters + // parameters to allow tenant-specific and provider-specific logic. public IGrainStorage Create(string tenantId) { string tenantProviderName = string.IsNullOrEmpty(tenantId) ? name : $"{tenantId}_{name}"; @@ -56,6 +89,9 @@ public IGrainStorage Create(string tenantId) var validator = ActivatorUtilities.CreateInstance(services, options, tenantProviderName); validator.ValidateConfiguration(); - return ActivatorUtilities.CreateInstance(services, tenantProviderName, options); + List providerParameters = [tenantProviderName]; + providerParameters.AddRange(getProviderParameters?.Invoke(services, name, tenantProviderName, options) ?? [options]); + + return ActivatorUtilities.CreateInstance(services, parameters: [.. providerParameters]); } } diff --git a/src/Orleans.Multitenant/MultitenantStorageOptions.cs b/src/Orleans.Multitenant/MultitenantStorageOptions.cs index 2e16ecd..f0e46a7 100644 --- a/src/Orleans.Multitenant/MultitenantStorageOptions.cs +++ b/src/Orleans.Multitenant/MultitenantStorageOptions.cs @@ -20,7 +20,7 @@ public sealed class MultitenantStorageOptions /// /// When a grain does not belong to a tenant (because the grain was not created via the tenant aware grain factory, so it's tenant id is null) and it's state is stored in a multitenant-enabled storage provider, /// the value of is passed as the tenantId parameter of the configureTenantOptions action that is specified - /// in AddMultitenantGrainStorage methods (e.g. )
+ /// in AddMultitenantGrainStorage methods (e.g. )
/// This allows to differentiate between an empty string tenant Id and a null tenant Id in multitenant storage ///
public string TenantIdForNullTenant { get; set; } = "Null"; diff --git a/src/Orleans.Multitenant/Orleans.Multitenant.csproj b/src/Orleans.Multitenant/Orleans.Multitenant.csproj index b06bc4e..3b8ed52 100644 --- a/src/Orleans.Multitenant/Orleans.Multitenant.csproj +++ b/src/Orleans.Multitenant/Orleans.Multitenant.csproj @@ -13,7 +13,7 @@ true Orleans.Multitenant - 2.1.0 + 2.2.8 Orleans Multitenant Secure, flexible tenant separation for Microsoft Orleans 8 VincentH.NET;Applicita diff --git a/src/Orleans.Multitenant/Readme.md b/src/Orleans.Multitenant/Readme.md index cd9bc9e..3b2069e 100644 --- a/src/Orleans.Multitenant/Readme.md +++ b/src/Orleans.Multitenant/Readme.md @@ -2,4 +2,4 @@ Docs: see the [repo readme](https://github.com/Applicita/Orleans.Multitenant#readme) and the inline C# documentation. All public Orleans.Multitenant API's come with full inline documentation. -[Release Notes](https://github.com/Applicita/Orleans.Multitenant/releases/tag/2-1-0) \ No newline at end of file +[Release Notes](https://github.com/Applicita/Orleans.Multitenant/releases/tag/2-2-8) \ No newline at end of file