This repository demonstrates how to implement Event Sourcing in .NET using custom events and streams. Event Sourcing is an advanced architectural pattern that stores changes to an applicationβs state as a sequence of events, making it easier to track, audit, and replay operations.
In this project, the example of a BankAccount is used to model:
- Money transfers.
- Tracking all operations on account balances.
- Managing the account's active state.
The implementation is designed within a console application to keep it simple and focused on learning.
- Event Sourcing: Every change to the BankAccount state is stored as an immutable event.
- Custom Events: Define and handle domain-specific events like
MoneyDeposited
,MoneyWithdrawn
, andAccountActivated
. - Stream Usage: Leverage streams for reading and replaying events.
- Event Replay: Rebuild the state of the BankAccount from a sequence of events.
The repository uses a BankAccount domain to demonstrate how events can track deposits, withdrawals, and other operations.
π¦ EventSourcing
β£ π BankAccount # Entry point showcasing event sourcing in action
β£ π Program # Unit tests for the event sourcing implementation
Ensure you have the following installed:
- .NET Core SDK
- A modern C# IDE (e.g., Visual Studio or JetBrains Rider)
git clone https://github.com/MrEshboboyev/event-sourcing.git
cd EventSourcing
dotnet run --project EventSourcing
Navigate through the BankAccount
and Program
files to see how custom events and stream handling are implemented.
// Base event type
public abstract record Event(Guid StreamId)
{
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}
// Specific events for bank account domain
public record AccountOpened(
Guid AccountId,
string AccountHolder,
decimal InitialDeposit,
string Currency = "USD") : Event(AccountId);
public record MoneyDeposited(
Guid AccountId,
decimal Amount,
string Description) : Event(AccountId);
// BankAccount class with event handling
public class BankAccount
{
public Guid Id { get; private set; }
public string AccountHolder { get; private set; }
public decimal Balance { get; private set; }
public string Currency { get; private set; }
public bool IsActive { get; private set; }
public List<Event> Events = [];
private BankAccount()
{
}
// Open a new bank account
public static BankAccount Open(
string accountHolder,
decimal initialDeposit,
string currency = "USD")
{
if (string.IsNullOrWhiteSpace(accountHolder))
{
throw new ArgumentException("Account holder name is required");
}
if (initialDeposit < 0)
{
throw new ArgumentException("The initial deposit can't be negative");
}
var bankAccount = new BankAccount();
var @event = new AccountOpened(Guid.NewGuid(), accountHolder, initialDeposit, currency);
bankAccount.Apply(@event);
return bankAccount;
}
private void Apply(Event @event)
{
// Apply the event to update the account state
switch (@event)
{
case AccountOpened e:
Id = e.AccountId;
AccountHolder = e.AccountHolder;
Balance = e.InitialDeposit;
Currency = e.Currency;
IsActive = true;
break;
case MoneyDeposited e:
Balance += e.Amount;
break;
case MoneyWithdrawn e:
Balance -= e.Amount;
break;
case MoneyTransferred e:
Balance -= e.Amount;
break;
case AccountClosed e:
IsActive = false;
break;
}
Events.Add(@event);
}
public static BankAccount ReplayEvents(IEnumerable<Event> events)
{
var bankAccount = new BankAccount();
foreach (var @event in events)
{
bankAccount.Apply(@event);
}
return bankAccount;
}
private void EnsureAccountIsActive()
{
if (!IsActive)
{
throw new InvalidOperationException("Account is closed");
}
}
}
public class EventStream
{
private readonly List<BankAccountEvent> _eventLog = new();
public void AppendEvent(BankAccountEvent @event)
{
_eventLog.Add(@event);
}
public IEnumerable<BankAccountEvent> GetEvents()
{
return _eventLog.AsReadOnly();
}
}
- Record deposits and withdrawals as events.
- Rebuild account balance by replaying all events.
- Track changes like account activation or deactivation.
- Replay events to determine the current state of the account.
The repository includes unit tests for validating the core functionalities of event sourcing.
- Auditability: Every state change is recorded, enabling complete historical tracking.
- Flexibility: Rebuild the application state at any point by replaying events.
- Scalable Design: Ideal for domains requiring event-driven and asynchronous processing.
This project was developed by MrEshboboyev, a software developer passionate about event-driven architectures, clean code, and scalable solutions.
This project is licensed under the MIT License. Feel free to use and adapt the code for your own projects.
C#, .NET, Event Sourcing, Streams, Bank Account, Event-Driven Architecture, Console Application, CQRS, Software Architecture, Clean Code, Financial Operations
Feel free to suggest additional features or ask questions! π