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.
InSkillCoach
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
-
Séparation des préoccupations
- Chaque microservice doit avoir une responsabilité unique et bien définie
-
Gestion de la configuration
- Utilisez des fournisseurs de configuration externes (Azure App Configuration, Kubernetes ConfigMaps/Secrets)
-
Résilience
- Implémentez des patterns comme Circuit Breaker, Retry, Timeout avec Polly
-
Versioning des API
- Utilisez des versions explicites dans les routes ou les en-têtes HTTP
-
Traçabilité
- Utilisez des identifiants de corrélation pour suivre les requêtes à travers les services
-
Documentation
- Utilisez Swagger/OpenAPI pour documenter vos API
-
Architecture par domaine
- Organisez vos microservices autour des domaines métier (DDD)
-
Infrastructure as Code
- Automatisez le déploiement et la configuration avec des outils comme Terraform ou Pulumi
Ressources pour approfondir
- Microsoft Learn : Microservices avec .NET
- eShopOnContainers : Exemple de référence
- Steeltoe : Framework pour les microservices .NET
- Dapr : Runtime distribué pour les microservices
- Livre : “Building Microservices with ASP.NET Core” de Kevin Hoffman
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.
À 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+
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion