One of the more abstract concepts you'll handle when building your business is what the workflow will look like.
At its core, setting up a standardized workflow is about enabling your service providers (agents, hosts, customer service reps, administrators, and the rest of the gang) to better serve your customers.
To illustrate a very real-world example, today we'll build a C# and ASP.NET MVC webapp for finding and booking vacation properties — tentatively called Airtng.
Here's how it'll work:
We'll be using the Twilio REST API to send our users messages at important junctures. Here's a bit more on our API:
AirTNG.Web/Domain/Reservations/Notifier.cs
1using System;2using System.Linq;3using System.Text;4using System.Threading.Tasks;5using AirTNG.Web.Domain.Twilio;6using AirTNG.Web.Models;7using AirTNG.Web.Models.Repository;89namespace AirTNG.Web.Domain.Reservations10{11public interface INotifier12{13Task SendNotificationAsync(Reservation reservation);14}1516public class Notifier : INotifier17{18private readonly ITwilioMessageSender _client;19private readonly IReservationsRepository _repository;2021public Notifier() : this(22new TwilioMessageSender(),23new ReservationsRepository()) { }2425public Notifier(ITwilioMessageSender client, IReservationsRepository repository)26{27_client = client;28_repository = repository;29}3031public async Task SendNotificationAsync(Reservation reservation)32{33var pendingReservations = await _repository.FindPendingReservationsAsync();34if (pendingReservations.Count() < 2)35{36var notification = BuildNotification(reservation);37await _client.SendMessageAsync(notification.To, notification.From, notification.Messsage);38}39}4041private static Notification BuildNotification(Reservation reservation)42{43var message = new StringBuilder();44message.AppendFormat("You have a new reservation request from {0} for {1}:{2}",45reservation.Name,46reservation.VacationProperty.Description,47Environment.NewLine);48message.AppendFormat("{0}{1}",49reservation.Message,50Environment.NewLine);51message.Append("Reply [accept] or [reject]");5253return new Notification54{55From = PhoneNumbers.Twilio,56To = reservation.PhoneNumber,57Messsage = message.ToString()58};59}60}61}
Ready to go? Boldly click the button right after this sentence.
For this use case to work we have to handle authentication. We will rely on ASP.NET Identity for this purpose.
Each User
will need to have a phone_number
that we will use later to send SMS notifications.
AirTNG.Web/Models/IdentityModels.cs
1using System.Collections.Generic;2using System.Data.Entity;3using System.Security.Claims;4using System.Threading.Tasks;5using Microsoft.AspNet.Identity;6using Microsoft.AspNet.Identity.EntityFramework;78namespace AirTNG.Web.Models9{10// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.11public class ApplicationUser : IdentityUser12{13public string Name { get; set; }1415public virtual IList<VacationProperty> VacationProperties { get; set; }1617public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)18{19// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType20var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);21// Add custom user claims here22return userIdentity;23}24}2526public class ApplicationDbContext : IdentityDbContext<ApplicationUser>27{28public ApplicationDbContext()29: base("AirTNGConnection", false)30{31}3233public static ApplicationDbContext Create()34{35return new ApplicationDbContext();36}3738public DbSet<VacationProperty> VacationProperties { get; set; }39public DbSet<Reservation> Reservations { get; set; }40}41}
Next let's take a look at the Vacation Property model.
Our rental application will require listing properties.
The VacationProperty
belongs to the User
who created it (we'll call this user the host from this point on) and contains only two properties: a Description
and an ImageUrl
.
A VacationProperty
can have many Reservations.
AirTNG.Web/Models/VacationProperty.cs
1using System;2using System.Collections.Generic;34namespace AirTNG.Web.Models5{6public class VacationProperty7{8public int Id { get; set; }9public string UserId { get; set; }10public virtual ApplicationUser User { get; set; }11public string Description { get; set; }12public string ImageUrl { get; set; }13public DateTime CreatedAt { get; set; }14public virtual IList<Reservation> Reservations { get; set; }15}16}
Next, let's see what our Reservation model looks like.
The Reservation
model is at the center of the workflow for this application.
It is responsible for keeping track of:
VacationProperty
it is associated with.User
who owns that vacation property (the host). Through this property the user will have access to the host phone number indirectly.Status
of the reservationAirTNG.Web/Models/Reservation.cs
1using System;2using System.ComponentModel.DataAnnotations.Schema;34namespace AirTNG.Web.Models5{6public class Reservation7{8public int Id { get; set; }9public string Name { get; set; }10public string PhoneNumber { get; set; }11public ReservationStatus Status { get; set; }12public string Message { get; set; }13public DateTime CreatedAt { get; set; }14public int VactionPropertyId { get; set; }15[ForeignKey("VactionPropertyId")]16public virtual VacationProperty VacationProperty { get; set; }17}18}
Now that our models are ready, let's have a look at the controller that will create reservations.
The reservation creation form holds only a single field: the message that will be sent to the host when one of her properties is reserved. The rest of the information needed to create a reservation is taken from the VacationProperty itself.
A reservation is created with a default status ReservationStatus.Pending
. That way when the host replies with an accept
or reject
response the application knows which reservation to update.
AirTNG.Web/Controllers/ReservationsController.cs
1using System;2using System.Threading.Tasks;3using System.Web.Mvc;4using AirTNG.Web.Domain.Reservations;5using AirTNG.Web.Models;6using AirTNG.Web.Models.Repository;7using AirTNG.Web.ViewModels;8using Twilio.AspNet.Mvc;9using Twilio.TwiML;1011namespace AirTNG.Web.Controllers12{13[Authorize]14public class ReservationsController : TwilioController15{16private readonly IVacationPropertiesRepository _vacationPropertiesRepository;17private readonly IReservationsRepository _reservationsRepository;18private readonly IUsersRepository _usersRepository;19private readonly INotifier _notifier;2021public ReservationsController() : this(22new VacationPropertiesRepository(),23new ReservationsRepository(),24new UsersRepository(),25new Notifier()) { }2627public ReservationsController(28IVacationPropertiesRepository vacationPropertiesRepository,29IReservationsRepository reservationsRepository,30IUsersRepository usersRepository,31INotifier notifier)32{33_vacationPropertiesRepository = vacationPropertiesRepository;34_reservationsRepository = reservationsRepository;35_usersRepository = usersRepository;36_notifier = notifier;37}3839// GET: Reservations/Create40public async Task<ActionResult> Create(int id)41{42var vacationProperty = await _vacationPropertiesRepository.FindAsync(id);43var reservation = new ReservationViewModel44{45ImageUrl = vacationProperty.ImageUrl,46Description = vacationProperty.Description,47VacationPropertyId = vacationProperty.Id,48VacationPropertyDescription = vacationProperty.Description,49UserName = vacationProperty.User.Name,50UserPhoneNumber = vacationProperty.User.PhoneNumber,51};5253return View(reservation);54}5556// POST: Reservations/Create57[HttpPost]58public async Task<ActionResult> Create(ReservationViewModel model)59{60if (ModelState.IsValid)61{62var reservation = new Reservation63{64Message = model.Message,65PhoneNumber = model.UserPhoneNumber,66Name = model.UserName,67VactionPropertyId = model.VacationPropertyId,68Status = ReservationStatus.Pending,69CreatedAt = DateTime.Now70};7172await _reservationsRepository.CreateAsync(reservation);73reservation.VacationProperty = new VacationProperty {Description = model.VacationPropertyDescription};74await _notifier.SendNotificationAsync(reservation);7576return RedirectToAction("Index", "VacationProperties");77}7879return View(model);80}8182// POST Reservations/Handle83[HttpPost]84[AllowAnonymous]85public async Task<TwiMLResult> Handle(string from, string body)86{87string smsResponse;8889try90{91var host = await _usersRepository.FindByPhoneNumberAsync(from);92var reservation = await _reservationsRepository.FindFirstPendingReservationByHostAsync(host.Id);9394var smsRequest = body;95reservation.Status =96smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||97smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase)98? ReservationStatus.Confirmed99: ReservationStatus.Rejected;100101await _reservationsRepository.UpdateAsync(reservation);102smsResponse =103string.Format("You have successfully {0} the reservation", reservation.Status);104}105catch (Exception)106{107smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";108}109110return TwiML(Respond(smsResponse));111}112113private static MessagingResponse Respond(string message)114{115var response = new MessagingResponse();116response.Message(message);117118return response;119}120}121}
Next, let's see how we will send SMS notifications to the vacation rental host.
When a reservation is created we want to notify the owner of the property that someone is interested.
This is where we use Twilio C# Helper Library to send a SMS message to the host, using our Twilio phone number. As you can see, sending SMS messages using Twilio takes just a few lines of code.
Next we just have to wait for the host to send an SMS response accepting or rejecting the reservation. Then we can notify the guest and host that the reservation information has been updated.
AirTNG.Web/Domain/Reservations/Notifier.cs
1using System;2using System.Linq;3using System.Text;4using System.Threading.Tasks;5using AirTNG.Web.Domain.Twilio;6using AirTNG.Web.Models;7using AirTNG.Web.Models.Repository;89namespace AirTNG.Web.Domain.Reservations10{11public interface INotifier12{13Task SendNotificationAsync(Reservation reservation);14}1516public class Notifier : INotifier17{18private readonly ITwilioMessageSender _client;19private readonly IReservationsRepository _repository;2021public Notifier() : this(22new TwilioMessageSender(),23new ReservationsRepository()) { }2425public Notifier(ITwilioMessageSender client, IReservationsRepository repository)26{27_client = client;28_repository = repository;29}3031public async Task SendNotificationAsync(Reservation reservation)32{33var pendingReservations = await _repository.FindPendingReservationsAsync();34if (pendingReservations.Count() < 2)35{36var notification = BuildNotification(reservation);37await _client.SendMessageAsync(notification.To, notification.From, notification.Messsage);38}39}4041private static Notification BuildNotification(Reservation reservation)42{43var message = new StringBuilder();44message.AppendFormat("You have a new reservation request from {0} for {1}:{2}",45reservation.Name,46reservation.VacationProperty.Description,47Environment.NewLine);48message.AppendFormat("{0}{1}",49reservation.Message,50Environment.NewLine);51message.Append("Reply [accept] or [reject]");5253return new Notification54{55From = PhoneNumbers.Twilio,56To = reservation.PhoneNumber,57Messsage = message.ToString()58};59}60}61}
Now's let's peek at how we're handling the host's responses.
The Reservations/Handle
controller handles our incoming Twilio request and does four things:
AirTNG.Web/Controllers/ReservationsController.cs
1using System;2using System.Threading.Tasks;3using System.Web.Mvc;4using AirTNG.Web.Domain.Reservations;5using AirTNG.Web.Models;6using AirTNG.Web.Models.Repository;7using AirTNG.Web.ViewModels;8using Twilio.AspNet.Mvc;9using Twilio.TwiML;1011namespace AirTNG.Web.Controllers12{13[Authorize]14public class ReservationsController : TwilioController15{16private readonly IVacationPropertiesRepository _vacationPropertiesRepository;17private readonly IReservationsRepository _reservationsRepository;18private readonly IUsersRepository _usersRepository;19private readonly INotifier _notifier;2021public ReservationsController() : this(22new VacationPropertiesRepository(),23new ReservationsRepository(),24new UsersRepository(),25new Notifier()) { }2627public ReservationsController(28IVacationPropertiesRepository vacationPropertiesRepository,29IReservationsRepository reservationsRepository,30IUsersRepository usersRepository,31INotifier notifier)32{33_vacationPropertiesRepository = vacationPropertiesRepository;34_reservationsRepository = reservationsRepository;35_usersRepository = usersRepository;36_notifier = notifier;37}3839// GET: Reservations/Create40public async Task<ActionResult> Create(int id)41{42var vacationProperty = await _vacationPropertiesRepository.FindAsync(id);43var reservation = new ReservationViewModel44{45ImageUrl = vacationProperty.ImageUrl,46Description = vacationProperty.Description,47VacationPropertyId = vacationProperty.Id,48VacationPropertyDescription = vacationProperty.Description,49UserName = vacationProperty.User.Name,50UserPhoneNumber = vacationProperty.User.PhoneNumber,51};5253return View(reservation);54}5556// POST: Reservations/Create57[HttpPost]58public async Task<ActionResult> Create(ReservationViewModel model)59{60if (ModelState.IsValid)61{62var reservation = new Reservation63{64Message = model.Message,65PhoneNumber = model.UserPhoneNumber,66Name = model.UserName,67VactionPropertyId = model.VacationPropertyId,68Status = ReservationStatus.Pending,69CreatedAt = DateTime.Now70};7172await _reservationsRepository.CreateAsync(reservation);73reservation.VacationProperty = new VacationProperty {Description = model.VacationPropertyDescription};74await _notifier.SendNotificationAsync(reservation);7576return RedirectToAction("Index", "VacationProperties");77}7879return View(model);80}8182// POST Reservations/Handle83[HttpPost]84[AllowAnonymous]85public async Task<TwiMLResult> Handle(string from, string body)86{87string smsResponse;8889try90{91var host = await _usersRepository.FindByPhoneNumberAsync(from);92var reservation = await _reservationsRepository.FindFirstPendingReservationByHostAsync(host.Id);9394var smsRequest = body;95reservation.Status =96smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||97smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase)98? ReservationStatus.Confirmed99: ReservationStatus.Rejected;100101await _reservationsRepository.UpdateAsync(reservation);102smsResponse =103string.Format("You have successfully {0} the reservation", reservation.Status);104}105catch (Exception)106{107smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";108}109110return TwiML(Respond(smsResponse));111}112113private static MessagingResponse Respond(string message)114{115var response = new MessagingResponse();116response.Message(message);117118return response;119}120}121}
Let's have closer look at how Twilio webhooks are configured to enable incoming requests to our application.
In the Twilio console, you must setup the SMS webhook to call your application's end point in the route Reservations/Handle
.
One way to expose your development machine to the outside world is using ngrok. The URL for the SMS webhook on your number would look like this:
1http://<subdomain>.ngrok.io/Reservations/Handle2
An incoming request from Twilio comes with some helpful parameters, such as a from
phone number and the message body
.
We'll use the from
parameter to look for the host and check if they have any pending reservations. If they do, we'll use the message body to check for 'accept' and 'reject'.
In the last step, we'll use Twilio's TwiML as a response to Twilio to send an SMS message to the guest.
AirTNG.Web/Controllers/ReservationsController.cs
1using System;2using System.Threading.Tasks;3using System.Web.Mvc;4using AirTNG.Web.Domain.Reservations;5using AirTNG.Web.Models;6using AirTNG.Web.Models.Repository;7using AirTNG.Web.ViewModels;8using Twilio.AspNet.Mvc;9using Twilio.TwiML;1011namespace AirTNG.Web.Controllers12{13[Authorize]14public class ReservationsController : TwilioController15{16private readonly IVacationPropertiesRepository _vacationPropertiesRepository;17private readonly IReservationsRepository _reservationsRepository;18private readonly IUsersRepository _usersRepository;19private readonly INotifier _notifier;2021public ReservationsController() : this(22new VacationPropertiesRepository(),23new ReservationsRepository(),24new UsersRepository(),25new Notifier()) { }2627public ReservationsController(28IVacationPropertiesRepository vacationPropertiesRepository,29IReservationsRepository reservationsRepository,30IUsersRepository usersRepository,31INotifier notifier)32{33_vacationPropertiesRepository = vacationPropertiesRepository;34_reservationsRepository = reservationsRepository;35_usersRepository = usersRepository;36_notifier = notifier;37}3839// GET: Reservations/Create40public async Task<ActionResult> Create(int id)41{42var vacationProperty = await _vacationPropertiesRepository.FindAsync(id);43var reservation = new ReservationViewModel44{45ImageUrl = vacationProperty.ImageUrl,46Description = vacationProperty.Description,47VacationPropertyId = vacationProperty.Id,48VacationPropertyDescription = vacationProperty.Description,49UserName = vacationProperty.User.Name,50UserPhoneNumber = vacationProperty.User.PhoneNumber,51};5253return View(reservation);54}5556// POST: Reservations/Create57[HttpPost]58public async Task<ActionResult> Create(ReservationViewModel model)59{60if (ModelState.IsValid)61{62var reservation = new Reservation63{64Message = model.Message,65PhoneNumber = model.UserPhoneNumber,66Name = model.UserName,67VactionPropertyId = model.VacationPropertyId,68Status = ReservationStatus.Pending,69CreatedAt = DateTime.Now70};7172await _reservationsRepository.CreateAsync(reservation);73reservation.VacationProperty = new VacationProperty {Description = model.VacationPropertyDescription};74await _notifier.SendNotificationAsync(reservation);7576return RedirectToAction("Index", "VacationProperties");77}7879return View(model);80}8182// POST Reservations/Handle83[HttpPost]84[AllowAnonymous]85public async Task<TwiMLResult> Handle(string from, string body)86{87string smsResponse;8889try90{91var host = await _usersRepository.FindByPhoneNumberAsync(from);92var reservation = await _reservationsRepository.FindFirstPendingReservationByHostAsync(host.Id);9394var smsRequest = body;95reservation.Status =96smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||97smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase)98? ReservationStatus.Confirmed99: ReservationStatus.Rejected;100101await _reservationsRepository.UpdateAsync(reservation);102smsResponse =103string.Format("You have successfully {0} the reservation", reservation.Status);104}105catch (Exception)106{107smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";108}109110return TwiML(Respond(smsResponse));111}112113private static MessagingResponse Respond(string message)114{115var response = new MessagingResponse();116response.Message(message);117118return response;119}120}121}
Now that we know how to expose a webhook to handle Twilio requests, let's see how we generate the TwiML needed.
After updating the reservation status, we must notify the host that they have successfully confirmed or rejected the reservation. We also have to return a friendly error message if there are no pending reservations.
If the reservation is confirmed or rejected we send an additional SMS to the guest to deliver the news.
We use the verb Message from TwiML to instruct Twilio's server that it should send the SMS messages.
AirTNG.Web/Controllers/ReservationsController.cs
1using System;2using System.Threading.Tasks;3using System.Web.Mvc;4using AirTNG.Web.Domain.Reservations;5using AirTNG.Web.Models;6using AirTNG.Web.Models.Repository;7using AirTNG.Web.ViewModels;8using Twilio.AspNet.Mvc;9using Twilio.TwiML;1011namespace AirTNG.Web.Controllers12{13[Authorize]14public class ReservationsController : TwilioController15{16private readonly IVacationPropertiesRepository _vacationPropertiesRepository;17private readonly IReservationsRepository _reservationsRepository;18private readonly IUsersRepository _usersRepository;19private readonly INotifier _notifier;2021public ReservationsController() : this(22new VacationPropertiesRepository(),23new ReservationsRepository(),24new UsersRepository(),25new Notifier()) { }2627public ReservationsController(28IVacationPropertiesRepository vacationPropertiesRepository,29IReservationsRepository reservationsRepository,30IUsersRepository usersRepository,31INotifier notifier)32{33_vacationPropertiesRepository = vacationPropertiesRepository;34_reservationsRepository = reservationsRepository;35_usersRepository = usersRepository;36_notifier = notifier;37}3839// GET: Reservations/Create40public async Task<ActionResult> Create(int id)41{42var vacationProperty = await _vacationPropertiesRepository.FindAsync(id);43var reservation = new ReservationViewModel44{45ImageUrl = vacationProperty.ImageUrl,46Description = vacationProperty.Description,47VacationPropertyId = vacationProperty.Id,48VacationPropertyDescription = vacationProperty.Description,49UserName = vacationProperty.User.Name,50UserPhoneNumber = vacationProperty.User.PhoneNumber,51};5253return View(reservation);54}5556// POST: Reservations/Create57[HttpPost]58public async Task<ActionResult> Create(ReservationViewModel model)59{60if (ModelState.IsValid)61{62var reservation = new Reservation63{64Message = model.Message,65PhoneNumber = model.UserPhoneNumber,66Name = model.UserName,67VactionPropertyId = model.VacationPropertyId,68Status = ReservationStatus.Pending,69CreatedAt = DateTime.Now70};7172await _reservationsRepository.CreateAsync(reservation);73reservation.VacationProperty = new VacationProperty {Description = model.VacationPropertyDescription};74await _notifier.SendNotificationAsync(reservation);7576return RedirectToAction("Index", "VacationProperties");77}7879return View(model);80}8182// POST Reservations/Handle83[HttpPost]84[AllowAnonymous]85public async Task<TwiMLResult> Handle(string from, string body)86{87string smsResponse;8889try90{91var host = await _usersRepository.FindByPhoneNumberAsync(from);92var reservation = await _reservationsRepository.FindFirstPendingReservationByHostAsync(host.Id);9394var smsRequest = body;95reservation.Status =96smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||97smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase)98? ReservationStatus.Confirmed99: ReservationStatus.Rejected;100101await _reservationsRepository.UpdateAsync(reservation);102smsResponse =103string.Format("You have successfully {0} the reservation", reservation.Status);104}105catch (Exception)106{107smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";108}109110return TwiML(Respond(smsResponse));111}112113private static MessagingResponse Respond(string message)114{115var response = new MessagingResponse();116response.Message(message);117118return response;119}120}121}
Congratulations! You've just learned how to automate your workflow with Twilio SMS.
Next, lets see what else we can do with the Twilio C# SDK.
If you're a .NET developer working with Twilio you know we've got a lot of great content here on the Docs site. Here are just a couple ideas for your next tutorial:
You can route callers to the right people and information with an IVR (interactive voice response) system.
Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages.