How to easily extend your app using MediatR notifications
August 31, 2016 · 6 minute read
"You don't want domain knowledge leaking into your controllers".
Probably the biggest question you face as a developer every single day is “where to put your code”? Should you use repositories or query classes? Should you have one class or two to represent your domain object? Should you write a new class or extend an existing one?
In short what constitutes a single responsibility and how separate should your concerns actually be?
MediatR Notifications can help with some of these thorny issues.
An example
Imagine you’ve created your shiny new web app and put in place a nice and simple contact us form.
Abiding by the KISS principle you’ve added some basic functionality so that when your customers complete this form it sends an email with their details.
public IHttpActionResult Post([FromBody]ContactUsForm contactUs)
{
var mailMessage = new MailMessage(contactUs.EmailAddress, "you@yoursite.com");
mailMessage.Subject = "Contact from web site";
mailMessage.Body = contactUs.Message;
var smtp = new SmtpClient();
smtp.Send(mailMessage);
return Ok();
}
Granted, this is a simplistic implementation for demo purposes. In reality you’d be thinking about exception handling etc.
Now your boss comes along and asks for the ability to report on how often people are submitting their details.
OK, so that’s fine. Once you’ve sorted out your database schema etc. you add some code to also save this request to the database.
public IHttpActionResult Post([FromBody]ContactUsForm contactUs)
{
var mailMessage = new MailMessage(contactUs.EmailAddress, "you@yoursite.com");
mailMessage.Subject = "Contact from web site";
mailMessage.Body = contactUs.Message;
var smtp = new SmtpClient();
smtp.Send(mailMessage);
var userMessage = new UserMessage {
Received = System.DateTime.Now,
UserEmailAddress = contactUs.EmailAddress,
UserFullName = contactUs.FullName
};
db.UserMessages.Add(userMessage);
db.SaveChanges();
return Ok();
}
At this point your Single Responsibility Principle alarm bells are going off but you decide to leave it alone for now.
This boss of yours is never happy so they come back a few days later and ask if you can also save the customer’s details to your CRM system.
Back to the controller we go.
public class ContactController : ApiController
{
using BlogSite.Models;
using BlogSite.Models.Crm;
using System.Net.Mail;
using System.Web.Http;
namespace BlogSite.Controllers.Api
{
public class ContactController : ApiController
{
private ApplicationDbContext db = new ApplicationDbContext();
public IHttpActionResult Post([FromBody]ContactUsForm contactUs)
{
var mailMessage = new MailMessage(contactUs.EmailAddress, "you@yoursite.com");
mailMessage.Subject = "Contact from web site";
mailMessage.Body = contactUs.Message;
var smtp = new SmtpClient();
smtp.Send(mailMessage);
var userMessage = new UserMessage
{
Received = System.DateTime.Now,
UserEmailAddress = contactUs.EmailAddress,
UserFullName = contactUs.FullName
};
db.UserMessages.Add(userMessage);
db.SaveChanges();
var crm = new CrmInterface();
crm.Connect();
crm.SaveContact(new CrmContact { EmailAddress = contactUs.EmailAddress, FullName = contactUs.FullName });
return Ok();
}
}
}
}
(Again, a somewhat simplified example).
Taking stock
And so it goes on. Every new requirement results in a change to the existing controller.
This is potentially risky. Any kind of change to existing code raises the possibility of breaking existing functionality.
Furthermore this code executes synchronously. If your CRM system or mail server is slow at handling requests your users are going to be waiting around for a while.
Separating Concerns
By now it’s very clear that this controller action is doing a tad too much. One approach would be to pull each distinct responsibility into it’s own class.
using BlogSite.Services.Crm;
using BlogSite.Services.Data;
using BlogSite.Services.Email;
using System.Web.Http;
namespace BlogSite.Controllers.Api
{
public class ContactController : ApiController
{
private IEmailService _emailService;
private IUserMessageService _userMessageService;
private ICrm _crm;
public ContactController(IEmailService emailService, IUserMessageService userMessageService, ICrm crm)
{
_emailService = emailService;
_userMessageService = userMessageService;
_crm = crm;
}
public IHttpActionResult Post([FromBody]ContactUsForm contactUs)
{
_emailService.Send(contactUs.EmailAddress, "you@yoursite.net", "Contact from web site", contactUs.Message);
_userMessageService.RecordUserMessageReceived(contactUs.EmailAddress, contactUs.FullName);
_crm.SaveContact(new CrmContact { EmailAddress = contactUs.EmailAddress, FullName = contactUs.FullName });
return Ok();
}
}
}
This is better, concerns have been separated and the controller action is looking a lot leaner.
However you’re still running synchronously (you could amend the services to run asynchronously), your controller is directly coupled to the services (look at the usings for evidence of that one) and you’ll still need to come back and modify this controller if your demanding boss swings back around with another feature request.
What’s in a name?
At this point you’ve had to think what to call these separate classes.
Classes with names like “service” and “manager” tend to get bigger and bigger over time and rarely stick to one responsibility.
If you try using specific verbs instead you’re inevitably left wondering what to call the method. For example, SendEmail.Send();
feels a bit clunky.
It does have the benefit of encouraging single responsibility though: SendEmail.SaveToDatabase();
clearly suggests a new class is trying to break out.
Switching to MediatR Notifications
So how can MediatR help?
If you’re new to MediatR check out this post on simplifying your controllers with the command pattern and MediatR for a quick recap.
One of the things MediatR gives you is the option to raise one notification in your code then have multiple actions performed off the back of it.
Instead of putting all your business logic in the controller action (or indeed delegating to multiple services) you can publish a notification.
Notifications can be published synchronously or asynchronously.
public class ContactController : ApiController
{
private IMediator _mediator;
public ContactController(IMediator mediator)
{
_mediator = mediator;
}
public async Task<IHttpActionResult> Post([FromBody]ContactUsForm contactUs)
{
var messageReceivedFromUserNotification = new MessageReceivedFromUserNotification {
EmailAddress = contactUs.EmailAddress,
FullName = contactUs.FullName,
Message = contactUs.Message,
SubmittedAt = DateTime.Now
};
await _mediator.PublishAsync(messageReceivedFromUserNotification);
return Ok();
}
}
From the controller’s perspective that’s it, job done, it can put it’s feet up and never be bothered again.
The notification is effectively a DTO representing the details of the notification.
public class MessageReceivedFromUserNotification : IAsyncNotification
{
public DateTime SubmittedAt { get; set; }
public string FullName { get; set; }
public string EmailAddress { get; set; }
public string Message { get; set; }
}
Next up you’ll need a handler for each distinct action you want to take when this notification is raised.
public class SaveUserMessage : IAsyncNotificationHandler<MessageReceivedFromUserNotification>
{
private ApplicationDbContext db = new ApplicationDbContext();
public async Task Handle(MessageReceivedFromUserNotification notification)
{
var userMessage = new UserMessage
{
Received = notification.SubmittedAt,
UserEmailAddress = notification.EmailAddress,
UserFullName = notification.FullName
};
db.UserMessages.Add(userMessage);
await db.SaveChangesAsync();
}
}
Adding additional functionality becomes an exercise in adding new handlers.
public class NotifySalesUserMessageReceived : IAsyncNotificationHandler<MessageReceivedFromUserNotification>
{
public async Task Handle(MessageReceivedFromUserNotification notification)
{
var mailMessage = new MailMessage(notification.EmailAddress, "you@yoursite.com");
mailMessage.Subject = "Contact from web site";
mailMessage.Body = notification.Message;
var smtp = new SmtpClient();
await smtp.SendMailAsync(mailMessage);
}
}
Surviving flaky external services
Finally, if you’re communicating with external systems (database, CRM, SMTP servers) and really need resilience even if those services go down, you might consider going even further than MediatR and use a service bus.
For example, you could implement queues using Azure Service Bus, RabbitMQ etc.
This takes the decoupling one step further and ensures your “notifications” can be persisted and retried in the event of any external system “flakiness”.
The good thing is, if you use MediatR in the first place, switching to a messaging/queuing approach is a simple exercise.