Category Archives: ASP .NET

ASP.NET 5.0 (MVC 6.0)–plik project.json oraz struktura projektu

O nowościach w MVC 6.0 pisałem już np. tutaj http://www.pzielinski.com/index.php?s=MVC+6

Moim zdaniem, największe zmiany jednak mają miejsce w infrastrukturze i architekturze wewnętrznej ASP.NET. Jedną z tych zmian jest plik project.json, który definiuje wszelkie referencje w projekcie. Tworząc nowy projekt, project.json będzie wyglądać następująco:

{ "webroot": "wwwroot", "userSecretsId": "aspnet5-WebApplication3-157b1a61-1b29-4343-8796-15c96e7e7daa", "version": "1.0.0-*", "dependencies": { "EntityFramework.SqlServer": "7.0.0-beta4", "EntityFramework.Commands": "7.0.0-beta4", "Microsoft.AspNet.Mvc": "6.0.0-beta4", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta4", "Microsoft.AspNet.Authentication.Cookies": "1.0.0-beta4", "Microsoft.AspNet.Authentication.Facebook": "1.0.0-beta4", "Microsoft.AspNet.Authentication.Google": "1.0.0-beta4", "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-beta4", "Microsoft.AspNet.Authentication.Twitter": "1.0.0-beta4", "Microsoft.AspNet.Diagnostics": "1.0.0-beta4", "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta4", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta4", "Microsoft.AspNet.Server.IIS": "1.0.0-beta4", "Microsoft.AspNet.Server.WebListener": "1.0.0-beta4", "Microsoft.AspNet.StaticFiles": "1.0.0-beta4", "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta4", "Microsoft.Framework.ConfigurationModel.Json": "1.0.0-beta4", "Microsoft.Framework.ConfigurationModel.UserSecrets": "1.0.0-beta4", "Microsoft.Framework.CodeGenerators.Mvc": "1.0.0-beta4", "Microsoft.Framework.Logging": "1.0.0-beta4", "Microsoft.Framework.Logging.Console": "1.0.0-beta4", "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta4" }, "commands": { "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000", "gen": "Microsoft.Framework.CodeGeneration", "ef": "EntityFramework.Commands" }, "frameworks": { "dnx451": { }, "dnxcore50": { } }, "exclude": [ "wwwroot", "node_modules", "bower_components" ], "publishExclude": [ "node_modules", "bower_components", "**.xproj", "**.user", "**.vspscc" ], "scripts": { "postrestore": [ "npm install", "bower install" ], "prepare": [ "gulp copy" ] } }

Widzimy, że największą sekcją są zależności. Nie ma pliku package.config, który wcześniej zawierał referencje do pakietów NuGet. Jeśli chcemy dodać referencje wystarczy, że dodamy ją bezpośrednio do project.json:

"dependencies": { "NUnit": "3.0.0-alpha", ... }

Po edycji pliku, pakiet zostanie zainstalowany i nie musimy nic więcej robić:

image

Visual Studio zawsze synchronizuje stan projektu z project.json. Wystarczy więc dodać, zmodyfikować lub usunąć referencję w project.json, a reszta zostanie wykonana przez VS. Oczywiście możemy skorzystać ze standardowych komend instalacji NuGet(install-package), ale bardzo często edycja project.json jest szybsza, zwłaszcza ze wsparciem Visual Studio. Wpisując pierwsze znaki, VisualStudio podpowie nam listą dopasowanych bibliotek:

image

Analogicznie sprawa wygląda z wersją:

image

Project.json zastępuje tak naprawdę web.config. Na przykład wersję frameworku określa się za pomocą:

"frameworks": { "dnx451": { }, "dnxcore50": { } },

Zaglądając do folderu, w którym znajduje się aplikacja, przekonamy się o kilku innych drobiazgach:

  1. Brak global.asax.
  2. Brak pliku projektu. Co prawda istnieje xproj, ale to zupełnie co innego. Otwierając xproj w notatniku zobaczymy, że nie ma tam referencji czy struktury projektu. Wszelkie referencje znajdują się w opisanym wyżej project.json.
  3. Brak assembly.cs
  4. Brak package.config
  5. Brak web.config

Wszystko po to, aby ułatwić hostowanie aplikacji w innych środowiskach niż IIS. O szczegółach napiszę w osobnym wpisie, ale możliwy jest m.in. self-hosting. Project.json (wraz z Startup.cs) zastąpił wszystkie powyższe pliki. Startup.cs jest implementacją specyfikacji OWIN i nowej architektury, ale o tym również innym razem.

Na zakończenie, screenshot z nowej solucji w ASP.NET 5.0:

image

ASP.NET WebAPI a JSONP

W poprzednim wpisie pokazałem jak korzystać z JSONP w JQuery. Wiemy, że usługa musi rozpoznawać parametr callback i zwrócić treść w odpowiedniej formie tzn. “callback(dane)”. Załóżmy, że mamy następujący kontroler:

public class DataController : ApiController { public string[] Get() { return new[] {"Hello", "World", "!!!"}; } }

Domyślnie WebAPI nie wspiera JSONP. Możemy to potwierdzić wysyłając następujące zapytanie:

GET http://localhost:5081/api/data?callback='myfunction' HTTP/1.1 User-Agent: Fiddler Host: localhost:5081

Odpowiedź będzie wyglądać tak:

HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: application/json; charset=utf-8 Expires: -1 Server: Microsoft-IIS/10.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcUGlvdHJcRG93bmxvYWRzXFNwb3RsaWdodERldmVsb3BlclRlc3RcTGltZXJpY2tcV2ViQXBwbGljYXRpb240XFdlYkFwcGxpY2F0aW9uNFxhcGlcZGF0YQ==?= X-Powered-By: ASP.NET Date: Fri, 12 Jun 2015 18:30:34 GMT Content-Length: 23 ["Hello","World","!!!"]

Widzimy, że nasz callback nie został rozpoznany.

Na szczęście możemy skorzystać z gotowego pakietu “WebApiContrib.Formatting.Jsonp”. Najpierw zatem należy zainstalować go za pomocą:

Install-Package WebApiContrib.Formatting.Jsonp

 

W pliku Global.asax.cs następnie wystarczy, że umieścimy:

GlobalConfiguration.Configuration.AddJsonpFormatter();

Instrukcja po prostu dodaje formater JSONP, który rozpozna przychodzące zapytania JSONP i umieści odpowiednio callback. Wysyłając teraz takie same zapytanie, serwis odpowie:

HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: text/javascript; charset=utf-8 Expires: -1 Server: Microsoft-IIS/10.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcUGlvdHJcRG93bmxvYWRzXFNwb3RsaWdodERldmVsb3BlclRlc3RcTGltZXJpY2tcV2ViQXBwbGljYXRpb240XFdlYkFwcGxpY2F0aW9uNFxhcGlcZGF0YQ==?= X-Powered-By: ASP.NET Date: Fri, 12 Jun 2015 18:39:22 GMT Content-Length: 36 myfunction(["Hello","World","!!!"]);

Powyższą odpowiedź można załadować jako skrypt JavaScript i wykonać.  Jeśli mamy tylko zaimplementowaną myfunction, przekazane parametry będą stanowić dane, które chcemy pobrać z serwisu.

Jeśli pominiemy parametr callback, wtedy serwis zwróci standardową odpowiedź czyli czyste dane.

JSONP – wywoływanie zewnętrznych usług z JavaScript

Bardzo często tworzymy osobne usługi, które dostarczają jakieś dane. Pisząc aplikacje ASP.NET Web nierzadko chcemy korzystać z zewnętrznych usług, zamiast hostować dane w tym samym projekcie. Niestety może to spowodować problemy, jeśli chcemy skonsumować usługę w JavaScript, a należy ona do innej domeny.

Załóżmy, że mamy jakąś usługę REST. Dla testów posłużyłem się http://www.mocky.io. Polecam tą stronę, można generować tam własne “mocki”.  Dla tego wpisu stworzyłem mock, który zwraca następującą treść np.:

{Text:Hello world!}

Następnie na stronie, mam następujący kod JavaScript, który po prostu próbuje połączyć się z usługą i odczytać dane:

$.ajax({ url: http://www.mocky.io/v2/55772bf754441acd1b6697d0 }).done(function(data) { $(#testLabel).text(data.Text); });

Szybko przekonamy się, że wykonanie zakończy się błędem:

XMLHttpRequest cannot load http://www.mocky.io/v2/55772bf754441acd1b6697d0. No Access-Control-Allow-Origin header is present on the requested resource. Origin http://localhost:35447 is therefore not allowed access.

Przeglądarka zawsze blokuje wywołania Ajax, które próbują wywołać usługę z innej domeny (w tym przypadku localhost –> mocky.io).

Jeśli usługa zwraca dane JSON, jednym z rozwiązań może być JSONP, czyli “JavaScript Object Notation with Padding”.

Rozwiązanie wykorzystuje lukę w przeglądarkach i jest bardzo często stosowane. JSON jak wiemy, może stanowić poprawny kod JavaScript. Przeglądarki nie dopuszczają wywołań do innych domen, ale dozwolone jest załadowanie kodu, które znajduje się w innej domenie, np. poprzez <script src=’adres do zewnetrznej domeny’./>.  Z tego względu, jeśli potraktujemy zawartość zwróconą przez naszą usługę jako kod, wtedy będziemy w stanie połączyć się z nią.

Wyobraźmy sobie, że usługa zamiast zwracać {“Text”:”Hello world”}, zwraca callback({“Text:”HelloWorld”}). Dla przeglądarki jest to jak najbardziej poprawny kod i można go wykonać.

Technika jest na tyle popularna, że istnieje wiele narzędzi, które wspierają ją. Na przykład, dla powyższego wywołania ajaxowego możemy:

$.ajax({ url: http://www.mocky.io/v2/55772bf754441acd1b6697d0, dataType:jsonp }).done(function(data) { $(#testLabel).text(data.Text); });

Wystarczy dodać dataType:’jsonp’ i zobaczymy na ekranie załadowany tekst. Warto przeanalizować, jakie zapytanie jest wysłane oraz jaka odpowiedź konkretnie przychodzi. Zaglądać do logów zobaczymy, że wysyłamy:

“http://www.mocky.io/v2/55772bf754441acd1b6697d0?callback=jQuery110208627150375396013_1433874507041&_=1433874507042”.

Nie trudno spostrzec, że został dodany parametr callback, wraz z nazwą funkcji. Usługa (w tym przypadku mocky.io) rozpozna parametr callback i zwróci odpowiedź w postaci:

jQuery110208627150375396013_1433874507041({“Text”:”Hello world!”});

Następnie jQuery wykona daną funkcję, która tak naprawdę odczyta parametr wejściowy i przekaże ją do funkcji done. Innymi słowy, kod zwrócony przez usługę zostanie wykonany w formie funkcji callback’a, który wywołuje “done” przekazując dostarczony parametr, który stanowi dane zwrócone przez usługę.

Warto zwrócić uwagę, że usługa musi rozpoznawać parametr callback i zwracać zawartość w formie callback(dane) zamiast po prostu czyste dane. Domyślnie WebAPI nie zrobi tego za nas, ale w przyszłym wpisie pokażę w jaki sposób to osigągnąć. Czyste dane JSON zwracane przez usługi, stanowią poprawny kod JavaScript, ale nic z nimi nie możemy zrobić. Jeśli opleciemy je w funkcję, umożliwi nam to przechwycenie tych danych poprzez implementację danej funkcji i odczytanie parametrów wejściowych.

Visual Studio Web Essentials 2013–nieużywane CSS, inspekcja oraz edycja elementów

Web essentials  jest zestawem narzędzi usprawniającym pracę z aplikacjami webowymi. Wersję 2013 można ściągnąć z stąd:

https://visualstudiogallery.msdn.microsoft.com/56633663-6799-41d7-9df7-0f2a504ca361

Po zainstalowaniu pakietu i odpaleniu aplikacji z Visual Studio, w przeglądarce  na samym dole zobaczymy nowy pasek:

image

Zacznijmy od “inspekcji”. Naciskając na przycisk Inspect, możemy zaznaczyć dowolny fragment strony:

image

Na powyższym screenie zaznaczyłem tekst. Przechodząc teraz do Visual Studio, zostanie zaznaczony widok i powyższy tekst:

image

Bardzo to ułatwia pracę z nowym kodem, gdzie nie wiemy jeszcze jak aplikacja została podzielona i jakie widoki znajdują się. Wystarczy wtedy odpalić stronę i zaznaczyć fragment, który chcemy znaleźć w kodzie.

Analogicznie możemy skorzystać z opcji “Design”. Klikając na ten przycisk, a potem na dowolny fragment strony, będziemy mogli edytować fragment strony:

image

Co ważne, zmiana zostanie odzwierciedlona w pliku źródłowym i przechodząc z powrotem do VS, zobaczymy nową zawartość:

image

Kolejna opcja to “Unused CSS”. Przyciskając go, pojawi się ikonka sugerująca nagrywanie:

image

Wszelkie nieużywane style będą pojawiać się w VS:

image

To oczywiście nie koniec możliwości Web Essentials.  Więcej o pakiecie napiszę w przyszłych postach.

ASP.NET WebApi – testowanie routingu, część II

W poprzednim wpisie zajęliśmy się testowaniem routingu w WebAPI. Napisany kod był dość brzydki i warto go po prostu umieścić  w osobnej klasie,  tak abyśmy mogli z niego korzystać w różnych testach.

Zaznaczam, że wciąż nie jest to kod produkcyjny. Zdecydowanie nie będzie pokrywał wszystkich scenariuszy, dlatego odradzam umieszczenie go w swoim wewnętrznym repozytorium NuGet. Z drugiej jednak strony, nic nie szkodzi na przeszkodzie, aby korzystać z niego konkretnym projekcie i w razie potrzeby naprawić jego usterki.

Na GitHub znajduje się już inna biblioteka WebAPIContrib,  gdzie można skorzystać z bardzo dobrej implementacji:

https://github.com/WebApiContrib/WebAPIContrib/blob/master/src/WebApiContrib.Testing/RouteTestingExtensions.cs

Niestety ma ona referencje do zbyt wielu bibliotek, dlatego zdecydowałem napisać się własny mini-helper, który jest wzorowany na powyższym kodzie. Jeśli ktoś z Was chce użyć kodu w produkcji, zdecydowanie polecam przyjrzenie się WebApiContrig i potraktowanie mojego wpisu bardziej jako przewodnika a nie implementacji z której można korzystać (poniższy kod powstał w 10 minut…).

Tak jak w poprzednim poście, załóżmy, że mamy następujący kontroler:

public class SampleController : ApiController { [Route("site/{siteId}/person/{personid}/profile/words/{searchText}")] public int[] GetData(int siteId, int personId, string searchText) { return Enumerable.Range(1, 10).ToArray(); } }

Zamiast kopiować kod  z poprzedniego wpisu za każdym razem jak chcemy przetestować routing, chcemy użyć po prostu wyrażenia lambda np.:

const string url = "http://www.test.com/site/1/person/3/profile/words/ppp"; RoutingAssert.Verify<SampleController>(url, x => x.GetData(1, 3, "ppp"));

Pierwszy parametr to oczywiście URL a drugi to wyrażenie z którego możemy odczytać następujące informacje:

  1. Typ kontrolera
  2. Nazwę akcji
  3. Typ i wartości przekazanych parametrów.

Nie trudno domyślić się, że największym wyzwaniem będzie odczytanie tych wartości z wyrażenia – reszta pozostanie taka sama jak w poprzednim wpisie.

Zaczniemy zatem od następującej sygnatury:

public class RoutingAssert { public static void Verify<T>(string url, Expression<Action<T>> mapping) { } }

Z poprzedniego wpisu pamiętamy, że potrzebujemy zainicjalizować HttpConfiguration:

private static HttpConfiguration InitConfig() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); return config; }

W tej chwili, inicjalizujemy wyłącznie routing za pomocą atrybutów. 

Kolejny etap to stworzenie HTTPRequest:

private static HttpRequestMessage InitRequest(string url, HttpConfiguration config) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; return request; }

Następnie za pomocą selektora, możemy uzyskać deskryptor kontrolera:

private HttpControllerDescriptor GetControllerDescriptor(HttpConfiguration config, HttpRequestMessage request) { IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpControllerDescriptor controllerDescriptor = controllerSelector.SelectController(request); return controllerDescriptor; }

Chcemy również weryfikować akcje, zatem potrzebujemy ActionDescriptor:

private static HttpActionDescriptor GetActionDescriptor(HttpConfiguration config, HttpControllerDescriptor controllerDescriptor, HttpRequestMessage request) { var routeData = (IHttpRouteData)request.Properties[HttpPropertyKeys.HttpRouteDataKey]; HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request); controllerContext.ControllerDescriptor = controllerDescriptor; IHttpActionSelector actionSelector = new ApiControllerActionSelector(); HttpActionDescriptor actionDescriptor = actionSelector.SelectAction(controllerContext); return actionDescriptor; }

Korzystając z powyższych metod pomocniczych, główna metoda Verify aktualnie wygląda następująco:

public static void Verify<T>(string url, Expression<Action<T>> mapping) { HttpConfiguration config = InitConfig(); HttpRequestMessage request = InitRequest(url, config); HttpControllerDescriptor controllerDescriptor = GetControllerDescriptor(config, request); HttpActionDescriptor actionDescriptor = GetActionDescriptor(config, controllerDescriptor, request); }

Asercja kontrolera jest bardzo prosta i wystarczy:

Assert.That(controllerDescriptor.ControllerType, Is.EqualTo(typeof(T)));

Największe wyzwanie to oczywiście akcja, wraz z parametrami. Z tego względu warto stworzyć kolejną metodę pomocniczą o następującej sygnaturze:

private static void AssertAction<T>(Expression<Action<T>> mapping, HttpActionDescriptor actionDescriptor,HttpRequestMessage request)

Asercja nazwy akcji również jest dość prosta:

var expectedMethod = (MethodCallExpression)mapping.Body; Assert.That(actionDescriptor.ActionName,Is.EqualTo(expectedMethod.Method.Name));

Najwięcej wysiłku musimy włożyć w wyodrębnienie argumentów z wyrażenia. Przede wszystkim potrzebujemy:

HttpParameterDescriptor[] actualParameters = actionDescriptor.GetParameters().ToArray(); var httpRouteData = (IHttpRouteData[]) routeData.Values.First().Value;

Pierwsza kolekcja to lista deskryptorów dla parametrów. Tak samo jak ControllerDescriptor oraz ActionDescriptor, opisują one po prostu typy i nazwy przekazanych argumentów. RouteData pochodzi z URL i zawiera faktyczne wartości, które zostały przekazane. Za pomocą RouteData oraz ActualParameters będziemy w stanie odczytać przekazane parametry.

Spójrzmy teraz na najważniejszą metodę, AssertParameters:

private static void AssertParameters(IHttpRouteData routeData, HttpParameterDescriptor[] actualParameters, MethodCallExpression expectedMethod) { for (int i = 0; i < expectedMethod.Arguments.Count; i++) { var expectedValue = ((ConstantExpression)expectedMethod.Arguments[i]).Value; var actualValue = routeData.Values[actualParameters[i].ParameterName]; Assert.That(actualValue, Is.EqualTo(expectedValue.ToString())); } }

Zmienna expectedValue pochodzi z przekazanego wyrażenia (lambda), a actualValue z wspomnianego właśnie RouteData. Kod jest bardzo uproszczony i nie przewiduje np. DateTime czy złożonych parametrów. Podsumowując, cały helper wygląda następująco:

public class RoutingAssert { public static void Verify<T>(string url, Expression<Action<T>> mapping) { HttpConfiguration config = InitConfig(); HttpRequestMessage request = InitRequest(url, config); HttpControllerDescriptor controllerDescriptor = GetControllerDescriptor(config, request); HttpActionDescriptor actionDescriptor = GetActionDescriptor(config, controllerDescriptor, request); Assert.That(controllerDescriptor.ControllerType, Is.EqualTo(typeof(T))); AssertAction(mapping,actionDescriptor,request); } private static void AssertAction(Expression<Action<T>> mapping, HttpActionDescriptor actionDescriptor,HttpRequestMessage request) { var routeData = GetRouteDataFromRequest(request); var expectedMethod = (MethodCallExpression)mapping.Body; Assert.That(actionDescriptor.ActionName,Is.EqualTo(expectedMethod.Method.Name)); HttpParameterDescriptor[] actualParameters = actionDescriptor.GetParameters().ToArray(); var httpRouteData = (IHttpRouteData[]) routeData.Values.First().Value; AssertParameters(httpRouteData[0], actualParameters, expectedMethod); } private static HttpActionDescriptor GetActionDescriptor(HttpConfiguration config, HttpControllerDescriptor controllerDescriptor, HttpRequestMessage request) { var routeData = GetRouteDataFromRequest(request); HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request); controllerContext.ControllerDescriptor = controllerDescriptor; IHttpActionSelector actionSelector = new ApiControllerActionSelector(); HttpActionDescriptor actionDescriptor = actionSelector.SelectAction(controllerContext); return actionDescriptor; } private static IHttpRouteData GetRouteDataFromRequest(HttpRequestMessage request) { return (IHttpRouteData)request.Properties[HttpPropertyKeys.HttpRouteDataKey]; } private static HttpControllerDescriptor GetControllerDescriptor(HttpConfiguration config, HttpRequestMessage request) { IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpControllerDescriptor controllerDescriptor = controllerSelector.SelectController(request); return controllerDescriptor; } private static HttpRequestMessage InitRequest(string url, HttpConfiguration config) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; return request; } private static HttpConfiguration InitConfig() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); return config; } private static void AssertParameters(IHttpRouteData routeData, HttpParameterDescriptor[] actualParameters, MethodCallExpression expectedMethod) { for (int i = 0; i < expectedMethod.Arguments.Count; i++) { var expectedValue = ((ConstantExpression)expectedMethod.Arguments[i]).Value; var actualValue = routeData.Values[actualParameters[i].ParameterName]; Assert.That(actualValue, Is.EqualTo(expectedValue.ToString())); } } }

Jeśli metoda przyjmuje jako parametr wyrażenia lambda lub DateTime powyższy kod nie zadziała. Podobnie jest on ograniczony wyłącznie do HTTP GET. Myślę jednak, że taka próbka daje wystarczająco dużo informacji, aby rozszerzyć helper już we własnym zakresie.

ASP.NET WebAPI – testowanie routingu

Szczególnie w przypadku WebAPI, routing może być dość skomplikowany. W Nancy bardzo łatwo przetestować mapowanie między URL a zaimplementowaną logiką. W WebAPI moim zdaniem jest to dużo mniej wygodnie, ale wciąż powinniśmy zadbać o to, aby mieć zautomatyzowane testy dla routing’u.

Załóżmy, że mamy kontroler z niestandardowym routingiem:

public class SampleController : ApiController { [Route("site/{siteId}/person/{personid}/profile/words/{searchText}")] public int[] GetData(int siteId, int personId, string searchText) { return Enumerable.Range(1, 10).ToArray(); } }

Przykładowy, dopasowany URL to “http://localhost:35447/site/1/person/3/profile/words/pi”.

Możemy mieć przetestowaną całą logikę, zwracane kody HTTP, ale i tak bez testów routingu ryzykujemy bardzo dużo.

Zacznijmy od testu kontrolera. Chcemy upewnić się, że wywołanie powyższego linka, spowoduje zmapowanie do SampleController:

[TestFixture] public class RoutingTests { const string Url = "http://www.test.com/site/1/person/3/profile/words/ppp"; [SetUp] public void Setup() { } [Test] public void VerifyController() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; HttpControllerDescriptor descriptor = controllerSelector.SelectController(request); Assert.That(descriptor.ControllerType, Is.EqualTo(typeof (SampleController))); } }

Najpierw tworzymy instancję HttpConfiguration oraz wywołujemy MapHttpAttributeRoutes, ponieważ w powyższym przykładzie, użyliśmy atrybutów do konfiguracji routingu.

Następnie musimy zainicjalizować IHttpControllerSelector – klasę odpowiedzialną za wybranie kontrolera na podstawie URL.

W teście, korzystamy z HttpRequestMessage. Niezbędne jest zatem przekazanie danych routingu za pomocą:

var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;

Przejdźmy teraz do rozszerzenia powyższego testu, tak aby zawierał on również akcję:

[Test] public void VeryfiControllerAndAction() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; HttpControllerDescriptor controllerDescriptor = controllerSelector.SelectController(request); HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request); controllerContext.ControllerDescriptor = controllerDescriptor; IHttpActionSelector actionSelector = new ApiControllerActionSelector(); HttpActionDescriptor actionDescriptor = actionSelector.SelectAction(controllerContext); Assert.That(actionDescriptor.ActionName, Is.EqualTo("GetData")); Assert.That(controllerDescriptor.ControllerType, Is.EqualTo(typeof(SampleController))); }

Widzimy, że sprawa trochę się skomplikowała, ale zasada jest analogiczna – potrzebujemy dodatkowo dostępu do IHttpActionSelector.

Powyższy kod jest zdecydowanie zły i brzydki. W następnym w poście napiszemy wrapper, który umożliwi nam korzystanie z tego w produkcyjnym kodzie. Ponadto, to nie jest jedyny sposób testowania rountingu w ASP.NET i zajmiemy się  również innymi podejściami.

ASP.NET MVC 6.0 – ViewComponents

W ASP.NET MVC 6.0 (ASP.NET 5.0) usunięto metodę Html.Action. Służyła ona głównie do generowania widoków częściowych. Na przykład, jeśli jakąś funkcjonalność mieliśmy na każdej stronie, wtedy warto było stworzyć child-action i renderować osobno każdą część. Tworzyliśmy wtedy osobny widok i akcję  w kontrolerze. Główny widok wywoływał daną akcję (child-action) na kontrolerze i renderował wskazaną część. Więcej o ChildAction można poczytać tutaj.

Dlaczego zatem Html.Action został usunięty? Zdecydowano, że  stare rozwiązanie nie było zbyt intuicyjne… Osobiście nie miałem z nim większych problemów, aczkolwiek ViewComponents jest również dobrym podejściem.

Załóżmy, że chcemy wyświetlać baner na każdej podstronie. Dla uproszczenia, nasz baner będzie po prostu zawierał aktualny czas:

public class BannerViewComponent:ViewComponent { public IViewComponentResult Invoke(int itemsCount) { DateTime[] data = new DateTime[itemsCount]; for (int i = 0; i < itemsCount; i++) data[i] = DateTime.Now; return View(data); } }

Zwracamy zawsze IViewComponentResult i metoda powinna nazywać się Invoke.  Z kolei typ i liczba parametrów są dowolne. W naszym przypadku przekazywany jest parametr itemsCount. Mamy tutaj pełną dowolność. Kolejny etap to stworzenie widoku, wyświetlającego przekazaną listę DateTime:

@model System.DateTime[] <h3>Sample</h3> @foreach (var dateTime in Model) { <p>@dateTime</p> }

W widoku nie ma nic nadzwyczajnego. Czysty szablon razor.  Należy jednak pamiętać, aby nazwać go Default.cshtml i umieścić w folderze Views/{nazwa kontrolera}/Components/Banner.

W głównym widoku z kolei wywołujemy po prostu:

@Component.Invoke("Banner", 5);

Drugi parametr to ItemsCount, który służy nam jako parametr wejściowy do ViewComponent. Myślę, że kod wygląda bardziej przejrzyście niż w przypadku Action, ale nie stanowi dla mnie to jakieś wielkiej różnicy. Intencja jest bardziej jasna z ViewComponent, ale moim zdaniem usunięcie starej metody spowoduje więcej problemów niż korzyści.

ASP.NET MVC 6 Tag Helpers

Tag Helpers mają na celu ułatwić tworzenie widoków. Cel jest taki, aby widok jak najbardziej przypominał czysty plik HTML. Pierwszym etapem było wprowadzenie Razor, a teraz w MVC 6 mamy tzw. Tag Helper. W celu przetestowania tego samemu, należy zainstalować najpierw Visual Studio 2015.

Zacznijmy od przykładu. Klasyczny sposób na definiowanie linku to:

@Html.ActionLink("Jakis tekst", "About","Home",null,new { @class="styles1"})

Jeśli preferujemy składnię bardziej zbliżoną do HTML można byłoby także:

<a href="@Url.Action("About","Home")" cl class="styles1">Jakis tekst</a>

Oba rozwiązania nie są jednak idealne. Pierwsze kompletnie nie przypomina HTML, a drugie jest dość toporne. Używanie Url.Action jako parametr href jest mało wygodne.

Dzięki AnchorTagHelper możemy teraz:

<a asp-controller="Home" asp-action="About" class="styles1">Jakis tekst</a>

Oprócz tego, istnieje wiele innych helper’ow, np. InputTagHelper:

<input asp-for="StartDate" asp-format="{0:yyyy-MM-dd}" />

InputTagHelper ma dwa atrybuty, asp-for oraz asp-format. Pierwszy służy do określenia pola w modelu, a drugi formatu, w którym powinna wyświetlić się wartość. Klasyczny sposób wyglądałby następująco:

@Html.TextBoxFor(model => model.StartDate)

Do dyspozycji mamy również LabelTagHelper:

<label asp-for="FirstName" />

Nie chcę pokazywać przykładów dla każdego helpera bo wyglądają one analogicznie do siebie. Dla zainteresowanych wspomnę tylko, że mamy jeszcze SelectTagHelper, TextAreaTagHelper, ValidationMessageTagHelper, ValidationSummaryTagHelper oraz FormTagHelper.

Na zakończenie formularz:

<form asp-action="Submit" asp-controller="Home" asp-anti-forgery="true" method="post"> <div> <label asp-for="FirstName" /> <input asp-for="FirstName" type="text" /> <span class="validation" asp-validation-for="FirstName" /> </div> <strong>Validation Errors</strong> <div class="validation" asp-validation-summary="ModelOnly" /> </form>

Moim zdaniem ładniej to wygląda niż mieszanie C# z HTML, jak to było we wcześniejszych wersjach.

REST API: Dokumentacja w ASP.NET Web API

Jakiś czas temu, pisałem o Swagger, jako sposobie na dokumentacje REST API. Dzisiaj chciałbym pokazać kolejny mechanizm na generowanie dokumentacji, tym razem napisany przez Microsoft i dostępny od razu w ASP.NET. Od kilku lat jest on już dostępny bez żadnych dodatkowych instalacji.

Jeśli uruchomimy przykładową aplikację WebAPI, zobaczymy w prawym górnym rogu link do API:

image

Po kliknięciu w link API, zobaczymy wygenerowaną dokumentację:

image

Analogicznie do Swagger, możemy kliknąć na danej operacji i zobaczyć szczegóły:

image

UI możemy modyfikować. Jeśli przełączymy się do solucji, to zobaczymy, że całość jest umieszczona w Areas:

image

Najlepsze w tej dokumentacji jest to, że jest generowana na podstawie komentarzy. Nie musimy zatem edytować żadnych zewnętrznych plików JSON a dokumentacja sama jest bardzo blisko kodu (w formie komentarzy).

Przejdźmy zatem do kontrolera i dodajmy jakiś komentarz:

// GET api/Account/ManageInfo?returnUrl=%2F&generateState=true /// <summary> /// Przykladowy komentarz /// </summary> /// <param name="returnUrl">Parameter 1 blabla</param> /// <param name="generateState">Paramter 2 blablabla</param> /// <returns></returns> [Route("ManageInfo")] public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false)

Jeśli skompilujemy teraz kod i przejdziemy do dokumentacji niestety nic nie zobaczymy. Domyślnie nie są analizowane komentarze XML. Musimy przejść do pliku HelpPageConfig i odkomentować następującą linie kodu:

config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));

Następnie otwieramy właściwości projektu i w zakładce Build włączamy generowanie komentarzy XML do App_Data/XmlDocument.xml:

image

Po odpaleniu strony zobaczymy komentarze:

image

Jeśli kogoś interesuje jak działa powyższy mechanizm generowania dokumentacji zachęcam do poczytania o klasie ApiExplorer.

W każdym razie, dzięki temu mechanizmowi (ASP.NET HelpPages) nie rozsynchronizujemy pliku dokumentacji jak to możliwe było w przypadku Swagger.

Do Swagger jeszcze chcę powrócić w przyszłym poście, ponieważ istnieje wsparcie dla ASP.NET WebAPI i również większość rzeczy może być automatycznie generowane.

REST Batching: Ograniczanie liczby zapytań

Kilka wpisów wcześniej zacząłem tematykę micro-serwisów oraz wzorca bramki. Jednym z wyzwań podczas rozłupywania monolitu jest zbyt wysoka liczba zapytań do innych serwisów, co powoduje utratę wydajności. 

Jeśli w monolicie była klasa np. CustomersRepository to teraz będzie to kompletnie nowa usługa. Wysłanie wiadomości do takiej usługo odbywa się przez jakiś protokół – w przypadku REST zwykle jest to HTTP. W monolicie nie było ważne to, że wywołaliśmy np. GetCustomerById(1), potem GetCustomerById(2) itp. Mam na myśli, że wywołania do repozytorium były bardzo tanie ponieważ odbywały się w pamięci, w tym samym procesie.

W przypadku HTTP, wiąże to się z ogromnym obciążeniem ze względu np. na potrzebę wysyłania HTTP header za każdym razem. Dlaczego więc nie wysyłać kilku zapytań w jednym pakiecie?

Taki mechanizm nazywa się po prostu batching. Załóżmy, że mamy następujący kontroler REST API:

public class CustomersController : ApiController { public int GetCustomerById(int id) { Random random = new Random(id); return random.Next(100); } }

Jeśli chcemy dane klientów o identyfikatorach 2 i 3, możemy w batchu wysłać je w następujący sposób:

POST http://piotr-pc:8289/api/$batch HTTP/1.1 Content-Type: multipart/mixed; boundary="8530da6b-1778-487a-a058-e78640a096e0" Host: piotr-pc:8289 Content-Length: 336 Expect: 100-continue Connection: Keep-Alive --8530da6b-1778-487a-a058-e78640a096e0 Content-Type: application/http; msgtype=request GET /api/customers/1 HTTP/1.1 Host: piotr-pc:8289 --8530da6b-1778-487a-a058-e78640a096e0 Content-Type: application/http; msgtype=request GET /api/customers/2 HTTP/1.1 Host: piotr-pc:8289 --8530da6b-1778-487a-a058-e78640a096e0--

Widzimy, że mamy jeden nagłówek, a potem w HTTP body przekazujemy konkretne zapytania, w tym przypadku /api/customers/1 oraz /api/customers/2.

Odpowiedz z kolei również przyjdzie w jednej paczce i wygląda następująco:

HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Length: 366 Content-Type: multipart/mixed; boundary="996affb8-9317-4fbd-8899-8cce1223b023" Expires: -1 Server: Microsoft-IIS/8.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcUGlvdHJcRG93bmxvYWRzXFNwb3RsaWdodERldmVsb3BlclRlc3RcTGltZXJpY2tcV2ViQXBwbGljYXRpb24xXFdlYkFwcGxpY2F0aW9uMVxhcGlcJGJhdGNo?= X-Powered-By: ASP.NET Date: Sun, 12 Apr 2015 19:24:25 GMT --996affb8-9317-4fbd-8899-8cce1223b023 Content-Type: application/http; msgtype=response HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 24 --996affb8-9317-4fbd-8899-8cce1223b023 Content-Type: application/http; msgtype=response HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 77 --996affb8-9317-4fbd-8899-8cce1223b023--

W tym przypadku są to liczby 24 i 77.

ASP.NET MVC wspiera powyższy mechanizm i wystarczy w pliku konfiguracyjnym określić routing:

config.Routes.MapHttpBatchRoute( routeName: "WebApiBatch", routeTemplate: "api/$batch", batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer));

I to wszystko! Nie musimy męczyć się z żadną własną implementacją. W przypadku klienta, może to wyglądać następująco:

const string baseAddress = "http://piotr-pc:8289"; HttpClient client = new HttpClient(); var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch") { Content = new MultipartContent() { new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/customers/1")), new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/customers/2")) }}; HttpResponseMessage batchResponse = client.SendAsync(batchRequest).Result; MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result; foreach (var content in streamProvider.Contents) { HttpResponseMessage response = content.ReadAsHttpResponseMessageAsync().Result; Console.WriteLine(response.Content.ReadAsStringAsync().Result); }

Domyślnie każde zapytanie z batch’a jest wykonywane sekwencyjne. W przypadku powyższego scenariusza nie ma to sensu. Nic nie stoi na przeszkodzie, aby jednocześnie, z dwóch różnych wątków pobierać dane. Wystarczy zmienić konfigurację na:

aultHttpBatchHandler(GlobalConfiguration.DefaultServer) { ExecutionOrder = BatchExecutionOrder.NonSequential }; config.Routes.MapHttpBatchRoute( routeName: "WebApiBatch", routeTemplate: "api/$batch", batchHandler: batchHandler);

Jeśli powyższy batching z jakich względów nie odpowiada nam, możemy zawsze napisać własny, na przykład dziedzicząc po  DefaultHttpBatchHandler.