Введение в создание приложений Twilio с использованием ASP.NET MVC

Автор: | 17.07.2026

На прошлой неделе мне выпала честь выступить с докладом «Введение в создание приложений Twilio с использованием ASP.NET MVC» на mvcConf 2 — виртуальной конференции для разработчиков ASP.NET MVC с более чем 3000 зарегистрированными участниками. В докладе были рассмотрены способы использования возможностей фреймворка ASP.NET MVC для упрощения разработки голосовых приложений и приложений для обмена SMS. Вы можете посмотреть запись сессии ниже. Если вы предпочитаете текст видео, продолжайте читать для получения дополнительной информации о темах, затронутых в докладе.

Этот пост предполагает базовое знание работы ASP.NET MVC и TwiML. В своих примерах я использую MVC Music Store в качестве отправной точки. Исходный код магазина можно найти на CodePlex.

Сначала мы создадим функцию запроса статуса заказа, с помощью которой клиенты смогут звонить, чтобы получить обновления о заказе. Затем я продемонстрирую, как защитить действия контроллеров с помощью проверки запросов. Наконец, мы соберём все части вместе, чтобы создать ту же функцию запроса статуса заказа, но с использованием SMS-интерфейса вместо телефонного звонка.

Когда звонок поступает на ваш номер Twilio, на URL, указанный в панели управления вашего аккаунта, отправляется POST-запрос. Ваш URL отвечает TwiML для управления звонком. Давайте создадим новый CallController.cs и добавим наш первый метод действия:

«`csharp

public ActionResult Incoming(string CallSid, string From, string To)

{

var doc = new XDocument();

var response = new XElement(«Response»);

var gather = new XElement(«Gather»,

new XAttribute(«action», Url.Action(«Input»)),

new XElement(«Say», «Спасибо за звонок. Пожалуйста, введите номер вашего заказа, а затем нажмите решётку.»)

);

response.Add(gather);

doc.Add(response);

return new ContentResult

{

Content = doc.ToString(),

ContentType = «text/xml»

};

}

«`

Первое, что вы заметите в приведённом выше коде, — это параметры метода. Они привязываются к некоторым POST-параметрам, которые Twilio отправляет с каждым запросом, используя автоматическую привязку параметров, предоставляемую ASP.NET MVC. Затем мы формируем XML-документ, который предлагает звонящему ввести номер заказа, и возвращаем результат. Я начинаю с «длинного пути», чтобы продемонстрировать концепции. Позже мы рефакторим код, чтобы сделать его более лаконичным и идиоматичным для ASP.NET MVC.

На данном этапе вы можете загрузить это на общедоступный URL, установить голосовой URL вашего номера телефона на *http://example.com/Incoming* и позвонить на свой номер, чтобы услышать приглашение. Если вы введёте цифру, то получите ошибку, так как мы ещё не написали эту часть. Давайте сделаем это сейчас.

Элемент , который мы используем, включает параметр action, указывающий на метод действия ‘Input’. Когда цифры собраны, POST-запрос будет отправлен на этот метод действия с параметром ‘Digits’ вместе со стандартными данными.

«`csharp

public ActionResult Input(string Digits, string CallSid, string From, string To)

{

var id = Convert.ToInt32(Digits.Replace(«#», «»));

var order = storeDB.Orders.Find(id);

var doc = new XDocument();

var response = new XElement(«Response»);

if (order != null)

{

var orderStatus = order.Status;

var orderNum = string.Join(«. «, id.ToString().ToCharArray());

var say = new XElement(«Say», «Статус заказа » + orderNum + «. — » + orderStatus + «.»);

say.Add(new XAttribute(«voice», «woman»));

say.Add(new XAttribute(«loop», 3));

response.Add(say);

}

else

{

response.Add(new XElement(«Say», «Мы не смогли найти ваш заказ. Пожалуйста, попробуйте снова.»));

response.Add(new XElement(«Redirect», Url.Action(«Incoming»)));

}

doc.Add(response);

return new ContentResult

{

Content = doc.ToString(),

ContentType = «text/xml»

};

}

«`

Мы начинаем с удаления символа # из параметра Digits, так как нам нужен только идентификатор заказа. После получения очищенного ввода мы запрашиваем базу данных по введённому идентификатору заказа. Если заказ найден, статус зачитывается звонящему три раза женским голосом. Если заказ не найден, мы уведомляем звонящего и перенаправляем его обратно к методу действия /Incoming, чтобы начать процесс заново.

Операторы возврата в конце обоих методов работают, но выглядят не очень красиво. Для их улучшения мы создадим пользовательский TwimlResult, который наследуется от ActionResult.

«`csharp

public class TwimlResult : ActionResult

{

public XDocument Response { get; set; }

public TwimlResult(XDocument response)

{

Response = response;

}

public override void ExecuteResult(ControllerContext context)

{

context.HttpContext.Response.ContentType = «text/xml»;

Response.Save(context.HttpContext.Response.Output);

}

}

«`

Этот класс принимает XDocument, устанавливает заголовок типа контента в нужное значение и записывает содержимое в выходной поток. Теперь мы можем обновить действия контроллеров, чтобы они просто делали следующее:

«`csharp

return new TwimlResult(doc);

«`

Мы можем сделать это ещё короче, как вы увидите далее.

Если вы собираетесь возвращать много TwiML, то вскоре обнаружите, что повторяете много общих шагов в своих контроллерах. Мы можем перенести общую логику в базовый класс TwilioController, от которого будут наследоваться все наши контроллеры.

Первое, что мы можем сделать, — это добавить метод в наш базовый контроллер, чтобы упростить возврат TwimlResult.

«`csharp

public class TwimlController : Controller

{

public TwimlResult Twiml(XDocument response)

{

return new TwimlResult(response);

}

}

«`

Теперь в наших действиях контроллеров мы можем отправлять TwiML с помощью **return Twiml(doc);**

Ранее я упоминал, что существуют общие параметры, которые отправляются с каждым запросом. Вместо использования привязки параметров для их получения при каждом запросе мы можем перенести эту логику в базовый контроллер. Обратите внимание, что если вы активно тестируете свои контроллеры, этот метод может усложнить их тестирование, поэтому он подходит не всем. Если вы согласны с оговорками, добавьте следующий код в TwilioController:

«`csharp

public string CallSid { get; set; }

public string To { get; set; }

public string From { get; set; }

public string AccountSid { get; set; }

public string CallStatus { get; set; }

public string FromCity { get; set; }

public string FromState { get; set; }

public string FromZip { get; set; }

public string FromCountry { get; set; }

public string ToCity { get; set; }

public string ToState { get; set; }

public string ToZip { get; set; }

public string ToCountry { get; set; }

public string Digits { get; set; }

public string SmsSid { get; set; }

public string Body { get; set; }

protected override void OnActionExecuting(ActionExecutingContext filterContext)

{

CallSid = filterContext.HttpContext.Request.Params[«CallSid»];

To = filterContext.HttpContext.Request.Params[«To»];

From = filterContext.HttpContext.Request.Params[«From»];

AccountSid = filterContext.HttpContext.Request.Params[«AccountSid»];

CallStatus = filterContext.HttpContext.Request.Params[«CallStatus»];

FromCity = filterContext.HttpContext.Request.Params[«FromCity»];

FromState = filterContext.HttpContext.Request.Params[«FromState»];

FromZip = filterContext.HttpContext.Request.Params[«FromZip»];

FromCountry = filterContext.HttpContext.Request.Params[«FromCountry»];

ToCity = filterContext.HttpContext.Request.Params[«ToCity»];

ToState = filterContext.HttpContext.Request.Params[«ToState»];

ToZip = filterContext.HttpContext.Request.Params[«ToZip»];

ToCountry = filterContext.HttpContext.Request.Params[«ToCountry»];

Digits = filterContext.HttpContext.Request.Params[«Digits»];

SmsSid = filterContext.HttpContext.Request.Params[«SmsSid»];

Body = filterContext.HttpContext.Request.Params[«Body»];

base.OnActionExecuting(filterContext);

}

«`

Этот код извлекает POST-параметры и заполняет свойства, чтобы они были доступны в строго типизированном виде при каждом запросе. В наших действиях контроллеров теперь можно писать код следующим образом:

«`csharp

public ActionResult Input()

{

var id = Convert.ToInt32(Digits.Replace(«#», «»)); // Digits предзаполнен TwilioController

var order = storeDB.Orders.Find(id);

// и так далее

}

«`

Я также включил вспомогательные методы для генерации XML-элементов в мой TwilioController.

«`csharp

public XElement Verb(string name, object value)

{

return Verb(name, value, new { });

}

public XElement Verb(string name, object value, object attributes)

{

var element = new XElement(name, value);

foreach (var prop in attributes.GetType().GetProperties())

{

element.Add(new XAttribute(prop.Name, prop.GetValue(attributes, null)));

}

return element;

}

«`

Вы можете использовать это для более простого построения ваших TwiML-ответов следующим образом:

«`csharp

public ActionResult Incoming()

{

var doc = new XDocument();

var response = new XElement(«Response»);

var gather = Verb(«Gather»,

Verb(«Say», «Спасибо за звонок. Пожалуйста, введите номер вашего заказа, а затем нажмите решётку.»),

new { action = Url.Action(«Input») }

);

response.Add(gather);

doc.Add(response);

return Twiml(doc);

}

«`

Поскольку URL-адреса, которые вы используете для обработки запросов Twilio, должны быть общедоступными, они становятся открытыми для всех, кто знает URL. Мы используем систему под названием «Проверка запросов» (Request Validation), чтобы вы могли убедиться, что запрос исходит от нас. Вкратце, мы объединяем все элементы запроса, хешируем их с вашим AuthToken и отправляем подпись в заголовке. Вы можете сгенерировать то же значение на своём сервере и сравнить значения, чтобы убедиться в легитимности запроса.

Я включил всю логику для этого в атрибут ValidateRequestAttribute, который можно прикрепить к конкретным действиям контроллеров, целому контроллеру или базовому классу TwilioController. Класс немного длинный для поста, но вы можете найти его в приложении в конце. Использовать его просто:

«`csharp

[ValidateRequest(«AuthToken»)] // замените на ваш AuthToken из панели управления

public class TwimlController : Controller

{

}

«`

Теперь, если кто-то попытается получить доступ к вашему URL, не являясь Twilio, он получит ошибку. Обратите внимание, что запросы, которые исходят и заканчиваются на вашем локальном компьютере, исключены из проверки, чтобы упростить тестирование во время локальной разработки.

Мы можем использовать часть той же логики и наши новые удобства в TwilioController для написания SmsController, чтобы проверять статус заказа на основе номера телефона отправителя:

«`csharp

[ValidateRequest(«AuthToken»)]

public class SmsController : TwimlController

{

MusicStoreEntities storeDB = new MusicStoreEntities();

public ActionResult Incoming()

{

var doc = new XDocument();

var response = new XElement(«Response»);

// получить последний заказ по номеру телефона

var order = from o in storeDB.Orders

where o.Phone == From

orderby o.OrderDate descending

select o;

if (order.Any())

{

var mostRecent = order.First();

response.Add(Verb(«Sms»,

«Заказ » + mostRecent.OrderId + » имеет статус » + mostRecent.Status + «.»)

);

}

else

{

response.Add(Verb(«Sms», «Заказ не найден.»));

}

doc.Add(response);

return Twiml(doc);

}

}

«`

Я много времени уделил входящим сценариям использования, но вы также можете легко использовать Twilio для отправки SMS-сообщений и совершения исходящих звонков. У меня есть вспомогательная библиотека, которую я собрал, чтобы упростить это в .NET, и вы можете найти её на GitHub. Мы объединим эту библиотеку и некоторые идеи из этого поста в новую официальную .NET-библиотеку, которая скоро станет доступна.

Вы можете скачать код из этого поста здесь. Чтобы использовать их, просто добавьте эти файлы в ваш проект MVC и обновите пространства имён по мере необходимости.

Если у вас есть вопросы по темам, обсуждаемым в этом посте, не стесняйтесь оставлять комментарии ниже, публиковать сообщения на наших форумах или писать нашим сотрудникам поддержки, которые будут рады помочь вам.