Microservicios y DDD en .NET 9
En los capítulos anteriores, exploramos los fundamentos de Domain-Driven Design (DDD), su enfoque estratégico, los bloques tácticos y la persistencia con .NET 9. Ahora, nos adentramos en la aplicación de DDD en arquitecturas de microservicios, un enfoque que alinea perfectamente con los contextos acotados de DDD. Como líder de KitsuneData Integral Solutions, he implementado microservicios para transformar procesos empresariales, y en este capítulo te guiaré para hacerlo con .NET 9. Cubriremos cómo alinear microservicios con contextos acotados, la comunicación mediante eventos, la implementación con ASP.NET Core 9, el despliegue con Docker, y un ejemplo práctico que extiende el sistema de e-commerce de los capítulos previos.
5.1 Alineando microservicios con contextos acotados
En DDD, los contextos acotados dividen el dominio en modelos independientes, cada uno con su propio lenguaje ubicuo. Los microservicios son una implementación natural de estos contextos, donde cada microservicio encapsula un contexto acotado y opera de forma autónoma.
Principios clave
- Un contexto por microservicio: Cada microservicio representa un contexto acotado (e.g., “Gestión de Pedidos” o “Facturación”).
- Autonomía: Cada microservicio tiene su propia base de datos y lógica de negocio.
- Lenguaje ubicuo: El código del microservicio refleja el vocabulario del contexto.
Ejemplo: En nuestro sistema de e-commerce, el contexto “Gestión de Pedidos” se implementa como un microservicio que maneja pedidos y líneas, mientras que “Facturación” es otro microservicio que genera facturas basadas en eventos.
5.2 Comunicación entre microservicios
La comunicación entre microservicios debe ser asíncrona y basada en eventos para mantener la autonomía. En DDD, los eventos de dominio (como PedidoConfirmadoEvent
) son ideales para esto.
Patrones comunes
- Publicación/Suscripción: Un microservicio publica eventos (e.g., mediante un message broker como RabbitMQ) y otros se suscriben.
- Eventos de integración: Extienden los eventos de dominio para incluir datos necesarios por otros contextos.
- Sagas: Coordinan flujos entre microservicios mediante una secuencia de eventos.
Ejemplo en C# (publicación de un evento):
public record PedidoConfirmadoEvent(Guid PedidoId, Guid ClienteId, decimal Total, DateTime FechaConfirmacion);
public interface IEventoPublisher
{
Task PublicarAsync<T>(T evento);
}
public class RabbitMqEventoPublisher : IEventoPublisher
{
private readonly IConnection _connection;
public RabbitMqEventoPublisher(IConnection connection)
{
_connection = connection;
}
public async Task PublicarAsync<T>(T evento)
{
using var channel = _connection.CreateModel();
var eventName = typeof(T).Name;
channel.QueueDeclare(eventName, durable: true, exclusive: false, autoDelete: false);
var body = JsonSerializer.SerializeToUtf8Bytes(evento);
channel.BasicPublish("", eventName, null, body);
await Task.CompletedTask;
}
}
5.3 Implementación de microservicios con ASP.NET Core 9
ASP.NET Core 9 es ideal para microservicios gracias a sus minimal APIs, soporte para gRPC y optimizaciones de rendimiento. En DDD, cada microservicio implementa un contexto acotado, con su propio modelo de dominio, repositorio y base de datos.
Configuración básica
- Usa minimal APIs para endpoints ligeros.
- Integra EF Core 9 para persistencia.
- Configura inyección de dependencias para servicios y repositorios.
Ejemplo de minimal API:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IPedidoRepository, PedidoRepository>();
builder.Services.AddScoped<PedidoService>();
builder.Services.AddSingleton<IEventoPublisher, RabbitMqEventoPublisher>();
var app = builder.Build();
app.MapPost("/pedidos/confirmar/{id}", async (Guid id, PedidoService service) =>
{
await service.ConfirmarPedidoAsync(id);
return Results.Ok();
});
app.Run();
5.4 Despliegue con Docker y .NET 9
Docker simplifica el despliegue de microservicios, asegurando consistencia entre entornos. En .NET 9, los contenedores son más eficientes gracias a mejoras en el runtime y soporte nativo para AOT (Ahead-of-Time compilation).
Configuración de Docker
- Crea un
Dockerfile
para cada microservicio. - Usa
docker-compose
para orquestar servicios y bases de datos.
Ejemplo de Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["PedidosService/PedidosService.csproj", "PedidosService/"]
RUN dotnet restore "PedidosService/PedidosService.csproj"
COPY . .
WORKDIR "/src/PedidosService"
RUN dotnet build "PedidosService.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "PedidosService.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "PedidosService.dll"]
Ejemplo de docker-compose.yml:
version: '3.8'
services:
pedidos-service:
build:
context: .
dockerfile: PedidosService/Dockerfile
ports:
- "5000:80"
depends_on:
- sqlserver
- rabbitmq
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=YourStrong@Passw0rd
ports:
- "1433:1433"
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
5.5 Ejemplo práctico
Extendamos el agregado Pedido
(Capítulos 3 y 4) a un microservicio de “Gestión de Pedidos” que interactúa con un microservicio de “Facturación” mediante eventos.
Escenario
- Contexto acotado: Gestión de Pedidos.
- Funcionalidad: Crear y confirmar pedidos, persistirlos con EF Core 9, y publicar eventos para Facturación.
- Interacción: El evento
PedidoConfirmadoEvent
desencadena la creación de una factura en el microservicio de Facturación.
Código del microservicio
// Modelo de dominio (como en Capítulo 4)
public class Pedido
{
public Guid Id { get; private set; }
public Guid ClienteId { get; private set; }
public List<LineaPedido> Lineas { get; private set; } = new();
public decimal Total { get; private set; }
public string Estado { get; private set; }
public Direccion DireccionEnvio { get; private set; }
public byte[] RowVersion { get; private set; }
private Pedido() { } // Para EF Core
public Pedido(Guid clienteId, Direccion direccionEnvio)
{
Id = Guid.NewGuid();
ClienteId = clienteId;
DireccionEnvio = direccionEnvio?.Validar() ?? throw new ArgumentNullException(nameof(direccionEnvio));
Estado = "Creado";
}
public void AgregarLinea(Guid productoId, int cantidad, decimal precio)
{
var linea = new LineaPedido(productoId, cantidad, precio);
Lineas.Add(linea);
Total += linea.Subtotal;
}
public void Confirmar(IDescuentoService descuentoService)
{
if (Lineas.Count == 0)
throw new InvalidOperationException("El pedido no tiene líneas.");
Total = descuentoService.CalcularDescuento(ClienteId, Total);
Estado = "Confirmado";
}
}
public record LineaPedido(Guid ProductoId, int Cantidad, decimal Precio)
{
public decimal Subtotal => Cantidad * Precio;
}
public record Direccion(string Calle, string Ciudad, string CodigoPostal)
{
public Direccion Validar()
{
if (string.IsNullOrEmpty(Calle) || string.IsNullOrEmpty(Ciudad) || string.IsNullOrEmpty(CodigoPostal))
throw new ArgumentException("Todos los campos de la dirección son requeridos.");
return this;
}
}
public record PedidoConfirmadoEvent(Guid PedidoId, Guid ClienteId, decimal Total, DateTime FechaConfirmacion);
// Repositorio y contexto (como en Capítulo 4)
public interface IPedidoRepository
{
Task<Pedido> ObtenerPorIdAsync(Guid id);
Task AgregarAsync(Pedido pedido);
Task ActualizarAsync(Pedido pedido);
}
public class PedidoRepository : IPedidoRepository
{
private readonly AppDbContext _context;
public PedidoRepository(AppDbContext context)
{
_context = context;
}
public async Task<Pedido> ObtenerPorIdAsync(Guid id)
{
return await _context.Pedidos
.Include(p => p.Lineas)
.Include(p => p.DireccionEnvio)
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task AgregarAsync(Pedido pedido)
{
await _context.Pedidos.AddAsync(pedido);
await _context.SaveChangesAsync();
}
public async Task ActualizarAsync(Pedido pedido)
{
_context.Pedidos.Update(pedido);
await _context.SaveChangesAsync();
}
}
public class AppDbContext : DbContext
{
public DbSet<Pedido> Pedidos { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Pedido>(entity =>
{
entity.HasKey(p => p.Id);
entity.Property(p => p.ClienteId).IsRequired();
entity.Property(p => p.Estado).IsRequired();
entity.Property(p => p.Total).HasPrecision(18, 2);
entity.Property(p => p.RowVersion).IsRowVersion();
entity.OwnsMany(p => p.Lineas, linea =>
{
linea.WithOwner().HasForeignKey("PedidoId");
linea.Property(l => l.ProductoId).IsRequired();
linea.Property(l => l.Cantidad).IsRequired();
linea.Property(l => l.Precio).HasPrecision(18, 2);
});
entity.OwnsOne(p => p.DireccionEnvio, dir =>
{
dir.Property(d => d.Calle).IsRequired();
dir.Property(d => d.Ciudad).IsRequired();
dir.Property(d => d.CodigoPostal).IsRequired();
});
});
}
}
// Servicio de dominio
public interface IDescuentoService
{
decimal CalcularDescuento(Guid clienteId, decimal total);
}
public class DescuentoService : IDescuentoService
{
public decimal CalcularDescuento(Guid clienteId, decimal total)
{
// Simulación: 10% de descuento para clientes VIP
bool esClienteVip = false; // Consultar en implementación real
return esClienteVip ? total * 0.9m : total;
}
}
// Servicio de aplicación
public class PedidoService
{
private readonly IPedidoRepository _repository;
private readonly IDescuentoService _descuentoService;
private readonly IEventoPublisher _publisher;
public PedidoService(IPedidoRepository repository, IDescuentoService descuentoService, IEventoPublisher publisher)
{
_repository = repository;
_descuentoService = descuentoService;
_publisher = publisher;
}
public async Task CrearPedidoAsync(Guid clienteId, Direccion direccionEnvio)
{
var pedido = new Pedido(clienteId, direccionEnvio);
await _repository.AgregarAsync(pedido);
}
public async Task ConfirmarPedidoAsync(Guid pedidoId)
{
var pedido = await _repository.ObtenerPorIdAsync(pedidoId);
if (pedido == null)
throw new InvalidOperationException("Pedido no encontrado.");
pedido.Confirmar(_descuentoService);
await _repository.ActualizarAsync(pedido);
await _publisher.PublicarAsync(new PedidoConfirmadoEvent(pedido.Id, pedido.ClienteId, pedido.Total, DateTime.UtcNow));
}
}
// Programa principal (minimal API)
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IPedidoRepository, PedidoRepository>();
builder.Services.AddScoped<IDescuentoService, DescuentoService>();
builder.Services.AddSingleton<IEventoPublisher, RabbitMqEventoPublisher>();
builder.Services.AddScoped<PedidoService>();
var app = builder.Build();
app.MapPost("/pedidos", async (CrearPedidoRequest request, PedidoService service) =>
{
var direccion = new Direccion(request.Calle, request.Ciudad, request.CodigoPostal);
await service.CrearPedidoAsync(request.ClienteId, direccion);
return Results.Created();
});
app.MapPost("/pedidos/confirmar/{id}", async (Guid id, PedidoService service) =>
{
await service.ConfirmarPedidoAsync(id);
return Results.Ok();
});
app.Run();
}
}
public record CrearPedidoRequest(Guid ClienteId, string Calle, string Ciudad, string CodigoPostal);
Explicación
- Microservicio: Implementa el contexto “Gestión de Pedidos” con un modelo de dominio (
Pedido
,LineaPedido
,Direccion
), repositorio (PedidoRepository
), y servicio (PedidoService
). - Persistencia: Usa EF Core 9 con SQL Server, configurado en
AppDbContext
. - Eventos: Publica
PedidoConfirmadoEvent
usando RabbitMQ para notificar al microservicio de Facturación. - API: Expone endpoints con minimal APIs para crear y confirmar pedidos.
- Docker: El
Dockerfile
ydocker-compose.yml
permiten desplegar el microservicio con su base de datos y message broker.
En KitsuneData, hemos usado este enfoque para implementar sistemas escalables que alinean microservicios con dominios empresariales. En el Capítulo 6, exploraremos patrones avanzados como CQRS y Event Sourcing. Este capítulo ha mostrado cómo combinar DDD con microservicios en .NET 9, desde el diseño hasta el despliegue. En el próximo capítulo, profundizaremos en patrones avanzados para sistemas distribuidos.