Microservices with C# and .NET
Microservices split a monolith into independently deployable services, each owning a business domain. .NET provides first-class support for HTTP APIs, gRPC, messaging, and containerized deployment — making it a strong choice for enterprise microservice architectures.
Monolith vs Microservices
Monolith: Microservices:
┌──────────────────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Users │ Orders │ Pay │ │ User │ │ Order │ │Payment │
│ Shared Database │ │ Service│ │Service │ │Service │
└──────────────────────┘ └────────┘ └────────┘ └────────┘
↑
API Gateway (YARP)
Extract services when team size, deployment conflicts, or scaling needs justify the operational overhead. Start with a modular monolith and split along natural boundaries.
Service Boundaries
Decompose by business capability, not technical layer:
✅ UserService, OrderService, PaymentService, NotificationService
❌ ControllerService, RepositoryService, ValidationService
Each service:
- Owns its database (no shared tables)
- Exposes a well-defined API
- Deploys independently
- Can fail without cascading (with proper resilience)
Project Structure
src/
├── Services/
│ ├── UserService/
│ │ ├── UserService.Api/
│ │ ├── UserService.Domain/
│ │ └── UserService.Infrastructure/
│ ├── OrderService/
│ └── PaymentService/
├── Gateway/
│ └── ApiGateway/
└── Shared/
└── Contracts/ # DTOs, event schemas only
Avoid shared libraries with business logic — they create coupling. Share only contracts (DTOs, event definitions).
REST API Service
// OrderService.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<OrderDbContext>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Orders")));
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
var app = builder.Build();
app.MapGet("/orders/{id:guid}", async (Guid id, IOrderRepository repo) =>
{
var order = await repo.GetByIdAsync(id);
return order is null ? Results.NotFound() : Results.Ok(order);
});
app.MapPost("/orders", async (CreateOrderRequest req, IOrderRepository repo) =>
{
var order = await repo.CreateAsync(req);
return Results.Created($"/orders/{order.Id}", order);
});
app.Run();
gRPC for Service-to-Service
gRPC is faster and strongly typed — ideal for internal communication:
// orders.proto
syntax = "proto3";
service OrderService {
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
}
message GetOrderRequest { string id = 1; }
message OrderResponse { string id = 1; string status = 2; double total = 3; }
// Client
var channel = GrpcChannel.ForAddress("https://order-service:5001");
var client = new OrderService.OrderServiceClient(channel);
var response = await client.GetOrderAsync(new GetOrderRequest { Id = orderId });
Enable HTTP/2 and TLS between services in production.
Message Bus with MassTransit
Decouple services with asynchronous events:
// Shared contract
public record OrderPlaced(Guid OrderId, Guid CustomerId, decimal Total);
// Order Service — publish
await publishEndpoint.Publish(new OrderPlaced(order.Id, order.CustomerId, order.Total));
// Payment Service — consume
public class OrderPlacedConsumer : IConsumer<OrderPlaced>
{
public async Task Consume(ConsumeContext<OrderPlaced> context)
{
var msg = context.Message;
await paymentService.ProcessPayment(msg.OrderId, msg.Total);
}
}
Configure RabbitMQ or Azure Service Bus as the transport. Consumers must be idempotent.
API Gateway with YARP
// Gateway/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
{
"ReverseProxy": {
"Routes": {
"orders": {
"ClusterId": "order-cluster",
"Match": { "Path": "/api/orders/{**catch-all}" }
}
},
"Clusters": {
"order-cluster": {
"Destinations": {
"order1": { "Address": "http://order-service:8080" }
}
}
}
}
}
YARP handles routing, load balancing, and transforms at the edge.
Resilience with Polly
builder.Services.AddHttpClient<IOrderClient, OrderClient>()
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
});
Patterns: retry with backoff, circuit breaker, timeout, bulkhead. Prevent cascading failures when a downstream service is unhealthy.
Health Checks
builder.Services.AddHealthChecks()
.AddNpgSql(connectionString)
.AddRabbitMQ(rabbitConnectionString);
app.MapHealthChecks("/health");
app.MapHealthChecks("/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
});
Kubernetes uses /health (liveness) and /ready (readiness) probes.
Containerization
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish OrderService.Api -c Release -o /app/publish
FROM base AS final
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "OrderService.Api.dll"]
Deploy to Kubernetes, Azure Container Apps, or AWS ECS.
Distributed Tracing
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter());
Correlate requests across services with trace IDs. Tools: Jaeger, Application Insights, Grafana Tempo.
Data Management
- Database per service — no cross-service JOINs
- Saga pattern for distributed transactions (choreography via events or orchestration)
- CQRS when read and write patterns differ significantly
- Event sourcing for audit-heavy domains (optional, adds complexity)
Production Checklist
- Clear service boundaries with owned databases
- API gateway for external clients
- gRPC or message bus for internal communication
- Resilience policies (retry, circuit breaker, timeout)
- Health checks and structured logging
- Distributed tracing enabled
- Container images with non-root user
- CI/CD per service (independent deploy pipelines)
- Contract tests between services
.NET microservices succeed when teams embrace autonomous services, async communication, and operational observability from day one.