diff --git a/docs/configuration/storeoptions.md b/docs/configuration/storeoptions.md
index 5abeb9482a..26fc4912b6 100644
--- a/docs/configuration/storeoptions.md
+++ b/docs/configuration/storeoptions.md
@@ -337,5 +337,5 @@ var store = DocumentStore.For(_ =>
_.NameDataLength = 100;
});
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/diagnostics.md b/docs/diagnostics.md
index abab9fc68d..25b8a39fc3 100644
--- a/docs/diagnostics.md
+++ b/docs/diagnostics.md
@@ -365,7 +365,7 @@ var store = DocumentStore.For(_ =>
_.Logger(new ConsoleMartenLogger());
});
```
-snippet source | anchor
+snippet source | anchor
You can also directly apply a session logger to any `IQuerySession` or `IDocumentSession` like this:
@@ -377,7 +377,7 @@ using var session = store.LightweightSession();
// Replace the logger for only this one session
session.Logger = new RecordingLogger();
```
-snippet source | anchor
+snippet source | anchor
The session logging is a different abstraction specifically so that you _could_ track database commands issued per session. In effect, my own shop is going to use this capability to understand what HTTP endpoints or service bus message handlers are being unnecessarily chatty in their database interactions. We also hope that the contextual logging of commands per document session makes it easier to understand how our systems behave.
diff --git a/docs/documents/execute-custom-sql.md b/docs/documents/execute-custom-sql.md
index b40ae538fe..108fe634de 100644
--- a/docs/documents/execute-custom-sql.md
+++ b/docs/documents/execute-custom-sql.md
@@ -5,7 +5,7 @@ Use `QueueSqlCommand(string sql, params object[] parameterValues)` method to reg
`?` placeholders can be used to denote parameter values. Postgres [type casts `::`](https://www.postgresql.org/docs/15/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS) can be applied to the parameter if needed. If the `?` character is not suitable as a placeholder because you need to use `?` in your sql query, you can change the placeholder by providing an alternative. Pass this in before the sql argument.
-
+
```cs
theSession.QueueSqlCommand("insert into names (name) values ('Jeremy')");
theSession.QueueSqlCommand("insert into names (name) values ('Babu')");
@@ -17,5 +17,5 @@ theSession.QueueSqlCommand("insert into data (raw_value) values (?::jsonb)", jso
// Use ^ as the parameter placeholder
theSession.QueueSqlCommand('^', "insert into data (raw_value) values (^::jsonb)", json);
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/documents/metadata.md b/docs/documents/metadata.md
index a912f458ff..5ece1b4fe9 100644
--- a/docs/documents/metadata.md
+++ b/docs/documents/metadata.md
@@ -46,6 +46,7 @@ var store = DocumentStore.For(opts =>
x.CorrelationId.Enabled = true;
x.CausationId.Enabled = true;
x.Headers.Enabled = true;
+
});
// Or just globally turn on columns for all document
@@ -55,10 +56,13 @@ var store = DocumentStore.For(opts =>
x.Metadata.CausationId.Enabled = true;
x.Metadata.CorrelationId.Enabled = true;
x.Metadata.Headers.Enabled = true;
+
+ // This column is "opt in"
+ x.Metadata.CreatedAt.Enabled = true;
});
});
```
-snippet source | anchor
+snippet source | anchor
Next, you relay the actual values for these fields at the document session level as shown below:
@@ -74,7 +78,7 @@ public void SettingMetadata(IDocumentSession session, string correlationId, stri
session.CausationId = causationId;
}
```
-snippet source | anchor
+snippet source | anchor
Headers are a little bit different, with the ability to set individual header key/value pairs
@@ -88,7 +92,7 @@ public void SetHeader(IDocumentSession session, string sagaId)
session.SetHeader("saga-id", sagaId);
}
```
-snippet source | anchor
+snippet source | anchor
## Tracking Metadata on Documents
@@ -112,7 +116,7 @@ public class DocWithMetadata
public bool IsDeleted { get; set; }
}
```
-snippet source | anchor
+snippet source | anchor
To enable the Marten mapping to metadata values, use this syntax:
@@ -134,7 +138,7 @@ var store = DocumentStore.For(opts =>
});
});
```
-snippet source | anchor
+snippet source | anchor
::: tip
diff --git a/docs/documents/multi-tenancy.md b/docs/documents/multi-tenancy.md
index 9da861f73f..eaad7a507a 100644
--- a/docs/documents/multi-tenancy.md
+++ b/docs/documents/multi-tenancy.md
@@ -446,7 +446,7 @@ builder.Services.AddMarten(opts =>
opts.Policies.PartitionMultiTenantedDocumentsUsingMartenManagement("tenants");
});
```
-snippet source | anchor
+snippet source | anchor
The tenant to partition name mapping will be stored in a table created by Marten called `mt_tenant_partitions` with
@@ -467,7 +467,7 @@ await theStore
// with the named tenant ids
.AddMartenManagedTenantsAsync(CancellationToken.None, "a1", "a2", "a3");
```
-snippet source | anchor
+snippet source | anchor
The API above will try to add any missing table partitions to all known document types. There is also a separate overload
@@ -492,7 +492,7 @@ public class DocThatShouldBeExempted1
public Guid Id { get; set; }
}
```
-snippet source | anchor
+snippet source | anchor
or exempt a single document type through the fluent interface:
@@ -502,7 +502,7 @@ or exempt a single document type through the fluent interface:
```cs
opts.Schema.For().DoNotPartition();
```
-snippet source | anchor
+snippet source | anchor
## Implementation Details
diff --git a/docs/documents/partial-updates-patching.md b/docs/documents/partial-updates-patching.md
index 2a037601a6..b73660cde2 100644
--- a/docs/documents/partial-updates-patching.md
+++ b/docs/documents/partial-updates-patching.md
@@ -37,7 +37,7 @@ To apply a patch to all documents matching a given criteria, use the following s
// Change every Target document where the Color is Blue
theSession.Patch(x => x.Color == Colors.Blue).Set(x => x.Number, 2);
```
-snippet source | anchor
+snippet source | anchor
## Set a single Property/Field
@@ -66,7 +66,7 @@ public async Task set_an_immediate_property_by_id()
}
}
```
-snippet source | anchor
+snippet source | anchor
### Set a new Property/Field
@@ -86,7 +86,7 @@ using (var query = theStore.QuerySession())
query.Query(where).Count.ShouldBe(0);
}
```
-snippet source | anchor
+snippet source | anchor
## Duplicate an existing Property/Field
@@ -110,7 +110,7 @@ using (var query = theStore.QuerySession())
result.AnotherString.ShouldBe(target.String);
}
```
-snippet source | anchor
+snippet source | anchor
The same value can be copied to multiple new locations:
@@ -123,7 +123,7 @@ theSession.Patch(target.Id).Duplicate(t => t.String,
t => t.Inner.String,
t => t.Inner.AnotherString);
```
-snippet source | anchor
+snippet source | anchor
The new locations need not exist in the persisted document, null or absent parents will be initialized
@@ -153,7 +153,7 @@ public async Task increment_for_int()
}
}
```
-snippet source | anchor
+snippet source | anchor
By default, the `Patch.Increment()` operation will add 1 to the existing value. You can optionally override the increment:
@@ -179,7 +179,7 @@ public async Task increment_for_int_with_explicit_increment()
}
}
```
-snippet source | anchor
+snippet source | anchor
## Append Element to a Child Collection
@@ -216,7 +216,7 @@ public async Task append_complex_element()
}
}
```
-snippet source | anchor
+snippet source | anchor
The `Patch.AppendIfNotExists()` operation will treat the child collection as a set rather than a list and only append the element if it does not already exist within the collection
@@ -255,7 +255,7 @@ public async Task insert_first_complex_element()
}
}
```
-snippet source | anchor
+snippet source | anchor
The `Patch.InsertIfNotExists()` operation will only insert the element if the element at the designated index does not already exist.
@@ -294,7 +294,7 @@ public async Task remove_primitive_element()
}
}
```
-snippet source | anchor
+snippet source | anchor
Removing complex items can also be accomplished, matching is performed on all fields:
@@ -326,7 +326,7 @@ public async Task remove_complex_element()
}
}
```
-snippet source | anchor
+snippet source | anchor
To remove reoccurring values from a collection specify `RemoveAction.RemoveAll`:
@@ -368,7 +368,7 @@ public async Task remove_repeated_primitive_elements()
}
}
```
-snippet source | anchor
+snippet source | anchor
## Rename a Property/Field
@@ -401,7 +401,7 @@ public async Task rename_deep_prop()
}
}
```
-snippet source | anchor
+snippet source | anchor
Renaming can be used on nested values.
@@ -418,7 +418,7 @@ To delete a redundant property no longer available on the class use the string o
```cs
theSession.Patch(target.Id).Delete("String");
```
-snippet source | anchor
+snippet source | anchor
To delete a redundant property nested on a child class specify a location lambda:
@@ -428,7 +428,7 @@ To delete a redundant property nested on a child class specify a location lambda
```cs
theSession.Patch(target.Id).Delete("String", t => t.Inner);
```
-snippet source | anchor
+snippet source | anchor
A current property may be erased simply with a lambda:
@@ -438,7 +438,7 @@ A current property may be erased simply with a lambda:
```cs
theSession.Patch(target.Id).Delete(t => t.Inner);
```
-snippet source | anchor
+snippet source | anchor
Many documents may be patched using a where expressions:
@@ -456,7 +456,7 @@ using (var query = theStore.QuerySession())
query.Query(where).Count(t => t.String != null).ShouldBe(0);
}
```
-snippet source | anchor
+snippet source | anchor
## Multi-field patching/chaining patch operations
@@ -484,5 +484,5 @@ public async Task able_to_chain_patch_operations()
}
}
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/documents/plv8.md b/docs/documents/plv8.md
index 3e6b3c1106..8461f7606d 100644
--- a/docs/documents/plv8.md
+++ b/docs/documents/plv8.md
@@ -77,7 +77,7 @@ To apply a patch to all documents matching a given criteria, use the following s
// Change every Target document where the Color is Blue
theSession.Patch(x => x.Color == Colors.Blue).Set(x => x.Number, 2);
```
-snippet source | anchor
+snippet source | anchor
### Setting a single Property/Field
@@ -106,7 +106,7 @@ public async Task set_an_immediate_property_by_id()
}
}
```
-snippet source | anchor
+snippet source | anchor
### Initialize a new Property/Field
@@ -126,7 +126,7 @@ using (var query = theStore.QuerySession())
query.Query(where).Count.ShouldBe(0);
}
```
-snippet source | anchor
+snippet source | anchor
### Duplicating an existing Property/Field
@@ -150,7 +150,7 @@ using (var query = theStore.QuerySession())
result.AnotherString.ShouldBe(target.String);
}
```
-snippet source | anchor
+snippet source | anchor
The same value can be copied to multiple new locations:
@@ -163,7 +163,7 @@ theSession.Patch(target.Id).Duplicate(t => t.String,
t => t.Inner.String,
t => t.Inner.AnotherString);
```
-snippet source | anchor
+snippet source | anchor
The new locations need not exist in the persisted document, null or absent parents will be initialized
@@ -193,7 +193,7 @@ public async Task increment_for_int()
}
}
```
-snippet source | anchor
+snippet source | anchor
By default, the `Patch.Increment()` operation will add 1 to the existing value. You can optionally override the increment:
@@ -219,7 +219,7 @@ public async Task increment_for_int_with_explicit_increment()
}
}
```
-snippet source | anchor
+snippet source | anchor
### Append an Element to a Child Collection
@@ -256,7 +256,7 @@ public async Task append_complex_element()
}
}
```
-snippet source | anchor
+snippet source | anchor
The `Patch.AppendIfNotExists()` operation will treat the child collection as a set rather than a list and only append the element if it does not already exist within the collection
@@ -295,7 +295,7 @@ public async Task insert_first_complex_element()
}
}
```
-snippet source | anchor
+snippet source | anchor
The `Patch.InsertIfNotExists()` operation will only insert the element if the element at the designated index does not already exist.
@@ -333,7 +333,7 @@ public async Task remove_primitive_element()
}
}
```
-snippet source | anchor
+snippet source | anchor
Removing complex items can also be accomplished, matching is performed on all fields:
@@ -365,7 +365,7 @@ public async Task remove_complex_element()
}
}
```
-snippet source | anchor
+snippet source | anchor
To remove reoccurring values from a collection specify `RemoveAction.RemoveAll`:
@@ -407,7 +407,7 @@ public async Task remove_repeated_primitive_elements()
}
}
```
-snippet source | anchor
+snippet source | anchor
### Rename a Property/Field
@@ -440,7 +440,7 @@ public async Task rename_deep_prop()
}
}
```
-snippet source | anchor
+snippet source | anchor
Renaming can be used on nested values.
@@ -457,7 +457,7 @@ To delete a redundant property no longer available on the class use the string o
```cs
theSession.Patch(target.Id).Delete("String");
```
-snippet source | anchor
+snippet source | anchor
To delete a redundant property nested on a child class specify a location lambda:
@@ -467,7 +467,7 @@ To delete a redundant property nested on a child class specify a location lambda
```cs
theSession.Patch(target.Id).Delete("String", t => t.Inner);
```
-snippet source | anchor
+snippet source | anchor
A current property may be erased simply with a lambda:
@@ -477,7 +477,7 @@ A current property may be erased simply with a lambda:
```cs
theSession.Patch(target.Id).Delete(t => t.Inner);
```
-snippet source | anchor
+snippet source | anchor
Many documents may be patched using a where expressions:
@@ -495,7 +495,7 @@ using (var query = theStore.QuerySession())
query.Query(where).Count(t => t.String != null).ShouldBe(0);
}
```
-snippet source | anchor
+snippet source | anchor
## Javascript Transformations
diff --git a/docs/documents/querying/linq/sql.md b/docs/documents/querying/linq/sql.md
index edc1802753..25b4fde6f1 100644
--- a/docs/documents/querying/linq/sql.md
+++ b/docs/documents/querying/linq/sql.md
@@ -26,7 +26,7 @@ public async Task query_with_matches_sql()
Older version of Marten also offer the `MatchesJsonPath()` method which uses the `^` character as a placeholder. This will continue to be supported.
-
+
```cs
var results2 = await theSession
.Query().Where(x => x.MatchesSql('^', "d.data @? '$ ? (@.Children[*] == null || @.Children[*].size() == 0)'"))
@@ -37,5 +37,5 @@ var results3 = await theSession
.Query().Where(x => x.MatchesJsonPath("d.data @? '$ ? (@.Children[*] == null || @.Children[*].size() == 0)'"))
.ToListAsync();
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/events/metadata.md b/docs/events/metadata.md
index cdcaca8cb6..a7413d5460 100644
--- a/docs/events/metadata.md
+++ b/docs/events/metadata.md
@@ -21,7 +21,7 @@ var store = DocumentStore.For(opts =>
opts.Events.MetadataConfig.CorrelationIdEnabled = true;
});
```
-snippet source | anchor
+snippet source | anchor
The actual metadata is accessible from the `IEvent` interface event wrappers as shown below (which are implemented by internal `Event`):
@@ -132,5 +132,5 @@ public interface IEvent
object? GetHeader(string key);
}
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/events/projections/aggregate-projections.md b/docs/events/projections/aggregate-projections.md
index 2149dd3f86..4d8d948784 100644
--- a/docs/events/projections/aggregate-projections.md
+++ b/docs/events/projections/aggregate-projections.md
@@ -178,7 +178,7 @@ public class Payment
}
}
```
-snippet source | anchor
+snippet source | anchor
Just note that for single stream aggregations, your strong typed identifier types will need to wrap either a `Guid` or
@@ -187,7 +187,16 @@ Just note that for single stream aggregations, your strong typed identifier type
At this point, the `FetchForWriting` and `FetchForLatest` APIs do not directly support strongly typed identifiers and you
will have to just pass in the wrapped, primitive value like this:
-snippet: sample_use_fetch_for_writing_with_strong_typed_identifier
+
+
+```cs
+private async Task use_fetch_for_writing_with_strong_typed_identifier(PaymentId id, IDocumentSession session)
+{
+ var stream = await session.Events.FetchForWriting(id.Value);
+}
+```
+snippet source | anchor
+
## Aggregate Creation
@@ -706,8 +715,8 @@ As of Marten 7.33, this mechanism executes for every single event in the current
:::
At any point in an `Apply()` or `Create()` or `ShouldDelete()` method, you can take in either the generic `IEvent` wrapper
-or the specific `IEvent` wrapper type for the specific event. _Sometimes_ though, you may want to automatically take your
-aggregated document with metadata from the very last event the projection is encountering at one time. _If_ you are using
+or the specific `IEvent` wrapper type for the specific event. _Sometimes_ though, you may want to automatically tag your
+aggregated document with metadata from applied events. _If_ you are using
either `SingleStreamProjection` or `MultiStreamProjection` as the base class for a projection, you can
override the `ApplyMetadata(T aggregate, IEvent lastEvent)` method in your projection to manually map event metadata to
your aggregate in any way you wish.
@@ -726,6 +735,8 @@ public class Item
public bool Completed { get; set; }
public string LastModifiedBy { get; set; }
public DateTimeOffset? LastModified { get; set; }
+
+ public int Version { get; set; }
}
public record ItemStarted(string Description);
@@ -757,16 +768,15 @@ public class ItemProjection: SingleStreamProjection-
// Apply the last timestamp
aggregate.LastModified = lastEvent.Timestamp;
- if (lastEvent.Headers.TryGetValue("last-modified-by", out var person))
- {
- aggregate.LastModifiedBy = person?.ToString() ?? "System";
- }
+ var person = lastEvent.GetHeader("last-modified-by");
+
+ aggregate.LastModifiedBy = person?.ToString() ?? "System";
return aggregate;
}
}
```
-snippet source | anchor
+snippet source | anchor
And the same projection in usage in a unit test to see how it's all put together:
@@ -774,37 +784,35 @@ And the same projection in usage in a unit test to see how it's all put together
```cs
-public class using_apply_metadata : OneOffConfigurationsContext
+[Fact]
+public async Task apply_metadata()
{
- [Fact]
- public async Task apply_metadata()
+ StoreOptions(opts =>
{
- StoreOptions(opts =>
- {
- opts.Projections.Add(ProjectionLifecycle.Inline);
+ opts.Projections.Add(ProjectionLifecycle.Inline);
- // THIS IS NECESSARY FOR THIS SAMPLE!
- opts.Events.MetadataConfig.HeadersEnabled = true;
- });
+ // THIS IS NECESSARY FOR THIS SAMPLE!
+ opts.Events.MetadataConfig.HeadersEnabled = true;
+ });
- // Setting a header value on the session, which will get tagged on each
- // event captured by the current session
- theSession.SetHeader("last-modified-by", "Glenn Frey");
+ // Setting a header value on the session, which will get tagged on each
+ // event captured by the current session
+ theSession.SetHeader("last-modified-by", "Glenn Frey");
- var id = theSession.Events.StartStream
- (new ItemStarted("Blue item")).Id;
- await theSession.SaveChangesAsync();
+ var id = theSession.Events.StartStream
- (new ItemStarted("Blue item")).Id;
+ await theSession.SaveChangesAsync();
- theSession.Events.Append(id, new ItemWorked(), new ItemWorked(), new ItemFinished());
- await theSession.SaveChangesAsync();
+ theSession.Events.Append(id, new ItemWorked(), new ItemWorked(), new ItemFinished());
+ await theSession.SaveChangesAsync();
- var item = await theSession.LoadAsync
- (id);
+ var item = await theSession.LoadAsync
- (id);
- // RIP Glenn Frey, take it easy!
- item.LastModifiedBy.ShouldBe("Glenn Frey");
- }
+ // RIP Glenn Frey, take it easy!
+ item.LastModifiedBy.ShouldBe("Glenn Frey");
+ item.Version.ShouldBe(4);
}
```
-snippet source | anchor
+snippet source | anchor
## Raising Events, Messages, or other Operations in Aggregation Projections
diff --git a/docs/events/projections/custom-aggregates.md b/docs/events/projections/custom-aggregates.md
index 8663220c2c..2bacd83220 100644
--- a/docs/events/projections/custom-aggregates.md
+++ b/docs/events/projections/custom-aggregates.md
@@ -33,7 +33,7 @@ public class Increment
{
}
```
-snippet source | anchor
+snippet source | anchor
And a simple aggregate document type like this:
@@ -57,7 +57,7 @@ public class StartAndStopAggregate: ISoftDeleted
}
}
```
-snippet source | anchor
+snippet source | anchor
As you can see, `StartAndStopAggregate` as a `Guid` as its identity and is also [soft-deleted](/documents/deletes.html#soft-deletes) when stored by
@@ -121,6 +121,11 @@ public class StartAndStopProjection: CustomProjectionsnippet source | anchor
+snippet source | anchor
## Custom Grouping
@@ -174,7 +179,7 @@ public class ExplicitCounter: CustomProjection
}
}
```
-snippet source | anchor
+snippet source | anchor
Note that this usage is valid for all possible projection lifecycles now (`Live`, `Inline`, and `Async`).
diff --git a/docs/events/projections/custom.md b/docs/events/projections/custom.md
index 163ffca96b..88cfd0f242 100644
--- a/docs/events/projections/custom.md
+++ b/docs/events/projections/custom.md
@@ -71,7 +71,7 @@ public class QuestPatchTestProjection: IProjection
}
}
```
-snippet source | anchor
+snippet source | anchor
And the custom projection can be registered in your Marten `DocumentStore` like this:
@@ -90,5 +90,5 @@ var store = DocumentStore.For(opts =>
opts.Projections.Add(new QuestPatchTestProjection(), ProjectionLifecycle.Async);
});
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/events/querying.md b/docs/events/querying.md
index 20f05efe04..bc536b28b6 100644
--- a/docs/events/querying.md
+++ b/docs/events/querying.md
@@ -144,7 +144,7 @@ public interface IEvent
object? GetHeader(string key);
}
```
-snippet source | anchor
+snippet source | anchor
## Stream State
diff --git a/docs/events/storage.md b/docs/events/storage.md
index 6c1c4aa688..dfa6ad6961 100644
--- a/docs/events/storage.md
+++ b/docs/events/storage.md
@@ -107,7 +107,7 @@ public string? CorrelationId { get; set; }
///
public Dictionary? Headers { get; set; }
```
-snippet source | anchor
+snippet source | anchor
The full event data is available on `EventStream` and `IEvent` objects immediately after committing a transaction that involves event capture. See [diagnostics and instrumentation](/diagnostics) for more information on capturing event data in the instrumentation hooks.
diff --git a/docs/schema/index.md b/docs/schema/index.md
index 5dd9ba8811..115d0a1092 100644
--- a/docs/schema/index.md
+++ b/docs/schema/index.md
@@ -32,7 +32,7 @@ var store = DocumentStore.For(opts =>
opts.AutoCreateSchemaObjects = AutoCreate.None;
});
```
-snippet source | anchor
+snippet source | anchor
To prevent unnecessary loss of data, even in development, on the first usage of a document type, Marten will:
diff --git a/docs/schema/migrations.md b/docs/schema/migrations.md
index 69621883f9..6dd4a93cb3 100644
--- a/docs/schema/migrations.md
+++ b/docs/schema/migrations.md
@@ -43,7 +43,7 @@ var store = DocumentStore.For(opts =>
opts.AutoCreateSchemaObjects = AutoCreate.None;
});
```
-snippet source | anchor
+snippet source | anchor
As long as you're using a permissive auto creation mode (i.e., not _None_), you should be able to code in your application model
diff --git a/src/EventSourcingTests/Aggregation/using_apply_metadata.cs b/src/EventSourcingTests/Aggregation/using_apply_metadata.cs
index 39e30d74a5..1faab56e62 100644
--- a/src/EventSourcingTests/Aggregation/using_apply_metadata.cs
+++ b/src/EventSourcingTests/Aggregation/using_apply_metadata.cs
@@ -9,10 +9,11 @@
namespace EventSourcingTests.Aggregation;
-#region sample_apply_metadata
+
public class using_apply_metadata : OneOffConfigurationsContext
{
+ #region sample_apply_metadata
[Fact]
public async Task apply_metadata()
{
@@ -40,6 +41,7 @@ public async Task apply_metadata()
item.LastModifiedBy.ShouldBe("Glenn Frey");
item.Version.ShouldBe(4);
}
+#endregion
[Theory]
[InlineData(ProjectionLifecycle.Live)]
@@ -133,9 +135,38 @@ public async Task use_with_fetch_for_writing_for_specific_version(ProjectionLife
item.Aggregate.LastModifiedBy.ShouldBe("Glenn Frey");
item.Aggregate.Version.ShouldBe(4);
}
+
+ [Fact]
+ public async Task apply_metadata_on_record()
+ {
+ StoreOptions(opts =>
+ {
+ opts.Projections.Add(ProjectionLifecycle.Inline);
+
+ // THIS IS NECESSARY FOR THIS SAMPLE!
+ opts.Events.MetadataConfig.HeadersEnabled = true;
+ });
+
+ // Setting a header value on the session, which will get tagged on each
+ // event captured by the current session
+ theSession.SetHeader("last-modified-by", "Glenn Frey");
+
+ var id = theSession.Events.StartStream(new ItemStarted("Blue item")).Id;
+ await theSession.SaveChangesAsync();
+
+ theSession.Events.Append(id, new ItemWorked(), new ItemWorked(), new ItemFinished());
+ await theSession.SaveChangesAsync();
+
+ var item = await theSession.LoadAsync(id);
+
+ // RIP Glenn Frey, take it easy!
+ item.LastModifiedBy.ShouldBe("Glenn Frey");
+ item.Version.ShouldBe(4);
+ }
}
-#endregion
+
+
#region sample_using_ApplyMetadata
@@ -181,13 +212,60 @@ public override Item ApplyMetadata(Item aggregate, IEvent lastEvent)
// Apply the last timestamp
aggregate.LastModified = lastEvent.Timestamp;
- if (lastEvent.Headers.TryGetValue("last-modified-by", out var person))
- {
- aggregate.LastModifiedBy = person?.ToString() ?? "System";
- }
+ var person = lastEvent.GetHeader("last-modified-by");
+
+ aggregate.LastModifiedBy = person?.ToString() ?? "System";
return aggregate;
}
}
#endregion
+
+
+public record ItemRecord(
+ Guid Id,
+ string Description,
+ bool Started,
+ DateTimeOffset WorkedOn,
+ bool Completed,
+ string LastModifiedBy,
+ DateTimeOffset? LastModified,
+ int Version);
+
+
+public class ItemRecordProjection: SingleStreamProjection
+{
+ public ItemRecord Create(ItemStarted started)
+ {
+ return new ItemRecord(
+ Guid.Empty,
+ started.Description,
+ true,
+ default,
+ false,
+ string.Empty,
+ null,
+ 0);
+ }
+
+ public void Apply(ItemRecord item, IEvent worked)
+ {
+ // Nothing, I know, this is weird
+ }
+
+ public ItemRecord Apply(ItemRecord item, ItemFinished finished)
+ {
+ return item with { Completed = true };
+ }
+
+ public override ItemRecord ApplyMetadata(ItemRecord aggregate, IEvent lastEvent)
+ {
+ var person = lastEvent.GetHeader("last-modified-by");
+ return aggregate with
+ {
+ LastModified = lastEvent.Timestamp,
+ LastModifiedBy = person?.ToString() ?? "System"
+ };
+ }
+}
diff --git a/src/Marten/Events/Aggregation/AggregationRuntime.cs b/src/Marten/Events/Aggregation/AggregationRuntime.cs
index e14da4cb19..8bfa6ee87c 100644
--- a/src/Marten/Events/Aggregation/AggregationRuntime.cs
+++ b/src/Marten/Events/Aggregation/AggregationRuntime.cs
@@ -144,7 +144,7 @@ public async ValueTask ApplyChangesAsync(DocumentSessionBase session,
}
}
- var lastEvent = tryApplyMetadata(slice, aggregate);
+ (var lastEvent, aggregate) = tryApplyMetadata(slice, aggregate);
maybeArchiveStream(session, slice);
@@ -191,7 +191,7 @@ private void maybeArchiveStream(DocumentSessionBase session, EventSlice slice, TDoc? aggregate)
+ private (IEvent?, TDoc?) tryApplyMetadata(EventSlice slice, TDoc? aggregate)
{
var lastEvent = slice.Events().LastOrDefault();
if (aggregate != null)
@@ -201,11 +201,11 @@ private void maybeArchiveStream(DocumentSessionBase session, EventSlice slice)
diff --git a/src/Marten/Events/Aggregation/CustomProjection.cs b/src/Marten/Events/Aggregation/CustomProjection.cs
index d3a451ddee..5001f2d2b6 100644
--- a/src/Marten/Events/Aggregation/CustomProjection.cs
+++ b/src/Marten/Events/Aggregation/CustomProjection.cs
@@ -140,8 +140,11 @@ public virtual async ValueTask ApplyChangesAsync(DocumentSessionBase session, Ev
var snapshot = slice.Aggregate;
snapshot = await BuildAsync(session, snapshot, slice.Events()).ConfigureAwait(false);
- ApplyMetadata(snapshot, slice.Events().Last());
+ foreach (var @event in slice.Events())
+ {
+ snapshot = ApplyMetadata(snapshot, @event);
+ }
session.StorageFor().SetIdentity(snapshot, slice.Id);
slice.Aggregate = snapshot;
@@ -347,7 +350,7 @@ public object ApplyMetadata(object aggregate, IEvent lastEvent)
}
///
- /// Template method that is called on the last event in a slice of events that
+ /// Template method that is called for each event in a slice of events that
/// are updating an aggregate. This was added specifically to add metadata like "LastModifiedBy"
/// from the last event to an aggregate with user-defined logic. Override this for your own specific logic
///
@@ -381,7 +384,10 @@ async ValueTask ILiveAggregator.BuildAsync(IReadOnlyList eve
if (Lifecycle == ProjectionLifecycle.Live)
{
slice.Aggregate = await BuildAsync(session, slice.Aggregate, slice.Events()).ConfigureAwait(false);
- ApplyMetadata(slice.Aggregate, events.Last());
+ foreach (var @event in events)
+ {
+ slice.Aggregate = ApplyMetadata(slice.Aggregate, @event);
+ }
}
else
{