生产者(Producers)
应用程序或服务可以使用两种不同的方法生成消息。消息可以被发送或发布。每种方法的行为非常不同,但通过查看每种方法涉及的消息类型很容易理解。
当消息被发送时,它使用 DestinationAddress
被传递到特定端点,例如队列。当消息被发布时,它不会被发送到特定端点,而是广播给任何订阅了该消息类型的消费者。对于这两种不同的行为,我们将发送的消息描述为命令,将发布的消息描述为事件。
有关消息名称的更多信息,请参阅消息概念页面。
发送(Send)
要发送消息,DestinationAddress
用于将消息传递到端点——例如队列。调用 ISendEndpoint
接口上的 Send
方法重载之一,然后将消息发送到传输。ISendEndpoint
从以下对象之一获得:
- 正在消费的消息的
ConsumeContext
这确保了相关头信息、消息头信息和跟踪信息被传播到发送的消息。 ISendEndpointProvider
实例
这可以作为参数传递,但通常在通过依赖注入容器解析的对象的构造函数上指定。IBus
最后的选择,仅应用于由初始化者发送的消息——启动业务流程的进程。
一旦调用了 Send
方法(仅一次或多次以发送一系列消息),ISendEndpoint
引用应超出作用域。
应用程序不应存储
ISendEndpoint 引用,MassTransit 会自动缓存并在不再需要时丢弃它.
例如,IBus
实例是一个发送端点提供者,但它永远不应被消费者用来获取 ISendEndpoint
。ConsumeContext
也可以提供发送端点,并且应该被使用,因为它更接近消费者。
这一点再怎么强调也不为过——始终从最近的范围获取
ISendEndpoint。有大量逻辑使用对话、相关性和初始化者标识符将消息流联系在一起。通过跳过一级并超出最近的范围,将丢失关键信息,从而阻止有用的跟踪标识符传播。
发送端点(Send Endpoint)
要从发送端点提供者获取发送端点,请调用 GetSendEndpoint
方法,如下所示。该方法是异步的,因此请确保等待结果。
public record SubmitOrder
{
public string OrderId { get; init; }
}
public async Task SendOrder(ISendEndpointProvider sendEndpointProvider)
{
var endpoint = await sendEndpointProvider.GetSendEndpoint(_serviceAddress);
await endpoint.Send(new SubmitOrder { OrderId = "123" });
}
Send
方法有许多重载。因为 MassTransit 是围绕过滤器和管道构建的,所以管道用于自定义 Send
的消息传递行为。还有一些有用的重载(通过扩展方法),使发送更容易,并且由于管道构造等原因减少噪音。
带超时的发送(Send with Timeout)
如果应用程序和代理之间存在连接问题,Send
方法将在内部重试,直到连接恢复,阻止返回的 Task
直到发送操作完成。Send
方法支持传递 CancellationToken
,可用于取消操作。
要指定超时,请使用 CancellationTokenSource
,如下所示。
var timeout = TimeSpan.FromSeconds(30);
using var source = new CancellationTokenSource(timeout);
await endpoint.Send(new SubmitOrder { OrderId = "123" }, source.Token);
通常,Send
调用会很快完成,只需几毫秒。如果令牌被取消,发送操作将抛出 OperationCanceledException
。
端点地址(Endpoint Address)
端点地址是一个完全限定的 URI,可能包括特定于传输的详细信息。例如,本地 RabbitMQ 服务器上的一个端点将是:
rabbitmq://localhost/input-queue
传输特定的详细信息可能包括查询参数,例如:
rabbitmq://localhost/input-queue?durable=false
这将配置队列为非持久的,消息只会存储在内存中,因此不会在代理重启后存活。
短地址(Short Addresses)
从 MassTransit v6 开始,支持短地址。例如,要获取 RabbitMQ 上队列的发送端点,调用者只需指定:
GetSendEndpoint(new Uri("queue:input-queue"))
这将返回 input-queue
交换机的发送端点,该交换机将绑定到 input-queue
队列。如果交换机或队列不存在,将创建它们。这种简短的语法消除了了解代理的方案、主机、端口和虚拟主机的需求,只需要队列和/或交换机的详细信息。
每个传输都有一组特定的支持短地址。
支持的地址方案
短地址 | RabbitMQ | Azure Service Bus | ActiveMQ | Amazon SQS |
---|---|---|---|---|
queue:name | ✔️ | ✔️ | ✔️ | ✔️ |
topic:name | ✔️ | ✔️ | ✔️ | |
exchange:name | ✔️ |
地址约定(Address Conventions)
尽管这些约定是可用的,但作者倾向于不喜欢它们,基于这个 Stack Overflow 回答。
使用发送端点可能看起来过于冗长,因为在发送任何消息之前,你需要获取发送端点,为此你需要一个端点地址。通常,地址保存在配置中,从应用程序的各个部分访问配置不是一个好的做法。
端点约定通过允许你配置消息类型和端点地址之间的映射来解决这个问题。这里的一个潜在缺点是你将无法使用约定将相同类型的消息发送到不同的端点。如果你需要这样做,请继续使用 GetSendEndpoint
方法。
约定是这样配置的:
EndpointConvention.Map<SubmitOrder>(new Uri("rabbitmq://mq.acme.com/order/order_processing"));
现在,你不再需要为此类型的消息获取发送端点,可以像这样发送:
public async Task Post(SubmitOrderRequest request)
{
if (AllGoodWith(request))
await _bus.Send(ConvertToCommand(request));
}
此外,在消费者内部,你可以使用 ConsumeContext.Send
重载执行相同的操作:
EndpointConvention.Map<StartDelivery>(new Uri(ConfigurationManager.AppSettings["deliveryServiceQueue"]));
public class SubmitOrderConsumer :
IConsumer<SubmitOrder>
{
private readonly IOrderSubmitter _orderSubmitter;
public SubmitOrderConsumer(IOrderSubmitter submitter)
=> _orderSubmitter = submitter;
public async Task Consume(IConsumeContext<SubmitOrder> context)
{
await _orderSubmitter.Process(context.Message);
await context.Send(new StartDelivery(context.Message.OrderId, DateTime.UtcNow));
}
}
EndpointConvention.Map<T>
方法是静态的,因此可以从任何地方调用。重要的是要记住,你不能为相同的消息配置两次约定。如果你尝试这样做,Map
方法将抛出异常。这在编写测试时也很重要,因此你需要在配置测试总线(harness)的同时配置约定。
最好在启动总线之前配置发送约定。
发布(Publish)
消息的发布方式与消息的发送方式类似,但在这种情况下,使用单个 IPublishEndpoint
。端点的相同规则适用,应使用最近的发布端点实例。因此,对于消费者使用 ConsumeContext
,对于在消费者上下文之外发布的应用程序使用 IBus
。
在 MassTransit 中,Publish
遵循发布-订阅消息传递模式。对于每个发布的消息,消息的副本将传递给每个订阅者。实现这一点的机制由消息传输实现,但从语义上讲,无论使用哪种传输,操作都是相同的。
发布消息的相同指南适用,应使用最近的实例。
- 正在消费的消息的
ConsumeContext
这确保了相关头信息、消息头信息和跟踪信息被传播到发布的消息。 IPublishEndpoint
实例
这可以作为参数传递,但通常在通过依赖注入容器解析的对象的构造函数上指定。IBus
最后的选择,仅应用于由初始化者发布的消息——启动业务流程的进程。
要发布消息,请参见以下代码:
public record OrderSubmitted
{
public string OrderId { get; init; }
public DateTime OrderDate { get; init; }
}
public async Task NotifyOrderSubmitted(IPublishEndpoint publishEndpoint)
{
await publishEndpoint.Publish<OrderSubmitted>(new()
{
OrderId = "27",
OrderDate = DateTime.UtcNow,
});
}
Publish
也支持取消,包括超时。有关详细信息,请参见上面的注释。
如果你计划从消费者内部发布消息,这个示例会更合适:
public class SubmitOrderConsumer : IConsumer<SubmitOrder>
{
private readonly IOrderSubmitter _orderSubmitter;
public SubmitOrderConsumer(IOrderSubmitter submitter)
=> _orderSubmitter = submitter;
public async Task Consume(IConsumeContext<SubmitOrder> context)
{
await _orderSubmitter.Process(context.Message);
await context.Publish<OrderSubmitted>(new()
{
OrderId = context.Message.OrderId,
OrderDate = DateTime.UtcNow
})
}
}
消息初始化(Message Initialization)
MassTransit 可以使用传递给 publish
或 send
方法的匿名对象来初始化消息。虽然最初设计用于初始化基于接口的消息类型,但匿名对象也可以用于初始化使用类或记录定义的消息类型。
对象属性(Object Properties)
Send
、Publish
以及大多数行为类似的方法(调度、响应请求等)都支持传递一个值对象,该对象用于设置指定接口的属性。一个简单的示例如下所示。
考虑这个提交订单的示例消息契约。
public record SubmitOrder
{
public Guid OrderId { get; init; }
public DateTime OrderDate { get; init; }
public string OrderNumber { get; init; }
public decimal OrderAmount { get; init; }
}
要向端点发送此消息:
await endpoint.Send<SubmitOrder>(new // <-- notice no ()
{
OrderId = NewId.NextGuid(),
OrderDate = DateTime.UtcNow,
OrderNumber = "18001",
OrderAmount = 123.45m
});
匿名对象属性按名称匹配,并且有一组广泛的类型转换可用于匹配接口定义的类型。大多数数字、字符串和日期/时间转换都受支持,以及一些高级转换(包括变量和异步 Task<T>
结果)。
集合,包括数组、列表和字典,得到了广泛支持,包括列表元素以及字典键和值的转换。例如,一个 (int, decimal)
字典可以动态转换为 (long, string)
,使用默认的格式转换。
嵌套对象也受支持,例如,如果一个属性是 Address
类型,并且创建了另一个匿名对象(或任何属性名称与消息契约上的属性名称匹配的类型),这些属性将设置在消息契约上。
接口消息(Interface Messages)
MassTransit 支持接口消息类型,并且有一些便利方法可以初始化接口,而无需创建实现该接口的类。
public interface SubmitOrder
{
public string OrderId { get; init; }
public DateTime OrderDate { get; init; }
public decimal OrderAmount { get; init; }
}
public async Task SendOrder(ISendEndpoint endpoint)
{
await endpoint.Send<SubmitOrder>(new
{
OrderId = "27",
OrderDate = DateTime.UtcNow,
OrderAmount = 123.45m
});
}
头信息(Headers)
可以使用双下划线(据说是“dunder”)属性名称在匿名对象中指定头信息值。例如,要设置消息的生存时间,请指定一个持续时间属性。记住,任何可以转换为 TimeSpan
的值都可以使用!
public record GetOrderStatus
{
public Guid OrderId { get; init; }
}
var response = await requestClient.GetResponse<OrderStatus>(new
{
__TimeToLive = 15000, // 15 seconds, or in this case, 15000 milliseconds
OrderId = orderId,
});
实际上,这是一个不好的例子,因为请求客户端已经设置了消息过期时间,但你明白我的意思。
要添加自定义头信息值,使用特殊的属性名称格式。在名称中,下划线转换为破折号,双下划线转换为下划线。在以下示例中:
var response = await requestClient.GetResponse<OrderStatus>(new
{
__Header_X_B3_TraceId = zipkinTraceId,
__Header_X_B3_SpanId = zipkinSpanId,
OrderId = orderId,
});
这将设置由开放跟踪(或 Zipkin,如上所示)使用的头信息作为请求消息的一部分,以便服务可以共享跨度/跟踪。在这种情况下,X-B3-TraceId
和 X-B3-SpanId
将被添加到消息信封中,并且根据传输,也会复制到传输头信息中。
变量(Variables)
MassTransit 还支持变量,这些是添加到匿名对象的特殊类型。按照上面的示例,初始化可以更改为使用变量来设置 OrderId
和 OrderDate
。变量在整个消息创建过程中是一致的,多次使用相同的变量将返回相同的值。例如,用于设置 OrderId
的 Id
将用于设置每个项目中的 OrderId
。
public record OrderItem
{
public Guid OrderId { get; init; }
public string ItemNumber { get; init; }
}
public record SubmitOrder
{
public Guid OrderId { get; init; }
public DateTime OrderDate { get; init; }
public string OrderNumber { get; init; }
public decimal OrderAmount { get; init; }
public OrderItem[] OrderItems { get; init; }
}
await endpoint.Send<SubmitOrder>(new
{
OrderId = InVar.Id,
OrderDate = InVar.Timestamp,
OrderNumber = "18001",
OrderAmount = 123.45m,
OrderItems = new[]
{
new { OrderId = InVar.Id, ItemNumber = "237" },
new { OrderId = InVar.Id, ItemNumber = "762" }
}
});
异步属性(Async Properties)
消息初始化器是异步的,这使得可以做一些非常酷的事情,包括等待 Task
输入属性完成并使用结果来初始化属性。一个示例如下所示。
public record OrderUpdated
{
public Guid CorrelationId { get; init; }
public DateTime Timestamp { get; init; }
public Guid OrderId { get; init; }
public Customer Customer { get; init; }
}
public async Task<CustomerInfo> LoadCustomer(Guid orderId)
{
// work happens up in here
}
await context.Publish<OrderUpdated>(new
{
InVar.CorrelationId,
InVar.Timestamp,
OrderId = context.Message.OrderId,
Customer = LoadCustomer(context.Message.OrderId)
});
属性初始化器将等待任务结果,然后使用它来初始化属性(转换所有类型等,就像它处理任何其他对象一样)。
虽然当然可以等待对
LoadCustomer
的调用,但属性是并行初始化的,因此,允许初始化器等待Task
可以带来更好的整体性能。然而,你的情况可能会有所不同。
发送头信息(Send Headers)
有多种消息头可用于消息的相关性和跟踪。当发生故障时,还可以覆盖 MassTransit 的一些默认行为。例如,当消费者抛出异常时,通常会发布故障。如果应用程序希望将故障传递到特定地址,可以通过头信息指定 FaultAddress
。如何做到这一点如下所示。
public record SubmitOrder
{
public string OrderId { get; init; }
public DateTime OrderDate { get; init; }
public decimal OrderAmount { get; init; }
}
public async Task SendOrder(ISendEndpoint endpoint)
{
await endpoint.Send<SubmitOrder>(new
{
OrderId = "27",
OrderDate = DateTime.UtcNow,
OrderAmount = 123.45m
}, context => context.FaultAddress = new Uri("rabbitmq://localhost/order_faults"));
}
由于使用了消息初始化器,这实际上可以简化。
public async Task SendOrder(ISendEndpoint endpoint)
{
await endpoint.Send<SubmitOrder>(new
{
OrderId = "27",
OrderDate = DateTime.UtcNow,
OrderAmount = 123.45m,
// header names are prefixed with __, and types are converted as needed
__FaultAddress = "rabbitmq://localhost/order_faults"
});
}