Использование результатов действий
Пользовательские результаты действий могут использоваться для удаления кода, продублированного в рамках методов, и для извлечения зависимостей, которые могут усложнить тестирование действия. Хороший способ применения пользовательского результата действия – формирование функциональности, за исключением готовых к применению (так называемой "out-of-the-box") ActionResult
таких, как ViewResult
или RedirectResult
.
Избавление от дублирования с помощью результата действия
Чтобы избавиться от дублирования в сложных похожих методах, вы можете извлечь большинство кода и переместить его в результат действия. Приведенный ниже листинг демонстрирует, как отделить логику создания файла, в котором содержатся значения, разделенные запятыми (comma-separated value file), от коллекции объектов и инкапсулировать ее в результате действия.
Листинг 16-2: Класс CsvActionResult
public class CsvActionResult : ActionResult
{
public IEnumerable ModelListing { get; set; }
public CsvActionResult(IEnumerable modelListing)
{
ModelListing = modelListing;
}
public override void ExecuteResult(ControllerContext context)
{
byte[] data = new CsvFileCreator().AsBytes(ModelListing);
var fileResult = new FileContentResult(data, "text/csv")
{
FileDownloadName = "CsvFile.csv";
};
fileResult.ExecuteResult(context);
}
}
public class CsvFileCreator
{
public byte[] AsBytes(IEnumerable modelList)
{
StringBuilder sb = new StringBuilder();
BuildHeaders(modelList, sb);
BuildRows(modelList, sb);
return sb.AsBytes();
}
private void BuildHeaders(IEnumerable modelList, StringBuilder sb)
{
foreach (PropertyInfo property in
modelList.GetType().GetElementType().GetProperties())
{
sb.AppendFormat("{0},", property.Name);
}
sb.NewLine();
}
private void BuildRows(IEnumerable modelList, StringBuilder sb)
{
foreach (object modelItem in modelList)
{
BuildRowData(modelList, modelItem, sb);
sb.NewLine();
}
}
private void BuildRowData(IEnumerable modelList, object modelItem, StringBuilder sb)
{
foreach (PropertyInfo info in
modelList.GetType().GetElementType().GetProperties())
{
object value = info.GetValue(modelItem, new object[0]);
sb.AppendFormat("{0},", value);
}
}
}
Строка 3: Хранит данные, которые необходимо отобразить
Строки 4-7: Принимает данные, которые необходимо отобразить
Строки 8-16: Создает выходной результат
Строка 25: Преобразовывает данные в массив байтов
Строка 28: Создает строку заголовка для CSV файла
Строки 37, 45: Создает строки CSV файла
В листинге 16-2 продемонстрировано, как обращение к классу CsvFileCreator
было перенесено в пользовательский результат действия с названием CsvActionResult
. Этот результат действия в дальнейшем отвечает за создание экземпляров и выполнение CsvFileCreator
, а также за установку соответствующего типа содержимого для файла, которое отправляется в пользовательский веб-браузер.
В следующем листинге показано, как почистить действие ExportUsers
таким образом, чтобы в результате перенести логику создания CSV файла в результат действия CsvActionResult
.
Листинг 16-3: Упрощенный метод действия, использующий CsvActionResult
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Export()
{
return View();
}
public ActionResult ExportUsers()
{
IEnumerable<User> model = UserRepository.GetUsers();
return new CsvActionResult(model);
}
}
Строка 7: Страница, содержащая ссылку для скачивания
Строка 11: Действие, которое отправляет CSV файл
Мы поняли, что большинство разработчиков сначала будут склоняться к тому, чтобы поместить такую логику в действие, что означает, что метод действия будет сложно протестировать, и что он будет содержать логику, которая может быть продублирована в других методах действий приложения. Дублирование кода – это то, что вам хотелось бы сократить так, чтобы было проще поддерживать ваш код.
Теперь код метода действия для отображения CsvActionResult
почищен и легок для понимания, а простое действие абстрагирования логики и помещения ее в результат действия предоставляет нам возможность некоторого повторного использования. На данный момент добавление в приложение еще нескольких экспортов CSV является достаточно тривиальным, потому что логика находится в результате действия.
Использование результатов действий для абстрагирования трудно тестируемых зависимостей
Еще одним отличным применением результатов действий является абстрагирование зависимостей, которые сложно тестировать. Несмотря на то, что MVC Framework при использовании фреймворка и создании контроллеров предоставляет вам огромные возможности управления, все еще существуют некоторые возможности ASP.NET, которые трудно смоделировать в тесте. Убрав код, который сложно тестировать, из действия и поместив его в метод Execute
результата действия, вы убедитесь в том, что стало значительно легче выполнять модульное тестирование действий. Все это потому, что при модульном тестировании действия вы утверждаете тип результата действия, который возвращает действие, и состояние результата действия. Метод Execute
результата действия не выполняется как часть модульного теста.
Приведенный ниже листинг демонстрирует LogoutActionResult
, который инкапсулирует трудно тестируемый метод FormsAuthentication.SignOut
.
Листинг 16-4: Перемещение трудно тестируемого кода в ActionResult
public class LogoutActionResult : ActionResult
{
public RedirectToRouteResult ActionAfterLogout
{
get;
set;
}
public LogoutActionResult(RedirectToRouteResult actionAfterLogout)
{
ActionAfterLogout = actionAfterLogout
}
public override void ExecuteResult(ControllerContext context)
{
FormsAuthentication.SignOut();
ActionAfterLogout.ExecuteResult(context);
}
}
Строка 14: SignOut является трудно тестируемым
Строка 15: Выполняется результат
ActionAfterLogout
В листинге 16-4 демонстрируется, как перемещение вызова FormsAuthentication.SignOut()
из действия в результат действия абстрагирует эту строку кода и исключает выполнение его в рамках метода действия. Данная возможность позволяет действию возвращать LogoutActionResult
, как это показано в листинге 16-5, а при тестировании этого метода не приходится иметь дело с вызовами класса FormsAuthentication
. Этот тест может только утверждать тот факт, что действие возвращает LogoutActionResult
. Помимо этого тест может утверждать значения в RedirectToRouteResult
, чтобы убедиться в том, что действие корректно настраивает перенаправление.
Листинг 16-5: Метод действия, использующий LogoutActionResult
public ActionResult Logout()
{
var redirect = RedirectToAction("Index", "Home");
return new LogoutActionResult(redirect);
}
Строка 4: Тестируемый метод действия Logout
В листинге 16-5 показано, что метод действия Logout
возвращает новый метод LogoutActionResult
. Параметром конструктора LogoutActionResult
является результат RedirectToAction
, который будет переправлять веб-браузер к действию Index
в HomeController
.