diff --git a/Directory.Packages.props b/Directory.Packages.props index 37aabf94fd..c4d3518eea 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -171,21 +171,24 @@ message, since descriptions ship to monitoring consoles). Also carries the jasperfx#594 patch half (a timed-out blue/green side-effect gate re-reads progression and succeeds when the replay had already reached the mark, plus a configurable DaemonSettings.SideEffectGateTimeout), - jasperfx#595 (BatchingChannel could deliver its trailing batch twice on shutdown) and #597. --> - - + jasperfx#595 (BatchingChannel could deliver its trailing batch twice on shutdown) and #597. + JasperFx 2.37.2: compliance wave 2 (marten#5118) — four more shared suites plus the seam + members they need. Nothing outside JasperFx.Events.ComplianceTests changed; the other + packages move only because the line versions together. --> + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/EventSourcingTests/Aggregation/auto_discover_aggregate_types.cs b/src/EventSourcingTests/Aggregation/auto_discover_aggregate_types.cs deleted file mode 100644 index 2fea327da3..0000000000 --- a/src/EventSourcingTests/Aggregation/auto_discover_aggregate_types.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using JasperFx.Events; -using JasperFx.Events.ComplianceTests; -using Marten; -using Marten.Testing.Harness; -using Shouldly; -using Xunit; - -namespace EventSourcingTests.Aggregation; - -/// -/// Tests that verify self-aggregating types with source-generated evolvers -/// are automatically discovered and registered in StoreOptions.Projections -/// even without explicit Snapshot<T>() registration. -/// -public class auto_discover_aggregate_types : IntegrationContext -{ - public auto_discover_aggregate_types(DefaultStoreFixture fixture) : base(fixture) - { - } - - [Fact] - public void self_aggregating_types_are_auto_discovered() - { - // These types have source-generated evolvers via Apply/Create methods - // but are NOT explicitly registered via Projections.Snapshot() - var aggregateTypes = theStore.Options.Projections.AllAggregateTypes().ToArray(); - - // MutableIEventEvolveAggregate has a generated evolver via Evolve(IEvent) - aggregateTypes.ShouldContain(typeof(MutableIEventEvolveAggregate)); - } - - [Fact] - public async Task auto_discovered_type_works_for_live_aggregation() - { - // No explicit Snapshot() registration — relies on auto-discovery - var streamId = Guid.NewGuid(); - theSession.Events.StartStream(streamId, new EvolveAEvent(), new EvolveBEvent(), new EvolveCEvent()); - await theSession.SaveChangesAsync(); - - var aggregate = await theSession.Events.AggregateStreamAsync(streamId); - aggregate.ShouldNotBeNull(); - aggregate.ACount.ShouldBe(1); - aggregate.BCount.ShouldBe(1); - aggregate.CCount.ShouldBe(1); - } -} diff --git a/src/EventSourcingTests/Compliance/marten_event_store_compliance.cs b/src/EventSourcingTests/Compliance/marten_event_store_compliance.cs index 67aa4d9f49..7435f2b11b 100644 --- a/src/EventSourcingTests/Compliance/marten_event_store_compliance.cs +++ b/src/EventSourcingTests/Compliance/marten_event_store_compliance.cs @@ -22,3 +22,15 @@ public class assign_tag_where_compliance public class async_daemon_compliance : AsyncDaemonCompliance; + +public class auto_discovered_aggregate_compliance + : AutoDiscoveredAggregateCompliance; + +public class event_projection_registration_compliance + : EventProjectionRegistrationCompliance; + +public class event_projection_enrichment_compliance + : EventProjectionEnrichmentCompliance; + +public class rebuild_concurrency_cap_compliance + : RebuildConcurrencyCapCompliance; diff --git a/src/EventSourcingTests/Projections/event_projection_enrichment_tests.cs b/src/EventSourcingTests/Projections/event_projection_enrichment_tests.cs deleted file mode 100644 index 9fba37b418..0000000000 --- a/src/EventSourcingTests/Projections/event_projection_enrichment_tests.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JasperFx.Events; -using JasperFx.Events.Projections; -using Marten; -using Marten.Events.Projections; -using Marten.Testing.Documents; -using Marten.Testing.Harness; -using Shouldly; -using Xunit; - -namespace EventSourcingTests.Projections; - -public class event_projection_enrichment_tests : OneOffConfigurationsContext -{ - [Fact] - public async Task enrichment_sets_data_before_apply_inline() - { - StoreOptions(opts => - { - opts.Projections.Add(new SimpleEnrichmentProjection(), ProjectionLifecycle.Inline); - }); - - await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync(); - - // Use TaskAssigned as the only event — enrichment sets UserName, - // ProjectAsync reads it and stores a TaskSummary - var taskId = Guid.NewGuid(); - await using (var session = theStore.LightweightSession()) - { - session.Events.StartStream(taskId, - new TaskAssigned { TaskId = taskId, UserId = Guid.NewGuid() }); - await session.SaveChangesAsync(); - } - - await using (var query = theStore.QuerySession()) - { - var summary = await query.LoadAsync(taskId); - summary.ShouldNotBeNull(); - // The enrichment hardcodes the name — if set, enrichment ran before Apply - summary.AssignedUserName.ShouldBe("Enriched User"); - } - } - - [Fact] - public async Task enrichment_with_database_lookup_inline() - { - StoreOptions(opts => - { - opts.Projections.Add(new DatabaseLookupEnrichmentProjection(), ProjectionLifecycle.Inline); - }); - - await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync(); - - // Pre-store a User - var userId = Guid.NewGuid(); - await using (var session = theStore.LightweightSession()) - { - session.Store(new User { Id = userId, FirstName = "Alice", LastName = "Smith" }); - await session.SaveChangesAsync(); - } - - var taskId = Guid.NewGuid(); - await using (var session = theStore.LightweightSession()) - { - session.Events.StartStream(taskId, - new TaskAssigned { TaskId = taskId, UserId = userId }); - await session.SaveChangesAsync(); - } - - await using (var query = theStore.QuerySession()) - { - var summary = await query.LoadAsync(taskId); - summary.ShouldNotBeNull(); - summary.AssignedUserName.ShouldBe("Alice Smith"); - } - } - - [Fact] - public async Task enrichment_is_called_before_apply() - { - var callOrder = new List(); - - StoreOptions(opts => - { - opts.Projections.Add( - new CallOrderTrackingProjection(callOrder), - ProjectionLifecycle.Inline); - }); - - await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync(); - - var streamId = Guid.NewGuid(); - await using var session = theStore.LightweightSession(); - session.Events.StartStream(streamId, new TaskCreated { TaskId = streamId, Title = "Test" }); - await session.SaveChangesAsync(); - - callOrder.ShouldBe(new[] { "EnrichEventsAsync", "Apply:TaskCreated" }); - } -} - -#region Test Events - -public class TaskCreated -{ - public Guid TaskId { get; set; } - public string Title { get; set; } = ""; -} - -public class TaskAssigned -{ - public Guid TaskId { get; set; } - public Guid UserId { get; set; } - public string? UserName { get; set; } -} - -#endregion - -#region Test Documents - -public class TaskSummary -{ - public Guid Id { get; set; } - public string Title { get; set; } = ""; - public string? AssignedUserName { get; set; } -} - -#endregion - -#region Simple Enrichment (no DB lookup) - -public partial class SimpleEnrichmentProjection : EventProjection -{ - // TaskAssigned handler reads UserName that was set by EnrichEventsAsync - public void Project(TaskAssigned e, IDocumentOperations ops) - { - ops.Store(new TaskSummary - { - Id = e.TaskId, - AssignedUserName = e.UserName - }); - } - - public override Task EnrichEventsAsync(IQuerySession querySession, - IReadOnlyList events, CancellationToken cancellation) - { - foreach (var e in events.OfType>()) - { - e.Data.UserName = "Enriched User"; - } - return Task.CompletedTask; - } -} - -#endregion - -#region Database Lookup Enrichment - -public partial class DatabaseLookupEnrichmentProjection : EventProjection -{ - // Stores a TaskSummary using the enriched UserName - public void Project(TaskAssigned e, IDocumentOperations ops) - { - ops.Store(new TaskSummary - { - Id = e.TaskId, - AssignedUserName = e.UserName - }); - } - - public override async Task EnrichEventsAsync(IQuerySession querySession, - IReadOnlyList events, CancellationToken cancellation) - { - var assigned = events.OfType>().ToArray(); - if (assigned.Length == 0) return; - - var userIds = assigned.Select(e => e.Data.UserId).Distinct().ToArray(); - var users = await querySession.LoadManyAsync(cancellation, userIds); - var lookup = users.ToDictionary(u => u.Id); - - foreach (var e in assigned) - { - if (lookup.TryGetValue(e.Data.UserId, out var user)) - { - e.Data.UserName = $"{user.FirstName} {user.LastName}"; - } - } - } -} - -#endregion - -#region Call Order Tracking - -public partial class CallOrderTrackingProjection : EventProjection -{ - private readonly List _callOrder; - - public CallOrderTrackingProjection(List callOrder) - { - _callOrder = callOrder; - } - - public void Project(TaskCreated e, IDocumentOperations ops) - { - _callOrder.Add($"Apply:{nameof(TaskCreated)}"); - } - - public override Task EnrichEventsAsync(IQuerySession querySession, - IReadOnlyList events, CancellationToken cancellation) - { - _callOrder.Add(nameof(EnrichEventsAsync)); - return Task.CompletedTask; - } -} - -#endregion diff --git a/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs b/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs deleted file mode 100644 index c46775773c..0000000000 --- a/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JasperFx.Events; -using JasperFx.Events.Projections; -using Marten; -using Marten.Events.Projections; -using Marten.Testing.Harness; -using Shouldly; -using Xunit; - -namespace EventSourcingTests.Projections; - -/// -/// Document type used in an EventProjection but NOT explicitly registered with Marten. -/// The source generator should discover this type from Store/Insert calls in ApplyAsync -/// and register it automatically. -/// See https://github.com/JasperFx/marten/issues/4166 -/// -public class AuditRecord -{ - public Guid Id { get; set; } - public Guid StreamId { get; set; } - public string EventType { get; set; } = string.Empty; - public DateTimeOffset Timestamp { get; set; } -} - -public class AuditableEvent -{ - public string Description { get; set; } = string.Empty; -} - -/// -/// An EventProjection with an explicit ApplyAsync override that stores a document type -/// using operations.Store<T>(). The source generator should detect this and emit a -/// constructor that registers AuditRecord as a published type. -/// See https://github.com/JasperFx/marten/issues/4166 -/// -public partial class AuditRecordProjection : EventProjection -{ - public override ValueTask ApplyAsync(IDocumentOperations operations, IEvent e, CancellationToken cancellation) - { - switch (e.Data) - { - case AuditableEvent: - operations.Store(new AuditRecord - { - Id = Guid.NewGuid(), - StreamId = e.StreamId, - EventType = e.Data.GetType().Name, - Timestamp = e.Timestamp - }); - break; - } - - return new ValueTask(); - } -} - -/// -/// An EventProjection with conventional Create method that returns a document type. -/// The source generator should register this type via the emitted constructor. -/// -public partial class AuditRecordCreatorProjection : EventProjection -{ - public AuditRecord Create(AuditableEvent e) => new AuditRecord - { - Id = Guid.NewGuid(), - EventType = nameof(AuditableEvent) - }; -} - -public class event_projection_should_register_document_types : OneOffConfigurationsContext -{ - [Fact] - public void explicit_apply_async_projection_should_register_document_types() - { - // Issue #4166: Document types used in operations.Store() inside an explicit - // ApplyAsync override should be automatically discovered and registered. - // 9.0: AllDocumentMappings is lazy now (#4303). Use the public - // AllKnownDocumentTypes() API which triggers full materialization. - StoreOptions(opts => - { - opts.Projections.Add(new AuditRecordProjection(), ProjectionLifecycle.Inline); - }); - - var documentTypes = ((IReadOnlyStoreOptions)theStore.Options) - .AllKnownDocumentTypes() - .Select(x => x.DocumentType).ToList(); - - documentTypes.ShouldContain(typeof(AuditRecord), - "AuditRecord should be auto-discovered from operations.Store() in ApplyAsync"); - } - - [Fact] - public void conventional_create_projection_should_register_document_types() - { - // EventProjection with Create method should also register the return type. - // 9.0: as above — AllDocumentMappings is lazy, AllKnownDocumentTypes() - // is the materialize-then-snapshot accessor. - StoreOptions(opts => - { - opts.Projections.Add(new AuditRecordCreatorProjection(), ProjectionLifecycle.Inline); - }); - - var documentTypes = ((IReadOnlyStoreOptions)theStore.Options) - .AllKnownDocumentTypes() - .Select(x => x.DocumentType).ToList(); - - documentTypes.ShouldContain(typeof(AuditRecord), - "AuditRecord should be auto-discovered from Create method return type"); - } -} diff --git a/src/EventSourcingTests/rebuild_concurrency_cap_resolution.cs b/src/EventSourcingTests/rebuild_concurrency_cap_resolution.cs deleted file mode 100644 index d4229484d5..0000000000 --- a/src/EventSourcingTests/rebuild_concurrency_cap_resolution.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using JasperFx.Events; -using Marten; -using Marten.Testing.Harness; -using Npgsql; -using Shouldly; -using Xunit; - -namespace EventSourcingTests; - -// jasperfx#420 / marten#4710: resolution of the per-database rebuild concurrency cap -// surfaced through IEventStore.MaxConcurrentRebuildsPerDatabase. -public class rebuild_concurrency_cap_resolution: OneOffConfigurationsContext -{ - [Fact] - public void configured_value_wins_over_derived_default() - { - var store = SeparateStore(opts => opts.Projections.MaxConcurrentRebuildsPerDatabase = 3); - ((IEventStore)store).MaxConcurrentRebuildsPerDatabase.ShouldBe(3); - } - - [Fact] - public void non_positive_configured_value_disables_the_cap() - { - var store = SeparateStore(opts => opts.Projections.MaxConcurrentRebuildsPerDatabase = 0); - ((IEventStore)store).MaxConcurrentRebuildsPerDatabase.ShouldBeNull(); - } - - [Fact] - public void derived_default_is_pool_size_over_eight_with_floor_of_one() - { - var connectionString = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString) - { - MaxPoolSize = 64 - }.ConnectionString; - - var store = SeparateStore(opts => opts.Connection(connectionString)); - - ((IEventStore)store).MaxConcurrentRebuildsPerDatabase.ShouldBe(8); - } - - [Fact] - public void derived_default_floors_at_one_for_tiny_pools() - { - var connectionString = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString) - { - MaxPoolSize = 5 - }.ConnectionString; - - var store = SeparateStore(opts => opts.Connection(connectionString)); - - ((IEventStore)store).MaxConcurrentRebuildsPerDatabase.ShouldBe(1); - } - - [Fact] - public async Task usage_descriptor_carries_the_effective_cap() - { - // jasperfx#434: CritterWatch#309's rebuild dispatcher reads the effective cap - // off the EventStoreUsage descriptor rather than guessing. - var store = SeparateStore(opts => opts.Projections.MaxConcurrentRebuildsPerDatabase = 6); - - var usage = await ((IEventStore)store).TryCreateUsage(CancellationToken.None); - - usage.ShouldNotBeNull(); - usage.MaxConcurrentRebuildsPerDatabase.ShouldBe(6); - } -} diff --git a/src/Marten.Testing/Harness/ComplianceQuerySessionAlias.cs b/src/Marten.Testing/Harness/ComplianceQuerySessionAlias.cs index 6a1f34a9c6..decec7b053 100644 --- a/src/Marten.Testing/Harness/ComplianceQuerySessionAlias.cs +++ b/src/Marten.Testing/Harness/ComplianceQuerySessionAlias.cs @@ -3,3 +3,8 @@ // by type name, so a per-consumer global alias lets one shared source file bind to Marten's // IQuerySession here and to Polecat's in Polecat. global using ComplianceQuerySession = Marten.IQuerySession; + +// Same mechanism for the EventProjection suites. Those declare projection types at file scope, so +// they cannot reach the pair their suite class is generic over. +global using ComplianceOperations = Marten.IDocumentOperations; +global using ComplianceEventProjection = Marten.Events.Projections.EventProjection; diff --git a/src/Marten.Testing/Harness/MartenComplianceFixture.cs b/src/Marten.Testing/Harness/MartenComplianceFixture.cs index 7d479f1b66..1ecdd07017 100644 --- a/src/Marten.Testing/Harness/MartenComplianceFixture.cs +++ b/src/Marten.Testing/Harness/MartenComplianceFixture.cs @@ -10,6 +10,7 @@ using JasperFx.Events.Tags; using Marten.Events; using Marten.Services.BatchQuerying; +using Npgsql; namespace Marten.Testing.Harness; @@ -27,12 +28,17 @@ public class MartenComplianceFixture: EventStoreComplianceFixture _store.LightweightSession(); // No shared JasperFx interface declares SaveChangesAsync -- in Marten it lives on @@ -62,8 +81,14 @@ public override Task SaveChangesAsync(IDocumentOperations session, CancellationT $"Marten cannot load documents by an identity of type {id.GetType().FullName}") }; + public override void StoreDocument(IDocumentOperations session, T document) => session.Store(document); + public override JasperFx.Events.IEventStoreOperations EventsFor(IDocumentOperations session) => session.Events; + public override IEventStore EventStore => _store; + + public override IEnumerable AllAggregateTypes() => _store.Options.Projections.AllAggregateTypes(); + public override IComplianceBatch CreateBatch(IQuerySession session) => new MartenComplianceBatch(session.CreateBatchQuery()); @@ -125,6 +150,9 @@ public void Snapshot(SnapshotLifecycle lifecycle) where TDoc : notnull public void LiveAggregation() where TDoc : notnull => _options.Projections.LiveStreamAggregation(); + + public void AddProjection(ProjectionBase projection, ProjectionLifecycle lifecycle) + => _options.Projections.Add((IProjectionSource)projection, lifecycle); } internal class MartenComplianceBatch: IComplianceBatch