0%
Microservices avec .NET : Architecture, Patterns et Bonnes Pratiques

Microservices avec .NET : Architecture, Patterns et Bonnes Pratiques

Guide complet pour concevoir, développer et déployer des architectures microservices robustes avec .NET et ASP.NET Core.

I

InSkillCoach

· min

Microservices avec .NET : Architecture, Patterns et Bonnes Pratiques

Introduction aux microservices

L’architecture microservices est une approche de développement logiciel qui structure une application comme un ensemble de services faiblement couplés, déployables indépendamment. Contrairement aux architectures monolithiques traditionnelles, les microservices permettent une meilleure agilité, scalabilité et résilience des applications.

Pourquoi adopter les microservices avec .NET ?

  • Déploiement indépendant : Chaque service peut évoluer séparément
  • Scalabilité ciblée : Scaling horizontal uniquement pour les services qui en ont besoin
  • Résilience améliorée : La défaillance d’un service n’entraîne pas l’arrêt complet du système
  • Équipes autonomes : Les équipes peuvent développer, tester et déployer leurs services indépendamment
  • Adaptabilité technologique : Possibilité d’utiliser différentes technologies pour différents services
  • Intégration parfaite avec l’écosystème .NET et ASP.NET Core

Fondamentaux des microservices avec .NET

Architecture de base

Une architecture microservices typique en .NET comprend généralement :

  • API Gateway : Point d’entrée unique pour les clients (souvent avec Ocelot)
  • Services métier : Services ASP.NET Core implémentant la logique métier
  • Service de découverte : Pour localiser les instances de services (Consul, Eureka)
  • Base de données par service : Chaque service possède sa propre base de données
  • Message Broker : Pour la communication asynchrone (RabbitMQ, Azure Service Bus)
  • Services d’identité : Pour l’authentification et l’autorisation (IdentityServer)
  • Containers et orchestration : Docker et Kubernetes pour le déploiement

Création d’un microservice ASP.NET Core

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

// Ajouter les services nécessaires
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();

// Configurer le pipeline de requêtes HTTP
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

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

app.Run();

Modèles de conception pour microservices

Pattern CQRS (Command Query Responsibility Segregation)

// Commande
public class CreateProductCommand : IRequest<int>
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}

// Gestionnaire de commande
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;
    }
}

// Requête
public class GetProductQuery : IRequest<ProductDto>
{
    public int Id { get; set; }
}

// Gestionnaire de requête
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
        };
    }
}

Pattern Saga pour les transactions distribuées

// Définition d'une saga pour le processus de commande
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)
    {
        // Créer la commande
        var order = new Order(message.OrderId, message.CustomerId, message.Products);
        await _orderRepository.SaveAsync(order);
        
        // Déclencher le processus de paiement
        await _bus.Publish(new ProcessPaymentCommand
        {
            OrderId = message.OrderId,
            Amount = order.TotalAmount,
            PaymentDetails = message.PaymentDetails
        });
    }
    
    public async Task Handle(PaymentProcessedEvent message, IMessageHandlerContext context)
    {
        // Mettre à jour le statut de la commande
        var order = await _orderRepository.GetByIdAsync(message.OrderId);
        order.MarkPaymentComplete();
        await _orderRepository.UpdateAsync(order);
        
        // Réserver l'inventaire
        await _bus.Publish(new ReserveInventoryCommand
        {
            OrderId = message.OrderId,
            Products = order.Products
        });
    }
    
    // Gérer les autres événements...
    
    // Gérer les compensations en cas d'échec
    private async Task CompensateOrder(string orderId, string reason)
    {
        var order = await _orderRepository.GetByIdAsync(orderId);
        order.Cancel(reason);
        await _orderRepository.UpdateAsync(order);
        
        // Émettre des événements de compensation (remboursement, libération d'inventaire, etc.)
    }
}

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

Communication entre microservices

Communication synchrone avec HTTP/REST

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

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

// Stratégies de résilience
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));
}

Communication asynchrone avec des événements

// Définition d'un événement
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; }
}

// Publication d'un événement avec MassTransit et 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)
    {
        // Logique de création de commande
        var order = new Order(command);
        await _orderRepository.AddAsync(order);
        
        // Publication de l'événement
        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);
    }
}

// Configuration de MassTransit
services.AddMassTransit(x =>
{
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("rabbitmq://localhost");
        cfg.ConfigureEndpoints(context);
    });
});

Persistance des données

Base de données par service

// DbContext pour le service Produit
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)");
    }
}

// Configuration dans Program.cs
services.AddDbContext<ProductContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("ProductDb")));

Pattern Repository

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

// Implémentation du 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();
        }
    }
}

Déploiement et orchestration

Containerisation avec Docker

# Dockerfile pour un microservice .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"]

Orchestration avec 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

Observabilité et monitoring

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é avec OpenTelemetry

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

Sécurité des microservices

Authentication avec 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)
{
    // ...
}

Sécurité des API avec 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();

Tests pour microservices

Tests unitaires

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

Tests d’intégration

// 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 =>
            {
                // Remplacer le DbContext par une base de données en mémoire
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<ProductContext>));
                
                if (descriptor != null)
                {
                    services.Remove(descriptor);
                }
                
                services.AddDbContext<ProductContext>(options =>
                {
                    options.UseInMemoryDatabase("TestProductDb");
                });
                
                // Ajouter des données de test
                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());
    }
}

Bonnes pratiques pour les microservices .NET

  1. Séparation des préoccupations

    • Chaque microservice doit avoir une responsabilité unique et bien définie
  2. Gestion de la configuration

    • Utilisez des fournisseurs de configuration externes (Azure App Configuration, Kubernetes ConfigMaps/Secrets)
  3. Résilience

    • Implémentez des patterns comme Circuit Breaker, Retry, Timeout avec Polly
  4. Versioning des API

    • Utilisez des versions explicites dans les routes ou les en-têtes HTTP
  5. Traçabilité

    • Utilisez des identifiants de corrélation pour suivre les requêtes à travers les services
  6. Documentation

    • Utilisez Swagger/OpenAPI pour documenter vos API
  7. Architecture par domaine

    • Organisez vos microservices autour des domaines métier (DDD)
  8. Infrastructure as Code

    • Automatisez le déploiement et la configuration avec des outils comme Terraform ou Pulumi

Ressources pour approfondir

Conclusion

L’architecture microservices offre de nombreux avantages pour les applications modernes, mais elle introduit également une complexité supplémentaire. Avec .NET et son écosystème riche, vous disposez de tous les outils nécessaires pour concevoir, développer et déployer des microservices robustes, évolutifs et maintenables. En suivant les patterns et les bonnes pratiques présentés dans ce guide, vous serez en mesure de créer des architectures microservices efficaces qui répondent aux besoins de votre entreprise.

InSkillCoach

À propos de InSkillCoach

Expert en formation et technologies

Coach spécialisé dans les technologies avancées et l'IA, porté par GNeurone Inc.

Certifications:

  • AWS Certified Solutions Architect – Professional
  • Certifications Google Cloud
  • Microsoft Certified: DevOps Engineer Expert
  • Certified Kubernetes Administrator (CKA)
  • CompTIA Security+
1.1k
133

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !