A .NET Core event sourcing framework.
Easy to be integrated in ASP.NET Core web application, Lambda function or Azure function.
Support various of event store:
- in file system as plain text file (see File system setup instructions)
- in AWS DynamoDB (see DynamoDB setup instructions)
- in Azure CosmosDB (see CosmosDB setup instructions)
- in any relational database supported by EF Core, e.g., Microsoft SQL Server,MySQL, etc. (see EF Core setup instructions)
- JKang.EventSourcing
- JKang.EventSourcing.Persistence.FileSystem
- JKang.EventSourcing.Persistence.EfCore
- JKang.EventSourcing.Persistence.DynamoDB
- JKang.EventSourcing.Persistence.CosmosDB
- JKang.EventSourcing.Persistence.S3
- JKang.EventSourcing.Persistence.Caching
Let's implement a simple gift card management system with the following use cases:
- Create gift cards with initial credit
- Debit the gift card specifying amount while overpaying is not allowed
I'm adopting DDD (Domain Driven Design) approach and implement the GiftCard entity as an Rich Domain Aggregate which encapsulates/protects its internal data/state, and contains itself business logics ensuring data integrity.
public sealed class GiftCardCreated : AggregateCreatedEvent<Guid>
{
public GiftCardCreated(Guid aggregateId, DateTime timestamp, decimal initialCredit)
: base(aggregateId, timestamp)
{
InitialCredit = initialCredit;
}
public decimal InitialCredit { get; }
}
public class GiftCardDebited : AggregateEvent<Guid>
{
public GiftCardDebited(Guid aggregateId, int aggregateVersion, DateTime timestamp, decimal amount)
: base(aggregateId, aggregateVersion, timestamp)
{
Amount = amount;
}
public decimal Amount { get; }
}
Notes:
- It's recommended to implement aggregate event in an immutable way.
- Inheriting from
AggregateEvent<TKey>
orAggregateCreatedEvent<TKey>
is not mandatory, but an aggreagte event must at least implementIAggregateEvent<TKey>
interface. - In order to use built-in event stores, please make sure event can be properly serialized using Json.NET.
public class GiftCard : Aggregate<Guid>
{
/// <summary>
/// Constructor for creating an new gift card from scratch
/// </summary>
public GiftCard(decimal initialCredit)
: base(new GiftCardCreated(Guid.NewGuid(), DateTime.UtcNow, initialCredit))
{ }
/// <summary>
/// Constructor for rehydrating gift card from historical events
/// </summary>
public GiftCard(Guid id, IEnumerable<IAggregateEvent<Guid>> savedEvents)
: base(id, savedEvents)
{ }
/// <summary>
/// Constructor for rehydrating gift card from a snapshot + historical events after the snapshot
/// </summary>
public GiftCard(Guid id, IAggregateSnapshot<Guid> snapshot, IEnumerable<IAggregateEvent<Guid>> savedEvents)
: base(id, snapshot, savedEvents)
{ }
public decimal Balance { get; private set; }
public void Debit(decimal amout)
=> ReceiveEvent(new GiftCardDebited(Id, GetNextVersion(), DateTime.UtcNow, amout));
protected override void ApplyEvent(IAggregateEvent<Guid> @event)
{
if (@event is GiftCardCreated created)
{
Balance = created.InitialCredit;
}
else if (@event is GiftCardDebited debited)
{
if (debited.Amount < 0)
{
throw new InvalidOperationException("Negative debit amout is not allowed.");
}
if (Balance < debited.Amount)
{
throw new InvalidOperationException("Not enough credit");
}
Balance -= debited.Amount;
}
}
}
Notes:
- Please ensure that state of domain aggregate can only be changed by applying aggregate events.
- Inheriting from
Aggregate<TKey>
is not mandatory, but the minimum requirements for implementing a domain aggregate are:- Implement
IAggregate<TKey>
interface - Have a public constructor with signature
MyAggregate(TKey id, IEnumerable<IAggregateEvent<TKey>> savedEvents)
- Have a public constructor with signature
MyAggregate(TKey id, IAggregateSnapshot<TKey> snapshot, IEnumerable<IAggregateEvent<TKey>> savedEvents)
- Implement
By definition of Event Sourcing, persisting an aggregate insists on persisting all historical events.
public interface IGiftCardRepository
{
Task SaveGiftCardAsync(GiftCard giftCard);
Task<GiftCard> FindGiftCardAsync(Guid id);
}
public class GiftCardRepository : AggregateRepository<GiftCard, Guid>,
IGiftCardRepository
{
public GiftCardRepository(IEventStore<GiftCard, Guid> eventStore)
: base(eventStore)
{ }
public Task SaveGiftCardAsync(GiftCard giftCard) =>
SaveAggregateAsync(giftCard);
public Task<GiftCard> FindGiftCardAsync(Guid id) =>
FindAggregateAsync(id);
}
Step 4 - Register your repository interface and configure event store in dependency injection framework
services
.AddScoped<IGiftCardRepository, GiftCardRepository>();
services
.AddEventSourcing(builder =>
{
builder.UseTextFileEventStore<GiftCard, Guid>(x =>
x.Folder = "C:/Temp/GiftcardEvents");
});
Notes:
// create a new gift card with initial credit 100
var giftCard = new GiftCard(100);
// persist the gift card
await _repository.SaveGiftCardAsync(giftCard);
// rehydrate the giftcard
giftCard = await _repository.FindGiftCardAsync(giftCard.Id);
// payments
giftCard.Debit(40); // ==> balance: 60
giftCard.Debit(50); // ==> balance: 10
giftCard.Debit(20); // ==> invalid operation exception
See this page.
See this page.
Consider install the nuget package JKang.EventSourcing.Persistence.Caching
and inherit the CachedAggregateRepository
class.
It leverages Microsoft.Extensions.Caching.Distributed.IDistributedCache
to cache aggregate every time after loaded from or saved into repository.
Consider configuring a short sliding expiration (e.g., 5 sec) to reduce the chance of having cache out of date.
Please feel free to download, fork and/or provide any feedback!