json logo

Pisząc moją pracę inżynierską korzystałem z wielu fajnych rzeczy w tym – z tytułowego JSON. W MVC użycie i samo działanie JSON-a w kontrolerze było bajecznie proste oraz bez jakichś większych fuck-upów pod względem implementacyjnym ze strony Microsoftu (aż dziwne xD). Dziś postanowiłem zobaczyć jak się ma sprawa z wykorzystaniem tej technologii w zwykłej aplikacji ASP.NET (nie MVC) i po całym dniu udręki z kolejnymi rzeczami – doszedłem do etapu, w którym wszystko działa tak, jakbym tego chciał. Trochę długo niestety, ale i sporo problemów po drodze się pojawiło, ale wszystko po kolei. O tyle jest szczęśliwie, że mając już rozwiązania wszystkich problemów nic nowego raczej nas nie zaskoczy :)

Żeby móc pobierać i wysyłać dane z/do naszej witryny należy najpierw skonfigurować WebService, który będzie się tym zajmował w naszej aplikacji po stronie serwera. To jest pierwsza różnica w stosunku do tego jak to wyglądało w ASP.NET MVC – tam nie było to konieczne. Wystarczyło stworzyć nową akcję w kontrolerze, zwrócić jako wynik JsonResult i już. Nie mając do dyspozycji MVC, musimy utworzyć i skonfigurować serwis (nie jest to co prawda jedyna opcja, ale ta wydaje się jak najbardziej odpowiednia – w szczególności w kontekście tego, do czego będziemy go używali). Po utworzeniu otrzymujemy przykładową metodę, którą użyjemy na początku do testów. Musimy ją także opatrzyć odpowiednim atrybutem [ScriptMethod(ResponseFormat = ResponseFormat.Json)], co pozwoli nam później wywoływać tę metodę po stronie klienta. Wpiszmy sobie w przeglądarce adres naszego web service’u (bez nazwy metody), a otworzy się nam strona z wylistowanymi metodami w danym serwisie. Gdy klikniemy zaś na nazwę metody by przetestować to jak działa, to zobaczymy… że to w cale nie JSON, tylko zwykły XML. Może być to trochę mylące, ale generalnie chodzi o to, że domyślnym sposobem wyświetlania zawartości serwisu, gdy nie zostało to stricte wskazane w nagłówku zapytania jest XML.

image

Rzecz kolejna jaka pozostała nam do zrobienia to przygotowanie kodu po stronie klienta. Kod ten zasadniczo wysyła zapytanie do serwisu, odbiera odpowiedź w formie JSON i coś z nią robi. Ot, taka prosta implementacja na potrzeby dalszej części wpisu:

$.ajax({
    type: "POST",
    url: 'SampleService.asmx/HelloWorld',
    data: '{}',
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function(msg) {
        $('#info').html(msg);
    },
    error: function(e, x) {
        $('#info').html("Błąd! Error Message: " + e + ", ErrorType: " + x);
    }
});

Mając już wszystko przygotowane możemy przejść do problemów xD

Issue #1: Wersja jQuery, a ASP.NET AJAX

Jeśli ciągle otrzymujecie wywołanie error do tego z kodem błędu parseerror, to zweryfikujcie wersję jQuery, której używacie. Ja miałem dość nieprzyjemny problem, bo korzystałem z wersji 1.5.1, która miała pewien bug z odbieraniem odpowiedzi JSON z serwisów ASP.NET –.–’. W wersjach 1.6 oraz 1.3 nie było tego problemu, ale chwilę zajęło zanim ustaliło się, że to akurat z tego wynika problem.

Tak przy okazji: w razie problemów i trudnością z ustaleniem gdzie leży problem: w serwisie lub też w kodzie JS, warto spróbować wywołać jakiś ogólnodostępny serwis JSON w celu przetestowania jedynie strony klienta. Taki przykładowy serwis to coś z Twittera, np: http://twitter.com/statuses/user_timeline/floubadour.json?count=2.

Issue #2: Serializacja prostych struktur danych

Po ilości wpisów tego typu, jakie udało mi się znaleźć w Internecie, zgaduję, że jest to dość powszechny błąd, o którym ludzie nie zdają sobie zwyczajnie sprawy. Wynika to trochę z niedoborów dokumentacyjnych działania Web Service i trochę jakby niedopowiedzenia ze strony Microsoftu. Weźmy sobie (bardzo mało ambitny) przykład po stronie serwera:

//example: HOW TO NOT IMPLEMENT JSON WEB SERVICE IN ASP.NET

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public string GetUsers(int? roleId)  
{
    try
    {
        var prms = new[] { new MySqlParameter("role_id", roleId.ToString()) };
        var res = ExecuteQuery("SELECT DISTINCT * FROM `users` U JOIN `userroles` UR ON (UR.`id_user` = U.`id_user`) WHERE UR.`id_role` = ?role_id", prms);
        var users = new object[res.Rows.Count];
        for (int i = 0; i < res.Rows.Count; i++)
        {
            users[i] = new { UserName = res.Rows[i]["UserName"].ToString(), Email = res.Rows[i]["email"].ToString() };
        }
        //for JSON.NET
        return JsonConvert.SerializeObject(users);
        //or for standard JSON Serializer in .NET:
        var serializer = new JavaScriptSerializer();
        return serializer.Serialize(users);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return null;
    }
}

Coś mniej więcej takiego można wyczytać z wszelkiej dokumentacji (w szczególności JSON.NET lub na StackOverflow), poza tym wygląda to całkiem logicznie, jednak jest to zupełnie niepoprawne podejście w połączeniu z serwisami ASP.NET. Niepoprawne, gdyż serwis ten sam w sobie zapewnia serializację, na którą mamy minimalny wpływ. W przykładzie powyżej zaś sami serializujemy obiekty do płaskiej formy ciągu znaków (nawet wskazujemy, że zwrócimy jedynie string z metody, co jest w większości przypadków nielogiczne). Poza pewnym logicznym udziwnieniem mamy też kolejny problem przy tym podejściu: mianowicie w wyniku naszej serializacji otrzymaliśmy w pełni poprawny JSON, ale zanim zostanie przesłany do klienta jest serializowany jeszcze raz, przez co cały nasz obiekt zamienia się jedynie w wartość klucza docelowego JSON-a ze wszystkimi tego konsekwencjami. Po stronie JavaScriptu nie jest to może wielki problem, ale wymusza to na nas wykonanie dodatkowego dekodowania danych. Jedno robione jest przez samo jQuery, a drugie musimy ręcznie wykonać na tym zserializowanym kluczu. Wydajność--;

Jak powinno się zatem robić? Wystarczy wyrzucić naszą serializację i zwrócić w takiej funkcji obiekt, który nas interesuje. Poprawny kod:

//example: GOOD APPROACH

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public object GetUsers(int? roleId)  
{
    try
    {
        var prms = new[] { new MySqlParameter("role_id", roleId.ToString()) };
        var res = ExecuteQuery("SELECT DISTINCT * FROM `users` U JOIN `userroles` UR ON (UR.`id_user` = U.`id_user`) WHERE UR.`id_role` = ?role_id", prms);
        var users = new object[res.Rows.Count];
        for (int i = 0; i < res.Rows.Count; i++)
        {
            users[i] = new { UserName = res.Rows[i]["UserName"].ToString(), Email = res.Rows[i]["email"].ToString() };
        }
        return users;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return null;
    }
}

Issue #3: The D

Należy także pamiętać o pewnej błahostce, która może dać się we znaki w razie migracji z ASP.NET 2.0 –> ASP.NET 3.5. Po migracji zauważymy, że wszystkie nasze skrypty nie będą działały, bo… zmieniła się struktura zwracanej odpowiedzi JSON. Różnicę przedstawię na podstawie odpowiedzi do poprzedniego (poprawionego) przykładu:

Wersja ASP.NET 2.0 oraz każda inna implementacja w każdym innym języku zwróci oczekiwaną odpowiedź:

{"Rows":[{"userName":"sasanka","id_user":1,"email":"lala@lala.pl"}]}

Tymczasem wersja 3.5 zwróci nam coś takiego:

{"d": {"Rows":[{"userName":"sasanka","id_user":1,"email":"lala@lala.pl"}]}}

Różnica niewielka. Pojawiło się po prostu d jako główny klucz, w którym zawiera się cała poprawna odpowiedź. Programiści w Microsofcie tłumaczą takie posunięcie względami bezpieczeństwa (tutaj jest to pokrótce wyjaśnione). Cóż… może mają rację, ale w dalszym ciągu mogli dać możliwość wykorzystania jakiejś flagi w ustawieniach, która powoduje, że wynikowy JSON będzie pozbawiony tych “bezpiecznych udziwnień”. Nic o takiej fladze niestety nie wiem.

Nam jako biednym programistom pozostaje stosowanie takich to magicznych zabezpieczeń w kodzie JavaScript:

success: function(msg) {  
    if (msg.hasOwnProperty("d")) 
        successFunc(msg.d); 
    else 
        successFunc(msg); 
},

Issue #4: Serializacja niestandardowych struktur danych

Dochodzimy do najmniej trywialnej części. Jeśli już zostało na nas wymuszone, że musimy używać standardowego serializera ASP.NET w serwisie, to co zrobić, gdy obiekt serializowany jest zbyt skomplikowany lub zawiera referencje cykliczne, przez co serializacja nie odniesie skutku? Generalnie nie jest problemem napisanie własnego konwertera, który specjalnie dla nas dokona serializacji uciążliwego obiektu, ale jak go podpiąć do standardowego serializera? Dokumentacja na ten temat milczy, a w Internecie niezbyt dużo pomocnych informacji. Może nikomu nie jest to potrzebne? Teoretycznie coś jest w tym stwierdzeniu, w szczególności, że wszędzie teraz stosujemy ORM-y, w szczególności te zgodne z POCO oraz lokalne obiekty z danymi. W takich przypadkach bowiem w ogóle nie jest nam potrzebna ingerencja w działanie web service’u. Ale zastanówmy się jaki jest tego sens. W takim standardowym przepływie pracy jaka musi zostać wykonana w przypadku stosowania ORM mamy minimum dwa wystąpienia mechanizmu refleksji w celu konwersji jednego rodzaju danych na drugi. Raz na obiekty z danych tabularnych, a raz z obiektów na dane… tabularne! Troszkę inne, ale w dalszym ciągu tabularne. Dopiszmy do tego, że refleksja jest bardzo złożonym i “ciężkim” mechanizmem to powinniśmy zrozumieć bezsens takiego podejścia.

Jeśli w naszej aplikacji już stosujemy ORM-a to chcąc - nie chcąc powinniśmy wykorzystać te dane do JSON w celu chociażby zgodności z zasadą DRY, ale jeśli nie mamy mapera relacyjno-obiektowego to będziemy chcieli zdecydowanie wykorzystać najprostsze metody do pobierania i konwersji danych. Najprostszą metodą do pobrania danych z bazy jest zapewne ADO.NET i zapisanie ich do DataTable. Jedyne czego nam brakuje to klasy konwertującej obiekt tego typu do JSON. Kod takich klas można znaleźć w referencjach (przy czym uwaga odnośnie JSON.NET – w tym pakiecie już domyślnie się znajduje obsługa tego typu obiektów, więc nie trzeba się przejmować).

image

Żeby jednak te konwertery były wykorzystane przy serializowaniu po stronie serwisu należy wyedytować plik web.config i dodać taką sekcję:

<system.web.extensions>  
   <scripting>
      <webServices>
         <jsonSerialization>
            <converters>
               <add name="DataTableConverter" type="ControlTester.WebExtensionsDataTableConverter, ControlTester" />
               <add name="DataRowConverter" type="ControlTester.WebExtensionsDataRowConverter, ControlTester" />
               <add name="DataSetConverter" type="ControlTester.WebExtensionsDataSetConverter, ControlTester" />
            </converters>
         </jsonSerialization>
      </webServices>
   </scripting>
</system.web.extensions>  

Dzięki temu powiększy się liczba obsługiwanych obiektów do konwersji, a przez to będziemy mieli ułatwioną możliwość pobierania i przekazywania danych do klienta. Tak nawiasem mówiąc, znalezienie informacji o tym nie należało do rzeczy prostych, a przecież jest to dla programisty o niebo bardziej interesująca wiadomość niż automatycznie wygenerowana dokumentacja na MSDN ;/

Mimo sporej ilości uciążliwości osiągnęliśmy niniejszym etap, w którym da się już korzystać z cudowności jaką jest JSON w ASP.NET :) W najbliższym czasie napiszę może o tym jak fajnie wykorzystać to czego dziś się nauczyliśmy do jakiejś przyjemnej aplikacji internetowej ;]

Referencje

JSON

JSON parse error – dyskusja

Never worry about ASP.NET AJAX’s .d again

A breaking change between versions of ASP.NET AJAX

DataTable JSON Serialization in JSON.NET and JavaScriptSerializer

Simple JSON using JavascriptSerializer and custom JavascriptConverter

ASP.NET web services mistake: manual JSON serialization