Employee Directory with C# and ASP.NET MVC

January 10, 2017
Written by
Reviewed by
Kat King
Twilion
Hector Ortega
Contributor
Opinions expressed by Twilio contributors are their own
Paul Kamp
Twilion

employee-directory-csharp

This ASP.NET application is an Employee Directory that can be used from any SMS client to find an employee's contact information. To build this app, we will need to:

  • Create an Employee model
  • Receive an incoming SMS
  • Search for an Employee by name
  • Respond with an outgoing MMS

Create an Employee Model

The first thing we need is a database of employees. We will be using Entity Framework (EF) Code First for this. That means we can use a POCO (Plain Old CLR Object) to represent our employees.

Our employee entity has a few fields for contact information, including their name, phone number, and a public URL containing an image of them.

Editor: this is a migrated tutorial. Clone the original code from https://github.com/TwilioDevEd/employee-directory-csharp/

namespace EmployeeDirectory.Web.Models
{
  public class Employee
  {
    public int Id { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
    public string ImageUrl { get; set; }
  }
}

Now that we have a model that represents an employee, let's see how to search for employees by name.

Search for an Employee by Name

The EmployeeDirectoryService class allows us to search the database for employees either by name or by their unique database identifier. When searching by name, we'll return a list of employees whose name might match a search query, or just one if we find an exact match. If we know the ID of the employee we're looking for, we can return it right away.

using EmployeeDirectory.Web.Models;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;

namespace EmployeeDirectory.Web.Services
{
    public class EmployeeDirectoryService
    {
        private readonly EmployeeDbContext _context;

        public EmployeeDirectoryService(EmployeeDbContext context)
        {
            _context = context;
        }

        public async Task<IEnumerable<Employee>> FindByNamePartialAsync(string partialName)
        {
            partialName = partialName.ToLower();
            return await _context.Employees
                .Where(e => e.FullName.ToLower().Contains(partialName))
                .ToListAsync();
        }

        public async Task<Employee> FindByIdAsync(int id)
        {
            return await _context.Employees
                .FirstOrDefaultAsync(e => e.Id == id);
        }
    }
}

Now, let's use this search functionality when responding to an SMS from a user.

Receive an incoming SMS

When your number receives an SMS message, Twilio will send an HTTP POST request to our application. This will be handled by the Lookup action in EmployeeController.

We check for numeric input (more on that later) or perform a lookup for the desired employee. The results are packaged up as a XML and sent back to Twilio and, in turn, the original sender of the SMS.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using EmployeeDirectory.Web.Models;
using EmployeeDirectory.Web.Services;
using Twilio.AspNet.Common;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace EmployeeDirectory.Web.Controllers
{
    public class EmployeeController : TwilioController
    {
        public const string NotFoundMessage = "We could not find an employee matching '{0}'";
        public const string CookieName = "EmpDirIdList";
        private readonly EmployeeDirectoryService _service;

        public EmployeeController()
        {
            _service = new EmployeeDirectoryService(new EmployeeDbContext());
        }

        public EmployeeController(EmployeeDirectoryService service)
        {
            _service = service;
        }

        // Twilio will call this whenever our phone # receives an SMS message.
        [HttpPost]
        public async Task<TwiMLResult> Lookup(SmsRequest request)
        {
            var incomingMessageText = request.Body;

            var employees = await GetEmployeesIfNumericInput(incomingMessageText) ??
                            await _service.FindByNamePartialAsync(incomingMessageText);

            var response = GetTwilioResponseForEmployees(employees, incomingMessageText);
            return TwiML(response);
        }

        private async Task<IEnumerable<Employee>> GetEmployeesIfNumericInput(string incomingMessageText)
        {
            var num = ParseNumber(incomingMessageText);
            var keys = GetListOfKeysFromCookie();

            if (IsNumberAndInListOfKeys(num, keys))
            {
                return await GetListOfOneEmployeeById(keys, num);
            }

            return null;
        }

        private static int ParseNumber(string incomingMessageText)
        {
            int value;
            return int.TryParse(incomingMessageText, out value) ? value : 0;
        }

        private string[] GetListOfKeysFromCookie()
        {
            var cookie = Request.Cookies.Get(CookieName);
            if (cookie == null || cookie.Value == null) return null;
            return cookie.Value.Split('~');
        }

        private static bool IsNumberAndInListOfKeys(int num, IReadOnlyList<string> keys)
        {
            return num > 0 && keys != null && num <= keys.Count;
        }

        private async Task<IEnumerable<Employee>> GetListOfOneEmployeeById(IReadOnlyList<string> keys, int num)
        {
            var id = ParseNumber(keys[num - 1]);
            var employee = await _service.FindByIdAsync(id);
            return employee == null ? null : ConvertToArray(employee);
        }

        private static IEnumerable<Employee> ConvertToArray(Employee employee)
        {
            return new [] { employee };
        }

        private MessagingResponse GetTwilioResponseForEmployees(IEnumerable<Employee> employees, string incomingMessageText)
        {
            var response = new MessagingResponse();
            var employeeList = employees.ToList();

            switch (employeeList.Count)
            {
                case 0: // No Employees Found
                    response.Message(string.Format(NotFoundMessage, incomingMessageText));
                    break;

                case 1: // A Single Employee Found
                    var employee = employeeList.First();
                    response.Message(
                        new Message().Body(
                            employee.FullName + " - " + 
                            employee.Email + " - " + 
                            employee.PhoneNumber)
                            .Media(employee.ImageUrl));
                    break;

                default: // Multiple Employees Found
                    response.Message(GetMessageForMultipleEmployees(employeeList));
                    break;
            }

            return response;
        }

        private string GetMessageForMultipleEmployees(IReadOnlyList<Employee> employees)
        {
            var msg = "We found: ";
            var idList = "";
            for (var i = 0; i < employees.Count; i++)
            {
                msg += i + 1 + "-" + employees[i].FullName;
                idList += employees[i].Id;
                if (i == employees.Count - 1) continue;
                msg += ", ";
                idList += "~";
            }
            msg += " - Reply with # of desired person";

            AddListOfIdsCookie(idList);
            return msg;
        }

        private void AddListOfIdsCookie(string idList)
        {
            var cookie = new HttpCookie(CookieName, idList)
            {
                Expires = DateTime.UtcNow.AddHours(4)
            };
            Response.Cookies.Add(cookie);
        }
    }
}

Now that we have our search route, let's see how we can request a specific employee by name.

Respond with Single Match for an Employee Name

Let's say the service finds a single employee matching the text message. In this case, we simply write out a response that contains the employee's contact information, including a photo, making our response a MMS message.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using EmployeeDirectory.Web.Models;
using EmployeeDirectory.Web.Services;
using Twilio.AspNet.Common;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace EmployeeDirectory.Web.Controllers
{
    public class EmployeeController : TwilioController
    {
        public const string NotFoundMessage = "We could not find an employee matching '{0}'";
        public const string CookieName = "EmpDirIdList";
        private readonly EmployeeDirectoryService _service;

        public EmployeeController()
        {
            _service = new EmployeeDirectoryService(new EmployeeDbContext());
        }

        public EmployeeController(EmployeeDirectoryService service)
        {
            _service = service;
        }

        // Twilio will call this whenever our phone # receives an SMS message.
        [HttpPost]
        public async Task<TwiMLResult> Lookup(SmsRequest request)
        {
            var incomingMessageText = request.Body;

            var employees = await GetEmployeesIfNumericInput(incomingMessageText) ??
                            await _service.FindByNamePartialAsync(incomingMessageText);

            var response = GetTwilioResponseForEmployees(employees, incomingMessageText);
            return TwiML(response);
        }

        private async Task<IEnumerable<Employee>> GetEmployeesIfNumericInput(string incomingMessageText)
        {
            var num = ParseNumber(incomingMessageText);
            var keys = GetListOfKeysFromCookie();

            if (IsNumberAndInListOfKeys(num, keys))
            {
                return await GetListOfOneEmployeeById(keys, num);
            }

            return null;
        }

        private static int ParseNumber(string incomingMessageText)
        {
            int value;
            return int.TryParse(incomingMessageText, out value) ? value : 0;
        }

        private string[] GetListOfKeysFromCookie()
        {
            var cookie = Request.Cookies.Get(CookieName);
            if (cookie == null || cookie.Value == null) return null;
            return cookie.Value.Split('~');
        }

        private static bool IsNumberAndInListOfKeys(int num, IReadOnlyList<string> keys)
        {
            return num > 0 && keys != null && num <= keys.Count;
        }

        private async Task<IEnumerable<Employee>> GetListOfOneEmployeeById(IReadOnlyList<string> keys, int num)
        {
            var id = ParseNumber(keys[num - 1]);
            var employee = await _service.FindByIdAsync(id);
            return employee == null ? null : ConvertToArray(employee);
        }

        private static IEnumerable<Employee> ConvertToArray(Employee employee)
        {
            return new [] { employee };
        }

        private MessagingResponse GetTwilioResponseForEmployees(IEnumerable<Employee> employees, string incomingMessageText)
        {
            var response = new MessagingResponse();
            var employeeList = employees.ToList();

            switch (employeeList.Count)
            {
                case 0: // No Employees Found
                    response.Message(string.Format(NotFoundMessage, incomingMessageText));
                    break;

                case 1: // A Single Employee Found
                    var employee = employeeList.First();
                    response.Message(
                        new Message().Body(
                            employee.FullName + " - " + 
                            employee.Email + " - " + 
                            employee.PhoneNumber)
                            .Media(employee.ImageUrl));
                    break;

                default: // Multiple Employees Found
                    response.Message(GetMessageForMultipleEmployees(employeeList));
                    break;
            }

            return response;
        }

        private string GetMessageForMultipleEmployees(IReadOnlyList<Employee> employees)
        {
            var msg = "We found: ";
            var idList = "";
            for (var i = 0; i < employees.Count; i++)
            {
                msg += i + 1 + "-" + employees[i].FullName;
                idList += employees[i].Id;
                if (i == employees.Count - 1) continue;
                msg += ", ";
                idList += "~";
            }
            msg += " - Reply with # of desired person";

            AddListOfIdsCookie(idList);
            return msg;
        }

        private void AddListOfIdsCookie(string idList)
        {
            var cookie = new HttpCookie(CookieName, idList)
            {
                Expires = DateTime.UtcNow.AddHours(4)
            };
            Response.Cookies.Add(cookie);
        }
    }
}

As you can see, The Twilio C# Helper Library simplifies the way you can generate and send SMS messages. Together with a simple database query, the application is able to return valuable information over SMS. Let's see how we handle multiple or no results next.

Handle Multiple or No Results

If we don't find any employees, we can simply return a "Not found" message.

What about multiple matches? For this case, we want to return a list of the matching employees' names along with an incrementing number the end user can use to make their selection. For example, if someone searched for "David" they might get something like:

We found: 1-David Prothero, 2-David Smith
 - Reply with # of desired person
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using EmployeeDirectory.Web.Models;
using EmployeeDirectory.Web.Services;
using Twilio.AspNet.Common;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace EmployeeDirectory.Web.Controllers
{
    public class EmployeeController : TwilioController
    {
        public const string NotFoundMessage = "We could not find an employee matching '{0}'";
        public const string CookieName = "EmpDirIdList";
        private readonly EmployeeDirectoryService _service;

        public EmployeeController()
        {
            _service = new EmployeeDirectoryService(new EmployeeDbContext());
        }

        public EmployeeController(EmployeeDirectoryService service)
        {
            _service = service;
        }

        // Twilio will call this whenever our phone # receives an SMS message.
        [HttpPost]
        public async Task<TwiMLResult> Lookup(SmsRequest request)
        {
            var incomingMessageText = request.Body;

            var employees = await GetEmployeesIfNumericInput(incomingMessageText) ??
                            await _service.FindByNamePartialAsync(incomingMessageText);

            var response = GetTwilioResponseForEmployees(employees, incomingMessageText);
            return TwiML(response);
        }

        private async Task<IEnumerable<Employee>> GetEmployeesIfNumericInput(string incomingMessageText)
        {
            var num = ParseNumber(incomingMessageText);
            var keys = GetListOfKeysFromCookie();

            if (IsNumberAndInListOfKeys(num, keys))
            {
                return await GetListOfOneEmployeeById(keys, num);
            }

            return null;
        }

        private static int ParseNumber(string incomingMessageText)
        {
            int value;
            return int.TryParse(incomingMessageText, out value) ? value : 0;
        }

        private string[] GetListOfKeysFromCookie()
        {
            var cookie = Request.Cookies.Get(CookieName);
            if (cookie == null || cookie.Value == null) return null;
            return cookie.Value.Split('~');
        }

        private static bool IsNumberAndInListOfKeys(int num, IReadOnlyList<string> keys)
        {
            return num > 0 && keys != null && num <= keys.Count;
        }

        private async Task<IEnumerable<Employee>> GetListOfOneEmployeeById(IReadOnlyList<string> keys, int num)
        {
            var id = ParseNumber(keys[num - 1]);
            var employee = await _service.FindByIdAsync(id);
            return employee == null ? null : ConvertToArray(employee);
        }

        private static IEnumerable<Employee> ConvertToArray(Employee employee)
        {
            return new [] { employee };
        }

        private MessagingResponse GetTwilioResponseForEmployees(IEnumerable<Employee> employees, string incomingMessageText)
        {
            var response = new MessagingResponse();
            var employeeList = employees.ToList();

            switch (employeeList.Count)
            {
                case 0: // No Employees Found
                    response.Message(string.Format(NotFoundMessage, incomingMessageText));
                    break;

                case 1: // A Single Employee Found
                    var employee = employeeList.First();
                    response.Message(
                        new Message().Body(
                            employee.FullName + " - " + 
                            employee.Email + " - " + 
                            employee.PhoneNumber)
                            .Media(employee.ImageUrl));
                    break;

                default: // Multiple Employees Found
                    response.Message(GetMessageForMultipleEmployees(employeeList));
                    break;
            }

            return response;
        }

        private string GetMessageForMultipleEmployees(IReadOnlyList<Employee> employees)
        {
            var msg = "We found: ";
            var idList = "";
            for (var i = 0; i < employees.Count; i++)
            {
                msg += i + 1 + "-" + employees[i].FullName;
                idList += employees[i].Id;
                if (i == employees.Count - 1) continue;
                msg += ", ";
                idList += "~";
            }
            msg += " - Reply with # of desired person";

            AddListOfIdsCookie(idList);
            return msg;
        }

        private void AddListOfIdsCookie(string idList)
        {
            var cookie = new HttpCookie(CookieName, idList)
            {
                Expires = DateTime.UtcNow.AddHours(4)
            };
            Response.Cookies.Add(cookie);
        }
    }
}

Let's see how these options are stored next.

Cache the List of Possible Matches

For the message text returned to the user, we build a numbered menu of possible matches.

Our app needs to remember — between SMS messages from the user — the mapping of the 1, 2, 3 selection numbers to the actual unique ID's of employees. You will notice we are placing them in a cookie, which Twilio will send back with every HTTP request to our application.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using EmployeeDirectory.Web.Models;
using EmployeeDirectory.Web.Services;
using Twilio.AspNet.Common;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace EmployeeDirectory.Web.Controllers
{
    public class EmployeeController : TwilioController
    {
        public const string NotFoundMessage = "We could not find an employee matching '{0}'";
        public const string CookieName = "EmpDirIdList";
        private readonly EmployeeDirectoryService _service;

        public EmployeeController()
        {
            _service = new EmployeeDirectoryService(new EmployeeDbContext());
        }

        public EmployeeController(EmployeeDirectoryService service)
        {
            _service = service;
        }

        // Twilio will call this whenever our phone # receives an SMS message.
        [HttpPost]
        public async Task<TwiMLResult> Lookup(SmsRequest request)
        {
            var incomingMessageText = request.Body;

            var employees = await GetEmployeesIfNumericInput(incomingMessageText) ??
                            await _service.FindByNamePartialAsync(incomingMessageText);

            var response = GetTwilioResponseForEmployees(employees, incomingMessageText);
            return TwiML(response);
        }

        private async Task<IEnumerable<Employee>> GetEmployeesIfNumericInput(string incomingMessageText)
        {
            var num = ParseNumber(incomingMessageText);
            var keys = GetListOfKeysFromCookie();

            if (IsNumberAndInListOfKeys(num, keys))
            {
                return await GetListOfOneEmployeeById(keys, num);
            }

            return null;
        }

        private static int ParseNumber(string incomingMessageText)
        {
            int value;
            return int.TryParse(incomingMessageText, out value) ? value : 0;
        }

        private string[] GetListOfKeysFromCookie()
        {
            var cookie = Request.Cookies.Get(CookieName);
            if (cookie == null || cookie.Value == null) return null;
            return cookie.Value.Split('~');
        }

        private static bool IsNumberAndInListOfKeys(int num, IReadOnlyList<string> keys)
        {
            return num > 0 && keys != null && num <= keys.Count;
        }

        private async Task<IEnumerable<Employee>> GetListOfOneEmployeeById(IReadOnlyList<string> keys, int num)
        {
            var id = ParseNumber(keys[num - 1]);
            var employee = await _service.FindByIdAsync(id);
            return employee == null ? null : ConvertToArray(employee);
        }

        private static IEnumerable<Employee> ConvertToArray(Employee employee)
        {
            return new [] { employee };
        }

        private MessagingResponse GetTwilioResponseForEmployees(IEnumerable<Employee> employees, string incomingMessageText)
        {
            var response = new MessagingResponse();
            var employeeList = employees.ToList();

            switch (employeeList.Count)
            {
                case 0: // No Employees Found
                    response.Message(string.Format(NotFoundMessage, incomingMessageText));
                    break;

                case 1: // A Single Employee Found
                    var employee = employeeList.First();
                    response.Message(
                        new Message().Body(
                            employee.FullName + " - " + 
                            employee.Email + " - " + 
                            employee.PhoneNumber)
                            .Media(employee.ImageUrl));
                    break;

                default: // Multiple Employees Found
                    response.Message(GetMessageForMultipleEmployees(employeeList));
                    break;
            }

            return response;
        }

        private string GetMessageForMultipleEmployees(IReadOnlyList<Employee> employees)
        {
            var msg = "We found: ";
            var idList = "";
            for (var i = 0; i < employees.Count; i++)
            {
                msg += i + 1 + "-" + employees[i].FullName;
                idList += employees[i].Id;
                if (i == employees.Count - 1) continue;
                msg += ", ";
                idList += "~";
            }
            msg += " - Reply with # of desired person";

            AddListOfIdsCookie(idList);
            return msg;
        }

        private void AddListOfIdsCookie(string idList)
        {
            var cookie = new HttpCookie(CookieName, idList)
            {
                Expires = DateTime.UtcNow.AddHours(4)
            };
            Response.Cookies.Add(cookie);
        }
    }
}

When the user that queried the employee directory receives the message with a list of employees, they will text back a number that corresponds to the result on the list that they are interested in querying further. Twilio will send a request to the webhook which handles incoming SMS messages. At this point, our app will try to parse the user's message to determine what to do next.

Return Employee's Contact Information by Number Choice

When we receive an SMS message, we check whether:

  • The body of the text input is, in fact, a number.
  • A cookie exists with the mapping of numbers to ID's.
  • The mappings contain the number sent to us by the user.

If any of those checks fail, then we'll simply proceed with our typical name lookup.

However, if those conditions are all met, we return a single employee that matches their selection.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using EmployeeDirectory.Web.Models;
using EmployeeDirectory.Web.Services;
using Twilio.AspNet.Common;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace EmployeeDirectory.Web.Controllers
{
    public class EmployeeController : TwilioController
    {
        public const string NotFoundMessage = "We could not find an employee matching '{0}'";
        public const string CookieName = "EmpDirIdList";
        private readonly EmployeeDirectoryService _service;

        public EmployeeController()
        {
            _service = new EmployeeDirectoryService(new EmployeeDbContext());
        }

        public EmployeeController(EmployeeDirectoryService service)
        {
            _service = service;
        }

        // Twilio will call this whenever our phone # receives an SMS message.
        [HttpPost]
        public async Task<TwiMLResult> Lookup(SmsRequest request)
        {
            var incomingMessageText = request.Body;

            var employees = await GetEmployeesIfNumericInput(incomingMessageText) ??
                            await _service.FindByNamePartialAsync(incomingMessageText);

            var response = GetTwilioResponseForEmployees(employees, incomingMessageText);
            return TwiML(response);
        }

        private async Task<IEnumerable<Employee>> GetEmployeesIfNumericInput(string incomingMessageText)
        {
            var num = ParseNumber(incomingMessageText);
            var keys = GetListOfKeysFromCookie();

            if (IsNumberAndInListOfKeys(num, keys))
            {
                return await GetListOfOneEmployeeById(keys, num);
            }

            return null;
        }

        private static int ParseNumber(string incomingMessageText)
        {
            int value;
            return int.TryParse(incomingMessageText, out value) ? value : 0;
        }

        private string[] GetListOfKeysFromCookie()
        {
            var cookie = Request.Cookies.Get(CookieName);
            if (cookie == null || cookie.Value == null) return null;
            return cookie.Value.Split('~');
        }

        private static bool IsNumberAndInListOfKeys(int num, IReadOnlyList<string> keys)
        {
            return num > 0 && keys != null && num <= keys.Count;
        }

        private async Task<IEnumerable<Employee>> GetListOfOneEmployeeById(IReadOnlyList<string> keys, int num)
        {
            var id = ParseNumber(keys[num - 1]);
            var employee = await _service.FindByIdAsync(id);
            return employee == null ? null : ConvertToArray(employee);
        }

        private static IEnumerable<Employee> ConvertToArray(Employee employee)
        {
            return new [] { employee };
        }

        private MessagingResponse GetTwilioResponseForEmployees(IEnumerable<Employee> employees, string incomingMessageText)
        {
            var response = new MessagingResponse();
            var employeeList = employees.ToList();

            switch (employeeList.Count)
            {
                case 0: // No Employees Found
                    response.Message(string.Format(NotFoundMessage, incomingMessageText));
                    break;

                case 1: // A Single Employee Found
                    var employee = employeeList.First();
                    response.Message(
                        new Message().Body(
                            employee.FullName + " - " + 
                            employee.Email + " - " + 
                            employee.PhoneNumber)
                            .Media(employee.ImageUrl));
                    break;

                default: // Multiple Employees Found
                    response.Message(GetMessageForMultipleEmployees(employeeList));
                    break;
            }

            return response;
        }

        private string GetMessageForMultipleEmployees(IReadOnlyList<Employee> employees)
        {
            var msg = "We found: ";
            var idList = "";
            for (var i = 0; i < employees.Count; i++)
            {
                msg += i + 1 + "-" + employees[i].FullName;
                idList += employees[i].Id;
                if (i == employees.Count - 1) continue;
                msg += ", ";
                idList += "~";
            }
            msg += " - Reply with # of desired person";

            AddListOfIdsCookie(idList);
            return msg;
        }

        private void AddListOfIdsCookie(string idList)
        {
            var cookie = new HttpCookie(CookieName, idList)
            {
                Expires = DateTime.UtcNow.AddHours(4)
            };
            Response.Cookies.Add(cookie);
        }
    }
}

The only thing left to do is celebrate.

You've come pretty far. Take a look at the code on GitHub to run the application yourself. There you will find the complete Visual Studio solution and instructions for getting up and running.

Where to Next?

If you're a C# developer working with Twilio, you might want to check out these other tutorials.

Two-Factor Authentication

Appointment Reminders

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think!