Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ resharper_csharp_blank_lines_around_single_line_field = 0
resharper_csharp_blank_lines_around_single_line_invocable = 1
resharper_csharp_empty_block_style = together
resharper_csharp_int_align_comments = true
resharper_csharp_max_line_length = 140
resharper_csharp_max_line_length = 180
resharper_csharp_other_braces = end_of_line
resharper_csharp_wrap_after_declaration_lpar = true
resharper_csharp_wrap_arguments_style = chop_if_long
Expand Down
36 changes: 14 additions & 22 deletions src/Core/src/Eventuous.Application/CommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ ActOnAggregate<TAggregate, TCommand> action
/// <param name="action">Asynchronous action to be performed on the aggregate,
/// given the aggregate instance and the command</param>
/// <typeparam name="TCommand">Command type</typeparam>
[PublicAPI]
protected void OnExistingAsync<TCommand>(
GetIdFromCommand<TId, TCommand> getId,
ActOnAggregateAsync<TAggregate, TCommand> action
Expand All @@ -104,6 +105,7 @@ ActOnAggregateAsync<TAggregate, TCommand> action
/// <param name="action">Asynchronous action to be performed on the aggregate,
/// given the aggregate instance and the command</param>
/// <typeparam name="TCommand">Command type</typeparam>
[PublicAPI]
protected void OnExistingAsync<TCommand>(
GetIdFromCommandAsync<TId, TCommand> getId,
ActOnAggregateAsync<TAggregate, TCommand> action
Expand Down Expand Up @@ -134,6 +136,7 @@ ActOnAggregate<TAggregate, TCommand> action
/// <param name="action">Action to be performed on the aggregate,
/// given the aggregate instance and the command</param>
/// <typeparam name="TCommand">Command type</typeparam>
[PublicAPI]
protected void OnAny<TCommand>(
GetIdFromCommandAsync<TId, TCommand> getId,
ActOnAggregate<TAggregate, TCommand> action
Expand All @@ -149,6 +152,7 @@ ActOnAggregate<TAggregate, TCommand> action
/// <param name="action">Asynchronous action to be performed on the aggregate,
/// given the aggregate instance and the command</param>
/// <typeparam name="TCommand">Command type</typeparam>
[PublicAPI]
protected void OnAnyAsync<TCommand>(
GetIdFromCommand<TId, TCommand> getId,
ActOnAggregateAsync<TAggregate, TCommand> action
Expand All @@ -164,6 +168,7 @@ ActOnAggregateAsync<TAggregate, TCommand> action
/// <param name="action">Asynchronous action to be performed on the aggregate,
/// given the aggregate instance and the command</param>
/// <typeparam name="TCommand">Command type</typeparam>
[PublicAPI]
protected void OnAnyAsync<TCommand>(
GetIdFromCommandAsync<TId, TCommand> getId,
ActOnAggregateAsync<TAggregate, TCommand> action
Expand All @@ -177,6 +182,7 @@ ActOnAggregateAsync<TAggregate, TCommand> action
/// </summary>
/// <param name="action">Function, which returns some aggregate instance to store</param>
/// <typeparam name="TCommand">Command type</typeparam>
[PublicAPI]
protected void OnAsync<TCommand>(ArbitraryActAsync<TCommand> action)
where TCommand : class
=> _handlers.AddHandler<TCommand>(
Expand Down Expand Up @@ -211,20 +217,13 @@ public async Task<Result<TState>> Handle<TCommand>(TCommand command, Cancellatio

var aggregateId = await getId(command, cancellationToken).NoContext();

var streamName = _streamNameMap.GetStreamName<TAggregate, TId>(aggregateId);

try {
var aggregate = registeredHandler.ExpectedState switch {
ExpectedState.Any => await Store.LoadOrNew<TAggregate>(streamName, cancellationToken)
.NoContext(),
ExpectedState.Existing => await Store.Load<TAggregate>(streamName, cancellationToken)
.NoContext(),
ExpectedState.New => Create(),
ExpectedState.Unknown => default,
_ => throw new ArgumentOutOfRangeException(
nameof(registeredHandler.ExpectedState),
"Unknown expected state"
)
ExpectedState.Any => await Store.LoadOrNew<TAggregate, TState, TId>(_streamNameMap, aggregateId, cancellationToken).NoContext(),
ExpectedState.Existing => await Store.Load<TAggregate, TState, TId>(_streamNameMap, aggregateId, cancellationToken).NoContext(),
ExpectedState.New => Create(aggregateId),
ExpectedState.Unknown => default,
_ => throw new ArgumentOutOfRangeException(nameof(registeredHandler.ExpectedState), "Unknown expected state")
};

var result = await registeredHandler
Expand All @@ -234,14 +233,7 @@ public async Task<Result<TState>> Handle<TCommand>(TCommand command, Cancellatio
// 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<TState>(result.State, Array.Empty<Change>(), 0);

var storeResult = await Store.Store(
streamName != default
? streamName
: GetAggregateStreamName(),
result,
cancellationToken
)
.NoContext();
var storeResult = await Store.Store(GetAggregateStreamName(), result, cancellationToken).NoContext();

var changes = result.Changes.Select(x => new Change(x, _typeMap.GetTypeName(x)));

Expand All @@ -255,8 +247,8 @@ public async Task<Result<TState>> Handle<TCommand>(TCommand command, Cancellatio
return new ErrorResult<TState>($"Error handling command {typeof(TCommand).Name}", e);
}

TAggregate Create()
=> _factoryRegistry.CreateInstance<TAggregate, TState>();
TAggregate Create(TId id)
=> _factoryRegistry.CreateInstance<TAggregate, TState>().WithId<TAggregate, TState, TId>(id);

StreamName GetAggregateStreamName()
=> _streamNameMap.GetStreamName<TAggregate, TId>(aggregateId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@
<ProjectReference Include="..\Eventuous.Persistence\Eventuous.Persistence.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Eventuous.Diagnostics\DiagnosticName.cs">
<Link>Diagnostics\DiagnosticName.cs</Link>
</Compile>
<Compile Include="..\Eventuous.Shared\Tools\Ensure.cs">
<Link>Tools\Ensure.cs</Link>
</Compile>
<Compile Include="..\Eventuous.Shared\Tools\TaskExtensions.cs">
<Link>Tools\TaskExtensions.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Exceptions\ExceptionMessages.restext">
Expand Down
7 changes: 6 additions & 1 deletion src/Core/src/Eventuous.Domain/AggregateState.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Eventuous;

[Obsolete("Use State<T> instead")]
public abstract record AggregateState<T>: State<T> where T: AggregateState<T> { }
public abstract record AggregateState<T> : State<T> where T : AggregateState<T> { }

[PublicAPI]
public abstract record State<T> where T : State<T> {
Expand All @@ -24,3 +24,8 @@ protected void On<TEvent>(Func<T, TEvent, T> handle) {

readonly Dictionary<Type, Func<T, object, T>> _handlers = new();
}

[PublicAPI]
public abstract record State<T, TId> : State<T> where T : State<T> where TId : AggregateId {
public TId Id { get; internal set; } = null!;
}
11 changes: 7 additions & 4 deletions src/Core/src/Eventuous.Domain/Eventuous.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
<RootNamespace>Eventuous</RootNamespace>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Exceptions\ExceptionMessages.restext">
<LogicalName>Eventuous.ExceptionMessages.resources</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Exceptions\ExceptionMessages.restext">
<LogicalName>Eventuous.ExceptionMessages.resources</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Include="$(CoreRoot)\Eventuous.Shared\Tools\Ensure.cs">
<Link>Tools\Ensure.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<None Remove="Eventuous.Domain.csproj.DotSettings" />
<None Remove="Eventuous.Domain.csproj.DotSettings"/>
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Eventuous.Persistence"/>
</ItemGroup>
</Project>
60 changes: 60 additions & 0 deletions src/Core/src/Eventuous.Persistence/AggregateStoreExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (C) Ubiquitous AS. All rights reserved
// Licensed under the Apache License, Version 2.0.

namespace Eventuous;

public static class AggregateStoreExtensions {
/// <summary>
/// Loads an aggregate by its ID, assigns the State.Id property
/// </summary>
/// <param name="store">Aggregate store instance</param>
/// <param name="streamNameMap">Stream name map</param>
/// <param name="id">Aggregate id</param>
/// <param name="cancellationToken"></param>
/// <typeparam name="T">Aggregate type</typeparam>
/// <typeparam name="TState">State type</typeparam>
/// <typeparam name="TId">Aggregate id type</typeparam>
/// <returns></returns>
public static async Task<T> Load<T, TState, TId>(
this IAggregateStore store,
StreamNameMap streamNameMap,
TId id,
CancellationToken cancellationToken
) where T : Aggregate<TState> where TId : AggregateId where TState : State<TState>, new() {
var aggregate = await store.Load<T>(streamNameMap.GetStreamName<T, TId>(id), cancellationToken);
return aggregate.WithId<T, TState, TId>(id);
}

/// <summary>
/// Loads an aggregate by its ID, assigns the State.Id property.
/// If the aggregate stream is not found, returns a new aggregate instance
/// </summary>
/// <param name="store">Aggregate store instance</param>
/// <param name="streamNameMap">Stream name map</param>
/// <param name="id">Aggregate id</param>
/// <param name="cancellationToken"></param>
/// <typeparam name="T">Aggregate type</typeparam>
/// <typeparam name="TState">State type</typeparam>
/// <typeparam name="TId">Aggregate id type</typeparam>
/// <returns></returns>
public static async Task<T> LoadOrNew<T, TState, TId>(
this IAggregateStore store,
StreamNameMap streamNameMap,
TId id,
CancellationToken cancellationToken
) where T : Aggregate<TState> where TId : AggregateId where TState : State<TState>, new() {
var aggregate = await store.LoadOrNew<T>(streamNameMap.GetStreamName<T, TId>(id), cancellationToken);
return aggregate.WithId<T, TState, TId>(id);
}

internal static T WithId<T, TState, TId>(this T aggregate, TId id)
where T : Aggregate<TState>
where TState : State<TState>, new()
where TId : AggregateId {
if (aggregate.State is State<TState, TId> stateWithId) {
stateWithId.Id = id;
}

return aggregate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
<Compile Include="..\Eventuous.Shared\Tools\TaskExtensions.cs">
<Link>Tools\TaskExtensions.cs</Link>
</Compile>
<Compile Include="..\Eventuous.Shared\Tools\Ensure.cs">
<Link>Tools\Ensure.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Eventuous.Diagnostics\Eventuous.Diagnostics.csproj" />
Expand All @@ -21,5 +18,6 @@
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Eventuous.Spyglass" />
<InternalsVisibleTo Include="Eventuous.Application"/>
</ItemGroup>
</Project>
42 changes: 42 additions & 0 deletions src/Core/test/Eventuous.Tests.Application/StateWithIdTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Eventuous.Sut.App;
using Eventuous.Sut.Domain;
using Eventuous.TestHelpers.Fakes;
using NodaTime;

namespace Eventuous.Tests.Application;

public class StateWithIdTests {
readonly BookingService _service;
readonly AggregateStore _aggregateStore;

public StateWithIdTests() {
var store = new InMemoryEventStore();
_aggregateStore = new AggregateStore(store);
_service = new BookingService(_aggregateStore);
}

[Fact]
public async Task ShouldGetIdForNew() {
var map = new StreamNameMap();
var id = Guid.NewGuid().ToString();
var state = await Seed(id);

var bookingId = new BookingId(id);

// Ensure that the id was set when the aggregate was created
state.State!.Id.Should().Be(bookingId);

var instance = await _aggregateStore.Load<Booking, BookingState, BookingId>(map, bookingId, default);

// Ensure that the id was set when the aggregate was loaded
instance.State.Id.Should().Be(bookingId);
}

async Task<Result<BookingState>> Seed(string id) {
var checkIn = LocalDate.FromDateTime(DateTime.Today);
var checkOut = checkIn.PlusDays(1);
var cmd = new Commands.BookRoom(id, "234", checkIn, checkOut, 100);

return await _service.Handle(cmd, default);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Eventuous.AspNetCore.Web;
public static class AggregateFactoryBuilderExtensions {
/// <summary>
/// Adds registered aggregate factories to the registry. The registry is then used by
/// <see cref="ApplicationService{T,TState,TId}"/> and <see cref="AggregateStore"/>
/// <see cref="CommandService{T,TState,TId}"/> and <see cref="AggregateStore"/>
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ async Task<IResult>(HttpContext context, ICommandService<TAggregate> service) =>
}

/// <summary>
/// Maps commands that are annotated either with <seealso cref="AggregateCommands"/> and/or
/// Maps commands that are annotated either with <seealso cref="AggregateCommandsAttribute"/> and/or
/// <seealso cref="HttpCommandAttribute"/> in given assemblies. Will use assemblies of the current
/// application domain if no assembly is specified explicitly.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion test/Eventuous.Sut.Domain/BookingState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Eventuous.Sut.Domain;

public record BookingState : State<BookingState> {
public record BookingState : State<BookingState, BookingId> {
public BookingState() {
On<RoomBooked>(
(state, booked) => state with {
Expand Down