在NServiceBus的 Azure Service Bus传输版本6 中实现多态路由,如何在不枚举所有具体类型的情况下进行?
我们正在尝试将NServiceBus从 8版升级到10版,同时带来Azure Service Bus传输的新拓扑结构。我们在如何把从单主题拓扑迁移到按事件主题拓扑上遇到了一些问题。
我们的设置大致如下:当下单一个产品时,我们有一个基础事件类:
public class ProductOrdered : IEvent {
public int OrderId { get; set; }
}
对于我们组合中的不同产品组,我们派生这个类型以包含更具体的属性。
然后我们有两种类型的订阅者:
- 仅对
SomeConcreteProductOrdered事件感兴趣的订阅者,因此实现IHandleMessages<SomeConcreteProductOrdered>。 - 不需要了解不同产品组的全部细节,只关心任意
ProductOrdered的订阅者。这些会实现IHandleMessages<ProductOrdered>。
根据 文档,支持此类多态性的方式是在订阅者上指定要监听的队列:
topology.SubscribeTo<ProductOrdered>("SomeConcreteProductOrdered");
topology.SubscribeTo<ProductOrdered>("AnotherConcreteProductOrdered");
然而,这会要求我们完整枚举 ProductOrdered 的各种实现。这些实现可能随时间变化,需要在多处维护,最重要的是这些订阅者迄今并不需要知道。
所以我们在想,是否还有其他方式来设置,这样就不需要在消费端枚举该基类的所有具体实现?
我们在想要么让发布端将消息同时发布到 SomeConcreteProductOrdered 主题和 ProductOrdered 主题,以便不同的消费者按他们的 IHandleMessages<T> 实现来监听相应的主题。
另一种方案是在 SomeConcreteProductOrdered 主题到 ProductOrdered 主题创建一个订阅(本质上是在主题和订阅中重新构建继承树),但我们在想是否可以使用端点安装程序来自动配置这些资源。
解决方案
从单主题拓扑迁移到按事件主题拓扑时,一个重要变化是每个具体事件类型都发布到它自己的主题。不再存在一个可以通过筛选隐式处理继承关系的单一捆绑主题。
因此,对ProductOrdered的订阅不再自动意味着“所有派生类型”。传输层不会在代理层推断你的CLR继承层次结构。必须明确地定义哪些具体主题应该流向该处理程序。
集中式显式映射(推荐的起点)
没有内置的方法在不显式映射的情况下订阅“所有派生类型”。
发布端侧对一个共享的ProductOrdered主题的多路复用
最简单的办法是将映射集中化:
- 启动时扫描你的共享契约程序集
- 找到所有可赋值给ProductOrdered的非抽象类型
- 对每个调用
topology.SubscribeTo<ProductOrdered>(...)
这让处理程序保持简洁(IHandleMessages<ProductOrdered> 保持原样),并避免在多处维护列表。
对大多数系统来说,这通常是最低复杂度的解决方案,并且与按事件主题最为契合。
如果你想完全避免在订阅端枚举派生类型,可以将所有派生事件路由到单一主题:
topology.PublishTo<SomeConcreteProductOrdered>("ProductOrdered");
topology.PublishTo<AnotherConcreteProductOrdered>("ProductOrdered");
现在所有 ProductOrdered 家族的事件都进入一个主题。想要所有内容的订阅者只需订阅该主题。如果某些订阅者只关心特定的具体类型,可以将具体类型名称提升为本地经纪人属性:
transport.OutgoingNativeMessageCustomization = (operation, message) =>
{
if (operation is MulticastTransportOperation multicast)
{
// Subject used here for demonstration only
message.Subject = multicast.MessageType.FullName;
}
};
然后在订阅上定义一个CorrelationFilter,例如:
resource subscription 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2021-06-01-preview' = {
name: '${productOrderedTopic.name}/subscriber'
properties: {
rule: {
name: 'SomeConcreteProductOnly'
filterType: 'CorrelationFilter'
correlationFilter: {
subject: 'MyNamespace.SomeConcreteProductOrdered'
}
}
}
}
CorrelationFilter在实际应用中运行良好,并且可以有很多这样的筛选器,因此这是一个合理的权衡。然而:
- 你重新引入了筛选开销。
- 所有派生事件现在共享同一个主题和配额。
- 你通常通过基础设施即代码来管理订阅和规则,而不是自动订阅。
这是一个有效的选择,但这是对该事件族回归到分组主题的有意识的选择。
ProductOrdered是一个稳定/独立的契约
如果ProductOrdered是在它自己的程序集中的一个稳定契约,且消费者不得引用任何产品特定的程序集,那么订阅端枚举将不可行。按设计,这些端点在运行时无法知道派生类型。
在这种情况下,对具体事件仍然保持按事件主题,并使用自动转发订阅将它们投射到一个稳定的抽象主题。
例如:
- 主题:CarInsurance.OrderProcessing.CarInsuranceProductOrdered
- 订阅:ProductOrderedForwarder
- ForwardTo: OrderProcessing.ProductOrdered (topic)
- 主题:HomeInsurance.OrderProcessing.HomeInsuranceProductOrdered
- 订阅:ProductOrderedForwarder
- ForwardTo: OrderProcessing.ProductOrdered (topic)
抽象的消费者只订阅OrderProcessing.ProductOrdered,而对派生类型保持不知情。
- 自动转发是在订阅上配置,通常通过IaC提供。端点安装程序当前不支持这一点。这将是你在CorrelationFilter方面的类似选择。不过你也可以使用ServiceBusAdministrationClient在运行时或安装阶段填充这些配置,尽管通常不建议在部署管道之外使用提升权限。
- 转发不会改变消息类型。具体的事件类型仍然是被转发的对象,因此载荷必须与ProductOrdered兼容,如果消费者只引用基类契约,这听起来确实成立,NServiceBus会添加额外的封装消息类型头。
- Azure Service Bus最多支持4 跳转转发,因此避免过长的转发链,但总体来说这通常可以工作,因为你仍有空间让端点订阅转发到输入队列。
在按事件主题的模式下,如果不进行显式映射或有意进行多路复用,就没有自动的多态订阅机制。更多细节和高级场景的提示,请参阅我们的 拓扑文档页面。
如果你有更多问题或想讨论你们的具体约束,欢迎直接联系我们的支持团队。