Migrate
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
# 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/)
|
||||
Reference in New Issue
Block a user