Skip to content

Commit

Permalink
Merge pull request #1397 from Cratis/fix/rehydration
Browse files Browse the repository at this point in the history
Fix/rehydration
  • Loading branch information
einari authored Sep 12, 2024
2 parents e20a37a + 378c219 commit 22ce0cf
Show file tree
Hide file tree
Showing 52 changed files with 546 additions and 64 deletions.
10 changes: 10 additions & 0 deletions Integration/Orleans.InProcess/AggregateRoots/Concepts/UserId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Concepts;

public record UserId(Guid Value) : ConceptAs<Guid>(Value)
{
public static implicit operator Guid(UserId value) => value.Value;
public static implicit operator UserId(Guid value) => new(value);
}
10 changes: 10 additions & 0 deletions Integration/Orleans.InProcess/AggregateRoots/Concepts/UserName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Concepts;

public record UserName(string Value) : ConceptAs<string>(Value)
{
public static implicit operator string(UserName value) => value.Value;
public static implicit operator UserName(string value) => new(value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Cratis.Chronicle.Aggregates;

Check warning on line 1 in Integration/Orleans.InProcess/AggregateRoots/Domain.Interfaces/IUser.cs

View workflow job for this annotation

GitHub Actions / dotnet-build

A source file is missing a required header. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0073)
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Concepts;
using IAggregateRoot = Cratis.Chronicle.Orleans.Aggregates.IAggregateRoot;

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Domain.Interfaces;

public record UserInternalState(StateProperty<UserName> Name, StateProperty<bool> Deleted);

Check warning on line 7 in Integration/Orleans.InProcess/AggregateRoots/Domain.Interfaces/IUser.cs

View workflow job for this annotation

GitHub Actions / dotnet-build


public interface IUser : IIntegrationTestAggregateRoot<UserInternalState>
{
Task Onboard(UserName name);
Task Delete();
Task ChangeUserName(UserName newName);
Task<bool> Exists();
}
42 changes: 42 additions & 0 deletions Integration/Orleans.InProcess/AggregateRoots/Domain/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Cratis.Chronicle.Aggregates;

Check warning on line 1 in Integration/Orleans.InProcess/AggregateRoots/Domain/User.cs

View workflow job for this annotation

GitHub Actions / dotnet-build

A source file is missing a required header. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0073)
using Cratis.Chronicle.Concepts.Events;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Concepts;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Domain.Interfaces;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Events;
using AggregateRoot = Cratis.Chronicle.Orleans.Aggregates.AggregateRoot;
using IAggregateRootFactory = Cratis.Chronicle.Orleans.Aggregates.IAggregateRootFactory;

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Domain;

public class User(IAggregateRootFactory aggregateRootFactory) : AggregateRoot, IUser
{
public class UserDeleted(EventSourceId id) : InvalidOperationException($"User {id} is deleted");

public StateProperty<UserName> Name = StateProperty<UserName>.Empty;
public StateProperty<bool> Deleted = StateProperty<bool>.Empty;

public Task Onboard(UserName name) => ApplyIfNotDeleted(new UserOnBoarded(name));
public Task Delete() => ApplyIfNotDeleted(new Events.UserDeleted());
public Task ChangeUserName(UserName newName) =>
Name.Value == newName ? Task.CompletedTask : ApplyIfNotDeleted(new UserNameChanged(newName));

public Task<bool> Exists() => Task.FromResult(!IsNew && !Deleted.Value);
public Task<CorrelationId> GetCorrelationId() => Task.FromResult(Context!.UnitOfWOrk.CorrelationId);
public Task<bool> GetIsNew() => Task.FromResult(IsNew);

Task ApplyIfNotDeleted(object evt) => Deleted.Value ? throw new UserDeleted(IdentityString) : Apply(evt);

public Task On(UserOnBoarded evt)
{
Name = Name.New(evt.Name);
return Task.CompletedTask;
}

public Task On(UserDeleted evt)
{
Deleted = Deleted.New(true);
return Task.CompletedTask;
}

public Task<UserInternalState> GetState() => Task.FromResult(new UserInternalState(Name, Deleted));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Events;

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots;

public record EventAndEventSourceId(EventSourceId EventSourceId, object Event);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Cratis.Chronicle.Events;

Check warning on line 1 in Integration/Orleans.InProcess/AggregateRoots/Events/UserDeleted.cs

View workflow job for this annotation

GitHub Actions / dotnet-build

A source file is missing a required header. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0073)

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Events;

[EventType]
public record UserDeleted;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Concepts;

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Events;
[EventType]
public record UserNameChanged(UserName NewName);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Concepts;

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Events;

[EventType]
public record UserOnBoarded(UserName Name);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Orleans.Aggregates;

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots;

public interface IIntegrationTestAggregateRoot<TInternalState> : IAggregateRoot
where TInternalState : class
{
Task<TInternalState> GetState();
Task<CorrelationId> GetCorrelationId();
Task<bool> GetIsNew();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Integration.Base;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Domain;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Domain.Interfaces;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Events;
using Cratis.Chronicle.Orleans.Aggregates;
using Cratis.Chronicle.Transactions;

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Scenarios.given;

public class context_for_aggregate_root<TAggregate, TInternalState>(GlobalFixture globalFixture) : IntegrationSpecificationContext(globalFixture)
where TAggregate : IIntegrationTestAggregateRoot<TInternalState>
where TInternalState : class
{
#pragma warning disable CA2213 // Disposable fields should be disposed
protected GlobalFixture _globalFixture = globalFixture;
#pragma warning restore CA2213 // Disposable fields should be disposed

public TInternalState ResultState;
public bool IsNew;
public IUnitOfWork UnitOfWork;

public override IEnumerable<Type> AggregateRoots => [typeof(User)];
public override IEnumerable<Type> EventTypes => [typeof(UserOnBoarded), typeof(UserDeleted), typeof(UserNameChanged)];

protected List<EventAndEventSourceId> EventsWithEventSourceIdToAppend = [];
protected IAggregateRootFactory AggregateRootFactory => Services.GetRequiredService<IAggregateRootFactory>();
protected IUnitOfWorkManager UnitOfWorkManager => Services.GetRequiredService<IUnitOfWorkManager>();

protected override void ConfigureServices(IServiceCollection services)
{
}

protected async Task DoOnAggregate(EventSourceId eventSourceId, Func<TAggregate, Task> action, bool commitUnitOfWork = true)
{
var user = await AggregateRootFactory.Get<TAggregate>(eventSourceId);
IsNew = await user.GetIsNew();
await action(user);
var correlationId = await user.GetCorrelationId();
ResultState = await user.GetState();
UnitOfWorkManager.TryGetFor(correlationId, out UnitOfWork);
if (commitUnitOfWork)
{
await UnitOfWork!.Commit();
}
}

void Establish()
{
}

async Task Because()
{
foreach (var @event in EventsWithEventSourceIdToAppend)
{
await EventStore.EventLog.Append(@event.EventSourceId, @event.Event);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Chronicle.Aggregates;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Integration.Base;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Concepts;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Domain;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Domain.Interfaces;
using Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Events;
using Cratis.Chronicle.Transactions;
using context = Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Scenarios.when_there_are_events_to_rehydrate.and_performing_no_action_on_aggregate.context;

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots.Scenarios.when_there_are_events_to_rehydrate;

[Collection(GlobalCollection.Name)]
public class and_performing_no_action_on_aggregate(context context) : Given<context>(context)
{
public class context(GlobalFixture globalFixture) : given.context_for_aggregate_root<IUser, UserInternalState>(globalFixture)
{
UserId _userId;
public UserName UserName;

public bool UserExists;

void Establish()
{
_userId = Guid.NewGuid();
UserName = "some name";
EventsWithEventSourceIdToAppend.Add(new EventAndEventSourceId(_userId.Value, new UserOnBoarded(UserName)));
}

Task Because() => DoOnAggregate(_userId.Value, async user => UserExists = await user.Exists());
}

[Fact]
void should_not_be_new_aggregate() => Context.IsNew.ShouldBeFalse();

[Fact]
void should_return_that_user_exists() => Context.UserExists.ShouldBeTrue();

[Fact]
void should_commit_unit_of_work_successfully() => Context.UnitOfWork.IsSuccess.ShouldBeTrue();

[Fact]
void should_not_commit_any_events() => Context.UnitOfWork.GetEvents().ShouldBeEmpty();

[Fact]
void should_not_be_deleted() => Context.ResultState.Deleted.ShouldEqual(new StateProperty<bool>(false, 0));

[Fact]
void should_have_assigned_the_username_once() => Context.ResultState.Name.ShouldEqual(new StateProperty<UserName>(Context.UserName, 1));
}
11 changes: 11 additions & 0 deletions Integration/Orleans.InProcess/AggregateRoots/StateProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

namespace Cratis.Chronicle.Integration.Orleans.InProcess.AggregateRoots;

public record StateProperty<TValue>(TValue Value, int CallCount)
{
public static StateProperty<TValue> Empty => new(default, 0);

public StateProperty<TValue> New(TValue value) => IncrementCallCount() with { Value = value };

public StateProperty<TValue> IncrementCallCount() => this with { CallCount = CallCount + 1 };
}
3 changes: 3 additions & 0 deletions Integration/Orleans.InProcess/Orleans.InProcess.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
</ItemGroup>

<ItemGroup>
<Folder Include="AggregateRoots\Domain\" />
<Folder Include="AggregateRoots\Scenarios\given\" />
<Folder Include="AggregateRoots\Scenarios\when_there_are_events_to_rehydrate\" />
<Folder Include="for_Observers\when_appending_event\" />
</ItemGroup>

Expand Down
16 changes: 13 additions & 3 deletions Source/Clients/AspNetCore/Transactions/UnitOfWorkActionFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,32 @@
using Cratis.Chronicle.Events.Constraints;
using Cratis.Chronicle.Transactions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace Cratis.Chronicle.AspNetCore.Transactions;

/// <summary>
/// Represents an implementation of <see cref="IAsyncActionFilter"/> for managing units of work and errors.
/// </summary>
/// <param name="unitOfWorkManager">The <see cref="IUnitOfWorkManager"/> to use.</param>
public class UnitOfWorkActionFilter(IUnitOfWorkManager unitOfWorkManager) : IAsyncActionFilter
/// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
public class UnitOfWorkActionFilter(IUnitOfWorkManager unitOfWorkManager, ILogger<UnitOfWorkActionFilter> logger) : IAsyncActionFilter
{
/// <inheritdoc/>
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
await next();

var unitOfWork = unitOfWorkManager.Current;
await unitOfWork.Commit();

if (!unitOfWork.IsCompleted)
{
await unitOfWork.Commit();
}
else
{
logger.AlreadyCompletedManually(unitOfWork.CorrelationId);
}
if (!unitOfWork.IsSuccess)
{
foreach (var violation in unitOfWork.GetConstraintViolations())
Expand All @@ -29,4 +39,4 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Execution;
using Microsoft.Extensions.Logging;

namespace Cratis.Chronicle.AspNetCore.Transactions;

internal static partial class UnitOfWorkActionFilterLogging
{
[LoggerMessage(LogLevel.Warning, "The unit of work for correlation {CorrelationId} has already been completed manually either by Commit or Rollback. It is recommended to not complete the unit of work manually in this context.")]
internal static partial void AlreadyCompletedManually(this ILogger<UnitOfWorkActionFilter> logger, CorrelationId correlationId);
}
15 changes: 13 additions & 2 deletions Source/Clients/AspNetCore/Transactions/UnitOfWorkMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ public class UnitOfWorkMiddleware(IUnitOfWorkManager unitOfWorkManager, RequestD
/// <returns>Awaitable task.</returns>
public async Task InvokeAsync(HttpContext context)
{
unitOfWorkManager.Begin(CorrelationId.New());
await next(context);
var unitOfWork = unitOfWorkManager.Begin(CorrelationId.New());
try
{
await next(context);
}
catch
{
if (!unitOfWork.IsCompleted)
{
unitOfWork.Dispose();
}
throw;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ void Establish()

_unitOfWork = Substitute.For<IUnitOfWork>();

_aggregateRootContext = new AggregateRootContext(_eventSourceId, _eventSequence, _aggregateRoot, _unitOfWork);
_aggregateRootContext = new AggregateRootContext(_eventSourceId, _eventSequence, _aggregateRoot, _unitOfWork, EventSequenceNumber.First);
_aggregateRoot._context = _aggregateRootContext;
_aggregateRoot._mutation = _mutation;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ void Establish()
_aggregateRoot = new();
_eventSourceId = Guid.NewGuid().ToString();
_unitOfWork = Substitute.For<IUnitOfWork>();
_aggregateRootContext = new AggregateRootContext(_eventSourceId, _eventSequence, _aggregateRoot, _unitOfWork);
_aggregateRootContext = new AggregateRootContext(_eventSourceId, _eventSequence, _aggregateRoot, _unitOfWork, EventSequenceNumber.First);

_aggregateRoot._context = _aggregateRootContext;
_aggregateRoot._mutation = _mutation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class when_creating_for_stateful_aggregate_root : given.an_aggregate_root
EventSourceId.New(),
Substitute.For<IEventSequence>(),
new StatefulAggregateRoot(),
Substitute.For<IUnitOfWork>());
Substitute.For<IUnitOfWork>(),
EventSequenceNumber.First);

async Task Because() => _result = await _factory.Create<StatefulAggregateRoot>(_context);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class when_creating_for_stateless_aggregate_root : given.an_aggregate_roo
EventSourceId.New(),
Substitute.For<IEventSequence>(),
new StatelessAggregateRoot(),
Substitute.For<IUnitOfWork>());
Substitute.For<IUnitOfWork>(),
EventSequenceNumber.First);

async Task Because() => _result = await _factory.Create<StatelessAggregateRoot>(_context);

Expand Down
Loading

0 comments on commit 22ce0cf

Please sign in to comment.