diff --git a/src/Orleans.Multitenant/Extensions.cs b/src/Orleans.Multitenant/Extensions.cs index 3674740..85db65f 100644 --- a/src/Orleans.Multitenant/Extensions.cs +++ b/src/Orleans.Multitenant/Extensions.cs @@ -192,7 +192,12 @@ public static IServiceCollection AddMultitenantGrainStorageThe 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 +/// +/// The complete set of tenant storage provider construction parameters to pass to DI.
+/// When this factory is provided, it has full ownership of the parameter list — is NOT prepended automatically.
+/// 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.
+/// For standard providers that DO follow the (string name, TOptions options) convention, omit this factory and rely on the default behavior. +///
public delegate object[] GrainStorageProviderParametersFactory( IServiceProvider services, string providerName, diff --git a/src/Orleans.Multitenant/Internal/SiloLifecycle.cs b/src/Orleans.Multitenant/Internal/SiloLifecycle.cs index a8ee23c..ca20492 100644 --- a/src/Orleans.Multitenant/Internal/SiloLifecycle.cs +++ b/src/Orleans.Multitenant/Internal/SiloLifecycle.cs @@ -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)]; diff --git a/src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs b/src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs index 87c3ed7..dd621a5 100644 --- a/src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs +++ b/src/Orleans.Multitenant/Internal/TenantGrainStorageFactory.cs @@ -86,8 +86,15 @@ public IGrainStorage Create(string tenantId) if (options is IStorageProviderSerializerOptions serializerOptions && serializerOptions.GrainStorageSerializer == default) serializerOptions.GrainStorageSerializer = services.GetKeyedService(name) ?? services.GetRequiredService(); - List 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 providerParameters = getProviderParameters is not null + ? [.. getProviderParameters.Invoke(services, name, tenantProviderName, options)] + : [tenantProviderName, options]; var configuredOptions = providerParameters.OfType().SingleOrDefault(); if (configuredOptions is not null) diff --git a/src/Tests/UnitTests/GrainStorageProviderParametersTests.cs b/src/Tests/UnitTests/GrainStorageProviderParametersTests.cs new file mode 100644 index 0000000..f5b77ce --- /dev/null +++ b/src/Tests/UnitTests/GrainStorageProviderParametersTests.cs @@ -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 state) : Grain, ITestGrain +{ + public Task UseStorage() => state.ReadStateAsync(); +} + +[GenerateSerializer] +public class TestState { } + +public sealed class GrainStorageProviderParametersTests(GrainStorageProviderParametersTests.ClusterFixture fixture) : IClassFixture +{ + readonly TestCluster cluster = fixture.Cluster; + + [Fact] + public async Task CustomStorageProvider_IsInstantiatedWithCorrectParameters() + { + string tenantId = "TenantA"; + var grain = cluster.Client.ForTenant(tenantId).GetGrain(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(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(string stateName, GrainId grainId, IGrainState grainState) => Task.CompletedTask; + public Task ReadStateAsync(string stateName, GrainId grainId, IGrainState grainState) => Task.CompletedTask; + public Task WriteStateAsync(string stateName, GrainId grainId, IGrainState 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(); + + 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( + (siloBuilder, name) => siloBuilder.ConfigureServices(services => + services.AddKeyedSingleton(name, (sp, key) => new CustomGrainStorage((string)key!, new CustomOptions(), "default"))), + getProviderParameters: (services, name, tenantProviderName, options) => [tenantProviderName, options, "ExtraValue"] + ); + } + } +}