people walking in hallway

Implementing Event Sourcing in ASP.NET Core

Event sourcing is an architectural pattern where state changes are logged as a sequence of events. This approach differs from traditional CRUD operations, where the current state is stored directly in the database. Event sourcing provides several benefits, including a complete history of changes, easier debugging, and the ability to rebuild system state from events. In this article, we will explore how to implement event sourcing in an ASP.NET Core application.

What is Event Sourcing?

Event sourcing records all changes to an application’s state as a series of events. Each event represents a significant change and is stored in an append-only log. The current state is derived by replaying these events. This pattern contrasts with the traditional approach of storing the current state directly in a database.

Advantages of Event Sourcing

  1. Complete Audit Trail: Every change to the system is logged, providing a comprehensive history.
  2. Rebuild State: The system’s state can be rebuilt at any point in time by replaying the events.
  3. Debugging and Troubleshooting: Easier to identify issues by examining the sequence of events.
  4. Scalability: Event logs can be partitioned and scaled more efficiently than traditional databases.

Setting Up an ASP.NET Core Project

Let’s start by setting up an ASP.NET Core Web API project:

dotnet new webapi -n EventSourcingDemo
cd EventSourcingDemo

Implementing Event Sourcing

We will create a simple example where we manage accounts and their transactions using event sourcing.

1. Define Events

Events represent changes in the system. Let’s define our events.

Events/AccountEvents.cs:

namespace EventSourcingDemo.Events
{
    public abstract class AccountEvent
    {
        public Guid Id { get; set; }
        public DateTime OccurredOn { get; set; }
    }

    public class AccountCreated : AccountEvent
    {
        public string Owner { get; set; }
    }

    public class MoneyDeposited : AccountEvent
    {
        public decimal Amount { get; set; }
    }

    public class MoneyWithdrawn : AccountEvent
    {
        public decimal Amount { get; set; }
    }
}

2. Event Store

An event store is used to save and retrieve events. For simplicity, we’ll use an in-memory event store.

Services/InMemoryEventStore.cs:

using EventSourcingDemo.Events;
using System.Collections.Generic;
using System.Linq;

namespace EventSourcingDemo.Services
{
    public class InMemoryEventStore
    {
        private readonly List<AccountEvent> _events = new List<AccountEvent>();

        public void Save(AccountEvent accountEvent)
        {
            _events.Add(accountEvent);
        }

        public IEnumerable<AccountEvent> GetEvents(Guid accountId)
        {
            return _events.Where(e => e.Id == accountId);
        }
    }
}

3. Aggregate

An aggregate is the central point where events are applied to modify the state.

Models/Account.cs:

using EventSourcingDemo.Events;
using System;
using System.Collections.Generic;

namespace EventSourcingDemo.Models
{
    public class Account
    {
        public Guid Id { get; private set; }
        public string Owner { get; private set; }
        public decimal Balance { get; private set; }

        private List<AccountEvent> _changes = new List<AccountEvent>();

        public Account(IEnumerable<AccountEvent> events)
        {
            foreach (var @event in events)
            {
                Apply(@event);
            }
        }

        public static Account Create(Guid id, string owner)
        {
            var account = new Account(new List<AccountEvent>());
            account.Apply(new AccountCreated { Id = id, Owner = owner, OccurredOn = DateTime.UtcNow });
            return account;
        }

        public void Deposit(decimal amount)
        {
            Apply(new MoneyDeposited { Id = Id, Amount = amount, OccurredOn = DateTime.UtcNow });
        }

        public void Withdraw(decimal amount)
        {
            Apply(new MoneyWithdrawn { Id = Id, Amount = amount, OccurredOn = DateTime.UtcNow });
        }

        public IEnumerable<AccountEvent> GetUncommittedChanges() => _changes;

        private void Apply(AccountEvent @event)
        {
            switch (@event)
            {
                case AccountCreated e:
                    Id = e.Id;
                    Owner = e.Owner;
                    Balance = 0;
                    break;
                case MoneyDeposited e:
                    Balance += e.Amount;
                    break;
                case MoneyWithdrawn e:
                    Balance -= e.Amount;
                    break;
            }
            _changes.Add(@event);
        }
    }
}

4. Controller

Now let’s create a controller to handle HTTP requests and manage account operations.

Controllers/AccountController.cs:

using EventSourcingDemo.Events;
using EventSourcingDemo.Models;
using EventSourcingDemo.Services;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;

namespace EventSourcingDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class AccountController : ControllerBase
    {
        private readonly InMemoryEventStore _eventStore;

        public AccountController(InMemoryEventStore eventStore)
        {
            _eventStore = eventStore;
        }

        [HttpPost("create")]
        public IActionResult Create(string owner)
        {
            var id = Guid.NewGuid();
            var account = Account.Create(id, owner);

            foreach (var @event in account.GetUncommittedChanges())
            {
                _eventStore.Save(@event);
            }

            return Ok(account);
        }

        [HttpPost("{id}/deposit")]
        public IActionResult Deposit(Guid id, decimal amount)
        {
            var events = _eventStore.GetEvents(id);
            var account = new Account(events);
            account.Deposit(amount);

            foreach (var @event in account.GetUncommittedChanges())
            {
                _eventStore.Save(@event);
            }

            return Ok(account);
        }

        [HttpPost("{id}/withdraw")]
        public IActionResult Withdraw(Guid id, decimal amount)
        {
            var events = _eventStore.GetEvents(id);
            var account = new Account(events);
            account.Withdraw(amount);

            foreach (var @event in account.GetUncommittedChanges())
            {
                _eventStore.Save(@event);
            }

            return Ok(account);
        }

        [HttpGet("{id}")]
        public IActionResult Get(Guid id)
        {
            var events = _eventStore.GetEvents(id);
            if (!events.Any())
            {
                return NotFound();
            }

            var account = new Account(events);
            return Ok(account);
        }
    }
}

Conclusion

In this article, we explored the concept of event sourcing and demonstrated how to implement it in an ASP.NET Core application. We created a simple account management system where all state changes are recorded as events. By following these steps, you can leverage the power of event sourcing in your own applications, providing a robust and scalable solution for managing state changes.

Similar Posts