Artykuł

sxc.hu sxc.hu
mar 13 2014
0

Routing na bazie atrybutów w ASP.NET MVC 5

W czystym HTMLu, czy nawet PHP, można stworzyć pojedynczą stronę wykorzystując tylko jeden dokument (odpowiednio HTML, PHP). Podobnie sprawa wygląda również w ASP.NET Web Forms. W tym przypadku mamy co prawda plik aspx oraz odpowiadający mu dokument code-behind, ale są one na tyle silnie ze sobą związane, że funkcjonują jako jeden byt. Stronę ASP.NET Web Forms uruchomimy więc podając po prostu nazwę wybranego pliku aspx.

Zupełnie inaczej sprawy mają się w przypadku ASP.NET MVC, gdzie na jeden pojedynczy link z reguły składa się kilka elementów składowych - kontroler, klasy odpowiedzialne za logikę oraz strona widoku. Takie podejście wymusiło wprowadzenie wirtualnych ścieżek, którymi zarządza system routingu.

Twórcy ASP.NET MVC całkiem sprytnie to wymyślili i jeśli tylko trzymamy się standardowych reguł, to nie powinno być większych problemów z ogarnięciem tego tematu. Życie bywa jednak przewrotne, a aplikacje skomplikowane. Wszystko to powoduje, że reguły routingu w klasycznej postaci szybko stają się zawiłe i nieczytelne.

Jeśli stanąłeś kiedykolwiek przed takim właśnie problemem, to mam dla Ciebie dobrą nowinę, a jest nią routing oparty o atrybuty, wprowadzony w ASP.NET MVC 5.

Attribute routing - o co chodzi?

Attribute routing (bo tak właśnie brzmi oryginalna nazwa tego mechanizmu) wprowadza do odrobinę już skostniałych reguł routingu powiew świeżości. Chyba można śmiało stwierdzić, że od momentu powstania samego routingu nie pojawiła się w nim tak istotna nowość, która nie oszukujmy się, była bardzo wyczekiwana w tym przypadku.

Dotychczas wszystkie reguły znajdowały się w centralnym miejscu. Najpierw był to plik Global.asax, a później zostały one wydelegowane do dokumentu RouteConfig.cs. Najważniejszym elementem tego pliku jest oczywiście metoda RegisterRoutes, która domyślnie wygląda mniej więcej tak:

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 }
	);
}

Nowe podejście pozwala na swego rodzaju decentralizację. Od teraz możemy tworzyć odpowiednie reguły przypisując je bezpośrednio do kontrolerów oraz metod. W tym celu wykorzystywane tytułowe atrybuty, czyli mechanizm dobrze znany w ASP.NET MVC, wykorzystywany choćby do Data annotations.

Dzięki takiemu podejściu możemy w 100% określić sposób wywołania określonego kontrolera i/lub akcji. Niestety wprowadzenie mechanizmu atrybutów dla wybranego elementu, automatycznie wyłącza go z konwencjonalnych reguł routingu. Jest to w pewnym sensie niebezpieczne, ponieważ nowy mechanizm musi zostać wcześniej aktywowany w metodzie RegisterRoutes. Jeśli tego nie uczynimy to możemy doprowadzić do sytuacji, że pewien określony kod będzie w ogóle nieosiągalny.

Attribute routing - aktywacja mechanizmu

Tak jak wspomniałem w poprzednim akapicie, aby skorzystać z nowego mechanizmu musimy go najpierw włączyć. Kod odpowiedzialny za tą operację najlepiej umieścić na początku metody RegisterRoutes:

public static void RegisterRoutes(RouteCollection routes)
{
	routes.MapMvcAttributeRoutes();
	
	// konwencjonalne reguły
}

Przechodząc na nowy mechanizm, warto mimo wszystko pozostawić domyślną regułę, która powinna obsłużyć ewentualnie nieobsłużone przez atrybuty metody. W wielu przypadkach jest to dużo lepsze wyjście niż sztuczne wpisywanie atrybutów przy każdej metodzie/kontrolerze, w momencie gdy normalnie obsłużyła by je domyślna reguła.

Attribute rounting w praktyce

Po aktywacji mechanizmu, możemy przystąpić do pisania reguł w nowy sposób. Jest to ogółem bardzo proste i sprowadza się do zdefiniowania wartości atrybutu Route przy elemencie, dla którego ścieżkę chcemy określić. Dla każdego tego typu atrybutu możemy określić również nazwę, ale większość osób trzyma się konwencji ograniczającej się do podania tylko wspomnianej wyżej reguły określonej w API jako parametr Template. Spójrzcie na poniższy listing kodu (dla uproszczenia pomijam i będę pomijał usingi):

public class HelloController : Controller
{
    [Route("Test")]
    public string Index()
    {
        return "Hello World";
    }

    public string SayAge(int age)
    {
        return "Your age is " + age;
    }
}

Dodaliśmy nowy atrybut do metody Index w kontrolerze Hello. Korzystając ze starego sposobu i domyślnej reguły, moglibyśmy się dostać do tej metody wykorzystując ścieżkę:

http://host/Hello/Index/

Ponieważ jednak postanowiliśmy skorzystać z nowego wynalazku, adres uprościł się do postaci:

http://host/Test/

Jednocześnie tak jak wspominałem wcześniej, automatycznie wyłączyliśmy dostęp starego typu do tego zasobu, co łatwo możecie sprawdzić w trybie debugowania. Nie ruszaliśmy metody About i ta będzie dostępna po staremu:

http://host/Hello/About/

Ale tylko przy założeniu, że nie skasowaliście domyślnej reguły, o czym wspominałem Wam wcześniej w tym tekście;-)

Segmenty i ograniczenia

Routing na bazie atrybutów obsługuje oczywiście segmenty, które dobrze znacie z klasycznych reguł. Nie ma również problemu z umieszczaniem predefiniowanych zmiennych oraz obsługą parametrów wybranej metody:

[Route("{controller}/Age/{age}")]
public string SayAge(int age)
{
    return "Your age is " + age;
}

Tak skonstruowana reguła pozwoli na definiowanie wywołań w postaci:

http://host/Hello/Age/xyz/

Gdzie xyz to wasz wiek, który zostanie przetworzony dalej przez metodę (w tym przypadku po prostu wyświetli się on na ekranie razem z resztą zdefiniowanego wewnątrz stringu). Jak pewnie zauważyliście zastosowaliśmy tutaj:

  • Zmienną mówiącą o użyciu aktualnego kontrolera
  • Zmienioną nazwę metody - zamiast SayAge - Age
  • Wiek w postaci zmiennej age

Niestety nasze rozwiązanie ma pewną wadę. Choć metoda wyraźnie przyjmuje parametr typu int, to takich ograniczeń w żaden sposób nie stawia już nasza reguła routingu. Jeśli w takiej sytuacji użytkownik spróbuje np. takiego wywołania:

http://host/Hello/Age/Zenek/

Na ekranie może pojawić się bardzo brzydki błąd. Na szczęście możemy to szybko i łatwo zmienić, wprowadzając ograniczenie dla typu int do naszego parametru:

[Route("{controller}/Age/{age:int}")]
public string SayAge(int age)
{
    return "Your age is " + age;
}

Dzięki takiemu rozwiązaniu, metoda zostanie wywołana tylko w sytuacji gdy użytkownik rzeczywiście poda wartość o odpowiadającym nam typie. Jeśli tego nie zrobi, to na ekranie pojawi się błąd 404 - chyba że zdefiniujemy inną regułą dla tego typu sytuacji awaryjnych.

Route Prefix

Jeśli chcielibyście by cały kontroler korzystał z określonej nazwy, możecie zdefiniować atrybut typu RoutePrefix, który będzie stanowił prefiks dla wszystkich reguł (metod) wewnątrz tej klasy:

[RoutePrefix("Hello")]
public class HelloController : Controller
{
    [Route("~/Test")]
    public string Index()
    {
        return "Hello World";
    }

    [Route("Age/{age:int}")]
    public string SayAge(int age)
    {
        return "Your age is " + age;
    }
}

Dzięki temu atrybutowi nie będziecie musieli podawać nazwy kontrolera przy każdej z jego metod, co widać przy okazji ścieżki do metody SayAge (jej wywołanie nie zmieniło się). RoutePrefix można oczywiście obejść i jest to bardzo przydatne w sytuacji gdy jakaś konkretna metoda ma być dostępna spod innej ścieżki. W takim celu wystarczyć zastosować znak tyldy, który standardowo odwołuje się do głównego katalogu aplikacji.

Ostatecznie dostęp do obu metod uzyskamy za pomocą następujących ścieżek:

http://host/Test/
http://host/Hello/Age/xyz/

Podsumowanie

Attribute routing daje nowe tchnienie mechanizmowi routingu w ASP.NET MVC. Dzięki temu podejściu łatwiej jest uzyskać konkretny, specyficzny efekt. W moim odczuciu nie warto jednak popadać w hura optymizm, ponieważ starsze rozwiązanie działa bardziej globalnie i w wielu prostych aplikacjach będzie wystarczające. Włączenie mechanizmu atrybutów sprawa zaś, że musimy teraz uważać na każde miejsce aplikacji, by przypadkiem czasem nie zablokować sobie dostęp do określonej funkcjonalności.

Materiały

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus