Files
pos-system/microservices/.agent/skills/inter-service-communication/references/REFERENCE.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

15 KiB

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
  2. Integration Events
  3. Event Consumers
  4. HTTP Client Patterns
  5. Outbox Pattern
  6. gRPC Setup

MassTransit Setup

Complete MassTransit Configuration

/// <summary>
/// EN: Configure MassTransit with RabbitMQ for production.
/// VI: Cấu hình MassTransit với RabbitMQ cho production.
/// </summary>

// 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<OrderStateMachine, OrderState>()
    //     .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<ValidationException>();
        });

        // 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

{
  "RabbitMQ": {
    "Host": "localhost",
    "VirtualHost": "/",
    "Username": "guest",
    "Password": "guest"
  }
}

Integration Events

Event Definitions

/// <summary>
/// EN: Integration event base record.
/// VI: Integration event record cơ sở.
/// </summary>
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;
}

/// <summary>
/// EN: Event when order status changes.
/// VI: Event khi trạng thái order thay đổi.
/// </summary>
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!;
}

/// <summary>
/// EN: Event when product price changes.
/// VI: Event khi giá sản phẩm thay đổi.
/// </summary>
public record ProductPriceChangedIntegrationEvent : IntegrationEvent
{
    public Guid ProductId { get; init; }
    public decimal OldPrice { get; init; }
    public decimal NewPrice { get; init; }
}

/// <summary>
/// EN: Event when payment is completed.
/// VI: Event khi thanh toán hoàn thành.
/// </summary>
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

/// <summary>
/// EN: Service to publish integration events.
/// VI: Service publish integration events.
/// </summary>
public interface IEventPublisher
{
    Task PublishAsync<T>(T @event, CancellationToken ct = default) where T : class;
}

public class MassTransitEventPublisher : IEventPublisher
{
    private readonly IPublishEndpoint _publishEndpoint;
    private readonly ILogger<MassTransitEventPublisher> _logger;

    public MassTransitEventPublisher(
        IPublishEndpoint publishEndpoint,
        ILogger<MassTransitEventPublisher> logger)
    {
        _publishEndpoint = publishEndpoint;
        _logger = logger;
    }

    public async Task PublishAsync<T>(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

/// <summary>
/// EN: Idempotent consumer for payment completed events.
/// VI: Consumer idempotent cho payment completed events.
/// </summary>
public class PaymentCompletedConsumer : IConsumer<PaymentCompletedIntegrationEvent>
{
    private readonly IOrderService _orderService;
    private readonly IIdempotencyService _idempotency;
    private readonly ILogger<PaymentCompletedConsumer> _logger;

    public PaymentCompletedConsumer(
        IOrderService orderService,
        IIdempotencyService idempotency,
        ILogger<PaymentCompletedConsumer> logger)
    {
        _orderService = orderService;
        _idempotency = idempotency;
        _logger = logger;
    }

    public async Task Consume(ConsumeContext<PaymentCompletedIntegrationEvent> 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)

/// <summary>
/// EN: Consumer definition with custom retry and concurrency.
/// VI: Consumer definition với retry và concurrency tùy chỉnh.
/// </summary>
public class PaymentCompletedConsumerDefinition 
    : ConsumerDefinition<PaymentCompletedConsumer>
{
    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<PaymentCompletedConsumer> 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<OrderNotFoundException>();
        });

        // 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

/// <summary>
/// EN: Typed HTTP client for IAM service.
/// VI: Typed HTTP client cho IAM service.
/// </summary>
public interface IIamServiceClient
{
    Task<UserInfoDto?> GetUserAsync(string userId, CancellationToken ct = default);
    Task<bool> ValidateTokenAsync(string token, CancellationToken ct = default);
}

public class IamServiceClient : IIamServiceClient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<IamServiceClient> _logger;

    public IamServiceClient(
        HttpClient httpClient,
        ILogger<IamServiceClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<UserInfoDto?> 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<ApiResponse<UserInfoDto>>(ct);
            return result?.Data;
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to get user {UserId} from IAM service", userId);
            throw;
        }
    }

    public async Task<bool> 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

/// <summary>
/// EN: Register resilient HTTP clients.
/// VI: Đăng ký HTTP clients có khả năng phục hồi.
/// </summary>

builder.Services.AddHttpClient<IIamServiceClient, IamServiceClient>(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<HttpResponseMessage>()
            .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
            .Handle<HttpRequestException>()
            .Handle<TimeoutRejectedException>()
    });

    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

/// <summary>
/// EN: Outbox message entity for reliable event publishing.
/// VI: Outbox message entity cho event publishing đáng tin cậy.
/// </summary>
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<OutboxMessage>(entity =>
{
    entity.ToTable("OutboxMessages");
    entity.HasKey(e => e.Id);
    entity.HasIndex(e => e.ProcessedOn);
});

Outbox Processor

/// <summary>
/// EN: Background service to process outbox messages.
/// VI: Background service xử lý outbox messages.
/// </summary>
public class OutboxProcessor : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<OutboxProcessor> _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<ApplicationDbContext>();
        var publisher = scope.ServiceProvider.GetRequiredService<IPublishEndpoint>();

        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