WCF: Data, Versioning en DTO's

Door CodeCaster op woensdag 25 januari 2012 23:45 - Reacties (7)
Categorie: Tech, Views: 5.217

In het vorige blog in deze 'serie' was te zien hoe je gemakkelijk een eigen datatype kunt gebruiken als invoer en uitvoer voor een WCF-service. Dit datatype gaat nu worden uitgebreid, zodat het bruikbaar wordt voor de postcodeservice die ik wil bouwen.

Ik heb de postcodedatabase van 6pp geÔmporteerd in SQL Express, heb de nodige relaties en indexen aangelegd en wil die data nu gaan gebruiken in de uiteindelijke applicatie.

Het probleem (tweede definitie) om externe data, in dit geval uit de database, in objecten te krijgen is op verschillende manier op te lossen. Ik heb uit gemak voor het Entity Framework gekozen. Dit was niet het beste alternatief, wat weer een blog op zich waard is, maar voor nu voldoet het.
Data
Een koppeling met een database is snel gemaakt. Rechtsklik op je project, kies voor Add -> New item... en selecteer uit de groep Data het item ADO.NET Entity Data Model. Geef het beestje een naam en doorloop de wizard om het datamodel vanuit de database te laten genereren. In de ideale wereld modelleren we eerst, maar het geluk die werkwijze te kunnen hanteren heb ik nog nooit mogen treffen.

Na het laten genereren van de datalaag en de daar tussen- en onderliggende koppelingen kun je deze heel eenvoudig in de code aanspreken. Je instantiŽert de zogeheten ObjectContext, en kunt vervolgens de zich daarin bevindende entities aanspreken. Concreet:
C#:
1
2
3
4
5
6
7
8
9
10
11
[OperationContract]
public List<Street> GetStreetsByName(Street needle)
{
    PostcodeModelEntities context = new PostcodeModelEntities();
            
    var result = context.Streets.Where(
                                    c => c.street1 == needle.street1
                                    );

    return result.ToList<Street>();
}



Zoals je aan het attribuut kunt zien, is dit een methode die via de service aangeroepen kan worden. De parameter van de methode, de gegevens waar een straat bij gezocht moet worden, is van het type Street, dat uit de datalaag komt.

Dit zorgt voor twee onoverkomelijke problemen. Je moet ten eerste middels het DataMember-attribuut de Street-klasse aanpassen, zodat de juiste members geserializeerd worden wanneer het object door de service wordt versuurd of ontvangen. Dit is niet aan te raden, want de Street-klasse wordt gegenereerd. Bij het opnieuw laten genereren, verdwijnen de attributen weer.

Ten tweede heb je in dit geval veel meer aan een 'platgeslagen' model, omdat je de client opzadelt met een model dat alle interne koppelingen met tabellen bevat, waar die client alleen maar wil zoeken op straatnaam.

Binnen de service wordt ook een Address-klasse gebruikt, met wat methodes om bijvoorbeeld postcodes te formatteren, en die verder eigenlijk over precies de voor deze situatie geschikte properties beschikt. Eigenlijk voldoet hij ook prima als retourobject, dus dit zou een potentiŽle signature kunnen zijn:
C#:
1
List<Address> GetAddressesByAddress(Address needle);



Wanneer dit het geval is, moet je rekening houden met het feit dat ieder Address-object dat via deze functie de applicatie binnenkomt, niet in deze applicatie tot leven is gekomen.

Het is bijvoorbeeld in dit geval gebruikelijk dat het object binnen de applicatie zijn eigen context bijhoudt, net als bijvoorbeeld referenties naar andere objecten, die beide na het ontvangen via bovengenoemde methode niet gevuld of zelfs niet bekend zijn. Je kunt, als zich bijvoorbeeld code die de datalaag aanspreekt in het object bevindt, niet simpelweg de Save()-method daarop aanroepen, omdat de koppeling met de database(laag) verbroken is.

Dit wordt veroorzaakt door bovengenoemde serializatie, maar in andere gevallen geldt dat je de client simpelweg niet kunt vertrouwen. Als je namelijk niet via de daarvoor bedoelde attributen aangeeft dat een bepaalde property verplicht is, kan de client succesvol een verzoek sturen zonder die property in het XML-bericht. De waarde die die property van dat object zal dan ook null zijn, waar je in je code zeker rekening mee dient te houden.

Maar wat voor object zou je dan wel moeten gebruiken? Zo dun mogelijk, het liefst met value types? Zo dus?
C#:
1
List<Address> GetAddressesByStreetAndCity(String street, String city);

Versioning
Het heeft zo z'n voor- en nadelen. Een voordeel is dat het haast niet duidelijker kan zijn welke parameters er meegestuurd kunnen worden. Geen geneste properties, gewoon een tweetal strings. Het grote nadeel hiervan is echter dat versioning lastig wordt. Als je namelijk nog op andere eigenschappen zou willen zoeken, bijvoorbeeld ook op land, moet je de signature van de method aanpassen, waardoor clients die niet op de hoogte zijn van deze update de methode nog op de oude manier aanroepen, en dus een keiharde fout terugkrijgen. Het toevoegen van een overload, een methode met dezelfde naam maar met andere parameters, is voor de onderhoudbaarheid noch voor het oog een pretje:
C#:
1
2
3
4
5
6
7
8
public List<Address> GetAddressesByAddress(string street, string city)
{
    return GetAddressesByAddress(street, city, null);
}

public List<Address> GetAddressesByAddress(string street, string city, string country)
{
    ...



En daarnaast kan het ook gewoon niet, althans, niet zonder de geŽxporteerde methodenaam aan te passen. Tijd voor een andere aanpak dus, we werken immers met berichten, niet met methodes.
Data Transfer Objects
Een DTO, waar het eerdergenoemde 'platgeslagen' ook op van toepassing is, wordt gebruikt om waarden in op te slaan op een manier die transport transparant maakt. Zodoende introduceren we een DTO, en bijbehorende signature:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
[DataContract]
public class AddressQuery
{
    [DataMember(IsRequired=true)]
    public String Street { get; set; }

    [DataMember(IsRequired=true)]
    public String City { get; set; }
}

[OperationContract]
List<Address> GetAddressesByAddress(AddressQuery needle);



Zo heb je ťťn method, die je tot in het oneindige kunt versionen, zolang je de members Street en City maar intact laat. Voeg een datamember toe, en oude clients (die vanaf het begin al werken met de klasse AddressQuery) kunnen gewoon hun verzoeken blijven sturen, terwijl je naar hartelust properties kunt toevoegen aan de klasse AddressQuery, zolang je ze maar niet verplicht maakt en controleert op null zijn.

Dit is natuurlijk niet toepasbaar op alle situaties. Blijf altijd waken voor YAGNI. Als je een service aan het bouwen bent die slechts door ťťn of een handvol clients geconsumeerd gaat worden, die wellicht ook nog eens onder jouw beheer vallen, kun je het hele versioning-aspect laten vallen, als je denkt dat dat ontwikkel- en onderhoudstijd scheelt.
Finally, the cool part
Nu een DTO en een methode zijn gedefinieerd, kan de logica worden geÔmplementeerd. Dat kan vrij eenvoudig:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public List<Address> GetAddressesByAddress(AddressQuery needle)
{
    using (PostcodeModelEntities context = new PostcodeModelEntities())
    {
        var data = context.Streets.Where(
                                        c => c.street1 == needle.Street &&
                                        c.Postcode.City.Citynames.FirstOrDefault().name == needle.City
                                        );

        // clients using wsdl < rev 40 won't send a Country
        if (!String.IsNullOrEmpty(needle.Country))
        {
            data = data.Where(d => d.Postcode.City.Province.Country.name == needle.Country);
        }

        return data.Select(s => (Adress)s).ToList();
    }
}


De FirstOrDefault() lijkt overigens niet op z'n plek bij de property Postcode.City.Citynames op regel 8, maar dankzij het geŽrfde datamodel hebben we een 'verkeerde' relatie van City naar Citynames: de tabel Citynames bevat een kolom city_id. Wie weet waarom een City geen cityname_id heeft mag het zeggen. Het voordeel van een real-life voorbeeld, uitdagingen die je in het echt ook kunt tegenkomen. ;)

Zoals je op regel 19 ziet, wordt er een expliciete cast van Street naar Address gedaan. Dat gebeurt met de volgende method, die zich in de klasse Address bevindt:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
public static explicit operator Address(Street input)
{
    Address result = new Address();

    result.Street = input.street1;
    result.City = input.Postcode.City.Citynames.FirstOrDefault().name;
    result.Country = input.Postcode.City.Province.Country.name;
// knip tig regels saaie code
    result.Latitude = input.lat;
    result.Longitude = input.lng;

    return result;
}


Dit vind ik echter geen fijne manier van werken. Ik wil namelijk geen veldnamen overtikken, als ik een dataobject naar een domeinobject wil omdopen. Met dat criterium ging ik op zoek, en heb ik AutoMapper gevonden.
AutoMapper
Er wordt geadviseerd om AutoMapper via NuGet te installeren (ohooh had je dat nog niet?). Automapper is naar eigen zeggen "A simple little tool", van veertig MB. Na de installatie opent m'n browser met een link naar ... de download van AutoMapper, die automatisch start. Tot dusver geen pluspunten.

Goed, middels een aantal klikken zijn we dan toch bij het moment suprŤme aangekomen. De volgende code zou zijn magie moeten tonen:
C#:
1
2
3
4
5
6
7
8
public List<Address> GetAddressesByAddressWithAutomapper(AddressQuery needle)
{
// knip querycode
            
    Mapper.CreateMap<Street, Address>();

    return data.Select(s => Mapper.Map<Street, Adress>(s)).ToList(); 
}


Maar helaas, een testrun doet uitwijzen dat de geretourneerde objecten allemaal leeg zijn. Op de property Postcode na dan, die de informatie uit .ToString() bevat, namelijk "PostcodeService.Models.Postcode". Niet bijzonder interessant.

Dit is te wijten aan de te verschillende namen van de properties. AutoMapper werkt prima wanneer de naamgeving consistent is, maar anders kan 'ie natuurlijk niet weten welke properties aan elkaar gekoppeld mogen worden. Gelukkig kunnen we een handje helpen:
C#:
1
2
3
4
5
6
7
8
Mapper.CreateMap<Street, Address>()
        .ForMember(street => street.Street, Street => Street.MapFrom(s => s.street1))
        .ForMember(street => street.City, street => street.MapFrom(s => s.Postcode.City.Citynames.FirstOrDefault().name))
        .ForMember(street => street.Country, street => street.MapFrom(s => s.Postcode.City.Province.Country.name))
// knip tig regels saaie code
        .ForMember(street => street.Latitude, street => street.MapFrom(s => s.lat))
        .ForMember(street => street.Longitude, street => street.MapFrom(s => s.lng))
        ;


Wait, what? Dit is alleen maar meer verbose, en ik ben alsnog handmatig properties aan elkaar aan het knopen. Geen verbetering dus, integendeel zelfs, dus geen optie. Hoe zou jij dit oplossen? Of is zelf typen in dit geval het handigst? :)



http://codecaster.nl/got/rmb/star1.pnghttp://codecaster.nl/got/rmb/star2.pnghttp://codecaster.nl/got/rmb/star3.pnghttp://codecaster.nl/got/rmb/star4.pnghttp://codecaster.nl/got/rmb/star5.pnghttp://codecaster.nl/got/rmb/stats.gif

Volgende: Communicatie deel 6: de vaatwasser 01-'12 Communicatie deel 6: de vaatwasser
Volgende: Spatiefouten 01-'12 Spatiefouten

Reacties


Door Tweakers user alwinuzz, donderdag 26 januari 2012 01:41

Automapper lijkt me alleen handig als je goede conventies hebt, waarbij Automapper alles zelf kan verzinnen. Met zo'n "geŽrfd datamodel" heb je er echt geen zak aan :)
Je laatste stukje voorbeeldcode schiet in ieder geval z'n doel voorbij.

Onderstaand stukje lijkt mij gewoon het duidelijkst. Je zal toch ergens de omslag van oud model naar nieuw model moeten maken. Dit is zonder magic strings en heel erg duidelijk voor je eventuele collega's.

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
public static explicit operator Address(Street input) 
{ 
    Address result = new Address(); 

    result.Street = input.street1; 
    result.City = input.Postcode.City.Citynames.FirstOrDefault().name; 
    result.Country = input.Postcode.City.Province.Country.name; 
// knip tig regels saaie code 
    result.Latitude = input.lat; 
    result.Longitude = input.lng; 

    return result; 
}


Enige dingetje: ik zou zelf geen operator doen maar een Street.ToAdress() of Adress.FromStreet(street), of een constructor new Adress(street) (ligt eraan of je Street mag veranderen).

Leuke post!
Hopelijk hier geen spatie fouten :+


PS

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<Address> results = new List<Address>(); 
    ...
    foreach (Street s in data) 
    { 
        Address a = (Address)s; 
        results.Add(a); 
    } 
// Waarom geen
    var results = data.Select(s => (Adress)s).ToList();
// of
    var results = data.Select(s => s.ToAdress()).ToList();
// ?

    return results;


foreach is zůů C# 2.0 :)

[Reactie gewijzigd op donderdag 26 januari 2012 01:49]


Door Tweakers user asfaloth_arwen, donderdag 26 januari 2012 07:58

Automapper kan wel werken met intellisense ;)

C#:
1
2
Mapper.CreateMap<Street, Address>() 
        .ForMember(dst => dst.Street, options => options.MapFrom(src => src.street1))



Verder ben ik wel benieuwd naar goede ideeŽn om goed te kunnen mappen, aangezien ik hier vaak ook naar op zoek ben :)

[Reactie gewijzigd op donderdag 26 januari 2012 08:02]


Door Tweakers user Guldan, donderdag 26 januari 2012 09:18

Ik werk zelf met Webservice Software factory. Die heeft een z.g.n Guidance Navigator hulpmiddel voor een translator klasse. Hierin kun je via dropdowns aangeven welke velden van de een naar de andere geconverteerd moeten worden.

Echter werkt dit ook niet wanneer de velden van een verschillend type zijn en zul je dit handmatig moeten doen. Maar het neemt je iig wat werk uit handen.

Door Tweakers user CodeCaster, donderdag 26 januari 2012 09:26

Bedankt voor de reacties! De suggesties uit de eerste twee zijn doorgevoerd. :)

Ik had al vaker van de Service Factory gehoord, ik zal er wanneer ik verder ga eens naar kijken.

[Reactie gewijzigd op donderdag 26 januari 2012 09:26]


Door Tweakers user kipusoep, donderdag 26 januari 2012 13:54

Zou je geen using om je ObjectContext instantiŽring heen zetten?

using(PostcodeModelEntities context = new PostcodeModelEntities())
{
// Do stuff
}

Door Tweakers user CodeCaster, donderdag 26 januari 2012 14:55

Dat is inderdaad een nuttige toevoeging. Ik heb de code hier niet bij de hand, zal het later updaten.

Door Tweakers user Deathraven, donderdag 26 januari 2012 17:41

Je gaat dan wel niet echt in op waarom EF niet zo'n handige keuze was. Maar je linked wel naar ORMBattle.net. Ik zou niet teveel waarde hechten aan deze website...

http://ormbattle.net/inde...nt-believe-oren-eini.html

Overigens kun je met AutoMapper ook derived types automatisch laten mappen en andere ongein. Moet je anders maar eens naar de DynamicMap methode kijken.

Verder prima blogpost :)

[Reactie gewijzigd op donderdag 26 januari 2012 17:42]


Reageren is niet meer mogelijk