ASP.NET címkéhez tartozó bejegyzések

WCF Web API bevezető

Manapság a fejlesztők nap mint nap új kihívásokkal szembesülnek azzal kapcsolatosan, hogyan tegyék közkincsé adataikat, szolgáltatásaikat mások számára. Én például évek óta folyamatosan ezen dilemmák tengerében hánykolódok, és nem véletlenül. Általában amikor neki kezdek egy projekt fejlesztésének, mindig messzemenő távlatokban próbálok gondolkodni, túl a határain a jelenlegi eszközöknek, platformoknak. Mondhatom, hogy egyenlő partnert szeretnék látni minden fejlesztőben, legyen az .NET, Java, PHP, Delphi, C++, Objective-C, Visual Basic, vagy bármilyen fejlesztő.

Én azt szeretném, hogy ezek mindegyike egyformán részesülhessen a munkám gyümölcséből, azaz nyitni szeretnék egy új, szélesebb világ felé, nem pedig ahogy Anders Heijlsberg is fogalmazott, egy fekete dobozba bezárkózni, és ezáltal gyakorlatilag a játszóterem határait súlyosan megszabni, ahol ráadásul magam vagyok saját magam gátja, vagy még drámaiasabban, ahova csak azt engedem be, akit személyesen ismerek és csak azzal a feltétellel, hogy vigye az egész játszóteret, a saját játszóterét pedig égesse fel, mondván: “itt az enyém, az jobb lesz Neked”. Arról már nem is beszélve, hogy melyik a költségtakarékosabb megoldás mindkét fél számára.

Fejlesztői szempontból megvilágítva a problémát: ha van egy dektop, mobil, vagy webes alkalmazásom, ami tud egy kis millió nagyszerű dolgot, üzleti szempontból értékes szolgáltatásokat nyújt, nagy rá a kereslet a piacon (rengeteg van ilyen jelenleg), akkor érdemes elgondolkozni azon, hogy ezt ne csak mint egy kézzel fogható csomagolt terméket (fekete doboz hasonlat), hanem mint szolgáltatást is elérhetővé tegyem mások számára, amiből szintén én profitálhatok, de úgy, hogy közben másokat se kell rákényszerítenem semmi fajta lemondásra, lásd például Facebook API, Twitter API, Google API, Microsoft Live, stb.

Ezzel csak azt akartam érzékeltetni, hogy igenis ideje lenne megnyílni a kisebb, sőt mikrovállalatoknak is affelé, hogy ami értéket teremtenek, azt a teljes világ felé megtegyék egyúttal. Ehhez természetesen hozzátartozik nem pusztán a szolgáltatásban, mint fogalomban való gondolkodás, hanem más infrastruktúrális elvárások is, belevéve a globalizációt és lokalizációt egyaránt, a multiplatform támogatást, stb., stb.

A probléma gyökerei mélyre nyúlnak és nagyon szerteágazóak. Nem pusztán a fejlesztési idő, ill. a költségei nőnek a fejlesztésnek – beleértve a humán erőforrásigényt is, hanem kismillió infrastruktúrális problémával szintúgy meg kell ütközni, amik akár a különböző implementációkból is adódhatnak. Én például régebben még Delphi-s időkben erősen megütköztem a Borland SOAP implementációjával, ami hemzsegett nemcsak a bugoktól, hanem az implementációból fakadó hiányosságoktól is. De akkor most jussunk el odáig, hogy WCF:

A WCF a kezdetektől fogva egy igen erőteljes eszköz, mondhatni  szinte egységesítette az összes kommunikáiós platformot mára. Tényleg fantansztikus látni, hogy a fejlesztőnek csak interfészeket, osztályokat, OOP szemléletű módszertanokat kell alkalmaznia, és minden más alacsonyszintű implementációt elrejt a szeme elől a framework. Az egyetlen nagy probléma idáig az volt vele, hogy az MS teljesen megfeledkezett egy nagyon fontos dologról az évek során, amit úgy hívnak, hogy Web. Miközben a Google erőteljesen átállt a REST alapú megoldásokra és mára mondhatjuk, hogy teljesen elhajította a régi SOAP-os szolgáltatásait, továbbá a web alapú megoldásaival + szolgáltatásaival egy páratlan nagyságrendű piacot hasított ki magának, addig a nagy Microsoft ült és bámult ki a bamba fejéből újjal mutogatva, meg legyingetve egy olyan dologra, amiről már a kezdetek kezdetén tudta mindenki, hogy valami órási nagy dolog van születőben. Ezt még megtetőzte most a Facebook osztatlan sikere is, és végül elértük hála Istennek azt a küszöb értéket, amikor az MS is beszáll egy igen erőteljes eszközzel erre a piacra, ez pedig az új születőben lévő: WCF Web API.

Még mielőtt belemennék a technikai részletekbe, kielemezve, hogy hol tart ez a projekt és merre felé halad tovább, érdemes említést tenni arról, hogy mi volt a probléma a korábbi megoldásokkal. Először is nézzük, miben kellett gondolkoznunk korábban amikor egy WCF szolgáltatáson keresztül valamilyen protokollt (http, https, stb.) szerettük volna publikálni az adatainkat:

Először is, volt az első és legalapvetőbb (személyes eddigi kedvencem), a sima WCF szerviz SOAP endpointon keresztül publikálva. Nagy előnye, hogy teljes uralom van minden felett, beleértve az üzeneteket, a biztonságot, a tranzakciókat, az egyéb rendszerekkel való interoperabilitást, a testreszabhatóságot, stb. További előnye, hogy könnyen fogasztható bármilyen desktop alkalmazásból, ami támogatja az adott verziójú SOAP specifikációt. Általában a fejlesztői eszközöknek van hozzá saját proxy generátoruk, ami szinte egy az egybe odavarázsolta nekünk a szerver implementációt. Sőt .NET kliens proxyk esetében ha a DataContract-ok külön assembly-be vannak fordítva, akkor megoszthatók a kliensel kipipálva ezt az opciót a kliens proxy generálásakor, így a validációs attribútumok és lokalizációs erőforrások is megosztva vannak a kliens és a szerver között, magyarul egy gonddal kevesebb. Egy nagy hátránya, hogy mivel ma már minden a Web-ről szól és szinte minden a böngészőben zajlik, iszonyat körülményes használni kliens oldalon JavaScript-ből és legfőképp, egy kliens oldali JavaScript API-t építeni rá. További hátránya a REST szolgáltatásokkal szemben amit sokan felhoznak még, az automatikus cachelés hiánya. Egyesek úgy látják, hogy ha ők WCF REST vagy WCF Web API szolgáltatásokat készítenek ezt a problémát megoldják, én nem így látom. Tény, hogy ez az előnye megvan az ezen típusú szolgáltatásoknak, de szerveroldalon még mindig szükség van egy átfogó, osztott cache megoldásra (pl. AppFabric), ami garantálja a megfelelő teljesítmény elosztást bevonva akár több gépet is ebbe a folyamatba. Mellesleg statikusan renderelt HTML tartalomra is szükség van az esetek jórészébe, ami SEO szempontjából nem utolsó szempont, és azt nem biztos, hogy a legjobb ötlet REST szervizen átküldeni az ASP.NET MVC alkalmazásba, hogy onnan tovább küldjük a böngésző felé. De jegyzem meg, ilyen fajta támogatás is van a WCF Web API-hoz. Van egy saját fejlesztésű HTTP kliens, ami megkönnyíti az URL mögötti erőforrások (JSON, XML, stb.) deszerializációját bármilyen kliens számára. De ha még fírtatjuk a hátrányokat, amiket lehet sorolni, a WCF-nek van egy szép szolgáltatása, amit úgy hívnak, hogy steaming. Egyértelmű, hogy nagyméretű adatokat, fájlokat ezen keresztül érdemes felküldeni. Egyetlen szépséghibája, hogy egy web app-ból ezt is körülményes használni. Ugyanennek az elérése az új WCF Web API-val gyerekjáték és nincs az az agyalágyult, nyakatekert, kretén megközelítése, mint pl. a WCF Data Service-nek és akkor most térjünk is át rá:

WCF Data Services (Astoria): Mondhatnám, hogy kezdetben vala az Ige, hogy kúrjunk ki mindent úgy ahogy van, mi szem szájnak ingere. Aztán utólag toldozzuk, foldozzuk mi nééékünk nem kölllene, de ha már valami komolyabb dolgot szeretnék, szarjuk szét az egészet, írva 800 sor baromságot a meghackelésére. Röviden és tömören így lehetne összefoglalni ezt a tömör gyönyört iránta. De félretéve a tréfát, célját tekintve ez nem az, ami nekünk kell. Arra kiválóan alkalmas, ha gyorsan össze kell dobni valamit, még JSON formatter is van hozzá, sőt WPF kliensből is gyönyörűen fogyaszható kliens change trackingel, de a Web API paradigmáihoz képest csak egy óvodás szintjével ér fel.

Harmadik, amit aztán tényleg rühellek, az a WCF RIA Services. Na ez az amit az életbe soha nem fogok használni. Ízig vérig rám akarja erőltetni a saját modelljét, sőt még MS platformon belül is gátakat szab, hiszen WPF kliens támogatásnak se híre, se hamva nem volt amikor én ezt utoljára láttam. A legnagyobb baj ezekkel a megoldásokkal, mint pl. a RIA Services, Data Services, hogy leginkább a nyers adatra és azok direkt fogyasztására fektetik a hangsúlyt, mintsem egy rendszerezett, logikailag jól felépített megvalósításra, számba véve az egyéb erőforrások ugyanilyen könnyed módon történő fogyasztását, lásd. fájl feltöltés, letöltés, streaming, stb. esete.

Negyedik a WCF REST, ami mindenre használható, csak web fejlesztésre nem. Ez ugyan már közeledik egy normális REST API-hoz, de még mindig nagyon alacsony szintű és szét kell hackelni a WCF-et egy egyszerű feladat megvalósításához is, mint pl. API kulcs validáció, különböző média formátumok támogatása (JSON, XML, plain/text, vagy amilyet csak akarunk Web API esetében), OAuth 2.0 támogatás, stb. De még idáig el se kell jutnunk, már fejbe vagyunk lőve ott, hogy az MS által implementált fantasztikus DataContractJsonSerializer nem kezeli a körkörös referenciákat, sőt még exception-t is dob amikor a DataContract attributumhoz hozzáírja valaki az IsReference = True tulajdonságértéket, aminek épp az lenne a célja, hogy ezt a prolémát feloldja.

Egy kis kitéről: Hogyan lehet körkörös referenciát gyártani?
Amikor POCO objektumokkal dolgozunk, használva az Entity Framework Code First fantasztikus szolgáltatásait, aminek épp az lenne a lényege, hogy végre modellezhetjük az üzleti elvásároknak megfelelő reprezentációját az adatoknak egy halom DTO osztály gyártása és mappelése helyett, ő szépen betölti az navigációs tulajdonságokon elérhető objektumokat (feltével ha szeretnék, de szeretnék, mert le szeretnénk küldeni), ő szépen azokon is beállítja a referenciákat az alap objektumra. Kész is, megint hackelni kell azt, aminek magától értetődőnek kellene lennie, mert nem lehet JSON-ba szerializálni. Mellesleg itt jegyzem meg, hogy az EF CF-nek Beta állapotban van egy újabb nagyszerű szolgáltatása, melynek neve EF CF Migrations, amely automatizáltan képes követni a változásokat a POCO osztályok kódjaiban és ez alapján upgradelni, sőt downgradelni is képes az adatbázist. Van hozzá C# kódgenerátor is, ami képes ezeket a műveleteket C# kóddal is megírni a hagyományos direkt SQL helyett. Még elég kezdetleges, nálam egy bonyolultabb objektumgráf esetén meghal.

Végre rátérhetünk ténylegesen a WCF Web API-ra, de az már nem ma lesz, mert egy könyvet tudnék írni erről, úgyhogy csak lépésről lépésre. De ha már így említettem a JSON támogatás hiányát (ami természetesen Web API-nál is adott a frameworkbe épített standard MS serializer miatt), és ha szeretné valaki kipróbálni addig is a cuccot, egy kicsit megreformáltam a JSON.NET -hez már mások által készített Media Type Formattert, így most JSONP-t is támogat közkívánatra, csak Nektek:

namespace Microsoft.ApplicationServer.Http
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Http.Formatting;
    using System.Net.Http.Headers;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Bson;

    public class JsonNetMediaTypeFormatter : MediaTypeFormatter
    {
        private readonly JsonSerializerSettings serializerSettings;

        public JsonNetMediaTypeFormatter()
            : this(new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore })
        {
        }

        public JsonNetMediaTypeFormatter(JsonSerializerSettings serializerSettings)
        {
            Contract.Requires(serializerSettings != null);

            this.serializerSettings = serializerSettings;
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/bson"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/javascript"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/ecmascript"));
        }

        protected override void OnWriteToStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext context)
        {
            var serializer = JsonSerializer.Create(this.serializerSettings);

            // NOTE: we don't dispose or close the writer as that would 
            // close the stream, which is used by the rest of the pipeline.
            var writer = GetWriter(contentHeaders, stream);

            var values = default(IEnumerable<string>);
            if (contentHeaders.TryGetValues("jsonp-callback"out values))
            {
                var callback = values.First();
                writer.WriteRaw(callback + "(");
                writer.Flush();
                serializer.Serialize(writer, value);
                writer.WriteRaw(")");
            }
            else
            {
                serializer.Serialize(writer, value);
            }

            writer.Flush();
        }

        protected override object OnReadFromStream(Type type, Stream stream, HttpContentHeaders contentHeaders)
        {
            var serializer = JsonSerializer.Create(this.serializerSettings);
            var reader = GetReader(contentHeaders, stream);
            var result = serializer.Deserialize(reader, type);
            return result;
        }

        private static JsonReader GetReader(HttpContentHeaders contentHeaders, Stream stream)
        {
            var mediaType = contentHeaders.ContentType.MediaType;

            if (mediaType.EndsWith("json") ||
                mediaType.EndsWith("javascript") ||
                mediaType.EndsWith("ecmascript"))
            {
                return new JsonTextReader(new StreamReader(stream));
            }
            else
            {
                return new BsonReader(stream);
            }
        }

        private JsonWriter GetWriter(HttpContentHeaders contentHeaders, Stream stream)
        {
            var mediaType = contentHeaders.ContentType.MediaType;

            if (mediaType.EndsWith("json") ||
                mediaType.EndsWith("javascript") ||
                mediaType.EndsWith("ecmascript"))
            {
                return new JsonTextWriter(new StreamWriter(stream));
            }
            else
            {
                return new BsonWriter(stream);
            }
        }
    }
}

A JSON.NET egyébként egy kitűnő JSON lib, ami támogat camelCase formázást is például, ami szerintem jobban követi a JavaScript standardet, legalábbis ahogy a nagy cégek API-jait nézem, tud ISO dátumba formázni amit pl. az ASP.NET MVC JsonResult nagyon nem szeret megtenni, ezáltal szintén hackelni kell, hogy vissza lehessen postolni adatot scriptből hiba mentesen, használni pedig csak annyi WCF Web API-ból, hogy a Global.asax-ban a WebApiConfiguration példány “Formatters” kollekciójához hozzáadunk egy példányt:

var catalog = new AggregateCatalog(
    new AssemblyCatalog(typeof(Global).Assembly),
    new AssemblyCatalog(typeof(RepositoryBase).Assembly));

var container = new CompositionContainer(catalog);
var config = new MefConfiguration(container);

config.EnableHelpPage = true;
config.EnableTestClient = true;
var settings = new JsonSerializerSettings();
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
settings.Converters.Add(new IsoDateTimeConverter());
config.Formatters.Insert(0, new JsonNetMediaTypeFormatter(settings));

RouteTable.Routes.SetDefaultHttpConfiguration(config);

Ezt most így kiollóztam, azért olyan amilyen. A MefConfiguration egy WebApiConfiguration származék, csak ezt használtam, mert a Managed Extensibility Frameworkot használom most épp dependency injection-hoz, ami egyébként jóval több ennél, de még akár erre is használható. Ettől függetlenül az MS Unity-vel is szépen megoldható, de mivel úgy látom most az MS a MEF-re szállt rá, akkor legyen.

Szerintem ennyi információ indulásnak elég. Tehát látszik már most kapásból, hogy szépen bővíthető, testreszabható az egész Web API. Saját média típus formatterek, saját HTTP message, exception, response handlerek injektálhatóak bele, stb., stb. Auto formáz xml, json, html, plain text-be. Van már JSONP támogatás, készülőben van az OAuth 2.0 támogatás, sőt már a Facebook OAuth 2.0 támogatás is ott figyel a legfrissebb kódokban, úgyhogy hajrá skacok! Én meg jó szokásomhoz híven hajnali 3-kor ismért ágynak dőlök. Jó éjt! 🙂