Microservices with .NET: Architecture, Patterns, and Best Practices
A complete guide to designing, building, and deploying robust microservices architectures with .NET and ASP.NET Core.
InSkillCoach
Microservices with .NET: Architecture, Patterns, and Best Practices
Introduction to Microservices
Microservices architecture is a software development approach that structures an application as a set of loosely coupled, independently deployable services. Unlike traditional monolithic architectures, microservices provide greater agility, scalability, and resilience.
Why Adopt Microservices with .NET?
- Independent deployment: Each service can evolve separately
- Targeted scalability: Scale horizontally only the services that need it
- Improved resilience: The failure of one service does not bring down the entire system
- Autonomous teams: Teams can develop, test, and deploy their services independently
- Technology flexibility: You can use different technologies for different services
- Seamless integration with the .NET and ASP.NET Core ecosystem
Microservices Fundamentals with .NET
Basic Architecture
A typical microservices architecture in .NET generally includes:
- API Gateway: A single entry point for clients (often implemented with Ocelot)
- Business services: ASP.NET Core services that implement the business logic
- Discovery service: To locate service instances (Consul, Eureka)
- Database per service: Each service owns its own database
- Message broker: For asynchronous communication (RabbitMQ, Azure Service Bus)
- Identity services: For authentication and authorization (IdentityServer)
- Containers and orchestration: Docker and Kubernetes for deployment
Creating an ASP.NET Core Microservice
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add the required services
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();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Design Patterns for Microservices
CQRS Pattern (Command Query Responsibility Segregation)
// Command
public class CreateProductCommand : IRequest<int>
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
// Command handler
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;
}
}
// Query
public class GetProductQuery : IRequest<ProductDto>
{
public int Id { get; set; }
}
// Query handler
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
};
}
}
Saga Pattern for Distributed Transactions
// Definition of a saga for the order process
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)
{
// Create the order
var order = new Order(message.OrderId, message.CustomerId, message.Products);
await _orderRepository.SaveAsync(order);
// Trigger the payment process
await _bus.Publish(new ProcessPaymentCommand
{
OrderId = message.OrderId,
Amount = order.TotalAmount,
PaymentDetails = message.PaymentDetails
});
}
public async Task Handle(PaymentProcessedEvent message, IMessageHandlerContext context)
{
// Update the order status
var order = await _orderRepository.GetByIdAsync(message.OrderId);
order.MarkPaymentComplete();
await _orderRepository.UpdateAsync(order);
// Reserve the inventory
await _bus.Publish(new ReserveInventoryCommand
{
OrderId = message.OrderId,
Products = order.Products
});
}
// Handle the other events...
// Handle compensations on failure
private async Task CompensateOrder(string orderId, string reason)
{
var order = await _orderRepository.GetByIdAsync(orderId);
order.Cancel(reason);
await _orderRepository.UpdateAsync(order);
// Emit compensation events (refund, inventory release, etc.)
}
}
API Gateway with 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 for the 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 Between Microservices
Synchronous Communication with HTTP/REST
// HTTP client service
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());
// Resilience policies
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));
}
Asynchronous Communication with Events
// Definition of an event
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; }
}
// Publishing an event with MassTransit and 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)
{
// Order creation logic
var order = new Order(command);
await _orderRepository.AddAsync(order);
// Publish the event
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);
}
}
// MassTransit configuration
services.AddMassTransit(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("rabbitmq://localhost");
cfg.ConfigureEndpoints(context);
});
});
Data Persistence
Database per Service
// DbContext for the Product service
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 in Program.cs
services.AddDbContext<ProductContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("ProductDb")));
Repository Pattern
// Repository interface
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);
}
// Repository implementation
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();
}
}
}
Deployment and Orchestration
Containerization with Docker
# Dockerfile for a .NET microservice
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 with 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
Observability and Monitoring
Health Checks
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ProductContext>("database")
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddRabbitMQ($"amqp://guest:guest@rabbitmq:5672/", name: "rabbitmq");
var app = builder.Build();
// Configure health check endpoints
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();
Distributed Tracing with OpenTelemetry
// Program.cs
services.AddOpenTelemetryTracing(builder =>
{
builder
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("ProductService"))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddJaegerExporter(options =>
{
options.AgentHost = "jaeger";
options.AgentPort = 6831;
});
});
Microservices Security
Authentication with 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)
{
// ...
}
API Security with 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();
Testing Microservices
Unit Tests
// 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);
}
}
Integration Tests
// 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 =>
{
// Replace the DbContext with an in-memory database
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ProductContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.AddDbContext<ProductContext>(options =>
{
options.UseInMemoryDatabase("TestProductDb");
});
// Add test data
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());
}
}
Best Practices for .NET Microservices
-
Separation of Concerns
- Each microservice should have a single, well-defined responsibility
-
Configuration Management
- Use external configuration providers (Azure App Configuration, Kubernetes ConfigMaps/Secrets)
-
Resilience
- Implement patterns such as Circuit Breaker, Retry, and Timeout with Polly
-
API Versioning
- Use explicit versions in routes or HTTP headers
-
Traceability
- Use correlation IDs to follow requests across services
-
Documentation
- Use Swagger/OpenAPI to document your APIs
-
Domain-Based Architecture
- Organize your microservices around business domains (DDD)
-
Infrastructure as Code
- Automate deployment and configuration with tools like Terraform or Pulumi
Further Resources
- Microsoft Learn: Microservices with .NET
- eShopOnContainers: Reference example
- Steeltoe: Framework for .NET microservices
- Dapr: Distributed runtime for microservices
- Book: “Building Microservices with ASP.NET Core” by Kevin Hoffman
Conclusion
Microservices architecture offers many benefits for modern applications, but it also introduces additional complexity. With .NET and its rich ecosystem, you have all the tools you need to design, develop, and deploy robust, scalable, and maintainable microservices. By following the patterns and best practices presented in this guide, you’ll be able to build effective microservices architectures that meet your organization’s needs.
About InSkillCoach
Training and technology expert
Coach specializing in advanced technologies and AI, backed by GNeurone Inc.
Certifications:
- AWS Certified Solutions Architect – Professional
- Google Cloud certifications
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comments
Comments are powered by GitHub Discussions
Sign in with GitHub to join the discussion