
Previous article: ASP.NET Core FluentValidation & AutoMapper Guide | Clean Data & Robust APIs (Part-38 of 40)
Table of Contents
Microservices Architecture Fundamentals
gRPC Deep Dive
Building gRPC Services
Advanced gRPC Patterns
Message Brokers with RabbitMQ
Event-Driven Architecture
Service Discovery & API Gateway
Containerization & Orchestration
Distributed Data Management
Observability & Monitoring
Security in Microservices
Real-World E-Commerce Platform
1. Microservices Architecture Fundamentals
The Monolith to Microservices Journey
Real-World Scenario: Imagine an e-commerce platform that started as a monolith but now faces scaling challenges:
// Traditional Monolithic Architecture Problems
public class ECommerceMonolith
{
// Tightly coupled components
public async Task<OrderResult> ProcessOrderAsync(OrderRequest request)
{
// Inventory management
var inventoryResult = await _inventoryService.CheckStockAsync(request.Items);
if (!inventoryResult.Success)
return OrderResult.Failed("Insufficient stock");
// Payment processing
var paymentResult = await _paymentService.ProcessPaymentAsync(request.Payment);
if (!paymentResult.Success)
return OrderResult.Failed("Payment failed");
// Order creation
var order = await _orderService.CreateOrderAsync(request);
// Shipping calculation
var shipping = await _shippingService.CalculateShippingAsync(order);
// Notification
await _notificationService.SendOrderConfirmationAsync(order);
// Analytics
await _analyticsService.TrackOrderAsync(order);
return OrderResult.Success(order);
}
}
Microservices Core Principles
// Microservices Definition
public class MicroservicesPrinciples
{
public List<string> KeyPrinciples = new()
{
"Single Responsibility: Each service focuses on one business capability",
"Independent Deployment: Services can be deployed separately",
"Decentralized Data Management: Each service owns its data",
"Infrastructure Automation: CI/CD and containerization",
"Design for Failure: Resilience and fault tolerance",
"Evolutionary Design: Services can evolve independently"
};
}
Benefits and Trade-offs
Benefits
Independent scaling of services
Technology diversity across services
Faster development and deployment cycles
Improved fault isolation
Better team autonomy
Challenges
Distributed system complexity
Data consistency challenges
Network latency and reliability
Operational overhead
Testing complexity
2. gRPC Deep Dive
What is gRPC?
gRPC is a modern, high-performance RPC (Remote Procedure Call) framework that can run in any environment.
// Protobuf definition for Product Service
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
option csharp_namespace = "ProductService.Grpc";
service ProductService {
rpc GetProduct (GetProductRequest) returns (ProductResponse);
rpc CreateProduct (CreateProductRequest) returns (ProductResponse);
rpc UpdateProduct (UpdateProductRequest) returns (ProductResponse);
rpc DeleteProduct (DeleteProductRequest) returns (google.protobuf.Empty);
rpc ListProducts (ListProductsRequest) returns (stream ProductResponse);
rpc BulkCreateProducts (stream CreateProductRequest) returns (BulkCreateResponse);
}
message GetProductRequest {
int32 product_id = 1;
}
message CreateProductRequest {
string name = 1;
string description = 2;
double price = 3;
int32 stock_quantity = 4;
string category_id = 5;
}
message UpdateProductRequest {
int32 product_id = 1;
string name = 2;
string description = 3;
double price = 4;
int32 stock_quantity = 5;
}
message DeleteProductRequest {
int32 product_id = 1;
}
message ListProductsRequest {
int32 page_size = 1;
string page_token = 2;
string filter = 3;
}
message ProductResponse {
int32 product_id = 1;
string name = 2;
string description = 3;
double price = 4;
int32 stock_quantity = 5;
string category_id = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
ProductStatus status = 9;
}
message BulkCreateResponse {
int32 created_count = 1;
repeated string errors = 2;
}
enum ProductStatus {
ACTIVE = 0;
INACTIVE = 1;
OUT_OF_STOCK = 2;
DISCONTINUED = 3;
}
gRPC vs REST Comparison
// Communication Pattern Comparison
public class CommunicationPatterns
{
public void CompareProtocols()
{
var restCharacteristics = new
{
Protocol = "HTTP/1.1",
PayloadFormat = "JSON/XML",
Communication = "Request-Response",
Streaming = "Limited (SSE, WebSockets)",
Performance = "Good for human-readable APIs",
UseCases = "Public APIs, Browsers, Mobile Apps"
};
var grpcCharacteristics = new
{
Protocol = "HTTP/2",
PayloadFormat = "Protocol Buffers (Binary)",
Communication = "Multiple patterns",
Streaming = "Full support (Client, Server, Bidirectional)",
Performance = "High for service-to-service",
UseCases = "Microservices, Mobile, Real-time"
};
}
}
HTTP/2 Benefits
// HTTP/2 Features that power gRPC
public class Http2Features
{
public List<string> KeyFeatures = new()
{
"Binary Framing: More efficient than text-based HTTP/1.1",
"Multiplexing: Multiple requests over single connection",
"Header Compression: HPACK reduces overhead",
"Server Push: Server can push resources to client",
"Stream Prioritization: Important requests get priority",
"Flow Control: Prevents resource exhaustion"
};
}
3. Building gRPC Services
Complete Product Service Implementation
Project Structure
Microservices/
├── src/
│ ├── ProductService/
│ │ ├── Protos/
│ │ ├── Services/
│ │ ├── Models/
│ │ └── Data/
│ ├── OrderService/
│ ├── InventoryService/
│ └── ApiGateway/
├── contracts/
└── docker-compose.yml
Service Setup and Configuration
// ProductService/Program.cs
using ProductService.Data;
using ProductService.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to container
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.Interceptors.Add<ExceptionInterceptor>();
options.Interceptors.Add<LoggingInterceptor>();
});
builder.Services.AddDbContext<ProductContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("ProductDatabase"));
});
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddSingleton<IServiceDiscovery, ConsulServiceDiscovery>();
// Health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ProductContext>()
.AddUrlGroup(new Uri("http://localhost:5000/health"), "self");
// Distributed caching
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
var app = builder.Build();
// Configure the HTTP request pipeline
app.MapGrpcService<ProductGrpcService>();
app.MapHealthChecks("/health");
// gRPC-Web for browser compatibility
app.MapGrpcService<ProductGrpcService>().EnableGrpcWeb();
app.MapGet("/", () => "Product gRPC Service is running!");
app.Run();
gRPC Service Implementation
// ProductService/Services/ProductGrpcService.cs
using Grpc.Core;
using ProductService.Data;
using ProductService.Models;
using Google.Protobuf.WellKnownTypes;
namespace ProductService.Services;
public class ProductGrpcService : ProductService.Grpc.ProductService.ProductServiceBase
{
private readonly IProductRepository _productRepository;
private readonly ILogger<ProductGrpcService> _logger;
private readonly ICacheService _cacheService;
public ProductGrpcService(
IProductRepository productRepository,
ILogger<ProductGrpcService> logger,
ICacheService cacheService)
{
_productRepository = productRepository;
_logger = logger;
_cacheService = cacheService;
}
public override async Task<ProductResponse> GetProduct(
GetProductRequest request,
ServerCallContext context)
{
_logger.LogInformation("Getting product with ID: {ProductId}", request.ProductId);
// Check cache first
var cacheKey = $"product_{request.ProductId}";
var cachedProduct = await _cacheService.GetAsync<ProductResponse>(cacheKey);
if (cachedProduct != null)
{
_logger.LogDebug("Cache hit for product {ProductId}", request.ProductId);
return cachedProduct;
}
var product = await _productRepository.GetByIdAsync(request.ProductId);
if (product == null)
{
_logger.LogWarning("Product with ID {ProductId} not found", request.ProductId);
throw new RpcException(new Status(StatusCode.NotFound,
$"Product with ID {request.ProductId} not found"));
}
var response = MapToProductResponse(product);
// Cache the response
await _cacheService.SetAsync(cacheKey, response, TimeSpan.FromMinutes(30));
return response;
}
public override async Task<ProductResponse> CreateProduct(
CreateProductRequest request,
ServerCallContext context)
{
_logger.LogInformation("Creating new product: {ProductName}", request.Name);
// Validate request
if (string.IsNullOrWhiteSpace(request.Name))
{
throw new RpcException(new Status(StatusCode.InvalidArgument,
"Product name is required"));
}
if (request.Price <= 0)
{
throw new RpcException(new Status(StatusCode.InvalidArgument,
"Product price must be positive"));
}
var product = new Product
{
Name = request.Name,
Description = request.Description,
Price = (decimal)request.Price,
StockQuantity = request.StockQuantity,
CategoryId = request.CategoryId,
Status = ProductStatus.Active,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
await _productRepository.AddAsync(product);
await _productRepository.SaveChangesAsync();
_logger.LogInformation("Product created with ID: {ProductId}", product.Id);
// Invalidate relevant caches
await _cacheService.RemoveAsync("products_list_*");
return MapToProductResponse(product);
}
public override async Task ListProducts(
ListProductsRequest request,
IServerStreamWriter<ProductResponse> responseStream,
ServerCallContext context)
{
_logger.LogInformation("Streaming products with page size: {PageSize}", request.PageSize);
var pageSize = request.PageSize > 0 ? request.PageSize : 50;
var products = _productRepository.GetAllActiveProducts();
if (!string.IsNullOrEmpty(request.Filter))
{
products = products.Where(p => p.Name.Contains(request.Filter) ||
p.Description.Contains(request.Filter));
}
var count = 0;
foreach (var product in products)
{
if (context.CancellationToken.IsCancellationRequested)
break;
await responseStream.WriteAsync(MapToProductResponse(product));
count++;
// Simulate some delay for demonstration
if (count % 10 == 0)
await Task.Delay(100, context.CancellationToken);
}
_logger.LogInformation("Streamed {Count} products", count);
}
public override async Task<BulkCreateResponse> BulkCreateProducts(
IAsyncStreamReader<CreateProductRequest> requestStream,
ServerCallContext context)
{
_logger.LogInformation("Starting bulk product creation");
var response = new BulkCreateResponse();
var errors = new List<string>();
await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
{
try
{
var product = new Product
{
Name = request.Name,
Description = request.Description,
Price = (decimal)request.Price,
StockQuantity = request.StockQuantity,
CategoryId = request.CategoryId,
Status = ProductStatus.Active,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
await _productRepository.AddAsync(product);
response.CreatedCount++;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create product: {ProductName}", request.Name);
errors.Add($"Failed to create {request.Name}: {ex.Message}");
}
}
if (response.CreatedCount > 0)
{
await _productRepository.SaveChangesAsync();
await _cacheService.RemoveAsync("products_list_*");
}
response.Errors.AddRange(errors);
_logger.LogInformation("Bulk creation completed: {CreatedCount} created, {ErrorCount} errors",
response.CreatedCount, errors.Count);
return response;
}
private ProductResponse MapToProductResponse(Product product)
{
return new ProductResponse
{
ProductId = product.Id,
Name = product.Name,
Description = product.Description ?? string.Empty,
Price = (double)product.Price,
StockQuantity = product.StockQuantity,
CategoryId = product.CategoryId ?? string.Empty,
CreatedAt = Timestamp.FromDateTime(product.CreatedAt),
UpdatedAt = product.UpdatedAt.HasValue ?
Timestamp.FromDateTime(product.UpdatedAt.Value) : null,
Status = MapProductStatus(product.Status)
};
}
private ProductService.Grpc.ProductStatus MapProductStatus(ProductStatus status)
{
return status switch
{
ProductStatus.Active => ProductService.Grpc.ProductStatus.Active,
ProductStatus.Inactive => ProductService.Grpc.ProductStatus.Inactive,
ProductStatus.OutOfStock => ProductService.Grpc.ProductStatus.OutOfStock,
ProductStatus.Discontinued => ProductService.Grpc.ProductStatus.Discontinued,
_ => ProductService.Grpc.ProductStatus.Inactive
};
}
}
gRPC Interceptors
// ProductService/Interceptors/ExceptionInterceptor.cs
public class ExceptionInterceptor : Interceptor
{
private readonly ILogger<ExceptionInterceptor> _logger;
public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger)
{
_logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(request, context);
}
catch (RpcException)
{
throw; // Already handled
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing gRPC call {Method}", context.Method);
// Map to appropriate gRPC status
var status = ex switch
{
ArgumentException => new Status(StatusCode.InvalidArgument, ex.Message),
KeyNotFoundException => new Status(StatusCode.NotFound, ex.Message),
UnauthorizedAccessException => new Status(StatusCode.PermissionDenied, ex.Message),
_ => new Status(StatusCode.Internal, "An internal error occurred")
};
throw new RpcException(status);
}
}
}
// ProductService/Interceptors/LoggingInterceptor.cs
public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var stopwatch = Stopwatch.StartNew();
_logger.LogInformation("Starting gRPC call {Method} from {Peer}",
context.Method, context.Peer);
try
{
var response = await continuation(request, context);
stopwatch.Stop();
_logger.LogInformation("Completed gRPC call {Method} in {ElapsedMs}ms",
context.Method, stopwatch.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "gRPC call {Method} failed after {ElapsedMs}ms",
context.Method, stopwatch.ElapsedMilliseconds);
throw;
}
}
}
4. Advanced gRPC Patterns
Client-Side Implementation
// ProductService client implementation
public class ProductServiceClient : IProductServiceClient, IDisposable
{
private readonly ProductService.Grpc.ProductService.ProductServiceClient _client;
private readonly ChannelBase _channel;
private readonly ILogger<ProductServiceClient> _logger;
public ProductServiceClient(
string serviceUrl,
ILogger<ProductServiceClient> logger)
{
_logger = logger;
// Create channel with configuration
_channel = GrpcChannel.ForAddress(serviceUrl, new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
EnableMultipleHttp2Connections = true
},
ServiceConfig = new ServiceConfig
{
LoadBalancingConfigs = { new RoundRobinConfig() },
MethodConfigs =
{
new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 3,
InitialBackoff = TimeSpan.FromSeconds(1),
MaxBackoff = TimeSpan.FromSeconds(5),
BackoffMultiplier = 1.5,
RetryableStatusCodes = { StatusCode.Unavailable }
}
}
}
}
});
_client = new ProductService.Grpc.ProductService.ProductServiceClient(_channel);
}
public async Task<ProductResponse> GetProductAsync(int productId, CancellationToken cancellationToken = default)
{
try
{
var request = new GetProductRequest { ProductId = productId };
return await _client.GetProductAsync(request, cancellationToken: cancellationToken);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
_logger.LogWarning("Product {ProductId} not found", productId);
throw new ProductNotFoundException(productId, ex);
}
catch (RpcException ex)
{
_logger.LogError(ex, "gRPC error getting product {ProductId}", productId);
throw new ProductServiceException("Failed to get product", ex);
}
}
public async IAsyncEnumerable<ProductResponse> StreamProductsAsync(
int pageSize = 50,
string filter = null)
{
var request = new ListProductsRequest
{
PageSize = pageSize,
Filter = filter ?? string.Empty
};
using var call = _client.ListProducts(request);
await foreach (var product in call.ResponseStream.ReadAllAsync())
{
yield return product;
}
}
public async Task<BulkCreateResponse> BulkCreateProductsAsync(
IAsyncEnumerable<CreateProductRequest> products,
CancellationToken cancellationToken = default)
{
using var call = _client.BulkCreateProducts(cancellationToken: cancellationToken);
await foreach (var product in products)
{
await call.RequestStream.WriteAsync(product);
}
await call.RequestStream.CompleteAsync();
return await call.ResponseAsync;
}
public void Dispose()
{
_channel?.Dispose();
}
}
Bidirectional Streaming Example
// Real-time inventory update service
public class InventoryGrpcService : InventoryService.Grpc.InventoryService.InventoryServiceBase
{
private readonly IInventoryRepository _inventoryRepository;
private readonly ILogger<InventoryGrpcService> _logger;
public InventoryGrpcService(
IInventoryRepository inventoryRepository,
ILogger<InventoryGrpcService> logger)
{
_inventoryRepository = inventoryRepository;
_logger = logger;
}
public override async Task StreamInventoryUpdates(
IAsyncStreamReader<InventoryUpdateRequest> requestStream,
IServerStreamWriter<InventoryUpdateResponse> responseStream,
ServerCallContext context)
{
_logger.LogInformation("Starting bidirectional inventory stream");
// Read requests and send responses concurrently
var readTask = ReadRequestsAsync(requestStream, context.CancellationToken);
var writeTask = WriteResponsesAsync(responseStream, context.CancellationToken);
await Task.WhenAll(readTask, writeTask);
}
private async Task ReadRequestsAsync(
IAsyncStreamReader<InventoryUpdateRequest> requestStream,
CancellationToken cancellationToken)
{
await foreach (var request in requestStream.ReadAllAsync(cancellationToken))
{
try
{
_logger.LogDebug("Processing inventory update for product {ProductId}",
request.ProductId);
// Process inventory update
await _inventoryRepository.UpdateStockAsync(
request.ProductId,
request.QuantityChange,
request.Reason);
// You could also broadcast this update to other connected clients
await BroadcastInventoryUpdate(request.ProductId, request.QuantityChange);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process inventory update for product {ProductId}",
request.ProductId);
}
}
}
private async Task WriteResponsesAsync(
IServerStreamWriter<InventoryUpdateResponse> responseStream,
CancellationToken cancellationToken)
{
// Simulate sending periodic inventory summaries
var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
try
{
var lowStockItems = await _inventoryRepository.GetLowStockItemsAsync(threshold: 10);
var response = new InventoryUpdateResponse
{
Summary = new InventorySummary
{
Timestamp = Timestamp.FromDateTime(DateTime.UtcNow),
LowStockItemCount = lowStockItems.Count,
TotalProducts = await _inventoryRepository.GetTotalProductCountAsync()
}
};
response.LowStockItems.AddRange(lowStockItems.Select(item =>
new LowStockItem
{
ProductId = item.ProductId,
ProductName = item.ProductName,
CurrentStock = item.CurrentStock,
MinimumRequired = item.MinimumRequired
}));
await responseStream.WriteAsync(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send inventory summary");
}
}
}
private async Task BroadcastInventoryUpdate(int productId, int quantityChange)
{
// Implementation for broadcasting to other connected clients
// This could use Redis pub/sub or other mechanisms
}
}
gRPC Health Checks
// Custom health check implementation
public class ProductServiceHealthCheck : HealthCheck.HealthBase
{
private readonly ProductContext _dbContext;
private readonly ILogger<ProductServiceHealthCheck> _logger;
public ProductServiceHealthCheck(
ProductContext dbContext,
ILogger<ProductServiceHealthCheck> logger)
{
_dbContext = dbContext;
_logger = logger;
}
public override async Task<HealthCheckResponse> Check(
HealthCheckRequest request,
ServerCallContext context)
{
var status = HealthCheckResponse.Types.ServingStatus.Serving;
var checks = new Dictionary<string, string>();
// Check database connectivity
try
{
await _dbContext.Database.CanConnectAsync(context.CancellationToken);
checks["database"] = "healthy";
}
catch (Exception ex)
{
status = HealthCheckResponse.Types.ServingStatus.NotServing;
checks["database"] = $"unhealthy: {ex.Message}";
_logger.LogError(ex, "Database health check failed");
}
// Check memory usage
var memoryUsage = GC.GetTotalMemory(forceFullCollection: false) / 1024 / 1024;
checks["memory_mb"] = memoryUsage.ToString();
if (memoryUsage > 500) // 500MB threshold
{
status = HealthCheckResponse.Types.ServingStatus.NotServing;
checks["memory"] = "high_usage";
}
return new HealthCheckResponse
{
Status = status
};
}
}
5. Message Brokers with RabbitMQ
RabbitMQ Fundamentals
// RabbitMQ configuration and setup
public static class RabbitMQExtensions
{
public static IServiceCollection AddRabbitMQ(this IServiceCollection services, IConfiguration configuration)
{
var rabbitMQConfig = configuration.GetSection("RabbitMQ").Get<RabbitMQConfig>();
services.AddSingleton(rabbitMQConfig);
// Connection factory
services.AddSingleton<IConnectionFactory>(sp =>
{
return new ConnectionFactory
{
HostName = rabbitMQConfig.HostName,
Port = rabbitMQConfig.Port,
UserName = rabbitMQConfig.UserName,
Password = rabbitMQConfig.Password,
VirtualHost = rabbitMQConfig.VirtualHost,
DispatchConsumersAsync = true,
AutomaticRecoveryEnabled = true,
NetworkRecoveryInterval = TimeSpan.FromSeconds(10)
};
});
// Connection
services.AddSingleton<IConnection>(sp =>
{
var factory = sp.GetRequiredService<IConnectionFactory>();
return factory.CreateConnection();
});
// Channel
services.AddSingleton<IModel>(sp =>
{
var connection = sp.GetRequiredService<IConnection>();
var channel = connection.CreateModel();
// Configure exchanges and queues
ConfigureInfrastructure(channel, rabbitMQConfig);
return channel;
});
services.AddScoped<IMessagePublisher, RabbitMQMessagePublisher>();
services.AddHostedService<OrderCreatedEventConsumer>();
return services;
}
private static void ConfigureInfrastructure(IModel channel, RabbitMQConfig config)
{
// Exchanges
channel.ExchangeDeclare("order-events", ExchangeType.Topic, durable: true, autoDelete: false);
channel.ExchangeDeclare("inventory-events", ExchangeType.Topic, durable: true, autoDelete: false);
channel.ExchangeDeclare("dead-letter", ExchangeType.Fanout, durable: true, autoDelete: false);
// Queues
channel.QueueDeclare("order.created", durable: true, exclusive: false, autoDelete: false);
channel.QueueDeclare("inventory.update", durable: true, exclusive: false, autoDelete: false);
channel.QueueDeclare("dead-letter-queue", durable: true, exclusive: false, autoDelete: false);
// Dead letter queue
var deadLetterArgs = new Dictionary<string, object>
{
{ "x-dead-letter-exchange", "dead-letter" },
{ "x-message-ttl", 30000 } // 30 seconds
};
channel.QueueDeclare("order.created.retry", durable: true, exclusive: false, autoDelete: false, arguments: deadLetterArgs);
// Bindings
channel.QueueBind("order.created", "order-events", "order.created");
channel.QueueBind("inventory.update", "inventory-events", "inventory.updated");
channel.QueueBind("dead-letter-queue", "dead-letter", "");
channel.QueueBind("order.created.retry", "order-events", "order.created");
}
}
public class RabbitMQConfig
{
public string HostName { get; set; } = "localhost";
public int Port { get; set; } = 5672;
public string UserName { get; set; } = "guest";
public string Password { get; set; } = "guest";
public string VirtualHost { get; set; } = "/";
}
Message Publisher Implementation
// Message publisher service
public class RabbitMQMessagePublisher : IMessagePublisher
{
private readonly IModel _channel;
private readonly ILogger<RabbitMQMessagePublisher> _logger;
private readonly ISerializer _serializer;
public RabbitMQMessagePublisher(
IModel channel,
ILogger<RabbitMQMessagePublisher> logger,
ISerializer serializer)
{
_channel = channel;
_logger = logger;
_serializer = serializer;
}
public async Task PublishAsync<T>(T message, string exchange, string routingKey) where T : class
{
try
{
var body = _serializer.SerializeToBytes(message);
var properties = _channel.CreateBasicProperties();
properties.Persistent = true;
properties.ContentType = "application/json";
properties.MessageId = Guid.NewGuid().ToString();
properties.Timestamp = new AmqpTimestamp(DateTimeOffset.UtcNow.ToUnixTimeSeconds());
_channel.BasicPublish(
exchange: exchange,
routingKey: routingKey,
mandatory: true,
basicProperties: properties,
body: body);
_logger.LogDebug("Published message to {Exchange} with routing key {RoutingKey}",
exchange, routingKey);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to publish message to {Exchange} with routing key {RoutingKey}",
exchange, routingKey);
throw;
}
}
public async Task PublishWithRetryAsync<T>(T message, string exchange, string routingKey, int maxRetries = 3)
where T : class
{
var retryCount = 0;
while (retryCount < maxRetries)
{
try
{
await PublishAsync(message, exchange, routingKey);
return;
}
catch (Exception ex) when (retryCount < maxRetries - 1)
{
retryCount++;
_logger.LogWarning(ex,
"Publish failed, retry {RetryCount}/{MaxRetries} after delay",
retryCount, maxRetries);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
}
}
throw new MessagePublishException($"Failed to publish message after {maxRetries} retries");
}
}

Join the conversation! Your thoughts help the community grow.