在前文中,我们探讨了 Brighter 与 AWS SNS/SQS 的集成以及 Brighter V10 RC1。本文重点介绍迁移到 Brighter V10 的关键步骤,聚焦 AWS SNS/SQS 配置变更与破坏性更新。
Brighter V10 针对 AWS SNS/SQS 的新特性
V10 版本带来重大增强:
- 直接 SQS 支持:无需 SNS 即可直接发布/消费 SQS 消息
- FIFO 支持:完整兼容 SNS/SQS FIFO 队列
- LocalStack 集成:优化本地 AWS 环境模拟支持
环境要求
.NET 8 或更高版本
安装以下 NuGet 包:
- Paramore.Brighter.MessagingGateway.AWSSQS:实现 AWS SNS/SQS 集成
- Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection:注册 Brighter 到 Microsoft DI
- Paramore.Brighter.ServiceActivator.Extensions.Hosting:将 Brighter 作为后台服务托管
- Serilog.AspNetCore:结构化日志(可选但推荐)
Brighter 核心概念回顾
请求(命令/事件)
使用 IRequest 定义消息:
public class Greeting() : Event(Guid.NewGuid())
{
public string Name { get; set; } = string.Empty;
}
- 命令 (Command):单接收方操作(如 SendEmail)
- 事件 (Event):广播通知(如 OrderShipped)
消息映射器(可选)
在 Brighter 消息与应用对象间转换:
public class SqsFifoMapper : IAmAMessageMapperAsync<SqsFifoEvent>
{
public Task<Message> MapToMessageAsync(SqsFifoEvent request, Publication publication,
CancellationToken cancellationToken = new CancellationToken())
{
return Task.FromResult(new Message(new MessageHeader
{
MessageId = request.Id,
Topic = publication.Topic!,
// 重点:FIFO 需设置 PartitionKey
PartitionKey = request.PartitionValue,
MessageType = MessageType.MT_EVENT,
TimeStamp = DateTimeOffset.UtcNow
},
new MessageBody(JsonSerializer.SerializeToUtf8Bytes(request, JsonSerialisationOptions.Options))));
}
public Task<SqsFifoEvent> MapToRequestAsync(Message message, CancellationToken cancellationToken = new CancellationToken())
{
return Task.FromResult(JsonSerializer.Deserialize<SqsFifoEvent>(message.Body.Bytes, JsonSerialisationOptions.Options)!);
}
public IRequestContext? Context { get; set; }
}
V10 变更:
- 异步管道需实现 IAmAMessageMapperAsync
- FIFO 场景必须通过自定义映射器设置分区键
请求处理器
处理消息的核心逻辑:
public class GreetingHandler(ILogger<GreetingHandler> logger) : RequestHandler<Greeting>
{
public override Greeting Handle(Greeting command)
{
logger.LogInformation("Hello {Name}", command.Name);
// 可继续发布新事件
}
}
配置 Brighter 使用 AWS SNS/SQS
1. 连接设置
var connection = new AWSMessagingGatewayConnection(
new BasicAWSCredentials("test", "test"),
RegionEndpoint.USEast1,
cfg => cfg.ServiceURL = "http://localhost:4566" // LocalStack
);
2. SQS 订阅配置
支持三种模式:
.Subscriptions = [
// 模式 ①: SNS → SQS (Pub/Sub)
new SqsSubscription<Greeting>(
topic: "greeting.topic".ToValidSNSTopicName(),
channelType: ChannelType.PubSub, // 必须为 PubSub
...),
// 模式 ②: SQS → SQS (点对点)
new SqsSubscription<Farewell>(
topic: new RoutingKey("farewell.queue".ToValidSQSQueueName()),
channelType: ChannelType.PointToPoint, // 必须为 PointToPoint
...),
// 模式 ③: FIFO SNS → SQS
new SqsSubscription<SnsFifoEvent>(
topicAttributes: new SnsAttributes { Type = SqsType.Fifo }, // 启用 FIFO
queueAttributes: new SqsAttributes(type: SqsType.Fifo), // 启用 FIFO
...)
]
3. 生产者配置
混合使用 SNS/SQS 生产者:
.UseExternalBus(opt =>
{
opt.ProducerRegistry = new CombinedProducerRegistryFactory(
// SNS 生产者
new SnsMessageProducerFactory(connection, [...]),
// SQS 生产者
new SqsMessageProducerFactory(connection, [...])
).Create();
});
Brighter V10 破坏性变更
消息映射器重构
| 变更点 | V9 | V10 |
| 默认序列化 | 必须实现映射器 | 内置 JSON 序列化,无需自定义映射器 |
| 接口拆分 | 单一 `IAmAMessageMapper` | IAmAMessageMapper (同步) + IAmAMessageMapperAsync (异步) |
| 方法签名 | MapToMessage(request) | MapToMessage(request, publication) |
当前限制:V10 RC1 仍需通过自定义映射器设置 FIFO 分区键
订阅模型变更
明确消息泵类型
// V9: 布尔值 isAsync
// V10: 枚举值 MessagePumpType.Proactor/Reactor
messagePumpType: MessagePumpType.Proactor
属性重命名
// V9: ChannelFactory → V10: DefaultChannelFactory
opt.DefaultChannelFactory = new ChannelFactory(connection)
通道类型必须显式声明
- ChannelType.PubSub (SNS→SQS)
- ChannelType.PointToPoint (SQS→SQS)
FIFO 配置迁移
new SqsSubscription<SnsFifoEvent>(
...
topicAttributes: new SnsAttributes { Type = SqsType.Fifo },
queueAttributes: new SqsAttributes(type: SqsType.Fifo))
发布模型变更
注册方式统一
// V10
.UseExternalBus(opt => { ... })
// V9
.UseExternalBus(new RmqProducerRegistryFactory(...))
请求类型必须显式指定
new SqsPublication<Farewell> { ... } // 泛型指定
// 或
new SqsPublication { RequestType = typeof(Farewell) }
新增独立 SQS 发布器
new SqsPublication<Farewell> { ... } // 专用于 SQS
迁移步骤总结
1. 更新消息映射器:实现异步接口 IAmAMessageMapperAsync,适配新方法签名
2. 显式声明通道类型:所有订阅配置需明确 ChannelType.PubSub 或 ChannelType.PointToPoint
3. 采用新生产者模型:使用 CombinedProducerRegistryFactory 混合注册 SNS/SQS 生产者
4. 简化序列化:移除不必要的自定义映射器,优先使用内置 JSON 序列化
5. FIFO 适配:通过 SnsAttributes/SqsAttributes 启用 FIFO 并设置分区键
完整示例代码: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.JsonConverters;
using Paramore.Brighter.MessagingGateway.AWSSQS;
using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection;
using Paramore.Brighter.ServiceActivator.Extensions.Hosting;
using Paramore.Brighter.Transforms.Attributes;
using Serilog;
using IRequestContext = Paramore.Brighter.IRequestContext;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
var host = new HostBuilder()
.UseSerilog()
.ConfigureServices((_, services) =>
{
var connection = new AWSMessagingGatewayConnection(new BasicAWSCredentials("test", "test"),
RegionEndpoint.USEast1,
cfg => cfg.ServiceURL = "http://localhost:4566");
services
.AddHostedService<ServiceActivatorHostedService>()
.AddServiceActivator(opt =>
{
opt.Subscriptions = [
new SqsSubscription<Greeting>(
"greeting-subscription", // Optional
"greeting-queue", // SQS queue name
ChannelType.PubSub,
"greeting.topic".ToValidSNSTopicName(), // SNS Topic Name
bufferSize: 2,
messagePumpType: MessagePumpType.Proactor),
new SqsSubscription<Farewell>(
new SubscriptionName("farawell-subscription"), // Optional
new ChannelName("farewell.queue"), // SQS queue name
ChannelType.PointToPoint,
new RoutingKey("farewell.queue".ToValidSQSQueueName()), // SNS Topic Name
bufferSize: 2,
messagePumpType: MessagePumpType.Proactor),
new SqsSubscription<SnsFifoEvent>(
new SubscriptionName("sns-sample-fifo-subscription"), // Optional
new ChannelName("sns-sample-fifo".ToValidSQSQueueName(true)), // SQS queue name
ChannelType.PubSub,
new RoutingKey("sns-sample-fifo".ToValidSNSTopicName(true)), // SNS Topic Name
bufferSize: 2,
messagePumpType: MessagePumpType.Proactor,
topicAttributes: new SnsAttributes { Type = SqsType.Fifo },
queueAttributes: new SqsAttributes(type: SqsType.Fifo)),
];
opt.DefaultChannelFactory= new ChannelFactory(connection);
})
.AutoFromAssemblies()
.UseExternalBus(opt =>
{
opt.ProducerRegistry = new CombinedProducerRegistryFactory(
new SnsMessageProducerFactory(connection, [
new SnsPublication<Greeting>
{
Topic = "greeting.topic".ToValidSNSTopicName(),
MakeChannels = OnMissingChannel.Create
},
new SnsPublication<SnsFifoEvent>
{
Topic = "sns-sample-fifo".ToValidSNSTopicName(true),
MakeChannels = OnMissingChannel.Create,
TopicAttributes = new SnsAttributes
{
Type = SqsType.Fifo
}
}
]),
new SqsMessageProducerFactory(connection, [
new SqsPublication<Farewell>
{
ChannelName = "farewell.queue".ToValidSQSQueueName(),
Topic = "farewell.queue".ToValidSQSQueueName(),
MakeChannels = OnMissingChannel.Create
}
// ,
// new SqsPublication<Farewell>
// {
// Topic = new RoutingKey("farewell.queue".ToValidSQSQueueName()),
// MakeChannels = OnMissingChannel.Create,
// QueueAttributes = new SqsAttributes()
// }
])
).Create();
});
})
.Build();
await host.StartAsync();
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>();
await process.PostAsync(new Greeting { Name = name });
await process.PostAsync(new SnsFifoEvent { Value = name, PartitionValue = "123" });
}
await host.StopAsync();
public class SnsFifoMapper : IAmAMessageMapperAsync<SnsFifoEvent>
{
public Task<Message> MapToMessageAsync(SnsFifoEvent request, Publication publication,
CancellationToken cancellationToken = new CancellationToken())
{
return Task.FromResult(new Message(new MessageHeader
{
MessageId = request.Id,
Topic = publication.Topic!,
PartitionKey = request.PartitionValue,
MessageType = MessageType.MT_EVENT,
TimeStamp = DateTimeOffset.UtcNow
},
new MessageBody(JsonSerializer.SerializeToUtf8Bytes(request, JsonSerialisationOptions.Options))));
}
public Task<SnsFifoEvent> MapToRequestAsync(Message message, CancellationToken cancellationToken = new CancellationToken())
{
return Task.FromResult(JsonSerializer.Deserialize<SnsFifoEvent>(message.Body.Bytes, JsonSerialisationOptions.Options)!);
}
public IRequestContext? Context { get; set; }
}
public class SqsFifoMapper : IAmAMessageMapperAsync<SqsFifoEvent>
{
public Task<Message> MapToMessageAsync(SqsFifoEvent request, Publication publication,
CancellationToken cancellationToken = new CancellationToken())
{
return Task.FromResult(new Message(new MessageHeader
{
MessageId = request.Id,
Topic = publication.Topic!,
PartitionKey = request.PartitionValue,
MessageType = MessageType.MT_EVENT,
TimeStamp = DateTimeOffset.UtcNow
},
new MessageBody(JsonSerializer.SerializeToUtf8Bytes(request, JsonSerialisationOptions.Options))));
}
public Task<SqsFifoEvent> MapToRequestAsync(Message message, CancellationToken cancellationToken = new CancellationToken())
{
return Task.FromResult(JsonSerializer.Deserialize<SqsFifoEvent>(message.Body.Bytes, JsonSerialisationOptions.Options)!);
}
public IRequestContext? Context { get; set; }
}
public class Greeting() : Event(Guid.CreateVersion7())
{
public string Name { get; set; } = string.Empty;
}
public class Farewell() : Command(Guid.CreateVersion7())
{
public string Name { get; set; } = string.Empty;
}
public class SnsFifoEvent() : Event(Guid.CreateVersion7())
{
public string PartitionValue { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
public class SqsFifoEvent() : Event(Guid.CreateVersion7())
{
public string PartitionValue { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
public class GreetingHandler(IAmACommandProcessor processor, ILogger<GreetingHandler> logger) : RequestHandlerAsync<Greeting>
{
public override async Task<Greeting> HandleAsync(Greeting command, CancellationToken cancellationToken = default)
{
logger.LogInformation("Hello {Name}", command.Name);
await processor.PostAsync(new Farewell { Name = command.Name }, cancellationToken: cancellationToken);
return await base.HandleAsync(command, cancellationToken);
}
}
public class FarewellHandler(ILogger<FarewellHandler> logger)
: RequestHandlerAsync<Farewell>
{
public override Task<Farewell> HandleAsync(Farewell command, CancellationToken cancellationToken = default)
{
logger.LogInformation("Bye bye {Name}", command.Name);
return base.HandleAsync(command, cancellationToken);
}
}
public class SnsFifoHandler(ILogger<SnsFifoHandler> logger)
: RequestHandlerAsync<SnsFifoEvent>
{
public override Task<SnsFifoEvent> HandleAsync(SnsFifoEvent command, CancellationToken cancellationToken = default)
{
logger.LogInformation("SNS Fifo {Name}", command.Value);
return base.HandleAsync(command, cancellationToken);
}
}
public class SqsFifoHandler(ILogger<SqsFifoHandler> logger)
: RequestHandlerAsync<SqsFifoEvent>
{
public override Task<SqsFifoEvent> HandleAsync(SqsFifoEvent command, CancellationToken cancellationToken = default)
{
logger.LogInformation("SNS Fifo {Name}", command.Value);
return base.HandleAsync(command, cancellationToken);
}
}
375

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



