Skip to content
Open
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
1 change: 1 addition & 0 deletions Eventuous.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<Project Path="src/Core/src/Eventuous.Persistence/Eventuous.Persistence.csproj" />
<Project Path="src/Core/src/Eventuous.Producers/Eventuous.Producers.csproj" />
<Project Path="src/Core/src/Eventuous.Serialization/Eventuous.Serialization.csproj" />
<Project Path="src/Core/src/Eventuous.Serialization.Json.Dynamic/Eventuous.Serialization.Json.Dynamic.csproj" />
<Project Path="src/Core/src/Eventuous.Shared/Eventuous.Shared.csproj" />
<Project Path="src/Core/src/Eventuous.Subscriptions/Eventuous.Subscriptions.csproj" />
<Project Path="src/Core/src/Eventuous/Eventuous.csproj" />
Expand Down
45 changes: 37 additions & 8 deletions docs/src/content/docs/next/persistence/serialisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,29 +92,58 @@ With the source generator in place, calling `RegisterKnownEventTypes()` is typic

### Default serializer

Eventuous provides a default serializer implementation, which uses `System.Text.Json`. You just need to register it in the `Startup` to make it available for the infrastructure components, like [aggregate store](../aggregate-store) and [subscriptions](../../subscriptions/subs-concept).
Eventuous provides two built-in serializer implementations, both using `System.Text.Json`:

Normally, you don't need to register or provide the serializer instance to any of the Eventuous classes that perform serialization and deserialization work. It's because they will use the default serializer instance instead.
- **`DefaultStaticEventSerializer`** — uses a `JsonSerializerContext` for source-generated, AOT-compatible serialization. This is the recommended choice for new applications and is required for Native AOT.
- **`DefaultEventSerializer`** — uses reflection-based serialization. Available in the `Eventuous.Serialization.Json.Dynamic` package.

However, you can register the default serializer with different options, or a custom serializer instead:
The `IEventSerializer` interface itself has no AOT restrictions, so custom serializer implementations work cleanly in AOT scenarios.

#### AOT-compatible serializer (recommended)

For AOT applications, use `DefaultStaticEventSerializer` with a `JsonSerializerContext`:

```csharp title="Program.cs"
builder.Services.AddSingleton<IEventSerializer>(
// Define a JsonSerializerContext with your event types
[JsonSerializable(typeof(RoomBooked))]
[JsonSerializable(typeof(BookingPaid))]
[JsonSerializable(typeof(BookingCancelled))]
public partial class BookingSerializerContext : JsonSerializerContext;

// Register at startup
EventSerializer.SetDefault(
new DefaultStaticEventSerializer(BookingSerializerContext.Default)
);
```

#### Reflection-based serializer

For non-AOT applications, the `DefaultEventSerializer` from the `Eventuous.Serialization.Json.Dynamic` package provides a simpler setup that doesn't require a `JsonSerializerContext`:

```csharp title="Program.cs"
EventSerializer.SetDefault(
new DefaultEventSerializer(
new JsonSerializerOptions(JsonSerializerDefaults.Default)
)
);
```

You might want to avoid registering the serializer and override the one that Eventuous uses as the default instance:
:::caution
`DefaultEventSerializer` uses reflection-based JSON serialization and is not compatible with Native AOT. If you plan to publish your application as AOT, use `DefaultStaticEventSerializer` instead.
:::

#### Configuring via DI

You can also register the serializer in the DI container:

```csharp title="Program.cs"
var defaultSerializer = new DefaultEventSerializer(
new JsonSerializerOptions(JsonSerializerDefaults.Default)
builder.Services.AddSingleton<IEventSerializer>(
new DefaultStaticEventSerializer(BookingSerializerContext.Default)
);
DefaultEventSerializer.SetDefaultSerializer(serializer);
```

Infrastructure components like event stores, subscriptions, and producers will resolve `IEventSerializer` from DI when available. If no serializer is registered in DI, they fall back to `EventSerializer.Default`, which must be configured explicitly at startup.

### Metadata serializer

In many cases you might want to store event metadata in addition to the event payload. Normally, you'd use the same way to serialize both the event payload and its metadata, but it's not always the case. For example, you might store your events in Protobuf, but keep metadata as JSON.
Expand Down
1 change: 1 addition & 0 deletions samples/kurrentdb/Bookings/Bookings.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<ProjectReference Include="$(SrcRoot)\Core\gen\Eventuous.Shared.Generators\Eventuous.Shared.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="$(SrcRoot)\Experimental\gen\Eventuous.Spyglass.Generators\Eventuous.Spyglass.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Bookings.Domain\Bookings.Domain.csproj"/>
<ProjectReference Include="..\..\..\src\Core\src\Eventuous.Serialization.Json.Dynamic\Eventuous.Serialization.Json.Dynamic.csproj"/>
</ItemGroup>
<ItemGroup>
<Content Update="Properties\launchSettings.json">
Expand Down
2 changes: 1 addition & 1 deletion samples/kurrentdb/Bookings/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// .WriteTo.Seq("http://localhost:5341")
.CreateLogger();

DefaultEventSerializer.SetDefaultSerializer(new DefaultStaticEventSerializer(new SourceGenerationContext()));
EventSerializer.SetDefault(new DefaultStaticEventSerializer(new SourceGenerationContext()));
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();

Expand Down
2 changes: 1 addition & 1 deletion samples/kurrentdb/Bookings/Registrations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ namespace Bookings;
public static class Registrations {
extension(IServiceCollection services) {
public void AddEventuous(IConfiguration configuration) {
DefaultEventSerializer.SetDefaultSerializer(
EventSerializer.SetDefault(
new DefaultEventSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web).ConfigureForNodaTime(DateTimeZoneProviders.Tzdb))
);

Expand Down
1 change: 1 addition & 0 deletions samples/postgres/Bookings/Bookings.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
<ProjectReference Include="$(SrcRoot)\RabbitMq\src\Eventuous.RabbitMq\Eventuous.RabbitMq.csproj"/>
<ProjectReference Include="$(SrcRoot)\Core\gen\Eventuous.Subscriptions.Generators\Eventuous.Subscriptions.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Bookings.Domain\Bookings.Domain.csproj" />
<ProjectReference Include="..\..\..\src\Core\src\Eventuous.Serialization.Json.Dynamic\Eventuous.Serialization.Json.Dynamic.csproj"/>
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion samples/postgres/Bookings/Registrations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace Bookings;

public static class Registrations {
public static void AddEventuous(this IServiceCollection services, IConfiguration configuration) {
DefaultEventSerializer.SetDefaultSerializer(
EventSerializer.SetDefault(
new DefaultEventSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web).ConfigureForNodaTime(DateTimeZoneProviders.Tzdb))
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public ServiceBusProducer(
_options = options;
_log = log;
_sender = client.CreateSender(options.QueueOrTopicName, options.SenderOptions);
_serializer = serializer ?? DefaultEventSerializer.Instance;
_serializer = serializer ?? EventSerializer.Default;
_messageBatchBuilder = new(_sender, this._serializer, options.AttributeNames, SetActivityMessageType);
log?.LogInformation("ServiceBusProducer created for {QueueOrTopicName}", options.QueueOrTopicName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class ConvertEventToMessage {

public ConvertEventToMessage() {
var builder = new ServiceBusMessageBuilder(
DefaultEventSerializer.Instance,
EventSerializer.Default,
"test-stream",
new(),
new() {
Expand Down Expand Up @@ -92,7 +92,7 @@ public class WithMessagePropertiesInMetaData {

public WithMessagePropertiesInMetaData() {
var attributeNames = new ServiceBusMessageAttributeNames();
var builder = new ServiceBusMessageBuilder(DefaultEventSerializer.Instance, "test-stream", attributeNames, new());
var builder = new ServiceBusMessageBuilder(EventSerializer.Default, "test-stream", attributeNames, new());

_message = builder.CreateServiceBusMessage(
new(
Expand Down
12 changes: 12 additions & 0 deletions src/Azure/test/Eventuous.Tests.Azure.ServiceBus/TestSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;

namespace Eventuous.Tests.Azure.ServiceBus;

static class TestSetup {
[ModuleInitializer]
[SuppressMessage("Usage", "CA2255")]
internal static void Initialize()
=> EventSerializer.SetDefault(new DefaultEventSerializer(new(JsonSerializerDefaults.Web)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ protected IDefineExpectedState<TCommand, TAggregate, TState, TId> On<TCommand>()
/// <param name="cancellationToken">Cancellation token</param>
/// <returns><see cref="Result{TState}"/> of the execution</returns>
/// <exception cref="Exceptions.CommandHandlerNotFound{TCommand}"></exception>
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<Result<TState>> Handle<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : class {
if (!_handlers.TryGet<TCommand>(out var registeredHandler)) {
Log.CommandHandlerNotFound<TCommand>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ namespace Eventuous.Diagnostics;
InnerService = appService;
}

[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public Task<Result<TState>> Handle<TCommand>(TCommand command, CancellationToken cancellationToken)
where TCommand : class
=> CommandServiceActivity.TryExecute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ protected CommandService(IEventStore store, ITypeMapper? typeMap = null, AmendEv
/// <typeparam name="TCommand">Command type</typeparam>
/// <returns><seealso cref="Result{TState}"/> instance</returns>
/// <exception cref="ArgumentOutOfRangeException">Throws when there's no command handler was registered for the command type</exception>
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<Result<TState>> Handle<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : class {
if (!_handlers.TryGet<TCommand>(out var registeredHandler)) {
Log.CommandHandlerNotFound<TCommand>();
Expand Down
2 changes: 0 additions & 2 deletions src/Core/src/Eventuous.Application/ICommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
namespace Eventuous;

public interface ICommandService<TState> where TState : State<TState>, new() {
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
Task<Result<TState>> Handle<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : class;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ namespace Eventuous.Persistence;

static class WriterExtensions {
extension(IEventWriter writer) {
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<AppendEventsResult> Store(ProposedAppend append, AmendEvent? amendEvent, CancellationToken cancellationToken) {
Ensure.NotNull(append.Events);

Expand All @@ -33,8 +31,6 @@ NewStreamEvent ToStreamEvent(ProposedEvent evt) {
}
}

[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<AppendEventsResult[]> Store(
IReadOnlyCollection<ProposedAppend> appends,
AmendEvent? amendEvent,
Expand Down
2 changes: 0 additions & 2 deletions src/Core/src/Eventuous.Application/ThrowingCommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ namespace Eventuous;
/// <typeparam name="TState"></typeparam>
public class ThrowingCommandService<TState>(ICommandService<TState> inner) : ICommandService<TState>
where TState : State<TState>, new() {
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<Result<TState>> Handle<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : class {
var result = await inner.Handle(command, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ public static class AggregatePersistenceExtensions {
/// <typeparam name="TState">Aggregate state type</typeparam>
/// <returns>Append event result</returns>
/// <exception cref="OptimisticConcurrencyException{T, TState}">Gets thrown if the expected stream version mismatches with the given original stream version</exception>
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<AppendEventsResult> StoreAggregate<TAggregate, TState>(
StreamName streamName,
TAggregate aggregate,
Expand Down Expand Up @@ -52,8 +50,6 @@ public async Task<AppendEventsResult> StoreAggregate<TAggregate, TState>(
/// <typeparam name="TId">Aggregate identity type</typeparam>
/// <returns>Append event result</returns>
/// <exception cref="OptimisticConcurrencyException{T, TState}">Gets thrown if the expected stream version mismatches with the given original stream version</exception>
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public Task<AppendEventsResult> StoreAggregate<TAggregate, TState, TId>(
TAggregate aggregate,
TId id,
Expand Down Expand Up @@ -84,8 +80,6 @@ public Task<AppendEventsResult> StoreAggregate<TAggregate, TState, TId>(
/// <typeparam name="TId">Aggregate identity type</typeparam>
/// <returns>Append event result</returns>
/// <exception cref="OptimisticConcurrencyException{T, TState}">Gets thrown if the expected stream version mismatches with the given original stream version</exception>
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public Task<AppendEventsResult> StoreAggregate<TAggregate, TState, TId>(
TAggregate aggregate,
StreamNameMap? streamNameMap = null,
Expand Down Expand Up @@ -115,8 +109,6 @@ public Task<AppendEventsResult> StoreAggregate<TAggregate, TState, TId>(
/// <returns>Aggregate instance</returns>
/// <exception cref="AggregateNotFoundException{T,TState}">If failIfNotFound set to true, this exception is thrown if there's no stream</exception>
/// <exception cref="Exception"></exception>
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<TAggregate> LoadAggregate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TAggregate, TState>(
StreamName streamName,
bool failIfNotFound = true,
Expand Down Expand Up @@ -155,8 +147,6 @@ public Task<AppendEventsResult> StoreAggregate<TAggregate, TState, TId>(
/// <returns>Aggregate instance</returns>
/// <exception cref="AggregateNotFoundException{T,TState}">If failIfNotFound set to true, this exception is thrown if there's no stream</exception>
/// <exception cref="Exception"></exception>
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<TAggregate> LoadAggregate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TAggregate, TState, TId>(
TId aggregateId,
StreamNameMap? streamNameMap = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,17 @@ public AggregateStore(

/// <inheritdoc/>
[Obsolete("Use IEventWriter.StoreAggregate<TAggregate, TState> instead.")]
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public Task<AppendEventsResult> Store<TAggregate, TState>(StreamName streamName, TAggregate aggregate, CancellationToken cancellationToken)
where TAggregate : Aggregate<TState> where TState : State<TState>, new()
=> _eventWriter.StoreAggregate<TAggregate, TState>(streamName, aggregate, _amendEvent, cancellationToken);

/// <inheritdoc/>
[Obsolete("Use IEventReader.LoadAggregate<TAggregate, TState> instead.")]
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public Task<T> Load<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T, TState>(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate<TState> where TState : State<TState>, new()
=> _eventReader.LoadAggregate<T, TState>(streamName, true, _factoryRegistry, cancellationToken);

/// <inheritdoc/>
[Obsolete("Use IEventReader.LoadAggregate<TAggregate, TState> instead.")]
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public Task<T> LoadOrNew<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T, TState>(StreamName streamName, CancellationToken cancellationToken)
where T : Aggregate<TState> where TState : State<TState>, new()
=> _eventReader.LoadAggregate<T, TState>(streamName, false, _factoryRegistry, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public static class AggregateStoreExtensions {
/// <typeparam name="TId">Aggregate id type</typeparam>
/// <returns></returns>
[Obsolete("Use IEventReader.LoadAggregates instead.")]
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<T> Load
<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T, TState, TId>(StreamNameMap streamNameMap, TId id, CancellationToken cancellationToken)
where T : Aggregate<TState> where TId : Id where TState : State<TState>, new() {
Expand All @@ -39,8 +37,6 @@ public async Task<T> Load
/// <typeparam name="TId">Aggregate id type</typeparam>
/// <returns></returns>
[Obsolete("Use IEventReader.LoadAggregates instead.")]
[RequiresDynamicCode(AttrConstants.DynamicSerializationMessage)]
[RequiresUnreferencedCode(AttrConstants.DynamicSerializationMessage)]
public async Task<TAggregate> LoadOrNew<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TAggregate, TState, TId>(
StreamNameMap streamNameMap,
TId id,
Expand Down
Loading
Loading