0%
Microsserviços com .NET: Arquitetura, Padrões e Boas Práticas

Microsserviços com .NET: Arquitetura, Padrões e Boas Práticas

Guia completo para projetar, desenvolver e implantar arquiteturas de microsserviços robustas com .NET e ASP.NET Core.

I

InSkillCoach

· min

Microsserviços com .NET: Arquitetura, Padrões e Boas Práticas

Introdução aos microsserviços

A arquitetura de microsserviços é uma abordagem de desenvolvimento de software que estrutura uma aplicação como um conjunto de serviços fracamente acoplados e implantáveis de forma independente. Diferentemente das arquiteturas monolíticas tradicionais, os microsserviços proporcionam mais agilidade, escalabilidade e resiliência às aplicações.

Por que adotar microsserviços com .NET?

  • Implantação independente: cada serviço pode evoluir separadamente
  • Escalabilidade direcionada: escalonamento horizontal apenas para os serviços que precisam dele
  • Resiliência aprimorada: a falha de um serviço não derruba o sistema inteiro
  • Equipes autônomas: as equipes podem desenvolver, testar e implantar seus serviços de forma independente
  • Flexibilidade tecnológica: possibilidade de usar tecnologias diferentes para serviços diferentes
  • Integração perfeita com o ecossistema .NET e o ASP.NET Core

Fundamentos dos microsserviços com .NET

Arquitetura básica

Uma arquitetura de microsserviços típica em .NET geralmente inclui:

  • API Gateway: ponto de entrada único para os clientes (frequentemente com Ocelot)
  • Serviços de negócio: serviços ASP.NET Core que implementam a lógica de negócio
  • Serviço de descoberta: para localizar as instâncias dos serviços (Consul, Eureka)
  • Banco de dados por serviço: cada serviço possui seu próprio banco de dados
  • Message Broker: para comunicação assíncrona (RabbitMQ, Azure Service Bus)
  • Serviços de identidade: para autenticação e autorização (IdentityServer)
  • Contêineres e orquestração: Docker e Kubernetes para a implantação

Criação de um microsserviço ASP.NET Core

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Adicionar os serviços necessários
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 o pipeline de requisições HTTP
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Padrões de projeto para microsserviços

Padrão 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; }
}

// Manipulador 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; }
}

// Manipulador 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
        };
    }
}

Padrão Saga para transações distribuídas

// Definição de uma saga para o processo 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)
    {
        // Criar o pedido
        var order = new Order(message.OrderId, message.CustomerId, message.Products);
        await _orderRepository.SaveAsync(order);
        
        // Disparar o processo de pagamento
        await _bus.Publish(new ProcessPaymentCommand
        {
            OrderId = message.OrderId,
            Amount = order.TotalAmount,
            PaymentDetails = message.PaymentDetails
        });
    }
    
    public async Task Handle(PaymentProcessedEvent message, IMessageHandlerContext context)
    {
        // Atualizar o status do pedido
        var order = await _orderRepository.GetByIdAsync(message.OrderId);
        order.MarkPaymentComplete();
        await _orderRepository.UpdateAsync(order);
        
        // Reservar o estoque
        await _bus.Publish(new ReserveInventoryCommand
        {
            OrderId = message.OrderId,
            Products = order.Products
        });
    }
    
    // Tratar os outros eventos...
    
    // Tratar as compensações em caso de falha
    private async Task CompensateOrder(string orderId, string reason)
    {
        var order = await _orderRepository.GetByIdAsync(orderId);
        order.Cancel(reason);
        await _orderRepository.UpdateAsync(order);
        
        // Emitir eventos de compensação (reembolso, liberação de estoque, etc.)
    }
}

API Gateway com 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();

Comunicação entre microsserviços

Comunicação síncrona com HTTP/REST

// Serviço HTTP client
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>();
    }
}

// Configuração
services.AddHttpClient<IProductService, ProductService>(client =>
{
    client.BaseAddress = new Uri("https://product-service");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());

// Estratégias de resiliência
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));
}

Comunicação assíncrona com eventos

// Definição de um 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; }
}

// Publicação de um evento com MassTransit e 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 criação de pedido
        var order = new Order(command);
        await _orderRepository.AddAsync(order);
        
        // Publicação do 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);
    }
}

// Configuração do MassTransit
services.AddMassTransit(x =>
{
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("rabbitmq://localhost");
        cfg.ConfigureEndpoints(context);
    });
});

Persistência de dados

Banco de dados por serviço

// DbContext para o serviço de Produto
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)");
    }
}

// Configuração no Program.cs
services.AddDbContext<ProductContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("ProductDb")));

Padrão Repository

// Interface do 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);
}

// Implementação do 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();
        }
    }
}

Implantação e orquestração

Conteinerização com Docker

# Dockerfile para um microsserviço .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"]

Orquestração com 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

Observabilidade e monitoramento

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 distribuído com OpenTelemetry

// Program.cs
services.AddOpenTelemetryTracing(builder =>
{
    builder
        .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("ProductService"))
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddJaegerExporter(options =>
        {
            options.AgentHost = "jaeger";
            options.AgentPort = 6831;
        });
});

Segurança dos microsserviços

Autenticação com 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)
{
    // ...
}

Segurança de APIs com 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();

Testes para microsserviços

Testes unitários

// 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);
    }
}

Testes de integração

// 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 =>
            {
                // Substituir o DbContext por um banco de dados em memória
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<ProductContext>));
                
                if (descriptor != null)
                {
                    services.Remove(descriptor);
                }
                
                services.AddDbContext<ProductContext>(options =>
                {
                    options.UseInMemoryDatabase("TestProductDb");
                });
                
                // Adicionar dados de teste
                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());
    }
}

Boas práticas para microsserviços .NET

  1. Separação de responsabilidades

    • Cada microsserviço deve ter uma responsabilidade única e bem definida
  2. Gerenciamento de configuração

    • Utilize provedores de configuração externos (Azure App Configuration, Kubernetes ConfigMaps/Secrets)
  3. Resiliência

    • Implemente padrões como Circuit Breaker, Retry e Timeout com Polly
  4. Versionamento de APIs

    • Utilize versões explícitas nas rotas ou nos cabeçalhos HTTP
  5. Rastreabilidade

    • Utilize identificadores de correlação para acompanhar as requisições entre os serviços
  6. Documentação

    • Utilize Swagger/OpenAPI para documentar suas APIs
  7. Arquitetura por domínio

    • Organize seus microsserviços em torno dos domínios de negócio (DDD)
  8. Infrastructure as Code

    • Automatize a implantação e a configuração com ferramentas como Terraform ou Pulumi

Recursos para se aprofundar

Conclusão

A arquitetura de microsserviços oferece muitas vantagens para aplicações modernas, mas também introduz uma complexidade adicional. Com o .NET e seu rico ecossistema, você dispõe de todas as ferramentas necessárias para projetar, desenvolver e implantar microsserviços robustos, escaláveis e de fácil manutenção. Seguindo os padrões e as boas práticas apresentados neste guia, você será capaz de criar arquiteturas de microsserviços eficazes, que atendam às necessidades da sua empresa.

InSkillCoach

Sobre InSkillCoach

Especialista em treinamento e tecnologias

Coach especializado em tecnologias avançadas e IA, apoiado pela GNeurone Inc.

Certificações:

  • AWS Certified Solutions Architect – Professional
  • Certificações Google Cloud
  • Microsoft Certified: DevOps Engineer Expert
  • Certified Kubernetes Administrator (CKA)
  • CompTIA Security+
1.3k
217

Comentários

Os comentários são alimentados pelo GitHub Discussions

Conecte-se com o GitHub para participar da discussão

Link copiado!