原文地址:https://docs.particular.net/tutorials/intro-to-nservicebus/2-sending-a-command/
侵删。
能够发送和接收message是任何NServiceBus 系统的主要特征。在两个进程之间传递持久化的message能使这个传递更加可靠,哪怕其中一个进程暂时不可用。在这个课程中我们将会展示如何发送并且处理一个信息。在接下来的15-20分钟里,你会学到如何定义message和message handler,如何在本地发送和接收message并且使用内置的日志功能。
什么是message
message是一组数据,他们通过单向communication在两个endpoint之间传递。在NServiceBus中,我们将message定义成一个简单的类。
在这节课中,我们将会关注commands。在第四节课:发布事件中,我们会展开讲到event。
要定义一个command,先生成一个类然后让它继承ICommand标记接口。
public class DoSomething :
ICommand
{
public string SomeProperty { get; set; }
}
这个标记接口没有实现任何方法,只是让NServiceBus 知晓这个类是一个command,因此它可以在开启一个endpoint的时候构建一些关于message类型的元数据。你在这个message中构建的任何属性都构成了message数据本身。
command类的名字一样也很重要。一个command是做一件事情的请求,因此它应当以一种祈使语气的方式来命名。PlaceOrder 和ChargeCreditCard 都是很好的command命名方式,因为他们看上去很像一个“请求”。PlaceOrder 将会下一个订单,而ChargeCreditCard 将会在信用卡中扣款。然而CustomerMessage就不是一个好名字。它只看失去不是那么像一个请求,并且不是非常一目了然。其他开发者应当一看名字就知道这个command的目的是什么。
command的名字也应当传递一些业务含义。UpdateCustomerPropertyXYZ虽然比CustomerMessage 更加一目了然,然而也不是一个好的command名字,因为它仅仅关注数据的操作而没有业务含义在里面。MarkCustomerAsGold,或者类似于这样的名字,就更加面向业务了——它也许是一个更加的选择。
当发送一个message的时候,endpoint的序列化工具会将DoSomething 类的实例序列化,然后把它添加到即将发出到队列的message中去。在另一头,接收方endpoint会将这个message反序列化成一个实例来在代码中使用。
message甚至可以包含一些子对象或者集合。(这个由序列化工具类型来决定)
public class DoSomethingComplex :
ICommand
{
public int SomeId { get; set; }
public ChildClass ChildStuff { get; set; }
public List<ChildClass> ListOfStuff { get; set; } = new List<ChildClass>();
} public class ChildClass
{
public string SomeProperty { get; set; }
}
message是两个endpoint之间的协议。message的任何改变都会对发送方和接收方产生影响。你的message中包含的的属性越多,就有越多产生变动的原因,因此确保你的message越精简越好。
同时你不能再你的message类中嵌入逻辑。每一个message都应该只包含自动属性不能有包含计算的属性或者方法。同样的,通过默认的无参构造函数来实例化集合属性也是一个很好的做法,就像上面那样,因此你永远不要担心会产生一个null的集合。
实际上,message应当只能包含数据。通过确保你的message足够小,并且赋予它清晰的目的,你就可以让你的代码更加容易理解和扩展。
组织messages
message是数据协议,他们在各个endpoint之间共享。因此你实际上不能把这些类放在各个endpoint的相同程序集中。他们应该在分布在不同的类库里。
message 的程序集应该是独立的,意味着他们应当仅仅包含NServiceBus message类型和任何被message自身需要的类型。例如,如果一个message使用了一个美剧类型作为他的一个属性,这个枚举类就也应该在message程序集中。
message程序集不应当依赖除了.NET Framework类库和NServiceBus 核心库之外的程序集,因为ICommand 接口位于NServiceBus 核心库中。
参照这些方法会让你的message协议在以后更加容易扩展。
处理message
我们构造了message handler来处理message,这个类实现了IHandleMessages<T>接口,T是一个message类型。一个message handler示例如下:
public class DoSomethingHandler :
IHandleMessages<DoSomething>
{
public Task Handle(DoSomething message, IMessageHandlerContext context)
{
// Do something with the message here
return Task.CompletedTask;
}
}
IHandleMessages<T> 实现了一个handle方法,NServiceBus 将会在一个T类型的message(在DoSomething中)到达的时候调用这个方法。handle方法接收message和一个包含处理message上下文API的IMessageHandlerContext实现。
除了显式返回一个task,你也可以在一个handler方法前面添加async关键字:
public class DoSomethingHandler :
IHandleMessages<DoSomething>
{
public async Task Handle(DoSomething message, IMessageHandlerContext context)
{
// Do something with the message here
}
}
如果你想要学习更多使用async 方法的构建handler方式,可以参阅Asynchronous Handlers。
一个类可以实现多个IHandleMessages<T>来处理多种message类型。这样就可以将一些逻辑上相关联的handler组成一组,尽管处理每个message都会实例化出一个新的对象。
public class DoSomethingHandler :
IHandleMessages<DoSomething>,
IHandleMessages<DoSomethingElse>
{
public Task Handle(DoSomething message, IMessageHandlerContext context)
{
Console.WriteLine("Received DoSomething");
return Task.CompletedTask;
} public Task Handle(DoSomethingElse message, IMessageHandlerContext context)
{
Console.WriteLine("Received DoSomethingElse");
return Task.CompletedTask;
}
}
当NServiceBus 开启的时候,它将会找到所有的这些message handler类并且自动将他们合并在一起,因此当message到的时候他们都会被调用。这里不需要进行任何的初始化和配置。
handler在一个类还是多各类中实现都是一样的。当NServiceBus 启动的时候,它会找到所有的message handler然后将他们合并在一起,不需要任何配置。这样的组合方法是为了让你的代码更加清晰。
练习
现在让我们继续使用上节课构建的解决方案,将它进行一些更改让它能够发送message。你也可以直接拿一个已经完成的上节课的例子来开始。
当我们完成的时候,ClientUI endpoint会向自己发送一个 PlaceOrder ,然后处理这个message,就像下面这个图描述的这样:
创建一个message程序集
为了在endpoint之间分享message,它们必须各自独立在不同的程序集中间,现在让我们来创建这些程序集。
1.在这个解决方案中,生成一个新的项目然后选择类库项目类型。
2.把项目的名称设置成Message
3.把自动生成的class1.cs文件删掉
4.添加 NServiceBus NuGet 包到这个项目中
5.在ClientUI 项目中,添加对Message项目的引用
创建一个message
我们将要在一个叫Commands的文件夹创建我们第一个command。
1.在Message项目中,创建一个新的叫做PlaceOrder的类
2.将PlaceOrder标记成public并且实现ICommand接口
3.添加一个string类型的公共OrderId属性
.NET Framework 在System.Windows.Input名称空间下面包含一个它定义的ICommand 接口。因此在你自动解析名称空间的时候,你要选择NServiceBus.ICommand。大多数你需要的类型都会在NServiceBus名称空间中。
完成之后,你的PlaceOrder 类应该是这样子的:
namespace Messages
{
public class PlaceOrder :
ICommand
{
public string OrderId { get; set; }
}
}
创建一个handler
现在我们已经定义了一个message了,我们可以创建一个相应的message handler。现在,然我们处理在ClientUI endpoint本地的message。
1.在ClientUI 项目中,创建一个叫做PlaceOrderHandler的类。
2.将这个handler类定义成public,然后实现IHandleMessages<PlaceOrder>接口。
3.添加一个日志实例,这能够让你使用和NServiceBus用的一样的日志系统。它在Console.WriteLine()上有一个很重要的优点:日志的信息都会在控制台上面展现。使用下面的代码将日志实例添加到你的handler类中:
static ILog logger = LogManager.GetLogger<PlaceOrderHandler>();
4.在handle方法中,使用logger来记录PlaceOrder message的接收,包括OrderId 的值:
5.因为我们所有在这个handler中做的事情都是同步的,返回Task.CompletedTask.
完成之后,你的PlaceOrderHandler 类应该是这样子的:
public class PlaceOrderHandler :
IHandleMessages<PlaceOrder>
{
static ILog log = LogManager.GetLogger<PlaceOrderHandler>(); public Task Handle(PlaceOrder message, IMessageHandlerContext context)
{
log.Info($"Received PlaceOrder, OrderId = {message.OrderId}");
return Task.CompletedTask;
}
}
因为LogManager.GetLogger(..); 的开销很大,所以请将logger实现作为成静态成员。
发送一个message
现在我们有了一个message和一个处理它的handler了,让我们来发送message。
在ClientUI 项目中,我们暂时先按回车键来终止endpoint。现在让我们来创建一个循环来让它更加具有互动性,我们可以使用键盘输入来决定是发送message还是退出。
将下面的方法添加到Program.cs 文件中:
static ILog log = LogManager.GetLogger<Program>(); static async Task RunLoop(IEndpointInstance endpointInstance)
{
while (true)
{
log.Info("Press 'P' to place an order, or 'Q' to quit.");
var key = Console.ReadKey();
Console.WriteLine(); switch (key.Key)
{
case ConsoleKey.P:
// Instantiate the command
var command = new PlaceOrder
{
OrderId = Guid.NewGuid().ToString()
}; // Send the command to the local endpoint
log.Info($"Sending PlaceOrder command, OrderId = {command.OrderId}");
await endpointInstance.SendLocal(command)
.ConfigureAwait(false); break; case ConsoleKey.Q:
return; default:
log.Info("Unknown input. Please try again.");
break;
}
}
}
当我们想要下一个订单的时候,我们先仔细地观察这个例子。为了生成一个 PlaceOrder 的command,我们简单地实例化一个PlaceOrder 类,然后给OrderId一个唯一值。记录完这些细节的之后,我们可以通过调用SendLocal 方法来发送它。
SendLocal(object message)是一个定义在IEndpointInstance 接口的方法,就像我们在这里使用的,它也定义在IMessageHandlerContext 接口中,这个接口在我们定义我们的message handler的时候我们也见到过。Local 意味着我们不把message发送到外面的endpoint去(位于一个不同的进程),因此我们倾向于在发送同时也接收message的endpoint中处理它。使用SendLocal(), 我们不需要其他任何的信息告诉message它要被发送到哪里。
在这节课程中,我们使用了SendLocal(而不是其他的更加常用的Send方法)。这样我们可以探索如何定义,发送和处理message,不需要第二个endpoint来处理它们。通过SendLocal方法,我们也不需要定义路由规则来控制这个message发送到哪里。我们将会在下一个课程中学习这些东西。
因为SendLocal() 返回一个Task,我们需要保证合理地await它。
现在让我们修改这个AsyncMain ,调用新的RunLoop 方法:
var endpointInstance = await Endpoint.Start(endpointConfiguration)
.ConfigureAwait(false); // Remove these two lines
Console.WriteLine("Press Enter to exit...");
Console.ReadLine(); // Replace with:
await RunLoop(endpointInstance); await endpointInstance.Stop()
.ConfigureAwait(false);
运行解决方案
现在我们可以运行这个解决方案。我们只要在控制台中输入P,一个command message就会被发送然后在同一个项目中的handler里面处理。
INFO ClientUI.Program Press 'P' to place an order, or 'Q' to quit.
p
INFO ClientUI.Program Sending PlaceOrder command, OrderId = 1fb61e01-34a3-4562-82b1-85278565b59d
INFO ClientUI.Program Press 'P' to place an order, or 'Q' to quit.
INFO ClientUI.PlaceOrderHandler Received PlaceOrder, OrderId = 1fb61e01-34a3-4562-82b1-85278565b59d
p
INFO ClientUI.Program Sending PlaceOrder command, OrderId = d9e59362-ccf4-4323-8298-4bbc052fb877
INFO ClientUI.Program Press 'P' to place an order, or 'Q' to quit.
INFO ClientUI.PlaceOrderHandler Received PlaceOrder, OrderId = d9e59362-ccf4-4323-8298-4bbc052fb877
需要注意的是在发送message之后,ClientUI.Program的提示在ClientUI.PlaceOrderHandler 确认接收到message之后显示。这个是因为和直接调用Handle方法不一样,这个message是异步发送的,然后控制台立即返回到RunLoop(这个方法会立即重复提示出信息)。很快,当message被接收和处理之后,我们会看到Received PlaceOrder 的提示。
总结
在这节课中我们学习了关于message,message的程序集和message handler。我们创建了一个message和一个handler然后我们使用SendLocal() 方法来发送message到同一个endpoint中。
在下节课中,我们会创建第二个messaging endpoint,将我们的message处理转移到那里去。然后我们将会配置ClientUI ,将message发送到新的endpoint中。我们也会观察如何接受方endpoint不在线的时候我们发送message过去会发生什么。