zoukankan      html  css  js  c++  java
  • 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过去会发生什么。

  • 相关阅读:
    转:调试Release发布版程序的Crash错误
    [原创]桓泽学音频编解码(9):MP3 多相滤波器组算法分析
    [转] 一些你不知道但是超美的地方,一定要去
    [原创]桓泽学音频编解码(11):AC3 exponent(指数部分)模块解码算法分析
    [原创]桓泽学音频编解码(14):AC3 时频转换模块算法分析
    [原创]桓泽学音频编解码(15):AC3 最终章 多声道处理模块算法分析
    Android Supported Media Formats
    VoiceChatter 编译记录
    [原创]桓泽学音频编解码(4):MP3 和 AAC 中反量化原理,优化设计与参考代码中实现
    OPUS 视频PPT介绍
  • 原文地址:https://www.cnblogs.com/balavatasky/p/6557557.html
Copyright © 2011-2022 走看看