# Inter-Service Communication - Detailed Reference Detailed code examples cho giao tiếp liên dịch vụ trong GoodGo. ## Table of Contents 1. [MassTransit Setup](#masstransit-setup) 2. [Integration Events](#integration-events) 3. [Event Consumers](#event-consumers) 4. [HTTP Client Patterns](#http-client-patterns) 5. [Outbox Pattern](#outbox-pattern) 6. [gRPC Setup](#grpc-setup) --- ## MassTransit Setup ### Complete MassTransit Configuration ```csharp /// /// EN: Configure MassTransit with RabbitMQ for production. /// VI: Cấu hình MassTransit với RabbitMQ cho production. /// // Program.cs builder.Services.AddMassTransit(x => { // EN: Register all consumers from assembly // VI: Đăng ký tất cả consumers từ assembly x.AddConsumers(typeof(Program).Assembly); // EN: Configure saga if needed // x.AddSagaStateMachine() // .EntityFrameworkRepository(...); x.UsingRabbitMq((context, cfg) => { var rabbitConfig = builder.Configuration.GetSection("RabbitMQ"); cfg.Host(rabbitConfig["Host"], rabbitConfig["VirtualHost"] ?? "/", h => { h.Username(rabbitConfig["Username"]!); h.Password(rabbitConfig["Password"]!); }); // EN: Global retry policy // VI: Retry policy toàn cục cfg.UseMessageRetry(r => { r.Incremental(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2)); r.Ignore(); }); // EN: Dead letter queue for failed messages // VI: Dead letter queue cho messages thất bại cfg.UseDelayedRedelivery(r => r.Intervals( TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); // EN: Configure endpoints // VI: Cấu hình endpoints cfg.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter("goodgo", false)); }); }); ``` ### appsettings.json ```json { "RabbitMQ": { "Host": "localhost", "VirtualHost": "/", "Username": "guest", "Password": "guest" } } ``` --- ## Integration Events ### Event Definitions ```csharp /// /// EN: Integration event base record. /// VI: Integration event record cơ sở. /// public abstract record IntegrationEvent { public Guid Id { get; init; } = Guid.NewGuid(); public DateTime OccurredOn { get; init; } = DateTime.UtcNow; public string CorrelationId { get; init; } = string.Empty; } /// /// EN: Event when order status changes. /// VI: Event khi trạng thái order thay đổi. /// public record OrderStatusChangedIntegrationEvent : IntegrationEvent { public Guid OrderId { get; init; } public string PreviousStatus { get; init; } = default!; public string NewStatus { get; init; } = default!; public string UserId { get; init; } = default!; } /// /// EN: Event when product price changes. /// VI: Event khi giá sản phẩm thay đổi. /// public record ProductPriceChangedIntegrationEvent : IntegrationEvent { public Guid ProductId { get; init; } public decimal OldPrice { get; init; } public decimal NewPrice { get; init; } } /// /// EN: Event when payment is completed. /// VI: Event khi thanh toán hoàn thành. /// public record PaymentCompletedIntegrationEvent : IntegrationEvent { public Guid PaymentId { get; init; } public Guid OrderId { get; init; } public decimal Amount { get; init; } public string PaymentMethod { get; init; } = default!; } ``` ### Event Publisher Service ```csharp /// /// EN: Service to publish integration events. /// VI: Service publish integration events. /// public interface IEventPublisher { Task PublishAsync(T @event, CancellationToken ct = default) where T : class; } public class MassTransitEventPublisher : IEventPublisher { private readonly IPublishEndpoint _publishEndpoint; private readonly ILogger _logger; public MassTransitEventPublisher( IPublishEndpoint publishEndpoint, ILogger logger) { _publishEndpoint = publishEndpoint; _logger = logger; } public async Task PublishAsync(T @event, CancellationToken ct = default) where T : class { try { await _publishEndpoint.Publish(@event, ct); _logger.LogInformation( "Published event {EventType}: {@Event}", typeof(T).Name, @event); } catch (Exception ex) { _logger.LogError(ex, "Failed to publish event {EventType}: {@Event}", typeof(T).Name, @event); throw; } } } ``` --- ## Event Consumers ### Consumer with Idempotency ```csharp /// /// EN: Idempotent consumer for payment completed events. /// VI: Consumer idempotent cho payment completed events. /// public class PaymentCompletedConsumer : IConsumer { private readonly IOrderService _orderService; private readonly IIdempotencyService _idempotency; private readonly ILogger _logger; public PaymentCompletedConsumer( IOrderService orderService, IIdempotencyService idempotency, ILogger logger) { _orderService = orderService; _idempotency = idempotency; _logger = logger; } public async Task Consume(ConsumeContext context) { var @event = context.Message; // EN: Check if already processed // VI: Kiểm tra đã xử lý chưa if (await _idempotency.HasBeenProcessedAsync(@event.Id)) { _logger.LogInformation( "Event {EventId} already processed, skipping", @event.Id); return; } try { _logger.LogInformation( "Processing PaymentCompleted for Order {OrderId}", @event.OrderId); await _orderService.ConfirmPaymentAsync( @event.OrderId, @event.PaymentId, @event.Amount, context.CancellationToken); // EN: Mark as processed // VI: Đánh dấu đã xử lý await _idempotency.MarkAsProcessedAsync(@event.Id); _logger.LogInformation( "Successfully processed PaymentCompleted for Order {OrderId}", @event.OrderId); } catch (Exception ex) { _logger.LogError(ex, "Failed to process PaymentCompleted for Order {OrderId}", @event.OrderId); throw; // EN: Will trigger retry } } } ``` ### Consumer Definition (Advanced Config) ```csharp /// /// EN: Consumer definition with custom retry and concurrency. /// VI: Consumer definition với retry và concurrency tùy chỉnh. /// public class PaymentCompletedConsumerDefinition : ConsumerDefinition { public PaymentCompletedConsumerDefinition() { // EN: Endpoint name // VI: Tên endpoint EndpointName = "order-service-payment-completed"; // EN: Prefetch count for concurrency // VI: Prefetch count cho concurrency ConcurrentMessageLimit = 10; } protected override void ConfigureConsumer( IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { // EN: Custom retry for this consumer // VI: Retry tùy chỉnh cho consumer này endpointConfigurator.UseMessageRetry(r => { r.Intervals( TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(5)); r.Ignore(); }); // EN: Circuit breaker // VI: Circuit breaker endpointConfigurator.UseCircuitBreaker(cb => { cb.TrackingPeriod = TimeSpan.FromMinutes(1); cb.TripThreshold = 15; cb.ActiveThreshold = 10; cb.ResetInterval = TimeSpan.FromMinutes(5); }); } } ``` --- ## HTTP Client Patterns ### Typed HTTP Client ```csharp /// /// EN: Typed HTTP client for IAM service. /// VI: Typed HTTP client cho IAM service. /// public interface IIamServiceClient { Task GetUserAsync(string userId, CancellationToken ct = default); Task ValidateTokenAsync(string token, CancellationToken ct = default); } public class IamServiceClient : IIamServiceClient { private readonly HttpClient _httpClient; private readonly ILogger _logger; public IamServiceClient( HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } public async Task GetUserAsync(string userId, CancellationToken ct = default) { try { var response = await _httpClient.GetAsync($"/api/v1/users/{userId}", ct); if (response.StatusCode == HttpStatusCode.NotFound) return null; response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync>(ct); return result?.Data; } catch (HttpRequestException ex) { _logger.LogError(ex, "Failed to get user {UserId} from IAM service", userId); throw; } } public async Task ValidateTokenAsync(string token, CancellationToken ct = default) { try { var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/auth/validate"); request.Content = JsonContent.Create(new { Token = token }); var response = await _httpClient.SendAsync(request, ct); return response.IsSuccessStatusCode; } catch (HttpRequestException ex) { _logger.LogWarning(ex, "Token validation failed"); return false; } } } ``` ### HTTP Client Registration with Polly ```csharp /// /// EN: Register resilient HTTP clients. /// VI: Đăng ký HTTP clients có khả năng phục hồi. /// builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(builder.Configuration["Services:Iam:BaseUrl"]!); client.Timeout = TimeSpan.FromSeconds(30); }) .AddResilienceHandler("iam-client", builder => { builder.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3, Delay = TimeSpan.FromMilliseconds(300), BackoffType = DelayBackoffType.Exponential, UseJitter = true, ShouldHandle = new PredicateBuilder() .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError) .Handle() .Handle() }); builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions { FailureRatio = 0.3, SamplingDuration = TimeSpan.FromSeconds(60), MinimumThroughput = 10, BreakDuration = TimeSpan.FromSeconds(30) }); builder.AddTimeout(TimeSpan.FromSeconds(10)); }); ``` --- ## Outbox Pattern ### Outbox Entity ```csharp /// /// EN: Outbox message entity for reliable event publishing. /// VI: Outbox message entity cho event publishing đáng tin cậy. /// public class OutboxMessage { public Guid Id { get; set; } public string Type { get; set; } = default!; public string Payload { get; set; } = default!; public DateTime OccurredOn { get; set; } public DateTime? ProcessedOn { get; set; } public string? Error { get; set; } public int RetryCount { get; set; } } // EN: DbContext configuration // VI: Cấu hình DbContext modelBuilder.Entity(entity => { entity.ToTable("OutboxMessages"); entity.HasKey(e => e.Id); entity.HasIndex(e => e.ProcessedOn); }); ``` ### Outbox Processor ```csharp /// /// EN: Background service to process outbox messages. /// VI: Background service xử lý outbox messages. /// public class OutboxProcessor : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly TimeSpan _pollingInterval = TimeSpan.FromSeconds(5); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await ProcessPendingMessagesAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "Error processing outbox messages"); } await Task.Delay(_pollingInterval, stoppingToken); } } private async Task ProcessPendingMessagesAsync(CancellationToken ct) { using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var publisher = scope.ServiceProvider.GetRequiredService(); var messages = await dbContext.OutboxMessages .Where(m => m.ProcessedOn == null && m.RetryCount < 3) .OrderBy(m => m.OccurredOn) .Take(100) .ToListAsync(ct); foreach (var message in messages) { try { var eventType = Type.GetType(message.Type)!; var @event = JsonSerializer.Deserialize(message.Payload, eventType)!; await publisher.Publish(@event, eventType, ct); message.ProcessedOn = DateTime.UtcNow; _logger.LogDebug("Processed outbox message {Id}", message.Id); } catch (Exception ex) { message.RetryCount++; message.Error = ex.Message; _logger.LogWarning(ex, "Failed to process outbox message {Id}", message.Id); } } await dbContext.SaveChangesAsync(ct); } } ``` --- ## Resources / Tài Nguyên - [MassTransit Documentation](https://masstransit.io/) - [RabbitMQ Documentation](https://www.rabbitmq.com/documentation.html) - [Polly Documentation](https://github.com/App-vNext/Polly) - [gRPC for .NET](https://docs.microsoft.com/en-us/aspnet/core/grpc/)