Microservices in C#: Advanced Architecture and Practice
The transition from a monolith to a microservice architecture on the .NET platform is not just a technical decision, but a strategic step towards scalability and fault tolerance. However, at an advanced level, developers face non-trivial problems: distributed transactions, data consistency, and complex orchestration. In this article, we will analyze architectural patterns that will help build a reliable microservice ecosystem in C#, and look at practical code examples.
1. Saga Pattern: Managing Distributed Transactions
In microservices, traditional ACID transactions are impossible. The Saga pattern solves the data consistency problem by breaking a global operation into a sequence of local transactions with compensating actions.
Choreography vs. Orchestration:
- Choreography — each service publishes events and reacts to them. Suitable for simple scenarios.
- Orchestration — a central coordinator (Orchestrator) manages the steps. Recommended for complex business processes.
Example of an orchestrator implementation in C# using MassTransit:
public class OrderSaga : MassTransitStateMachine<OrderSagaState>{ public State Submitted { get; set; } public State PaymentPending { get; set; } public State Completed { get; set; }
public Event<OrderSubmitted> OrderSubmitted { get; set; } public Event<PaymentProcessed> PaymentProcessed { get; set; } public Event<PaymentFailed> PaymentFailed { get; set; }
public OrderSaga() { InstanceState(x => x.CurrentState);
Initially( When(OrderSubmitted) .Then(context => context.Saga.OrderId = context.Message.OrderId) .TransitionTo(Submitted) .Publish(context => new ProcessPayment { OrderId = context.Saga.OrderId, Amount = context.Message.Amount }) );
During(Submitted, When(PaymentProcessed) .TransitionTo(Completed) .Finalize(), When(PaymentFailed) .Then(context => Console.WriteLine("Compensation: order cancellation")) .TransitionTo(Final) ); }}This code demonstrates how the orchestrator manages the order lifecycle: upon successful payment, the saga completes; upon error, compensation is triggered.
2. CQRS and Event Sourcing: Separation of Responsibilities
The CQRS (Command Query Responsibility Segregation) pattern separates commands (write) and queries (read) into different models. Combined with Event Sourcing (ES), it provides full auditing and flexibility.
CQRS+ES Architecture:
- Commands — change state through events.
- Events — stored in an Event Store (e.g., EventStoreDB).
- Projections — build read models for fast queries.
Example of a command and handler:
public class CreateOrderCommand : ICommand{ public Guid OrderId { get; set; } public decimal Amount { get; set; }}
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand>{ private readonly IEventStore _eventStore;
public async Task Handle(CreateOrderCommand command) { var orderCreated = new OrderCreatedEvent { OrderId = command.OrderId, Amount = command.Amount, Timestamp = DateTime.UtcNow };
await _eventStore.AppendEventAsync( $"order-{command.OrderId}", orderCreated ); }