-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
139: Practice Exercise: Building a Simple Event-Sourced Order System
Think about how a bank account actually works. If you look at your mobile app, you see a "Current Balance." That's a convenience, but it's not the source of truth. The real truth is the ledger—the long, immutable list of every single deposit and withdrawal you've ever made. If the bank lost your balance but kept the ledger, they could just start at zero and "replay" every transaction to figure out exactly how much money you have right now. That is exactly what event sourcing is.
In a standard C# CRUD application, we usually just overwrite the database row. We change OrderStatus from Pending to Shipped. But once you do that, the information that it was ever pending is gone. In event sourcing, we don't update state; we append events.
Mapping the Ledger to C#
To move this from a bank analogy into an Order system, we have to shift our thinking. Instead of a table of "Orders," we need a stream of events. Here is how that maps out:
- The Transaction becomes an Event (e.g.,
OrderPlaced,ItemAddedToOrder). - The Ledger becomes the Event Store (a list or database table that only allows appends).
- The Current Balance becomes the Projected State (the
Orderobject we build by replaying those events).
Wiring Up the Order Aggregate
I've found that the cleanest way to handle this in C# is to separate the event (the data) from the logic (how the state changes). You want your events to be simple POCOs (Plain Old CLR Objects) or records. The Order class then "consumes" these events to build its current state.
public record OrderCreated(Guid OrderId, DateTime CreatedAt);
public record ItemAdded(string ProductName, decimal Price);
public record OrderShipped(DateTime ShippedDate);
public class Order
{
public Guid Id { get; private set; }
public decimal TotalAmount { get; private set; }
public bool IsShipped { get; private set; }
public List<string> Items { get; private set; } = new();
// This is where the magic happens: the "Apply" method
public void Apply(object @event)
{
switch (@event)
{
case OrderCreated e:
Id = e.OrderId;
break;
case ItemAdded e:
Items.Add(e.ProductName);
TotalAmount += e.Price;
break;
case OrderShipped e:
IsShipped = true;
break;
}
}
}
Replaying the History
Now, when you want to "load" an order from your database, you don't fetch a single row. You fetch every event associated with that OrderId and loop through them. I like this approach because it gives you a perfect audit log for free. If a customer asks why their total is $150, you don't have to guess—you can literally show them the sequence of ItemAdded events.
// Imagine this comes from a database
var eventStream = new List<object>
{
new OrderCreated(Guid.NewGuid(), DateTime.UtcNow),
new ItemAdded("Mechanical Keyboard", 120.00m),
new ItemAdded("USB-C Cable", 30.00m),
new OrderShipped(DateTime.UtcNow)
};
var order = new Order();
foreach (var @event in eventStream)
{
order.Apply(@event);
}
Console.WriteLine($"Order {order.Id} total: {order.TotalAmount}. Shipped: {order.IsShipped}");
It feels like more work upfront, sure. You're writing more classes and doing more loops. But as your system grows, you'll realize that being able to "time travel" to see exactly what an order looked like last Tuesday is an absolute lifesaver during debugging.
📋 Practical Task
Implementing Order Cancellation Logic in an Event-Sourced System
Your task is to extend the event-sourced order system to handle order cancellations. However, there is a business rule: An order cannot be cancelled if it has already been shipped.
Please complete the following:
- Create a new record called
OrderCancelledcontaining aReasonstring. - Add a
bool IsCancelledproperty to theOrderclass. - Update the
Applymethod in theOrderclass to handle theOrderCancelledevent. - Create a method in the
Orderclass calledCanCancel()that returnstrueonly if the order has not been shipped and is not already cancelled. - Write a small test script that attempts to cancel an order after a
OrderShippedevent has been applied, and use theCanCancel()method to prevent theOrderCancelledevent from being added to the stream.
There are no comments for now.