Automated Survey with C# and ASP.NET MVC

Download the Code

This ASP.NET MVC sample application demonstrates the use of the Twilio C# helper library and TwiML to deliver a survey to be completed via voice call.

In this tutorial, we'll walk you through the code necessary to power our automated survey. To run this sample app yourself, download the code and follow the instructions on GitHub.

Loading Code Samples...
Language
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace AutomatedSurvey.Web.Models
{
    public class Survey
    {
        public int Id { get; set; }
        [Required]
        public string Title { get; set; }
        [Timestamp]
        public byte[] Timestamp { get; set; }
        public virtual IList<Question> Questions { get; set; }
    }
}
/AutomatedSurvey.Web/Models/Survey.cs
Create a model to represent a survey in the database.

/AutomatedSurvey.Web/Models/Survey.cs

Building an automated survey can seem like a daunting project to take on. With the help of the Twilio C# helper library, we will do this in a few simple steps! Click the button below to get started.

Instacart uses Twilio to power their customer service surveys and integrate that feedback into their customer database. Read more here.

Configuring the Twilio Phone Number

To receive incoming calls, we need to setup our Twilio phone number.  

IVR Webhook Configuration

As you can see, the Twilio Console makes it easy to setup your webhooks for handling SMS messages and phone calls for your phone numbers. Setting up your Twilio phone number is a critical step for letting Twilio know where to forward messages and calls. If you don't already have a running server for this project, you can test your webhooks locally using ngrok.

Up next, we will dynamically create a survey from a series of questions stored in a database.

Creating a Survey

The questions are created using the Configuration.Seed method. The advantage of this approach is that the data is populated when you run Update-Database.

Loading Code Samples...
Language
using System.Data.Entity.Migrations;
using AutomatedSurvey.Web.Models;

namespace AutomatedSurvey.Web.Migrations
{
    internal sealed class Configuration : DbMigrationsConfiguration<AutomatedSurveysContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(AutomatedSurveysContext context)
        {
            context.Surveys.AddOrUpdate(
                survey => new { survey.Id, survey.Title },
                new Survey { Id = 1, Title = "Twilio" });

            context.SaveChanges();

            context.Questions.AddOrUpdate(
                question => new { question.Body, question.Type, question.SurveyId },
                new Question
                {
                    Body = "Hello. Thanks for taking the Twilio Developer Education survey. On a scale of 0 to 9 how would you rate this tutorial?",
                    Type = QuestionType.Numeric,
                    SurveyId = 1
                },
                new Question
                {
                    Body = "On a scale of 0 to 9 how would you rate the design of this tutorial?",
                    Type = QuestionType.Numeric,
                    SurveyId = 1
                },
                new Question
                {
                    Body = "In your own words please describe your feelings about Twilio right now? Press the pound sign when you are finished.",
                    Type = QuestionType.Voice,
                    SurveyId = 1
                },
                new Question
                {
                    Body = "Do you like my voice? Please be honest, I dislike liars.",
                    Type = QuestionType.YesNo,
                    SurveyId = 1
                });

            context.SaveChanges();
        }
    }
}
/AutomatedSurvey.Web/Migrations/Configuration.cs
Seed survey questions in the DB.

/AutomatedSurvey.Web/Migrations/Configuration.cs

Now you know how to seed your database with the first survey. Next, we need to generate the TwiML for the initial response sent to the user.

Responding to Twilio's Initial Request

Whenever one of your Twilio phone numbers receives a call, Twilio will forward the request to the appropriate webhook.

For this application Twilio should be configured to make a GET request to the application’s surveys/connectcall endpoint. Here the application finds the last created survey. Then, it uses the <Say> verb to speak the welcome message, and the <Redirect> verb to redirect to the first question in the survey.

Loading Code Samples...
Language
using System.Linq;
using System.Web.Mvc;
using AutomatedSurvey.Web.Models;
using AutomatedSurvey.Web.Models.Repository;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Controllers
{
    public class SurveysController : TwilioController
    {
        private readonly IRepository<Survey> _surveysRepository;
        private readonly IRepository<Answer> _answersRepository;

        public SurveysController() : this(new SurveysRespository(), new AnswersRepository()) { }

        public SurveysController(
            IRepository<Survey> surveysRepository, IRepository<Answer> answersRepository)
        {
            _surveysRepository = surveysRepository;
            _answersRepository = answersRepository;
        }

        // Webhook for Twilio survey number
        // GET: connectcall
        public ActionResult ConnectCall()
        {
            var response = new VoiceResponse();
            var survey = _surveysRepository.FirstOrDefault();
            var welcomeMessage = string.Format("Thank you for taking the {0} survey", survey.Title);

            response.Say(welcomeMessage);
            response.Redirect(Url.Action("find", "questions", new { id = 1 }));

            return TwiML(response);
        }

        // GET: surveys/results
        public ActionResult Results()
        {
            var answers = _answersRepository.All();
            var uniqueAnswers = answers
                .Select(answer => answer.CallSid)
                .Distinct().ToList();

            ViewBag.UniqueAnswers = uniqueAnswers;
            ViewBag.SurveyTitle = _surveysRepository.FirstOrDefault().Title;
            return View(answers);
        }
    }
}
/AutomatedSurvey.Web/Controllers/SurveysController.cs
Generate welcome message with TwiML and redirect users to the first survey question.

/AutomatedSurvey.Web/Controllers/SurveysController.cs

Now you've seen how to handle incoming calls and generate the TwiML necessary to redirect the user to the first question. In the next section, we will see how to build a voice response and gather user input at the same time.

Asking the Caller a Question

At this point, Twilio has made a request for the first question. We’re using the Twilio C# helper library to generate a TwiML response.

After using the verb <Say> to ask the user a question, we use either the verb  <Gather>  or the verb <Record> to collect an answer, depending on what type of question your survey will ask.

Loading Code Samples...
Language
using System.Collections.Generic;
using AutomatedSurvey.Web.Models;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Domain
{
    public class Response
    {
        private readonly Question _question;

        public Response(Question question)
        {
            _question = question;
        }

        public static IDictionary<QuestionType, string> QuestionTypeToMessage
        {
            get
            {
                return new Dictionary<QuestionType, string>
                {
                    {QuestionType.Voice, "Please record your answer after the beep and then hit the pound sign"},
                    {QuestionType.Numeric, "Please press a number between 0 and 9 and then hit the pound sign"},
                    {QuestionType.YesNo, "Please press the 1 for yes and the 0 for no and then hit the pound sign"}
                };
            }
        }

        /// <summary>
        /// Builds an instance.
        /// </summary>
        /// <returns>A new instance of the VoiceResponse</returns>
        public VoiceResponse Build()
        {
            var response = new VoiceResponse();
            response.Say(_question.Body);
            response.Say(QuestionTypeToMessage[_question.Type]);
            AddRecordOrGatherCommands(response);

            return response;
        }

        private void AddRecordOrGatherCommands(VoiceResponse response)
        {
            var questionType = _question.Type;
            switch (questionType)
            {
                case QuestionType.Voice:
                    response.Record(action: GenerateUrl(_question));
                    break;
                case QuestionType.Numeric:
                case QuestionType.YesNo:
                    response.Gather(action: GenerateUrl(_question));
                    break;
            }
        }

        private static string GenerateUrl(Question question)
        {
            return string.Format("/answers/create?questionId={0}", question.Id);
        }
    }
}
/AutomatedSurvey.Web/Domain/Response.cs
Build a TwiML response for the next question.

/AutomatedSurvey.Web/Domain/Response.cs

Next, we will explore how to gather diverse types of information such as numbers and voice recordings.

Ask different types of questions

If we want a number or boolean (yes/no) response from the user, we use the <Gather> verb. For a free-form speech response, we use <Record> to collect an answer.

Both TwiML tags have an action attribute and a method attribute. Twilio will use both attributes to make another HTTP request to our application with the user's answer.

More specifically, the action attribute will be set to POST to an action method named answers/create which will be used to save the user's answer.

Loading Code Samples...
Language
using System.Collections.Generic;
using AutomatedSurvey.Web.Models;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Domain
{
    public class Response
    {
        private readonly Question _question;

        public Response(Question question)
        {
            _question = question;
        }

        public static IDictionary<QuestionType, string> QuestionTypeToMessage
        {
            get
            {
                return new Dictionary<QuestionType, string>
                {
                    {QuestionType.Voice, "Please record your answer after the beep and then hit the pound sign"},
                    {QuestionType.Numeric, "Please press a number between 0 and 9 and then hit the pound sign"},
                    {QuestionType.YesNo, "Please press the 1 for yes and the 0 for no and then hit the pound sign"}
                };
            }
        }

        /// <summary>
        /// Builds an instance.
        /// </summary>
        /// <returns>A new instance of the VoiceResponse</returns>
        public VoiceResponse Build()
        {
            var response = new VoiceResponse();
            response.Say(_question.Body);
            response.Say(QuestionTypeToMessage[_question.Type]);
            AddRecordOrGatherCommands(response);

            return response;
        }

        private void AddRecordOrGatherCommands(VoiceResponse response)
        {
            var questionType = _question.Type;
            switch (questionType)
            {
                case QuestionType.Voice:
                    response.Record(action: GenerateUrl(_question));
                    break;
                case QuestionType.Numeric:
                case QuestionType.YesNo:
                    response.Gather(action: GenerateUrl(_question));
                    break;
            }
        }

        private static string GenerateUrl(Question question)
        {
            return string.Format("/answers/create?questionId={0}", question.Id);
        }
    }
}
/AutomatedSurvey.Web/Domain/Response.cs
Generate the gather verb to correspond to the question type.

/AutomatedSurvey.Web/Domain/Response.cs

Now you can generate the TwiML response that will ask your question, as well as set the correct method to gather input that will best serve your analysis. However, we still need to store all of the information gathered in the various steps of the survey for use later on.

Record the caller’s response

When the callers have finished entering their responses, Twilio will make a request to this controller with all the call parameters we will need. For this sample application, we will store either the RecordingUrl for free-form voice responses, or parse Digits for number and boolean responses. We'll also track the CallSid so we can track answers for a particular survey response.

Loading Code Samples...
Language
using System.Web.Mvc;
using AutomatedSurvey.Web.Domain;
using AutomatedSurvey.Web.Models;
using AutomatedSurvey.Web.Models.Repository;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Controllers
{
    public class AnswersController : TwilioController
    {
        private readonly IRepository<Question> _questionsRepository;
        private readonly IRepository<Answer> _answersRepository;

        public AnswersController()
            : this(
                new QuestionsRepository(),
                new AnswersRepository()) { }

        public AnswersController(
            IRepository<Question> questionsRepository,
            IRepository<Answer> answersRepository
            )
        {
            _questionsRepository = questionsRepository;
            _answersRepository = answersRepository;
        }

        [HttpPost]
        public ActionResult Create(
            [Bind(Include = "QuestionId,RecordingUrl,Digits,CallSid,From")]
            Answer answer)
        {
            _answersRepository.Create(answer);

            var nextQuestion = new QuestionFinder(_questionsRepository).FindNext(answer.QuestionId);
            var response = (nextQuestion != null ? new Response(nextQuestion).Build() : ExitResponse);

            return TwiML(response);
        }

        private static VoiceResponse ExitResponse
        {
            get
            {
                var response = new VoiceResponse();
                response.Say("Thanks for your time. Good bye");
                response.Hangup();
                return response;
            }
        }
    }
}
/AutomatedSurvey.Web/Controllers/AnswersController.cs
Save the answer to the DB and redirect the user to the next question.

/AutomatedSurvey.Web/Controllers/AnswersController.cs

Most surveys include more than a single question so we must now redirect the user to the next question in this survey until the survey is complete. Once the survey is complete, you'll probably want an easy way to look at the survey results.

Display the survey results

For this route we simply query the database and display the information using a Razor template.

You can access this page in the application at /surveys/results.

Loading Code Samples...
Language
using System.Linq;
using System.Web.Mvc;
using AutomatedSurvey.Web.Models;
using AutomatedSurvey.Web.Models.Repository;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Controllers
{
    public class SurveysController : TwilioController
    {
        private readonly IRepository<Survey> _surveysRepository;
        private readonly IRepository<Answer> _answersRepository;

        public SurveysController() : this(new SurveysRespository(), new AnswersRepository()) { }

        public SurveysController(
            IRepository<Survey> surveysRepository, IRepository<Answer> answersRepository)
        {
            _surveysRepository = surveysRepository;
            _answersRepository = answersRepository;
        }

        // Webhook for Twilio survey number
        // GET: connectcall
        public ActionResult ConnectCall()
        {
            var response = new VoiceResponse();
            var survey = _surveysRepository.FirstOrDefault();
            var welcomeMessage = string.Format("Thank you for taking the {0} survey", survey.Title);

            response.Say(welcomeMessage);
            response.Redirect(Url.Action("find", "questions", new { id = 1 }));

            return TwiML(response);
        }

        // GET: surveys/results
        public ActionResult Results()
        {
            var answers = _answersRepository.All();
            var uniqueAnswers = answers
                .Select(answer => answer.CallSid)
                .Distinct().ToList();

            ViewBag.UniqueAnswers = uniqueAnswers;
            ViewBag.SurveyTitle = _surveysRepository.FirstOrDefault().Title;
            return View(answers);
        }
    }
}
/AutomatedSurvey.Web/Controllers/SurveysController.cs
Query the DB for all of the answers to a specific survey and render the results view.

/AutomatedSurvey.Web/Controllers/SurveysController.cs

As you saw, building an application using the Twilio C# helper library was very straightforward. We hope you found this sample application useful.

Where to Next?

If you are a C# developer working with Twilio, you might want to check out other tutorials:

Employee Directory with C# and ASP.NET MVC

Use Twilio to accept SMS messages and turn them into queries against a SQL database.

IVR: Phone Tree with C# and ASP.NET MVC

IVRs (interactive voice responses) are automated phone systems that can facilitate communication between callers and businesses.

Did this help?

Thanks for checking this tutorial out! If you have any feedback to share with us, we'd love to hear it. Reach out to us on Twitter and let us know what you build!

Agustin Camino
Orlando Hidalgo
Jose Oliveros
Kat King
Andrew Baker
Hector Ortega

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace AutomatedSurvey.Web.Models
{
    public class Survey
    {
        public int Id { get; set; }
        [Required]
        public string Title { get; set; }
        [Timestamp]
        public byte[] Timestamp { get; set; }
        public virtual IList<Question> Questions { get; set; }
    }
}
using System.Data.Entity.Migrations;
using AutomatedSurvey.Web.Models;

namespace AutomatedSurvey.Web.Migrations
{
    internal sealed class Configuration : DbMigrationsConfiguration<AutomatedSurveysContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(AutomatedSurveysContext context)
        {
            context.Surveys.AddOrUpdate(
                survey => new { survey.Id, survey.Title },
                new Survey { Id = 1, Title = "Twilio" });

            context.SaveChanges();

            context.Questions.AddOrUpdate(
                question => new { question.Body, question.Type, question.SurveyId },
                new Question
                {
                    Body = "Hello. Thanks for taking the Twilio Developer Education survey. On a scale of 0 to 9 how would you rate this tutorial?",
                    Type = QuestionType.Numeric,
                    SurveyId = 1
                },
                new Question
                {
                    Body = "On a scale of 0 to 9 how would you rate the design of this tutorial?",
                    Type = QuestionType.Numeric,
                    SurveyId = 1
                },
                new Question
                {
                    Body = "In your own words please describe your feelings about Twilio right now? Press the pound sign when you are finished.",
                    Type = QuestionType.Voice,
                    SurveyId = 1
                },
                new Question
                {
                    Body = "Do you like my voice? Please be honest, I dislike liars.",
                    Type = QuestionType.YesNo,
                    SurveyId = 1
                });

            context.SaveChanges();
        }
    }
}
using System.Linq;
using System.Web.Mvc;
using AutomatedSurvey.Web.Models;
using AutomatedSurvey.Web.Models.Repository;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Controllers
{
    public class SurveysController : TwilioController
    {
        private readonly IRepository<Survey> _surveysRepository;
        private readonly IRepository<Answer> _answersRepository;

        public SurveysController() : this(new SurveysRespository(), new AnswersRepository()) { }

        public SurveysController(
            IRepository<Survey> surveysRepository, IRepository<Answer> answersRepository)
        {
            _surveysRepository = surveysRepository;
            _answersRepository = answersRepository;
        }

        // Webhook for Twilio survey number
        // GET: connectcall
        public ActionResult ConnectCall()
        {
            var response = new VoiceResponse();
            var survey = _surveysRepository.FirstOrDefault();
            var welcomeMessage = string.Format("Thank you for taking the {0} survey", survey.Title);

            response.Say(welcomeMessage);
            response.Redirect(Url.Action("find", "questions", new { id = 1 }));

            return TwiML(response);
        }

        // GET: surveys/results
        public ActionResult Results()
        {
            var answers = _answersRepository.All();
            var uniqueAnswers = answers
                .Select(answer => answer.CallSid)
                .Distinct().ToList();

            ViewBag.UniqueAnswers = uniqueAnswers;
            ViewBag.SurveyTitle = _surveysRepository.FirstOrDefault().Title;
            return View(answers);
        }
    }
}
using System.Collections.Generic;
using AutomatedSurvey.Web.Models;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Domain
{
    public class Response
    {
        private readonly Question _question;

        public Response(Question question)
        {
            _question = question;
        }

        public static IDictionary<QuestionType, string> QuestionTypeToMessage
        {
            get
            {
                return new Dictionary<QuestionType, string>
                {
                    {QuestionType.Voice, "Please record your answer after the beep and then hit the pound sign"},
                    {QuestionType.Numeric, "Please press a number between 0 and 9 and then hit the pound sign"},
                    {QuestionType.YesNo, "Please press the 1 for yes and the 0 for no and then hit the pound sign"}
                };
            }
        }

        /// <summary>
        /// Builds an instance.
        /// </summary>
        /// <returns>A new instance of the VoiceResponse</returns>
        public VoiceResponse Build()
        {
            var response = new VoiceResponse();
            response.Say(_question.Body);
            response.Say(QuestionTypeToMessage[_question.Type]);
            AddRecordOrGatherCommands(response);

            return response;
        }

        private void AddRecordOrGatherCommands(VoiceResponse response)
        {
            var questionType = _question.Type;
            switch (questionType)
            {
                case QuestionType.Voice:
                    response.Record(action: GenerateUrl(_question));
                    break;
                case QuestionType.Numeric:
                case QuestionType.YesNo:
                    response.Gather(action: GenerateUrl(_question));
                    break;
            }
        }

        private static string GenerateUrl(Question question)
        {
            return string.Format("/answers/create?questionId={0}", question.Id);
        }
    }
}
using System.Collections.Generic;
using AutomatedSurvey.Web.Models;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Domain
{
    public class Response
    {
        private readonly Question _question;

        public Response(Question question)
        {
            _question = question;
        }

        public static IDictionary<QuestionType, string> QuestionTypeToMessage
        {
            get
            {
                return new Dictionary<QuestionType, string>
                {
                    {QuestionType.Voice, "Please record your answer after the beep and then hit the pound sign"},
                    {QuestionType.Numeric, "Please press a number between 0 and 9 and then hit the pound sign"},
                    {QuestionType.YesNo, "Please press the 1 for yes and the 0 for no and then hit the pound sign"}
                };
            }
        }

        /// <summary>
        /// Builds an instance.
        /// </summary>
        /// <returns>A new instance of the VoiceResponse</returns>
        public VoiceResponse Build()
        {
            var response = new VoiceResponse();
            response.Say(_question.Body);
            response.Say(QuestionTypeToMessage[_question.Type]);
            AddRecordOrGatherCommands(response);

            return response;
        }

        private void AddRecordOrGatherCommands(VoiceResponse response)
        {
            var questionType = _question.Type;
            switch (questionType)
            {
                case QuestionType.Voice:
                    response.Record(action: GenerateUrl(_question));
                    break;
                case QuestionType.Numeric:
                case QuestionType.YesNo:
                    response.Gather(action: GenerateUrl(_question));
                    break;
            }
        }

        private static string GenerateUrl(Question question)
        {
            return string.Format("/answers/create?questionId={0}", question.Id);
        }
    }
}
using System.Web.Mvc;
using AutomatedSurvey.Web.Domain;
using AutomatedSurvey.Web.Models;
using AutomatedSurvey.Web.Models.Repository;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Controllers
{
    public class AnswersController : TwilioController
    {
        private readonly IRepository<Question> _questionsRepository;
        private readonly IRepository<Answer> _answersRepository;

        public AnswersController()
            : this(
                new QuestionsRepository(),
                new AnswersRepository()) { }

        public AnswersController(
            IRepository<Question> questionsRepository,
            IRepository<Answer> answersRepository
            )
        {
            _questionsRepository = questionsRepository;
            _answersRepository = answersRepository;
        }

        [HttpPost]
        public ActionResult Create(
            [Bind(Include = "QuestionId,RecordingUrl,Digits,CallSid,From")]
            Answer answer)
        {
            _answersRepository.Create(answer);

            var nextQuestion = new QuestionFinder(_questionsRepository).FindNext(answer.QuestionId);
            var response = (nextQuestion != null ? new Response(nextQuestion).Build() : ExitResponse);

            return TwiML(response);
        }

        private static VoiceResponse ExitResponse
        {
            get
            {
                var response = new VoiceResponse();
                response.Say("Thanks for your time. Good bye");
                response.Hangup();
                return response;
            }
        }
    }
}
using System.Linq;
using System.Web.Mvc;
using AutomatedSurvey.Web.Models;
using AutomatedSurvey.Web.Models.Repository;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace AutomatedSurvey.Web.Controllers
{
    public class SurveysController : TwilioController
    {
        private readonly IRepository<Survey> _surveysRepository;
        private readonly IRepository<Answer> _answersRepository;

        public SurveysController() : this(new SurveysRespository(), new AnswersRepository()) { }

        public SurveysController(
            IRepository<Survey> surveysRepository, IRepository<Answer> answersRepository)
        {
            _surveysRepository = surveysRepository;
            _answersRepository = answersRepository;
        }

        // Webhook for Twilio survey number
        // GET: connectcall
        public ActionResult ConnectCall()
        {
            var response = new VoiceResponse();
            var survey = _surveysRepository.FirstOrDefault();
            var welcomeMessage = string.Format("Thank you for taking the {0} survey", survey.Title);

            response.Say(welcomeMessage);
            response.Redirect(Url.Action("find", "questions", new { id = 1 }));

            return TwiML(response);
        }

        // GET: surveys/results
        public ActionResult Results()
        {
            var answers = _answersRepository.All();
            var uniqueAnswers = answers
                .Select(answer => answer.CallSid)
                .Distinct().ToList();

            ViewBag.UniqueAnswers = uniqueAnswers;
            ViewBag.SurveyTitle = _surveysRepository.FirstOrDefault().Title;
            return View(answers);
        }
    }
}