Skip to main content

Webinar recording

Live coding: NServiceBus in the real world

Watch me extend the NServiceBus system from the previous webinar, adding features you’ll need to run a real-world system in production.

🔗Why attend?

In Live coding your first NServiceBus system, I started from scratch and built an NServiceBus system that sent commands, used the publish/subscribe pattern, and handled long-running business workflows using sagas.

However, the code from that demo is still very much demoware. It uses the learning transport which is great for a proof-of-concept, but can’t be used in production.

In the real world, we need to be able to test our system. We need to be able to extend it without copying and pasting code all over the place. We need to be prepared to handle when messages fail. And above all, we need to run it on a production message queue.

By watching me code (and maybe even screw up a few times) you’ll find out how to do all those things, and hopefully learn a few other tips and tricks along the way.

🔗In this webinar you’ll learn how to:

  • Test NServiceBus message handlers and sagas
  • Create behaviors to manage infrastructure concerns across an entire system
  • Deal with the errors that will inevitably happen in a real-world system
  • Upgrade from the Learning Transport to a production message queue like RabbitMQ, and what to consider when selecting a message transport

🔗Transcription

00:01 David Boike
Hi, everyone. My name is David Boike and in the previous webinar, which you can still watch online, I built a complete NServiceBus system from scratch. That demo included how to send messages, do publish, subscribe, and orchestrate long running business processes with sagas, but in the end, it was still a demo. Today, I'm going to take that same code and elevate it to something more production-ready. This is the exact same code. The only things I've done to it while you haven't been watching are I updated it from .NET 5 to .NET 6, and so I'm using Visual Studio 2022, and I've updated the NServiceBus dependency from version 7.5 to 7.6. So while I do have a plan for today, this is still live coding, which makes me either stupid or crazy or maybe some of both. So once again, I've got my colleague William Brander here to save me in case I forget a using statement or a semicolon. Say hello, William.
00:51 William Brander
Hi, everyone.
00:55 David Boike
All right, let's get started. The first thing I want to do is show you something I had prepared for the last webinar but didn't have time for, how to deal with processing failures in your system, which is going to happen in real life and you need to be ready for it. This is also one of the most useful features of message-driven systems in general, and especially NServiceBus systems, because your system can be ready to deal with messages failing and your front-end users never even really need to know anything bad happened. So, we've got our platform tools, which include ServiceControl and ServicePulse, which normally, if you were already using a full message transport, you would install as a service on some server in your system. But since this project is still using our learning transport, we can use this package we have called the Platform Sample and we can take a look at how it works, so let's do that.
01:46 David Boike
So, in this solution, we're going to first create a console app. So, dotnet new console named Platform, and we're going to add it to the solution. So dotnet solution add Platform. And then we're going to add the Platform Sample package to that. So that's dotnet add to the Platform project. I'm having trouble typing today, package particular Platform Sample. Now we'll go to Visual Studio and it'll refresh and we'll have a platform project and where is it at? It's right here. And you can see how adding the new get package for the Platform Sample has actually brought in a folder called Platform here with ServiceControl and ServicePulse.
02:42 David Boike
So these are portable versions of the platform tools that we can use. So in order to actually run those, we need to open our program CS. And because we're in 2022, we're getting the minimal API stuff going on here. So I'm just going to get rid of all this. I'm going to say, using Particular, and then PlatformLauncher.Launch. And we're going to want that project to actually run when we start the system. So I'm going to adjust the startup projects for the system. We already have the client UI sales billing and shipping endpoints that we had in the last webinar already starting. But now I'm going to add the platform to that as well. So it's going to start.
03:31 David Boike
And now in order to test this, what we're really testing is how to deal with messages that fail. So we need something to blow up. So how are we are going to do that? Let's go to our sales endpoint and into the place order handler. And if you are not familiar with this, this is some code that we wrote in the last webinar. We were testing it to see what happened if there were intermittent failures back then, so you can review the previous webinar for that. So what we actually want to do here is we want to uncomment this code that's throwing an exception boom, but where we were doing a random and we were only wanting it to fail 20% of the time, now we want it to fail always. So we're going to set that to anything greater than zero. So now we are going to demo it. Let's press start. The first time you press start is always the worst. Hopefully everything works. Everything's popping up.
04:31 David Boike
So if you're not familiar from the last webinar, we've got basically one console window for each of our endpoints. And then this is a new one for the platform exe. And what it's doing is it's finding free ports for ServiceControl and to do all of the stuff it needs to do. And it's now waiting for ServiceControl to be available. It takes a second for it to start up and then web browser opens, and this is the ServicePulse UI. And as we can see here, everything looks pretty good. Nothing's failing, nothing's red. So let's go back to... Let's actually hide that now and let's show our client UI and sales because... Oops, I didn't want to make that big, because client UI is where we're going to send messages from. And sales is the one that's going to fail. So let's actually... And I did it again. Let's press P to place an order and oh, we see this boom happening over in the sales.
05:26 David Boike
Now we're getting a warning. Now it's saying delayed retry will reschedule message, blah, blah, blah after a delay of 10 seconds because of an exception boom. And now we've already gotten it again. And it's saying now it'll retry after delay of 20 seconds because of an exception. What it's doing is it's showing you our delayed retries feature. So we automatically do five immediate retries. But if that fails, then it goes into delayed retries. The reason for that is if you have something like a database deadlock, that's probably going to resolve itself on the very next retry. So you want to do immediate retry, but if you have something that's not quite a systemic failure, but not kind of an immediate retry thing, you want to wait and back off a little bit. That would be for like hitting a web service that happens to be restarting or something like that.
06:15 David Boike
So we do three sets of retries by default and the increase in time each time. So we already did the 10 seconds and then 20 seconds, and now we're waiting for the last one, 30 seconds. And this is where I find out if I've vamped enough and here I have, good. So now we've gotten a red fail. Message was sent to the error queue because processing failed to an exception. So we've exhausted all of our retry now, and the message has gone to the error queue. Well, ServiceControl, its whole job is to look at that error queue. And if we look back at this now. we see that ServicePulse shows us that we have a failed message. And if we go and look at it, we can see that it's got this note that isn't filled in. I believe that we actually have a bug on that to hide that if that's not the case, but we can go and add an actual note to this and say some developer guy just threw an exception.
07:12 David Boike
So this is great for ops people to keep notes on these and talk about why they are and what we're doing with them and what the plan is. Just a little scratch space here. But what I want to do is I don't want to just retry one message. I don't want to show you what ServicePulse can actually do. I want to throw a lot more exceptions. And in order to do that, I don't want to wait for all of those delayed retries. So I want to show you how you can actually change that.
07:40 David Boike
So you can actually change the recoverability policy. We can go to the sales. We can go to the program CS and here and here where we have our endpoint configuration. We can say var recoverability equals endpointConfiguration.Recoverability. And then from the recoverability settings, we can say immediate retries. We want settings. We want the number of retries to be zero. And recoverability.Delayed, we also want that to be zero. And let's just look at some of the other settings in here. We have time increase. We can change the default time increase from 10 seconds to something else. We have a little call back for when a message is being retried. You can look at the number of retries. There's lots of things you can do in here. So settings.NumberOfRetries, zero for this as well. And that should be good.
08:45 David Boike
So now I can make a bunch of messages fail and they'll go immediately to the error queue and we can get on with that demo. But I just wanted to show you these retry settings and how you can customize that to what your endpoint is actually doing. Maybe you're doing something where immediate retries make no sense, because it's just hitting web services. And if it fails, you really do need to wait for maybe even a minute. You can set an endpoint to do that. So let's push play again. And I'm going to make ServicePulse littler so we can see both at the same time.
09:20 David Boike
Oh, of course, this is the ServicePulse from the old one. We're still waiting for ServiceControl to be available. It'll pop up a new window. There it is. So we've got the one failed message right now. And if I can find the one that's our client UI, then I can send a bunch more message, and you can see a bunch more messages immediately go to the failed queue and you see all the boom, boom, boom, boom, boom in here. So when you have a message fail like this in production, what you really want to do is you want to fix it because these operations all represent some users order. Like they may be put in their information on the website and it sent a message and you already told them, hey, that's great. We're going to send you your stuff, but you have these failed messages on the backend. That's valuable business information and we need to recover that. We need to replay these messages after we fix the error.
10:12 David Boike
So what we're going do is we're going to stop it and we're going to fix the problem. Luckily we engineered the problem ourselves, so it's as easy as commenting out this code. And now we'll start the new version. And now back here, we'll wait for the new ServicePulse window to show up. Should take just another second for ServiceControl to be available. There it is. So here we have our 18 failed messages. Now you don't have to replay each one individually, thank goodness. They're all organized in this message group. And so I'm going to bring the sales endpoint front and center here and also shipping because that's the end of the line in our system. After things go through shipping and through billing, then they... Oh, sorry through sales and then billing, they go to shipping and that's where the conversation of messages ends. So we are going to request a retry of this entire group.
11:14 David Boike
So it says retrying a whole group of message can take some time and put extra load in your system. Are you sure you wanted to retry this group of 18 messages? If that was a million messages that would certainly be a little more low than 18, I think we'll probably be pretty good with 18. We say yes and it starts to initialize the request and it's sending messages and there is a little bit of a delay, which I can use to take a drink of water. And there we go, all the messages in sales and all the in shipping.
11:45 David Boike
So all of those things that were stuck in that system, all of those things that in a normal system would be a yellow screen of death, we were able to capture it in error queue. We were able to fix the underlying issue and then we were able to completely resolve it. And the system is consistent all the way through without the front end users ever needing to know anything about it, which I think is pretty cool. That's one of the things I love most about NServiceBus.
12:13 David Boike
Next up, now that we've shown how to deal with failed messages, now we're going to show how to go to a real message transport. And in this case, I'm going to upgrade from the learning transport to RabbitMQ. So first I guess I want to show you RabbitMQ. I have it running in a container here. So this is a brand new RabbitMQ instance that I instantiated using a Docker container. It's absolutely empty. You can see that there's zero queues. And so we'll have to create some queues in order to use that too, but we'll see how that's done.
12:49 David Boike
So to update to an actual real transport, well, first of all, we need to actually remove the platform from starting up because that uses the learning transport, so we won't be using ServiceControlled ServicePulse for the rest of this demo. So let's just cancel that out. All right, and next we need to actually add the RabbitMQ package. So we are going to do dot... We are going to lose our PowerShell window. Back in our PowerShell window. We are going to do dotnet add to the ClientUI package NServiceBus RabbitMQ, and it does all of its nuget stuff.
13:33 David Boike
And we are still waiting. Okay. We need to do the same thing for basically each of our endpoints. So, oh, that's the old one. So I'll just type it again, dotnet add Sales package NServiceBus.RabbitMQ. And you can tell all your friends how your favorite part about this whole thing was watching David type basically the same thing four times. dotnet add Shipping package NServiceBus.RabbitMQ. Okay, so let's start... I'm surprised it didn't make me refresh. So let's start in actually the shipping endpoint and let's go to program. And here, before we had from the last webinar, UseTransport LearningTransport. So we are just going to change that to RabbitMQTransport, and it's there in Intel sense now that we have the package included.
14:44 David Boike
And now at this point, there are more settings that we need, but NServiceBus does a pretty good job of telling you what that is just by running it and dealing with the exceptions that it throws. So let's use shipping as a test case. So let's just run the shipping endpoint. We'll also look at what happens over here in the RabbitMQ window. So I'm just going to debug the shipping endpoint and I'm sure we'll get an exception. And what is it? Transport connection string has not been explicitly configured by a connection string method. Here's an example of what is required, endpointConfigUseTransport. It's telling us exactly what to do. I think that's a pretty good lesson about NServiceBus. We really try our best to provide good exception messages that tell you exactly what you need to do in that situation. So listen to them, I guess is my takeaway. We do get support cases where people ask, "Well, what's the problem with this?" And we go, "Well, did you read the exception message?" Oh, so let's do that. So I'm going to stop it. And I'm going to in the RabbitMQ transport, this actually returns a variable. So we're going to make that var transport equals, and then we can do transport.ConnectionString. And my connection string locally is host equals hostos.
16:13 David Boike
I'm running Windows within a virtual machine on macOS, and so the Docker container is actually on my Mac system, which is why I'm using this host entry. And then the username equals RabbitMQ and the password equals RabbitMQ and that's just how I have my container set up and semicolons are always important. So now that we have that, let's try it again and see what it says next. So we try running it again. A routing topology must be configured with one of the endpoint configuration dot use transport, use something, something, something routing topology methods. This is where you'd probably want to go check our documentation and I'll give you a shortcut. Basically in any new RabbitMQ system you're creating, you want to use the use conventional routing topology. I work here and I don't even remember what the differences are, but it basically describes how the transport uses the queues and topics and everything that's in RabbitMQ in order to route messages. So it's just something that you have to pick and that all of your endpoints have to agree on. So let's try it again, just the shipping endpoint.
17:34 David Boike
And now we get to somewhere where I'm actually not happy with our exception message, and I'm going to be opening an internal issue on this because it says operation interrupted exception. The AMQP operation was interrupted, the close-reason, blah, blah, blah. The important part is no queue shipping in the host. I don't think that's very valuable. I think we should fix that, and I'm going to make sure we do try to fix that. But for this webinar, what this really means is that we aren't creating queues. If we look at RabbitMQ and even if we hit at five to refresh that, we've got no queues. Obviously we can't connect to queues that aren't there. So, NServiceBus has a feature called enable installers. And so what we want to do for this while we're still in development is we want to do endpoint... And they capitalized it. Thank you, not, endpointConfiguration.EnableInstallers.
18:31 David Boike
Now this is potentially something you only want to do in development. So this is something that would be almost good to put an if def around to only do it for debug, not for release because in development, you want that rapid iteration. You want it to create whatever cues it needs, but in production, you should almost usually create your queues as some sort of a DevOps process. And then not touch them from each endpoint. But while we're in development like this, it's easiest to just do enable installers, and then the queues are created for us.
19:04 David Boike
So now let's run shipping and let's see what happens. Well, we're running. It's not doing anything because we're not sending in messages. But if we go look at RabbitMQ, we see that a lot has happened in here. We have a shipping queue, we have an error queue to send those errored messages to, and we have all these NSB delay level queues. And this is our implementation of delayed messages natively in RabbitMQ. Each one of these queues represents a different number of seconds delay where we do a bunch of binary math and give it a route to go through all of the queues so that once it arrives out at the end, it has been delayed by the amount of seconds you intended it. So you can just trust that those queues are there and not really look at them. The main ones are the shipping and the error. So we've got that, so that's good.
20:03 David Boike
Now we want to make the rest of the system work. So we need to copy our new configuration code to the other endpoints. So we've got the transport configuration and we've got the persistence configuration, and we're doing that because we have we have a saga in this endpoint. I'm just going to copy that to the other ones as well. We're still going to use learning persistence in this case, which is still storing it to the disc locally. If we were going for a full production setup, then we'd want to change that to something like our SQL persistence, that stores that kind of stuff to a SQL database or your persistence of choice. So I'm going to copy this whole chunk, and we're going to basically add this to the rest of the program CS files.
20:48 David Boike
So here is the sales endpoint, and I'll basically replace this with that. And platform doesn't matter, messages doesn't contain, it's just messages. It doesn't contain any endpoints. The client UI, here's the transport part. So we'll replace there and then the billing, and we will also replace there. If I did my copy pasting correctly, that should now work and we can run it, and we'll see if the system works on RabbitMQ.
21:26 David Boike
Here's our client UI again. Everything seems to be starting up fine. Let's check our queues. And now we can see we've got billing, sales, shipping, and error. There is not a client UI because the client UI application is a send only endpoint. So it doesn't have an actual queue that it processes messages from. So now we can hide this and we should be able to send a message and I'll put shipping here again, since that's where most of the activity happens with the saga that we have from the last webinar. So place an order. No, no, immediate retry, because why? Oh, the process cannot access the file in the saga storage. That is unfortunate. And that is why you should not use development only transport or persistence in a production system because sometimes things like that happen.
22:27 David Boike
So what I'm going to do is I'm going to go into file Explorer and in the bin, debug .net 6. This is the folder where that stuff gets stored. And so I'm just going to blow that folder away and hope that we have a better time now. So here we go. We sent messages, they went through sales and billing, and we have this saga and the shipping endpoint that first it receives the order placed, and it says, "Hey, we received order placed. Should we ship it?" Well, it hasn't been billed yet. So the answer is no.
23:07 David Boike
And then we get the order bill and it says, "Hey, we've gotten order billed. And now placed equals true and build equals true. Should we ship it?" And yes, order is placed and build. It's time to ship. And then we also had a timeout in there that because saga was completed, there was no saga found for the timeout, and so it was basically just thrown away, not a big deal.
23:27 David Boike
So we have taken a system from our learning transport to RabbitMQ in less than 15 minutes. So not actually that terrible. Next up any product system should really have tests. And that includes your NServiceBus handlers as well. So we're going to show now how to do some testing on an NServiceBus saga. So we're going to actually make tests for the shipping endpoint for the Shipping Policy saga. I'm going to close everything else.
24:05 David Boike
This is that saga you saw me narrating, the once we receive the order placed message and the order build message, then we want to go into this process order method and ship it. So first we're going to create a project for the tests and yes, you can go away. So we want a new class library, so dotnet new classlib name ShippingTests. And we want to add it to the solution. So dotnet solution add Shipping.Tests. And we want to add a reference from shipping tests to shipping so we can actually test it. So dotnet add shipping to Shipping Tests, a reference to Shipping/Shipping.csproj. And I hope that's the right path. And it seems that it worked. So now we can go look at that and we'll have to reload because the solution changed.
25:10 David Boike
And we can look at the project file. Now, remember I said I was using Visual Studio 2022 and .NET6. So that means that the implicit usings enabled and the nullable reference types got enabled by default. We're going to go ahead and go with that. So now we need to add the testing dependencies. So it's easier to just copy in this block of code into the file rather than try to do the dotnet add reference for all four of these things and have me completely screw up typing each one of them. So we're using the Microsoft Test Sdk and we're using NUnit and the NUnit3testadapter. And then we're also using the package NServiceBus testing, which has a bunch of testing primitives that make it easier to test NServiceBus handlers and sagas.
26:02 David Boike
So if we save that and do build, just to make sure everything's okay and it is, so now we can start to do the tests. So let's actually rename Class1 to ShippingPolicyTests because the shipping policy is the thing we're going to test. So we've got shipping policy tests and let's start a new test. So use the test attribute and it's going to fight me and I have to do the usings and all of that fun stuff, and unit framework.
26:39 David Boike
Okay. So we want a public async task should not publish after order placed. And what we want to test here is once we send the saga a message that we placed the order, we don't want to ship it yet. We want to also wait for order build. So that's what we want to check with this test. So these tests allow us to use the arrange-act-assert pattern. So we've got arrange. And so what we do is we basically new up a version of the saga. So ver saga equals new shipping. No thank you, news. I don't know what a new style parser is, but I don't want one, new ShippingPolicy and we probably need... Okay, we got the using, cool. And we need to prefill the data. The data equals new ShippingPolicy.SagaData. If you don't do this, it'll be no. So that's what we to do.
27:47 David Boike
And then we need a context. So if you're familiar with NServiceBus message handlers, and in fact let's look at it, what we're going to be doing is we're going to be executing this method. So we need to input the actual order placed message and an iMessage handler context. Well, in real life, I don't even know what the class is, an NServiceBus that provides that implementation, but for testing, we want something where we can make assertions after the test is over to see that everything we thought the method was going to do, it actually did. So we'll do context equals it's a new testable message handler, testable message handler context. And it's from using NServiceBus testing. Yeah. So if we peek at this, this is a class that implements... Well, through this chain, it implements iMessage handler context, but it has all the stuff we're going to need. And it'll be more useful to look at it through IntelliSense. And we'll get to that in a second. And then we also need the command we're going to send into the saga. So var order placed equals new order placed, and I'll probably need another using, still doesn't like me using messages.
29:10 David Boike
The order ID equals we'll just call it one, two, three, four, five for the test. So that's our arrange step. Now we need the act step, which is a lot easier. We're going to basically await having the saga handle that message. So we're going to give it the order placed, and the context. Now is the fun part, the assert part. So with the stuff that's in the context, we have a whole bunch of stuff in here, including the headers and the message being handled, the message handler, published messages, replied messages, basically everything that gets done with that message handler context in the handler gets reflected in here so we can assert on it. So we can... Get rid of this for a second. We can do assert that context published messages length is equal to not one, zero. We don't want anything published after we only received one message and we can change this with should not publish messages yet.
30:21 David Boike
We could also maybe assert that context.SentMessagelength is equal to zero. Should not send messages. That's a bit of a trick there as I'll show you in a second. So let's go to our test Explorer and let's try this. So let's run it. And it fails. But why? Let's expand that a little bit. Should not send messages expected zero, but was one what's going on here? Well, it actually turns out that sent messages includes time outs and the shipping policy does register a timeout on the first message. So that can be a little confusing. It's also available as a timeout property and that's probably where we want to check. So this isn't actually a good assertion. Unfortunately, that's just something you kind of need to know about how sagas work. But messages with a delay are essentially sent messages.
31:29 David Boike
So instead let's replace that with assert that context.SentMessages.Length is equal to one and let's add the description, timeout is sent message, should not send others. And then let's actually dive down into that as well. Let's try to get that timeout, which we can do. Let's say timeout equals context.TimeoutMessages. And this is only messages that are timeouts. And let's do single, which if you're familiar with the link method, single, that will throw an exception if there's more than one. And now let's say assert that timeout within is equal to time span from seconds.
32:28 David Boike
And now let's look in the shipping policy where it does that timeout on the handle order place message. That's actually a timeout of five seconds. So that's what we want to assert from seconds five. Now, if we run this and I have the right number of parentheses, I believe that should be green. Okay, cool. We have our first test. So now let's go a step further and let's test order build. And I know some people are a little bit religious when it comes to how much you should test in a unit test. I certainly don't want to step into that today, but for the sake of showing how something will work, let's make another test and let's do public async task should publish after both events. So let's do our arrange again. And this time we're going to basically set up our arrange as kind of what our state was at the assert of the last one.
33:34 David Boike
So we're going to do save var saga equals new ShippingPolicy. New line, please. And that the data equals new ShippingPolicy SagaData, but we want to prefill that placed equals true, which is what the saga data should look like once the order placed message has come through. So then we still need a context. I'm going to copy this one from up here, and paste it down here. And then we need the order build that we're going to use. So OrderBilled equals new OrderBilled, and we want to use the same order ID equals one, two, three, four, five. So now we can go to the act phase where we do await saga.Handle orderBilled, context. And now what do we want to assert in this test? Let's assert that context.PublishedMessages.Length is equal to one should publish OrderShipped, and assert that given that the saga will be over, we want to assert that the saga is completed. So let's try these tests now. Oh, it failed. What's going on? Should publish OrderShipped expected one, but with zero. What's actually going on there?
35:17 David Boike
Well, the answer is that we never actually published order shipped. So if we go back to our shipping policy and we come down to the block of code where it was actually possible to publish that message where the order is both placed and build, all we ever did in the previous webinar was log an info message, and mark is complete. We never actually published that order shipped. So we need to do that quick. So let's actually come over here to messages and we'll add a new class, we'll call it OrderShipped. That can be a public class and we'll implement IEvent. And that needs to come from NServiceBus.
36:08 David Boike
By the way, you may have noticed that in these projects now we have implicit usings enabled and in NServiceBus eight, we will be adding an implicit using for the NServiceBus name space. So you won't have to do this anymore with NServiceBus eight. So we'll add public string order ID. And it's cranky with me because we're using nullable reference types. And that won't be non-no by the time you exit the constructor. So I actually have to say string question mark to say that string could be null. So now we can go over the shipping policy and we can change this how it's supposed to be, to say, await context.Publish new OrderShipped, and we can do order ID equals, we get the stored order ID from the saga data, and that's how we ship it. And now we're doing async on this, so we can take out the return task, completed task. And now let's see what our test does. So now we run our test and it succeeds.
37:22 David Boike
So one thing you may have noticed about these tests is there's a lot of things to assert on, and that can be really rough and you might be missing something in one of these other collections that maybe you should have asserted on and didn't even know how it was. So I would actually like to introduce you to a package called verify.NServiceBus that was created by one of our NServiceBus champs, Simon Cropp. And it makes testing even easier by allowing verification tests or approval tests on these type of tests. So what we're going to do is first, I want to show you the read me for verify NServiceBus. You need to follow the instructions in here because it requires a module initializer. We have to put this code into our project somewhere for verify to know how to deal with our testable message context and other stuff like that. So I will hide that. And the first thing we're going to need to do is to add the libraries. So we are going to do dotnet add to Shipping.Tests, the package Verify.NServiceBus.
38:46 David Boike
And then we are going to also do, dotnet add to Shipping.Tests package Verify.NUnit. So verify is a package that does approve all tests. Verify NUnit is the adapter for using NUnit. There's also one for XUnit and other frameworks. And then verify that NServiceBus is the one that knows how to represent the testable message context and other stuff like that. So in our shipping tests, one thing that if you actually look at the transitive references to these verify things, you can remove NUnit because it's a dependency of verify NUnit, and you can remove NServiceBus testing because it is a dependency of verified NServiceBus. And then that prevents you from getting some duplicate reference things that might happen. So that builds. Now we need to create that module initializer that we looked at in the readme. So I'm going to add a class module initializer, very tiny CS, and I'm going to copy and paste that in. So this has got the module initializer attribute, which tells dotnet basically to run it when it starts up and in the initialize, it's calling this VerifyNServiceBus.Enable. Okay, can close that.
40:21 David Boike
Now we can take this test in shipping policy tests, and we can remove all of our asserts, because we're not going to need them. I'll just comment them out. And instead we're going to do await verify context. And if you're wondering where that method is coming from, it's actually part of the class verifier. But the, excuse me, the verify NUnit packages are registering implicit usings for the verifier and a static using for verifier.verify. So if you were still using .NET 5, or if you were still using Visual Studio 2019, you'd have to do verifier.verify, but this saves us a few keystrokes, so that's great. So let's run this test again and see what happens. And it's going to failed. I'll tell you that right now. You can see that it failed, but what happened is it popped up my diff viewer and it's giving me adjacent representation of what happened in that message handler context. So if I sent messages, there would be sent messages. All I did is I published a message with OrderShipped and the deal is you take this and you copy it to the right and you save. And what that does is you now have a... This is the name of your test .verify.txt, where this is .received.txt.
41:52 David Boike
So when you are running the test, it generates the new version by converting the context to adjacent representation. And then it does a comparison. And if it matches what you already have committed to your repository, then the test passes. So now if I were to run it again, it will succeed. But I don't know if you noticed, but I'm actually going to go to the file Explorer and I'm going to delete that file. I'm going to delete both of them because I want to see that diff viewer again. So let's rerun the test and I don't know if you noticed, but it says OrderShipped and there's nothing inside of it. This is actually really good because this shows something that was actually missing from our test before because we had a missing assert. It doesn't have an order ID in there. And the reason is unfortunately, a current limitation in our test infrastructure, you basically have to set up your saga data exactly how it should be before the test runs. And in this case, that means the data should had order ID equals one, two, three, four, five in it.
43:02 David Boike
So now if we run the test again, we will see more what we expect that we get an order shipped with an order ID property that's one, two, three, four, five. So we can copy that. This file gets committed to the repository. And then when we run the test again, everything is cool. I am actually currently working with a team right now to make this better, so you don't have to know exactly how NServiceBus handles sagas in all of these conditions. And you can do a little bit more black box testing of your NServiceBus sagas. So I think that's pretty exciting. Of course, I do. I'm working on it. So keep a watch out for that. It'll be released sometime soon.
43:49 David Boike
Okay. So now we've finished with testing. We didn't really show a handler testing, but a handler is just a saga with no state. So you don't have to worry about this stuff. You just create a new version... New up your handler and you execute it and it looks the exact same. So that's good enough for a quick webinar, because otherwise we won't be able to get to behaviors, which is next. So behaviors are a feature in NServiceBus that allows you to amend the message processing pipeline to your own liking. And it's really kind of advanced get out of jail free card feature because I haven't really run into much of anything that you can't customize with a behavior.
44:33 David Boike
They were really great for doing cross-cut things. So instead of having a message handler that first logs when it starts and then it tries to do like a set up something and create a whole bunch of things, and you sometimes find these message handlers that are 100 lines long, and the actual business logic is only four lines stuck in the middle of that. That's what behaviors are best at, and I'm going to show you how to build one right now. So first I'm going to create a conventions project.
45:06 David Boike
I'm going to do a dotnet new classlib name, RetailSystem.Conventions. So a lot of times I see companies basically doing CompanyName.Conventions. So I suppose we would do Particular.Conventions. And what that means is it's conventions for how endpoints are supposed to run within your environment. So we'll also do dotnet solution add RetailSystem.Conventions. And we'll do, let's see, we want to add NServiceBus to that. So dotnet add RetailSystem.Conventions. We want to add the package NServiceBus, and what did I do wrong? I misspelled it dotnet add RetailSystem.Conventions package NServiceBus. That's better.
46:06 David Boike
And we will also add the conventions to the shipping project. So we're going to test this out in the shipping project. So we'll do dotnet add shipping. We'll get a reference to RetailSystem.Conventions, RetailSystem.Conventions. csprog. Cool. Now we have what we need, and reload the solution. RetailSystem.Conventions. So we've got one class in here. Well, let's look at the project. I always like to look at the project to make sure everything looks like I intended to. And I've got my basic settings here and my NServiceBus dependency. So I'm good to go. So I'm going to rename this class and we're going to do a very simple behavior. It's just a timer behavior.
47:01 David Boike
It's kind of the simplest thing that'll show how to build these things. And so now that it's been renamed to timer behavior, I'm going to just right away add using NServiceBus.Pipeline. And that's how you know you're doing something advanced in NServiceBus. We try to put all of the common stuff right in the NServiceBus name space, but then more advanced stuff is hidden in deeper name spaces. So welcome to advanced, I guess. So in this class we're going to inherit from behavior of type and I'm going to put in Iinvoke handler context and I'll tell you what that is in a second after we get this implemented.
47:55 David Boike
So a behavior always uses a context and the context describes basically what stage of the pipeline that you are wanting to attach your message to. So for example, the incoming message pipeline has what starts off with an ITransport received context. And if we start looking at what's in here in intellisense, the transport received context has a message, the physical message being processed and a builder for creating dependencies and not a lot else because it's the earliest step in the process. But then if we step up to the next part is the IIncoming physical message context. And this is when the incoming message is basically an amount of bites basically represented it somehow either as a string or a memory or whatever. So in this case now we've actually parsed our message headers, so I believe we have message headers available. So this is a great place to do things that require the byte stream and the message headers like dealing with encryption or message signing or even converting the bytes on the wire into a different format, compression, whatever you want to do in there.
49:20 David Boike
Next up is after incoming physical message context, there's incoming logical message context. And this is after the message is gone through deserialization. So at this point, we can look at actually updating the message instance to something else. We can do sends and replies and all that stuff. We can look at the headers, the message handled, the message ID. There's a lot of things we can do here with the object that actually represents the message and the headers. And then lastly, and the one we are going to use, which I already showed here is the invoke handler context. And this is basically right before we invoke your user code in your IHandle messages class.
50:07 David Boike
Oh, and I should also mention, there's also an outgoing pipeline and it has a bunch of context too. There's an outgoing send context, an outgoing publish context, an outgoing reply context, those all wrap in to outgoing logical message context, which is the mirror of the incoming logical, an outgoing physical message context, which mirrors the incoming physical. And then there's a routing context and a batch dispatch context and a dispatch context, a lot of places to hook on functionality. But now let's return to the invoke handler context and let's fill in some code. So in here we have the context and we already looked at all of the things we can and access in here. And then we have this next thing. And the point of next is we want to invoke it when we want to invoke the rest of the pipeline. So in our case, that means wherever we want to actually handle or process the user code in the IHandle messages class, that's where we want to do that.
51:05 David Boike
So for this specific behavior, we're going to need a logger. So let's do static ILog and didn't want that. It'll help me eventually using NServiceBus logging. log equals LogManager.GetLogger and we'll give the timer behavior. So now we have a logger. So this is a timer behavior. We're basically timing how long the message handler takes. So we're going to create a stopwatch, equals stopwatch, and it's actually the class that we want. So if Visual Studio could help us, that would be great using system diagnostics. Stopwatch.StartNew. And then we want to get rid of this throw. We do not want to throw an exception. We want to do a try finally block because even if we throw an exception in our message handler, we want this thing to happen.
52:09 David Boike
So within the try, we're going to do that wait next. And within the finally, that's where we say stopwatch stop. And then we're going to say log.InfoHandling, and we can look at our context here, context.MessageBeingHandled.GetType.FullName in context.MessageHandler.HandlerType.FullName. Then we can just say took and then the stopwatchElapsedMilliseconds, and labeled that as milliseconds. See the end of that line.
53:07 David Boike
So handling the message type in the handler type took a number of milliseconds and that's basically it for the behavior itself. The only other thing we need to do is we need to register it. So in shipping for now, we can go to program and in our endpoint configuration, we can say endpointConfiguration.Pipeline.Register. And there is a bunch of choices, including something that uses something called a register step that you see there. But the easiest way to do this right now is to basically find the overload where you just create an instance of your behavior, assuming it's a Singleton and give it a description. So we are going to do a new timer behavior and then description is provides timing for each message handler. And we need to get a using for the timer behavior from our RetailSystem.Conventions. So this is probably the last demo we'll have timer for. So I'll start and it'll be slow building all the new code I just wrote. So we're looking at our shipping window, and we want to see those timers come out.
54:32 David Boike
So let's place an order. And so now we see here handling messages order placed in shipping policy took 48 milliseconds. Handling order build in shipping policy took 51 milliseconds. So we don't have to put that kind of repetitive logic into each one of our message handlers. Instead, we can create something that's reusable and we can register it with our endpoint at the endpoint config and in the future, maybe I'll show alternate ways of organizing that to make that easier to do. But for today, I think that's about all the time we have. So William, do we have any questions? Oh, before I get to questions, I want to mention that if you want to learn more about... I don't want to share that. No. If you want to learn more about not just the details of how to create an NServiceBus system, but really the theory behind how to do it effectively, I really recommend our absolutely free Distributed Systems Designed Fundamentals course, which is several hours of free training directly from Udi Dahan. You can get to it at this link, the go.particular.net/nsb-webinar.

About David Boike

David Boike is a solution architect who wrote two editions of Learning NServiceBus before joining Particular Software. When not educating developers about the potential of NServiceBus, he can be found smoking a brisket, brewing craft beer, or just trying to keep up with his two young children.

Additional resources

Need help getting started?