Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/configuration/hostbuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ public interface IConfigureMarten
void Configure(IServiceProvider services, StoreOptions options);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/MartenServiceCollectionExtensions.cs#L877-L888' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_IConfigureMarten' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/MartenServiceCollectionExtensions.cs#L880-L891' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_IConfigureMarten' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

You could alternatively implement a custom `IConfigureMarten` (or `IConfigureMarten<T> where T : IDocumentStore` if you're working with multiple databases class like so:
Expand Down Expand Up @@ -317,7 +317,7 @@ public interface IAsyncConfigureMarten
ValueTask Configure(StoreOptions options, CancellationToken cancellationToken);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/MartenServiceCollectionExtensions.cs#L890-L902' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_IAsyncConfigureMarten' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/MartenServiceCollectionExtensions.cs#L893-L905' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_IAsyncConfigureMarten' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

As an example from the tests, here's a custom version that uses the Feature Management service:
Expand Down
15 changes: 12 additions & 3 deletions docs/configuration/prebuilding.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,20 @@ public static class Program
return Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddMartenStore<IThingStore>(opts =>
{
opts.Connection(ConnectionSource.ConnectionString);
opts.RegisterDocumentType<Thing>();
opts.RegisterDocumentType<Thing2>();
opts.RegisterDocumentType<Thing3>();
opts.GeneratedCodeMode = TypeLoadMode.Static;
});

services.AddMartenStore<IOtherStore>(opts =>
{
opts.Connection(ConnectionSource.ConnectionString);
opts.RegisterDocumentType<Target>();
opts.GeneratedCodeMode = TypeLoadMode.Static;
opts.RegisterDocumentType<Target>();

// If you use compiled queries, you will need to register the
// compiled query types with Marten ahead of time
Expand All @@ -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<Activity>().AddSubClass<DaemonTests.TestingSupport.Trip>();

Expand Down Expand Up @@ -210,7 +219,7 @@ public static class Program
}
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CommandLineRunner/Program.cs#L29-L105' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_pre_build_types' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CommandLineRunner/Program.cs#L29-L115' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_pre_build_types' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Okay, after all that, there should be a new command line option called `codegen` for your project. Assuming
Expand Down
4 changes: 2 additions & 2 deletions docs/documents/multi-tenancy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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();
}
Expand Down
6 changes: 3 additions & 3 deletions docs/documents/querying/linq/child-collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions docs/documents/querying/linq/projections.md
Original file line number Diff line number Diff line change
Expand Up @@ -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())
{
Expand Down
197 changes: 29 additions & 168 deletions docs/events/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var store = DocumentStore.For(opts =>
<!-- endSnippet -->

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:
Expand Down Expand Up @@ -54,181 +54,42 @@ public Dictionary<string, object>? Headers { get; protected set; }
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/Internal/Sessions/QuerySession.Metadata.cs#L15-L34' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_session_metadata_tracking' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

::: 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<T>`):
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<T>`).

<!-- snippet: sample_query_event_metadata -->
<a id='snippet-sample_query_event_metadata'></a>
```cs
public interface IEvent
{
/// <summary>
/// Unique identifier for the event. Uses a sequential Guid
/// </summary>
Guid Id { get; set; }

/// <summary>
/// The version of the stream this event reflects. The place in the stream.
/// </summary>
long Version { get; set; }

/// <summary>
/// The sequential order of this event in the entire event store
/// </summary>
long Sequence { get; set; }

/// <summary>
/// The actual event data body
/// </summary>
object Data { get; }

/// <summary>
/// If using Guid's for the stream identity, this will
/// refer to the Stream's Id, otherwise it will always be Guid.Empty
/// </summary>
Guid StreamId { get; set; }

/// <summary>
/// If using strings as the stream identifier, this will refer
/// to the containing Stream's Id
/// </summary>
string? StreamKey { get; set; }

/// <summary>
/// The UTC time that this event was originally captured
/// </summary>
DateTimeOffset Timestamp { get; set; }

/// <summary>
/// If using multi-tenancy by tenant id
/// </summary>
string TenantId { get; set; }

/// <summary>
/// The .Net type of the event body
/// </summary>
Type EventType { get; }

/// <summary>
/// JasperFx.Event's type alias string for the Event type
/// </summary>
string EventTypeName { get; set; }

/// <summary>
/// JasperFx.Events's string representation of the event type
/// in assembly qualified name
/// </summary>
string DotNetTypeName { get; set; }

/// <summary>
/// Optional metadata describing the causation id
/// </summary>
string? CausationId { get; set; }

/// <summary>
/// Optional metadata describing the correlation id
/// </summary>
string? CorrelationId { get; set; }

/// <summary>
/// Optional user defined metadata values. This may be null.
/// </summary>
Dictionary<string, object>? Headers { get; set; }

/// <summary>
/// Has this event been archived and no longer applicable
/// to projected views
/// </summary>
bool IsArchived { get; set; }

/// <summary>
/// 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
/// </summary>
public string? AggregateTypeName { get; set; }

/// <summary>
/// Set an optional user defined metadata value by key
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
void SetHeader(string key, object value);

/// <summary>
/// Get an optional user defined metadata value by key
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
object? GetHeader(string key);

/// <summary>
/// Build a Func that can resolve an identity from the IEvent and even
/// handles the dastardly strong typed identifiers
/// </summary>
/// <typeparam name="TId"></typeparam>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
public static Func<IEvent, TId> CreateAggregateIdentitySource<TId>()
where TId : notnull
{
if (typeof(TId) == typeof(Guid)) return e => e.StreamId.As<TId>();
if (typeof(TId) == typeof(string)) return e => e.StreamKey!.As<TId>();

var valueTypeInfo = ValueTypeInfo.ForType(typeof(TId));

var e = Expression.Parameter(typeof(IEvent), "e");
var eMember = valueTypeInfo.SimpleType == typeof(Guid)
? ReflectionHelper.GetProperty<IEvent>(x => x.StreamId)
: ReflectionHelper.GetProperty<IEvent>(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<Func<IEvent, TId>>(wrapped, e);

return lambda.CompileFast();
}

/// <summary>
/// Optional metadata describing the user name or
/// process name for the unit of work that captured this event
/// </summary>
string? UserName { get; set; }

/// <summary>
/// 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
/// </summary>
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<QuestParty>(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");
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/EventSourcingTests/fetch_a_single_event_with_metadata.cs#L38-L56' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_event_metadata' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

::: tip
To utilize metadata within Projections, see [Using Event Metadata in Aggregates](/events/projections/aggregate-projections#using-event-metadata-in-aggregates).
:::

## Overriding Metadata <Badge type="tip" text="8.4" />

Expand Down
2 changes: 1 addition & 1 deletion docs/events/projections/rebuilding.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,5 @@ on `IDocumentStore`:
```cs
await theStore.Advanced.RebuildSingleStreamAsync<SimpleAggregate>(streamId);
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/EventSourcingTests/Aggregation/rebuilding_a_single_stream_projection.cs#L31-L35' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_rebuild_single_stream' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/EventSourcingTests/Aggregation/rebuilding_a_single_stream_projection.cs#L32-L36' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_rebuild_single_stream' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
27 changes: 24 additions & 3 deletions docs/testing/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<MartenSettings>(s =>
{
s.SchemaName = SchemaName;
Expand All @@ -67,7 +75,7 @@ public class AppFixture: IAsyncLifetime
}
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AspNetCore.Testing/AppFixture.cs#L10-L41' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_integration_appfixture' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AspNetCore.Testing/AppFixture.cs#L10-L49' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_integration_appfixture' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

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:
Expand Down Expand Up @@ -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
Expand All @@ -149,7 +162,7 @@ public abstract class SimplifiedIntegrationContext : IAsyncLifetime
}
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AspNetCore.Testing/Examples/SimplifiedIntegrationContext.cs#L7-L33' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_simplified_integration_context' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AspNetCore.Testing/Examples/SimplifiedIntegrationContext.cs#L7-L38' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_simplified_integration_context' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

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:
Expand Down Expand Up @@ -262,14 +275,22 @@ Host = await AlbaHost.For<Program>(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<MartenSettings>(s =>
{
s.SchemaName = SchemaName;
});
});
});
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AspNetCore.Testing/AppFixture.cs#L22-L33' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_integration_configure_scheme_name' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten.AspNetCore.Testing/AppFixture.cs#L22-L41' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_integration_configure_scheme_name' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

`MartenSettings` is a custom config class, you can customize any way you'd like:
Expand Down
Loading
Loading