Skip to content

Commit

Permalink
Merge pull request #8 from Applicita/feature-custom-provider-parameters
Browse files Browse the repository at this point in the history
Feature: custom storage provider parameters
  • Loading branch information
VincentH-Net authored Aug 19, 2024
2 parents a575640 + a287710 commit cfc2c21
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 29 deletions.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# <img src="img/CSharp-Toolkit-Icon.png" alt="Backend Toolkit" width="64px" />Orleans.Multitenant
# <img src="img/CSharp-Toolkit-Icon.png" alt="Backend Toolkit" width="64px" />Orleans.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)<br />
Expand Down Expand Up @@ -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<TGrainStorageOptions>? getProviderParameters` parameter.

E.g. the Orleans ADO.NET storage provider constructor expects an `IOptions<AdoNetGrainStorageOptions>` instead of an `AdoNetGrainStorageOptions`. You can use `getProviderParameters` to wrap the `AdoNetGrainStorageOptions` in an `IOptions<AdoNetGrainStorageOptions>`:

```csharp
.AddMultitenantGrainStorageAsDefault<AdoNetGrainStorage, AdoNetGrainStorageOptions, AdoNetGrainStorageOptionsValidator>(
(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
Expand Down
2 changes: 1 addition & 1 deletion src/Example/Apis/Apis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<PackageReference Include="Microsoft.Orleans.Persistence.AzureStorage" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Persistence.Memory" Version="8.1.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.1.0" />
<PackageReference Include="Orleans.Multitenant" Version="2.1.0" />
<PackageReference Include="Orleans.Multitenant" Version="2.2.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
Expand Down
30 changes: 15 additions & 15 deletions src/Example/Apis/Foundation/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,19 +12,19 @@
.UseLocalhostClustering()
.AddMultitenantCommunicationSeparation()
.AddMultitenantGrainStorageAsDefault<AzureTableGrainStorage, AzureTableStorageOptions, AzureTableGrainStorageOptionsValidator>(
(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.
Expand Down
2 changes: 1 addition & 1 deletion src/Example/Services.Tenant/Services.Tenant.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.1.0" />
<PackageReference Include="Orleans.Multitenant" Version="2.1.0" />
<PackageReference Include="Orleans.Multitenant" Version="2.2.8" />
</ItemGroup>

<ItemGroup>
Expand Down
31 changes: 30 additions & 1 deletion src/Orleans.Multitenant/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// </param>
/// <param name="configureTenantOptions">Action to configure the supplied <typeparamref name="TGrainStorageOptions"/> based on the supplied tenant ID (e.g. use the tenant ID in a storage table name to realize separate storage per tenant)</param>
/// <param name="getProviderParameters">Optional factory to transform or add constructor parameters for tenant grain storage providers; for details see <see cref="GrainStorageProviderParametersFactory{TGrainStorageOptions}"/><br />When omitted, only <typeparamref name="TGrainStorageOptions"/> is passed in</param>
/// <param name="configureOptions">Action to configure the <see cref="MultitenantStorageOptions"/></param>
/// <returns>The same instance of the <see cref="ISiloBuilder"/> for chaining</returns>
public static ISiloBuilder AddMultitenantGrainStorageAsDefault<TGrainStorage, TGrainStorageOptions, TGrainStorageOptionsValidator>(
this ISiloBuilder builder,
Func<ISiloBuilder, string, ISiloBuilder> addStorageProvider,
Action<TGrainStorageOptions, string>? configureTenantOptions = null,
GrainStorageProviderParametersFactory<TGrainStorageOptions>? getProviderParameters = null,
Action<OptionsBuilder<MultitenantStorageOptions>>? configureOptions = null)
where TGrainStorage : IGrainStorage
where TGrainStorageOptions : class, new()
Expand All @@ -53,6 +55,7 @@ public static ISiloBuilder AddMultitenantGrainStorageAsDefault<TGrainStorage, TG
ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME,
(services, name) => { _ = addStorageProvider(builder, name); return services; },
configureTenantOptions,
getProviderParameters,
configureOptions);

/// <summary>
Expand All @@ -70,13 +73,15 @@ public static ISiloBuilder AddMultitenantGrainStorageAsDefault<TGrainStorage, TG
/// 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
/// </param>
/// <param name="configureTenantOptions">Action to configure the supplied <typeparamref name="TGrainStorageOptions"/> based on the supplied tenant ID (e.g. use the tenant ID in a storage table name to realize separate storage per tenant)</param>
/// <param name="getProviderParameters">Optional factory to transform or add constructor parameters for tenant grain storage providers; for details see <see cref="GrainStorageProviderParametersFactory{TGrainStorageOptions}"/><br />When omitted, only <typeparamref name="TGrainStorageOptions"/> is passed in</param>
/// <param name="configureOptions">Action to configure the <see cref="MultitenantStorageOptions"/></param>
/// <returns>The same instance of the <see cref="ISiloBuilder"/> for chaining</returns>
public static ISiloBuilder AddMultitenantGrainStorage<TGrainStorage, TGrainStorageOptions, TGrainStorageOptionsValidator>(
this ISiloBuilder builder,
string name,
Func<ISiloBuilder, string, ISiloBuilder> addStorageProvider,
Action<TGrainStorageOptions, string>? configureTenantOptions = null,
GrainStorageProviderParametersFactory<TGrainStorageOptions>? getProviderParameters = null,
Action<OptionsBuilder<MultitenantStorageOptions>>? configureOptions = null)
where TGrainStorage : IGrainStorage
where TGrainStorageOptions : class, new()
Expand All @@ -85,6 +90,7 @@ public static ISiloBuilder AddMultitenantGrainStorage<TGrainStorage, TGrainStora
name,
(sevices, name) => { _ = addStorageProvider(builder, name); return sevices; },
configureTenantOptions,
getProviderParameters,
configureOptions));

/// <summary>Configure silo to use a specific stream provider type as a named stream provider, with tenant separation</summary>
Expand Down Expand Up @@ -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
/// </param>
/// <param name="configureTenantOptions">Action to configure the supplied <typeparamref name="TGrainStorageOptions"/> based on the supplied tenant ID (e.g. use the tenant ID in a storage table name to realize separate storage per tenant)</param>
/// <param name="getProviderParameters">Optional factory to transform or add constructor parameters for tenant grain storage providers; for details see <see cref="GrainStorageProviderParametersFactory{TGrainStorageOptions}"/><br />When omitted, only <typeparamref name="TGrainStorageOptions"/> is passed in</param>
/// <param name="configureOptions">Action to configure the <see cref="MultitenantStorageOptions"/></param>
/// <returns>The same instance of the <see cref="IServiceCollection"/> for chaining</returns>
public static IServiceCollection AddMultitenantGrainStorageAsDefault<TGrainStorage, TGrainStorageOptions, TGrainStorageOptionsValidator>(
this IServiceCollection services,
Func<IServiceCollection, string, IServiceCollection> addStorageProvider,
Action<TGrainStorageOptions, string>? configureTenantOptions = null,
GrainStorageProviderParametersFactory<TGrainStorageOptions>? getProviderParameters = null,
Action<OptionsBuilder<MultitenantStorageOptions>>? configureOptions = null)
where TGrainStorage : IGrainStorage
where TGrainStorageOptions : class, new()
Expand All @@ -135,6 +143,7 @@ public static IServiceCollection AddMultitenantGrainStorageAsDefault<TGrainStora
ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME,
addStorageProvider,
configureTenantOptions,
getProviderParameters,
configureOptions);

/// <summary>
Expand All @@ -152,13 +161,15 @@ public static IServiceCollection AddMultitenantGrainStorageAsDefault<TGrainStora
/// 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
/// </param>
/// <param name="configureTenantOptions">Action to configure the supplied <typeparamref name="TGrainStorageOptions"/> based on the supplied tenant ID (e.g. use the tenant ID in a storage table name to realize separate storage per tenant)</param>
/// <param name="getProviderParameters">Optional factory to transform or add constructor parameters for tenant grain storage providers; for details see <see cref="GrainStorageProviderParametersFactory{TGrainStorageOptions}"/><br />When omitted, only <typeparamref name="TGrainStorageOptions"/> is passed in</param>
/// <param name="configureOptions">Action to configure the <see cref="MultitenantStorageOptions"/></param>
/// <returns>The same instance of the <see cref="IServiceCollection"/> for chaining</returns>
public static IServiceCollection AddMultitenantGrainStorage<TGrainStorage, TGrainStorageOptions, TGrainStorageOptionsValidator>(
this IServiceCollection services,
string name,
Func<IServiceCollection, string, IServiceCollection> addStorageProvider,
Action<TGrainStorageOptions, string>? configureTenantOptions = null,
GrainStorageProviderParametersFactory<TGrainStorageOptions>? getProviderParameters = null,
Action<OptionsBuilder<MultitenantStorageOptions>>? configureOptions = null)
where TGrainStorage : IGrainStorage
where TGrainStorageOptions : class, new()
Expand All @@ -167,11 +178,29 @@ public static IServiceCollection AddMultitenantGrainStorage<TGrainStorage, TGrai
ArgumentNullException.ThrowIfNull(addStorageProvider);
return addStorageProvider(services, name).AddMultitenantGrainStorage(
name,
(services, name) => TenantGrainStorageFactoryFactory.Create<TGrainStorage, TGrainStorageOptions, TGrainStorageOptionsValidator>(services, name, configureTenantOptions),
(services, name) => TenantGrainStorageFactoryFactory.Create<TGrainStorage, TGrainStorageOptions, TGrainStorageOptionsValidator>(services, name, configureTenantOptions, getProviderParameters),
configureOptions);
}
}

/// <summary>
/// Factory delegate, used to create parameters for a tenant grain storage provider constructor.<br />
/// Allows to e.g. transform the options if the provider expects a different type than <typeparamref name="TGrainStorageOptions"/>,<br />
/// or to retrieve an add extra parameters like <see cref="Configuration.ClusterOptions" />
/// </summary>
/// <typeparam name="TGrainStorageOptions">The provider-specific grain storage options type, e.g. Orleans.Storage.MemoryGrainStorageOptions or Orleans.Storage.AzureTableStorageOptions</typeparam>
/// <param name="services">The silo services</param>
/// <param name="providerName">The name - without the tenant id - of the provider; can be used to access named provider services that are not tenant specific</param>
/// <param name="tenantProviderName">The name - including the tenant id - of the tenant provider; can be used to access named provider services that are tenant specific</param>
/// <param name="options">The options to pass to the provider. Note that configureTenantOptions and options validation have already been executed on this</param>
/// <returns>The tenant storage provider construction parameters to pass to DI. Don't include <paramref name="tenantProviderName"/> in these; it is added automatically</returns>
public delegate object[] GrainStorageProviderParametersFactory<in TGrainStorageOptions>(
IServiceProvider services,
string providerName,
string tenantProviderName,
TGrainStorageOptions options
);

public static class GrainExtensions
{
/// <summary>Get a tenant stream provider from within a <see cref="Grain"/>, for the tenant that this grain belongs to</summary>
Expand Down
Loading

0 comments on commit cfc2c21

Please sign in to comment.