https://github.com/jacqueskang/eventsourcing
.NET Core event sourcing framework
https://github.com/jacqueskang/eventsourcing
cosmosdb cqrs dynamodb event-sourcing
Last synced: about 1 year ago
JSON representation
.NET Core event sourcing framework
- Host: GitHub
- URL: https://github.com/jacqueskang/eventsourcing
- Owner: jacqueskang
- License: mit
- Created: 2018-11-12T08:20:07.000Z (over 7 years ago)
- Default Branch: develop
- Last Pushed: 2022-12-08T10:01:43.000Z (over 3 years ago)
- Last Synced: 2024-05-01T12:18:14.084Z (about 2 years ago)
- Topics: cosmosdb, cqrs, dynamodb, event-sourcing
- Language: C#
- Size: 1.24 MB
- Stars: 177
- Watchers: 9
- Forks: 28
- Open Issues: 14
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
[](https://dev.azure.com/jacques-kang/EventSourcing/_build/latest?definitionId=11&branchName=develop)
# EventSourcing
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](doc/FileSystemSetup.md))
- in AWS DynamoDB (see [DynamoDB setup instructions](doc/DynamoDBSetup.md))
- in Azure CosmosDB (see [CosmosDB setup instructions](doc/CosmosDBSetup.md))
- in any relational database supported by EF Core, e.g., Microsoft SQL Server,MySQL, etc. (see [EF Core setup instructions](doc/EfCoreSetup.md))
## NuGet packages
- JKang.EventSourcing [](https://badge.fury.io/nu/JKang.EventSourcing)
- JKang.EventSourcing.Persistence.FileSystem [](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.FileSystem)
- JKang.EventSourcing.Persistence.EfCore [](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.EfCore)
- JKang.EventSourcing.Persistence.DynamoDB [](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.DynamoDB)
- JKang.EventSourcing.Persistence.CosmosDB [](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.CosmosDB)
- JKang.EventSourcing.Persistence.S3 [](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.S3)
- JKang.EventSourcing.Persistence.Caching [](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.Caching)
## Quick Start:
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.
### Step 1 - Create aggregate events
```csharp
public sealed class GiftCardCreated : AggregateCreatedEvent
{
public GiftCardCreated(Guid aggregateId, DateTime timestamp, decimal initialCredit)
: base(aggregateId, timestamp)
{
InitialCredit = initialCredit;
}
public decimal InitialCredit { get; }
}
```
```csharp
public class GiftCardDebited : AggregateEvent
{
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` or `AggregateCreatedEvent` is not mandatory, but an aggreagte event must at least implement `IAggregateEvent` interface.
- In order to use built-in event stores, please make sure event can be properly serialized using [Json.NET](https://www.newtonsoft.com/json).
### Step 2 - Create domain aggregate
```csharp
public class GiftCard : Aggregate
{
///
/// Constructor for creating an new gift card from scratch
///
public GiftCard(decimal initialCredit)
: base(new GiftCardCreated(Guid.NewGuid(), DateTime.UtcNow, initialCredit))
{ }
///
/// Constructor for rehydrating gift card from historical events
///
public GiftCard(Guid id, IEnumerable> savedEvents)
: base(id, savedEvents)
{ }
///
/// Constructor for rehydrating gift card from a snapshot + historical events after the snapshot
///
public GiftCard(Guid id, IAggregateSnapshot snapshot, IEnumerable> 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 @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` is not mandatory, but the minimum requirements for implementing a domain aggregate are:
- Implement `IAggregate` interface
- Have a public constructor with signature `MyAggregate(TKey id, IEnumerable> savedEvents)`
- Have a public constructor with signature `MyAggregate(TKey id, IAggregateSnapshot snapshot, IEnumerable> savedEvents)`
### Step 3 - Implement repository
By definition of Event Sourcing, persisting an aggregate insists on persisting all historical events.
```csharp
public interface IGiftCardRepository
{
Task SaveGiftCardAsync(GiftCard giftCard);
Task FindGiftCardAsync(Guid id);
}
```
```csharp
public class GiftCardRepository : AggregateRepository,
IGiftCardRepository
{
public GiftCardRepository(IEventStore eventStore)
: base(eventStore)
{ }
public Task SaveGiftCardAsync(GiftCard giftCard) =>
SaveAggregateAsync(giftCard);
public Task FindGiftCardAsync(Guid id) =>
FindAggregateAsync(id);
}
```
### Step 4 - Register your repository interface and configure event store in dependency injection framework
```csharp
services
.AddScoped();
services
.AddEventSourcing(builder =>
{
builder.UseTextFileEventStore(x =>
x.Folder = "C:/Temp/GiftcardEvents");
});
```
Notes:
- You can choose other persistence store provided such as [CosmosDB](doc/CosmosDBSetup.md) or [DynamoDB](doc/DynamoDBSetup) etc.
### Step 5 - implmement use cases
```csharp
// 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
```
## FAQs
### How to programmatically initialize event store?
See [this page](doc/StoreInitialization.md).
### How to use snapshots to optimize performance?
See [this page](doc/Snapshots.md).
### How to improve performance using caching?
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!__