Implementing CQRS and Event Sourcing in .NET Core

Tapesh Mehta Tapesh Mehta | Published on: Jul 09, 2024 | Est. reading time: 6 minutes
Implementing CQRS and Event Sourcing in .NET Core

In software design, Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) are 2 patterns which could significantly increase the scalability, maintainability & functionality of your programs. While CQRS allows you to decouple your read and write operations, Event Sourcing stores all changes to the application state as a sequence of events. Together, these patterns can help you develop robust and scalable applications. This write-up will discuss the concepts of CQRS and Event Sourcing and show their implementation for a .NET Core program. For those looking to enhance their projects further, leveraging professional ASP.NET development services can provide additional expertise and support.

Understanding CQRS

CQRS Architectural Pattern

Command Query Responsibility Segregation (CQRS) is a pattern that separates the read and write operations of an application into distinct models. The primary objective is to optimize and scale these operations independently.

  1. Command Model (Write Model):
    • Handles operations that modify the application state.
    • Encompasses complex business logic and validations.
  2. Query Model (Read Model):
    • Manages operations that retrieve data.
    • Optimized for performance and simplicity, often bypassing complex business logic.

Benefits of CQRS

  • Scalability: Read and write operations can be scaled independently based on their specific requirements.
  • Performance: Read models can be optimized for fast query performance, while write models focus on maintaining data consistency.
  • Maintainability: Separation of concerns makes the codebase easier to manage and understand.

Implementing CQRS in .NET Core

Setting Up the Project

Create a new .NET Core project using the following command:

dotnet new webapi -n CQRSExample
cd CQRSExample

Defining the Domain Models

For this example, let’s consider a simple e-commerce domain with Order and Product entities.

public class Order
{
    public Guid Id { get; set; }
    public DateTime OrderDate { get; set; }
    public List<OrderItem> Items { get; set; }
    public decimal TotalAmount => Items.Sum(i => i.Quantity * i.UnitPrice);
}

public class OrderItem
{
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

Implementing the Command Model

Commands represent actions that modify the state of the system. Define a command for creating an order:

public class CreateOrderCommand
{
    public DateTime OrderDate { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class OrderItemDto
{
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

Handling Commands

Create a command handler to process the CreateOrderCommand:

public class CreateOrderCommandHandler
{
    private readonly ApplicationDbContext _context;

    public CreateOrderCommandHandler(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task Handle(CreateOrderCommand command)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            OrderDate = command.OrderDate,
            Items = command.Items.Select(i => new OrderItem
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                UnitPrice = i.UnitPrice
            }).ToList()
        };

        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    }
}

Implementing the Query Model

Queries are responsible for retrieving data. Define a query to get order details:

public class GetOrderQuery
{
    public Guid OrderId { get; set; }
}

public class OrderDto
{
    public Guid Id { get; set; }
    public DateTime OrderDate { get; set; }
    public List<OrderItemDto> Items { get; set; }
    public decimal TotalAmount { get; set; }
}

Handling Queries

Create a query handler to process the GetOrderQuery:

public class GetOrderQueryHandler
{
    private readonly ApplicationDbContext _context;

    public GetOrderQueryHandler(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<OrderDto> Handle(GetOrderQuery query)
    {
        var order = await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == query.OrderId);

        if (order == null)
        {
            return null;
        }

        return new OrderDto
        {
            Id = order.Id,
            OrderDate = order.OrderDate,
            Items = order.Items.Select(i => new OrderItemDto
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                UnitPrice = i.UnitPrice
            }).ToList(),
            TotalAmount = order.TotalAmount
        };
    }
}

Understanding Event Sourcing

Principles of Event Sourcing

Event Sourcing (ES) is a pattern where state changes are represented as a sequence of events. Instead of storing the current state, the system stores all events that have led to the current state.

Benefits of Event Sourcing

  • Auditability: Complete history of changes is maintained, providing a clear audit trail.
  • Consistency: Ensures that the application state is derived from a series of immutable events.
  • Scalability: Enables rebuilding of the application state from events, which can be distributed and processed independently.

Implementing Event Sourcing in .NET Core

Defining Events

Events represent state changes in the application. Define events for order creation and item addition:

public class OrderCreatedEvent
{
    public Guid OrderId { get; set; }
    public DateTime OrderDate { get; set; }
}

public class OrderItemAddedEvent
{
    public Guid OrderId { get; set; }
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

Storing Events

Create an event store to persist events:

public class EventStore
{
    private readonly List<object> _events = new List<object>();

    public void SaveEvent(object @event)
    {
        _events.Add(@event);
    }

    public IEnumerable<object> GetEvents()
    {
        return _events;
    }
}

Handling Events

Create event handlers to apply events to the domain model:

public class OrderEventHandler
{
    private readonly Dictionary<Guid, Order> _orders = new Dictionary<Guid, Order>();

    public void Handle(OrderCreatedEvent @event)
    {
        var order = new Order
        {
            Id = @event.OrderId,
            OrderDate = @event.OrderDate,
            Items = new List<OrderItem>()
        };

        _orders[@event.OrderId] = order;
    }

    public void Handle(OrderItemAddedEvent @event)
    {
        if (_orders.TryGetValue(@event.OrderId, out var order))
        {
            var item = new OrderItem
            {
                ProductId = @event.ProductId,
                Quantity = @event.Quantity,
                UnitPrice = @event.UnitPrice
            };

            order.Items.Add(item);
        }
    }

    public Order GetOrder(Guid orderId)
    {
        return _orders.TryGetValue(orderId, out var order) ? order : null;
    }
}

Integrating CQRS and Event Sourcing

Modify the command handler to emit events instead of directly modifying the state:

public class CreateOrderCommandHandler
{
    private readonly EventStore _eventStore;

    public CreateOrderCommandHandler(EventStore eventStore)
    {
        _eventStore = eventStore;
    }

    public async Task Handle(CreateOrderCommand command)
    {
        var orderCreatedEvent = new OrderCreatedEvent
        {
            OrderId = Guid.NewGuid(),
            OrderDate = command.OrderDate
        };

        _eventStore.SaveEvent(orderCreatedEvent);

        foreach (var item in command.Items)
        {
            var orderItemAddedEvent = new OrderItemAddedEvent
            {
                OrderId = orderCreatedEvent.OrderId,
                ProductId = item.ProductId,
                Quantity = item.Quantity,
                UnitPrice = item.UnitPrice
            };

            _eventStore.SaveEvent(orderItemAddedEvent);
        }
    }
}

Rebuilding State from Events

Implement a mechanism to rebuild the application state from stored events:

public class OrderService
{
    private readonly EventStore _eventStore;
    private readonly OrderEventHandler _eventHandler;

    public OrderService(EventStore eventStore, OrderEventHandler eventHandler)
    {
        _eventStore = eventStore;
        _eventHandler = eventHandler;
    }

    public void RebuildState()
    {
        foreach (var @event in _eventStore.GetEvents())
        {
            switch (@event)
            {
                case OrderCreatedEvent e:
                    _eventHandler.Handle(e);
                    break;
                case OrderItemAddedEvent e:
                    _eventHandler.Handle(e);
                    break;
            }
        }
    }

    public Order GetOrder(Guid orderId)
    {
        return _eventHandler.GetOrder(orderId);
    }
}

For those interested in learning more about .NET development, check out our .NET Development blogs. Stay updated with the latest insights and best practices!

Conclusion

CQRS and Event Sourcing in .NET Core could enhance the scalability, maintainability and performance of your programs. Decoupling the read & write operations and saving state changes as a sequence of events offers a far more powerful and flexible architecture. The setup and learning curve for utilizing these patterns is steep in the beginning but the long term advantages are worth the effort. With all the supplied code samples and explanations you can begin implementing CQRS and Event Sourcing in your .NET Core applications and experience the advantages firsthand.

Share

clutch profile designrush wirefuture profile goodfirms wirefuture profile
Software Solutions, Strategically Engineered! 📈

Success in the digital age requires strategy, and that's WireFuture's forte. We engineer software solutions that align with your business goals, driving growth and innovation.

Hire Now

Categories
.NET Development Angular Development JavaScript Development KnockoutJS Development NodeJS Development PHP Development Python Development React Development Software Development SQL Server Development VueJS Development All
About Author
wirefuture - founder

Tapesh Mehta

verified Verified
Expert in Software Development

Tapesh Mehta is a seasoned tech worker who has been making apps for the web, mobile devices, and desktop for over 13+ years. Tapesh knows a lot of different computer languages and frameworks. For robust web solutions, he is an expert in Asp.Net, PHP, and Python. He is also very good at making hybrid mobile apps, which use Ionic, Xamarin, and Flutter to make cross-platform user experiences that work well together. In addition, Tapesh has a lot of experience making complex desktop apps with WPF, which shows how flexible and creative he is when it comes to making software. His work is marked by a constant desire to learn and change.

Get in Touch
Your Ideas, Our Strategy – Let's Connect.

No commitment required. Whether you’re a charity, business, start-up or you just have an idea – we’re happy to talk through your project.

Embrace a worry-free experience as we proactively update, secure, and optimize your software, enabling you to focus on what matters most – driving innovation and achieving your business goals.

Hire Your A-Team Here to Unlock Potential & Drive Results
You can send an email to contact@wirefuture.com
clutch wirefuture profile designrush wirefuture profile goodfirms wirefuture profile good firms award-4 award-5 award-6