Главная страница   /   16.5. Использование результатов действий (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

16.5. Использование результатов действий

Пользовательские результаты действий могут использоваться для удаления кода, продублированного в рамках методов, и для извлечения зависимостей, которые могут усложнить тестирование действия. Хороший способ применения пользовательского результата действия – формирование функциональности, за исключением готовых к применению (так называемой "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.