Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Send Appointment Reminders with C# and ASP.NET MVC


(information)

Info

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:

  1. An administrator creates an appointment for a future date and time, and stores a customer's phone number in the database for that appointment
  2. A background process checks the database on a regular interval, looking for appointments that require a reminder to be sent out
  3. At a configured time in advance of the appointment, an SMS reminder is sent out to the customer to remind them of their appointment

Check out how Yelp uses SMS to confirm restaurant reservations for diners.(link takes you to an external page)


Building Blocks

building-blocks page anchor

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(link takes you to an external page) to see how to run the code yourself.


How To Read This Tutorial

how-to-read-this-tutorial page anchor

To implement appointment reminders, we will be working through a series of user stories(link takes you to an external page) 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:

  • A form to enter details about the appointment
  • A route and controller function on the server to render the form
  • A route and controller function on the server to handle the form POST request
  • A persistent 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(link takes you to an external page). 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(link takes you to an external page).

The Appointment Model

the-appointment-model page anchor

AppointmentReminders.Web/Models/Appointment.cs


_27
using System;
_27
using System.ComponentModel.DataAnnotations;
_27
using Microsoft.Ajax.Utilities;
_27
_27
namespace 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(link takes you to an external page) 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.

AppointmentReminders.Web/Views/Appointments/_Form.cshtml

_33
@using AppointmentReminders.Web.Extensions
_33
@model AppointmentReminders.Web.Models.Appointment
_33
_33
<div class="form-group">
_33
@Html.LabelFor(x => x.Name, new { @class = "control-label col-lg-2" })
_33
<div class="col-lg-10">
_33
@Html.TextBoxFor(x => x.Name, new { @class = "form-control", @required = "required" })
_33
</div>
_33
</div>
_33
<div class="form-group">
_33
@Html.LabelFor(x => x.PhoneNumber, new { @class = "control-label col-lg-2" })
_33
<div class="col-lg-10">
_33
@Html.TextBoxFor(x => x.PhoneNumber, new { @class = "form-control", @required = "required" })
_33
</div>
_33
</div>
_33
<div class="form-group">
_33
<label class="control-label col-lg-2">Time and Date</label>
_33
<div class="col-lg-10">
_33
<div class="row">
_33
<div class="col-lg-6">
_33
<div class='input-group date' id="datetimepicker">
_33
@Html.TextBox("Time", Model.Time.ToCustomDateString(), new { @class = "form-control"})
_33
<span class="input-group-addon">
_33
<span class="glyphicon glyphicon-calendar"></span>
_33
</span>
_33
</div>
_33
</div>
_33
<div class="col-lg-6 pull-right">
_33
@Html.DropDownListFor(x => x.Timezone, (IEnumerable<SelectListItem>)ViewBag.Timezones, new { @class = "form-control" })
_33
</div>
_33
</div>
_33
</div>
_33
</div>

Now that we have a model and an UI, we will see how we can interact with Appointments.


Interacting with Appointments

interacting-with-appointments page anchor

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:

  • Show all appointments
  • Delete individual appoinments

We know what interactions we want to implement, so let's look first at how to list all upcoming Appointments.


Getting a List of Appointments

getting-a-list-of-appointments page anchor

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


_127
using System;
_127
using System.Linq;
_127
using System.Net;
_127
using System.Web.Mvc;
_127
using AppointmentReminders.Web.Models;
_127
using AppointmentReminders.Web.Models.Repository;
_127
_127
namespace 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.


Displaying All Appointments

displaying-all-appointments page anchor

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.

HTML Helpers

html-helpers page anchor

You may notice that instead of hard-coding the urls for Edit and Delete we are using an ASP.NET MVC HTML Helpers(link takes you to an external page). 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.

Display all Appointments

display-all-appointments page anchor
AppointmentReminders.Web/Views/Appointments/Index.cshtml

_59
@using AppointmentReminders.Web.Extensions
_59
@model IEnumerable<AppointmentReminders.Web.Models.Appointment>
_59
_59
<div class="page-header">
_59
<h1>Appointments</h1>
_59
</div>
_59
_59
<table class="table table-striped">
_59
<tr>
_59
<th>
_59
@Html.DisplayNameFor(model => model.Id)
_59
</th>
_59
<th>
_59
@Html.DisplayNameFor(model => model.Name)
_59
</th>
_59
<th>
_59
@Html.DisplayNameFor(model => model.PhoneNumber)
_59
</th>
_59
<th>
_59
@Html.DisplayNameFor(model => model.Time)
_59
</th>
_59
<th>
_59
@Html.DisplayNameFor(model => model.CreatedAt)
_59
</th>
_59
<th>
_59
Actions
_59
</th>
_59
</tr>
_59
_59
@foreach (var item in Model) {
_59
<tr>
_59
<td>
_59
@Html.ActionLink(item.Id.ToString(), "Details", new {Controller = "Appointments", id = item.Id})
_59
</td>
_59
<td>
_59
@Html.DisplayFor(modelItem => item.Name)
_59
</td>
_59
<td>
_59
@Html.DisplayFor(modelItem => item.PhoneNumber)
_59
</td>
_59
<td>
_59
@Html.DisplayFor(modelItem => item.Time)
_59
</td>
_59
<td>
_59
@Html.DisplayFor(modelItem => item.CreatedAt)
_59
</td>
_59
<td>
_59
@Html.ActionLink("Edit", "Edit", new { Controller = "Appointments", id = item.Id }, new { @class = "btn btn-default btn-xs" })
_59
_59
@Html.DeleteLink("Delete", "Appointments",
_59
new { id = item.Id },
_59
new { @class = "btn btn-danger btn-xs", onclick = "return confirm('Are you sure?');" })
_59
</td>
_59
</tr>
_59
}
_59
_59
</table>
_59
_59
@Html.ActionLink("New", "Create", new { Controller = "Appointments" }, new { @class = "btn btn-primary" })

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:

  • A script that checks the database for any appointment that is upcoming, and then sends a SMS.
  • A worker that runs that script continuously.

Let's take a look at how we decided to implement the latter with Hangfire(link takes you to an external page).


If you've never used a job scheduler before, you may want to check out this post by Scott Hanselman(link takes you to an external page) 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(link takes you to an external page).

Hangfire needs a backend of some kind to queue the upcoming jobs. In this implementation, we're using SQL Server Database(link takes you to an external page), but it's possible to use a different data store. You can check their documentation(link takes you to an external page) for further details.

AppointmentReminders.Web/packages.config

_27
<?xml version="1.0" encoding="utf-8"?>
_27
<packages>
_27
<package id="Antlr" version="3.5.0.2" targetFramework="net472" />
_27
<package id="EntityFramework" version="6.4.4" targetFramework="net472" />
_27
<package id="Hangfire" version="1.7.19" targetFramework="net472" />
_27
<package id="Hangfire.Core" version="1.7.18" targetFramework="net472" />
_27
<package id="Hangfire.SqlServer" version="1.7.18" targetFramework="net472" />
_27
<package id="JWT" version="7.3.1" targetFramework="net472" />
_27
<package id="Microsoft.AspNet.Mvc" version="5.2.7" targetFramework="net472" />
_27
<package id="Microsoft.AspNet.Razor" version="3.2.7" targetFramework="net472" />
_27
<package id="Microsoft.AspNet.Web.Optimization" version="1.1.3" targetFramework="net472" />
_27
<package id="Microsoft.AspNet.WebPages" version="3.2.7" targetFramework="net472" />
_27
<package id="Microsoft.IdentityModel.JsonWebTokens" version="6.8.0" targetFramework="net472" />
_27
<package id="Microsoft.IdentityModel.Logging" version="6.8.0" targetFramework="net472" />
_27
<package id="Microsoft.IdentityModel.Tokens" version="6.8.0" targetFramework="net472" />
_27
<package id="Microsoft.Owin" version="4.1.1" targetFramework="net472" />
_27
<package id="Microsoft.Owin.Host.SystemWeb" version="4.1.1" targetFramework="net472" />
_27
<package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net472" />
_27
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net472" />
_27
<package id="Owin" version="1.0" targetFramework="net472" />
_27
<package id="Portable.BouncyCastle" version="1.8.9" targetFramework="net472" />
_27
<package id="Portable.JWT" version="1.0.5" targetFramework="net472" />
_27
<package id="RestSharp" version="106.11.7" targetFramework="net472" />
_27
<package id="System.IdentityModel.Tokens.Jwt" version="6.8.0" targetFramework="net472" />
_27
<package id="Twilio" version="5.53.0" targetFramework="net472" />
_27
<package id="WebGrease" version="1.6.0" targetFramework="net472" />
_27
</packages>

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.


Hangfire Configuration Class

hangfire-configuration-class page anchor

We created a class named Hangfire to configure our job scheduler. This class defines two static methods:

  1. ConfigureHangfire to set initialization parameters for the job scheduler.
  2. InitialzeJobs to specify which recurring jobs should be run, and how often they should run.

AppointmentReminders.Web/App_Start/Hangfire.cs


_22
using Hangfire;
_22
using Owin;
_22
_22
namespace 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.


Starting the Job Scheduler

starting-the-job-scheduler page anchor

This ASP.NET MVC project is an OWIN-based application(link takes you to an external page), 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(link takes you to an external page).

AppointmentReminders.Web/Startup.c


_15
using Microsoft.Owin;
_15
using Owin;
_15
_15
[assembly: OwinStartup(typeof(AppointmentReminders.Web.Startup))]
_15
namespace 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.


Notification Background Job

notification-background-job page anchor

In this class, we define a method called Execute which is called every minute by Hangfire. Every time the job runs, we need to:

  1. Get a list of upcoming appointments that require notifications to be sent out
  2. Use Twilio to send appointment reminders via SMS

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.

Notifications Background Job

notifications-background-job page anchor

AppointmentReminders.Web/Workers/SendNotificationsJob.cs


_31
using System;
_31
using System.Collections.Generic;
_31
using AppointmentReminders.Web.Domain;
_31
using AppointmentReminders.Web.Models;
_31
using AppointmentReminders.Web.Models.Repository;
_31
using WebGrease.Css.Extensions;
_31
_31
namespace 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


_35
using System.Web.Configuration;
_35
using Twilio.Clients;
_35
using Twilio.Rest.Api.V2010.Account;
_35
using Twilio.Types;
_35
_35
namespace 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:

Click to Call

Put a button on your web page that connects visitors to live support or sales people via telephone.

Two-Factor Authentication

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(link takes you to an external page)... we'd love to hear your thoughts, and know what you're building!


Rate this page: