NServiceBus入门:发送一个命令(Introduction to NServiceBus: Sending a command)

原文地址: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,就像下面这个图描述的这样:

image

创建一个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 的值:

static ILog logger = LogManager.GetLogger<PlaceOrderHandler>();

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过去会发生什么。

posted @ 2017-03-16 00:34  balavatasky  阅读(422)  评论(0编辑  收藏  举报