Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/Orleans.Multitenant/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,12 @@ public static IServiceCollection AddMultitenantGrainStorage<TGrainStorage, TGrai
/// <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>
/// <returns>
/// The complete set of tenant storage provider construction parameters to pass to DI.<br />
/// When this factory is provided, it has full ownership of the parameter list — <paramref name="tenantProviderName"/> is NOT prepended automatically.<br />
/// This allows providers with non-standard constructor signatures (e.g. RavenDbGrainStorage, which does not accept a string name as its first parameter) to work correctly.<br />
/// For standard providers that DO follow the <c>(string name, TOptions options)</c> convention, omit this factory and rely on the default behavior.
/// </returns>
public delegate object[] GrainStorageProviderParametersFactory<in TGrainStorageOptions>(
IServiceProvider services,
string providerName,
Expand Down
2 changes: 1 addition & 1 deletion src/Orleans.Multitenant/Internal/SiloLifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ sealed record LifecycleStartupRecording(int HighestCompletedStageOnParticipate,

sealed class SiloLifecycleRepeater : IRepeatedSiloLifecycleObservable
{
internal static int[] AllServiceLifecycleStages { get; } =
internal static int[] AllServiceLifecycleStages => field ??=
[.. typeof(ServiceLifecycleStage).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static).Where(fi => fi.FieldType == typeof(int))
.Select(fi => (int)(fi.GetValue(null) ?? throw new InvalidCastException("static int field cannot have value null"))).OrderBy(value => value)];

Expand Down
11 changes: 9 additions & 2 deletions src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,15 @@ public IGrainStorage Create(string tenantId)
if (options is IStorageProviderSerializerOptions serializerOptions && serializerOptions.GrainStorageSerializer == default)
serializerOptions.GrainStorageSerializer = services.GetKeyedService<IGrainStorageSerializer>(name) ?? services.GetRequiredService<IGrainStorageSerializer>();

List<object> providerParameters = [tenantProviderName];
providerParameters.AddRange(getProviderParameters?.Invoke(services, name, tenantProviderName, options) ?? [options]);
// If getProviderParameters is supplied, the caller takes full ownership of the constructor
// parameter list (needed for providers like RavenDbGrainStorage whose constructors do not
// follow the conventional (string name, TOptions options) signature).
// Otherwise, fall back to the original convention: [tenantProviderName, options].
// This keeps backward compatibility for standard providers (Azure, AdoNet, etc.) and is
// suitable to PR upstream to VincentH-Net/Orleans.Multitenant.
List<object> providerParameters = getProviderParameters is not null
? [.. getProviderParameters.Invoke(services, name, tenantProviderName, options)]
: [tenantProviderName, options];

var configuredOptions = providerParameters.OfType<TGrainStorageOptions>().SingleOrDefault();
if (configuredOptions is not null)
Expand Down
93 changes: 93 additions & 0 deletions src/Tests/UnitTests/GrainStorageProviderParametersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using Microsoft.Extensions.DependencyInjection;
using Orleans.Configuration;
using Orleans.Storage;
using Orleans.TestingHost;

namespace OrleansMultitenant.Tests.UnitTests;

public interface ITestGrain : IGrainWithStringKey
{
Task UseStorage();
}

public class TestGrain([PersistentState("state")] IPersistentState<TestState> state) : Grain, ITestGrain
{
public Task UseStorage() => state.ReadStateAsync();
}

[GenerateSerializer]
public class TestState { }

public sealed class GrainStorageProviderParametersTests(GrainStorageProviderParametersTests.ClusterFixture fixture) : IClassFixture<GrainStorageProviderParametersTests.ClusterFixture>
{
readonly TestCluster cluster = fixture.Cluster;

[Fact]
public async Task CustomStorageProvider_IsInstantiatedWithCorrectParameters()
{
string tenantId = "TenantA";
var grain = cluster.Client.ForTenant(tenantId).GetGrain<ITestGrain>(Guid.NewGuid().ToString());

await grain.UseStorage();

object[]? parameters = CustomGrainStorage.GetLastParameters();
Assert.NotNull(parameters);
Assert.Equal(3, parameters.Length);
Assert.StartsWith("TenantA_", (string)parameters[0], StringComparison.Ordinal);
_ = Assert.IsType<CustomOptions>(parameters[1]);
Assert.Equal("ExtraValue", (string)parameters[2]);
}

public class CustomGrainStorage : IGrainStorage
{
static object[]? lastParameters;
public static object[]? GetLastParameters() => lastParameters;

public CustomGrainStorage(string name, CustomOptions options, string extraParameter)
=> lastParameters = [name, options, extraParameter];

public Task ClearStateAsync<T>(string stateName, GrainId grainId, IGrainState<T> grainState) => Task.CompletedTask;
public Task ReadStateAsync<T>(string stateName, GrainId grainId, IGrainState<T> grainState) => Task.CompletedTask;
public Task WriteStateAsync<T>(string stateName, GrainId grainId, IGrainState<T> grainState) => Task.CompletedTask;
}

public class CustomOptions
{
public string SomeValue { get; set; } = string.Empty;
}

public class CustomOptionsValidator(CustomOptions options, string name) : IConfigurationValidator
{
public void ValidateConfiguration()
{
_ = options;
_ = name;
}
}

public sealed class ClusterFixture : IDisposable
{
public ClusterFixture()
{
var builder = new TestClusterBuilder()
.AddSiloBuilderConfigurator<SiloConfigurator>();

Cluster = builder.Build();
Cluster.Deploy();
}

public void Dispose() => Cluster.StopAllSilos();

public TestCluster Cluster { get; }

sealed class SiloConfigurator : ISiloConfigurator
{
public void Configure(ISiloBuilder siloBuilder) => siloBuilder
.AddMultitenantGrainStorageAsDefault<CustomGrainStorage, CustomOptions, CustomOptionsValidator>(
(siloBuilder, name) => siloBuilder.ConfigureServices(services =>
services.AddKeyedSingleton<IGrainStorage>(name, (sp, key) => new CustomGrainStorage((string)key!, new CustomOptions(), "default"))),
getProviderParameters: (services, name, tenantProviderName, options) => [tenantProviderName, options, "ExtraValue"]
);
}
}
}