ETA Notifications with C# and ASP.NET MVC

January 10, 2017
Written by
Hector Ortega
Contributor
Opinions expressed by Twilio contributors are their own

eta-csharp-asp

Companies like Uber, TaskRabbit, and Instacart have built an entire industry around the fact that we, the customers, like to order things instantly, wherever we are. The key to those services working? Notifying customers when things change.

In this tutorial, we'll build a notification system for a fake on-demand laundry service Laundr.io in C# and the ASP.NET MVC.

Let's get started!

Trigger a Notifications

There are two cases we'd like to handle:

  1. Delivery person picks up laundry to be delivered ( /Pickup )
  2. Delivery person is arriving at the customer's house ( /Deliver )

In a production app we would probably trigger the second notification when the delivery person was physically near the customer, using GPS.

(In this case we'll just use a button!)

This is a migrated tutorial. You can clone the original code from https://github.com/TwilioDevEd/eta-notifications-csharp

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web.Configuration;
using System.Web.Mvc;
using ETANotifications.Models;
using ETANotifications.Models.Repository;
using ETANotifications.Services;

namespace ETANotifications.Controllers
{
    public class OrdersController : Controller
    {
        private readonly IOrderRepository _orderRepository;
        private readonly INotificationService _notificationServices;

        public OrdersController() : this(new OrderRepository(), new NotificationService())
        {
        }

        public OrdersController(IOrderRepository orderRepository, INotificationService notificationServices)
        {
            _orderRepository = orderRepository;
            _notificationServices = notificationServices;
        }

        // GET: Orders
        public async Task<ActionResult> Index()
        {
            return View(await _orderRepository.FindAllAsync());
        }

        // GET: Orders/Details/5
        public async Task<ActionResult> Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }

            Order order = await _orderRepository.FindAsync(id);
            if (order == null)
            {
                return HttpNotFound();
            }

            return View(order);
        }

        // POST: Orders/Pickup/5
        [HttpPost]
        public async Task<ActionResult> Pickup(int id)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.Status = "Shipped";
            order.NotificationStatus = "Queued";

            await _orderRepository.UpdateAsync(order);

            await _notificationServices.SendSmsNotification(order.CustomerPhoneNumber, 
                "Your clothes will be sent and will be delivered in 20 minutes", GetCallbackUri(id));

            return RedirectToAction("Details", new { id = id });
        }

        // POST: Orders/Deliver/5
        [HttpPost]
        public async Task<ActionResult> Deliver(int id)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.Status = "Delivered";
            order.NotificationStatus = "Queued";
            await _orderRepository.UpdateAsync(order);

            await _notificationServices.SendSmsNotification(order.CustomerPhoneNumber, 
                "Your clothes have been delivered", GetCallbackUri(id));
            return RedirectToAction("Details", new { id = id });
        }

        // POST: Orders/UpateNotificationStatus/5
        [HttpPost]
        public async Task<ActionResult> UpdateNotificationStatus(int id, string MessageStatus)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.NotificationStatus = MessageStatus.First().ToString().ToUpper() + MessageStatus.Substring(1);

            await _orderRepository.UpdateAsync(order);
            return new HttpStatusCodeResult(HttpStatusCode.OK);
        }

        protected string GetCallbackUri(int id)
        {
            Uri requestUrl = Url.RequestContext.HttpContext.Request.Url;

            return $"{requestUrl.Scheme}://{WebConfigurationManager.AppSettings["TestDomain"]}" +
                   $"{Url.Action("UpdateNotificationStatus", "Orders", new {id = id})}";
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _orderRepository.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

Next, let's look at how we set up the Twilio REST API Client to push out notifications.

Setting up the Twilio REST Client

Here we create a helper class with an authenticated Twilio REST API client that we can use anytime we need to send a text message.

We initialize it with our Twilio account credentials stored as environment variables.  You can find the Auth Token and Account SID in the console:

Account Credentials

 

using System;
using System.Threading.Tasks;
using System.Web.Configuration;
using Twilio.Clients;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;

namespace ETANotifications.Services
{
    public interface INotificationService
    {
        Task SendSmsNotification(string phoneNumber, string message, string statusCallback);
    }

    public class NotificationService : INotificationService
    {
        private readonly ITwilioRestClient _client;
        private readonly string _accountSid = WebConfigurationManager.AppSettings["TwilioAccountSid"];
        private readonly string _authToken = WebConfigurationManager.AppSettings["TwilioAuthToken"];
        private readonly string _twilioNumber = WebConfigurationManager.AppSettings["TwilioPhoneNumber"];

        public NotificationService()
        {
            _client = new TwilioRestClient(_accountSid, _authToken);
        }

        public NotificationService(ITwilioRestClient client)
        {
            _client = client;
        }

        public async Task SendSmsNotification(string phoneNumber, string message, string statusCallback)
        {
            var to = new PhoneNumber(phoneNumber);
            await MessageResource.CreateAsync(
                to,
                from: new PhoneNumber(_twilioNumber),
                body: message,
                statusCallback: new Uri(statusCallback),
                client: _client);
        }
    }
}

Twilio REST Client ready, routes in place?  Let's now look at how we handle notification triggers.

Handle a Notification Trigger

This code handles a HTTP POST request triggered by a delivery person in the field.

It uses our INotificationServices instance to send an SMS message to the customer's phone number, which we have registered in our database.  Simple!

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web.Configuration;
using System.Web.Mvc;
using ETANotifications.Models;
using ETANotifications.Models.Repository;
using ETANotifications.Services;

namespace ETANotifications.Controllers
{
    public class OrdersController : Controller
    {
        private readonly IOrderRepository _orderRepository;
        private readonly INotificationService _notificationServices;

        public OrdersController() : this(new OrderRepository(), new NotificationService())
        {
        }

        public OrdersController(IOrderRepository orderRepository, INotificationService notificationServices)
        {
            _orderRepository = orderRepository;
            _notificationServices = notificationServices;
        }

        // GET: Orders
        public async Task<ActionResult> Index()
        {
            return View(await _orderRepository.FindAllAsync());
        }

        // GET: Orders/Details/5
        public async Task<ActionResult> Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }

            Order order = await _orderRepository.FindAsync(id);
            if (order == null)
            {
                return HttpNotFound();
            }

            return View(order);
        }

        // POST: Orders/Pickup/5
        [HttpPost]
        public async Task<ActionResult> Pickup(int id)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.Status = "Shipped";
            order.NotificationStatus = "Queued";

            await _orderRepository.UpdateAsync(order);

            await _notificationServices.SendSmsNotification(order.CustomerPhoneNumber, 
                "Your clothes will be sent and will be delivered in 20 minutes", GetCallbackUri(id));

            return RedirectToAction("Details", new { id = id });
        }

        // POST: Orders/Deliver/5
        [HttpPost]
        public async Task<ActionResult> Deliver(int id)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.Status = "Delivered";
            order.NotificationStatus = "Queued";
            await _orderRepository.UpdateAsync(order);

            await _notificationServices.SendSmsNotification(order.CustomerPhoneNumber, 
                "Your clothes have been delivered", GetCallbackUri(id));
            return RedirectToAction("Details", new { id = id });
        }

        // POST: Orders/UpateNotificationStatus/5
        [HttpPost]
        public async Task<ActionResult> UpdateNotificationStatus(int id, string MessageStatus)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.NotificationStatus = MessageStatus.First().ToString().ToUpper() + MessageStatus.Substring(1);

            await _orderRepository.UpdateAsync(order);
            return new HttpStatusCodeResult(HttpStatusCode.OK);
        }

        protected string GetCallbackUri(int id)
        {
            Uri requestUrl = Url.RequestContext.HttpContext.Request.Url;

            return $"{requestUrl.Scheme}://{WebConfigurationManager.AppSettings["TestDomain"]}" +
                   $"{Url.Action("UpdateNotificationStatus", "Orders", new {id = id})}";
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _orderRepository.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

Now let's look closer at how we send out the SMS.

Send an SMS (or MMS)

This code demonstrates how we actually send the SMS.

Something missing?  An image of the clothes, perhaps?

You can add an optional mediaUrl as well.  Glad you pointed that out!

 

var mediaUrls = new List<Uri> () {
new Uri("http://lorempixel.com/image_output/fashion-q-c-640-480-1.jpg")
};
var to = new PhoneNumber(phoneNumber);
await MessageResource.CreateAsync(
to,
from: new PhoneNumber(_twilioNumber),
body: message,
statusCallback: new Uri(statusCallback),
client: _client,
mediaUrl: mediaUrls);

In addition to the required parameters, we can pass a `status_callback` url.  Twilio will then POST there to let us know when the status of our outgoing message changes.

using System;
using System.Threading.Tasks;
using System.Web.Configuration;
using Twilio.Clients;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;

namespace ETANotifications.Services
{
    public interface INotificationService
    {
        Task SendSmsNotification(string phoneNumber, string message, string statusCallback);
    }

    public class NotificationService : INotificationService
    {
        private readonly ITwilioRestClient _client;
        private readonly string _accountSid = WebConfigurationManager.AppSettings["TwilioAccountSid"];
        private readonly string _authToken = WebConfigurationManager.AppSettings["TwilioAuthToken"];
        private readonly string _twilioNumber = WebConfigurationManager.AppSettings["TwilioPhoneNumber"];

        public NotificationService()
        {
            _client = new TwilioRestClient(_accountSid, _authToken);
        }

        public NotificationService(ITwilioRestClient client)
        {
            _client = client;
        }

        public async Task SendSmsNotification(string phoneNumber, string message, string statusCallback)
        {
            var to = new PhoneNumber(phoneNumber);
            await MessageResource.CreateAsync(
                to,
                from: new PhoneNumber(_twilioNumber),
                body: message,
                statusCallback: new Uri(statusCallback),
                client: _client);
        }
    }
}

Let's look at the status callback in greater detail.

Handle a Twilio Status Callback

Twilio will make a POST request to this resource each time our message status changes to one of the following: queued, failed, sent, delivered, or undelivered.

We then update this notificationStatus on the Order and business logic can take over. This is a great place to add logic that would resend the message upon failure, or send out an automated survey a few minutes after a customer receives an order.

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web.Configuration;
using System.Web.Mvc;
using ETANotifications.Models;
using ETANotifications.Models.Repository;
using ETANotifications.Services;

namespace ETANotifications.Controllers
{
    public class OrdersController : Controller
    {
        private readonly IOrderRepository _orderRepository;
        private readonly INotificationService _notificationServices;

        public OrdersController() : this(new OrderRepository(), new NotificationService())
        {
        }

        public OrdersController(IOrderRepository orderRepository, INotificationService notificationServices)
        {
            _orderRepository = orderRepository;
            _notificationServices = notificationServices;
        }

        // GET: Orders
        public async Task<ActionResult> Index()
        {
            return View(await _orderRepository.FindAllAsync());
        }

        // GET: Orders/Details/5
        public async Task<ActionResult> Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }

            Order order = await _orderRepository.FindAsync(id);
            if (order == null)
            {
                return HttpNotFound();
            }

            return View(order);
        }

        // POST: Orders/Pickup/5
        [HttpPost]
        public async Task<ActionResult> Pickup(int id)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.Status = "Shipped";
            order.NotificationStatus = "Queued";

            await _orderRepository.UpdateAsync(order);

            await _notificationServices.SendSmsNotification(order.CustomerPhoneNumber, 
                "Your clothes will be sent and will be delivered in 20 minutes", GetCallbackUri(id));

            return RedirectToAction("Details", new { id = id });
        }

        // POST: Orders/Deliver/5
        [HttpPost]
        public async Task<ActionResult> Deliver(int id)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.Status = "Delivered";
            order.NotificationStatus = "Queued";
            await _orderRepository.UpdateAsync(order);

            await _notificationServices.SendSmsNotification(order.CustomerPhoneNumber, 
                "Your clothes have been delivered", GetCallbackUri(id));
            return RedirectToAction("Details", new { id = id });
        }

        // POST: Orders/UpateNotificationStatus/5
        [HttpPost]
        public async Task<ActionResult> UpdateNotificationStatus(int id, string MessageStatus)
        {
            Order order = await _orderRepository.FindAsync(id);
            order.NotificationStatus = MessageStatus.First().ToString().ToUpper() + MessageStatus.Substring(1);

            await _orderRepository.UpdateAsync(order);
            return new HttpStatusCodeResult(HttpStatusCode.OK);
        }

        protected string GetCallbackUri(int id)
        {
            Uri requestUrl = Url.RequestContext.HttpContext.Request.Url;

            return $"{requestUrl.Scheme}://{WebConfigurationManager.AppSettings["TestDomain"]}" +
                   $"{Url.Action("UpdateNotificationStatus", "Orders", new {id = id})}";
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _orderRepository.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

That's a wrap (fold?)! We've just implemented an on-demand notification service that alerts our customers when their order is picked up and arriving.

Now let's look at other easy to add features for your application.

Where to Next?

We love ASP.NET and C# here at Twilio.  Here are a couple other tutorials we've written:

Workflow Automation

Increase your rate of response by automating the workflows that are key to your business. In this tutorial, learn how to build a ready-for-scale automated SMS workflow for a vacation rental company.

Masked Numbers

Protect your users' privacy by anonymously connecting them with Twilio Voice and SMS. Learn how to create disposable phone numbers on-demand so two users can communicate without exchanging personal information.

Did this Help?

Thanks for checking this tutorial out! Let us know what you've built - or what you're building - on Twitter.