521 lines
15 KiB
Markdown
521 lines
15 KiB
Markdown
# 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
|
|
/// <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
|
|
|
|
```json
|
|
{
|
|
"RabbitMQ": {
|
|
"Host": "localhost",
|
|
"VirtualHost": "/",
|
|
"Username": "guest",
|
|
"Password": "guest"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Integration Events
|
|
|
|
### Event Definitions
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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)
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
```csharp
|
|
/// <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
|
|
|
|
- [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/)
|