O routingu w ASP.NET MVC pisałem już wielokrotnie. ASP.NET MVC 5 oraz Web API 2 wprowadzają jednak dodatkowy sposób definiowania URL – za pomocą atrybutu. Myślę, że szczególnie przydatne jest to w Web API, gdzie musimy bardzo często wspierać kilka wersji API jednocześnie. Zacznijmy od prostego przykładu:
public class PersonsController : ApiController
{
[Route("api/persons/v1/{id}")]
public string GetPerson(int id)
{
return "test" + id.ToString(CultureInfo.InvariantCulture);
}
}
Atrybut Route definiuje po prostu routing. Bez niego, domyślny URL musiałby być w postaci np. /api/persons/1. Powyższy wzorzec jednak dopasuje linki takie jak /api/v1/persons/5.
Jak widać sposób użycia jest bardzo prosty. Zamiast routing definiować globalnie, definiuje się go dla konkretnej metody (akcji). Ma to wiele zastosowań, np. wspieranie kilku wersji API (o tym w szczegółach kiedyś indziej). W skrócie, dzięki Route można określić, która metoda ma zostać wykonana dla konkretnych wersji API.
Inny przykład to filtrowanie za pomocą różnych kryteriów. Załóżmy, że mamy trzy metody, które filtrują informacje na podstawie daty, id i napisu. Za pomocą Web Api można stworzyć następujące akcje:
public class PersonsController : ApiController
public string GetPersonById(int id)
{
return "GetPersonById=" + id.ToString(CultureInfo.InvariantCulture);
}
public string GetPersonByFirstName(string firstName)
{
return "GetPersonByFirstName=" + firstName;
}
public string GetPersonByDateTime(DateTime dateTime)
{
return "GetPersonByDatetime=" + dateTime.ToString(CultureInfo.InvariantCulture);
}
}
Oczywiście w takiej postaci nie zadziała to ponieważ wysłanie zapytania HTTP GET do /api/persons/ spowoduje dopasowanie trzech metod. Za pomocą routingu łatwo jednak zmienić to do:
public class PersonsController : ApiController
{
[Route("api/persons/{id:int}")]
public string GetPersonById(int id)
{
return "GetPersonById=" + id.ToString(CultureInfo.InvariantCulture);
}
[Route("api/persons/{firstName:alpha}")]
public string GetPersonByFirstName(string firstName)
{
return "GetPersonByFirstName=" + firstName;
}
[Route("api/persons/{dateTime:datetime}")]
public string GetPersonByDateTime(DateTime dateTime)
{
return "GetPersonByDatetime=" + dateTime.ToString(CultureInfo.InvariantCulture);
}
}
Proszę zwrócić uwagę na typ danych, który definiujemy wraz z argumentem. Określamy, że ID musi być typu INT, firstName jest tekstem, a dateTime datą. Istnieje wiele innych ograniczeń takich jak np. bool, decimal, double. Możliwe jest użycie nawet wyrażeń regularnych. Za pomocą ich, możemy określić jakie warunki musi argument spełniać, aby akcja została dopasowana.
Wracając do przykładu. Jeśli użytkownik wywoła /api/persons/5 to oczywiście akcja GetPersonById zostanie dopasowana i wykonana. GetPersonByFirstName zostanie dopasowana do np. /api/persons/piotr. Z kolei “api/persons/11-11-2012” jest przykładem adresu dopasowanego do akcji GetPersonByDateTime.
Oprócz wielkiego zestawu typów argumentów zdefiniowanych w MVC, można samodzielnie napisać własne. Wystarczy zaimplementować interfejs IHttpRouteConstraint:
public interface IHttpRouteConstraint
{
/// <summary>
/// Determines whether this instance equals a specified route.
/// </summary>
///
/// <returns>
/// True if this instance equals a specified route; otherwise, false.
/// </returns>
/// <param name="request">The request.</param><param name="route">The route to compare.</param><param name="parameterName">The name of the parameter.</param><param name="values">A list of parameter values.</param><param name="routeDirection">The route direction.</param>
bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection);
}
Innym ciekawym zastosowaniem routingu jest zwrócenie np. kolekcji dla danej encji. Rozważmy następujący kontroler:
public class PersonsController : ApiController
{
public string GetPersonById(int id)
{
return "GetPersonById=" + id.ToString(CultureInfo.InvariantCulture);
}
[Route("api/persons/{id}/contacts")]
public string[] GetContacts(int id)
{
return new[] {"1", "2", "3"};
}
}
Standardowy URL /api/persons/1 zwróci pojedynczą osobę (GetPersonById). Z kolei /api/persons/5/contacts całą kolekcję.
Bardzo ułatwiającym pracę atrybutem jest RoutePrefix. Zwykle, początek URL w każdym kontrolerze jest taki sam. Zamiast duplikować go dla każdej akcji, możemy określić go raz za pomocą RoutePrefix:
[RoutePrefix("api/persons")]
public class PersonsController : ApiController
{
[Route("{id:int}")]
public string GetPersonById(int id)
{
return "GetPersonById=" + id.ToString(CultureInfo.InvariantCulture);
}
[Route("{firstName:alpha}")]
public string GetPersonByFirstName(string firstName)
{
return "GetPersonByFirstName=" + firstName;
}
[Route("{dateTime:datetime}")]
public string GetPersonByDateTime(DateTime dateTime)
{
return "GetPersonByDatetime=" + dateTime.ToString(CultureInfo.InvariantCulture);
}
}
Z kolei, jeśli jakaś akcja w kontrolerze, który ma RoutePrefix posiada inny prefiks, wtedy można skorzystać z pełnej ścieżki, którą należy zacząć znakiem ~:
[Route("~/api/fullpath/{id:int}")]
public string GetFullPath(int id)
{
return "GetFullPath=" + id.ToString(CultureInfo.InvariantCulture);
}
Atrybut Route pozwala również zdefiniować nazwę routingu oraz porządek, w którym będzie on dopasowywany (przydatne gdy jest kilka routingów z różnym priorytetem):
[Route("~/api/fullpath/{id:int}",Name="nazwa_drogi",Order=3)]
Nazwa z kolei przydaje się, gdy chcemy skorzystać gdzieś w kodzie z metod takich jak Url.Link itp.
Na zakończenie, warto zaznaczyć, żeby routing za pomocą atrybutów działał poprawnie, należy upewnić się, że jest on aktywowany za pomocą:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}