UseRouter() czyli ASP.NET Core bez MVC

Dziś krótki wpis o bardzo przydatnym i dosyć mało znanym ficzerze ASP.NET Core tj. możliwości używania „gołego” routera bez całej otoczki MVC. Zanim przejdę do samego kodu warto jedynie dopowiedzieć co złego jest w klasycznych podejściu z kontrolerami, które wszyscy znamy i lubimy? Otóż…nic! Warto jednak pamiętać, że linijki, które domyślnie znajdują się w Startup.cs tj:

 


services.AddMvc();

//...

app.UseMvc();

 

to coś więcej niż tylko „włączenie kontrolerów”. To cała masa dodatkowo zarejestrowane typów, middlewarów i innych ficzerów (jak np. razor pages), które często są nam najzwyczajniej w świecie niepotrzebne. W tym miejscu warto wspomnieć o bliźniaczej, acz mocno odchudzonej alternatywie tj.:

 

services.AddMvcCore();

 

Ta wersja rejestruje dużo mniej i na pewno warto użyć jej do prostych aplikacji. Jednakże możemy pójść o krok dalej i napisać kod à la Nancy, który całkowicie wyzwoli nas z potrzeby definiowania jakichkolwiek kontrolerów.

 

UseRouter() czyli budujemy własny routing

Zacznijmy od rejestracji samego routera we wspomnianej klasie Startup:

 


public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting();
}

 

Następnie w metodzie Configure możemy zdefiniować endpointy naszej aplikacji używając metody UseRouter. Zobaczmy jak wygląda to dla prostej metody GET:

 


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseRouter(builder => 
        builder.MapGet("/ping", async (request, response, data) =>
        {
            response.StatusCode = 200;
            await response.WriteAsync("pong");
        }));
}

 

Jak widzisz w samej metodzie operujemy na typie budowniczego, który udostępnia metody mapujące ścieżki do zdefiniowanych akcji. W naszym przykładzie taką metodą jest MapGetPierwszym argumentem jest endpoint, a drugim funkcja, która przyjmuje żądanie i odpowiedź HTTP oraz dane ścieżki, a zwraca Task. Mamy zatem dostęp do wszystkiego co mogłoby nas interesować w ramach procesowania żądań HTTP. W przykładzie powyżej jedyne co zrobiłem to ustawiłem jawnie status kod odpowiedzi, a także zapisałem do jej body tekst. Zobaczmy jak to działa:

 

 

Teraz dla odmiany zdefiniujmy endpoint metody POST, która np. odczyta JSONa umieszczonego w body żądania i umieści go w odpowiedzi:

 


app.UseRouter(builder => 
    builder.MapPost("/echo", async (request, response, data) =>
    {
        using (var reader = new StreamReader(request.Body))
        {
            var json = await reader.ReadToEndAsync();
            await response.WriteAsync(json);
        }
    }));

 

A działa to tak:

 

 

Zakładam, że być może w tym miejscu artykułu zastanawiasz się czy przedstawiony ficzer nadaje się do czegoś bardziej zaawansowanego niż tylko operowanie na gołych tekstach. W kontrolerach MVC przyzwyczajeni jesteśmy bowiem do faktu, że np. body czy query string  żądania są automatycznie deserializowane do obiektu C#. Jak sprawa ma się w tym przypadku? Przekonajmy się!

 


public static class Extensions
{
    public static IApplicationBuilder Post<T>(this IApplicationBuilder app, string path, Func<T, HttpResponse, Task> execute)
    {
        app.UseRouter(r =>
            r.MapPost(path, async (request, response, data) =>
            {
                using (var reader = new StreamReader(request.Body))
                {
                    var json = await reader.ReadToEndAsync();
                    var t = JsonConvert.DeserializeObject<T>(json);
                    await execute(t, response);
                }
            }));
        return app;
    }
}

 

Powyżej napisałem prostą metodę rozszerzającą, która opakowuje metodę MapPost. Zwróć uwagę, że ów metoda posiada parametr generyczny, który jest niczym innym jak typem, na który będziemy chcieli zdeserializować body żądania. Użycie metody jest bardzo proste:

 


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Post<User>("/users/name", (user, response) => response.WriteAsync(user.Name));
}

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

 

A tak prezentuje się nasz endpoint w akcji:

 

 

Zobaczmy jeszcze odczytywanie danych ze ścieżki żądania HTTP. W tym celu dodam kolejną metodę rozszerzającą:

 


public static T ReadQuery<T>(this HttpContext context) where T : class
{
    var request = context.Request;
    RouteValueDictionary values = null;
    if (HasRouteData(request))
    {
        values = request.HttpContext.GetRouteData().Values;
    }
    if (HasQueryString(request))
    {
        var queryString = HttpUtility.ParseQueryString(request.HttpContext.Request.QueryString.Value);
        values = values ?? new RouteValueDictionary();
        foreach (var key in queryString.AllKeys)
        {
            values.TryAdd(key, queryString[key]);
        }
    }
    return values is null
        ? JsonConvert.DeserializeObject<T>("{}")
        : JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(values));
}

private static bool HasQueryString(this HttpRequest request)
    => request.Query.Any();

private static bool HasRouteData(this HttpRequest request)
    => request.HttpContext.GetRouteData().Values.Any();

 

Powyższa metoda składa zadany typ generyczny zarówno z query string jak i argumentów ścieżki żądania. Na jej podstawie możemy stworzyć metodę GET, która jak poprzednio „opakuje” router:

 


public static IApplicationBuilder Get<T>(this IApplicationBuilder app, string path, Func<T, HttpResponse, Task> execute) where T : class
{
    app.UseRouter(r =>
        r.MapGet(path, async (request, response, data) =>
        {
            var query = request.HttpContext.ReadQuery<T>();
            execute(query, response);
        }));
    return app;
}

 

Użycie metody znów jest banalne:

 


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Get<User>("/users/age", (user, response) => response.WriteAsync(user.Age.ToString()));
}

 

Rezultat?

 

 

Oczywiście kwestia samej definicji metod rozszerzających zależy wyłącznie od Ciebie. Jeżeli chcesz, możesz zbudować je tak, aby w ogóle ukryć HttpRequest i HttpResponse. To co przedstawiłem w tym wpisie to jedynie jedna z opcji 🙂

 

Na sam koniec chciałbym wrzucić mały anons. Fragmenty przedstawionego kodu są częścią projektu Convey, który tworzę wspólnie z Piotrkiem Gankiewiczem. Idea była bardzo prosta – stworzyć grupę małych paczek, które ułatwiałyby programistom wpinanie do ich aplikacji np. omówionego dziś routera, konfiguracji MongoDB, RabbitMQ, Prometheusa i wielu innych przydatnych narzędzi. Jeżeli śledziłeś nasz projekt DShop, to jest to nic innego niż projekt Common, ale podzielony na dużo mniejsze (i czystsze) moduły 😉 O samym Convey powstanie na pewno osobny wpis, który przedstawi jego możliwości, ale już dziś zapraszam Cię na GitHuba i przejrzenia kodu. Projekt jest we wczesnej rozwoju fazie toteż dokumentacja jeszcze nie istnieje, ale zapewniam że wkrótce się pojawi. Tyle na dziś.

You may also like...