SignalR: wywoływanie metod

Dzisiaj trochę więcej szczegółów na temat, jak można wywoływać metody w Hub API. W ostatnim wpisie, zaimplementowaliśmy klasę, która wykonuje metodę po wszystkich klientach (broadcast). SignalR ma ogromne możliwości i istnieje wiele innych wzorców.

Dla przypomnienia broadcast wygląda następująco:

public void SendMessage(string message)
{
  Clients.All.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Clients posiada kilka metod, służących do wysyłania wiadomości w różnych sposób. Na przykład, aby wykonać metodę wyłącznie po stronie klienta, który wysłał zapytanie do serwera można:

public void SendMessage(string message)
{
  Clients.Caller.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Innymi słowy, gdy przeglądarka wywołuje SendMessage, newMessage zostanie wykonane wyłącznie w tej przeglądarce (a nie jak wcześniej we wszystkich podłączonych klientach).

Inną ciekawą konstrukcją jest wywołanie metody we wszystkich klientach, oprócz tego który zainicjował połączenie (wywołał metodę serwerową):

public void SendMessage(string message)
{
  Clients.Others.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Istnieje opcja wywołania metody dla określonego nazwą (string) połączenia:

Clients.Client("connectionID").newMessage(string.Format("{0}: {1}",DateTime.Now,message));

Analogicznie, wszystkie połączenia oprócz tych przekazanych jako parametr:

public void SendMessage(string message)
{
  Clients.AllExcept("connectionID1","connectionId2").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Jeśli znamy nazwę użytkownika (IUserIdProvider), możemy wywołać metodę po stronie klienta, który używa określonego loginu:

public void SendMessage(string message)
{
  Clients.User("user1").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Bardzo ciekawym rozwiązaniem są grupy. Umożliwiają one implementacje wzorca polegającym na publish\subscribe. Dzięki grupom, można wywoływać metody, wyłącznie w tych klientach, które nalezą do konkretnej grupy.

public void SendMessage(string message)
{
  Clients.Group("group_name").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Powyższy kod wywoła newMessage w klientach należących do group_name. Pozostaje pytanie, jak dodać użytkownika (połączenie) do grupy? Wystarczy:

public class MyFirstHub : Hub
{
   public void AddToGroup(string groupName)
   {
       Groups.Add(Context.ConnectionId, groupName);
   }
   public void RemoveFromGroup(string groupName)
   {
       Groups.Remove(Context.ConnectionId, groupName);
   }
}

Add automatycznie stworzy grupę jeśli takowa nie istnieje. Kod dodaje połączenie skojarzone z klientem, który wywołuje właśnie serwerową metodę.

Powyższy kod jest jednak nie do końca poprawny. Zaglądając do dokumentacji, zobaczymy:

 Task Add(string connectionId, string groupName);

Oznacza to, że jest to metoda asynchroniczna. W praktyce, wywołanie powyższego kodu, zwróci rezultat natychmiast, mimo, że nie ma pewności, że połączenie zostało dodane do konkretnej grupy.

Z tego względu dużo lepiej jest:

public class MyFirstHub : Hub
{
   public async void AddToGroup(string groupName)
   {
       await Groups.Add(Context.ConnectionId, groupName);
   }
   public async void RemoveFromGroup(string groupName)
   {
       await Groups.Remove(Context.ConnectionId, groupName);
   }
}

W ostatnim poście również wspomniałem, że należy dodać następującą referencję:

<script src="~/signalr/hubs"></script>    

Tak naprawdę, jest to referencja do automatycznie wygenerowanego kodu JavaScript. Sprawdźmy, co dokładnie jest wygenerowane:

/*!
 * ASP.NET SignalR JavaScript Library v2.1.0
 * http://signalr.net/
 *
 * Copyright Microsoft Open Technologies, Inc. All rights reserved.
 * Licensed under the Apache 2.0
 * https://github.com/SignalR/SignalR/blob/master/LICENSE.md
 *
 */

/// <reference path="..\..\SignalR.Client.JS\Scripts\jquery-1.6.4.js" />
/// <reference path="jquery.signalR.js" />
(function ($, window, undefined) {
    /// <param name="$" type="jQuery" />
    "use strict";

    if (typeof ($.signalR) !== "function") {
        throw new Error("SignalR: SignalR is not loaded. Please ensure jquery.signalR-x.js is referenced before ~/signalr/js.");
    }

    var signalR = $.signalR;

    function makeProxyCallback(hub, callback) {
        return function () {
            // Call the client hub method
            callback.apply(hub, $.makeArray(arguments));
        };
    }

    function registerHubProxies(instance, shouldSubscribe) {
        var key, hub, memberKey, memberValue, subscriptionMethod;

        for (key in instance) {
            if (instance.hasOwnProperty(key)) {
                hub = instance[key];

                if (!(hub.hubName)) {
                    // Not a client hub
                    continue;
                }

                if (shouldSubscribe) {
                    // We want to subscribe to the hub events
                    subscriptionMethod = hub.on;
                } else {
                    // We want to unsubscribe from the hub events
                    subscriptionMethod = hub.off;
                }

                // Loop through all members on the hub and find client hub functions to subscribe/unsubscribe
                for (memberKey in hub.client) {
                    if (hub.client.hasOwnProperty(memberKey)) {
                        memberValue = hub.client[memberKey];

                        if (!$.isFunction(memberValue)) {
                            // Not a client hub function
                            continue;
                        }

                        subscriptionMethod.call(hub, memberKey, makeProxyCallback(hub, memberValue));
                    }
                }
            }
        }
    }

    $.hubConnection.prototype.createHubProxies = function () {
        var proxies = {};
        this.starting(function () {
            // Register the hub proxies as subscribed
            // (instance, shouldSubscribe)
            registerHubProxies(proxies, true);

            this._registerSubscribedHubs();
        }).disconnected(function () {
            // Unsubscribe all hub proxies when we "disconnect".  This is to ensure that we do not re-add functional call backs.
            // (instance, shouldSubscribe)
            registerHubProxies(proxies, false);
        });

        proxies['myFirstHub'] = this.createHubProxy('myFirstHub'); 
        proxies['myFirstHub'].client = { };
        proxies['myFirstHub'].server = {
            addToGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["AddToGroup"], $.makeArray(arguments)));
             },

            removeFromGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["RemoveFromGroup"], $.makeArray(arguments)));
             }
        };

        return proxies;
    };

    signalR.hub = $.hubConnection("/signalr", { useDefaultPath: false });
    $.extend(signalR, signalR.hub.createHubProxies());

}(window.jQuery, window));

Jak widać, plik zawiera głównie proxy. Dzięki niemu, możemy w łatwy sposób w JavaScript wywołać kod C# znajdujący się na serwerze:

 proxies['myFirstHub'].server = {
            addToGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["AddToGroup"], $.makeArray(arguments)));
             },

            removeFromGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["RemoveFromGroup"], $.makeArray(arguments)));
             }
        };

ASP.NET: SignalR i Hub API

W poprzednim poście przedstawiłem framework SignalR oraz niskopoziomową klasę PersistentConnection. Zwykle jest ona trudna w użyciu i dlatego w praktyce dużo częściej korzysta się z Hub API.  Jak sama nazwa wskazuje, stanowi on koncentrator czyli miejsce gdzie składujemy metody zarówno wykonywane po stronie serwera jak i klienta. Dzięki niemu możemy wywołać metodę C# (serwer) jak i JavaScript (klient). Umożliwia on zatem komunikacje obustronną.

Spróbujmy zatem przerobić przykład z poprzedniego postu, aby używał Hub zamiast PersistentConnection. Najpierw oczywiście usuwamy MyFirstConnection i zastępujemy ją hub’em:

public class MyFirstHub : Hub
{
   public void SendMessage(string message)
   {
       Clients.All.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
   }
}

Nasz hub ma jedną metodę teraz – Send Message. Proszę zauważyć, że All jest typu dynamic i dlatego możemy jakąkolwiek metodę wykonać. Metody są definiowane w JavaScript dlatego naturalne jest, że taka metoda nie zostanie automatycznie wykryta przez VS.

Powyższy kod jest implementacją serwerowej metody  SendMessage.  Po odebraniu wiadomości jest ona wysyła do wszystkich klientów (broadcast). newMessage musi zatem zostać zdefiniowana w JavaScript po stronie klienta:

<script type="text/javascript">
    $(function () {

        var myFirstHub = $.connection.myFirstHub;
        myFirstHub.client.newMessage = function (message) {
            $('#messages').append('<li>' + message + '</li>');
        };

        $.connection.hub.start();
        
        $("#broadcast").click(function () {
            myFirstHub.server.sendMessage($('#msg').val());
        });

    });
    </script>

<input type="text" id="msg" />
<input type="button" id="broadcast" value="broadcast" />

<ul id="messages"></ul>

Najpierw tworzymy hub’a a potem za pomocą obiektu server możemy wysyłać wiadomości do serwera. Analogicznie, istnieje możliwość definiowania metod po stronie klienta, które mogą być wykonywane przez serwer.

Należy koniecznie dodać referencje do automatycznie wygenerowanych hubów w JS:

<script src="~/Scripts/jquery-1.10.2.js" type="text/javascript"></script>
<script src="~/Scripts/jquery.signalR-2.1.0.js" type="text/javascript"></script>
<script src="~/signalr/hubs"></script>    

Na zakończenie wystarczy tylko zmapować hub’a do URL:

[assembly: OwinStartupAttribute(typeof(WebApplication9.Startup))]
namespace WebApplication9
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
            app.MapSignalR();
        }
    }
}

W kolejnej części zajmiemy się bardziej szczegółowo wywoływaniem metod w SignalR.

ASP.NET: SignalR

Aplikacje webowe przeszły rewolucje od czasów pierwszych stron internetowych. Przez długi czas, aplikacje webowe opierały się na prostym schemacie zapytania i odpowiedzi. Wpisując jakiś adres w przeglądarce, generowany był po prostu dokument. Nie było możliwości interakcji. Nawigacja do innej strony, skutkowała przeładowaniem całej strony.

Później pojawił się AJAX, czyli wykonywanie metod usługi w tle. Był to na pewno krok do przodu ponieważ nie było potrzebne już przeładowanie całej strony, aby odświeżyć jakiś tylko fragment. Niestety wciąż była to komunikacja jednokierunkowa, od przeglądarki do serwera. W przeciwnym kierunku byłoby to możliwe wyłącznie poprzez przeładowanie całej strony. Nie istniał wtedy sposób na  łatwe powiadomienie przeglądarki, że stan bazy danych zmienił się.

Możliwe były oczywiście pewne obejścia. Najczęściej przeglądarka odpytywała serwer, czy są już jakieś zmiany, które powinny być odzwierciedlone w przeglądarce. Oczywiście w aplikacjach czasu rzeczywistego nie jest to perfekcyjne rozwiązanie, ponieważ wiąże się z dużym obciążeniem łącza i wynikającymi z tego opóźnieniami.

Dzisiaj na szczęście mamy inne, lepsze rozwiązania. Nie będę opisywał w tym poście szczegółów, ponieważ, chcę najpierw skupić się na samym framework’u SignalR, a potem dopiero prześledzimy jego internale.

W skrócie, tzw. Web Socket API umożliwia pełną komunikację między przeglądarką a serwerem. Możliwe jest zatem dzięki Web Socket, że klient wysyła w tle wiadomość do serwera (nic nadzwyczajnego) oraz serwer do przeglądarki (i to jest nowość). Jest to ogólny standard i nie ma nic wspólnego z ASP.NET. Ponadto, nie jest on wyłącznie ograniczony do przeglądarek internetowych. Działa zwykle na porcie 80 (korzystne dla firewalls itp.).

W HTTML 5 API do dyspozycji są tzw. Serer-Sent Events. Dzięki nim, serwer może powiadomić przeglądarkę o zmianach. Jest to jednak komunikacja również jednokierunkowa, od serwera do klienta.

SignalR jest z kolei frameworkiem dla ASP.NET ułatwiającym pisanie aplikacji czasu rzeczywistego, czyli takich gdzie nie chcemy przeładowywać całej strony bo dane zmieniają się w czasie “rzeczywistym”. Oczywiście kluczowym mechanizmem w takiej scenariuszu jest powiadomienie klientów o zmianach na serwerze. Można oczywiście bezpośrednio korzystać z Web Socket czy HTTML 5 SSE. Problem w tym, że część przeglądarek nie wspiera Web Socket ani SSE. SignalR za nas dostarczy warstwę abstrakcji, czyli wykorzysta najlepszą, dostępną aktualnie technologie.

Jeśli przeglądarka i serwer dostarczają Web Socket, to zostanie on oczywiście wykorzystany bo jest jedynym w pełni dwukierunkowym sposobem komunikacji. W przeciwnym razie HTML 5 SSE  może zostać użyty. W najgorszym przypadku, po prostu stary mechanizm odpytywania zostanie wykonany. Dzięki SignalR, programista ma jeden framework i nie interesuje go, co dokładnie przeglądarka wspiera. Framework dobierze najlepszą technologie jaka jest aktualnie dostępna. Programista  korzysta zawsze z tego samego API.

W SignalR do dyspozycji mamy dwa typy połączeń. PersistentConnection oraz HubAPI. Pierwszy z nic jest bardziej niskopoziomowy i dlatego dzisiaj nim zajmiemy się. W przyszłym wpisie pokażę Hub API.

Zaczynamy oczywiście od instalacji SignalR z NuGet:

image

NuGet zainstaluje pakiet, w którego skład wchodzą m.in. skrypty JS, biblioteki DLL oraz zostanie również wygenerowany Startup:

[assembly: OwinStartupAttribute(typeof(WebApplication9.Startup))]
namespace WebApplication9
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

Na razie o Startup nie musimy martwić się. Stwórzmy nasze pierwsze połączenie:

public class MyFirstConnection:PersistentConnection
{
   protected override Task OnReceived(IRequest request, string connectionId, string data)
   {
       return Connection.Broadcast(data);
   }
}

Przeładowujemy tutaj OnReceived, metodę, która jest wywołana, gdy któryś z klientów wyśle jakąś wiadomość. Następnie tą samą wiadomość, przekazujemy do pozostałych (broadcast).

Pora na kod JS, czyli stronę klienta (przykład z dokumentacji SignalR):

$(function () {
        var connection = $.connection('/echo');

        connection.received(function (data) {
            $('#messages').append('<li>' + data + '</li>');
        });

        connection.start().done(function() {
            $("#broadcast").click(function () {
                connection.send($('#msg').val());
            });
        });

    });

Jak widzimy, mamy tutaj kod, polegający na zewnętrznych bibliotekach np. metoda $.connection. Musimy zatem podłączyć bibliotekę jquery.signalR-2.1.0, która została zainstalowana w ramach pakietu SignalR (NuGet). Oprócz tego, musimy dodać referencje do jQuery:

<script src="~/Scripts/jquery-1.10.2.js"></script>
<script src="~/Scripts/jquery.signalR-2.1.0.js"></script>

Ostatnia rzecz jaka nam pozostała to mapowanie. Powyższy przykład, używa metody echo, która nie jest jeszcze zdefiniowana. Można zobaczyć w $.connection, że odwołujemy się do /echo, ale póki co nie zaimplementowaliśmy tego. Wystarczy, że zmapujemy /echo do naszego połączenia MyFirstConnection we wspomnianej wcześniej klasie Startup:

public partial class Startup
{
   public void Configuration(IAppBuilder app)
   {
       ConfigureAuth(app);
       app.MapSignalR<MyFirstConnection>("/echo");
   }
}

Po uruchomieniu aplikacji, szybko przekonamy się, że wpisując dane w oknie jednej przeglądarki, zostaną one rozesłane do pozostałych, podłączonych klientów (broadcast).

SpecFlow: Przekazywanie parametrów

W poprzednim wpisie, pokazałem jak stworzyć prosty zestaw testów za pomocą SpecFlow. Dzisiaj chciałbym bardziej przyjrzeć się parametrom, które można przekazywać. Rozważmy następujący scenariusz:

Scenario: Add two numbers
    Given I have entered 50 into the calculator
    And I have entered 70 into the calculator
    When I press add
    Then the result should be 120 on the screen

Liczby 50, 70 i 120 to parametry wejściowe. Jeśli mamy te same zachowania albo bardzo podobne, wtedy lepiej korzystać z argumentów, a nie duplikować wszędzie opis. Dla przypomnienia, wygenerowane metody będą wyglądać następująco:

[Given(@"I have entered (.*) into the calculator")]
public void GivenIHaveEnteredIntoTheCalculator(int p0)
{
  ScenarioContext.Current.Pending();
}
[Then(@"the result should be (.*) on the screen")]
public void ThenTheResultShouldBeOnTheScreen(int p0)
{
  ScenarioContext.Current.Pending();
}

Pozostaje pytanie, jak SpecFlow wie, że argument p0 to tak naprawdę liczba z tekstu? Jeśli spojrzymy na opis scenariusza, to jest to zwykły tekst – nie ma tam żadnego modyfikatora mówiącego o tym, że dana liczba to parametr a nie tekst. Tak naprawdę, wspomniane atrybuty to wyrażenia regularne:

[Given(@"I have entered (.*) into the calculator")]

Wszystko w nawiasach (.*) to dany parametr wejściowy. W powyższym przykładzie istnieje jeden parametr ponieważ mamy jedną grupę (.*).

Inny REGEX może wyglądać następująco:

[Given(@"I have entered (50|60) into the calculator")]

Powyższe wyrażenie zaakceptuje wyłącznie liczby 50 lub 60. Dla innych parametrów zostanie wyrzucony wyjątek:

No matching step definition found for one or more steps.

using System;

using TechTalk.SpecFlow;

Wprowadzenie do SpecFlow, BDD

Dzisiaj oprogramowanie jest naprawdę skomplikowane. W większości przypadków, skomplikowanie oprogramowania nie polega na zaawansowanych algorytmach. W praktyce, algorytmy tworzone są przez wąską grupę specjalistów i potem są po prostu wykorzystywane w formie DLL przez innych użytkowników (programistów). Oczywiście każda aplikacja ma jakieś algorytmy, ale w aplikacjach biznesowych, zwykle większym problemem jest utrzymanie całości i możliwość szybkiego dostarczenia produktu. Słaby kod cechuje się długimi cyklami testowania regresyjnego. W dobrym projekcie, z dużą liczbą automatycznych testów, taki czas jest znaczenie skrócony.

Innym poważnym problemem jest zgromadzenie wymaganiach i ich poprawna interpretacja. Wiele projektów cierpi ze zbyt częstych zmian i niejasnych wymaganiach. Jest to jednak nieodłączna część towarzyszącą programistom i po prostu trzeba znaleźć sposób, aby minimalizować problemy komunikacyjne między programistami a np. ekspertami z danej dziedziny.

W dzisiejszym wpisie chciałbym przestawić framework SpecFlow, ułatwiający pisanie testów BDD – Behaviour Driven Development. Podejście bardzo popularne, w szczególności w aplikacjach biznesowych, gdzie testy są tak naprawdę opisane przez user story.

Nie będę opisywał tutaj BDD bo to temat prawdopodobnie na inny wpis. Skupie się głównie na samym framework’u i podam tylko kilka różnić między BDD a klasycznym testowaniem.

Przede wszystkim należy wiedzieć jak działa Test-Driven-Development. W skrócie jest to podejście, gdzie najpierw piszę się testy jednostkowe, a potem dopiero implementacje interfejsów. Takim sposobem, każdy nowy kod, powinien być już pokryty testami. Daje to taką przewagę, że programiści są zmuszeni do pisania elastycznego kodu  – inaczej ciężko będzie napisać jakiekolwiek testy jednostkowe. Kolejną zaletą jest większa świadomość możliwych parametrów wejściowych i nieoczekiwanych scenariuszy.

Niestety bardzo często tworzone testy są ciężkie w utrzymaniu. Wiele testów to kontenery na metody, które z nazwy nic nie mówią. Skupiają się one na testowaniu API, a nie realnych scenariuszy. Jeśli mamy w kodzie metody o nazwach Method1NullTest to niewiele one mówią użytkownikowom czy testerom.

W BDD skupiamy się zatem na scenariuszach użycia aplikacji. Dlatego stanowią one tak naprawdę cześć opisu każdej, dobrze zdefiniowanej user story. Nie zapominajmy, że testy jednostkowe to black-box testing – nie interesuje nas jak coś jest zaimplementowane. Skupiamy się na tym, jak użytkownik może korzystać z naszej aplikacji. Rozważamy workflow’y, a nie konkretne gałęzie IF’a – bo jak zostało wspomniane, w black-box testing nie analizujemy kodu. Oczywiście metryki pokrycia kodu wciąż są pożytecznie i należy z nich korzystać. Chodzi mi bardziej, że programista powinien myśleć w kategoriach scenariuszy użycia, a nie pokrycia każdej gałęzi IF, przekazując tym samym wszystkie możliwe parametry wejściowe. Naturalnie, że w pewnych sytuacjach (algorytmy) sprawa wygląda inaczej i tam faktycznie testowanie polega na po prostu definiowaniu wejścia i wyjścia. W praktyce jednak, często błędy występującą w UI i w sposobie nawigacji (workflows).

BDD czy TDD to moim zdaniem bardzo naturalne podejścia – po prostu najpierw myślimy jak kod ma działać, a potem go implementujemy. Jeśli mamy sprzeczne wymagania to dowiemy się o tym na etapie definiowania testów, a nie na koniec, gdy 90% kodu jest już zaimplementowane.

W SpecFlow, zachowania czyli scenariusze opisane są za pomocą trzech kroków Given, When, Then (GWT):

1. Given stanowią preconditions czyli warunki wstępne. Innymi słowy, wyjaśniają one, jaki stan mamy przed danym scenariuszem.

2. When – zdarzenie, które powoduje wywołanie danego zachowania. Może być to np. kliknięcie w link na stronie internetowej czy wywołanie jakieś metody WebAPI.

3. Then – etap, w którym mamy jakiś rezultat do testowania.

Podsumowując, mając warunki Given, w momencie zdarzenia When, otrzymujemy jakieś dane, które chcemy zweryfikować.

SpecFlow integruje się ze środowiskiem Visual Studio. Musimy najpierw zainstalować odpowiedni plugin. Klikamy więc Tools->Extensions and Updates:

image

Następnie tworzymy nowy projekt (Windows Library) i instalujemy pakiet z NuGet, który zintegruje SpecFlow z nUnit:

image

Czas zacząć napisać nasz pierwszy scenariusz. W tym celu, przechodzimy do Add New Item i wybieramy SpecFlow Feature File:

image

Domyślnie zostanie wygenerowany przykładowy scenariusz:

Feature: SpecFlowFeature1
    In order to avoid silly mistakes
    As a math idiot
    I want to be told the sum of two numbers

@mytag
Scenario: Add two numbers
    Given I have entered 50 into the calculator
    And I have entered 70 into the calculator
    When I press add
    Then the result should be 120 on the screen

Jak widać, jest on opisany naturalnym językiem. Oznacza to, że taki scenariusz jest zrozumiały nie tyko dla programistów czy testerów, ale również osoby nietechnicznej. W tym wpisie, wykorzystamy powyższy, automatycznie wygenerowany scenariusz. W kolejnym poście, zajmiemy się ASP.NET MVC i jakimś bardziej praktycznym przykładem.

SpecFlow, jak wspomniałem integruje się z Visual Studio. Dzięki temu możemy kliknąć w Generate Step Definitions w kontekstowym menu i zobaczymy następujące okno:

image

Po naciśnięciu Generate, zostanie wygenerowany nowy plik z testami:

[Binding]
public class SpecFlowFeature1Steps
{
   [Given(@"I have entered (.*) into the calculator")]
   public void GivenIHaveEnteredIntoTheCalculator(int p0)
   {
       ScenarioContext.Current.Pending();
   }

   [When(@"I press add")]
   public void WhenIPressAdd()
   {
       ScenarioContext.Current.Pending();
   }

   [Then(@"the result should be (.*) on the screen")]
   public void ThenTheResultShouldBeOnTheScreen(int p0)
   {
       ScenarioContext.Current.Pending();
   }
}

Na razie testy są oczywiście puste. Możemy przejść do pliku feature i wybrać z menu kontekstowego “Run SpecFlow Scenarios”:

Implementacja takiego testu mogłaby wyglądać tak:

[Given(@"I have entered (.*) into the calculator")]
public void GivenIHaveEnteredIntoTheCalculator(int p0)
{
  _calculator.EnterNumber(p0);
}

[When(@"I press add")]
public void WhenIPressAdd()
{
  _calculator.Add();
}

[Then(@"the result should be (.*) on the screen")]
public void ThenTheResultShouldBeOnTheScreen(int p0)
{
  Assert.AreEqual(p0, _calculator.Output);
}

Proszę zwrócić uwagę na (.*)-  to są parametry przekazane z pliku features tzn.:

Given I have entered 50 into the calculator
And I have entered 70 into the calculator
When I press add
Then the result should be 120 on the screen

W kolejnym wpisie dokładnie przyjrzyjmy się jak parametry są rozpoznawalne i przekazywane oraz jak wykonywany jest scenariusz. Na razie warto zapamiętać kroki GWT – Given, When, Then ponieważ są one najczęściej stosowane.

Moim zdaniem, najtrudniejsze w tym jest poprawne opisywanie testów. Tego oczywiście nie da się nauczyć z dokumentacji i po prostu trzeba praktykować. Złe testy to marnowanie czasu i zwiększanie kosztów na utrzymanie tego. Testy po prostu muszą redukować czas przeznaczany na manualne testowanie i naprawianie bug’ów. Jeśli poprawa nie jest zauważalna to oznacza, że coś źle robimy. W momencie zgłoszenia bug’a oprócz jego naprawienia powinno również się uzupełnić brakujący zestaw testów jednostkowych – jasne jest, że coś zostało w nich pominięte skoro błąd został zgłoszony.

C#–zablokowane destruktory

O finalizers czy też destruktorach pisałem już wielokrotnie. Wspomniałem również, że lepiej ich unikać, jeśli oczywiście to możliwe. Głównym problem jest możliwość wypromowania obiektu do kolejnych generacji, ponieważ obiekty z destruktorami są umieszczane w kolejce freachable, gdzie na nową stają się osiągalne.

Jeśli ktoś nie pamięta, to polecam najpierw poczytanie o tym (w dziale articles są linki do moich tekstów o GC).

Dzisiaj jednak chciałbym wspomnieć o czymś innym. Wiemy, że jeden wątek zwykle monitoruje freachable i wywołuje destruktory. Z tego względu, realne zagrożenie to blokada przez jeden obiekt,  uniemożliwiająca zwolnienie zasobów wszystkich pozostałych obiektów. W każdej aplikacji jest to problem i ryzyko. Oczywiście, jeśli tworzymy jakiś rozszerzalny Engine to sprawa komplikuje się jeszcze bardziej. Zacznijmy od przykładu:

private class UglyObject
{
  private readonly string _text;
  private readonly int _time;

  public UglyObject(string text,int time)
  {
      _text = text;
      _time = time;
  }

  ~UglyObject()
  {
      Thread.Sleep(_time);
      Console.WriteLine("{0}: zwalnianie pamieci. {1} {2}",DateTime.Now,_text,_time);
  }
}

Prosta klasa z Finalizer. Następnie wywołajmy w pętli kilka inicjalizacji klasy UglyObject. Przekazujemy najpierw wartość zero do Thread.Sleep:

private static void Main(string[] args)
{
  Test();
  GC.Collect();
  GC.WaitForPendingFinalizers();
  Console.WriteLine("Koniec");
}

private static void Test()
{
  for (int i = 0; i < 10; i++)
      new UglyObject(i.ToString(CultureInfo.InvariantCulture),0);
}

Tutaj nic złego nie dzieje się. Na ekranie, po wymuszeniu GC dostaniemy oczywiście kolejne wywołania destruktora:

image

Następnie zmodyfikujmy metodę Test, tak aby Thread.Sleep blokował część wątków przez kilka sekund:

private static void Test()
{
  for (int i = 0; i < 10; i++)
  {
    new UglyObject(i.ToString(CultureInfo.InvariantCulture), i<5?60*1000:0);
  }
}

Jeśli kilka różnych wątków przetwarzałoby kolejkę freachable to możliwe byłoby natychmiastowe zwolnienie 5 ostatnich wątków, ponieważ przekazujemy tam wartość zero jako czas.

image

Jeszcze gorszy scenariusz to taki, w którym mamy jakąś synchronizacje (semafory, muteksy itp.), która może permanentnie zablokować kolejkę. Zwykle piszemy destruktory, gdy korzystamy z niezarządzanych zasobów. Niesie to ze sobą ryzyko, że kod, którego nie znamy jest wywoływany. Metody Finalizers zawsze powinny być szybkie i zwracać wynik natychmiast. Jeszcze groźniejszym przypadkiem są obiekty COM. Obiekty STA, mogą zostać wywołane wyłącznie przez wątek, które je stworzył. Jeśli taki wątek aktualnie jest zajęty, to całość będzie blokowana. Wiąże to się z Thread Affinity czyli przynależnością obiektu do wątku.

Skutek zablokowanych finalizerow to oczywiście memory leak i ciągłe zwiększanie zużycia pamięci.

W przypadku, gdy proces jest zamykany, również destruktory są wywoływane. Oczywiście CLR nie może sobie pozwolić, że jeden obiekt blokuje cały proces. Zwykle, po 2 sekundach wszystkie wątki zostaną po prostu anulowane i proces zostanie zabity.

ASP.NET MVC 5: Własny szablon

Już kilka tygodni typu pisałem o ASP.NET MVC 5 Scaffolding. Jak wspomniałem, domyślny szablon nadaje się do prototypów albo tymczasowych aplikacji. W praktyce będziemy chcieli wykorzystać IoC i własne usługi, a nie bezpośrednio operować na bazie danych. Za pomocą własnego szablonu, można wszystko dostosować do konkretnego projektu i infrastruktury.

Najpierw ściągamy pakiet szablonów VS z http://sidewaffle.com/. Oprócz tego, musimy zainstalować Visual Studio 2013 Update 2 oraz Visual Studio 2013 SDK.

Następnie w VS klikamy na Extensibility i Basic Scaffolder:

image

Dzięki SideWaffle zostaną wygenerowane podstawowe pliki potrzebne do stworzenia własnego szablonu w ASP.NET MVC 5. Dla podstawowej funkcjonalności wystarczy, że wyedytujemy kilka z nich.

image

Po wciśnięciu F5 uruchomi się Visual Studio experimental instance, co umożliwi nam debugowanie kodu. Jakbyśmy teraz chcieli skorzystać z naszego szablonu to zobaczymy tylko automatycznie wygenerowane nazwy, tzn.:

image

Po wciśnięciu Add ujrzyjmy automatycznie utworzone okno dialogowe:

image

Spróbujmy przejrzeć się wygenerowanym plikom i dodać jakąś własną logikę.  Zacznijmy od klasy CustomCodeGeneratorFactory, w której możemy określić metadane:

private static CodeGeneratorInformation _info = new CodeGeneratorInformation(
  displayName: "Custom Scaffolder",
  description: "This is a custom scaffolder.",
  author: "Author Name",
  version: new Version(1, 0, 0, 0),
  id: typeof(CustomCodeGenerator).Name,
  icon: ToImageSource(Resources._TemplateIconSample),
  gestures: new[] { "Controller", "View", "Area" },
  categories: new[] { Categories.Common, Categories.MvcController, Categories.Other });

Nie ma tutaj nic nadzwyczajnego. Proste informacje o autorze, nazwie szablonu itp:

private static CodeGeneratorInformation _info = new CodeGeneratorInformation(
       displayName: "Pierwszy szablon",
       description: "Moj pierwszy szablon.",
       author: "Piotr Zielinski",
       version: new Version(1, 0, 0, 0),
       id: typeof(CustomCodeGenerator).Name,
       icon: ToImageSource(Resources._TemplateIconSample),
       gestures: new[] { "Controller", "View", "Area" },
       categories: new[] { Categories.Common, Categories.MvcController, Categories.Other });

Dużo ciekawszą klasą jest CustomCodeGenerator, która dziedziczy po CodeGenerator. Najważniejsze są tam dwie metody: GenerateCode oraz ShowUIAndValidate:

public interface ICodeGenerator
{
/// <summary>
/// Performs code generation.
/// 
/// </summary>
void GenerateCode();
/// <summary>
/// Gathers and validates information necessary for code generation.
///             Any UI for the code generator should be displayed in this method.
/// 
/// </summary>
/// 
/// <returns>
/// Return value indicates whether information was gathered and code
///             generation can be performed.
/// 
/// </returns>
/// 
/// <remarks>
/// Implement and return true, if scaffolder needs no user interface.
/// 
/// </remarks>
bool ShowUIAndValidate();
/// <summary>
/// A list of NuGet packages upon which this
///             code generator depends in order to run successfully. Packages listed here will be installed
///             after ShowUIAndValidate succeeded and before the
///             GenerateCode method is invoked.
/// 
/// </summary>
/// 
/// <returns>
/// A list containing the NuGet package dependencies
/// </returns>
IEnumerable<NuGetPackage> Dependencies { get; }
}

Domyślna implementacja ShowUiAndValidate wygląda następująco:

public override bool ShowUIAndValidate()
{
  // Bring up the selection dialog and allow user to select a model type
  SelectModelWindow window = new SelectModelWindow(_viewModel);
  bool? showDialog = window.ShowDialog();
  return showDialog ?? false;
}

Innymi słowy, wyświetlane jest okno SelectModelWindow. Możemy oczywiście zastąpić to własnymi kontrolkami. Przykładowy SelectModelWIndow to nic innego jak okno WPF (XAML) i wygląda następująco:

<Window x:Class="BasicScaffolder3.UI.SelectModelWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d" Height="176" Width="511" Title="Model Types">
  <Grid>
    <Label Content="Choose a Model Type:" HorizontalAlignment="Left"   Margin="36,39,0,0" VerticalAlignment="Top"/>
    <ComboBox HorizontalAlignment="Left"
              Margin="169,43,0,0"
              VerticalAlignment="Top"
              ItemsSource="{Binding ModelTypes}"
              DisplayMemberPath="DisplayName"
              SelectedItem="{Binding SelectedModelType, Mode=OneWayToSource}"
              Width="311"/>
    <Button Content="Add" IsDefault="True" HorizontalAlignment="Left" Margin="317,102,0,0" VerticalAlignment="Top" Width="75" RenderTransformOrigin="-0.187,0.75" Click="Button_Click"/>
    <Button Content="Cancel" IsCancel="True" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Margin="405,102,0,0"/>

  </Grid>
</Window>

Jak ktoś zna WPF to nie będzie miał  z tym problemu. Z kolei domyślna implementacja GenerateCode to:

public override void GenerateCode()
{
  // Get the selected code type
  var codeType = _viewModel.SelectedModelType.CodeType;

  // Setup the scaffolding item creation parameters to be passed into the T4 template.
  var parameters = new Dictionary<string, object>()
  {
      { 
          /* This value should match the parameter in T4 */
          "ModelType", 
          
          /* This is the value passed */ 
          codeType
      }

      //You can pass more parameters after they are defined in the template
  };

  // Add the custom scaffolding item from T4 template.
  this.AddFileFromTemplate(Context.ActiveProject,
      "CustomCode",
      "CustomTextTemplate",
      parameters,
      skipIfExists: false);
}

Szczególnie proszę zwrócić uwagę na:

this.AddFileFromTemplate(Context.ActiveProject,
           "CustomCode",
           "CustomTextTemplate",
           parameters,
           skipIfExists: false);

Wywołanie oznacza, że zostanie użyty szablon T4 (zachęcam do przeczytania tych wpisów). CustomTextTemplate wygląda następująco:

<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ parameter name="ModelType" type="EnvDTE.CodeType" #>

/// This code was generated by Basic Scaffolder.

namespace <#= ModelType.Namespace.FullName #>
{
    public class <#= ModelType.Name #>Controller
    {
        public void <#= ModelType.Name #>Method()
        {
        }
    }
}

Oczywiście można tworzyć dowolną liczbę szablonów. Możliwe jest również dodanie statycznych plików za pomocą metody AddFile:

protected bool AddFile(Project project, string projectRelativePath, string sourceFilePath, bool skipIfExists);

Kolejna metoda to AddFodler, która oczywiście tworzy foldery:

protected ProjectItem AddFolder(Project project, string projectRelativePath);

Bardziej zawansowane szablony mogą korzystać z NuGet. Wystarczy przeładować właściwość Dependencies.

Myślę, że na wprowadzenie wystarczy. Szablony T4 to potężne narzędzie i za pomocą ich można wygenerować dowolny plik, dostosowany do każdego projektu.

Visual Studio 2013 – debugowanie asynchronicznego kodu

W VS 2013 usprawniono debugowanie kodu asynchronicznego. Wszyscy jesteśmy przyzwyczajeni już do async\await. Znacząco to ułatwia wykonywanie operacji asynchronicznych. Niestety, debugowanie w VS 2012 jest dość uciążliwe. Załóżmy, że mamy kod z wieloma metodami asynchronicznymi, które z kolei są pozagnieżdżane. W przypadku wyrzucenia wyjątku lub ustawienia breakpoint’a, call stack nie zawierał żadnych informacji. Przetestujmy opisany problem  na następującym kodzie:

public partial class MainWindow : Window
{
   public MainWindow()
   {
       InitializeComponent();
       DoSomething();
   }   
   private async void DoSomething()
   {
       await RunAsync();
   }

   private async Task RunAsync()
   {
       await Task.Delay(100);
       
       await DownloadNumberAsync();
   }
   private Task<int> DownloadNumberAsync()
   {
       return Task<int>.Factory.StartNew(DownloadNumber);
   }
   private int DownloadNumber()
   {   
       return 1;
   }
}

W Visual Studio 2012, gdy ustawimy breakpoint na linię await DownloadNumberAsync, call stack wyglądał następująco:

image

Z kolei w VS 2013 mamy pełny stos:

image

Dzięki ulepszeniom w VS 2013 i Windows 8.1, dużo łatwiej zrozumieć kod asynchroniczny. Wcześniej call stack pokazywał wyłącznie ostatnią metodę i nie wiadomo było, jak ona została wywołana. Na blogu kiedyś opisywałem internale async\await. Wiemy, że jest tam w rzeczywistości maszyna stanu, oparta na callbackach. Z tego względu, w poprzednich wersjach VS, nie wiadomo było jak metoda była wywołana. VS 2013 rozpoznaje konstrukcje async\await i można już je zaprezentować w sposób, który wynika z kodu c#, a nie implementacji w CLR.

Ciekawostka: implementacja wyrażeń regularnych w .NET

Ostatnio znajomy podesłał mi ciekawe wyrażenie regularne, którego wykonanie trwa bardzo długo w aktualnej implementacji REGEX. Wyrażenie to “(a?){30}a{30}”. Nic specjalnego. Najpierw mamy 30 opcjonalnych liter ‘a’, a potem 30 obowiązkowych. Nie wygląda to zbyt skomplikowanie. Spróbujmy wykonać wyrażenie na tekście “aaaaaaaaaaaaaaaaaaaaaaaaaaa”.

const int tests = 10;
for (int i = 0;i< tests; i++)
{
 var stopwatch = Stopwatch.StartNew();
var result = Regex.IsMatch(new string('a',35), 
 long time = stopwatch.ElapsedMilliseconds;
 Console.WriteLine(time);
}

Wykonanie pojedynczej operacji, zajmie kilka sekund.  Dlaczego? Związane jest to ze sposobem w jaki REGEX został zaimplementowany. Najpierw szukamy 30 znaków ‘a’ (opcjonalnych). Znajdujemy je. Następnie widzimy, że mamy 30 obowiązkowych znaków. Niestety nie znajdziemy ich. Musimy z tego względu usunąć jeden znak  z pierwszej grupy (czyli mamy 29 znaków opcjonalnych teraz) i znów dopasować drugą grupę. Oczywiście znów zabraknie nam literek ‘a’ w drugiej grupie i rozwiązanie będzie polegać na usunięciu kolejnego znaku z pierwszej grupy. Efekt jest taki, że wykonanie prostego zapytania zajmie bardzo dużo czasu. Gdybyśmy mieli więcej niż 60 liter ‘a’, nie wystąpiłby podobny problem ponieważ wszystkie znaki (zarówno opcjonalne jak i obowiązkowe), zostałby dopasowane.