How (not) to send emails from your web site – Topics and Subscriptions (Part 3)
February 4, 2016 · 4 minute read
So we’d got our contact us form working on the web site but now we had another requirement.
The boss wanted to know how many customers were using it.
If a tree falls in a forest…
The queue approach had successfully eliminated the database from our super email sender and so we didn’t have any data to report on. Once the message was consumed. it was cleared from the queue and that was that.
No problem, we just needed to be able to consume a “send email request” message twice, once to actually send it and once to log it in the database. Azure’s topics and subscriptions came to the rescue.
Now the user would complete the contact us form (as before) but the message would go to a topic (instead of a queue).
The benefit of topics is that they can have multiple subscriptions. In this case that meant having one subscription to send the email and another to log the event in a database.
Sending messages to a topic
So in code we now needed to send to a topic instead of a queue. Here’s an example with hardcoded values.
private static void SendMessage(string connectionString)
{
var topicClient = TopicClient.CreateFromConnectionString(connectionString, "contact-us");
var contactUsMessage = new ContactUsMessage
{
Subject = "Please tell me about your product",
Message = "I'm interesting in knowing more about the super email sender.",
CustomerEmail = "jon@azureinsights.net"
};
topicClient.Send(new BrokeredMessage(contactUsMessage));
}
We also made a subtle change in language from the example in part 2. We switched the terminology from send email to contact us. It made sense to log the user’s intent (to contact us) rather than the technicalities of how that interaction would actually occur.
After all, there’s no guarantee we’d continue to use email for the contact, we might have switched to a CRM, Huddle, Slack etc.
Consuming subscriptions
On the receiving side we now had two consumers. Firstly we needed to send the email.
private void ReceiveMessage(string connectionString, Emailer emailer)
{
var sendEmailSubscription = SubscriptionClient.CreateFromConnectionString(connectionString, "contact-us", "send-email");
sendEmailSubscription.OnMessage(message =>
{
try
{
var contactUsMessage = message.GetBody<ContactUsMessage>();
emailer.SendEmail(
to: "someone@abc.com",
@from: contactUsMessage.CustomerEmail,
subject: contactUsMessage.Subject,
body: contactUsMessage.Message);
message.Complete();
}
catch (Exception)
{
message.Abandon();
}
});
}
Here we’re consuming messages from the send-email subscription which is subscribed to the contact-us topic.
In addition, we needed to log the request for auditing/reporting purposes.
private void AuditMessage(string connectionString, AuditLog auditLog)
{
var sendEmailSubscription = SubscriptionClient.CreateFromConnectionString(connectionString, "contact-us", "audit-request");
sendEmailSubscription.OnMessage(message =>
{
try
{
var contactUsMessage = message.GetBody<ContactUsMessage>();
auditLog.LogContactUsRequest(contactUsMessage);
message.Complete();
}
catch (Exception)
{
message.Abandon();
}
});
}
The actual storage in this case was a SQL database but could have been whatever made sense for reporting purposes. Note we’re now consuming from the audit-request subscription which is also subscribed to the contact-us topic.
Timing and reliability
Note that each subscription is independent. In our example, once the user submitted the contact us form, the audit subscriber and email sender would both pick the request up.
There is no guarantee about the order or timing involved. For example if we ever stopped the Audit service from running, the messages would back up on the audit subscription but the email consumer would continue to process them.
This was a real benefit in our scenario because it meant we didn’t lose vital reporting information even if our service/database went down. We just kept the message on the subscription until we could handle it.
On the flip side, any SQL outage had no effect on the emails being sent.
Hosting your consumers
In our case we started off hosting the two consumers in separate Topshelf services. This gave us the option of starting/stopping the services individually.
They could of course both run in the same service.
Soon after we introduced Azure Service Bus we also migrated the consumers to Cloud Service Worker Roles.
Summary
Does your application need to handle user interactions in multiple ways? Do you want to architect your application into microservices, each with it’s own well defined responsibilities?
If so then Messaging, Queues, Topics and Subscriptions might just be the answer, it certainly helped us.