Roslyn–analiza kodu za pomocą LINQ

Analiza kodu za pomocą LINQ to chyba esencja Roslyn.  Bez Roslyn, kod był dla nas jak zwykły tekst (string) i w przypadku jakiejkolwiek analizy, musieliśmy sami parsować tekst i rozpoznawać odpowiednie fragmenty.

Najpierw tworzymy SyntaxTree za pomocą zwykłego tekstu:

SyntaxTree tree = CSharpSyntaxTree.ParseText(@" public class Class1 { public void TestMethod1() { } } public class Class2 { public void TestMethod2() { } } "); SyntaxNode root = tree.GetRoot();

Metoda DescendantNodes zwraca wszystkich potomków, również tych niebezpośrednich. Jeśli chcemy zwrócić wszystkie klasy wystarczy szukać węzłów typu ClassDecelarionSyntax:

foreach (ClassDeclarationSyntax classDeclarationSyntax in root.DescendantNodes().OfType<ClassDeclarationSyntax>()) { Console.WriteLine(classDeclarationSyntax.Identifier); }

Analogicznie, w celu wyświetlenia metod wystarczy:

foreach(ClassDeclarationSyntax classDeclarationSyntax in root.DescendantNodes().OfType<ClassDeclarationSyntax>()) { Console.WriteLine(classDeclarationSyntax.Identifier); foreach (var method in classDeclarationSyntax.DescendantNodes().OfType<MethodDeclarationSyntax>()) { Console.WriteLine("\t{0}",method.Identifier); } }

Jak wspomniałem, DescendantNodes zwraca wszystkich potomków więc możemy wyświetlić metody bezpośrednio korzystając z korzenia:

foreach (MethodDeclarationSyntax methodDeclarationSyntax in root.DescendantNodes().OfType<MethodDeclarationSyntax>()) { Console.WriteLine(methodDeclarationSyntax.Identifier); }

Jeśli chcemy tylko bezpośrednich potomków (czyli dzieci) wtedy korzystamy z ChildNodes:

foreach (MethodDeclarationSyntax methodDeclarationSyntax in root.ChildNodes().OfType<MethodDeclarationSyntax>()) { Console.WriteLine(methodDeclarationSyntax.Identifier); }

Zmodyfikujmy trochę kod, aby zawierał metodę z parametrami:

SyntaxTree tree = CSharpSyntaxTree.ParseText(@" public class Class1 { public void TestMethod1() { } public void TestMethod1(string a, string b, int c) { } } ");

Zapytanie zwracające wszystkie metody, które zawierają parametry to:

SyntaxNode root = tree.GetRoot(); MethodDeclarationSyntax methodWithPars = root.DescendantNodes().OfType<MethodDeclarationSyntax>().First(n => n.ParameterList.Parameters.Any()); Console.WriteLine(methodWithPars.Identifier);

Wyświetlenie parametrów sprowadza się do prostej pętli:

foreach (var par in methodWithPars.ParameterList.Parameters) { Console.WriteLine(par.ToString()); }

Często potrzebujemy pójść w drugą strony, to znaczy mamy wskaźnik na potomka, ale chcemy znaleźć rodzica. Do dyspozycji mamy metodę Ancestors:

foreach (var par in methodWithPars.ParameterList.Parameters) { string className = par.Ancestors().OfType<ClassDeclarationSyntax>().Single().Identifier.ToString(); Console.WriteLine("{0}:{1}",className,par.ToString()); }

Za pomocą LINQ możemy uzyskać dostęp do każdego fragmentu kodu, nie tylko deklaracji. Możemy np. wylistować IF”Y:

SyntaxTree tree = CSharpSyntaxTree.ParseText(@" public class Class1 { public void TestMethod1() { } public void TestMethod1(string a, string b, int c) { if(c==5 { } } } "); SyntaxNode root = tree.GetRoot(); var ifStatements = root.DescendantNodes().OfType<IfStatementSyntax>(); foreach (var ifStatementSyntax in ifStatements) { Console.WriteLine(ifStatementSyntax.ToString()); }

Analogicznie sprawa wygląda z wywołaniami metod, klauzulami using, switch\case, pętlami, zwracaniem wartości jak i z KAŻDYM elementem kodu. Innymi słowy drzewo jest na tyle szczegółowe, że “bez problemu” możemy napisać konwerter z C# na jakikolwiek inny język np. JavaScript, bez konieczności ręcznego parsowania kodu C#.

Roslyn–Scripting API

W poprzednim wpisie, w dużym skrócie przedstawiłem po co powstał Roslyn i komu może przydać się. W kilku kolejnych postach, zaprezentuję w praktyce jego najważniejsze funkcje.

Zaczynami od scripting API.  APi szczególnie przydatne w przypadku edytorów do gier czy bardziej zaawansowanych reguł biznesowych. Zacznijmy od klasycznego “hello world”:

CSharpScript.Run("System.Console.WriteLine(\"Hello world\")");

Nie trudno domyślić się, że Run wykona kod przekazany jako String – w tym przypadku jest to po prostu wyświetlenie napisu na ekranie.

Zwykle chcemy jednak zmodyfikować jakieś zmienne:

ScriptState result=CSharpScript.Run("int a=5;int b=5; int c=a+b;"); foreach (ScriptVariable scriptVariable in result.Variables) { Console.WriteLine(string.Format("{0}:{1}", scriptVariable.Name, scriptVariable.Value)); }

Pole Variable będzie zawierać wszelkie zmienne stworzone w skrypcie, wraz z ich wartościami. W tym przypadku będzie to “a:5,b:5,c:10”.

Możliwe jest również przekazywanie parametrów do skryptu. Stwórzmy dowolną strukturę:

public class Data { public int Number = 5; }

Następnie wykonanie skryptu będzie wyglądać następująco:

var data = new Data(); CSharpScript.Run("Number=Number*2", data); Console.WriteLine(data.Number);

Zmienna Number zostanie przekazana z wartością 5, a potem w skrypcie zostanie zmieniona na 10 i zwrócona na zewnątrz. W skrócie jest to argument wejściowo-wyjściowy.

Inna przydatna funkcja to Eval. Uruchamia ona po prostu skrypt i zwraca od razu to co znajduje się w nim na wyjściu. Na przykład, poniższy kod zwróci losowo wygenerowaną liczbę:

object result = CSharpScript.Eval("new Random().Next()"); Console.WriteLine(result.ToString());

To samo można oczywiście osiągnąć za pomocą Run, ale Eval w wielu sytuacjach jest bardziej intuicyjny w użyciu. Zarówno do Eval jak i Run można przekazać ScriptOptions. Bardzo często skrypty potrzebują referencji do zewnętrznych bibliotek i wtedy możemy:

ScriptOptions options = ScriptOptions.Default .AddReferences(Assembly.GetAssembly(typeof(Path))) .AddNamespaces("System.IO"); var currentDir = CSharpScript.Eval(@"Path.GetCurrentDirectory()", options);

Bardzo ciekawą opcją jest skompilowanie skryptu i użycie potem go jako zwykłej delegaty:

ScriptState state = CSharpScript.Run("int Add(int x,int y) { return x +y; }"); var add = state.CreateDelegate<Func<int, int,int>>("Add"); var sum = add(5, 10); Console.Write(sum); // 15

Roslyn – wprowadzanie

O projekcie Roslyn od dawna już słychać, ale jeszcze nigdy nie miałem okazji z niego poważnie korzystać, ani wspomnieć o tym na blogu.

Czym jest więc Roslyn? W skrócie pisząc jest to “compiler as service”. Roslyn dostarcza API za pomocą, którego możemy komunikować się kompilatorem.  Możemy zatem pisać programy, które rozpoznają kod (C#) i mogą go dynamicznie wykonywać. Innymi słowy, dzięki Roslyn jesteśmy w stanie załadować kod w formie czystego tekstu i w pamięci analizować go  już jako strukturę danych czyli drzewo. Dzięki temu odczytanie jakiekolwiek elementu kodu (nazwa klasy, warunek, modyfikator) jest bardzo proste i sprowadza się do wyrażenia LINQ na tej strukturze danych.

Załóżmy, że mamy następujący kod:

using System; using System.Collections.Generic; using System.Linq; using System.Security.Policy; using System.Text; using System.Threading.Tasks; namespace ClassLibrary1 { public class Class1 { public void Test(int argument) { if (argument > 10) { } else { throw new Exception(); } } } }

Instalując Roslyn, Visual Studio zostanie zintegrowany również z Roslyn Syntaax Visualizer – proste narzędzie wyświetlające kod w formie drzewa. Dla powyższego kodu, drzewo wygląda następująco:

image

Widzimy, że każdy nawet najmniejszy element posiada własną strukturę typu ClassDeclartion czy UsingDirective. Za pomocą LINQ możemy odczytać każdy szczegół.

Podczas pracy z Roslyn, pojawi się wiele terminów określających powyższe węzły. Pierwszym z nich jest SyntaxTree czyli wspomniane drzewo prezentujące dany fragment kodu (np. plik cs).

SyntaxNode to z kolei węzeł typu ClassDeclaration czy MethodDeclaration. Klasa czyli “class NazwaKlasy” stanowi ClassDeclarion. Analogicznie sygnatura metody to MethodDeclartion.

SyntaxToken to jeszcze mniejszy element drzewa. Technicznie jest to liść czyli węzeł bez dzieci. Jak wspomniałem, ClassDeclartion to np. “class NazwaKlasy {“ z kolei SyntaToken  dla tego węzła to “class”, “NazwaKlasy” oraz “{“. Widzimy, że SyntaToken to najdrobniejszy element i nie da go robić się na mniejsze.

SyntaxTrivia z kolei to najmniej znaczący element i może być nim spacja, koniec linii czy komentarz.

Za pomocą Roslyn, możemy zatem robić wszystko z kodem, co wymagało wcześniej własnego parsera czyli analizowanie kodu, wykonywanie poszczególnych części czy nawet odczytywanie konkretnych projektów w solucji (tzw. Workspaces API). Roslyn Scripting API dostarcza mechanizm  dynamicznego wykonywania kodu. Możemy przekazać po prostu kod jako tekst i zostanie on wykonany.

Nie wszystkim jednak Roslyn przyda się. Oczywiście największą grupą odbiorców będą twórcy narzędzi programistycznych. Liczenie metryk kodu czy analiza jakości kodu (CodeAnalysis, nDepend) zostanie uproszczona. Analogicznie wszelkie generatory kodu będą mogły teraz być tworzone za pomocą API.

Również aplikacje wykorzystujące C# jako język skryptowy staną się dużo łatwiejsze w implementacji. Na przykład twórcy gier komputerowych często wymagają podobnej funkcjonalności w edytorach dla AI.

Moim zdaniem dla większości programistów jest to ciekawostka, która jednak będzie miały ogromny wpływ na .NET jak i narzędzia, które będą powstawać. W aplikacjach biznesowych używanie Roslyn w czystej postaci raczej nie będzie zbyt częstym scenariuszem, aczkolwiek jestem w stanie wyobrazić sobie wiele sytuacji, gdzie jest to bardzo przydatne (np. silniki reguł biznesowych). Jak wspomniałem jednak, zwykle te aplikacje biznesowe korzystają z gotowych komponentów, które z kolei bardzo prawdopodobne będą bazowały na Roslyn.

W kolejnych postach, przyjrzyjmy się szczegółom API…

ASP.NET MVC 6 – AppSettings

W ostatnim wpisie wspomniałem, że Web.Config został usunięty i zastąpiony project.json. Pokazałem, jak dodać referencje czy skonfigurować wersję framework’a. Web.Config jednak zawierał jeszcze jedną ważną sekcję – AppSettings.

ASP.NET 5 wspiera różne typy plików konfiguracyjnych – JSON, INI oraz XML. Możemy wszystkie je dodać jednocześnie do solucji i nie spowoduje to konfliktów. Nie jesteśmy ograniczeni tylko do jednego typu czy pojedynczego pliku konfiguracyjnego. Dla testów, dodajmy 3 pliki, każdy w innym formacie:

JSON.json:

{ "jsontimeout": "1000" }

INI.ini:

initimeout=100

XML.xml:

<root> <xmltimeout>1000</xmltimeout> </root>

Następnie w Startup.cs musimy wskazać powyższe pliki za pomocą:

configuration.AddJsonFile("JSON.json"); configuration.AddIniFile("INI.ini"); configuration.AddXmlFile("XML.xml");

Zmienna configuration jest typu  IConfiguration.  Teraz w dowolnym miejscu, za pomocą również IConfiguration możemy:

string xmlValue = Configuration["xmltimeout"]; string initimeout = Configuration["initimeout"]; string jsontimeout = Configuration["jsontimeout"];

Jak widzimy, wersja ASP.NET 5 daje nam większe możliwości jeśli chodzi o przechowywanie wartości konfiguracyjnych. Nie jesteśmy ograniczeni wyłącznie do Web.config i formatu XML.

Wartości mogą być duplikowane, ale zawsze klucz zarejestrowany później nadpisuje wszystkie starsze.

Ponadto, możliwe jest przechowywanie całych struktur, a nie tylko typów prostych. Załóżmy, że w xml.xml mamy:

<Person> <FirstName>Piotr</FirstName> <LastName>Zielinski</LastName> </Person>

W C# z kolei, tworzymy reprezentację za pomocą klasy:

public class Person { public string FirstName { get; set; } public string LastName { get; set; } }

Wartość możemy przeczytać za pomocą metody Configuration.Get lub wstrzyknąć ją:

services.Configure<Person>(Configuration);

Po wstrzyknięciu, w każdym kontrolerze będziemy mieli dostęp do powyższych danych:

public class HomeController : Controller { private readonly IOptions<Person> _person; public HomeController(IOptions<Person> person) { _person = person; } }

Myślę, że to dobra zmiana, aczkolwiek konwersja aplikacji ze starszych wersji do MVC 6 zdecydowanie jest trudna i czasochłonna.

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.

SpecFlow – data driven testing

Nie jest to pierwszy wpis o SpecFlow i podstawy framework’a znajdziecie oczywiście  w archiwum.

W SpecFlow można definiować tzw. “Scenario outline”, które służą jako szablony dla testów. Oczywiście data-driven testing nie powinno być nadużywane i w szczególności dla acceptance tests nie ma to wielkiego sensu. SpecFlow jest często używany dla testów systemowych i UI, gdzie nie testujemy wszystkich możliwych kombinacji danych wejściowych.

Czasami jednak chcemy mieć bardziej sparametryzowany zestaw testów i tutaj  warto zastanowić się nad Scenario Outline.

Przykład (z oficjalnej dokumentacji):

Scenario Outline: eating Given there are <start> cucumbers When I eat <eat> cucumbers Then I should have <left> cucumbers Examples: | start | eat | left | | 12 | 5 | 7 | | 20 | 5 | 15 |

Jest to odpowiednik TestCase w przypadku czystego nUnit. Jeśli zajrzymy do code-behind, faktycznie zobaczymy, że to klasyczny data-driven test, który zawiera atrybuty TestCase:

[NUnit.Framework.TestAttribute()] [NUnit.Framework.DescriptionAttribute("eating")] [NUnit.Framework.TestCaseAttribute("12", "5", "7", null)] [NUnit.Framework.TestCaseAttribute("20", "5", "15", null)] public virtual void Eating(string start, string eat, string left, string[] exampleTags) { TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("eating", exampleTags); #line 6 this.ScenarioSetup(scenarioInfo); #line 7 testRunner.Given(string.Format("there are {0} cucumbers", start), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line 8 testRunner.When(string.Format("I eat {0} cucumbers", eat), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line 9 testRunner.Then(string.Format("I should have {0} cucumbers", left), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden this.ScenarioCleanup(); }

Powyższy scenariusz zostanie zatem wykonany dwukrotnie dla różnych zestawów danych.  Wygenerowane kroki wyglądają następująco:

[Binding] public class SampleScenarioSteps { [Given(@"there are (.*) cucumbers")] public void GivenThereAreCucumbers(int p0) { ScenarioContext.Current.Pending(); } [When(@"I eat (.*) cucumbers")] public void WhenIEatCucumbers(int p0) { ScenarioContext.Current.Pending(); } [Then(@"I should have (.*) cucumbers")] public void ThenIShouldHaveCucumbers(int p0) { ScenarioContext.Current.Pending(); } }

Jeszcze raz podkreślam, aby być ostrożnym z używaniem tego – często nie są testy jednostkowe. Z drugiej jednak strony, SpecFlow może być wykorzystywany dla klasycznych testów jednostkowych, pisanych bardziej w stylu BDD. W takiej sytuacji wciąż chcemy przetestować różne kombinacje i nic nie stoi na przeszkodzie, aby użyć ScenarioOutline. W końcu SpecFlow to nie tylko UI i acceptance testing.