Examples and Tutorials of Event Sourcing in .NET
Event Sourcing is perceived as a complex pattern. Some believe that it's like Nessie, everyone's heard about it, but rarely seen it. In fact, Event Sourcing is a pretty practical and straightforward concept. It helps build predictable applications closer to business. Nowadays, storage is cheap, and information is priceless. In Event Sourcing, no data is lost.
The workshop aims to build the knowledge of the general concept and its related patterns for the participants. The acquired knowledge will allow for the conscious design of architectural solutions and the analysis of associated risks.
The emphasis will be on a pragmatic understanding of architectures and applying it in practice using Marten and EventStoreDB.
You can do the workshop as a self-paced kit. That should give you a good foundation for starting your journey with Event Sourcing and learning tools like Marten and EventStoreDB. If you'd like to get full coverage with all nuances of the private workshop, feel free to contact me via email.
Fixed Kafka integration to correctly publish and get messages:
landoop/kafka-topics-ui
to landoop/kafka-ui
.See details in https://github.com/oskardudycz/EventSourcing.NetCore/pull/120.
Besides that:
See details in https://github.com/oskardudycz/EventSourcing.NetCore/pull/122.
aggregateId
part of streamId
to properly handle EventStoreDB category projections: https://github.com/oskardudycz/EventSourcing.NetCore/pull/117
Implemented correlation id and causation id for tracing operations fully.
Refactored previously existing CorrelationIdMiddleware
by adding causation id handling and renamed it to more general TracingMiddleware. It takes correlation id and causation id from HTTP headers and caches them into TracingScope.
TracingScope is also setting logging scope internally to keep Correlation id and causation id in logs. Thanks to that, the logic for tracing setup can be reused in middleware and subscriptions. The new causation id is generated based on the event id in subscriptions. Thanks to that, we can build the tree with a history of event handling.
Updated Optimistic Concurrency Scopes and generalised into AppendScope. It wraps both tracing and expected version handling. Pushed tracing metadata into Marten and EventStoreDB storage.
Replaced custom EventBus instead of Mediator one to have more flexibility (e.g. be able to create new DI and logging scopes).
See more in PRs: #106 and #108.
Added Marten Outbox Pattern/Subscription with custom Projection Plugged it into samples removing calling EventBus from the repository, by that getting at-least-once processing guarantee.
Read more about that in: https://event-driven.io/en/integrating_Marten/.
Other changes:
See more in PR: https://github.com/oskardudycz/EventSourcing.NetCore/pull/104
Removed not working correctly Github Action with Test Results.
Besides that:
Added optimistic concurrency to samples and did a huge all-around refactoring.
The most significant changes:
See more in: https://github.com/oskardudycz/EventSourcing.NetCore/pull/100.
Upgraded packages to the latest version
Upgraded also ESDB docker images to the latest LTS version.
Strongly typed ids (or, in general, a proper type system) can make your code more predictable. It reduces the chance of trivial mistakes, like accidentally changing parameters order of the same primitive type.
So for such code:
var reservationId = "RES/01";
var seatId = "SEAT/22";
var customerId = "CUS/291";
var reservation = new ReservationId (
reservationId,
seatId,
customerId
);
the compiler won't catch if you switch reservationId
with seatId
.
If you use strongly typed ids, then compile will catch that issue:
var reservationId = new ReservationId("RES/01");
var seatId = new SeatId("SEAT/22");
var customerId = new CustomerId("CUS/291");
var reservation = new ReservationId (
reservationId,
seatId,
customerId
);
They're not ideal, as they're usually not playing well with the storage engines. Typical issues are: serialisation, Linq queries, etc. For some cases they may be just overkill. You need to pick your poison.
To reduce tedious, copy/paste code, it's worth defining a strongly-typed id base class, like:
public class StronglyTypedValue<T>: IEquatable<StronglyTypedValue<T>> where T: IComparable<T>
{
public T Value { get; }
public StronglyTypedValue(T value)
{
Value = value;
}
public bool Equals(StronglyTypedValue<T>? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return EqualityComparer<T>.Default.Equals(Value, other.Value);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((StronglyTypedValue<T>)obj);
}
public override int GetHashCode()
{
return EqualityComparer<T>.Default.GetHashCode(Value);
}
public static bool operator ==(StronglyTypedValue<T>? left, StronglyTypedValue<T>? right)
{
return Equals(left, right);
}
public static bool operator !=(StronglyTypedValue<T>? left, StronglyTypedValue<T>? right)
{
return !Equals(left, right);
}
}
Then you can define specific id class as:
public class ReservationId: StronglyTypedValue<Guid>
{
public ReservationId(Guid value) : base(value)
{
}
}
You can even add additional rules:
public class ReservationNumber: StronglyTypedValue<string>
{
public ReservationNumber(string value) : base(value)
{
if (string.IsNullOrEmpty(value) || value.StartsWith("RES/") || value.Length <= 4)
throw new ArgumentOutOfRangeException(nameof(value));
}
}
The base class working with Marten, can be defined as:
public abstract class Aggregate<TKey, T>
where TKey: StronglyTypedValue<T>
where T : IComparable<T>
{
public TKey Id { get; set; } = default!;
[Identity]
public T AggregateId {
get => Id.Value;
set {}
}
public int Version { get; protected set; }
[JsonIgnore] private readonly Queue<object> uncommittedEvents = new();
public object[] DequeueUncommittedEvents()
{
var dequeuedEvents = uncommittedEvents.ToArray();
uncommittedEvents.Clear();
return dequeuedEvents;
}
protected void Enqueue(object @event)
{
uncommittedEvents.Enqueue(@event);
}
}
Marten requires the id with public setter and getter of string
or Guid
. We used the trick and added AggregateId
with a strongly-typed backing field. We also informed Marten of the Identity attribute to use this field in its internals.
Example aggregate can look like:
public class Reservation : Aggregate<ReservationId, Guid>
{
public CustomerId CustomerId { get; private set; } = default!;
public SeatId SeatId { get; private set; } = default!;
public ReservationNumber Number { get; private set; } = default!;
public ReservationStatus Status { get; private set; }
public static Reservation CreateTentative(
SeatId seatId,
CustomerId customerId)
{
return new Reservation(
new ReservationId(Guid.NewGuid()),
seatId,
customerId,
new ReservationNumber(Guid.NewGuid().ToString())
);
}
// (...)
}
See the full sample here.
Read more in the article:
Shows how to handle basic event schema versioning scenarios using event and stream transformations (e.g. upcasting):
See the whole sample at: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventsVersioning And details of changes in: https://github.com/oskardudycz/EventSourcing.NetCore/pull/75