diff --git a/src/EventSourcingTests/Aggregation/ancillary_store_enrichment_tests.cs b/src/EventSourcingTests/Aggregation/ancillary_store_enrichment_tests.cs index 650690fa6f..70ded7f522 100644 --- a/src/EventSourcingTests/Aggregation/ancillary_store_enrichment_tests.cs +++ b/src/EventSourcingTests/Aggregation/ancillary_store_enrichment_tests.cs @@ -18,42 +18,29 @@ namespace EventSourcingTests.Aggregation; // Marker interface for the ancillary store that holds reference products public interface IProductStore : IDocumentStore; -public class ancillary_store_enrichment_tests : IAsyncLifetime +public class ancillary_store_enrichment_tests : HostedStoreContext { private IHost _host = null!; private IDocumentStore _primaryStore = null!; - public async ValueTask InitializeAsync() + public override async ValueTask InitializeAsync() { - _host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => + _host = await StartHostAsync( + opts => opts.Projections.Add(ProjectionLifecycle.Async), + configureMarten: marten => marten.ApplyAllDatabaseChangesOnStartup(), + configureServices: services => { - services.AddMarten(opts => - { - opts.Connection(ConnectionSource.ConnectionString); - opts.DatabaseSchemaName = "ancillary_enrich_primary"; - opts.Projections.Add(ProjectionLifecycle.Async); - }) - .ApplyAllDatabaseChangesOnStartup(); - services.AddMartenStore(opts => - { - opts.Connection(ConnectionSource.ConnectionString); - opts.DatabaseSchemaName = "ancillary_enrich_products"; - }) - .ApplyAllDatabaseChangesOnStartup(); - }) - .StartAsync(); + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = $"{SchemaName}_products"; + }) + .ApplyAllDatabaseChangesOnStartup(); + }); _primaryStore = _host.Services.GetRequiredService(); } - public async ValueTask DisposeAsync() - { - await _host.StopAsync(); - _host.Dispose(); - } - [Fact] public async Task enrichment_from_ancillary_store_resolves_entity_and_maps_to_projection() { diff --git a/src/EventSourcingTests/Bugs/Bug_4441_force_catch_up_with_outbox.cs b/src/EventSourcingTests/Bugs/Bug_4441_force_catch_up_with_outbox.cs index 4823f17434..a28f513ac1 100644 --- a/src/EventSourcingTests/Bugs/Bug_4441_force_catch_up_with_outbox.cs +++ b/src/EventSourcingTests/Bugs/Bug_4441_force_catch_up_with_outbox.cs @@ -30,7 +30,7 @@ namespace EventSourcingTests.Bugs; // IMessageBatch must catch up cleanly and the listener hooks must fire even // when the projection raises no side effects. -public class Bug_4441_force_catch_up_with_outbox +public class Bug_4441_force_catch_up_with_outbox: HostedStoreContext { public class LetterCounts { @@ -55,17 +55,9 @@ public override LetterCounts Evolve(LetterCounts? snapshot, Guid id, IEvent e) [Fact(Timeout = 30000)] public async Task force_catch_up_returns_for_async_daemon_without_side_effects() { - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(s => - { - s.AddMarten(m => - { - m.Connection(ConnectionSource.ConnectionString); - m.DatabaseSchemaName = "bug4441_default"; - m.Projections.Add(ProjectionLifecycle.Async); - }).AddAsyncDaemon(DaemonMode.Solo); - }) - .StartAsync(); + var host = await StartHostAsync( + m => m.Projections.Add(ProjectionLifecycle.Async), + DaemonMode.Solo); var store = host.Services.GetRequiredService(); await store.Advanced.Clean.CompletelyRemoveAllAsync(); @@ -86,18 +78,12 @@ public async Task force_catch_up_invokes_message_batch_lifecycle_with_custom_out { var outbox = new RecordingOutbox(); - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(s => + var host = await StartHostAsync(m => { - s.AddMarten(m => - { - m.Connection(ConnectionSource.ConnectionString); - m.DatabaseSchemaName = "bug4441_outbox"; - m.Events.MessageOutbox = outbox; - m.Projections.Add(ProjectionLifecycle.Async); - }).AddAsyncDaemon(DaemonMode.Solo); - }) - .StartAsync(); + m.Events.MessageOutbox = outbox; + m.Projections.Add(ProjectionLifecycle.Async); + }, + DaemonMode.Solo); var store = host.Services.GetRequiredService(); await store.Advanced.Clean.CompletelyRemoveAllAsync(); diff --git a/src/EventSourcingTests/Bugs/Bug_4904_force_catch_up_under_externally_managed.cs b/src/EventSourcingTests/Bugs/Bug_4904_force_catch_up_under_externally_managed.cs index aeccbb6801..77e62ae3f9 100644 --- a/src/EventSourcingTests/Bugs/Bug_4904_force_catch_up_under_externally_managed.cs +++ b/src/EventSourcingTests/Bugs/Bug_4904_force_catch_up_under_externally_managed.cs @@ -38,7 +38,7 @@ namespace EventSourcingTests.Bugs; // The mitigation degrades ForceAll under ExternallyManaged to a read-only wait-for-non-stale that never // starts, stops, or drives an agent — so it can neither throw on the missing coordinator nor race the // externally-owned agents into an out-of-order progression write. This test pins that Marten-side contract. -public class Bug_4904_force_catch_up_under_externally_managed +public class Bug_4904_force_catch_up_under_externally_managed: HostedStoreContext { public class LetterCounts { @@ -63,17 +63,9 @@ public override LetterCounts Evolve(LetterCounts? snapshot, Guid id, IEvent e) [Fact(Timeout = 30000)] public async Task force_catch_up_under_externally_managed_does_not_resolve_a_coordinator() { - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(s => - { - s.AddMarten(m => - { - m.Connection(ConnectionSource.ConnectionString); - m.DatabaseSchemaName = "bug4904_externally_managed"; - m.Projections.Add(ProjectionLifecycle.Async); - }).AddAsyncDaemon(DaemonMode.ExternallyManaged); - }) - .StartAsync(); + var host = await StartHostAsync( + m => m.Projections.Add(ProjectionLifecycle.Async), + DaemonMode.ExternallyManaged); var store = host.Services.GetRequiredService(); await store.Advanced.Clean.CompletelyRemoveAllAsync(); diff --git a/src/EventSourcingTests/EventSourcingTests.csproj b/src/EventSourcingTests/EventSourcingTests.csproj index 23440041bf..5491b53e68 100644 --- a/src/EventSourcingTests/EventSourcingTests.csproj +++ b/src/EventSourcingTests/EventSourcingTests.csproj @@ -78,6 +78,9 @@ Harness\DestructiveIntegrationContext.cs + + Harness\HostedStoreContext.cs + Harness\IntegrationContext.cs diff --git a/src/EventSourcingTests/determining_the_event_store_identity.cs b/src/EventSourcingTests/determining_the_event_store_identity.cs index f431b111aa..d0fa320744 100644 --- a/src/EventSourcingTests/determining_the_event_store_identity.cs +++ b/src/EventSourcingTests/determining_the_event_store_identity.cs @@ -11,27 +11,19 @@ namespace EventSourcingTests; -public class determining_the_event_store_identity +public class determining_the_event_store_identity: HostedStoreContext { [Fact] public async Task use_correct_identities() { - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => + var host = await StartHostAsync(_ => { }, configureServices: services => + { + services.AddMartenStore(m => { - services.AddMarten(m => - { - m.Connection(ConnectionSource.ConnectionString); - m.DatabaseSchemaName = "es_identity"; - }); - - services.AddMartenStore(m => - { - m.Connection(ConnectionSource.ConnectionString); - m.DatabaseSchemaName = "things"; - }); - - }).StartAsync(); + m.Connection(ConnectionSource.ConnectionString); + m.DatabaseSchemaName = $"{SchemaName}_things"; + }); + }); var stores = host.Services.GetServices().ToArray(); stores.Single(x => x.GetType() == typeof(DocumentStore)).As().Identity.ShouldBe(new EventStoreIdentity("main", "marten")); diff --git a/src/EventSourcingTests/generating_event_store_descriptors.cs b/src/EventSourcingTests/generating_event_store_descriptors.cs index b0745ec2b7..bd6b9d5012 100644 --- a/src/EventSourcingTests/generating_event_store_descriptors.cs +++ b/src/EventSourcingTests/generating_event_store_descriptors.cs @@ -20,25 +20,18 @@ namespace EventSourcingTests; -public class generating_event_store_descriptors +public class generating_event_store_descriptors: HostedStoreContext { [Fact] public async Task find_default_capability_with_events_and_projections() { - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => - { - services.AddMarten(opts => - { - opts.Connection(ConnectionSource.ConnectionString); - opts.DatabaseSchemaName = "capabilities"; - - opts.Projections.Snapshot(SnapshotLifecycle.Async); - opts.Projections.Add(ProjectionLifecycle.Inline); - opts.Projections.LiveStreamAggregation(); - opts.Projections.Add(ProjectionLifecycle.Inline); - }); - }).StartAsync(); + var host = await StartHostAsync(opts => + { + opts.Projections.Snapshot(SnapshotLifecycle.Async); + opts.Projections.Add(ProjectionLifecycle.Inline); + opts.Projections.LiveStreamAggregation(); + opts.Projections.Add(ProjectionLifecycle.Inline); + }); var capabilities = host.Services.GetServices().ToArray(); capabilities.Length.ShouldBe(1); @@ -54,19 +47,12 @@ public async Task find_default_capability_with_events_and_projections() [Fact] public async Task subscription_descriptors_carry_implementation_and_aggregate_types() { - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => - { - services.AddMarten(opts => - { - opts.Connection(ConnectionSource.ConnectionString); - opts.DatabaseSchemaName = "descriptor_enrichment"; - - opts.Projections.Snapshot(SnapshotLifecycle.Async); - opts.Projections.Add(ProjectionLifecycle.Inline); - opts.Projections.Add(ProjectionLifecycle.Inline); - }); - }).StartAsync(); + var host = await StartHostAsync(opts => + { + opts.Projections.Snapshot(SnapshotLifecycle.Async); + opts.Projections.Add(ProjectionLifecycle.Inline); + opts.Projections.Add(ProjectionLifecycle.Inline); + }); var capability = host.Services.GetRequiredService(); var usage = await capability.TryCreateUsage(CancellationToken.None); @@ -96,15 +82,7 @@ public async Task subscription_descriptors_carry_implementation_and_aggregate_ty [Fact] public async Task max_event_sequence_matches_highest_persisted_seq_id() { - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => - { - services.AddMarten(opts => - { - opts.Connection(ConnectionSource.ConnectionString); - opts.DatabaseSchemaName = "max_seq_usage"; - }); - }).StartAsync(); + var host = await StartHostAsync(_ => { }); var store = host.Services.GetRequiredService(); diff --git a/src/EventSourcingTests/propagate_logger_to_projections.cs b/src/EventSourcingTests/propagate_logger_to_projections.cs index e4578ed974..d4cedcdf26 100644 --- a/src/EventSourcingTests/propagate_logger_to_projections.cs +++ b/src/EventSourcingTests/propagate_logger_to_projections.cs @@ -14,23 +14,16 @@ namespace EventSourcingTests; -public class propagate_logger_to_projections +public class propagate_logger_to_projections: HostedStoreContext { [Fact] public async Task loggers_exist_on_projections() { - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => - { - services.AddMarten(m => - { - m.Connection(ConnectionSource.ConnectionString); - m.DatabaseSchemaName = "system_part"; - - m.Projections.Add(ProjectionLifecycle.Inline); - m.Projections.Add(ProjectionLifecycle.Inline); - }); - }).StartAsync(); + var host = await StartHostAsync(m => + { + m.Projections.Add(ProjectionLifecycle.Inline); + m.Projections.Add(ProjectionLifecycle.Inline); + }); var store = host.DocumentStore(); var projections = store.Options.As().Projections.All; diff --git a/src/Marten.Testing/Harness/HostedStoreContext.cs b/src/Marten.Testing/Harness/HostedStoreContext.cs new file mode 100644 index 0000000000..3561d465b1 --- /dev/null +++ b/src/Marten.Testing/Harness/HostedStoreContext.cs @@ -0,0 +1,110 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JasperFx.Core.Reflection; +using JasperFx.Events.Daemon; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Marten.Testing.Harness +{ + /// + /// Base for tests that need a real with AddMarten — + /// daemon coordination, DI-registered projections, ancillary stores — + /// rather than a bare DocumentStore. Owns host lifetime: every host + /// started through is stopped and disposed + /// (newest first) when the test class completes. + /// + /// + /// The main store gets this class's (derived from + /// the test class name, like OneOffConfigurationsContext). Tests adding + /// secondary stores via AddMartenStore<T> in the + /// configureServices callback should derive their schema from + /// too (e.g. $"{SchemaName}_products") so + /// the whole class stays isolated under one prefix. + /// + public abstract class HostedStoreContext: IAsyncLifetime + { + private readonly List _hosts = new(); + + protected HostedStoreContext() + { + SchemaName = GetType().Name.ToLower().Sanitize(); + } + + protected string SchemaName { get; } + + /// + /// Start (and track) a host whose main Marten store is pre-configured + /// with the test connection string, , and + /// Npgsql logging disabled. maps to + /// AddAsyncDaemon; receives the + /// AddMarten fluent expression for chained calls like + /// ApplyAllDatabaseChangesOnStartup; + /// runs after AddMarten for extra registrations (ancillary stores, + /// logging...). + /// + protected async Task StartHostAsync( + Action configure, + DaemonMode? daemonMode = null, + Action? configureServices = null, + Action? configureMarten = null) + { + var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + var marten = services.AddMarten(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DisableNpgsqlLogging = true; + opts.DatabaseSchemaName = SchemaName; + + configure(opts); + }); + + if (daemonMode.HasValue) + { + marten.AddAsyncDaemon(daemonMode.Value); + } + + configureMarten?.Invoke(marten); + + configureServices?.Invoke(services); + }).StartAsync(); + + _hosts.Add(host); + + return host; + } + + protected static DocumentStore StoreOf(IHost host) + { + return (DocumentStore)host.Services.GetRequiredService(); + } + + public virtual ValueTask InitializeAsync() + { + return default; + } + + public virtual async ValueTask DisposeAsync() + { + for (var i = _hosts.Count - 1; i >= 0; i--) + { + var host = _hosts[i]; + try + { + await host.StopAsync(TimeSpan.FromSeconds(10)); + } + finally + { + host.Dispose(); + } + } + + _hosts.Clear(); + } + } +}