Microservicios con .NET: arquitectura, patrones y buenas prácticas
Guía completa para diseñar, desarrollar y desplegar arquitecturas de microservicios robustas con .NET y ASP.NET Core.
InSkillCoach
Microservicios con .NET: arquitectura, patrones y buenas prácticas
Introducción a los microservicios
La arquitectura de microservicios es un enfoque de desarrollo de software que estructura una aplicación como un conjunto de servicios débilmente acoplados y desplegables de forma independiente. A diferencia de las arquitecturas monolíticas tradicionales, los microservicios permiten una mayor agilidad, escalabilidad y resiliencia de las aplicaciones.
¿Por qué adoptar microservicios con .NET?
- Despliegue independiente: cada servicio puede evolucionar por separado
- Escalabilidad focalizada: escalado horizontal únicamente para los servicios que lo necesitan
- Mayor resiliencia: el fallo de un servicio no provoca la caída completa del sistema
- Equipos autónomos: los equipos pueden desarrollar, probar y desplegar sus servicios de forma independiente
- Adaptabilidad tecnológica: posibilidad de utilizar diferentes tecnologías para diferentes servicios
- Integración perfecta con el ecosistema .NET y ASP.NET Core
Fundamentos de los microservicios con .NET
Arquitectura básica
Una arquitectura de microservicios típica en .NET suele comprender:
- API Gateway: punto de entrada único para los clientes (a menudo con Ocelot)
- Servicios de negocio: servicios ASP.NET Core que implementan la lógica de negocio
- Servicio de descubrimiento: para localizar las instancias de los servicios (Consul, Eureka)
- Base de datos por servicio: cada servicio posee su propia base de datos
- Message Broker: para la comunicación asíncrona (RabbitMQ, Azure Service Bus)
- Servicios de identidad: para la autenticación y la autorización (IdentityServer)
- Contenedores y orquestación: Docker y Kubernetes para el despliegue
Creación de un microservicio ASP.NET Core
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Añadir los servicios necesarios
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<ProductContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("ProductDb")));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
var app = builder.Build();
// Configurar el pipeline de peticiones HTTP
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Patrones de diseño para microservicios
Patrón CQRS (Command Query Responsibility Segregation)
// Comando
public class CreateProductCommand : IRequest<int>
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
// Manejador de comando
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
private readonly ProductContext _context;
public CreateProductCommandHandler(ProductContext context)
{
_context = context;
}
public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var product = new Product
{
Name = request.Name,
Description = request.Description,
Price = request.Price
};
_context.Products.Add(product);
await _context.SaveChangesAsync(cancellationToken);
return product.Id;
}
}
// Consulta
public class GetProductQuery : IRequest<ProductDto>
{
public int Id { get; set; }
}
// Manejador de consulta
public class GetProductQueryHandler : IRequestHandler<GetProductQuery, ProductDto>
{
private readonly ProductContext _context;
public GetProductQueryHandler(ProductContext context)
{
_context = context;
}
public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken cancellationToken)
{
var product = await _context.Products.FindAsync(request.Id);
return new ProductDto
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price
};
}
}
Patrón Saga para las transacciones distribuidas
// Definición de una saga para el proceso de pedido
public class OrderSaga :
IAmStartedByMessages<CreateOrderCommand>,
IHandleMessages<PaymentProcessedEvent>,
IHandleMessages<InventoryReservedEvent>,
IHandleMessages<ShippingPreparedEvent>
{
private readonly IBus _bus;
private readonly IOrderRepository _orderRepository;
public OrderSaga(IBus bus, IOrderRepository orderRepository)
{
_bus = bus;
_orderRepository = orderRepository;
}
public async Task Handle(CreateOrderCommand message, IMessageHandlerContext context)
{
// Crear el pedido
var order = new Order(message.OrderId, message.CustomerId, message.Products);
await _orderRepository.SaveAsync(order);
// Desencadenar el proceso de pago
await _bus.Publish(new ProcessPaymentCommand
{
OrderId = message.OrderId,
Amount = order.TotalAmount,
PaymentDetails = message.PaymentDetails
});
}
public async Task Handle(PaymentProcessedEvent message, IMessageHandlerContext context)
{
// Actualizar el estado del pedido
var order = await _orderRepository.GetByIdAsync(message.OrderId);
order.MarkPaymentComplete();
await _orderRepository.UpdateAsync(order);
// Reservar el inventario
await _bus.Publish(new ReserveInventoryCommand
{
OrderId = message.OrderId,
Products = order.Products
});
}
// Gestionar los demás eventos...
// Gestionar las compensaciones en caso de fallo
private async Task CompensateOrder(string orderId, string reason)
{
var order = await _orderRepository.GetByIdAsync(orderId);
order.Cancel(reason);
await _orderRepository.UpdateAsync(order);
// Emitir eventos de compensación (reembolso, liberación de inventario, etc.)
}
}
API Gateway con Ocelot
// ocelot.json
{
"Routes": [
{
"DownstreamPathTemplate": "/api/products/{everything}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "product-service",
"Port": 443
}
],
"UpstreamPathTemplate": "/products/{everything}",
"UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ]
},
{
"DownstreamPathTemplate": "/api/orders/{everything}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "order-service",
"Port": 443
}
],
"UpstreamPathTemplate": "/orders/{everything}",
"UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ]
}
],
"GlobalConfiguration": {
"BaseUrl": "https://api.example.com"
}
}
// Program.cs para API Gateway
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration);
var app = builder.Build();
app.UseOcelot().Wait();
app.Run();
Comunicación entre microservicios
Comunicación síncrona con HTTP/REST
// Servicio cliente HTTP
public class ProductService : IProductService
{
private readonly HttpClient _httpClient;
public ProductService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<ProductDto> GetProductAsync(int id)
{
var response = await _httpClient.GetAsync($"/api/products/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ProductDto>();
}
}
// Configuración
services.AddHttpClient<IProductService, ProductService>(client =>
{
client.BaseAddress = new Uri("https://product-service");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
// Estrategias de resiliencia
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}
Comunicación asíncrona con eventos
// Definición de un evento
public class OrderCreatedEvent : IntegrationEvent
{
public int OrderId { get; set; }
public int CustomerId { get; set; }
public List<OrderItem> Items { get; set; }
public DateTime OrderDate { get; set; }
}
// Publicación de un evento con MassTransit y RabbitMQ
public class OrderController : ControllerBase
{
private readonly IOrderRepository _orderRepository;
private readonly IPublishEndpoint _publishEndpoint;
public OrderController(IOrderRepository orderRepository, IPublishEndpoint publishEndpoint)
{
_orderRepository = orderRepository;
_publishEndpoint = publishEndpoint;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderCommand command)
{
// Lógica de creación de pedido
var order = new Order(command);
await _orderRepository.AddAsync(order);
// Publicación del evento
await _publishEndpoint.Publish(new OrderCreatedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
Items = order.Items,
OrderDate = order.OrderDate
});
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
}
// Configuración de MassTransit
services.AddMassTransit(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("rabbitmq://localhost");
cfg.ConfigureEndpoints(context);
});
});
Persistencia de datos
Base de datos por servicio
// DbContext para el servicio Producto
public class ProductContext : DbContext
{
public ProductContext(DbContextOptions<ProductContext> options) : base(options)
{
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().ToTable("Products");
modelBuilder.Entity<Product>().HasKey(p => p.Id);
modelBuilder.Entity<Product>().Property(p => p.Name).IsRequired().HasMaxLength(100);
modelBuilder.Entity<Product>().Property(p => p.Price).HasColumnType("decimal(18,2)");
}
}
// Configuración en Program.cs
services.AddDbContext<ProductContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("ProductDb")));
Patrón Repository
// Interfaz del Repository
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task<IEnumerable<Product>> GetByNameAsync(string name);
Task AddAsync(Product product);
Task UpdateAsync(Product product);
Task DeleteAsync(int id);
}
// Implementación del Repository
public class ProductRepository : IProductRepository
{
private readonly ProductContext _context;
public ProductRepository(ProductContext context)
{
_context = context;
}
public async Task<Product> GetByIdAsync(int id)
{
return await _context.Products
.FindAsync(id);
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
return await _context.Products
.ToListAsync();
}
public async Task<IEnumerable<Product>> GetByNameAsync(string name)
{
return await _context.Products
.Where(p => p.Name.Contains(name))
.ToListAsync();
}
public async Task AddAsync(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Product product)
{
_context.Entry(product).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var product = await _context.Products.FindAsync(id);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
}
}
Despliegue y orquestación
Contenerización con Docker
# Dockerfile para un microservicio .NET
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["ProductService.csproj", "./"]
RUN dotnet restore "ProductService.csproj"
COPY . .
RUN dotnet build "ProductService.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ProductService.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ProductService.dll"]
Orquestación con Kubernetes
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
labels:
app: product-service
spec:
replicas: 3
selector:
matchLabels:
app: product-service
template:
metadata:
labels:
app: product-service
spec:
containers:
- name: product-service
image: acr.io/myorg/product-service:latest
ports:
- containerPort: 80
env:
- name: ConnectionStrings__ProductDb
valueFrom:
secretKeyRef:
name: product-service-secrets
key: db-connection-string
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /health/live
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 15
periodSeconds: 5
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: product-service
spec:
selector:
app: product-service
ports:
- port: 80
targetPort: 80
type: ClusterIP
Observabilidad y monitorización
Health Checks
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Ajouter les health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ProductContext>("database")
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddRabbitMQ($"amqp://guest:guest@rabbitmq:5672/", name: "rabbitmq");
var app = builder.Build();
// Configurer les endpoints de health check
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => true
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();
Tracing distribuido con OpenTelemetry
// Program.cs
services.AddOpenTelemetryTracing(builder =>
{
builder
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("ProductService"))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddJaegerExporter(options =>
{
options.AgentHost = "jaeger";
options.AgentPort = 6831;
});
});
Seguridad de los microservicios
Autenticación con IdentityServer
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://identity-service";
options.Audience = "product-api";
options.RequireHttpsMetadata = false;
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ReadProducts", policy =>
policy.RequireClaim("scope", "product.read"));
options.AddPolicy("WriteProducts", policy =>
policy.RequireClaim("scope", "product.write"));
});
// Controller
[Authorize(Policy = "ReadProducts")]
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetProduct(int id)
{
// ...
}
[Authorize(Policy = "WriteProducts")]
[HttpPost]
public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductCommand command)
{
// ...
}
Seguridad de las API con rate limiting
// Program.cs
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
});
});
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", token);
};
});
var app = builder.Build();
app.UseRateLimiter();
Pruebas para microservicios
Pruebas unitarias
// ProductServiceTests.cs
public class ProductServiceTests
{
[Fact]
public async Task GetProductAsync_ExistingProduct_ReturnsProduct()
{
// Arrange
var mockRepository = new Mock<IProductRepository>();
mockRepository.Setup(repo => repo.GetByIdAsync(1))
.ReturnsAsync(new Product { Id = 1, Name = "Test Product", Price = 10.99m });
var service = new ProductService(mockRepository.Object);
// Act
var result = await service.GetProductAsync(1);
// Assert
Assert.NotNull(result);
Assert.Equal(1, result.Id);
Assert.Equal("Test Product", result.Name);
}
}
Pruebas de integración
// ProductControllerIntegrationTests.cs
public class ProductControllerIntegrationTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ProductControllerIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Reemplazar el DbContext por una base de datos en memoria
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ProductContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.AddDbContext<ProductContext>(options =>
{
options.UseInMemoryDatabase("TestProductDb");
});
// Añadir datos de prueba
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ProductContext>();
db.Products.Add(new Product { Id = 1, Name = "Test Product", Price = 10.99m });
db.SaveChanges();
});
});
}
[Fact]
public async Task GetProducts_ReturnsSuccessAndCorrectContentType()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/products");
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal("application/json; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Buenas prácticas para los microservicios .NET
-
Separación de responsabilidades
- Cada microservicio debe tener una responsabilidad única y bien definida
-
Gestión de la configuración
- Utilice proveedores de configuración externos (Azure App Configuration, Kubernetes ConfigMaps/Secrets)
-
Resiliencia
- Implemente patrones como Circuit Breaker, Retry y Timeout con Polly
-
Versionado de las API
- Utilice versiones explícitas en las rutas o en las cabeceras HTTP
-
Trazabilidad
- Utilice identificadores de correlación para seguir las peticiones a través de los servicios
-
Documentación
- Utilice Swagger/OpenAPI para documentar sus API
-
Arquitectura por dominio
- Organice sus microservicios en torno a los dominios de negocio (DDD)
-
Infrastructure as Code
- Automatice el despliegue y la configuración con herramientas como Terraform o Pulumi
Recursos para profundizar
- Microsoft Learn: Microservices avec .NET
- eShopOnContainers: Ejemplo de referencia
- Steeltoe: Framework para los microservicios .NET
- Dapr: Runtime distribuido para microservicios
- Libro: “Building Microservices with ASP.NET Core” de Kevin Hoffman
Conclusión
La arquitectura de microservicios ofrece numerosas ventajas para las aplicaciones modernas, pero también introduce una complejidad adicional. Con .NET y su rico ecosistema, dispone de todas las herramientas necesarias para diseñar, desarrollar y desplegar microservicios robustos, escalables y mantenibles. Siguiendo los patrones y las buenas prácticas presentados en esta guía, podrá crear arquitecturas de microservicios eficaces que respondan a las necesidades de su empresa.
Acerca de InSkillCoach
Experto en formación y tecnologías
Coach especializado en tecnologías avanzadas e IA, impulsado por GNeurone Inc.
Certificaciones:
- AWS Certified Solutions Architect – Professional
- Certificaciones de Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comentarios
Los comentarios funcionan con GitHub Discussions
Inicia sesión con GitHub para participar en la discusión