If you’re building web applications, you will often encounter situations where delegating work to some asynchronous background process can be beneficial. Let’s take a very simple example: Sending emails!
We’re in the cloud
Let’s say we’re hosting the website in Azure, as an Azure Web Site, and we need to send an email at the end of some web request. The most obvious way of doing that could be to new up an
SmtpClient with our SendGrid credentials, and then just construct a
MailMessage and send it.
While this solution is simple, it’s not good, because it makes it impossible for us to have higher uptime than SendGrid (or whichever email provider you have picked). In fact, every time we add some kind of synchronous call to the outside from our system, we impose their potential availability problems on ourselves.
We can do better π
Let’s make it asynchronous
Now, instead of sending the email directly, let’s use Rebus in our website and delegate the integration to external systems, SendGrid included, to a message handler in the background.
This way, at the end of the web request, we just do this:
|
await _bus.Send(new SendEmail(recipient, subject, body)); |
and then our work handling the web request is over. Now we just need to have something handle the
SendEmail message.
Where to host the backend?
We could configure Rebus to use either Azure Service Bus or Azure Storage Queues to transport messages. If we do that, we can host the backend anywhere, including as a process running on a 386 with a 3G modem in the attic of an abandoned building somewhere, but I’ve got a way that’s even cooler: Just host it in the web site!
This way, we can have the backend be subject to the same auto-scaling and whatnot we might have configured for the web site, and if we’re a low traffic site, we can even get away with hosting it on the Free tier.
Moreover, our backend can be Git-deployed together with the website, which makes for a super-smooth experience.
How to do it?
It’s a good idea to consider the backend a separate application, even though we chose to deploy it as though it was one. This is just a simple example on how processes and applications are really orthogonal concepts – in general, it’s limiting to attempt to enforce a 1-to-1 between processes and applications(*).
What we should do is to have a 1-to-1 relationship between IoC container instances and applications, because that’s what IoC containers are for: To function as a container of one, logical application. In this case that means that we’ll spin up one Windsor container (or whichever IoC container is your favorite) for the web application, and one for the backend. In an OWIN
Startup configuration class, it might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public class Startup { public void Configuration(IAppBuilder app) { ConfigureWebApi(app); ConfigureBackend(app); } static void ConfigureWebApi(IAppBuilder app) { var httpConfiguration = new HttpConfiguration(); WebApiConfig.Register(httpConfiguration); var webContainer = new WindsorContainer() .Install(new ApiHandlerInstaller()) .Install(new WebRebusInstaller()); httpConfiguration.UseWindsorContainer(webContainer); app.RegisterForDisposal(webContainer, "Windsor container for the web app"); app.UseWebApi(httpConfiguration); } void ConfigureBackend(IAppBuilder app) { var backendContainer = new WindsorContainer() .Install(new RebusHandlerInstaller()) .Install(new BackendRebusInstaller()); app.RegisterForDisposal(backendContainer, "Windsor container for the background jobs"); } } |
In the code sample above,
UseWindsorContainer and
RegisterForDisposal are extension methods on
IAppBuilder.
UseWindsorContainer replaces Web API’s
IHttpControllerActivator with a
WindsorCompositionRoot like the one Mark Seemann wrote, and
RegisterForDisposal looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public static void RegisterForDisposal(this IAppBuilder appBuilder, IDisposable disposable, string description = null) { var descriptionOfDisposable = description ?? disposable.ToString(); var context = new OwinContext(appBuilder.Properties); var token = context.Get<CancellationToken>("host.OnAppDisposing"); if (token == CancellationToken.None) { Trace.TraceInformation("{0} could not be registered for disposal because the cancellation token could not be found", descriptionOfDisposable); return; } Trace.TraceInformation("Registering for disposal: {0}", descriptionOfDisposable); token.Register(() => { Trace.TraceInformation("Disposing {0}", descriptionOfDisposable); disposable.Dispose(); }); } |
which is how you make something be properly disposed when an OWIN-based application shuts down. Moreover, I’m using Windsor’s installer mechanism to register stuff in the containers.
Rebus configuration
Next thing to do, is to make sure that I configure Rebus correctly – since I have two separate applications, I will also treat them as such when I set up Rebus. This means that my web tier will have a one-way client, because it needs only to be able to
bus.Send, whereas the backend will have a more full configuration.
The one-way client might be configured like this:
|
public class WebRebusInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { var azureServiceBusConnectionString = ConfigurationManager .ConnectionStrings["azureServiceBusConnectionString"] .ConnectionString; Configure.With(new CastleWindsorContainerAdapter(container)) .Transport(t => t.UseAzureStorageQueuesAsOneWayClient(azureServiceBusConnectionString)) .Routing(r => r.TypeBased().MapAssemblyOf<SendEmail>("backend")) .Start(); } } |
this registering an
IBus instance in the container which is capable of sending
SendEmail messages, which will be routed to the queue named
backend.
And then, the backend might be configured like this:
|
public class BackendRebusInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { var azureServiceBusConnectionString = ConfigurationManager .ConnectionStrings["azureServiceBusConnectionString"] .ConnectionString; Configure.With(new CastleWindsorContainerAdapter(container)) .Transport(t => t.UseAzureStorageQueues(azureServiceBusConnectionString, "backend")) .Start(); } } |
Only thing left is to write the
SendEmailHandler:
Email message handler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public class SendEmailHandler : IHandleMessages<SendEmail> { public async Task Handle(SendEmail message) { var mail = CreateMailMessage(message); var client = GetSmtpClient(); using(client) { await client.SendMailAsync(mail); } } MailMessage CreateMailMessage(SendEmail message) { //... } SmtpClient GetSmtpClient() { //... } } |
Conclusion
Hosting a Rebus endpoint inside an Azure Web Site can be compelling for several reasons, where smooth deployment of cohesive units of web+backend can be made trivial.
I’ve done this several times myself, in web sites with multiple processes, including sagas and timeouts stored in SQL Azure, and basically everything Rebus can do – and so far, it has worked flawlessly.
Depending on your requirements, you might need to flick on the “Always On” setting in the portal
so that the site keeps running even though it’s not serving web requests.
Final words
If anyone has experiences doing something similar to this in Azure or with another cloud service provider, I’d be happy to hear about it π
(*) This 1-to-1-ness is, in my opinion, a thing that the microservices community does nok mention enough. I like to think about processes and applications much the same way as Udi Dahan describes it.