本文将重点讲解如何将 Brighter 与 AWS SNS/SQS 集成。
AWS SNS/SQS 简介
AWS SNS(Simple Notification Service)和 SQS(Simple Queue Service)构成了分布式系统中强大的消息传递基础架构。SNS 支持发布/订阅模式的消息分发(支持多消费者),而 SQS 提供基于队列的消息缓冲,确保可靠的消息处理。通过将这些服务与 Brighter(一个用于命令/查询处理和消息传递的 .NET 库)结合使用,可以构建具备高弹性和解耦特性的大规模系统。
本指南将逐步演示如何配置 Brighter 使用 SNS 发布事件并通过 SQS 消费事件,并附带实用代码示例及当前限制的解决方法。
环境要求
1. .NET 8 或更高版本
2. .NET 项目需引用以下 NuGet 包:
- Paramore.Brighter.MessagingGateway.AWSSQS:启用 AWS SNS/SQS 集成。
- Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection:连接 Brighter 与 AWS 消息服务。
- Paramore.Brighter.ServiceActivator.Extensions.Hosting:将 Brighter 托管为后台服务。
- Serilog.AspNetCore:结构化日志记录(可选但推荐)。
Brighter 快速回顾
在继续 AWS SNS/SQS 配置前,先回顾一下 Brighter 的核心概念。
请求(Command/Event)
使用 IRequest 定义消息:
public class Greeting : Event(Guid.NewGuid())
{
public string Name { get; set; } = string.Empty;
}
- 命令(Commands):单接收者操作(如 `SendEmail`)。
- 事件(Events):广播通知(如 `OrderShipped`)。
消息映射器(Message Mapper)
在 Brighter 消息与应用对象之间进行转换:
public class GreetingMapper : IAmAMessageMapper<Greeting>
{
public Message MapToMessage(Greeting request)
{
var header = new MessageHeader();
header.Id = request.Id;
header.TimeStamp = DateTime.UtcNow;
header.Topic = "greeting.topic"; // 目标主题名称
header.MessageType = MessageType.MT_EVENT;
var body = new MessageBody(JsonSerializer.Serialize(request));
return new Message(header, body);
}
public Greeting MapToRequest(Message message)
{
return JsonSerializer.Deserialize<Greeting>(message.Body.Bytes)!;
}
}
请求处理器(Request Handler)
处理传入的消息:
public class GreetingHandler(ILogger<GreetingHandler> logger) : RequestHandler<Greeting>
{
public override Greeting Handle(Greeting command)
{
logger.LogInformation("Hello {Name}", command.Name);
return base.Handle(command);
}
}
配置 Brighter 与 AWS SNS/SQS
连接设置
定义连接参数:
var connection = new AWSMessagingGatewayConnection(
FallbackCredentialsFactory.GetCredentials(), // AWS 凭证
AWSConfigs.RegionEndpoint, // 如:RegionEndpoint.USWest2
opt => { // 自定义 AWS 客户端设置 });
SQS 消费者配置
订阅 SQS 队列并将其映射到 SNS 主题:
.AddServiceActivator(opt =>
{
opt.Subscriptions = [
new SqsSubscription<Greeting>(
subscriptionName: new SubscriptionName("greeting-subscription"), // 可选:用于日志记录
channelName: new ChannelName("greeting-queue"), // SQS 队列名称
routingKey: new RoutingKey("greeting.topic").ToValidSNSTopicName(), // SNS 主题名称
bufferSize: 2 // 并发处理的消息数量
)
];
opt.ChannelFactory = new ChannelFactory(connection);
});
SNS 生产者配置
向 SNS 发布事件:
.UseExternalBus(new SnsProducerRegistryFactory(connection, new []
{
new SnsPublication
{
Topic = new RoutingKey("greeting.topic".ToValidSNSTopicName()),
MakeChannels = OnMissingChannel.Create
}
}).Create());
已知限制(Brighter v9)
- 强制依赖 SNS:所有消息必须通过 SNS 路由,即使仅使用 SQS。
- 不支持 FIFO:v9 不支持 FIFO 队列或主题(用于有序处理)。
- LocalStack 兼容性问题:自定义 AWS 客户端配置(如 LocalStack 端点)可能无法传播到 Brighter 的所有内部组件。
好消息:Brighter v10(正在开发中)将解决这些问题。
最佳实践
验证 SNS 主题名称
指定 SNS 主题时始终使用 `.ToValidSNSTopicName()` 方法。该方法确保主题名称符合 AWS 命名规则(仅允许字母数字字符、连字符和句点)。
new RoutingKey("greeting.topic".ToValidSNSTopicName()) // 转换为有效的 SNS 主题名称
配置死信队列(DLQ)*
通过设置死信队列(DLQ)提高容错能力。在 `SqsSubscription` 中使用 `redrivePolicy` 参数指定:
- DLQ 名称:用于存储失败消息的 SQS 队列名称。
- 最大重试次数:消息被移至 DLQ 前的重试次数。
new SqsSubscription<Greeting>(
subscriptionName: new SubscriptionName("greeting-subscription"),
channelName: new ChannelName("greeting-queue"),
routingKey: new RoutingKey("greeting.topic").ToValidSNSTopicName(),
bufferSize: 2,
redrivePolicy: new RedrivePolicy(
deadLetterQueueName: "greeting-dead", // DLQ 名称
maxReceiveCount: 3 // 重试阈值
)
)
结论
将 Brighter 与 AWS SNS/SQS 集成,可以将 Brighter 库的简单性与 AWS 弹性消息传递基础设施相结合。尽管 v9 存在一些限制,但其核心功能仍然能够实现:
- 解耦架构:生产者和消费者独立演化。
- 弹性:SQS 在流量高峰期间缓冲消息。
- 可扩展性:SNS/SQS 自动扩展以应对工作负载。
参考资料
完整代码:GitHub 仓库]
using System.Text.Json;
using Amazon;
using Amazon.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;
using Paramore.Brighter.MessagingGateway.AWSSQS;
using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection;
using Paramore.Brighter.ServiceActivator.Extensions.Hosting;
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
var host = new HostBuilder()
.UseSerilog()
.ConfigureServices((_, services) =>
{
var connection = new AWSMessagingGatewayConnection(FallbackCredentialsFactory.GetCredentials(), AWSConfigs.RegionEndpoint);
services
.AddHostedService<ServiceActivatorHostedService>()
.AddServiceActivator(opt =>
{
opt.Subscriptions = [
new SqsSubscription<Greeting>(
new SubscriptionName("greeting-subscription"), // Optional
new ChannelName("greeting-queue"), // SQS queue name
new RoutingKey("greeting.topic".ToValidSNSTopicName()), // SNS Topic Name
bufferSize: 2)
];
opt.ChannelFactory = new ChannelFactory(connection);
})
.AutoFromAssemblies()
.UseExternalBus(new SnsProducerRegistryFactory(connection, new []
{
new SnsPublication
{
Topic = new RoutingKey("greeting.topic".ToValidSNSTopicName()),
MakeChannels = OnMissingChannel.Create
}
}).Create());
})
.Build();
_ = host.RunAsync();
while (true)
{
await Task.Delay(TimeSpan.FromSeconds(10));
Console.Write("Say your name (or q to quit): ");
var name = Console.ReadLine();
if (string.IsNullOrEmpty(name))
{
continue;
}
if (name == "q")
{
break;
}
var process = host.Services.GetRequiredService<IAmACommandProcessor>();
process.Post(new Greeting { Name = name });
}
host.WaitForShutdown();
public class Greeting() : Event(Guid.NewGuid())
{
public string Name { get; set; } = string.Empty;
}
public class GreetingMapper : IAmAMessageMapper<Greeting>
{
public Message MapToMessage(Greeting request)
{
var header = new MessageHeader();
header.Id = request.Id;
header.TimeStamp = DateTime.UtcNow;
header.Topic = "greeting.topic";
header.MessageType = MessageType.MT_EVENT;
var body = new MessageBody(JsonSerializer.Serialize(request));
return new Message(header, body);
}
public Greeting MapToRequest(Message message)
{
return JsonSerializer.Deserialize<Greeting>(message.Body.Bytes)!;
}
}
public class GreetingHandler(ILogger<GreetingHandler> logger) : RequestHandler<Greeting>
{
public override Greeting Handle(Greeting command)
{
logger.LogInformation("Hello {Name}", command.Name);
return base.Handle(command);
}
}
871

被折叠的 条评论
为什么被折叠?



