使用 AWS SNS/SQS 迁移到 Brighter V10

在前文中,我们探讨了 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 包:  

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 破坏性变更  

消息映射器重构  

变更点V9V10
默认序列化必须实现映射器内置 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);
    }
}

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值