Ahoy! We now recommend you build your appointment reminders with Twilio's built in Message Scheduling functionality. Head on over to the Message Scheduling documentation to learn more about scheduling messages!
Ready to implement appointment reminders in your application? Here's how it works:
Check out how Yelp uses SMS to confirm restaurant reservations for diners.
Here are the technologies we'll use to get this done:
In this tutorial, we'll point out key snippets of code that make this application work. Check out the project README on GitHub to see how to run the code yourself.
To implement appointment reminders, we will be working through a series of user stories that describe how to fully implement appointment reminders in a web application. We'll walk through the code required to satisfy each story, and explore what we needed to add on each step.
All this can be done with the help of Twilio in under half an hour.
As a user, I want to create an appointment with a name, guest phone numbers, and a time in the future.
In order to build an automated appointment reminder application, we probably should start with an appointment. This story requires that we create a bit of UI and a model object to create and save a new Appointment
in our system. At a high level, here's what we will need to add:
POST
request
Appointment
model object to store information about the user
Alright, so we know what we need to create a new appointment. Now let's start by looking at the model, where we decide what information we want to store with the appointment.
The appointment model is fairly straightforward, but since humans will be interacting with it let's make sure we add some data validation.
Our application relies on ASP.NET Data Annotations. In our case, we only want to validate that some fields are required. To accomplish this we'll use [Required]
data annotation.
By default, ASP.NET MVC displays the property name when rendering a control. In our example those property names can be Name
or PhoneNumber
. For rendering Name
there shouldn't be any problem. But for PhoneNumber
we might want to display something nicer, like "Phone Number". For this kind of scenario we can use another data annotation: [Display(Name = "Phone Number")]
.
For validating the contents of the PhoneNumber
field, we're using [Phone]
data annotation, which confirms user-entered phone numbers conform loosely to E.164 formatting standards.
AppointmentReminders.Web/Models/Appointment.cs
_27using System;_27using System.ComponentModel.DataAnnotations;_27using Microsoft.Ajax.Utilities;_27_27namespace AppointmentReminders.Web.Models_27{_27 public class Appointment_27 {_27 public static int ReminderTime = 30;_27 public int Id { get; set; }_27_27 [Required]_27 public string Name { get; set; }_27_27 [Required, Phone, Display(Name = "Phone number")]_27 public string PhoneNumber { get; set; }_27_27 [Required]_27 public DateTime Time { get; set; }_27_27 [Required]_27 public string Timezone { get; set; }_27_27 [Display(Name = "Created at")]_27 public DateTime CreatedAt { get; set; }_27 }_27}
Our appointment model is now defined. It's time to take a look at the form that allows an administrator to create new appointments.
When we create a new appointment, we need a guest name, a phone number and a time. By using HTML Helper classes we can bind the form to the model object. Those helpers will generate the necessary HTML markup that will create a new appointment on submit.
Now that we have a model and an UI, we will see how we can interact with Appointments
.
As a user, I want to view a list of all future appointments, and be able to delete those appointments.
If you're an organization that handles a lot of appointments, you probably want to be able to view and manage them in a single interface. That's what we'll tackle in this user story. We'll create a UI to:
We know what interactions we want to implement, so let's look first at how to list all upcoming Appointments.
At the controller level, we'll get a list of all the appointments in the database and render them with a view. We also add a prompt if there aren't any appointments, so the admin user can create one.
AppointmentReminders.Web/Controllers/AppointmentsController.cs
_127using System;_127using System.Linq;_127using System.Net;_127using System.Web.Mvc;_127using AppointmentReminders.Web.Models;_127using AppointmentReminders.Web.Models.Repository;_127_127namespace AppointmentReminders.Web.Controllers_127{_127 public class AppointmentsController : Controller_127 {_127 private readonly IAppointmentRepository _repository;_127_127 public AppointmentsController() : this(new AppointmentRepository()) { }_127_127 public AppointmentsController(IAppointmentRepository repository)_127 {_127 _repository = repository;_127 }_127_127 public SelectListItem[] Timezones_127 {_127 get_127 {_127 var systemTimeZones = TimeZoneInfo.GetSystemTimeZones();_127 return systemTimeZones.Select(systemTimeZone => new SelectListItem_127 {_127 Text = systemTimeZone.DisplayName,_127 Value = systemTimeZone.Id_127 }).ToArray();_127 }_127 }_127_127 // GET: Appointments_127 public ActionResult Index()_127 {_127 var appointments = _repository.FindAll();_127 return View(appointments);_127 }_127_127 // GET: Appointments/Details/5_127 public ActionResult Details(int? id)_127 {_127 if (id == null)_127 {_127 return new HttpStatusCodeResult(HttpStatusCode.BadRequest);_127 }_127_127 var appointment = _repository.FindById(id.Value);_127 if (appointment == null)_127 {_127 return HttpNotFound();_127 }_127_127 return View(appointment);_127 }_127_127 // GET: Appointments/Create_127 public ActionResult Create()_127 {_127 ViewBag.Timezones = Timezones;_127 // Use an empty appointment to setup the default_127 // values._127 var appointment = new Appointment_127 {_127 Timezone = "Pacific Standard Time",_127 Time = DateTime.Now_127 };_127_127 return View(appointment);_127 }_127_127 [HttpPost]_127 public ActionResult Create([Bind(Include="ID,Name,PhoneNumber,Time,Timezone")]Appointment appointment)_127 {_127 appointment.CreatedAt = DateTime.Now;_127_127 if (ModelState.IsValid)_127 {_127 _repository.Create(appointment);_127_127 return RedirectToAction("Details", new {id = appointment.Id});_127 }_127_127 return View("Create", appointment);_127 }_127_127 // GET: Appointments/Edit/5_127 [HttpGet]_127 public ActionResult Edit(int? id)_127 {_127 if (id == null)_127 {_127 return new HttpStatusCodeResult(HttpStatusCode.BadRequest);_127 }_127_127 var appointment = _repository.FindById(id.Value);_127 if (appointment == null)_127 {_127 return HttpNotFound();_127 }_127_127 ViewBag.Timezones = Timezones;_127 return View(appointment);_127 }_127_127 // POST: /Appointments/Edit/5_127 [HttpPost]_127 public ActionResult Edit([Bind(Include = "ID,Name,PhoneNumber,Time,Timezone")] Appointment appointment)_127 {_127 if (ModelState.IsValid)_127 {_127 _repository.Update(appointment);_127 return RedirectToAction("Details", new { id = appointment.Id });_127 }_127 return View(appointment);_127 }_127_127 // DELETE: Appointments/Delete/5_127 [HttpDelete]_127 public ActionResult Delete(int id)_127 {_127 _repository.Delete(id);_127 return RedirectToAction("Index");_127 }_127 }_127}
That's how we return the list of appointments, now we need to render them. Let's look at the Appointments template for that.
The index view lists all appointments which are sorted by Id. The only thing we need to add to fulfil our user story is a delete button. We'll add the edit button just for kicks.
You may notice that instead of hard-coding the urls for Edit and Delete we are using an ASP.NET MVC HTML Helpers. If you view the rendered markup you will see these paths.
/Appointments/Edit/
id
for edit
/Appointments/Delete/
id
for delete
AppointmentsController.cs
contains methods which handle both the edit and delete operations.
Now that we have the ability to create, view, edit, and delete appointments, we can dig into the fun part: scheduling a recurring job that will send out reminders via SMS when an appointment is coming up!
As an appointment system, I want to notify a user via SMS an arbitrary interval before a future appointment.
There are a lot of ways to build this part of our application, but no matter how you implement it there should be two moving parts:
Let's take a look at how we decided to implement the latter with Hangfire.
If you've never used a job scheduler before, you may want to check out this post by Scott Hanselman that shows a few ways to run background tasks in ASP.NET MVC. We decided to use Hangfire because of its simplicity. If you have a better way to schedule jobs in ASP.NET MVC please let us know.
Hangfire needs a backend of some kind to queue the upcoming jobs. In this implementation, we're using SQL Server Database, but it's possible to use a different data store. You can check their documentation for further details.
Now that we have included Hangfire dependencies into the project, let's take a look at how to configure it to use it in our appointment reminders application.
We created a class named Hangfire
to configure our job scheduler. This class defines two static methods:
ConfigureHangfire
to set initialization parameters for the job scheduler.
InitialzeJobs
to specify which recurring jobs should be run, and how often they should run.
AppointmentReminders.Web/App_Start/Hangfire.cs
_22using Hangfire;_22using Owin;_22_22namespace AppointmentReminders.Web_22{_22 public class Hangfire_22 {_22 public static void ConfigureHangfire(IAppBuilder app)_22 {_22 GlobalConfiguration.Configuration_22 .UseSqlServerStorage("DefaultConnection");_22_22 app.UseHangfireDashboard("/jobs");_22 app.UseHangfireServer();_22 }_22_22 public static void InitializeJobs()_22 {_22 RecurringJob.AddOrUpdate<Workers.SendNotificationsJob>(job => job.Execute(), Cron.Minutely);_22 }_22 }_22}
That's it for the configuration. Let's take a quick look next at how we start up the job scheduler.
This ASP.NET MVC project is an OWIN-based application, which allows us to create a startup class to run any custom initialization logic required in our application. This is the preferred location to start Hangfire - check out their configuration docs for more information.
AppointmentReminders.Web/Startup.c
_15using Microsoft.Owin;_15using Owin;_15_15[assembly: OwinStartup(typeof(AppointmentReminders.Web.Startup))]_15namespace AppointmentReminders.Web_15{_15 public class Startup_15 {_15 public void Configuration(IAppBuilder app)_15 {_15 Hangfire.ConfigureHangfire(app);_15 Hangfire.InitializeJobs();_15 }_15 }_15}
Now that we've started the job scheduler, let's take a look at the logic that gets executed when our job runs.
In this class, we define a method called Execute
which is called every minute by Hangfire. Every time the job runs, we need to:
The AppointmentsFinder
class queries our SQL Server database to find all the appointments whose date and time are coming up soon. For each of those appointments, we'll use the Twilio REST API to send out a formatted message.
AppointmentReminders.Web/Workers/SendNotificationsJob.cs
_31using System;_31using System.Collections.Generic;_31using AppointmentReminders.Web.Domain;_31using AppointmentReminders.Web.Models;_31using AppointmentReminders.Web.Models.Repository;_31using WebGrease.Css.Extensions;_31_31namespace AppointmentReminders.Web.Workers_31{_31 public class SendNotificationsJob_31 {_31 private const string MessageTemplate =_31 "Hi {0}. Just a reminder that you have an appointment coming up at {1}.";_31_31 public void Execute()_31 {_31 var twilioRestClient = new Domain.Twilio.RestClient();_31_31 AvailableAppointments().ForEach(appointment =>_31 twilioRestClient.SendSmsMessage(_31 appointment.PhoneNumber,_31 string.Format(MessageTemplate, appointment.Name, appointment.Time.ToString("t"))));_31 }_31_31 private static IEnumerable<Appointment> AvailableAppointments()_31 {_31 return new AppointmentsFinder(new AppointmentRepository(), new TimeConverter())_31 .FindAvailableAppointments(DateTime.Now);_31 }_31 }_31}
Now that we are retrieving a list of upcoming Appointments, let's take a look next at how we use Twilio to send SMS notifications.
This class is responsible for reading our Twilio account credentials from Web.config
, and using the Twilio REST API to actually send out a notification to our users. We also need a Twilio number to use as the sender for the text message. Actually sending the message is a single line of code!
AppointmentReminders.Web/Domain/Twilio/RestClient.cs
_35using System.Web.Configuration;_35using Twilio.Clients;_35using Twilio.Rest.Api.V2010.Account;_35using Twilio.Types;_35_35namespace AppointmentReminders.Web.Domain.Twilio_35{_35 public class RestClient_35 {_35 private readonly ITwilioRestClient _client;_35 private readonly string _accountSid = WebConfigurationManager.AppSettings["AccountSid"];_35 private readonly string _authToken = WebConfigurationManager.AppSettings["AuthToken"];_35 private readonly string _twilioNumber = WebConfigurationManager.AppSettings["TwilioNumber"];_35_35 public RestClient()_35 {_35 _client = new TwilioRestClient(_accountSid, _authToken);_35 }_35_35 public RestClient(ITwilioRestClient client)_35 {_35 _client = client;_35 }_35_35 public void SendSmsMessage(string phoneNumber, string message)_35 {_35 var to = new PhoneNumber(phoneNumber);_35 MessageResource.Create(_35 to,_35 from: new PhoneNumber(_twilioNumber),_35 body: message,_35 client: _client);_35 }_35 }_35}
Fun tutorial, right? Where can we take it from here?
And with a little code and a dash of configuration, we're ready to get automated appointment reminders firing in our application. Good work!
If you are a C# developer working with Twilio, you might want to check out other tutorials:
Put a button on your web page that connects visitors to live support or sales people via telephone.
Improve the security of your Flask app's login functionality by adding two-factor authentication via text message.
Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter... we'd love to hear your thoughts, and know what you're building!