En enkel SOA komponent


søndag 12. april 2009 Diverse prosjekter

SOA, tjenesteorientert arkitektur, er et utvannet og mye missbrukt begrep. For meg dreier det seg først og fremst om å lage små, mer eller mindre uavhengige tjenester, som kan kombineres og sammarbeide for å lage større løsninger. Den siste tiden har jeg laget en rekke slike små tjenestekomponenter. Jeg bruker omtrent den samme fremgangsmåten hver gang, og fikk derfor lyst til å dele dette mønsteret her.

SOA Component

BrainSpill.CoreDet viktigste prinsippet mitt når jeg lager slike komponenter er enkelhet. Jeg lager en liten, no fuss komponent, som ikke vet noe om verden bortsett fra akkurat det tjenesten skal tilby. Den eier sine egne data, og er så generisk at den i prinsippet kan brukes til mange ting.

Du vil se eksempler på bruk av db4o, LINQ, Ninject og WCF i denne artikkelen. Eksempelet jeg skal vise er en komponent jeg har kalt BrainSpill - en tjeneste hvor man kan lagre og hente ut meldinger. Jeg har planer om å bruke BrainSpill til å lage en twitter-lignende tjeneste til bruk internt på jobben. Hvis du ser for deg en hjerne som flyter over av tanker og ideer som bare må deles med andre så skjønner du sikkert hvordan navnet på tjenesten oppstod.

Jeg begynner med å lage et interface hvor jeg definerer hva tjenesten skal tilby. Jeg velger å gjøre dette i et eget class library prosjekt jeg kaller for BrainSpill.Core. IMessageRepository skal ta imot nye meldinger, gi tilgang til å slette meldinger, hente ut de siste n meldingene, og fortelle hvor mange meldinger som totalt er lagret i jenesten. (Dette er første versjon, og interfacet vil nok vokse noe etterhvert.)

    7 [ServiceContract]

    8 public interface IMessageRepository

    9 {

   10     [OperationContract]

   11     void AddMessage(AddMessageRequest messageData);

   12

   13     [OperationContract]

   14     void DeleteMessage(MessageId id);

   15

   16     [OperationContract]

   17     IEnumerable<Message> GetLatestMessages(int numberOfMessages);

   18

   19     [OperationContract]

   20     int GetTotalNumberOfMessages();

   21 }

Jeg dekorerer interfacet med ServiceContract og metodene med OperationContract, slik at det kan publiseres vha. Windows Comunication Framework (WCF).

Legg merke til at jeg bruker tre andre klasser her: Message er en komplett melding lagret i tjenesten. AddMessageRequest holder dataene som brukes til å opprette en ny melding; en klient av tjenesten har ikke lov til å opprette fullverdige meldinger selv - det er f.eks. tjenesten som gir meldingen en id, lagringstidspunkt m.m. Jeg har også valgt å lage en egen klasse for meldingens id: MessageId. Denne er i prinsippet kun en wrapper for en Guid. Alle disse klassene dekoreres med DataContract-atributtet, og properties som skal være tilgjengelig på klientsiden dekoreres med DataMember.

Når alt dette er på plass koder jeg en konkret implementasjon av IMessageRepository. For tiden bruker jeg objektdatabasen db4o for lagring - det er absolutt det enkleste. Jeg gir derfor OdbMessageRepository en konstruktør som tar inn en IObjectContainer (en db4o database).

   15 public class OdbMessageRepository : IMessageRepository, IDisposable

   16 {

   17     private IObjectContainer _database;

   18

   19     public OdbMessageRepository(IObjectContainer database)

   20     {

   21         _database = database;

   22     }

Deretter implementerer jeg metodene definert i IMessageRepository. Takket være db4o er dette ganske enkelt. Jeg benytter en del LINQ, og jeg vet ikke om koden jeg har skrevet her er optimal (sansynligvis ikke), men eksperimentering har vist at det er godt nok for meg i denne omgang.

   24 public void AddMessage(AddMessageRequest messageData)

   25 {

   26     Message messageToAdd = new Message(messageData);

   27     _database.Store(messageToAdd);

   28 }

   29

   30 public void DeleteMessage(MessageId id)

   31 {

   32     _database.Delete(GetById(id));

   33 }

   34

   35 public IEnumerable<Message> GetLatestMessages(int numberOfMessages)

   36 {

   37     var query = from Message m in _database

   38                 orderby m.Created descending

   39                 select m;

   40

   41     return query.Take(numberOfMessages);

   42 }

   43

   44 public int GetTotalNumberOfMessages()

   45 {

   46     var query = from Message m in _database

   47                 select m;

   48

   49     return query.Count();

   50 }

   51

   52 private Message GetById(MessageId id)

   53 {

   54     var query = from Message m in _database

   55                 where m.Id == id

   56                 select m;

   57

   58     return query.First();

   59 }

Du la kanskje merke til at jeg lot OdbMessageRepository arver fra IDisposable? Objektdatabasen er nemlig en ressurs som må frigjøres på riktig måte. Derfor implementerer jeg Dispose().

   61 #region IDisposable Members

   62

   63 private bool _isDisposed;

   64

   65 public void Dispose()

   66 {

   67     Dispose(true);

   68     GC.SuppressFinalize(this);

   69 }

   70

   71 private void Dispose(bool disposing)

   72 {

   73     if (_isDisposed)

   74     {

   75         return;

   76     }

   77     _isDisposed = true;

   78

   79     if (disposing)

   80     {

   81         if (_database != null)

   82         {

   83             _database.Dispose();

   84             _database = null;

   85         }

   86     }

   87 }

   88

   89 #endregion

BrainSpill.ServiceNå er kjernefunksjonaliteten i BrainSpill ferdig implementert. Jeg legger så til et nytt prosjekt for selve windows tjenesten: BrainSpill.Service. Jeg oppretter BrainSpillService som arver fra System.ServiceProcess.ServiceBase.

Selv om det kanskje er overkill i et så lite prosjekt som dette har jeg lagt meg til vanen å alltid bruke dependency injection, og tjenesten oppretter derfor en IoC container i konstruktøren. Jeg bruker som regel Ninject, som tar inn en DependencyModule - mer om den om litt..

    9 public partial class BrainSpillService : ServiceBase

   10 {

   11     public const string SERVICE_NAME = "BrainSpillService";

   12

   13     private IKernel _IocContainer;

   14     public ServiceHost _serviceHost = null;

   15

   16     public BrainSpillService()

   17     {

   18         _IocContainer = new StandardKernel(

   19             new BrainSpillServiceDependencyModule());

   20

   21         ServiceName = SERVICE_NAME;

   22     }

   23

   24     protected override void OnStart(string[] args)

   25     {

   26         if (_serviceHost != null)

   27         {

   28             _serviceHost.Close();

   29         }

   30         _serviceHost = new ServiceHost(

   31             _IocContainer.Get<IMessageRepository>());

   32         _serviceHost.Open();

   33     }

   34

   35     protected override void OnStop()

   36     {

   37         if (_serviceHost != null)

   38         {

   39             _serviceHost.Close();

   40             _serviceHost = null;

   41         }

   42     }

   43

   44     protected override void Dispose(bool disposing)

   45     {

   46         if (disposing)

   47         {

   48             if (_IocContainer != null)

   49             {

   50                 // note: disposing the kernal also

   51                 // disposes the repository if needed

   52                 _IocContainer.Dispose();

   53                 _IocContainer = null;

   54             }

   55         }

   56

   57         base.Dispose(disposing);

   58     }

   59 }

Koden er nokså standard for en windows service. Jeg overstyrer OnStart, hvor jeg oppretter en ny WCF ServiceHost. Jeg bruker Ninject til å opprette denne for meg, og spesifiserer kun navnet på interfacet, nemlig IMessageRepository. I OnStop stenger jeg ned den samme hosten.

Jeg overstyrer også tjenestens Dispose-metode, hvor jeg passer på å frigjøre IoC containeren. Den vil da ta seg av å kalle Dispose på alle objekter den har opprettet, som i dette tilfellet er OdbMessageRepository (som igjen kaller Dispose på db4o databasen).

Som sagt brukte jeg en Dependency-modul da jeg opprettet IoC containeren - koden for den ser du nedenfor. Jeg arver fra Ninject's StandardModule, og overstyrer Load, hvor jeg definerer avhengighetene jeg trenger. I dette tilfellet er jeg kun interessert i å få opprette en IMessageRepository. Ved å bruke Ninject's fluent interface sier jeg at når jeg ber om en IMessageRepository så vil jeg ha en instans av OdbMessageRepository. Jeg spesifiserer også at Ninject skal bruke Sinleton-oppførsel, hvilket vil si at den alltid bruker én og samme instans av klassen.

Til slutt forteller jeg hvilken parameter Ninject skal sende inn i konstruktøren til OdbMessageRepository. Jeg sier at argumentet som heter "database", av type IObjectContainer, skal hentes fra metoden OpenDatabase(). Og OpenDatabase bruker db4o's factory til å åpne basen fra en fil (eller opprette den om den ikke finnes fra før) basert på en filsti definert i configfilen.

   10 public class BrainSpillServiceDependencyModule : StandardModule

   11 {

   12     public override void Load()

   13     {

   14         Bind<IMessageRepository>()

   15             .To<OdbMessageRepository>()

   16             .Using<SingletonBehavior>()

   17             .WithArgument<IObjectContainer>("database", OpenDatabase());

   18     }

   19

   20     private static IObjectContainer OpenDatabase()

   21     {

   22         return Db4oFactory.OpenFile(ConfigurationManager.AppSettings["database"]);

   23     }

   24 }

Det gjenstår noen steg for å få WCF til å fungere orntlig; jeg må definere adressen og bindingen - dette gjør jeg i configfilen.

CropperCapture[31]

Det irriterer meg at jeg må spesifisere den konkrete implementasjonen av interfacet her - OdbMessageRepository - men foreløpig vet jeg ikke om jeg kan unngå dette.

Når servicen er ferdig kompilert og installert BrainSpill, kan jeg nå legge til en service-referanse i klientprosjekter ved å referere til http://localhost:8000/BrainSpill/service/mex.

En liten detalj jeg oppdaget første gang jeg lagde en service som dette er at jeg også er nødt til å spesifisere InstanceContextMode for WCF tjenesten. Dette er pga. måten jeg oppretter ServiceHosten på - ved å gi inn en instans (fra IoC containeren) i stedet for en type (ref. BrainSpillService, linje 30 og 31). Dette er kanskje ikke ideelt, men inntil videre gjør jeg det slik, ved å spesifisere InstanceContextModeOdbMessageRepository:

   10 ///

   11 /// InstanceContextMode = InstanceContextMode.Single is needed because of the way

   12 /// the service is created (with an IoC container).

   13 ///

   14 [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]

   15 public class OdbMessageRepository : IMessageRepository, IDisposable

Nå er det ikke så mye igjen.., men for at alt skal fungere må jeg implementere en enkel Installer-komponent for windows servicen. Den ser slik ut:

    8 [RunInstaller(true)]

    9 public partial class BrainSpillInstaller : Installer

   10 {

   11

   12     private ServiceProcessInstaller _process;

   13     private ServiceInstaller _service;

   14

   15     public BrainSpillInstaller()

   16     {

   17         _process = new ServiceProcessInstaller();

   18         _process.Account = ServiceAccount.LocalSystem;

   19

   20         _service = new ServiceInstaller();

   21         _service.ServiceName = BrainSpillService.SERVICE_NAME;

   22

   23         Installers.Add(_process);

   24         Installers.Add(_service);

   25     }

   26 }

Og så må jeg ha en static void Main() som fyrer igang hele "sulamitten". Jeg plasserer den for seg selv i Program.cs.

   11 static void Main()

   12 {

   13     ServiceBase.Run(new BrainSpillService());

   14 }

Nå er jeg klar til å kompilere, og servicen er ferdig. For å ta den i bruk må den installeres og startes, og jeg lager vanligvis batch-filer for dette, sånn at jeg enkelt kan både installere og starte, og stoppe og avinstallere. Merk at du kan få problemer med servicen om du ikke alltid gjør dette i riktig rekkefølge. Om du forsøker å oppdatere servicen uten å stoppe og avinstallere først, kan det hende du må restarte maskinen for å få fjernet servicen.

Install.bat:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallUtil BrainSpill.Service.exe

Start.bat:

net start BrainSpillService

Stop.bat:

net stop BrainSpillService

Uninstall.bat:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallUtil /u BrainSpill.Service.exe

Så slik lager jeg tjenester for tiden. Beskrivelsen ble kanskje litt lang, men prinsippet er veldig enkelt. Spesielt fører bruken av objektdatabase (db4o) til at det blir lite kode. Har du synspunkter på denne fremgangsmåten, eller spørsmål, er du velkommen til å legge igjen en kommentar.

Knagger: , , , , ,


comments powered by Disqus