Skip to content

Commit 3e3e7eb

Browse files
Merge pull request #515 from Eventuous/feat/stream-event-created
Add Created timestamp to StreamEvent
2 parents 71036e9 + 86fc4be commit 3e3e7eb

7 files changed

Lines changed: 31 additions & 29 deletions

File tree

docs/src/content/docs/persistence/serialisation.md

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,29 @@ Then, you can call this code in your bootstrap code:
5252
BookingEvents.MapBookingEvents();
5353
```
5454

55-
### Auto-registration of types
55+
### Auto-registration with source generator
5656

57-
For convenience purposes, you can avoid manual mapping between type names and types by using the `EventType` attribute.
57+
The recommended way to register event types is to use the `[EventType]` attribute combined with the Eventuous source generator. The generator automatically discovers all types decorated with `[EventType]` in your project and generates a module initializer that registers them at startup — no manual registration code needed.
5858

59-
Annotate your events with it like this:
59+
Annotate your events with the `[EventType]` attribute:
6060

6161
```csharp
6262
[EventType("V1.FullyPaid")]
6363
public record BookingFullyPaid(string BookingId, DateTimeOffset FullyPaidAt);
64+
65+
[EventType("V1.RoomBooked")]
66+
public record RoomBooked(string RoomId, LocalDate CheckIn, LocalDate CheckOut, float Price);
6467
```
6568

66-
Then, use the registration code in the bootstrap code:
69+
That's it. The source generator produces a module initializer class per assembly, which calls `TypeMap.Instance.AddType(...)` for each annotated event type. Registration happens automatically when the assembly is loaded — you don't need to write any startup code.
70+
71+
:::tip
72+
Eventuous also includes a diagnostic analyzer (`EVTC001`) that warns you when an event type is used in aggregates or state projections but is missing the `[EventType]` attribute.
73+
:::
74+
75+
### Reflection-based registration
76+
77+
As an alternative to the source generator, you can use reflection-based registration. This scans assemblies at runtime for types decorated with `[EventType]`:
6778

6879
```csharp
6980
TypeMap.RegisterKnownEventTypes();
@@ -75,23 +86,9 @@ The registration won't work if event classes are defined in another assembly, wh
7586
TypeMap.RegisterKnownEventTypes(typeof(BookingFullyPaid).Assembly);
7687
```
7788

78-
If you use the .NET version that supports module initializers, you can register event types in the module. For example, if the domain event classes are located in a separate project, add the file `DomainModule.cs` to that project with the following code:
79-
80-
```csharp title="DomainModule.cs"
81-
using System.Diagnostics.CodeAnalysis;
82-
using System.Runtime.CompilerServices;
83-
using Eventuous;
84-
85-
namespace Bookings.Domain;
86-
87-
static class DomainModule {
88-
[ModuleInitializer]
89-
[SuppressMessage("Usage", "CA2255", MessageId = "The \'ModuleInitializer\' attribute should not be used in libraries")]
90-
internal static void InitializeDomainModule() => TypeMap.RegisterKnownEventTypes();
91-
}
92-
```
93-
94-
Then, you won't need to call the `TypeMap` registration in the application code at all.
89+
:::note
90+
With the source generator in place, calling `RegisterKnownEventTypes()` is typically unnecessary. The generator handles registration at compile time, which is both more reliable and avoids the overhead of runtime assembly scanning.
91+
:::
9592

9693
### Default serializer
9794

src/Core/src/Eventuous.Persistence/StreamEvent.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ public record struct StreamEvent(
1818
Metadata Metadata,
1919
string ContentType,
2020
long Revision,
21+
DateTime Created = default,
2122
bool FromArchive = false
2223
);

src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticEventStore.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ PersistedEvent AsDocument(NewStreamEvent evt, long position)
3131
(ulong)position + 1,
3232
evt.Payload,
3333
evt.Metadata.ToHeaders(),
34-
DateTime.Now
34+
DateTime.UtcNow
3535
);
3636
}
3737

@@ -87,7 +87,8 @@ async Task<StreamEvent[]> ReadEvents(Func<QueryContainerDescriptor<PersistedEven
8787
x.Message,
8888
Metadata.FromHeaders(x.Metadata),
8989
x.ContentType,
90-
x.StreamPosition
90+
x.StreamPosition,
91+
x.Created
9192
)
9293
)
9394
.ToArray();

src/KurrentDB/src/Eventuous.KurrentDB/KurrentDBEventStore.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,8 @@ StreamEvent AsStreamEvent(object payload)
382382
payload,
383383
DeserializeMetadata() ?? new Metadata(),
384384
resolvedEvent.Event.ContentType,
385-
resolvedEvent.Event.EventNumber.ToInt64()
385+
resolvedEvent.Event.EventNumber.ToInt64(),
386+
resolvedEvent.Event.Created
386387
);
387388
}
388389

src/Redis/src/Eventuous.Redis/RedisStore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,6 @@ static StreamEvent ToStreamEvent(StreamEntry evt, IEventSerializer serializer, I
127127
};
128128

129129
StreamEvent AsStreamEvent(object payload)
130-
=> new(Guid.Parse(evt[MessageId].ToString()), payload, meta ?? new Metadata(), ContentType, evt.Id.ToLong());
130+
=> new(Guid.Parse(evt[MessageId].ToString()), payload, meta ?? new Metadata(), ContentType, evt.Id.ToLong(), DateTime.Parse(evt[Created]!, CultureInfo.InvariantCulture));
131131
}
132132
}

src/Relational/src/Eventuous.Sql.Base/SqlEventStoreBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ StreamEvent ToStreamEvent(PersistedEvent evt) {
144144
_ => throw new("Unknown deserialization result")
145145
};
146146

147-
StreamEvent AsStreamEvent(object payload) => new(evt.MessageId, payload, meta ?? new Metadata(), ContentType, evt.StreamPosition);
147+
StreamEvent AsStreamEvent(object payload) => new(evt.MessageId, payload, meta ?? new Metadata(), ContentType, evt.StreamPosition, evt.Created);
148148
}
149149

150150
/// <inheritdoc />

src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ CancellationToken cancellationToken
2222
) {
2323
var existing = _storage.GetOrAdd(stream, s => new(s));
2424
existing.AppendEvents(expectedVersion, events);
25-
_global.AddRange(events.Select((x, i) => new StreamEvent(x.Id, x.Payload, x.Metadata, "application/json", _global.Count + i)));
25+
var now = DateTime.UtcNow;
26+
_global.AddRange(events.Select((x, i) => new StreamEvent(x.Id, x.Payload, x.Metadata, "application/json", _global.Count + i, now)));
2627

2728
return Task.FromResult(new AppendEventsResult((ulong)(_global.Count - 1), existing.Version));
2829
}
@@ -33,9 +34,10 @@ public Task<AppendEventsResult[]> AppendEvents(IReadOnlyCollection<NewStreamAppe
3334
var i = 0;
3435

3536
foreach (var append in appends) {
37+
var now = DateTime.UtcNow;
3638
var existing = _storage.GetOrAdd(append.StreamName, s => new(s));
3739
existing.AppendEvents(append.ExpectedVersion, append.Events);
38-
_global.AddRange(append.Events.Select((x, j) => new StreamEvent(x.Id, x.Payload, x.Metadata, "application/json", _global.Count + j)));
40+
_global.AddRange(append.Events.Select((x, j) => new StreamEvent(x.Id, x.Payload, x.Metadata, "application/json", _global.Count + j, now)));
3941
results[i++] = new AppendEventsResult((ulong)(_global.Count - 1), existing.Version);
4042
}
4143

@@ -97,7 +99,7 @@ public void AppendEvents(ExpectedStreamVersion expectedVersion, IReadOnlyCollect
9799

98100
foreach (var newEvent in events) {
99101
var version = ++Version;
100-
var streamEvent = new StreamEvent(newEvent.Id, newEvent.Payload, newEvent.Metadata, "application/json", version);
102+
var streamEvent = new StreamEvent(newEvent.Id, newEvent.Payload, newEvent.Metadata, "application/json", version, DateTime.UtcNow);
101103
_events.Add(new(streamEvent, version));
102104
}
103105
}

0 commit comments

Comments
 (0)