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:
1using System.Threading.Tasks;2using AirTNG.Web.Domain.Twilio;3using Twilio;4using Twilio.Rest.Api.V2010.Account;5using Twilio.Types;67namespace AirTNG.Web.Domain.Reservations8{9public interface ITwilioMessageSender10{11Task SendMessageAsync(string to, string body);12}1314public class TwilioMessageSender : ITwilioMessageSender15{1617private readonly TwilioConfiguration _configuration;1819public TwilioMessageSender(TwilioConfiguration configuration)20{21_configuration = configuration;2223TwilioClient.Init(_configuration.AccountSid, _configuration.AuthToken);24}2526public async Task SendMessageAsync(string to, string body)27{28await MessageResource.CreateAsync(new PhoneNumber(to),29from: new PhoneNumber(_configuration.PhoneNumber),30body: body);31}32}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.
1using System;2using Microsoft.AspNetCore.Identity;34namespace AirTNG.Web.Models5{6public class ApplicationUser:IdentityUser7{89public string Name { get; set; }1011}12}
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.
1using System;2using System.Collections.Generic;3using System.ComponentModel.DataAnnotations.Schema;4using Microsoft.AspNetCore.Identity;56namespace AirTNG.Web.Models7{8public class VacationProperty9{10public int Id { get; set; }11public string UserId { get; set; }12public string Description { get; set; }13public string ImageUrl { get; set; }14public DateTime CreatedAt { get; set; }1516[ForeignKey("UserId")]17public ApplicationUser User { get; set; }1819public virtual IList<Reservation> Reservations { get; set; }20}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.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 VacationPropertyId { get; set; }1516[ForeignKey("VacationPropertyId")]17public VacationProperty VacationProperty { get; set; }18}1920public enum ReservationStatus21{22Pending = 0,23Confirmed = 1,24Rejected = 225}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.
1using System;2using System.Threading.Tasks;3using AirTNG.Web.Data;4using AirTNG.Web.Domain.Reservations;5using Microsoft.AspNetCore.Mvc;6using AirTNG.Web.Models;7using Microsoft.AspNetCore.Authorization;8using Microsoft.AspNetCore.Identity;9using Microsoft.Extensions.Logging;1011namespace AirTNG.Web.Tests.Controllers12{13[Authorize]14public class ReservationController : Controller15{16private readonly IApplicationDbRepository _repository;17private readonly IUserRepository _userRepository;18private readonly INotifier _notifier;1920public ReservationController(21IApplicationDbRepository applicationDbRepository,22IUserRepository userRepository,23INotifier notifier)24{25_repository = applicationDbRepository;26_userRepository = userRepository;27_notifier = notifier;28}2930// GET: Reservation/Create/131public async Task<IActionResult> Create(int? id)32{33if (id == null)34{35return NotFound();36}37var property = await _repository.FindVacationPropertyFirstOrDefaultAsync(id);38if (property == null)39{40return NotFound();41}4243ViewData["VacationProperty"] = property;44return View();45}4647// POST: Reservation/Create/148// To protect from overposting attacks, please enable the specific properties you want to bind to, for49// more details see http://go.microsoft.com/fwlink/?LinkId=317598.50[HttpPost]51[ValidateAntiForgeryToken]52public async Task<IActionResult> Create(int id, [Bind("Message,VacationPropertyId")] Reservation reservation)53{54if (id != reservation.VacationPropertyId)55{56return NotFound();57}5859if (ModelState.IsValid)60{61var user = await _userRepository.GetUserAsync(HttpContext.User);62reservation.Status = ReservationStatus.Pending;63reservation.Name = user.Name;64reservation.PhoneNumber = user.PhoneNumber;65reservation.CreatedAt = DateTime.Now;6667await _repository.CreateReservationAsync(reservation);68var notification = Notification.BuildHostNotification(69await _repository.FindReservationWithRelationsAsync(reservation.Id));7071await _notifier.SendNotificationAsync(notification);7273return RedirectToAction("Index", "VacationProperty");74}7576ViewData["VacationProperty"] = await _repository.FindVacationPropertyFirstOrDefaultAsync(77reservation.VacationPropertyId);78return View(reservation);79}8081}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.
1using System;2using System.Linq;3using System.Text;4using System.Threading.Tasks;5using AirTNG.Web.Domain.Twilio;6using AirTNG.Web.Models;7using Twilio;8using Twilio.Clients;9using Twilio.TwiML.Messaging;1011namespace AirTNG.Web.Domain.Reservations12{13public interface INotifier14{15Task SendNotificationAsync(Notification notification);16}1718public class Notifier : INotifier19{20private readonly ITwilioMessageSender _client;2122public Notifier(TwilioConfiguration configuration) : this(23new TwilioMessageSender(configuration)24) { }2526public Notifier(ITwilioMessageSender client)27{28_client = client;29}3031public async Task SendNotificationAsync(Notification notification)32{33await _client.SendMessageAsync(notification.To, notification.Message);34}35}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:
1using System;2using System.Collections.Generic;3using System.Linq;4using System.Threading.Tasks;5using AirTNG.Web.Data;6using AirTNG.Web.Domain.Reservations;7using AirTNG.Web.Models;8using Microsoft.AspNetCore.Authorization;9using Microsoft.AspNetCore.Mvc;10using Microsoft.EntityFrameworkCore;11using Twilio.AspNet.Core;12using Twilio.TwiML;13using Twilio.TwiML.Voice;1415namespace AirTNG.Web.Tests.Controllers16{17public class SmsController: TwilioController18{19private readonly IApplicationDbRepository _repository;20private readonly INotifier _notifier;2122public SmsController(23IApplicationDbRepository repository,24INotifier notifier)25{26_repository = repository;27_notifier = notifier;28}293031// POST Sms/Handle32[HttpPost]33[AllowAnonymous]34public async Task<TwiMLResult> Handle(string from, string body)35{36string smsResponse;3738try39{40var host = await _repository.FindUserByPhoneNumberAsync(from);41var reservation = await _repository.FindFirstPendingReservationByHostAsync(host.Id);4243var smsRequest = body;44reservation.Status =45smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||46smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase)47? ReservationStatus.Confirmed48: ReservationStatus.Rejected;4950await _repository.UpdateReservationAsync(reservation);51smsResponse = $"You have successfully {reservation.Status} the reservation";5253// Notify guest with host response54var notification = Notification.BuildGuestNotification(reservation);5556await _notifier.SendNotificationAsync(notification);57}58catch (InvalidOperationException)59{60smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";61}62catch (Exception)63{64smsResponse = "Sorry, it looks like we get an error. Try later!";65}6667return TwiML(Respond(smsResponse));68}6970private static MessagingResponse Respond(string message)71{72var response = new MessagingResponse();73response.Message(message);7475return response;76}77}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 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.
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.
1using System;2using System.Collections.Generic;3using System.Linq;4using System.Threading.Tasks;5using AirTNG.Web.Data;6using AirTNG.Web.Domain.Reservations;7using AirTNG.Web.Models;8using Microsoft.AspNetCore.Authorization;9using Microsoft.AspNetCore.Mvc;10using Microsoft.EntityFrameworkCore;11using Twilio.AspNet.Core;12using Twilio.TwiML;13using Twilio.TwiML.Voice;1415namespace AirTNG.Web.Tests.Controllers16{17public class SmsController: TwilioController18{19private readonly IApplicationDbRepository _repository;20private readonly INotifier _notifier;2122public SmsController(23IApplicationDbRepository repository,24INotifier notifier)25{26_repository = repository;27_notifier = notifier;28}293031// POST Sms/Handle32[HttpPost]33[AllowAnonymous]34public async Task<TwiMLResult> Handle(string from, string body)35{36string smsResponse;3738try39{40var host = await _repository.FindUserByPhoneNumberAsync(from);41var reservation = await _repository.FindFirstPendingReservationByHostAsync(host.Id);4243var smsRequest = body;44reservation.Status =45smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||46smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase)47? ReservationStatus.Confirmed48: ReservationStatus.Rejected;4950await _repository.UpdateReservationAsync(reservation);51smsResponse = $"You have successfully {reservation.Status} the reservation";5253// Notify guest with host response54var notification = Notification.BuildGuestNotification(reservation);5556await _notifier.SendNotificationAsync(notification);57}58catch (InvalidOperationException)59{60smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";61}62catch (Exception)63{64smsResponse = "Sorry, it looks like we get an error. Try later!";65}6667return TwiML(Respond(smsResponse));68}6970private static MessagingResponse Respond(string message)71{72var response = new MessagingResponse();73response.Message(message);7475return response;76}77}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:
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.