diff --git a/docs/src/content/docs/persistence/serialisation.md b/docs/src/content/docs/persistence/serialisation.md index 06e12ea57..9ca3b2bc1 100644 --- a/docs/src/content/docs/persistence/serialisation.md +++ b/docs/src/content/docs/persistence/serialisation.md @@ -52,18 +52,29 @@ Then, you can call this code in your bootstrap code: BookingEvents.MapBookingEvents(); ``` -### Auto-registration of types +### Auto-registration with source generator -For convenience purposes, you can avoid manual mapping between type names and types by using the `EventType` attribute. +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. -Annotate your events with it like this: +Annotate your events with the `[EventType]` attribute: ```csharp [EventType("V1.FullyPaid")] public record BookingFullyPaid(string BookingId, DateTimeOffset FullyPaidAt); + +[EventType("V1.RoomBooked")] +public record RoomBooked(string RoomId, LocalDate CheckIn, LocalDate CheckOut, float Price); ``` -Then, use the registration code in the bootstrap code: +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. + +:::tip +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. +::: + +### Reflection-based registration + +As an alternative to the source generator, you can use reflection-based registration. This scans assemblies at runtime for types decorated with `[EventType]`: ```csharp TypeMap.RegisterKnownEventTypes(); @@ -75,23 +86,9 @@ The registration won't work if event classes are defined in another assembly, wh TypeMap.RegisterKnownEventTypes(typeof(BookingFullyPaid).Assembly); ``` -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: - -```csharp title="DomainModule.cs" -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Eventuous; - -namespace Bookings.Domain; - -static class DomainModule { - [ModuleInitializer] - [SuppressMessage("Usage", "CA2255", MessageId = "The \'ModuleInitializer\' attribute should not be used in libraries")] - internal static void InitializeDomainModule() => TypeMap.RegisterKnownEventTypes(); -} -``` - -Then, you won't need to call the `TypeMap` registration in the application code at all. +:::note +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. +::: ### Default serializer diff --git a/src/Core/src/Eventuous.Persistence/StreamEvent.cs b/src/Core/src/Eventuous.Persistence/StreamEvent.cs index 1903bd006..4da7cf4da 100644 --- a/src/Core/src/Eventuous.Persistence/StreamEvent.cs +++ b/src/Core/src/Eventuous.Persistence/StreamEvent.cs @@ -18,5 +18,6 @@ public record struct StreamEvent( Metadata Metadata, string ContentType, long Revision, + DateTime Created = default, bool FromArchive = false ); diff --git a/src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticEventStore.cs b/src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticEventStore.cs index 09d753097..f89893ce0 100644 --- a/src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticEventStore.cs +++ b/src/Experimental/src/Eventuous.ElasticSearch/Store/ElasticEventStore.cs @@ -31,7 +31,7 @@ PersistedEvent AsDocument(NewStreamEvent evt, long position) (ulong)position + 1, evt.Payload, evt.Metadata.ToHeaders(), - DateTime.Now + DateTime.UtcNow ); } @@ -87,7 +87,8 @@ async Task ReadEvents(Func new(Guid.Parse(evt[MessageId].ToString()), payload, meta ?? new Metadata(), ContentType, evt.Id.ToLong()); + => new(Guid.Parse(evt[MessageId].ToString()), payload, meta ?? new Metadata(), ContentType, evt.Id.ToLong(), DateTime.Parse(evt[Created]!, CultureInfo.InvariantCulture)); } } diff --git a/src/Relational/src/Eventuous.Sql.Base/SqlEventStoreBase.cs b/src/Relational/src/Eventuous.Sql.Base/SqlEventStoreBase.cs index 1a207f874..886400226 100644 --- a/src/Relational/src/Eventuous.Sql.Base/SqlEventStoreBase.cs +++ b/src/Relational/src/Eventuous.Sql.Base/SqlEventStoreBase.cs @@ -144,7 +144,7 @@ StreamEvent ToStreamEvent(PersistedEvent evt) { _ => throw new("Unknown deserialization result") }; - StreamEvent AsStreamEvent(object payload) => new(evt.MessageId, payload, meta ?? new Metadata(), ContentType, evt.StreamPosition); + StreamEvent AsStreamEvent(object payload) => new(evt.MessageId, payload, meta ?? new Metadata(), ContentType, evt.StreamPosition, evt.Created); } /// diff --git a/src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs b/src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs index b7ece66fc..fc08e5469 100644 --- a/src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs +++ b/src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs @@ -22,7 +22,8 @@ CancellationToken cancellationToken ) { var existing = _storage.GetOrAdd(stream, s => new(s)); existing.AppendEvents(expectedVersion, events); - _global.AddRange(events.Select((x, i) => new StreamEvent(x.Id, x.Payload, x.Metadata, "application/json", _global.Count + i))); + var now = DateTime.UtcNow; + _global.AddRange(events.Select((x, i) => new StreamEvent(x.Id, x.Payload, x.Metadata, "application/json", _global.Count + i, now))); return Task.FromResult(new AppendEventsResult((ulong)(_global.Count - 1), existing.Version)); } @@ -33,9 +34,10 @@ public Task AppendEvents(IReadOnlyCollection new(s)); existing.AppendEvents(append.ExpectedVersion, append.Events); - _global.AddRange(append.Events.Select((x, j) => new StreamEvent(x.Id, x.Payload, x.Metadata, "application/json", _global.Count + j))); + _global.AddRange(append.Events.Select((x, j) => new StreamEvent(x.Id, x.Payload, x.Metadata, "application/json", _global.Count + j, now))); results[i++] = new AppendEventsResult((ulong)(_global.Count - 1), existing.Version); } @@ -97,7 +99,7 @@ public void AppendEvents(ExpectedStreamVersion expectedVersion, IReadOnlyCollect foreach (var newEvent in events) { var version = ++Version; - var streamEvent = new StreamEvent(newEvent.Id, newEvent.Payload, newEvent.Metadata, "application/json", version); + var streamEvent = new StreamEvent(newEvent.Id, newEvent.Payload, newEvent.Metadata, "application/json", version, DateTime.UtcNow); _events.Add(new(streamEvent, version)); } }