Skip to content

Commit

Permalink
Simplified the serialisation handling
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Nov 6, 2024
1 parent c8220cd commit 9f0fe9a
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")]
namespace HotelManagement.EventStore
{
public class CommandHandler<T>
{
public CommandHandler(System.Func<T, object, T> evolve, System.Func<T> getInitial) { }
public System.Threading.Tasks.Task Handle(HotelManagement.EventStore.IEventStore eventStore, string id, System.Func<T, object[]> handle, System.Threading.CancellationToken ct) { }
}
public class EventSerializer : HotelManagement.EventStore.IEventSerializer
{
public EventSerializer(HotelManagement.EventStore.EventTypeMapping mapping, HotelManagement.EventStore.EventTransformations transformations, HotelManagement.EventStore.StreamTransformations? streamTransformations = null) { }
public object? Deserialize(HotelManagement.EventStore.SerializedEvent serializedEvent) { }
public System.Collections.Generic.List<object?> Deserialize(System.Collections.Generic.List<HotelManagement.EventStore.SerializedEvent> events) { }
public HotelManagement.EventStore.SerializedEvent Serialize(object @event) { }
}
public class EventTransformations
{
public EventTransformations() { }
public HotelManagement.EventStore.EventTransformations Register<TEvent>(string eventTypeName, System.Func<System.Text.Json.JsonDocument, TEvent> transformJson)
where TEvent : notnull { }
public HotelManagement.EventStore.EventTransformations Register<TOldEvent, TEvent>(string eventTypeName, System.Func<TOldEvent, TEvent> transformEvent)
where TOldEvent : notnull
where TEvent : notnull { }
public bool TryTransform(string eventTypeName, string json, out object? result) { }
}
public class EventTypeMapping
{
public EventTypeMapping() { }
public HotelManagement.EventStore.EventTypeMapping CustomMap(System.Type eventType, params string[] eventTypeNames) { }
public HotelManagement.EventStore.EventTypeMapping CustomMap<T>(params string[] eventTypeNames) { }
public string ToName(System.Type eventType) { }
public string ToName<TEventType>() { }
public System.Type? ToType(string eventTypeName) { }
}
public interface IEventSerializer
{
object? Deserialize(HotelManagement.EventStore.SerializedEvent serializedEvent);
System.Collections.Generic.List<object?> Deserialize(System.Collections.Generic.List<HotelManagement.EventStore.SerializedEvent> events);
HotelManagement.EventStore.SerializedEvent Serialize(object @event);
}
public interface IEventStore
{
System.Threading.Tasks.ValueTask AppendToStream(string streamId, System.Collections.Generic.IEnumerable<object> newEvents, System.Threading.CancellationToken ct = default);
System.Threading.Tasks.ValueTask<object[]> ReadStream(string streamId, System.Threading.CancellationToken ct = default);
}
public class InMemoryEventStore : HotelManagement.EventStore.IEventStore
{
public InMemoryEventStore(HotelManagement.EventStore.EventSerializer eventSerializer) { }
public System.Threading.Tasks.ValueTask AppendToStream(string streamId, System.Collections.Generic.IEnumerable<object> newEvents, System.Threading.CancellationToken _ = default) { }
public System.Threading.Tasks.ValueTask<object[]> ReadStream(string streamId, System.Threading.CancellationToken _ = default) { }
}
public class SerializedEvent : System.IEquatable<HotelManagement.EventStore.SerializedEvent>
{
public SerializedEvent(string EventType, string Data, string MetaData = "") { }
public string Data { get; init; }
public string EventType { get; init; }
public string MetaData { get; init; }
}
public class StreamTransformations
{
public StreamTransformations() { }
public HotelManagement.EventStore.StreamTransformations Register(System.Func<System.Collections.Generic.List<HotelManagement.EventStore.SerializedEvent>, System.Collections.Generic.List<HotelManagement.EventStore.SerializedEvent>> transformJson) { }
public System.Collections.Generic.List<HotelManagement.EventStore.SerializedEvent> Transform(System.Collections.Generic.List<HotelManagement.EventStore.SerializedEvent> events) { }
}
}
namespace HotelManagement.GuestStayAccounts
{
public class ChargeRecorded : System.IEquatable<HotelManagement.GuestStayAccounts.ChargeRecorded>
{
public ChargeRecorded(string GuestStayAccountId, decimal Amount, System.DateTimeOffset RecordedAt) { }
public decimal Amount { get; init; }
public string GuestStayAccountId { get; init; }
public System.DateTimeOffset RecordedAt { get; init; }
}
public class CheckIn : System.IEquatable<HotelManagement.GuestStayAccounts.CheckIn>
{
public CheckIn(string ClerkId, string GuestStayId, string RoomId, System.DateTimeOffset Now) { }
public string ClerkId { get; init; }
public string GuestStayId { get; init; }
public System.DateTimeOffset Now { get; init; }
public string RoomId { get; init; }
}
public class CheckOut : System.IEquatable<HotelManagement.GuestStayAccounts.CheckOut>
{
public CheckOut(string ClerkId, string GuestStayAccountId, System.DateTimeOffset Now) { }
public string ClerkId { get; init; }
public string GuestStayAccountId { get; init; }
public System.DateTimeOffset Now { get; init; }
}
public class GuestCheckedIn : System.IEquatable<HotelManagement.GuestStayAccounts.GuestCheckedIn>
{
public GuestCheckedIn(string GuestStayAccountId, string GuestStayId, string RoomId, string ClerkId, System.DateTimeOffset CheckedInAt) { }
public System.DateTimeOffset CheckedInAt { get; init; }
public string ClerkId { get; init; }
public string GuestStayAccountId { get; init; }
public string GuestStayId { get; init; }
public string RoomId { get; init; }
}
public class GuestCheckedOut : System.IEquatable<HotelManagement.GuestStayAccounts.GuestCheckedOut>
{
public GuestCheckedOut(string GuestStayAccountId, string ClerkId, System.DateTimeOffset CheckedOutAt) { }
public System.DateTimeOffset CheckedOutAt { get; init; }
public string ClerkId { get; init; }
public string GuestStayAccountId { get; init; }
}
public class GuestCheckoutFailed : System.IEquatable<HotelManagement.GuestStayAccounts.GuestCheckoutFailed>
{
public GuestCheckoutFailed(string GuestStayAccountId, string ClerkId, HotelManagement.GuestStayAccounts.GuestCheckoutFailed.FailureReason Reason, System.DateTimeOffset FailedAt) { }
public string ClerkId { get; init; }
public System.DateTimeOffset FailedAt { get; init; }
public string GuestStayAccountId { get; init; }
public HotelManagement.GuestStayAccounts.GuestCheckoutFailed.FailureReason Reason { get; init; }
public enum FailureReason
{
NotOpened = 0,
BalanceNotSettled = 1,
}
}
public class GuestStayAccount : System.IEquatable<HotelManagement.GuestStayAccounts.GuestStayAccount>
{
public static readonly HotelManagement.GuestStayAccounts.GuestStayAccount Initial;
public GuestStayAccount(string Id, [System.Runtime.CompilerServices.DecimalConstant(0, 0, 0u, 0u, 0u)] decimal Balance, HotelManagement.GuestStayAccounts.GuestStayAccountStatus Status = 1) { }
public bool IsSettled { get; }
public decimal Balance { get; init; }
public string Id { get; init; }
public HotelManagement.GuestStayAccounts.GuestStayAccountStatus Status { get; init; }
public static HotelManagement.GuestStayAccounts.GuestStayAccount Evolve(HotelManagement.GuestStayAccounts.GuestStayAccount state, object @event) { }
public static string GuestStayAccountId(string guestStayId, string roomId, System.DateOnly checkInDate) { }
}
public static class GuestStayAccountDecider
{
public static HotelManagement.GuestStayAccounts.GuestCheckedIn CheckIn(HotelManagement.GuestStayAccounts.CheckIn command, HotelManagement.GuestStayAccounts.GuestStayAccount state) { }
public static object CheckOut(HotelManagement.GuestStayAccounts.CheckOut command, HotelManagement.GuestStayAccounts.GuestStayAccount state) { }
public static HotelManagement.GuestStayAccounts.ChargeRecorded RecordCharge(HotelManagement.GuestStayAccounts.RecordCharge command, HotelManagement.GuestStayAccounts.GuestStayAccount state) { }
public static HotelManagement.GuestStayAccounts.PaymentRecorded RecordPayment(HotelManagement.GuestStayAccounts.RecordPayment command, HotelManagement.GuestStayAccounts.GuestStayAccount state) { }
}
public class GuestStayAccountService
{
public GuestStayAccountService(HotelManagement.EventStore.IEventStore eventStore) { }
public System.Threading.Tasks.Task CheckIn(HotelManagement.GuestStayAccounts.CheckIn command, System.Threading.CancellationToken ct = default) { }
public System.Threading.Tasks.Task CheckOut(HotelManagement.GuestStayAccounts.CheckOut command, System.Threading.CancellationToken ct = default) { }
public System.Threading.Tasks.Task RecordCharge(HotelManagement.GuestStayAccounts.RecordCharge command, System.Threading.CancellationToken ct = default) { }
public System.Threading.Tasks.Task RecordPayment(HotelManagement.GuestStayAccounts.RecordPayment command, System.Threading.CancellationToken ct = default) { }
}
public enum GuestStayAccountStatus
{
NotExisting = 0,
Opened = 1,
CheckedOut = 2,
}
public class PaymentRecorded : System.IEquatable<HotelManagement.GuestStayAccounts.PaymentRecorded>
{
public PaymentRecorded(string GuestStayAccountId, decimal Amount, System.DateTimeOffset RecordedAt) { }
public decimal Amount { get; init; }
public string GuestStayAccountId { get; init; }
public System.DateTimeOffset RecordedAt { get; init; }
}
public class RecordCharge : System.IEquatable<HotelManagement.GuestStayAccounts.RecordCharge>
{
public RecordCharge(string GuestStayAccountId, decimal Amount, System.DateTimeOffset Now) { }
public decimal Amount { get; init; }
public string GuestStayAccountId { get; init; }
public System.DateTimeOffset Now { get; init; }
}
public class RecordPayment : System.IEquatable<HotelManagement.GuestStayAccounts.RecordPayment>
{
public RecordPayment(string GuestStayAccountId, decimal Amount, System.DateTimeOffset Now) { }
public decimal Amount { get; init; }
public string GuestStayAccountId { get; init; }
public System.DateTimeOffset Now { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@ public void UpcastObjects_Should_BeForwardCompatible()

var events = new[]
{
new { EventType = eventTypeV1Name, EventData = JsonSerializer.Serialize(eventV1) },
new { EventType = eventTypeV2Name, EventData = JsonSerializer.Serialize(eventV2) },
new { EventType = eventTypeV3Name, EventData = JsonSerializer.Serialize(eventV3) }
new SerializedEvent(eventTypeV1Name, JsonSerializer.Serialize(eventV1)),
new SerializedEvent(eventTypeV2Name, JsonSerializer.Serialize(eventV2)),
new SerializedEvent(eventTypeV3Name, JsonSerializer.Serialize(eventV3))
};

// When
var deserializedEvents = events
.Select(ev => serializer.Deserialize(ev.EventType, ev.EventData))
.Select(serializer.Deserialize)
.OfType<PaymentRecorded>()
.ToList();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
namespace HotelManagement.EventStore;

public class CommandHandler<T, TEvent>(
IEventStore eventStore,
Func<T, TEvent, T> evolve,
public class CommandHandler<T>(
Func<T, object, T> evolve,
Func<T> getInitial
) where TEvent : notnull
)
{
public async Task GetAndUpdate(
Guid id,
Func<T, TEvent[]> handle,
public async Task Handle(
IEventStore eventStore,
string id,
Func<T, object[]> handle,
CancellationToken ct
)
{
var events = await eventStore.ReadStream<TEvent>(id, ct);
var events = await eventStore.ReadStream(id, ct);

var state = events.Aggregate(getInitial(), evolve);

var result = handle(state);

if(result.Length > 0)
await eventStore.AppendToStream(id, result.Cast<object>(), ct);
await eventStore.AppendToStream(id, result, ct);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@

namespace HotelManagement.EventStore;

public interface IEventSerializer
{
SerializedEvent Serialize(object @event);
object? Deserialize(SerializedEvent serializedEvent);
List<object?> Deserialize(List<SerializedEvent> events);
}

public class EventSerializer(
EventTypeMapping mapping,
EventTransformations transformations,
StreamTransformations? streamTransformations = null)
StreamTransformations? streamTransformations = null
): IEventSerializer
{
private readonly StreamTransformations streamTransformations = streamTransformations ?? new StreamTransformations();

public object? Deserialize(string eventTypeName, string json) =>
transformations.TryTransform(eventTypeName, json, out var transformed)
public SerializedEvent Serialize(object @event) =>
new(mapping.ToName(@event.GetType()), JsonSerializer.Serialize(@event));

public object? Deserialize(SerializedEvent serializedEvent) =>
transformations.TryTransform(serializedEvent.EventType, serializedEvent.Data, out var transformed)
? transformed
: JsonSerializer.Deserialize(json, mapping.ToType(eventTypeName)!);
: mapping.ToType(serializedEvent.EventType) is { } eventType
? JsonSerializer.Deserialize(serializedEvent.Data, eventType)
: null;

public List<object?> Deserialize(List<SerializedEvent> events) =>
streamTransformations.Transform(events)
.Select(@event => Deserialize(@event.EventType, @event.Data))
.Select(Deserialize)
.ToList();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,49 @@ namespace HotelManagement.EventStore;
public interface IEventStore
{
ValueTask AppendToStream(
Guid streamId,
string streamId,
IEnumerable<object> newEvents,
CancellationToken ct = default
);

ValueTask<TEvent[]> ReadStream<TEvent>(
Guid streamId,
ValueTask<object[]> ReadStream(
string streamId,
CancellationToken ct = default
) where TEvent : notnull;
);
}

public record EventMetadata(
Guid CorrelationId
);

public record SerializedEvent(
string EventType,
string Data,
string MetaData = ""
);

public class InMemoryEventStore: IEventStore
public class InMemoryEventStore(EventSerializer eventSerializer): IEventStore
{
private readonly Dictionary<Guid, List<SerializedEvent>> events = new();
private readonly Dictionary<string, List<SerializedEvent>> events = new();

public ValueTask AppendToStream(Guid streamId, IEnumerable<object> newEvents, CancellationToken _ = default)
public ValueTask AppendToStream(string streamId, IEnumerable<object> newEvents, CancellationToken _ = default)
{
if (!events.ContainsKey(streamId))
events[streamId] = [];

var serializedEvents = newEvents.Select(e =>
new SerializedEvent(e.GetType().FullName!, JsonSerializer.Serialize(e))
);
var serializedEvents = newEvents.Select(eventSerializer.Serialize);

events[streamId].AddRange(serializedEvents);

return ValueTask.CompletedTask;
}

public ValueTask<TEvent[]> ReadStream<TEvent>(Guid streamId, CancellationToken _ = default) where TEvent : notnull
public ValueTask<object[]> ReadStream(string streamId, CancellationToken _ = default)
{
var streamEvents = events.TryGetValue(streamId, out var stream)
? stream
: [];

var deserializedEvents = streamEvents
.Select(@event =>
Type.GetType(@event.EventType, true) is { } clrEventType
? JsonSerializer.Deserialize(@event.Data, clrEventType)
: null
)
var deserializedEvents = eventSerializer.Deserialize(streamEvents)
.Where(e => e != null)
.Cast<TEvent>().ToArray();
.Cast<object>()
.ToArray();

return ValueTask.FromResult(deserializedEvents);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using HotelManagement.EventStore;

namespace HotelManagement.GuestStayAccounts;

public class GuestStayAccountService(IEventStore eventStore)
{
private readonly CommandHandler<GuestStayAccount> commandHandler =
new(GuestStayAccount.Evolve, () => GuestStayAccount.Initial);

public Task CheckIn(CheckIn command, CancellationToken ct = default)
{
var guestStayAccountId = GuestStayAccount.GuestStayAccountId(
command.GuestStayId, command.RoomId, DateOnly.FromDateTime(command.Now.Date)
);

return commandHandler.Handle(eventStore, guestStayAccountId,
state => [GuestStayAccountDecider.CheckIn(command, state)], ct
);
}

public Task RecordCharge(RecordCharge command, CancellationToken ct = default) =>
commandHandler.Handle(eventStore, command.GuestStayAccountId,
state => [GuestStayAccountDecider.RecordCharge(command, state)], ct
);

public Task RecordPayment(RecordPayment command, CancellationToken ct = default) =>
commandHandler.Handle(eventStore, command.GuestStayAccountId,
state => [GuestStayAccountDecider.RecordPayment(command, state)], ct
);

public Task CheckOut(CheckOut command, CancellationToken ct = default) =>
commandHandler.Handle(eventStore, command.GuestStayAccountId,
state => [GuestStayAccountDecider.CheckOut(command, state)], ct
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ public static class GuestStayAccountDecider
{
public static GuestCheckedIn CheckIn(CheckIn command, GuestStayAccount state) =>
new GuestCheckedIn(
$"{command.GuestStayId}:{command.RoomId}:{command.Now.Date:yyyy-MM-dd}",
GuestStayAccount.GuestStayAccountId(
command.GuestStayId,
command.RoomId,
DateOnly.FromDateTime(command.Now.Date)
),
command.GuestStayId,
command.RoomId,
command.ClerkId,
Expand Down
Loading

0 comments on commit 9f0fe9a

Please sign in to comment.