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