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 Core 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:
_33using System.Threading.Tasks;_33using AirTNG.Web.Domain.Twilio;_33using Twilio;_33using Twilio.Rest.Api.V2010.Account;_33using Twilio.Types;_33_33namespace AirTNG.Web.Domain.Reservations_33{_33 public interface ITwilioMessageSender_33 {_33 Task SendMessageAsync(string to, string body);_33 }_33 _33 public class TwilioMessageSender : ITwilioMessageSender_33 {_33_33 private readonly TwilioConfiguration _configuration;_33 _33 public TwilioMessageSender(TwilioConfiguration configuration)_33 {_33 _configuration = configuration;_33 _33 TwilioClient.Init(_configuration.AccountSid, _configuration.AuthToken);_33 }_33_33 public async Task SendMessageAsync(string to, string body)_33 {_33 await MessageResource.CreateAsync(new PhoneNumber(to),_33 from: new PhoneNumber(_configuration.PhoneNumber),_33 body: body);_33 }_33 }_33}
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 Core Identity for this purpose.
Identity User already includes a phone_number
that will be required to later send SMS notifications.
_12using System;_12using Microsoft.AspNetCore.Identity;_12_12namespace AirTNG.Web.Models_12{_12 public class ApplicationUser:IdentityUser_12 {_12_12 public string Name { get; set; }_12_12 }_12}
Next let's take a look at the Vacation Property model.
Our rental application will obviously 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.
_21using System;_21using System.Collections.Generic;_21using System.ComponentModel.DataAnnotations.Schema;_21using Microsoft.AspNetCore.Identity;_21_21namespace AirTNG.Web.Models_21{_21 public class VacationProperty_21 {_21 public int Id { get; set; }_21 public string UserId { get; set; }_21 public string Description { get; set; }_21 public string ImageUrl { get; set; }_21 public DateTime CreatedAt { get; set; }_21 _21 [ForeignKey("UserId")]_21 public ApplicationUser User { get; set; }_21_21 public virtual IList<Reservation> Reservations { get; set; }_21 }_21}
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 to have access. Through this property the user will have access to the
host
phone number indirectly.
Name
and
PhoneNumber
of the guest.
Message
sent to the host on reservation.
Status
of the reservation.
_26using System;_26using System.ComponentModel.DataAnnotations.Schema;_26_26namespace AirTNG.Web.Models_26{_26 public class Reservation_26 {_26 public int Id { get; set; }_26 public string Name { get; set; }_26 public string PhoneNumber { get; set; }_26 public ReservationStatus Status { get; set; }_26 public string Message { get; set; }_26 public DateTime CreatedAt { get; set; }_26 public int VacationPropertyId { get; set; }_26 _26 [ForeignKey("VacationPropertyId")]_26 public VacationProperty VacationProperty { get; set; }_26 }_26 _26 public enum ReservationStatus_26 {_26 Pending = 0,_26 Confirmed = 1,_26 Rejected = 2_26 }_26}
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.
_82using System;_82using System.Threading.Tasks;_82using AirTNG.Web.Data;_82using AirTNG.Web.Domain.Reservations;_82using Microsoft.AspNetCore.Mvc;_82using AirTNG.Web.Models;_82using Microsoft.AspNetCore.Authorization;_82using Microsoft.AspNetCore.Identity;_82using Microsoft.Extensions.Logging;_82_82namespace AirTNG.Web.Tests.Controllers_82{_82 [Authorize]_82 public class ReservationController : Controller_82 {_82 private readonly IApplicationDbRepository _repository;_82 private readonly IUserRepository _userRepository;_82 private readonly INotifier _notifier;_82_82 public ReservationController(_82 IApplicationDbRepository applicationDbRepository,_82 IUserRepository userRepository,_82 INotifier notifier)_82 {_82 _repository = applicationDbRepository;_82 _userRepository = userRepository;_82 _notifier = notifier;_82 }_82_82 // GET: Reservation/Create/1_82 public async Task<IActionResult> Create(int? id)_82 {_82 if (id == null)_82 {_82 return NotFound();_82 }_82 var property = await _repository.FindVacationPropertyFirstOrDefaultAsync(id);_82 if (property == null)_82 {_82 return NotFound();_82 }_82_82 ViewData["VacationProperty"] = property; _82 return View();_82 }_82_82 // POST: Reservation/Create/1_82 // To protect from overposting attacks, please enable the specific properties you want to bind to, for _82 // more details see http://go.microsoft.com/fwlink/?LinkId=317598._82 [HttpPost]_82 [ValidateAntiForgeryToken]_82 public async Task<IActionResult> Create(int id, [Bind("Message,VacationPropertyId")] Reservation reservation)_82 {_82 if (id != reservation.VacationPropertyId)_82 {_82 return NotFound();_82 }_82 _82 if (ModelState.IsValid)_82 {_82 var user = await _userRepository.GetUserAsync(HttpContext.User);_82 reservation.Status = ReservationStatus.Pending;_82 reservation.Name = user.Name;_82 reservation.PhoneNumber = user.PhoneNumber;_82 reservation.CreatedAt = DateTime.Now;_82_82 await _repository.CreateReservationAsync(reservation);_82 var notification = Notification.BuildHostNotification(_82 await _repository.FindReservationWithRelationsAsync(reservation.Id));_82 _82 await _notifier.SendNotificationAsync(notification);_82 _82 return RedirectToAction("Index", "VacationProperty");_82 }_82 _82 ViewData["VacationProperty"] = await _repository.FindVacationPropertyFirstOrDefaultAsync(_82 reservation.VacationPropertyId);_82 return View(reservation);_82 }_82_82 }_82}
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.
_36using System;_36using System.Linq;_36using System.Text;_36using System.Threading.Tasks;_36using AirTNG.Web.Domain.Twilio;_36using AirTNG.Web.Models;_36using Twilio;_36using Twilio.Clients;_36using Twilio.TwiML.Messaging;_36_36namespace AirTNG.Web.Domain.Reservations_36{_36 public interface INotifier_36 {_36 Task SendNotificationAsync(Notification notification);_36 }_36_36 public class Notifier : INotifier_36 {_36 private readonly ITwilioMessageSender _client;_36_36 public Notifier(TwilioConfiguration configuration) : this(_36 new TwilioMessageSender(configuration)_36 ) { }_36_36 public Notifier(ITwilioMessageSender client)_36 {_36 _client = client;_36 }_36_36 public async Task SendNotificationAsync(Notification notification)_36 {_36 await _client.SendMessageAsync(notification.To, notification.Message);_36 }_36 }_36}
Now's let's peek at how we're handling the host's responses.
The Sms/Handle
controller handles our incoming Twilio request and does four things:
_78using System;_78using System.Collections.Generic;_78using System.Linq;_78using System.Threading.Tasks;_78using AirTNG.Web.Data;_78using AirTNG.Web.Domain.Reservations;_78using AirTNG.Web.Models;_78using Microsoft.AspNetCore.Authorization;_78using Microsoft.AspNetCore.Mvc;_78using Microsoft.EntityFrameworkCore;_78using Twilio.AspNet.Core;_78using Twilio.TwiML;_78using Twilio.TwiML.Voice;_78_78namespace AirTNG.Web.Tests.Controllers_78{_78 public class SmsController: TwilioController_78 {_78 private readonly IApplicationDbRepository _repository;_78 private readonly INotifier _notifier;_78_78 public SmsController(_78 IApplicationDbRepository repository,_78 INotifier notifier)_78 {_78 _repository = repository;_78 _notifier = notifier;_78 }_78 _78_78 // POST Sms/Handle_78 [HttpPost]_78 [AllowAnonymous]_78 public async Task<TwiMLResult> Handle(string from, string body)_78 {_78 string smsResponse;_78 _78 try_78 {_78 var host = await _repository.FindUserByPhoneNumberAsync(from);_78 var reservation = await _repository.FindFirstPendingReservationByHostAsync(host.Id);_78_78 var smsRequest = body;_78 reservation.Status =_78 smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||_78 smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase)_78 ? ReservationStatus.Confirmed_78 : ReservationStatus.Rejected;_78_78 await _repository.UpdateReservationAsync(reservation);_78 smsResponse = $"You have successfully {reservation.Status} the reservation";_78_78 // Notify guest with host response_78 var notification = Notification.BuildGuestNotification(reservation);_78_78 await _notifier.SendNotificationAsync(notification);_78 }_78 catch (InvalidOperationException)_78 {_78 smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";_78 }_78 catch (Exception)_78 {_78 smsResponse = "Sorry, it looks like we get an error. Try later!";_78 }_78 _78 return TwiML(Respond(smsResponse));_78 }_78_78 private static MessagingResponse Respond(string message)_78 {_78 var response = new MessagingResponse();_78 response.Message(message);_78_78 return response;_78 }_78 }_78}
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 web hook 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 web hook on your number would look like this:
_10http://<subdomain>.ngrok.io/Reservations/Handle
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 he/she has any pending reservations. If he/she does, 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.
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 he/she has 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.
_78using System;_78using System.Collections.Generic;_78using System.Linq;_78using System.Threading.Tasks;_78using AirTNG.Web.Data;_78using AirTNG.Web.Domain.Reservations;_78using AirTNG.Web.Models;_78using Microsoft.AspNetCore.Authorization;_78using Microsoft.AspNetCore.Mvc;_78using Microsoft.EntityFrameworkCore;_78using Twilio.AspNet.Core;_78using Twilio.TwiML;_78using Twilio.TwiML.Voice;_78_78namespace AirTNG.Web.Tests.Controllers_78{_78 public class SmsController: TwilioController_78 {_78 private readonly IApplicationDbRepository _repository;_78 private readonly INotifier _notifier;_78_78 public SmsController(_78 IApplicationDbRepository repository,_78 INotifier notifier)_78 {_78 _repository = repository;_78 _notifier = notifier;_78 }_78 _78_78 // POST Sms/Handle_78 [HttpPost]_78 [AllowAnonymous]_78 public async Task<TwiMLResult> Handle(string from, string body)_78 {_78 string smsResponse;_78 _78 try_78 {_78 var host = await _repository.FindUserByPhoneNumberAsync(from);_78 var reservation = await _repository.FindFirstPendingReservationByHostAsync(host.Id);_78_78 var smsRequest = body;_78 reservation.Status =_78 smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||_78 smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase)_78 ? ReservationStatus.Confirmed_78 : ReservationStatus.Rejected;_78_78 await _repository.UpdateReservationAsync(reservation);_78 smsResponse = $"You have successfully {reservation.Status} the reservation";_78_78 // Notify guest with host response_78 var notification = Notification.BuildGuestNotification(reservation);_78_78 await _notifier.SendNotificationAsync(notification);_78 }_78 catch (InvalidOperationException)_78 {_78 smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";_78 }_78 catch (Exception)_78 {_78 smsResponse = "Sorry, it looks like we get an error. Try later!";_78 }_78 _78 return TwiML(Respond(smsResponse));_78 }_78_78 private static MessagingResponse Respond(string message)_78 {_78 var response = new MessagingResponse();_78 response.Message(message);_78_78 return response;_78 }_78 }_78}
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:
Easily 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.
Thanks for checking out this tutorial! Tweet to us @twilio with what you're building!