-
-
Notifications
You must be signed in to change notification settings - Fork 525
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Business Processes starting point and refactored exercises solu…
…tions
- Loading branch information
1 parent
e779157
commit 44e910c
Showing
20 changed files
with
1,217 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
Workshops/EventDrivenArchitecture/03-BusinessProcesses/Core/Database.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
using System.Text.Json; | ||
|
||
namespace BusinessProcesses.Core; | ||
|
||
public class Database | ||
{ | ||
private readonly Dictionary<string, object> storage = new(); | ||
|
||
public ValueTask Store<T>(Guid id, T obj, CancellationToken _) where T : class | ||
{ | ||
storage[GetId<T>(id)] = obj; | ||
|
||
return ValueTask.CompletedTask; | ||
} | ||
|
||
public ValueTask Delete<T>(Guid id, CancellationToken _) | ||
{ | ||
storage.Remove(GetId<T>(id)); | ||
return ValueTask.CompletedTask; | ||
} | ||
|
||
public ValueTask<T?> Get<T>(Guid id, CancellationToken _) where T : class => | ||
ValueTask.FromResult( | ||
storage.TryGetValue(GetId<T>(id), out var result) | ||
? | ||
// Clone to simulate getting new instance on loading | ||
JsonSerializer.Deserialize<T>(JsonSerializer.Serialize((T)result)) | ||
: null | ||
); | ||
|
||
private static string GetId<T>(Guid id) => $"{typeof(T).Name}-{id}"; | ||
} |
65 changes: 65 additions & 0 deletions
65
Workshops/EventDrivenArchitecture/03-BusinessProcesses/Core/EventBus.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
using System.Collections.Concurrent; | ||
using FluentAssertions; | ||
|
||
namespace BusinessProcesses.Core; | ||
|
||
public class EventBus | ||
{ | ||
public ValueTask Publish(object[] events, CancellationToken _) | ||
{ | ||
foreach (var @event in events) | ||
{ | ||
if (!eventHandlers.TryGetValue(@event.GetType(), out var handlers)) | ||
continue; | ||
|
||
foreach (var middleware in middlewares) | ||
middleware(@event); | ||
|
||
foreach (var handler in handlers) | ||
handler(@event); | ||
} | ||
|
||
return ValueTask.CompletedTask; | ||
} | ||
|
||
public void Subscribe<T>(Action<T> eventHandler) | ||
{ | ||
Action<object> handler = x => eventHandler((T)x); | ||
|
||
eventHandlers.AddOrUpdate( | ||
typeof(T), | ||
_ => [handler], | ||
(_, handlers) => | ||
{ | ||
handlers.Add(handler); | ||
return handlers; | ||
} | ||
); | ||
} | ||
|
||
public void Use(Action<object> middleware) => | ||
middlewares.Add(middleware); | ||
|
||
private readonly ConcurrentDictionary<Type, List<Action<object>>> eventHandlers = new(); | ||
private readonly List<Action<object>> middlewares = []; | ||
} | ||
|
||
public class EventCatcher | ||
{ | ||
public List<object> Published { get; } = []; | ||
|
||
public void Catch(object @event) => | ||
Published.Add(@event); | ||
|
||
public void Reset() => Published.Clear(); | ||
|
||
public void ShouldNotReceiveAnyEvent() => | ||
Published.Should().BeEmpty(); | ||
|
||
public void ShouldReceiveSingleEvent<T>(T @event) | ||
{ | ||
Published.Should().HaveCount(1); | ||
Published.OfType<T>().Should().HaveCount(1); | ||
Published.Single().Should().Be(@event); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
Workshops/EventDrivenArchitecture/03-BusinessProcesses/Version1-Aggregates/Core/Aggregate.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace BusinessProcesses.Version1_Aggregates.Core; | ||
|
||
public abstract class Aggregate<TEvent, TId> | ||
where TEvent : class | ||
where TId : notnull | ||
{ | ||
[JsonInclude] public TId Id { get; protected set; } = default!; | ||
|
||
[NonSerialized] private readonly Queue<TEvent> uncommittedEvents = new(); | ||
|
||
public virtual void Apply(TEvent @event) { } | ||
|
||
public object[] DequeueUncommittedEvents() | ||
{ | ||
var dequeuedEvents = uncommittedEvents.Cast<object>().ToArray();; | ||
|
||
uncommittedEvents.Clear(); | ||
|
||
return dequeuedEvents; | ||
} | ||
|
||
protected void Enqueue(TEvent @event) | ||
{ | ||
uncommittedEvents.Enqueue(@event); | ||
Apply(@event); | ||
} | ||
} | ||
|
173 changes: 173 additions & 0 deletions
173
...EventDrivenArchitecture/03-BusinessProcesses/Version1-Aggregates/EntityDefinitionTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
using Bogus; | ||
using BusinessProcesses.Core; | ||
using BusinessProcesses.Version1_Aggregates.GroupCheckouts; | ||
using BusinessProcesses.Version1_Aggregates.GuestStayAccounts; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
using Database = BusinessProcesses.Core.Database; | ||
|
||
namespace BusinessProcesses.Version1_Aggregates; | ||
|
||
using static GuestStayAccountEvent; | ||
using static GuestStayAccountCommand; | ||
using static GroupCheckoutCommand; | ||
|
||
public class EntityDefinitionTests | ||
{ | ||
private readonly ITestOutputHelper testOutputHelper; | ||
|
||
[Fact] | ||
public async Task CheckingInGuest_Succeeds() | ||
{ | ||
// Given | ||
var guestStayId = Guid.NewGuid(); | ||
var command = new CheckInGuest(guestStayId, now); | ||
publishedEvents.Reset(); | ||
|
||
// When | ||
await guestStayFacade.CheckInGuest(command); | ||
|
||
// Then | ||
publishedEvents.ShouldReceiveSingleEvent(new GuestCheckedIn(guestStayId, now)); | ||
} | ||
|
||
[Fact] | ||
public async Task RecordingChargeForCheckedInGuest_Succeeds() | ||
{ | ||
// Given | ||
var guestStayId = Guid.NewGuid(); | ||
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1))); | ||
publishedEvents.Reset(); | ||
// And | ||
var amount = generate.Finance.Amount(); | ||
var command = new RecordCharge(guestStayId, amount, now); | ||
|
||
// When | ||
await guestStayFacade.RecordCharge(command); | ||
|
||
// Then | ||
publishedEvents.ShouldReceiveSingleEvent(new ChargeRecorded(guestStayId, amount, now)); | ||
} | ||
|
||
[Fact] | ||
public async Task RecordingPaymentForCheckedInGuest_Succeeds() | ||
{ | ||
// Given | ||
var guestStayId = Guid.NewGuid(); | ||
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1))); | ||
publishedEvents.Reset(); | ||
// And | ||
var amount = generate.Finance.Amount(); | ||
var command = new RecordPayment(guestStayId, amount, now); | ||
|
||
// When | ||
await guestStayFacade.RecordPayment(command); | ||
|
||
// Then | ||
publishedEvents.ShouldReceiveSingleEvent(new PaymentRecorded(guestStayId, amount, now)); | ||
} | ||
|
||
[Fact] | ||
public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds() | ||
{ | ||
// Given | ||
var guestStayId = Guid.NewGuid(); | ||
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1))); | ||
await guestStayFacade.RecordCharge(new RecordCharge(guestStayId, generate.Finance.Amount(), now.AddHours(-1))); | ||
publishedEvents.Reset(); | ||
// And | ||
var amount = generate.Finance.Amount(); | ||
var command = new RecordPayment(guestStayId, amount, now); | ||
|
||
// When | ||
await guestStayFacade.RecordPayment(command); | ||
|
||
// Then | ||
publishedEvents.ShouldReceiveSingleEvent(new PaymentRecorded(guestStayId, amount, now)); | ||
} | ||
|
||
[Fact] | ||
public async Task CheckingOutGuestWithSettledBalance_Succeeds() | ||
{ | ||
// Given | ||
var guestStayId = Guid.NewGuid(); | ||
|
||
var amount = generate.Finance.Amount(); | ||
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1))); | ||
await guestStayFacade.RecordCharge(new RecordCharge(guestStayId, amount, now.AddHours(-2))); | ||
await guestStayFacade.RecordPayment(new RecordPayment(guestStayId, amount, now.AddHours(-1))); | ||
publishedEvents.Reset(); | ||
// And | ||
var command = new CheckOutGuest(guestStayId, now); | ||
|
||
// When | ||
await guestStayFacade.CheckOutGuest(command); | ||
|
||
// Then | ||
publishedEvents.ShouldReceiveSingleEvent(new GuestCheckedOut(guestStayId, now)); | ||
} | ||
|
||
[Fact] | ||
public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFailed() | ||
{ | ||
// Given | ||
var guestStayId = Guid.NewGuid(); | ||
|
||
var amount = generate.Finance.Amount(); | ||
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1))); | ||
await guestStayFacade.RecordCharge(new RecordCharge(guestStayId, amount + 10, now.AddHours(-2))); | ||
await guestStayFacade.RecordPayment(new RecordPayment(guestStayId, amount, now.AddHours(-1))); | ||
publishedEvents.Reset(); | ||
// And | ||
var command = new CheckOutGuest(guestStayId, now); | ||
|
||
// When | ||
try | ||
{ | ||
await guestStayFacade.CheckOutGuest(command); | ||
} | ||
catch (Exception exc) | ||
{ | ||
testOutputHelper.WriteLine(exc.Message); | ||
} | ||
|
||
// Then | ||
publishedEvents.ShouldReceiveSingleEvent(new GuestCheckoutFailed(guestStayId, GuestCheckoutFailed.FailureReason.BalanceNotSettled, now)); | ||
} | ||
|
||
[Fact] | ||
public async Task GroupCheckoutForMultipleGuestStay_ShouldBeInitiated() | ||
{ | ||
// Given; | ||
Guid[] guestStays = [Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()]; | ||
|
||
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStays[0], now.AddDays(-1))); | ||
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStays[1], now.AddDays(-1))); | ||
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStays[2], now.AddDays(-1))); | ||
publishedEvents.Reset(); | ||
// And | ||
var groupCheckoutId = Guid.NewGuid(); | ||
var clerkId = Guid.NewGuid(); | ||
var command = new InitiateGroupCheckout(groupCheckoutId, clerkId, guestStays, now); | ||
|
||
// When | ||
await guestStayFacade.InitiateGroupCheckout(command); | ||
|
||
// Then | ||
publishedEvents.ShouldReceiveSingleEvent(new GroupCheckoutEvent.GroupCheckoutInitiated(groupCheckoutId, clerkId, guestStays, now)); | ||
} | ||
|
||
private readonly Database database = new(); | ||
private readonly EventBus eventBus = new(); | ||
private readonly EventCatcher publishedEvents = new(); | ||
private readonly GuestStayFacade guestStayFacade; | ||
private readonly Faker generate = new(); | ||
private readonly DateTimeOffset now = DateTimeOffset.Now; | ||
|
||
public EntityDefinitionTests(ITestOutputHelper testOutputHelper) | ||
{ | ||
this.testOutputHelper = testOutputHelper; | ||
guestStayFacade = new GuestStayFacade(database, eventBus); | ||
eventBus.Use(publishedEvents.Catch); | ||
} | ||
} |
Oops, something went wrong.