diff --git a/docs/configuration/hostbuilder.md b/docs/configuration/hostbuilder.md index 57661ab6c8..d61eb2978e 100644 --- a/docs/configuration/hostbuilder.md +++ b/docs/configuration/hostbuilder.md @@ -253,7 +253,7 @@ public interface IConfigureMarten void Configure(IServiceProvider services, StoreOptions options); } ``` -snippet source | anchor +snippet source | anchor You could alternatively implement a custom `IConfigureMarten` (or `IConfigureMarten where T : IDocumentStore` if you're working with multiple databases class like so: @@ -317,7 +317,7 @@ public interface IAsyncConfigureMarten ValueTask Configure(StoreOptions options, CancellationToken cancellationToken); } ``` -snippet source | anchor +snippet source | anchor As an example from the tests, here's a custom version that uses the Feature Management service: diff --git a/docs/configuration/prebuilding.md b/docs/configuration/prebuilding.md index 2e7901be18..7002bc7879 100644 --- a/docs/configuration/prebuilding.md +++ b/docs/configuration/prebuilding.md @@ -148,11 +148,20 @@ public static class Program return Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { + services.AddMartenStore(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.RegisterDocumentType(); + opts.RegisterDocumentType(); + opts.RegisterDocumentType(); + opts.GeneratedCodeMode = TypeLoadMode.Static; + }); + services.AddMartenStore(opts => { opts.Connection(ConnectionSource.ConnectionString); - opts.RegisterDocumentType(); opts.GeneratedCodeMode = TypeLoadMode.Static; + opts.RegisterDocumentType(); // If you use compiled queries, you will need to register the // compiled query types with Marten ahead of time @@ -176,7 +185,7 @@ public static class Program // This is important, setting this option tells Marten to // *try* to use pre-generated code at runtime - opts.GeneratedCodeMode = TypeLoadMode.Static; + //opts.GeneratedCodeMode = TypeLoadMode.Static; //opts.Schema.For().AddSubClass(); @@ -210,7 +219,7 @@ public static class Program } } ``` -snippet source | anchor +snippet source | anchor Okay, after all that, there should be a new command line option called `codegen` for your project. Assuming diff --git a/docs/documents/multi-tenancy.md b/docs/documents/multi-tenancy.md index 1b29741349..6bd21bebb8 100644 --- a/docs/documents/multi-tenancy.md +++ b/docs/documents/multi-tenancy.md @@ -16,7 +16,7 @@ The following sample demonstrates scoping a document session to tenancy identifi // Write some User documents to tenant "tenant1" using (var session = theStore.LightweightSession("tenant1")) { - session.Store(new User { Id = "u1", UserName = "Bill", Roles = new[] { "admin" } }); + session.Store(new User { Id = "u1", UserName = "Bill", Roles = ["admin"] }); session.Store(new User { Id = "u2", UserName = "Lindsey", Roles = [] }); await session.SaveChangesAsync(); } @@ -27,7 +27,7 @@ using (var session = theStore.LightweightSession("tenant1")) // Write some User documents to tenant "tenant1" using (var session = theStore.LightweightSession("tenant1")) { - session.Store(new User { Id = "u1", UserName = "Bill", Roles = new[] { "admin" } }); + session.Store(new User { Id = "u1", UserName = "Bill", Roles = ["admin"] }); session.Store(new User { Id = "u2", UserName = "Lindsey", Roles = [] }); await session.SaveChangesAsync(); } diff --git a/docs/documents/querying/linq/child-collections.md b/docs/documents/querying/linq/child-collections.md index c7a60f6e13..605db8654f 100644 --- a/docs/documents/querying/linq/child-collections.md +++ b/docs/documents/querying/linq/child-collections.md @@ -98,9 +98,9 @@ As of now, Marten allows you to do "contains" searches within Arrays, Lists & IL ```cs public async Task query_against_string_array() { - var doc1 = new DocWithArrays { Strings = new[] { "a", "b", "c" } }; - var doc2 = new DocWithArrays { Strings = new[] { "c", "d", "e" } }; - var doc3 = new DocWithArrays { Strings = new[] { "d", "e", "f" } }; + var doc1 = new DocWithArrays { Strings = ["a", "b", "c"] }; + var doc2 = new DocWithArrays { Strings = ["c", "d", "e"] }; + var doc3 = new DocWithArrays { Strings = ["d", "e", "f"] }; theSession.Store(doc1); theSession.Store(doc2); diff --git a/docs/documents/querying/linq/projections.md b/docs/documents/querying/linq/projections.md index ee11d4a5ce..eb6f805f3e 100644 --- a/docs/documents/querying/linq/projections.md +++ b/docs/documents/querying/linq/projections.md @@ -140,9 +140,9 @@ Marten has the ability to use the `SelectMany()` operator to issue queries again [Fact] public async Task can_do_simple_select_many_against_simple_array() { - var product1 = new Product {Tags = new[] {"a", "b", "c"}}; - var product2 = new Product {Tags = new[] {"b", "c", "d"}}; - var product3 = new Product {Tags = new[] {"d", "e", "f"}}; + var product1 = new Product {Tags = ["a", "b", "c"]}; + var product2 = new Product {Tags = ["b", "c", "d"]}; + var product3 = new Product {Tags = ["d", "e", "f"]}; using (var session = theStore.LightweightSession()) { diff --git a/docs/events/metadata.md b/docs/events/metadata.md index 399c63baa4..ee99f04e53 100644 --- a/docs/events/metadata.md +++ b/docs/events/metadata.md @@ -26,7 +26,7 @@ var store = DocumentStore.For(opts => By default, Marten runs "lean" by omitting the extra metadata storage on events shown above. Causation, correlation, user name (last modified by), and header fields must be individually enabled. -Event the database table columns for this data will not be created unless you opt in +The database table columns for this data will not be created unless you opt-in. When appending events, Marten will automatically tag events with the data from these properties on the `IDocumentSession` when capturing the new events: @@ -54,181 +54,42 @@ public Dictionary? Headers { get; protected set; } snippet source | anchor -::: warning -Open Telemetry `Activity` (spans) are only emitted if there is an active listener for your application. -::: - -In the data elements above, the correlation id and causation id is taken automatically from any active Open Telemetry span, -so these values should just flow from ASP.Net Core requests or typical message bus handlers (like Wolverine!) when Open Telemetry +The `CorrelationId` and `CausationId` is taken automatically from any active OpenTelemetry span, +so these values should just flow from ASP.NET Core requests or typical message bus handlers (like Wolverine!) when OpenTelemetry spans are enabled and being emitted. Values for `IDocumentSession.LastModifiedBy` and `IDocumentSession.Headers` will need to be set manually, but once they are, those values will flow through to new events captured by a session when `SaveChangesAsync()` is called. -::: tip -The basic [IEvent](https://github.com/JasperFx/jasperfx/blob/main/src/JasperFx.Events/Event.cs#L34-L176) abstraction and quite a bit of other generic -event sourcing code moved in Marten 8.0 to the shared JasperFx.Events library. -::: - -The actual metadata is accessible from the `IEvent` interface event wrappers as shown below (which are implemented by `Event`): +The actual metadata is accessible from the [IEvent](https://github.com/JasperFx/jasperfx/blob/main/src/JasperFx.Events/Event.cs#L34-L176) interface wrapper as shown (which is implemented by `Event`). + + ```cs -public interface IEvent -{ - /// - /// Unique identifier for the event. Uses a sequential Guid - /// - Guid Id { get; set; } - - /// - /// The version of the stream this event reflects. The place in the stream. - /// - long Version { get; set; } - - /// - /// The sequential order of this event in the entire event store - /// - long Sequence { get; set; } - - /// - /// The actual event data body - /// - object Data { get; } - - /// - /// If using Guid's for the stream identity, this will - /// refer to the Stream's Id, otherwise it will always be Guid.Empty - /// - Guid StreamId { get; set; } - - /// - /// If using strings as the stream identifier, this will refer - /// to the containing Stream's Id - /// - string? StreamKey { get; set; } - - /// - /// The UTC time that this event was originally captured - /// - DateTimeOffset Timestamp { get; set; } - - /// - /// If using multi-tenancy by tenant id - /// - string TenantId { get; set; } - - /// - /// The .Net type of the event body - /// - Type EventType { get; } - - /// - /// JasperFx.Event's type alias string for the Event type - /// - string EventTypeName { get; set; } - - /// - /// JasperFx.Events's string representation of the event type - /// in assembly qualified name - /// - string DotNetTypeName { get; set; } - - /// - /// Optional metadata describing the causation id - /// - string? CausationId { get; set; } - - /// - /// Optional metadata describing the correlation id - /// - string? CorrelationId { get; set; } - - /// - /// Optional user defined metadata values. This may be null. - /// - Dictionary? Headers { get; set; } - - /// - /// Has this event been archived and no longer applicable - /// to projected views - /// - bool IsArchived { get; set; } - - /// - /// JasperFx.Events's name for the aggregate type that will be persisted - /// to the streams table. This will only be available when running - /// within the Async Daemon - /// - public string? AggregateTypeName { get; set; } - - /// - /// Set an optional user defined metadata value by key - /// - /// - /// - void SetHeader(string key, object value); - - /// - /// Get an optional user defined metadata value by key - /// - /// - /// - object? GetHeader(string key); - - /// - /// Build a Func that can resolve an identity from the IEvent and even - /// handles the dastardly strong typed identifiers - /// - /// - /// - /// - public static Func CreateAggregateIdentitySource() - where TId : notnull - { - if (typeof(TId) == typeof(Guid)) return e => e.StreamId.As(); - if (typeof(TId) == typeof(string)) return e => e.StreamKey!.As(); - - var valueTypeInfo = ValueTypeInfo.ForType(typeof(TId)); - - var e = Expression.Parameter(typeof(IEvent), "e"); - var eMember = valueTypeInfo.SimpleType == typeof(Guid) - ? ReflectionHelper.GetProperty(x => x.StreamId) - : ReflectionHelper.GetProperty(x => x.StreamKey!); - - var raw = Expression.Call(e, eMember.GetMethod!); - Expression? wrapped = null; - if (valueTypeInfo.Builder != null) - { - wrapped = Expression.Call(null, valueTypeInfo.Builder, raw); - } - else if (valueTypeInfo.Ctor != null) - { - wrapped = Expression.New(valueTypeInfo.Ctor, raw); - } - else - { - throw new NotSupportedException("Cannot build a type converter for strong typed id type " + - valueTypeInfo.OuterType.FullNameInCode()); - } - - var lambda = Expression.Lambda>(wrapped, e); - - return lambda.CompileFast(); - } - - /// - /// Optional metadata describing the user name or - /// process name for the unit of work that captured this event - /// - string? UserName { get; set; } - - /// - /// No, this is *not* idiomatic event sourcing, but this may be used as metadata to direct - /// projection replays or subscription rewinding as an event that should not be used - /// - bool IsSkipped { get; set; } -} +// Apply metadata to the IDocumentSession +theSession.CorrelationId = "The Correlation"; +theSession.CausationId = "The Cause"; +theSession.LastModifiedBy = "Last Person"; +theSession.SetHeader("HeaderKey", "HeaderValue"); + +var streamId = theSession.Events + .StartStream(started, joined, slayed1, slayed2, joined2).Id; +await theSession.SaveChangesAsync(); + +var events = await theSession.Events.FetchStreamAsync(streamId); +events.Count.ShouldBe(5); +// Inspect metadata +events.ShouldAllBe(e => + e.Headers != null && e.Headers.ContainsKey("HeaderKey") && "HeaderValue".Equals(e.Headers["HeaderKey"])); +events.ShouldAllBe(e => e.CorrelationId == "The Correlation"); +events.ShouldAllBe(e => e.CausationId == "The Cause"); ``` +snippet source | anchor + + +::: tip + To utilize metadata within Projections, see [Using Event Metadata in Aggregates](/events/projections/aggregate-projections#using-event-metadata-in-aggregates). +::: ## Overriding Metadata diff --git a/docs/events/projections/rebuilding.md b/docs/events/projections/rebuilding.md index 2cd14cf153..55c077dab7 100644 --- a/docs/events/projections/rebuilding.md +++ b/docs/events/projections/rebuilding.md @@ -96,5 +96,5 @@ on `IDocumentStore`: ```cs await theStore.Advanced.RebuildSingleStreamAsync(streamId); ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/testing/integration.md b/docs/testing/integration.md index dea3c982b6..2be3521f1d 100644 --- a/docs/testing/integration.md +++ b/docs/testing/integration.md @@ -53,6 +53,14 @@ public class AppFixture: IAsyncLifetime { b.ConfigureServices((context, services) => { + // Important! You can make your test harness work a little faster (important on its own) + // and probably be more reliable by overriding your Marten configuration to run all + // async daemons in "Solo" mode so they spin up faster and there's no issues from + // PostgreSQL having trouble with advisory locks when projections are rapidly started and stopped + + // This was added in V8.8 + services.MartenDaemonModeIsSolo(); + services.Configure(s => { s.SchemaName = SchemaName; @@ -67,7 +75,7 @@ public class AppFixture: IAsyncLifetime } } ``` -snippet source | anchor +snippet source | anchor To prevent spinning up the entire host (and database setup) for every test (in parallel) you could create a collection fixture to share between your tests: @@ -138,6 +146,11 @@ public abstract class SimplifiedIntegrationContext : IAsyncLifetime { // Using Marten, wipe out all data and reset the state await Store.Advanced.ResetAllData(); + + // OR if you use the async daemon in your tests, use this + // instead to do the above, but also cleanly stop all projections, + // reset the data, then start all async projections and subscriptions up again + await Host.ResetAllMartenDataAsync(); } // This is required because of the IAsyncLifetime @@ -149,7 +162,7 @@ public abstract class SimplifiedIntegrationContext : IAsyncLifetime } } ``` -snippet source | anchor +snippet source | anchor If you're working with [multiple Marten databases](/configuration/hostbuilder#working-with-multiple-marten-databases), you can use the `IDocumentStore` extension method to get the store by its interface type: @@ -262,6 +275,14 @@ Host = await AlbaHost.For(b => { b.ConfigureServices((context, services) => { + // Important! You can make your test harness work a little faster (important on its own) + // and probably be more reliable by overriding your Marten configuration to run all + // async daemons in "Solo" mode so they spin up faster and there's no issues from + // PostgreSQL having trouble with advisory locks when projections are rapidly started and stopped + + // This was added in V8.8 + services.MartenDaemonModeIsSolo(); + services.Configure(s => { s.SchemaName = SchemaName; @@ -269,7 +290,7 @@ Host = await AlbaHost.For(b => }); }); ``` -snippet source | anchor +snippet source | anchor `MartenSettings` is a custom config class, you can customize any way you'd like: diff --git a/src/EventSourcingTests/fetch_a_single_event_with_metadata.cs b/src/EventSourcingTests/fetch_a_single_event_with_metadata.cs index 706eb9d3f5..a03d55f78e 100644 --- a/src/EventSourcingTests/fetch_a_single_event_with_metadata.cs +++ b/src/EventSourcingTests/fetch_a_single_event_with_metadata.cs @@ -24,32 +24,6 @@ public class fetch_a_single_event_with_metadata: IntegrationContext private readonly MembersJoined joined2 = new MembersJoined { Day = 5, Location = "Sendaria", Members = new string[] { "Silk", "Barak" } }; - [Fact] - public async Task fetch_with_metadata_synchronously() - { - StoreOptions(x => - { - x.Events.MetadataConfig.HeadersEnabled = true; - x.Events.MetadataConfig.CausationIdEnabled = true; - x.Events.MetadataConfig.CorrelationIdEnabled = true; - }); - - theSession.CorrelationId = "The Correlation"; - theSession.CausationId = "The Cause"; - theSession.LastModifiedBy = "Last Person"; - theSession.SetHeader("HeaderKey", "HeaderValue"); - - var streamId = theSession.Events - .StartStream(started, joined, slayed1, slayed2, joined2).Id; - await theSession.SaveChangesAsync(); - - var events = await theSession.Events.FetchStreamAsync(streamId); - events.Count.ShouldBe(5); - events.ShouldAllBe(e => - e.Headers != null && e.Headers.ContainsKey("HeaderKey") && "HeaderValue".Equals(e.Headers["HeaderKey"])); - events.ShouldAllBe(e => e.CorrelationId == "The Correlation"); - events.ShouldAllBe(e => e.CausationId == "The Cause"); - } [Fact] public async Task fetch_with_metadata_asynchronously() @@ -61,6 +35,8 @@ public async Task fetch_with_metadata_asynchronously() x.Events.MetadataConfig.CorrelationIdEnabled = true; }); +#region sample_query_event_metadata + // Apply metadata to the IDocumentSession theSession.CorrelationId = "The Correlation"; theSession.CausationId = "The Cause"; theSession.LastModifiedBy = "Last Person"; @@ -72,33 +48,14 @@ public async Task fetch_with_metadata_asynchronously() var events = await theSession.Events.FetchStreamAsync(streamId); events.Count.ShouldBe(5); + // Inspect metadata events.ShouldAllBe(e => e.Headers != null && e.Headers.ContainsKey("HeaderKey") && "HeaderValue".Equals(e.Headers["HeaderKey"])); events.ShouldAllBe(e => e.CorrelationId == "The Correlation"); events.ShouldAllBe(e => e.CausationId == "The Cause"); +#endregion } - [Fact] - public async Task fetch_synchronously() - { - var streamId = theSession.Events - .StartStream(started, joined, slayed1, slayed2, joined2).Id; - await theSession.SaveChangesAsync(); - - var events = await theSession.Events.FetchStreamAsync(streamId); - - (await theSession.Events.LoadAsync(Guid.NewGuid())).ShouldBeNull(); - - // Knowing the event type - var slayed1_2 = (await theSession.Events.LoadAsync(events[2].Id)); - slayed1_2.Version.ShouldBe(3); - slayed1_2.Data.Name.ShouldBe("Troll"); - - // Not knowing the event type - var slayed1_3 = (await theSession.Events.LoadAsync(events[2].Id)).ShouldBeOfType>(); - slayed1_3.Version.ShouldBe(3); - slayed1_3.Data.Name.ShouldBe("Troll"); - } [Fact] public async Task fetch_asynchronously()