.NET microservices patterns including service decomposition, communication, CQRS, saga, and resilience
From dotnet-blazornpx claudepluginhub markus41/claude --plugin dotnet-blazorThis skill is limited to using the following tools:
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Analyzes BMad project state from catalog CSV, configs, artifacts, and query to recommend next skills or answer questions. Useful for help requests, 'what next', or starting BMad.
E-Commerce Platform:
├── Catalog Service (products, categories, search)
├── Order Service (orders, order processing)
├── Payment Service (payment processing, refunds)
├── Inventory Service (stock management)
├── Notification Service (email, SMS, push)
├── Identity Service (authentication, users)
└── Gateway (API gateway, BFF)
Each service owns its data store. No shared databases.
// Catalog uses PostgreSQL
builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb");
// Order uses SQL Server
builder.AddSqlServerDbContext<OrderDbContext>("orderdb");
// Notification uses Cosmos DB
builder.AddCosmosDbContext<NotificationDbContext>("cosmosdb");
// Command side
public sealed record CreateOrderCommand(int CustomerId, List<OrderItemDto> Items);
public sealed class CreateOrderHandler(OrderDbContext db, IPublishEndpoint bus)
{
public async Task<int> HandleAsync(CreateOrderCommand command, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items);
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
await bus.Publish(new OrderCreatedEvent(order.Id, order.CustomerId), ct);
return order.Id;
}
}
// Query side (separate read model, possibly different DB)
public sealed class OrderQueryService(IReadOnlyDbContext readDb)
{
public async Task<OrderDetailDto?> GetByIdAsync(int id, CancellationToken ct) =>
await readDb.Orders
.AsNoTracking()
.Where(o => o.Id == id)
.Select(o => o.ToDetailDto())
.FirstOrDefaultAsync(ct);
}
// Using MassTransit state machine
public sealed class OrderSaga : MassTransitStateMachine<OrderSagaState>
{
public OrderSaga()
{
InstanceState(x => x.CurrentState);
Event(() => OrderCreated, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PaymentProcessed, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => InventoryReserved, x => x.CorrelateById(m => m.Message.OrderId));
Initially(
When(OrderCreated)
.Then(ctx => ctx.Saga.OrderId = ctx.Message.OrderId)
.Publish(ctx => new ProcessPaymentCommand(ctx.Saga.OrderId))
.TransitionTo(AwaitingPayment));
During(AwaitingPayment,
When(PaymentProcessed)
.Publish(ctx => new ReserveInventoryCommand(ctx.Saga.OrderId))
.TransitionTo(AwaitingInventory),
When(PaymentFailed)
.Publish(ctx => new CancelOrderCommand(ctx.Saga.OrderId))
.TransitionTo(Failed));
During(AwaitingInventory,
When(InventoryReserved)
.Publish(ctx => new FulfillOrderCommand(ctx.Saga.OrderId))
.TransitionTo(Completed));
}
}
// Microsoft.Extensions.Resilience + Polly v8
builder.Services.AddHttpClient<ICatalogClient>(client =>
client.BaseAddress = new("https+http://catalog-api"))
.AddStandardResilienceHandler(); // Retry + Circuit Breaker + Timeout
// Custom pipeline
builder.Services.AddResiliencePipeline("custom", pipeline =>
{
pipeline
.AddRetry(new() { MaxRetryAttempts = 3, BackoffType = DelayBackoffType.Exponential })
.AddCircuitBreaker(new() { FailureRatio = 0.5, MinimumThroughput = 10 })
.AddTimeout(TimeSpan.FromSeconds(5));
});
builder.Services.AddHealthChecks()
.AddNpgSql(connectionString, name: "database")
.AddRedis(redisConnection, name: "cache")
.AddRabbitMQ(rabbitConnection, name: "messaging")
.AddCheck<CustomHealthCheck>("custom");
app.MapHealthChecks("/health/ready", new() { Predicate = check => check.Tags.Contains("ready") });
app.MapHealthChecks("/health/live", new() { Predicate = _ => false }); // Just checks app is running
| Pattern | Use when | Trade-offs |
|---|---|---|
| Direct client-to-service | Few services, internal apps | Simple but couples clients to services |
| API Gateway (YARP/Ocelot) | Many services, external clients | Single entry point, adds latency |
| BFF (Backend for Frontend) | Multiple client types (web, mobile) | Client-optimized APIs, more gateways |
// YARP reverse proxy (Microsoft's recommended API gateway)
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
app.MapReverseProxy();
Each service owns its data. Cross-service data needs are resolved via:
Order Service ──(event)──> Catalog Service
│ OrderDB │ CatalogDB
│ (orders, items) │ (products, stock)
│ │
└──(HTTP)──> Payment Service
│ PaymentDB
│ (payments, refunds)
// Integration events cross service boundaries
public abstract record IntegrationEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime CreatedAt { get; } = DateTime.UtcNow;
}
public sealed record OrderSubmittedIntegrationEvent(
int OrderId, int BuyerId, decimal Total) : IntegrationEvent;
// Publish via outbox pattern for reliability
public sealed class OutboxPublisher(AppDbContext db, IEventBus bus)
{
public async Task PublishPendingEventsAsync(CancellationToken ct)
{
var pending = await db.OutboxMessages
.Where(m => !m.Published)
.OrderBy(m => m.CreatedAt)
.Take(50)
.ToListAsync(ct);
foreach (var message in pending)
{
await bus.PublishAsync(message.Event, ct);
message.Published = true;
message.PublishedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync(ct);
}
}
Each service can own a UI fragment:
@* Main Blazor app composes service-specific components *@
<CatalogProductList /> @* Owned by Catalog team *@
<OrderStatusWidget /> @* Owned by Order team *@
<CartSummary /> @* Owned by Cart team *@
Pattern options:
// JWT validation at API gateway
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://identity-service";
options.Audience = "catalog-api";
});
// Azure Key Vault for secrets
builder.Configuration.AddAzureKeyVault(
new Uri("https://myvault.vault.azure.net/"),
new DefaultAzureCredential());