Главная страница   /   13.3. Создание и регистрация простого роута (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

13.3. Создание и регистрация простого роута

Когда у вас есть URL паттерн, вы можете использовать его для определения роута. Роуты находятся в файле RouteConfig.cs, который находится в папке проекта App_Start. Вы можете увидеть исходное содержание, которое Visual Studio определяет для этого файла, в листинге 13-5.

Листинг 13-5: Содержание по умолчанию файла RouteConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
			routes.MapRoute(
			name: "Default",
			url: "{controller}/{action}/{id}",
			defaults: new
			{
				controller = "Home",
				action = "Index",
				id = UrlParameter.Optional
			}
			);
		}
	}
}

Статический метод RegisterRoutes, который определен в файле RouteConfig.cs, вызывается из файла Global.asax.cs, который устанавливает некоторые из основных функциональных возможностей MVC при запуске приложения. Вы можете увидеть содержимое по умолчанию файла Global.asax.cs в листинге 13-6, и мы выделили вызов метода RouteConfig.RegisterRoutes, который сделан из метода Application_Start.

Листинг 13-6: Содержание по умолчанию Global.asax.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class MvcApplication : System.Web.HttpApplication
	{
		protected void Application_Start()
		{
			AreaRegistration.RegisterAllAreas();
			WebApiConfig.Register(GlobalConfiguration.Configuration);
			FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
			RouteConfig.RegisterRoutes(RouteTable.Routes);
			BundleConfig.RegisterBundles(BundleTable.Bundles);
		}
	}
}

Метод Application_Start вызывается базовой платформой ASP.NET, когда MVC приложение запускается в первый раз, что приводит к вызову метода RouteConfig.RegisterRoutes. Параметром этого метода является значение статического свойства RouteTable.Routes, которое является экземпляром класса RouteCollection (мы опишем его далее).

Совет

Другие вызовы, сделанные методом Application_Start, рассматриваются в других главах. Мы описываем метод AreaRegistration.RegisterAllAreas в главе 14, метод WebApiConfig.Register в главе 25, метод FilterConfig.RegisterGlobalFilters в главе 16 и метод BundleConfig.RegisterBundles в главе 24.

В листинге 13-7 показано, как создать роут, используя пример URL паттерна из предыдущего раздела, в методе RegisterRoutes файла RouteConfig.cs (Мы удалили из метода все другие выражения, чтобы можно было сосредоточиться на примере).

Листинг 13-7: Регистрация роута
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler());
			routes.Add("MyRoute", myRoute);
		}
	}
}

Мы создаем новый роут, используя наш URL паттерн как параметр конструктора, который мы выражаем в виде строки. Мы также передаем конструктору экземпляр MvcRouteHandler. Различные ASP.NET технологии предлагают различные классы для адаптации правил маршрутизации, и этот класс мы используем для ASP.NET MVC приложений. Как только мы создали роут, мы добавляем его в объект RouteCollection, используя метод Add, передав ему имя, под которым будет известен роут, и созданный нами роут.

Совет

Именование маршрутов не является обязательным, и есть мнение, что таким образом мы жертвуем чистым разделением понятий, которое в ином случае может предоставить роутинг. Мы не сильно переживаем по поводу именования, но мы объясним, почему это может быть проблемой, в разделе "Создание URL из конкретного роута" далее в этой главе.

Более удобным способом регистрации роутов является использование метода MapRoute, определенного в классе RouteCollection. Листинг 13-8 показывает, как мы можем использовать этот метод, чтобы зарегистрировать роут. Результат этого такой же, как и в предыдущем примере, но синтаксис тут чище.

Листинг 13-8: Регистрация роута при помощи метода MapRoute
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			routes.MapRoute("MyRoute", "{controller}/{action}");
		}
	}
}

Такой подход является чуть более компактным, в основном потому, что мы не должны создавать экземпляр класса MvcRouteHandler. Метод MapRoute предназначен исключительно для использования с MVC приложениями. Приложения ASP.NET Web Forms могут использовать метод MapPageRoute, также определяемый в классе RouteCollection.

Использование простого роута

Вы можете увидеть результат изменений, которые мы сделали в маршрутизации, запустив приложение. Когда браузер пытается перейти к корневому URL приложения, вы увидите сообщение об ошибке, но если перейти к роуту, который соответствует нашему паттерну {controller}/{action}, вы увидите результат, показанный на рисунке 13-3, где проиллюстрирован переход к /Admin/Index.

Рисунок 13-3: Навигация при помощи простого роута

Наш простой роут не говорит MVC фреймворку, как реагировать на запросы для корневого URL, и поддерживает только один очень конкретный URL паттерн. Мы сейчас временно сделали шаг назад от функционала, который Visual Studio добавляет в файл RouteConfig.cs при создании MVC проекта. Мы покажем вам, как создавать более сложные паттерны и роуты, в остальной части этой главы.

Юнит тест: тестирование входящих URL

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

В предыдущих главах мы избегали создания общих вспомогательных методов для совместного использования тестов для того, чтобы каждый юнит тест был самодостаточным. Для этой главы мы используем другой подход. Легче всего сделать тестирование схемы маршрутизации для приложения, когда вы объединяете несколько тестов в одном методе, и это намного проще сделать благодаря некоторым вспомогательным методам.

Для проверки роутов нам нужно использовать mock-технологию для трех классов из MVC фреймворка: HttpRequestBase, HttpContextBase и HttpResponseBase (последний класс требуется для проверки исходящих URL, которые мы рассмотрим в следующей главе). Вместе эти классы воссоздают достаточно инфраструктуры MVC для поддержки системы маршрутизации. Мы добавили новый файл юнит тестов RouteTests.cs в проект модульного тестирования, и нашим первым дополнением является вспомогательный метод, который создает mock-объекты HttpContextBase:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Web;
using System.Web.Routing;
using Moq;
using System.Reflection;
namespace UrlsAndRoutes.Tests
{
	[TestClass]
	public class RouteTests
	{
		private HttpContextBase CreateHttpContext(string targetUrl = null, string httpMethod = "GET")
		{
			// создать mock-запрос
			Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();
			mockRequest.Setup(m => m.AppRelativeCurrentExecutionFilePath)
				.Returns(targetUrl);
			mockRequest.Setup(m => m.HttpMethod).Returns(httpMethod);

			// создать mock-response
			Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
			mockResponse.Setup(m => m.ApplyAppPathModifier(It.IsAny<string>()))
				.Returns<string>(s => s);

			// создать mock-контекст, используя запрос и ответ
			Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
			mockContext.Setup(m => m.Request).Returns(mockRequest.Object);
			mockContext.Setup(m => m.Response).Returns(mockResponse.Object);
			// вернуть mock-контекст
			return mockContext.Object;
		}
	}
}

Здесь все проще, чем кажется. Мы раскрываем URL, который мы хотим проверить, используя свойство AppRelativeCurrentExecutionFilePath класса HttpRequestBase, и раскрываем HttpRequestBase благодаря свойству Request mock-класса HttpContextBase. Наш следующий вспомогательный метод позволяет нам протестировать роут:

...
private void TestRouteMatch(string url, string controller, string action,
	object routeProperties = null, string httpMethod = "GET") 
{
	// Arrange
	RouteCollection routes = new RouteCollection();
	RouteConfig.RegisterRoutes(routes);
	// Act - обрабатывает роут
	RouteData result
		= routes.GetRouteData(CreateHttpContext(url, httpMethod));
	// Assert
	Assert.IsNotNull(result);
	Assert.IsTrue(TestIncomingRouteResult(result, controller,
		action, routeProperties));
}
...

Параметры этого метода позволяют задавать URL для тестирования, ожидаемые значения сегментных переменных controller и action, а также object, который содержит ожидаемые значения для любых дополнительных переменных, которые мы определили. Мы покажем вам, как создавать такие переменные, далее в этой главе. Мы также определили параметр для HTTP метода, которые мы объясним в разделе "Ограничение роутов".

Метод TestRouteMatch полагается на другой метод, TestIncomingRouteResult, чтобы сравнить результаты, полученные от системы маршрутизации, с ожидаемыми значениями сегментных переменных. Этот метод использует .NET рефлексию, поэтому мы можем использовать анонимный тип, чтобы выразить любые дополнительные переменные сегмента. Не волнуйтесь, если этот метод вам непонятен, так как он нужен, чтобы сделать тестирование более удобным, но не является обязательным требованием для понимания MVC. Вот метод TestIncomingRouteResult:

private bool TestIncomingRouteResult(RouteData routeResult,
	string controller, string action, object propertySet = null) 
{
	Func<object, object, bool> valCompare = (v1, v2) => {
		return StringComparer.InvariantCultureIgnoreCase
			.Compare(v1, v2) == 0;
	};

	bool result = valCompare(routeResult.Values["controller"], controller)
		&& valCompare(routeResult.Values["action"], action);
	if (propertySet != null) {
		PropertyInfo[] propInfo = propertySet.GetType().GetProperties();
		foreach (PropertyInfo pi in propInfo) {
			if (!(routeResult.Values.ContainsKey(pi.Name)
				&& valCompare(routeResult.Values[pi.Name],
					pi.GetValue(propertySet, null)))) 
			{
				result = false;
				break;
			}
		}
	}
	return result;
}

Нам также нужен метод, чтобы проверить, что URL не работает. Как вы увидите, это может быть важной частью определения URL схемы.

...
private void TestRouteFail(string url) 
{
	// Arrange
	RouteCollection routes = new RouteCollection();
	RouteConfig.RegisterRoutes(routes);
	// Act - обработка роута
	RouteData result = routes.GetRouteData(CreateHttpContext(url));
	// Assert
	Assert.IsTrue(result == null || result.Route == null);
}
...

TestRouteMatch и TestRouteFail содержат вызов метода Assert, который генерирует исключение, если утверждение не выполняется. Поскольку в C# исключения передаются вверх по стеку вызовов, мы можем создавать простые тестовые методы, которые проверяют набор URL-адресов и получают нужное нам тестовое поведение. Вот тестовый метод, проверяющий роут, который мы определили в листинге 13-8:

...
[TestMethod]
public void TestIncomingRoutes() {
	// проверка на URL, который мы надеемся получить
	TestRouteMatch("~/Admin/Index", "Admin", "Index");
	// проверка на то, что значения были получены из сегментов
	TestRouteMatch("~/One/Two", "One", "Two");
	// гарантия того, что слишком много или слишком мало сегментов не подходят
	TestRouteFail("~/Admin/Index/Segment");
	TestRouteFail("~/Admin");
}
...

Этот тест использует метод TestRouteMatch для проверки URL, который мы ожидаем. Он также проверяет URL в том же формате, чтобы убедиться, что значения controller и action получены надлежащим образом с помощью URL сегментов. Мы также используем метод TestRouteFail, чтобы убедиться, что наше приложение не будет принимать URL, которые имеют другое количество сегментов. При тестировании мы должны поставить перед URL тильду (~), потому что таким образом ASP.NET Framework представляет URL системе маршрутизации.

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