From dabcd4909f535c7ed87089c017c38dfec4c701ee Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 9 Oct 2025 12:39:22 -0500 Subject: [PATCH] Made the IntegrateWithWolverine usage w/ ancillary Marten stores consistent w/ main store. Closes GH-1737 --- .../service_location_assertions.cs | 82 ++++++++++++++++++- ...ncillary_stores_use_different_databases.cs | 2 +- ..._ancillary_marten_stores_with_wolverine.cs | 2 +- .../modular_monolith_usage.cs | 4 +- ...cillaryWolverineOptionsMartenExtensions.cs | 64 ++++++++++----- .../Wolverine.Marten/Wolverine.Marten.csproj | 2 +- ...ntainer_or_service_provider_in_handlers.cs | 50 ++++++++++- src/Wolverine/Wolverine.csproj | 2 +- 8 files changed, 178 insertions(+), 30 deletions(-) diff --git a/src/Http/Wolverine.Http.Tests/CodeGeneration/service_location_assertions.cs b/src/Http/Wolverine.Http.Tests/CodeGeneration/service_location_assertions.cs index 9126b7475..04f2c05b4 100644 --- a/src/Http/Wolverine.Http.Tests/CodeGeneration/service_location_assertions.cs +++ b/src/Http/Wolverine.Http.Tests/CodeGeneration/service_location_assertions.cs @@ -4,6 +4,7 @@ using JasperFx; using JasperFx.CodeGeneration.Frames; using JasperFx.CodeGeneration.Model; +using JasperFx.Core; using Marten; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -11,6 +12,8 @@ using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; +using Wolverine.Attributes; +using Wolverine.ComplianceTests; using Wolverine.Configuration; using Wolverine.Marten; @@ -43,6 +46,18 @@ private async Task buildHost(ServiceProviderSource providerSource, Ac opts.Services.AddScoped(s => new BigThing()); + opts.IncludeType(typeof(CSP5User)); + opts.CodeGeneration.AlwaysUseServiceLocationFor(); + + opts.Services.AddScoped(); + opts.Services.AddScoped(x => + { + var context = x.GetRequiredService(); + return context.Color.EqualsIgnoreCase("red") ? new RedFlag() : new GreenFlag(); + }); + + opts.Services.AddSingleton(new ColorContext("Red")); + configure(opts); }); @@ -196,6 +211,47 @@ public async Task can_use_service_locations_with_handler(ServiceLocationPolicy p } + + [Theory] + [InlineData(ServiceLocationPolicy.AllowedButWarn, ServiceProviderSource.IsolatedAndScoped)] + [InlineData(ServiceLocationPolicy.AlwaysAllowed, ServiceProviderSource.IsolatedAndScoped)] + [InlineData(ServiceLocationPolicy.AllowedButWarn, ServiceProviderSource.FromHttpContextRequestServices)] + [InlineData(ServiceLocationPolicy.AlwaysAllowed, ServiceProviderSource.FromHttpContextRequestServices)] + public async Task always_use_service_location_does_not_count_toward_validation(ServiceLocationPolicy policy, ServiceProviderSource source) + { + await using var host = await buildHost(source, opts => + { + opts.ServiceLocationPolicy = policy; + }); + + CSP5User.Flag = null; + + await host.InvokeAsync(new CSP5()); + CSP5User.Flag.ShouldBeOfType(); + } + + [Theory] + [InlineData(ServiceLocationPolicy.AllowedButWarn, ServiceProviderSource.IsolatedAndScoped)] + [InlineData(ServiceLocationPolicy.AlwaysAllowed, ServiceProviderSource.IsolatedAndScoped)] + [InlineData(ServiceLocationPolicy.AllowedButWarn, ServiceProviderSource.FromHttpContextRequestServices)] + [InlineData(ServiceLocationPolicy.AlwaysAllowed, ServiceProviderSource.FromHttpContextRequestServices)] + public async Task always_use_service_location_does_not_count_toward_validation_in_http_endpoint(ServiceLocationPolicy policy, ServiceProviderSource source) + { + await using var host = await buildHost(source, opts => + { + opts.ServiceLocationPolicy = policy; + }); + + CSP5User.Flag = null; + + await host.Scenario(x => + { + x.Post.Json(new CSP5()).ToUrl("/csp5"); + x.StatusCodeShouldBe(204); + }); + + CSP5User.Flag.ShouldBeOfType(); + } } public interface IWidget; @@ -261,4 +317,28 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { Messages.Add(formatter(state, exception)); } -} \ No newline at end of file +} + +public record CSP5; + +public static class CSP5User +{ + public static IFlag? Flag { get; set; } + + [WolverinePost("/csp5")] + public static void Handle(CSP5 message, IFlag flag, IGateway gateway) + { + Flag = flag; + } +} + + + +public interface IFlag; +public record ColorContext(string Color); + +public record RedFlag : IFlag; +public record GreenFlag : IFlag; + +public interface IGateway; +public class Gateway : IGateway; \ No newline at end of file diff --git a/src/Persistence/MartenTests/AncillaryStores/ancillary_stores_use_different_databases.cs b/src/Persistence/MartenTests/AncillaryStores/ancillary_stores_use_different_databases.cs index f4320721e..88ba0f1bd 100644 --- a/src/Persistence/MartenTests/AncillaryStores/ancillary_stores_use_different_databases.cs +++ b/src/Persistence/MartenTests/AncillaryStores/ancillary_stores_use_different_databases.cs @@ -61,7 +61,7 @@ public async Task InitializeAsync() opts.Services.AddMartenStore(m => { m.Connection(thingsConnectionString); - }).IntegrateWithWolverine(masterDatabaseConnectionString: Servers.PostgresConnectionString); + }).IntegrateWithWolverine(x => x.MainConnectionString = Servers.PostgresConnectionString); opts.Services.AddResourceSetupOnStartup(); }).StartAsync(); diff --git a/src/Persistence/MartenTests/AncillaryStores/bootstrapping_ancillary_marten_stores_with_wolverine.cs b/src/Persistence/MartenTests/AncillaryStores/bootstrapping_ancillary_marten_stores_with_wolverine.cs index ec602f24c..09890ea3d 100644 --- a/src/Persistence/MartenTests/AncillaryStores/bootstrapping_ancillary_marten_stores_with_wolverine.cs +++ b/src/Persistence/MartenTests/AncillaryStores/bootstrapping_ancillary_marten_stores_with_wolverine.cs @@ -95,7 +95,7 @@ public async Task InitializeAsync() tenancy.AddSingleTenantDatabase(tenant3ConnectionString, "tenant3"); }); m.DatabaseSchemaName = "things"; - }).IntegrateWithWolverine(masterDatabaseConnectionString: Servers.PostgresConnectionString); + }).IntegrateWithWolverine(x => x.MainConnectionString = Servers.PostgresConnectionString); opts.Services.AddResourceSetupOnStartup(); }).StartAsync(); diff --git a/src/Persistence/PersistenceTests/ModularMonoliths/modular_monolith_usage.cs b/src/Persistence/PersistenceTests/ModularMonoliths/modular_monolith_usage.cs index 26bf35de0..9b689725c 100644 --- a/src/Persistence/PersistenceTests/ModularMonoliths/modular_monolith_usage.cs +++ b/src/Persistence/PersistenceTests/ModularMonoliths/modular_monolith_usage.cs @@ -95,13 +95,13 @@ public async Task do_not_override_when_the_schema_name_is_explicitly_set() { m.Connection(Servers.PostgresConnectionString); m.DatabaseSchemaName = "players"; - }).IntegrateWithWolverine(schemaName:"different"); + }).IntegrateWithWolverine(x => x.SchemaName = "different"); opts.Services.AddMartenStore(m => { m.Connection(Servers.PostgresConnectionString); m.DatabaseSchemaName = "things"; - }).IntegrateWithWolverine(schemaName:"different"); + }).IntegrateWithWolverine(x => x.SchemaName = "different"); opts.Services.AddResourceSetupOnStartup(); }).StartAsync(); diff --git a/src/Persistence/Wolverine.Marten/AncillaryWolverineOptionsMartenExtensions.cs b/src/Persistence/Wolverine.Marten/AncillaryWolverineOptionsMartenExtensions.cs index 598783b82..532b5d35a 100644 --- a/src/Persistence/Wolverine.Marten/AncillaryWolverineOptionsMartenExtensions.cs +++ b/src/Persistence/Wolverine.Marten/AncillaryWolverineOptionsMartenExtensions.cs @@ -27,40 +27,60 @@ namespace Wolverine.Marten; -public static class AncillaryWolverineOptionsMartenExtensions +public class AncillaryMartenIntegration { /// - /// Integrate Marten with Wolverine's persistent outbox and add Marten-specific middleware - /// to Wolverine + /// Optionally move the Wolverine envelope storage to a separate schema. + /// The recommendation would be to either leave this null, or use the same + /// schema name as the main Marten store /// - /// - /// Optionally move the Wolverine envelope storage to a separate schema - /// + public string? SchemaName { get; set; } + + /// /// In the case of Marten using a database per tenant, you may wish to /// explicitly determine the master database for Wolverine where Wolverine will store node and envelope information. /// This does not have to be one of the tenant databases /// Wolverine will try to use the master database from the Marten configuration when possible - /// - /// + /// + public string? MainConnectionString { get; set; } + + /// /// In the case of Marten using a database per tenant, you may wish to /// explicitly determine the master database for Wolverine where Wolverine will store node and envelope information. /// This does not have to be one of the tenant databases /// Wolverine will try to use the master database from the Marten configuration when possible - /// - /// Optionally override whether to automatically create message database schema objects. Defaults to . - /// - public static MartenServiceCollectionExtensions.MartenStoreExpression IntegrateWithWolverine( - this MartenServiceCollectionExtensions.MartenStoreExpression expression, - string? schemaName = null, - string? masterDatabaseConnectionString = null, - NpgsqlDataSource? masterDataSource = null, - AutoCreate? autoCreate = null) where T : class, IDocumentStore + /// + public NpgsqlDataSource? MainDataSource { get; set; } + + /// + /// Optionally override whether to automatically create message database schema objects. Defaults to . + /// + public AutoCreate? AutoCreate { get; set; } + + internal void AssertValidity() { - if (schemaName.IsNotEmpty() && schemaName != schemaName.ToLowerInvariant()) + if (SchemaName.IsNotEmpty() && SchemaName != SchemaName.ToLowerInvariant()) { - throw new ArgumentOutOfRangeException(nameof(schemaName), + throw new ArgumentOutOfRangeException(nameof(SchemaName), "The schema name must be in all lower case characters"); } + } +} + +public static class AncillaryWolverineOptionsMartenExtensions +{ + /// + /// Integrate Marten with Wolverine's persistent outbox and add Marten-specific middleware + /// to Wolverine + /// + /// Optional configuration of ancillary Marten integration + public static MartenServiceCollectionExtensions.MartenStoreExpression IntegrateWithWolverine( + this MartenServiceCollectionExtensions.MartenStoreExpression expression, + Action? configure = null) where T : class, IDocumentStore + { + var integration = new AncillaryMartenIntegration(); + configure?.Invoke(integration); + integration.AssertValidity(); expression.Services.AddSingleton, MartenOverrides>(); @@ -71,15 +91,15 @@ public static MartenServiceCollectionExtensions.MartenStoreExpression Integra var runtime = s.GetRequiredService(); var logger = s.GetRequiredService>(); - schemaName ??= runtime.Options.Durability.MessageStorageSchemaName ?? store.Options.DatabaseSchemaName; + integration.SchemaName ??= runtime.Options.Durability.MessageStorageSchemaName ?? store.Options.DatabaseSchemaName; // TODO -- hacky. Need a way to expose this in Marten if (store.Tenancy.GetType().Name == "DefaultTenancy") { - return BuildSinglePostgresqlMessageStore(schemaName, autoCreate, store, runtime, logger); + return BuildSinglePostgresqlMessageStore(integration.SchemaName, integration.AutoCreate, store, runtime, logger); } - return BuildMultiTenantedMessageDatabase(schemaName, autoCreate, masterDatabaseConnectionString, masterDataSource, store, runtime); + return BuildMultiTenantedMessageDatabase(integration.SchemaName, integration.AutoCreate, integration.MainConnectionString, integration.MainDataSource, store, runtime); }); expression.Services.AddType(typeof(IDatabaseSource), typeof(MessageDatabaseDiscovery), diff --git a/src/Persistence/Wolverine.Marten/Wolverine.Marten.csproj b/src/Persistence/Wolverine.Marten/Wolverine.Marten.csproj index 94ed289ac..fc5501733 100644 --- a/src/Persistence/Wolverine.Marten/Wolverine.Marten.csproj +++ b/src/Persistence/Wolverine.Marten/Wolverine.Marten.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Testing/CoreTests/Compilation/using_container_or_service_provider_in_handlers.cs b/src/Testing/CoreTests/Compilation/using_container_or_service_provider_in_handlers.cs index 318a6e8e7..29231b8e2 100644 --- a/src/Testing/CoreTests/Compilation/using_container_or_service_provider_in_handlers.cs +++ b/src/Testing/CoreTests/Compilation/using_container_or_service_provider_in_handlers.cs @@ -1,5 +1,7 @@ using JasperFx.CodeGeneration; +using JasperFx.Core; using Microsoft.Extensions.DependencyInjection; +using Wolverine.Attributes; using Wolverine.ComplianceTests; using Xunit; using Xunit.Abstractions; @@ -33,6 +35,29 @@ public async Task IServiceProvider_as_method_parameter() { await Execute(new CSP4()); } + + [Fact] + public async Task using_service_location_with_one_service() + { + IfWolverineIsConfiguredAs(opts => + { + opts.IncludeType(typeof(CSP5User)); + opts.CodeGeneration.AlwaysUseServiceLocationFor(); + + opts.Services.AddScoped(); + opts.Services.AddScoped(x => + { + var context = x.GetRequiredService(); + return context.Color.EqualsIgnoreCase("red") ? new RedFlag() : new GreenFlag(); + }); + + opts.Services.AddSingleton(new ColorContext("Red")); + }); + + await Execute(new CSP5()); + + CSP5User.Flag.ShouldBeOfType(); + } } public class CSP3; @@ -60,4 +85,27 @@ public void Handle(CSP4 message, IServiceProvider container) { container.ShouldNotBeNull(); } -} \ No newline at end of file +} + +public record CSP5; + +// Just need this to be explicit +[WolverineIgnore] +public static class CSP5User +{ + public static IFlag? Flag { get; set; } + + public static void Handle(CSP5 message, IFlag flag, IGateway gateway) + { + Flag = flag; + } +} + +public interface IFlag; +public record ColorContext(string Color); + +public record RedFlag : IFlag; +public record GreenFlag : IFlag; + +public interface IGateway; +public class Gateway : IGateway; \ No newline at end of file diff --git a/src/Wolverine/Wolverine.csproj b/src/Wolverine/Wolverine.csproj index 9bf4d9f29..c78850b02 100644 --- a/src/Wolverine/Wolverine.csproj +++ b/src/Wolverine/Wolverine.csproj @@ -4,7 +4,7 @@ WolverineFx - +