0%
Microservicios con .NET: arquitectura, patrones y buenas prácticas

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.

I

InSkillCoach

· min

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

  1. Separación de responsabilidades

    • Cada microservicio debe tener una responsabilidad única y bien definida
  2. Gestión de la configuración

    • Utilice proveedores de configuración externos (Azure App Configuration, Kubernetes ConfigMaps/Secrets)
  3. Resiliencia

    • Implemente patrones como Circuit Breaker, Retry y Timeout con Polly
  4. Versionado de las API

    • Utilice versiones explícitas en las rutas o en las cabeceras HTTP
  5. Trazabilidad

    • Utilice identificadores de correlación para seguir las peticiones a través de los servicios
  6. Documentación

    • Utilice Swagger/OpenAPI para documentar sus API
  7. Arquitectura por dominio

    • Organice sus microservicios en torno a los dominios de negocio (DDD)
  8. Infrastructure as Code

    • Automatice el despliegue y la configuración con herramientas como Terraform o Pulumi

Recursos para profundizar

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.

InSkillCoach

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+
818
130

Comentarios

Los comentarios funcionan con GitHub Discussions

Inicia sesión con GitHub para participar en la discusión

¡Enlace copiado!