diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml
index b47b2e29..98e1f2ca 100644
--- a/.github/workflows/code_quality.yml
+++ b/.github/workflows/code_quality.yml
@@ -14,6 +14,8 @@ jobs:
with:
fetch-depth: 0
- name: 'Qodana Scan'
- uses: JetBrains/qodana-action@v2022.3.3
+ uses: JetBrains/qodana-action@v2023.2.1
+ with:
+ pr-mode: false
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 81fd823c..3de0cac5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -42,8 +42,8 @@
-
-
+
+
@@ -55,7 +55,7 @@
-
+
@@ -73,7 +73,7 @@
-
+
diff --git a/src/Benchmarks/Benchmarks/GapDetectionBenchmarks.cs b/src/Benchmarks/Benchmarks/GapDetectionBenchmarks.cs
index ca01cb5b..0e931a98 100644
--- a/src/Benchmarks/Benchmarks/GapDetectionBenchmarks.cs
+++ b/src/Benchmarks/Benchmarks/GapDetectionBenchmarks.cs
@@ -19,7 +19,7 @@ public class GapDetectionBenchmarks {
public void Setup() {
_store = new NoOpCheckpointStore();
- _store.CheckpointStored += (sender, checkpoint) => Console.WriteLine(checkpoint);
+ _store.CheckpointStored += (_, checkpoint) => Console.WriteLine(checkpoint);
var numbers = Enumerable.Range(1, 1000).ToList();
numbers.RemoveAll(x => x % 10 == 0);
diff --git a/src/Benchmarks/Benchmarks/TypeMapBenchmark.cs b/src/Benchmarks/Benchmarks/TypeMapBenchmark.cs
index 197e4223..23a2866c 100644
--- a/src/Benchmarks/Benchmarks/TypeMapBenchmark.cs
+++ b/src/Benchmarks/Benchmarks/TypeMapBenchmark.cs
@@ -12,6 +12,7 @@ public class TypeMapBenchmark {
KeyValuePair[] _types = null!;
[Params(5, 20, 100)]
+ // ReSharper disable once UnusedAutoPropertyAccessor.Global
public int TypesCount { get; set; }
[GlobalSetup]
diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlerBuilder.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlerBuilder.cs
new file mode 100644
index 00000000..eeaee1d5
--- /dev/null
+++ b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlerBuilder.cs
@@ -0,0 +1,113 @@
+// Copyright (C) Ubiquitous AS.All rights reserved
+// Licensed under the Apache License, Version 2.0.
+
+namespace Eventuous;
+
+public abstract class CommandHandlerBuilder
+ where TAggregate : Aggregate where TId : Id where TState : State, new() {
+ internal abstract RegisteredHandler Build();
+}
+
+///
+/// Builds a command handler for a specific command type. You would not need to instantiate this class directly,
+/// use function.
+///
+/// Default aggregate store instance for the command service
+/// Command type
+/// Aggregate type
+/// State of the aggregate type
+/// Identity of the aggregate type
+public class CommandHandlerBuilder(IAggregateStore? store) : CommandHandlerBuilder
+ where TCommand : class
+ where TAggregate : Aggregate, new()
+ where TState : State, new()
+ where TId : Id {
+ GetIdFromUntypedCommand? _getId;
+ HandleUntypedCommand? _action;
+ ResolveStore? _resolveStore;
+ ExpectedState _expectedState = ExpectedState.Any;
+
+ ///
+ /// Set the expected aggregate state for the command handler.
+ /// If the aggregate won't be in the expected state, the command handler will return an error.
+ /// The default is .
+ ///
+ /// Expected aggregate state
+ ///
+ public CommandHandlerBuilder InState(ExpectedState expectedState) {
+ _expectedState = expectedState;
+
+ return this;
+ }
+
+ ///
+ /// Defines how the aggregate id is extracted from the command.
+ ///
+ /// A function to get the aggregate id from the command.
+ ///
+ public CommandHandlerBuilder GetId(GetIdFromCommand getId) {
+ _getId = getId.AsGetId();
+
+ return this;
+ }
+
+ ///
+ /// Defines how the aggregate id is extracted from the command, asynchronously.
+ ///
+ /// A function to get the aggregate id from the command.
+ ///
+ public CommandHandlerBuilder GetIdAsync(GetIdFromCommandAsync getId) {
+ _getId = getId.AsGetId();
+
+ return this;
+ }
+
+ ///
+ /// Defines how the aggregate is acted upon by the command.
+ ///
+ /// A function that executes an operation on an aggregate
+ ///
+ public CommandHandlerBuilder Act(ActOnAggregate action) {
+ _action = action.AsAct();
+
+ return this;
+ }
+
+ ///
+ /// Defines how the aggregate is acted upon by the command, asynchronously.
+ ///
+ /// A function that executes an asynchronous operation on an aggregate
+ ///
+ public CommandHandlerBuilder ActAsync(ActOnAggregateAsync action) {
+ _action = action.AsAct();
+
+ return this;
+ }
+
+ ///
+ /// Defines how the aggregate store is resolved from the command. It is optional. If not defined, the default
+ /// aggregate store of the command service will be used.
+ ///
+ ///
+ ///
+ public CommandHandlerBuilder ResolveStore(ResolveStore? resolveStore) {
+ _resolveStore = resolveStore;
+
+ return this;
+ }
+
+ internal override RegisteredHandler Build() {
+ return new RegisteredHandler(
+ _expectedState,
+ Ensure.NotNull(_getId, $"Function to get the aggregate id from {typeof(TCommand).Name} is not defined"),
+ Ensure.NotNull(_action, $"Function to act on the aggregate for command {typeof(TCommand).Name} is not defined"),
+ (_resolveStore ?? DefaultResolve()).AsResolveStore()
+ );
+ }
+
+ ResolveStore DefaultResolve() {
+ ArgumentNullException.ThrowIfNull(store, nameof(store));
+
+ return _ => store;
+ }
+}
diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlersMap.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlersMap.cs
new file mode 100644
index 00000000..ff34c2e1
--- /dev/null
+++ b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlersMap.cs
@@ -0,0 +1,55 @@
+// Copyright (C) Ubiquitous AS. All rights reserved
+// Licensed under the Apache License, Version 2.0.
+
+using System.Reflection;
+
+namespace Eventuous;
+
+using static Diagnostics.ApplicationEventSource;
+
+public delegate Task ActOnAggregateAsync(TAggregate aggregate, TCommand command, CancellationToken cancellationToken)
+ where TAggregate : Aggregate;
+
+public delegate void ActOnAggregate(TAggregate aggregate, TCommand command) where TAggregate : Aggregate;
+
+delegate ValueTask HandleUntypedCommand(T aggregate, object command, CancellationToken cancellationToken) where T : Aggregate;
+
+public delegate Task GetIdFromCommandAsync(TCommand command, CancellationToken cancellationToken) where TId : Id where TCommand : class;
+
+public delegate TId GetIdFromCommand(TCommand command) where TId : Id where TCommand : class;
+
+delegate ValueTask GetIdFromUntypedCommand(object command, CancellationToken cancellationToken) where TId : Id;
+
+public delegate IAggregateStore ResolveStore(TCommand command) where TCommand : class;
+
+delegate IAggregateStore ResolveStoreFromCommand(object command);
+
+record RegisteredHandler(
+ ExpectedState ExpectedState,
+ GetIdFromUntypedCommand GetId,
+ HandleUntypedCommand Handler,
+ ResolveStoreFromCommand ResolveStore
+ ) where T : Aggregate where TId : Id;
+
+class HandlersMap where TAggregate : Aggregate where TId : Id {
+ readonly TypeMap> _typeMap = new();
+
+ static readonly MethodInfo AddHandlerInternalMethod =
+ typeof(HandlersMap).GetMethod(nameof(AddHandlerInternal), BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ internal void AddHandlerInternal(RegisteredHandler handler) {
+ try {
+ _typeMap.Add(handler);
+ Log.CommandHandlerRegistered();
+ } catch (Exceptions.DuplicateTypeException) {
+ Log.CommandHandlerAlreadyRegistered();
+
+ throw new Exceptions.CommandHandlerAlreadyRegistered();
+ }
+ }
+
+ internal void AddHandlerUntyped(Type command, RegisteredHandler handler)
+ => AddHandlerInternalMethod.MakeGenericMethod(command).Invoke(this, new object?[] { handler });
+
+ public bool TryGet([NotNullWhen(true)] out RegisteredHandler? handler) => _typeMap.TryGetValue(out handler);
+}
diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlingDelegateExtensions.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlingDelegateExtensions.cs
new file mode 100644
index 00000000..26a5830b
--- /dev/null
+++ b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlingDelegateExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (C) Ubiquitous AS.All rights reserved
+// Licensed under the Apache License, Version 2.0.
+
+namespace Eventuous;
+
+static class CommandHandlingDelegateExtensions {
+ public static GetIdFromUntypedCommand AsGetId(this GetIdFromCommandAsync getId) where TId : Id where TCommand : class
+ => async (cmd, ct) => await getId((TCommand)cmd, ct);
+
+ public static GetIdFromUntypedCommand AsGetId(this GetIdFromCommand getId) where TId : Id where TCommand : class
+ => (cmd, _) => ValueTask.FromResult(getId((TCommand)cmd));
+
+ public static HandleUntypedCommand AsAct(this ActOnAggregateAsync act) where TAggregate : Aggregate
+ => async (aggregate, cmd, ct) => {
+ await act(aggregate, (TCommand)cmd, ct).NoContext();
+
+ return aggregate;
+ };
+
+ public static HandleUntypedCommand AsAct(this ActOnAggregate act) where TAggregate : Aggregate
+ => (aggregate, cmd, _) => {
+ act(aggregate, (TCommand)cmd);
+
+ return ValueTask.FromResult(aggregate);
+ };
+
+ public static ResolveStoreFromCommand AsResolveStore(this ResolveStore resolveStore) where TCommand : class
+ => cmd => resolveStore((TCommand)cmd);
+}
diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandService.Async.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandService.Async.cs
new file mode 100644
index 00000000..c089869d
--- /dev/null
+++ b/src/Core/src/Eventuous.Application/AggregateService/CommandService.Async.cs
@@ -0,0 +1,88 @@
+// Copyright (C) Ubiquitous AS. All rights reserved
+// Licensed under the Apache License, Version 2.0.
+
+namespace Eventuous;
+
+public abstract partial class CommandService {
+ ///
+ /// Register an asynchronous handler for a command, which is expected to create a new aggregate instance.
+ ///
+ /// A function to get the aggregate id from the command
+ /// Asynchronous action to be performed on the aggregate, given the aggregate instance and the command
+ /// Resolve aggregate store from the command
+ /// Command type
+ [Obsolete("Use On().InState(ExpectedState.New).GetId(...).ActAsync(...).ResolveStore(...) instead")]
+ protected void OnNewAsync(
+ GetIdFromCommand getId,
+ ActOnAggregateAsync action,
+ ResolveStore? resolveStore = null
+ ) where TCommand : class
+ => On().InState(ExpectedState.New).GetId(getId).ActAsync(action).ResolveStore(resolveStore);
+
+ ///
+ /// Register an asynchronous handler for a command, which is expected to use an existing aggregate instance.
+ ///
+ /// A function to get the aggregate id from the command
+ /// Asynchronous action to be performed on the aggregate, given the aggregate instance and the command
+ /// Resolve aggregate store from the command
+ /// Command type
+ [Obsolete("Use On().InState(ExpectedState.Existing).GetId(...).ActAsync(...).ResolveStore(...) instead")]
+ [PublicAPI]
+ protected void OnExistingAsync(
+ GetIdFromCommand getId,
+ ActOnAggregateAsync action,
+ ResolveStore? resolveStore = null
+ ) where TCommand : class
+ => On().InState(ExpectedState.Existing).GetId(getId).ActAsync(action).ResolveStore(resolveStore);
+
+ ///
+ /// Register an asynchronous handler for a command, which is expected to use an existing aggregate instance.
+ ///
+ /// Asynchronous function to get the aggregate id from the command
+ /// Asynchronous action to be performed on the aggregate, given the aggregate instance and the command
+ /// Resolve aggregate store from the command
+ /// Command type
+ [Obsolete("Use On().InState(ExpectedState.Existing).GetIdAsync(...).ActAsync(...).ResolveStore(...) instead")]
+ [PublicAPI]
+ protected void OnExistingAsync(
+ GetIdFromCommandAsync getId,
+ ActOnAggregateAsync action,
+ ResolveStore? resolveStore = null
+ ) where TCommand : class
+ // => _handlers.AddHandler(ExpectedState.Existing, getId, action, resolveStore ?? DefaultResolve());
+ => On().InState(ExpectedState.Existing).GetIdAsync(getId).ActAsync(action).ResolveStore(resolveStore);
+
+ ///
+ /// Register an asynchronous handler for a command, which is expected to use an a new or an existing aggregate instance.
+ ///
+ /// A function to get the aggregate id from the command
+ /// Asynchronous action to be performed on the aggregate, given the aggregate instance and the command
+ /// Resolve aggregate store from the command
+ /// Command type
+ [Obsolete("Use On().InState(ExpectedState.Any).GetId(...).ActAsync(...).ResolveStore(...) instead")]
+ [PublicAPI]
+ protected void OnAnyAsync(
+ GetIdFromCommand getId,
+ ActOnAggregateAsync action,
+ ResolveStore? resolveStore = null
+ ) where TCommand : class
+ // => _handlers.AddHandler(ExpectedState.Any, getId, action, resolveStore ?? DefaultResolve());
+ => On().InState(ExpectedState.Any).GetId(getId).ActAsync(action).ResolveStore(resolveStore);
+
+ ///
+ /// Register an asynchronous handler for a command, which is expected to use an a new or an existing aggregate instance.
+ ///
+ /// Asynchronous function to get the aggregate id from the command
+ /// Asynchronous action to be performed on the aggregate, given the aggregate instance and the command
+ /// Resolve aggregate store from the command
+ /// Command type
+ [Obsolete("Use On().InState(ExpectedState.Any).GetIdAsync(...).ActAsync(...).ResolveStore(...) instead")]
+ [PublicAPI]
+ protected void OnAnyAsync(
+ GetIdFromCommandAsync getId,
+ ActOnAggregateAsync action,
+ ResolveStore? resolveStore = null
+ ) where TCommand : class
+ // => _handlers.AddHandler(ExpectedState.Any, getId, action, resolveStore ?? DefaultResolve());
+ => On().InState(ExpectedState.Any).GetIdAsync(getId).ActAsync(action).ResolveStore(resolveStore);
+}
diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandService.Sync.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandService.Sync.cs
new file mode 100644
index 00000000..f24404e6
--- /dev/null
+++ b/src/Core/src/Eventuous.Application/AggregateService/CommandService.Sync.cs
@@ -0,0 +1,51 @@
+// Copyright (C) Ubiquitous AS. All rights reserved
+// Licensed under the Apache License, Version 2.0.
+
+namespace Eventuous;
+
+public abstract partial class CommandService {
+ ///
+ /// Register a handler for a command, which is expected to create a new aggregate instance.
+ ///
+ /// A function to get the aggregate id from the command
+ /// Action to be performed on the aggregate, given the aggregate instance and the command
+ /// Resolve aggregate store from the command
+ /// Command type
+ [Obsolete("Use On().InState(ExpectedState.New).GetId(...).Act(...).ResolveStore(...) instead")]
+ protected void OnNew(
+ GetIdFromCommand getId,
+ ActOnAggregate action,
+ ResolveStore? resolveStore = null
+ ) where TCommand : class
+ => On().InState(ExpectedState.New).GetId(getId).Act(action).ResolveStore(resolveStore);
+
+ ///
+ /// Register a handler for a command, which is expected to use an existing aggregate instance.
+ ///
+ /// A function to get the aggregate id from the command
+ /// Action to be performed on the aggregate, given the aggregate instance and the command
+ /// Resolve aggregate store from the command
+ /// Command type
+ [Obsolete("Use On().InState(ExpectedState.Existing).GetId(...).Act(...).ResolveStore(...) instead")]
+ protected void OnExisting(
+ GetIdFromCommand getId,
+ ActOnAggregate action,
+ ResolveStore? resolveStore = null
+ ) where TCommand : class
+ => On().InState(ExpectedState.Existing).GetId(getId).Act(action).ResolveStore(resolveStore);
+
+ ///
+ /// Register a handler for a command, which is expected to use an a new or an existing aggregate instance.
+ ///
+ /// A function to get the aggregate id from the command
+ /// Action to be performed on the aggregate, given the aggregate instance and the command
+ /// Resolve aggregate store from the command
+ /// Command type
+ [Obsolete("Use On().InState(ExpectedState.Any).GetId(...).Act(...).ResolveStore(...) instead")]
+ protected void OnAny(
+ GetIdFromCommand getId,
+ ActOnAggregate action,
+ ResolveStore? resolveStore = null
+ ) where TCommand : class
+ => On().InState(ExpectedState.Any).GetId(getId).Act(action).ResolveStore(resolveStore);
+}
diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandService.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandService.cs
new file mode 100644
index 00000000..6aaaa368
--- /dev/null
+++ b/src/Core/src/Eventuous.Application/AggregateService/CommandService.cs
@@ -0,0 +1,123 @@
+// Copyright (C) Ubiquitous AS. All rights reserved
+// Licensed under the Apache License, Version 2.0.
+
+namespace Eventuous;
+
+using static Diagnostics.ApplicationEventSource;
+
+///
+/// Command service base class. A derived class should be scoped to handle commands for one aggregate type only.
+///
+/// The aggregate type
+/// The aggregate state type
+/// The aggregate identity type
+// [PublicAPI]
+public abstract partial class CommandService(
+ IAggregateStore? store,
+ AggregateFactoryRegistry? factoryRegistry = null,
+ StreamNameMap? streamNameMap = null,
+ TypeMapper? typeMap = null
+ )
+ : ICommandService, ICommandService
+ where TAggregate : Aggregate, new()
+ where TState : State, new()
+ where TId : Id {
+ [PublicAPI]
+ protected IAggregateStore? Store { get; } = store;
+
+ readonly HandlersMap _handlers = new();
+ readonly AggregateFactoryRegistry _factoryRegistry = factoryRegistry ?? AggregateFactoryRegistry.Instance;
+ readonly StreamNameMap _streamNameMap = streamNameMap ?? new StreamNameMap();
+ readonly TypeMapper _typeMap = typeMap ?? TypeMap.Instance;
+
+ bool _initialized;
+
+ ///
+ /// Returns the command handler builder for the specified command type.
+ ///
+ /// Command type
+ ///
+ protected CommandHandlerBuilder On() where TCommand : class {
+ var builder = new CommandHandlerBuilder(Store);
+ _builders.Add(typeof(TCommand), builder);
+
+ return builder;
+ }
+
+ ///
+ /// The command handler. Call this function from your edge (API).
+ ///
+ /// Command to execute
+ /// Cancellation token
+ /// of the execution
+ ///
+ public async Task> Handle(TCommand command, CancellationToken cancellationToken) where TCommand : class {
+ if (!_initialized) BuildHandlers();
+
+ if (!_handlers.TryGet(out var registeredHandler)) {
+ Log.CommandHandlerNotFound();
+ var exception = new Exceptions.CommandHandlerNotFound();
+
+ return new ErrorResult(exception);
+ }
+
+ var aggregateId = await registeredHandler.GetId(command, cancellationToken).NoContext();
+ var store = registeredHandler.ResolveStore(command);
+
+ try {
+ var aggregate = registeredHandler.ExpectedState switch {
+ ExpectedState.Any => await store.LoadOrNew(_streamNameMap, aggregateId, cancellationToken).NoContext(),
+ ExpectedState.Existing => await store.Load(_streamNameMap, aggregateId, cancellationToken).NoContext(),
+ ExpectedState.New => Create(aggregateId),
+ ExpectedState.Unknown => default,
+ _ => throw new ArgumentOutOfRangeException(nameof(registeredHandler.ExpectedState), "Unknown expected state")
+ };
+
+ var result = await registeredHandler
+ .Handler(aggregate!, command, cancellationToken)
+ .NoContext();
+
+ // Zero in the global position would mean nothing, so the receiver need to check the Changes.Length
+ if (result.Changes.Count == 0) return new OkResult(result.State, Array.Empty(), 0);
+
+ var storeResult = await store.Store(GetAggregateStreamName(), result, cancellationToken).NoContext();
+ var changes = result.Changes.Select(x => new Change(x, _typeMap.GetTypeName(x)));
+ Log.CommandHandled();
+
+ return new OkResult(result.State, changes, storeResult.GlobalPosition);
+ } catch (Exception e) {
+ Log.ErrorHandlingCommand(e);
+
+ return new ErrorResult($"Error handling command {typeof(TCommand).Name}", e);
+ }
+
+ TAggregate Create(TId id) => _factoryRegistry.CreateInstance().WithId(id);
+
+ StreamName GetAggregateStreamName() => _streamNameMap.GetStreamName(aggregateId);
+ }
+
+ async Task ICommandService.Handle(TCommand command, CancellationToken cancellationToken) where TCommand : class {
+ var result = await Handle(command, cancellationToken).NoContext();
+
+ return result switch {
+ OkResult(var state, var enumerable, _) => new OkResult(state, enumerable),
+ ErrorResult error => new ErrorResult(error.Message, error.Exception),
+ _ => throw new ApplicationException("Unknown result type")
+ };
+ }
+
+ readonly Dictionary> _builders = new();
+ readonly object _handlersLock = new();
+
+ void BuildHandlers() {
+ lock (_handlersLock) {
+ foreach (var commandType in _builders.Keys) {
+ var builder = _builders[commandType];
+ var handler = builder.Build();
+ _handlers.AddHandlerUntyped(commandType, handler);
+ }
+
+ _initialized = true;
+ }
+ }
+}
diff --git a/src/Core/src/Eventuous.Application/CommandService.cs b/src/Core/src/Eventuous.Application/CommandService.cs
deleted file mode 100644
index ac2801ab..00000000
--- a/src/Core/src/Eventuous.Application/CommandService.cs
+++ /dev/null
@@ -1,238 +0,0 @@
-// Copyright (C) Ubiquitous AS. All rights reserved
-// Licensed under the Apache License, Version 2.0.
-
-namespace Eventuous;
-
-using static Diagnostics.ApplicationEventSource;
-
-///
-/// Command service base class. A derived class should be scoped to handle commands for one aggregate type only.
-///
-/// The aggregate type
-/// The aggregate state type
-/// The aggregate identity type
-// [PublicAPI]
-public abstract class CommandService(
- IAggregateStore store,
- AggregateFactoryRegistry? factoryRegistry = null,
- StreamNameMap? streamNameMap = null,
- TypeMapper? typeMap = null
- )
- : ICommandService, ICommandService
- where TAggregate : Aggregate, new()
- where TState : State, new()
- where TId : Id {
- [PublicAPI]
- protected IAggregateStore Store { get; } = store;
-
- readonly HandlersMap _handlers = new();
- readonly IdMap _idMap = new();
- readonly AggregateFactoryRegistry _factoryRegistry = factoryRegistry ?? AggregateFactoryRegistry.Instance;
- readonly StreamNameMap _streamNameMap = streamNameMap ?? new StreamNameMap();
- readonly TypeMapper _typeMap = typeMap ?? TypeMap.Instance;
-
- ///
- /// Register a handler for a command, which is expected to create a new aggregate instance.
- ///
- /// A function to get the aggregate id from the command
- /// Action to be performed on the aggregate, given the aggregate instance and the command
- /// Command type
- protected void OnNew(GetIdFromCommand getId, ActOnAggregate action) where TCommand : class {
- _handlers.AddHandler(ExpectedState.New, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register an asynchronous handler for a command, which is expected to create a new aggregate instance.
- ///
- /// A function to get the aggregate id from the command
- /// Asynchronous action to be performed on the aggregate,
- /// given the aggregate instance and the command
- /// Command type
- protected void OnNewAsync(GetIdFromCommand getId, ActOnAggregateAsync action) where TCommand : class {
- _handlers.AddHandler(ExpectedState.New, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register a handler for a command, which is expected to use an existing aggregate instance.
- ///
- /// A function to get the aggregate id from the command
- /// Action to be performed on the aggregate, given the aggregate instance and the command
- /// Command type
- protected void OnExisting(GetIdFromCommand getId, ActOnAggregate action) where TCommand : class {
- _handlers.AddHandler(ExpectedState.Existing, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register an asynchronous handler for a command, which is expected to use an existing aggregate instance.
- ///
- /// A function to get the aggregate id from the command
- /// Asynchronous action to be performed on the aggregate,
- /// given the aggregate instance and the command
- /// Command type
- [PublicAPI]
- protected void OnExistingAsync(GetIdFromCommand getId, ActOnAggregateAsync action) where TCommand : class {
- _handlers.AddHandler(ExpectedState.Existing, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register an asynchronous handler for a command, which is expected to use an existing aggregate instance.
- ///
- /// Asynchronous function to get the aggregate id from the command
- /// Asynchronous action to be performed on the aggregate,
- /// given the aggregate instance and the command
- /// Command type
- [PublicAPI]
- protected void OnExistingAsync(GetIdFromCommandAsync getId, ActOnAggregateAsync action)
- where TCommand : class {
- _handlers.AddHandler(ExpectedState.Existing, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register a handler for a command, which is expected to use an a new or an existing aggregate instance.
- ///
- /// A function to get the aggregate id from the command
- /// Action to be performed on the aggregate,
- /// given the aggregate instance and the command
- /// Command type
- protected void OnAny(GetIdFromCommand getId, ActOnAggregate action) where TCommand : class {
- _handlers.AddHandler(ExpectedState.Any, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register a handler for a command, which is expected to use an a new or an existing aggregate instance.
- ///
- /// Asynchronous function to get the aggregate id from the command
- /// Action to be performed on the aggregate,
- /// given the aggregate instance and the command
- /// Command type
- [PublicAPI]
- protected void OnAny(GetIdFromCommandAsync getId, ActOnAggregate action) where TCommand : class {
- _handlers.AddHandler(ExpectedState.Any, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register an asynchronous handler for a command, which is expected to use an a new or an existing aggregate instance.
- ///
- /// A function to get the aggregate id from the command
- /// Asynchronous action to be performed on the aggregate,
- /// given the aggregate instance and the command
- /// Command type
- [PublicAPI]
- protected void OnAnyAsync(GetIdFromCommand getId, ActOnAggregateAsync action) where TCommand : class {
- _handlers.AddHandler(ExpectedState.Any, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register an asynchronous handler for a command, which is expected to use an a new or an existing aggregate instance.
- ///
- /// Asynchronous function to get the aggregate id from the command
- /// Asynchronous action to be performed on the aggregate,
- /// given the aggregate instance and the command
- /// Command type
- [PublicAPI]
- protected void OnAnyAsync(GetIdFromCommandAsync getId, ActOnAggregateAsync action) where TCommand : class {
- _handlers.AddHandler(ExpectedState.Any, action);
- _idMap.AddCommand(getId);
- }
-
- ///
- /// Register an asynchronous handler for a command, which can figure out the aggregate instance by itself, and then return one.
- ///
- /// Function, which returns some aggregate instance to store
- /// Command type
- [PublicAPI]
- protected void OnAsync(ArbitraryActAsync action) where TCommand : class
- => _handlers.AddHandler(
- new RegisteredHandler(
- ExpectedState.Unknown,
- async (_, cmd, ct) => await action((TCommand)cmd, ct).NoContext()
- )
- );
-
- ///
- /// The command handler. Call this function from your edge (API).
- ///
- /// Command to execute
- /// Cancellation token
- /// of the execution
- ///
- public async Task> Handle(TCommand command, CancellationToken cancellationToken)
- where TCommand : class {
- if (!_handlers.TryGet(out var registeredHandler)) {
- Log.CommandHandlerNotFound();
- var exception = new Exceptions.CommandHandlerNotFound();
-
- return new ErrorResult(exception);
- }
-
- var hasGetIdFunction = _idMap.TryGet(out var getId);
-
- if (!hasGetIdFunction || getId == null) {
- Log.CannotCalculateAggregateId();
- var exception = new Exceptions.CommandHandlerNotFound();
-
- return new ErrorResult(exception);
- }
-
- var aggregateId = await getId(command, cancellationToken).NoContext();
-
- try {
- var aggregate = registeredHandler.ExpectedState switch {
- ExpectedState.Any => await Store.LoadOrNew(_streamNameMap, aggregateId, cancellationToken).NoContext(),
- ExpectedState.Existing => await Store.Load(_streamNameMap, aggregateId, cancellationToken).NoContext(),
- ExpectedState.New => Create(aggregateId),
- ExpectedState.Unknown => default,
- _ => throw new ArgumentOutOfRangeException(nameof(registeredHandler.ExpectedState), "Unknown expected state")
- };
-
- var result = await registeredHandler
- .Handler(aggregate!, command, cancellationToken)
- .NoContext();
-
- // Zero in the global position would mean nothing, so the receiver need to check the Changes.Length
- if (result.Changes.Count == 0) return new OkResult(result.State, Array.Empty(), 0);
-
- var storeResult = await Store.Store(GetAggregateStreamName(), result, cancellationToken).NoContext();
-
- var changes = result.Changes.Select(x => new Change(x, _typeMap.GetTypeName(x)));
-
- Log.CommandHandled();
-
- return new OkResult(result.State, changes, storeResult.GlobalPosition);
- } catch (Exception e) {
- Log.ErrorHandlingCommand(e);
-
- return new ErrorResult($"Error handling command {typeof(TCommand).Name}", e);
- }
-
- TAggregate Create(TId id)
- => _factoryRegistry.CreateInstance().WithId(id);
-
- StreamName GetAggregateStreamName()
- => _streamNameMap.GetStreamName(aggregateId);
- }
-
- async Task ICommandService.Handle(TCommand command, CancellationToken cancellationToken)
- where TCommand : class {
- var result = await Handle(command, cancellationToken).NoContext();
-
- return result switch {
- OkResult(var state, var enumerable, _) => new OkResult(state, enumerable),
- ErrorResult error => new ErrorResult(error.Message, error.Exception),
- _ => throw new ApplicationException("Unknown result type")
- };
- }
-
- public delegate Task ArbitraryActAsync(
- TCommand command,
- CancellationToken cancellationToken
- );
-}
diff --git a/src/Core/src/Eventuous.Application/CommandToIdMap.cs b/src/Core/src/Eventuous.Application/CommandToIdMap.cs
deleted file mode 100644
index 74c34f98..00000000
--- a/src/Core/src/Eventuous.Application/CommandToIdMap.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) Ubiquitous AS. All rights reserved
-// Licensed under the Apache License, Version 2.0.
-
-namespace Eventuous;
-
-public delegate Task GetIdFromCommandAsync(TCommand command, CancellationToken cancellationToken)
- where TId : Id where TCommand : class;
-
-public delegate TId GetIdFromCommand(TCommand command) where TId : Id where TCommand : class;
-
-delegate ValueTask GetIdFromUntypedCommand(object command, CancellationToken cancellationToken)
- where TId : Id;
-
-class IdMap where TId : Id {
- readonly TypeMap> _typeMap = new();
-
- public void AddCommand(GetIdFromCommand getId) where TCommand : class
- => _typeMap.Add((cmd, _) => new ValueTask(getId((TCommand)cmd)));
-
- public void AddCommand(GetIdFromCommandAsync getId) where TCommand : class
- => _typeMap.Add(async (cmd, ct) => await getId((TCommand)cmd, ct));
-
- internal bool TryGet([NotNullWhen(true)] out GetIdFromUntypedCommand? getId) where TCommand : class
- => _typeMap.TryGetValue(out getId);
-}
diff --git a/src/Core/src/Eventuous.Application/CommandToStreamMap.cs b/src/Core/src/Eventuous.Application/CommandToStreamMap.cs
deleted file mode 100644
index 1641f0a9..00000000
--- a/src/Core/src/Eventuous.Application/CommandToStreamMap.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (C) Ubiquitous AS. All rights reserved
-// Licensed under the Apache License, Version 2.0.
-
-namespace Eventuous;
-
-public delegate StreamName GetStreamNameFromCommand(TCommand command) where TCommand : class;
-
-delegate ValueTask GetStreamNameFromUntypedCommand(object command, CancellationToken cancellationToken);
-
-public class CommandToStreamMap {
- readonly TypeMap _typeMap = new();
-
- public void AddCommand(GetStreamNameFromCommand getId) where TCommand : class
- => _typeMap.Add((cmd, _) => new ValueTask(getId((TCommand)cmd)));
-
- internal bool TryGet([NotNullWhen(true)] out GetStreamNameFromUntypedCommand? getId) where TCommand : class
- => _typeMap.TryGetValue(out getId);
-}
diff --git a/src/Core/src/Eventuous.Application/Diagnostics/ApplicationEventSource.cs b/src/Core/src/Eventuous.Application/Diagnostics/ApplicationEventSource.cs
index cbd90e36..1619478b 100644
--- a/src/Core/src/Eventuous.Application/Diagnostics/ApplicationEventSource.cs
+++ b/src/Core/src/Eventuous.Application/Diagnostics/ApplicationEventSource.cs
@@ -16,14 +16,10 @@ class ApplicationEventSource : EventSource {
const int CommandHandledId = 3;
const int CommandHandlerAlreadyRegisteredId = 4;
const int CommandHandlerRegisteredId = 5;
- const int CannotGetAggregateIdFromCommandId = 11;
[NonEvent]
public void CommandHandlerNotFound() => CommandHandlerNotFound(typeof(TCommand).Name);
- [NonEvent]
- public void CannotCalculateAggregateId() => CannotCalculateAggregateId(typeof(TCommand).Name);
-
[NonEvent]
public void ErrorHandlingCommand(Exception e) => ErrorHandlingCommand(typeof(TCommand).Name, e.ToString());
@@ -43,13 +39,6 @@ public void CommandHandlerRegistered() {
[Event(CommandHandlerNotFoundId, Message = "Handler not found for command: '{0}'", Level = EventLevel.Error)]
void CommandHandlerNotFound(string commandType) => WriteEvent(CommandHandlerNotFoundId, commandType);
- [Event(
- CannotGetAggregateIdFromCommandId,
- Message = "Cannot get aggregate id from command: '{0}'",
- Level = EventLevel.Error
- )]
- void CannotCalculateAggregateId(string commandType) => WriteEvent(CannotGetAggregateIdFromCommandId, commandType);
-
[Event(ErrorHandlingCommandId, Message = "Error handling command: '{0}' {1}", Level = EventLevel.Error)]
void ErrorHandlingCommand(string commandType, string exception)
=> WriteEvent(ErrorHandlingCommandId, commandType, exception);
diff --git a/src/Core/src/Eventuous.Application/Eventuous.Application.csproj b/src/Core/src/Eventuous.Application/Eventuous.Application.csproj
index ae060c26..d95ea841 100644
--- a/src/Core/src/Eventuous.Application/Eventuous.Application.csproj
+++ b/src/Core/src/Eventuous.Application/Eventuous.Application.csproj
@@ -3,14 +3,14 @@
Eventuous
-
-
+
+
Tools\Ensure.cs
-
+
@@ -18,6 +18,6 @@
-
+
diff --git a/src/Core/src/Eventuous.Application/Eventuous.Application.csproj.DotSettings b/src/Core/src/Eventuous.Application/Eventuous.Application.csproj.DotSettings
index 8c80dfa7..b1a2786e 100644
--- a/src/Core/src/Eventuous.Application/Eventuous.Application.csproj.DotSettings
+++ b/src/Core/src/Eventuous.Application/Eventuous.Application.csproj.DotSettings
@@ -1,2 +1,5 @@
+ True
+ True
+ TrueTrue
\ No newline at end of file
diff --git a/src/Core/src/Eventuous.Application/Exceptions/ExceptionMessages.cs b/src/Core/src/Eventuous.Application/Exceptions/ExceptionMessages.cs
index aa1586fb..a27cbf18 100644
--- a/src/Core/src/Eventuous.Application/Exceptions/ExceptionMessages.cs
+++ b/src/Core/src/Eventuous.Application/Exceptions/ExceptionMessages.cs
@@ -9,9 +9,6 @@ namespace Eventuous;
static class ExceptionMessages {
static readonly ResourceManager Resources = new("Eventuous.ExceptionMessages", Assembly.GetExecutingAssembly());
- internal static string AggregateIdEmpty(Type idType)
- => string.Format(Resources.GetString("AggregateIdEmpty")!, idType.Name);
-
internal static string MissingCommandHandler(Type type)
=> string.Format(Resources.GetString("MissingCommandHandler")!, type.Name);
diff --git a/src/Core/src/Eventuous.Application/Exceptions/Exceptions.cs b/src/Core/src/Eventuous.Application/Exceptions/Exceptions.cs
index 451844bb..74f69055 100644
--- a/src/Core/src/Eventuous.Application/Exceptions/Exceptions.cs
+++ b/src/Core/src/Eventuous.Application/Exceptions/Exceptions.cs
@@ -6,28 +6,15 @@ namespace Eventuous;
using static ExceptionMessages;
public static class Exceptions {
- public class CommandHandlerNotFound : Exception {
- public CommandHandlerNotFound(Type type) : base(MissingCommandHandler(type)) { }
- }
-
- public class UnableToResolveAggregateId : Exception {
- public UnableToResolveAggregateId(Type type) :
- base($"Unable to resolve aggregate id from command {type.Name}") { }
- }
-
- public class CommandHandlerNotFound : CommandHandlerNotFound {
- public CommandHandlerNotFound() : base(typeof(T)) { }
- }
-
- public class CommandHandlerAlreadyRegistered : Exception {
- public CommandHandlerAlreadyRegistered() : base(DuplicateCommandHandler()) { }
- }
-
- public class DuplicateTypeException : ArgumentException {
- public DuplicateTypeException() : base(DuplicateTypeKey(), typeof(T).FullName) { }
- }
-
- public class CommandMappingException : Exception {
- public CommandMappingException() : base(MissingCommandMap()) { }
- }
+ public class CommandHandlerNotFound(Type type) : Exception(MissingCommandHandler(type));
+
+ public class UnableToResolveAggregateId(Type type) : Exception($"Unable to resolve aggregate id from command {type.Name}");
+
+ public class CommandHandlerNotFound