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()