Bob Swart (aka Dr.Bob)
Delphi 7 Web Services migreren naar ASP.NET

ASP.NET biedt de mogelijkheid tot het implementeren van Web Services, en Delphi for .NET zal uiteraard ASP.NET Web Services ondersteunen. Maar Delphi 6 en 7 bieden ook al ondersteuning voor Web Services, maar dan zonder gebruik te maken van ASP.NET. Op zich geen probleem, behalve als je straks bestaande Web Service code wilt migreren van Delphi 6 of 7 naar Delphi for .NET, want daar komt wat meer bij kijken.

Dit artikel beschrijft het belangrijkste deel van de Conference to the Point van 12 december 2003, waar ik in een sessie van 1 uur en 15 minuten in meer detail ben ingaan op het onderwerp, en heb laten zien hoe we bestaande Delphi 7 web services op eenvoudige wijze kunnen migreren naar ASP.NET. Ik heb hierbij zelfs als eindresultaat een cross-platform project overgehouden die zowel met Delphi 7 als met Delphi for .NET te compileren is tot een Web Service toepassing.

Delphi 7 Web Service
Ik wil beginnen met het bouwen van een Delphi 7 Web Service - op de meest default manier die er is - en vervolgens laten zien hoe ik hier een ASP.NET Web Service van kan maken.
Start Delphi 7 Enterprise voor het eerste deel van het voorbeeld. Doe File | New - Other, ga naar de WebServices tab en maak een nieuwe Soap Server Application. In de dialoog die volgt kun je kiezen voor het target van de Win32 Web Service. Het maakt me niet uit wat je hier kiest: ikzelf kies om te beginnen vaak voor CGI (lekker makkelijk), maar ook ISAPI of een Web App Debugger executable is goed. Dat is slechts de "schil" van het project - het gaat mij om het SOAP object dat erin komt te zitten.
Bij Delphi 7 komt op een gegeven moment de vraag "Create Interface for SOAP module", om te kijken of je een SOAP Interface wilt toevoegen aan je project. Natuurlijk wil je dat, maar je kunt hetzelfde bereiken door de SOAP Server Interface wizard te gebruiken die je ook in de WebServices tab van de Object Repository kunt vinden (handig te weten voor als je meer dan één SOAP object in je toepassing wilt stoppen).
Hoe je er ook komt, in ieder geval heb je uiteindelijk de "Add New WebService" dialoog voor je, te zien in figuur 1:

Add New WebService dialoog

Vul hier een service naam en unit naam in, en zorg dat je de voorbeeld methodes krijgt (dan kan ik die gebruiken om te migreren naar Delphi for .NET en ASP.NET). De waarde voor de Service Activation Model optie wordt overigens nog steeds genegeerd door Delphi 7 (zie http://www.drbob42.com/SOAP/Delphi7.htm voor een oplossing).
Het resultaat is dat je een xxxIntf.pas unit (met de interface definitie) en een xxxImpl.pas unit (met de implementatie) krijgt. Deze twee units bevatten de kern van je SOAP object, en als je dit naar een ASP.NET oplossing wil migreren zul je deze units moeten aanpakken. Het grootste probleem van migratie is echter dat het tijd kost. En in de tussentijd worden wellicht aanpassingen vereist aan het originele systeem. Bovendien zullen - na de migratie van de server - de meeste clients maar langzaam overstappen van de "oude" server naar de nieuwe ASP.NET server, zodat je een tijd lang beide servers in de lucht moet houden. En als er dan een bug wordt gevonden, dan moet je die wellicht in beide systemen oplossen. Het zou dus erg handig zijn als de migratie een beetje meer dan alleen maar migratie is, en als we een bug bijvoorbeeld maar éénmaal hoeven op te lossen (of een wijziging in de specificaties - een nieuwe gebruikerswens bijvoorbeeld - maar in één exemplaar van de source code hoeven door te voeren).
De oplossing is het veelvuldig gebruikmaken van IFDEFs om zodoende te code met zowel Delphi 6-7 als Delphi for .NET te laten compileren (en in theorie ook met Kylix, zodat je het resultaat ook nog op Linux kunt deployen).

Implementatie
Het lijkt verkeerd om, maar laten we beginnen bij de implementatie. De xxxImpl.pas unit bevat de volgende code (deze werkt alleen nog maar met Delphi 6 of 7):

  { Invokable implementation File for TCrossPlatformSoapService which implements
    ICrossPlatformSoapService }

  unit CrossPlatformSoapServiceImpl;
  interface
  uses
    InvokeRegistry, Types, XSBuiltIns,
    CrossPlatformSoapServiceIntf;

  type
    { TCrossPlatformSoapService }
    TCrossPlatformSoapService = class(TInvokableClass, ICrossPlatformSoapService)
    public
      function echoEnum(const Value: TEnumTest): TEnumTest; stdcall;
      function echoDoubleArray(const Value: TDoubleArray): TDoubleArray; stdcall;
      function echoMyEmployee(const Value: TMyEmployee): TMyEmployee; stdcall;
      function echoDouble(const Value: Double): Double; stdcall;
    end;

  implementation

  function TCrossPlatformSoapService.echoEnum(const Value: TEnumTest): TEnumTest;
    stdcall;
  begin
    { TODO : Implement method echoEnum }
    Result := Value;
  end;

  function TCrossPlatformSoapService.echoDoubleArray(const Value: TDoubleArray): TDoubleArray;
    stdcall;
  begin
    { TODO : Implement method echoDoubleArray }
    Result := Value;
  end;

  function TCrossPlatformSoapService.echoMyEmployee(const Value: TMyEmployee): TMyEmployee;
    stdcall;
  begin
    { TODO : Implement method echoMyEmployee }
    Result := TMyEmployee.Create;
    Result.FirstName := 'Bob';
    Result.LastName := 'Swart';
  end;

  function TCrossPlatformSoapService.echoDouble(const Value: Double): Double;
    stdcall;
  begin
    { TODO : Implement method echoDouble }
    Result := Value;
  end;

  initialization
    { Invokable classes must be registered }
    InvRegistry.RegisterInvokableClass(TCrossPlatformSoapService);
  end.

Uses Clause
We beginnen met de uses clause. De Borland implementatie van SOAP maakt gebruik van de InvokeRegistry, Types, en XSBuiltIns units, maar voor ASP.NET kunnen we volstaan met de System.Web.Services unit. Omdat de CLR compiler optie is voorgedefinieerd voor Delphi for .NET, kunnen we dit als volgt uitdrukken:

  uses
  {$IFDEF CLR}
    System.Web.Services,
  {$ELSE}
    InvokeRegistry, Types, XSBuiltIns,
  {$ENDIF}
    CrossPlatformSoapServiceIntf;

TCrossPlatformSoapService
Vervolgens komen we bij de definitie van de TCrossPlatformSoapService. Nog afgezien van het feit dat een T-prefix niet meer gebruikelijk is onder .NET (maar je de naam van de SOAP server waarschijnlijk niet zomaar kan wijzigen), zullen we ook hier de nodige wijzigingen moeten aanbrengen. De Delphi 7 versie van de code geeft aan dat de nieuwe web service is afgeleid van TInvokableClass en een bepaald interface implementeert, (ICrossPlatformSoapService in mijn geval). Dat interface is onder .NET niet meer nodig, en de voorvader van de web service heet daar gewoon WebService. Dus dat wordt als volgt:

  type
    { TCrossPlatformSoapService }
  {$IFDEF CLR}
    [WebService(Namespace='http://eBob42.org',
                Description='Geschreven in Delphi 7, bruikbaar in ASP.NET')]
    TCrossPlatformSoapService = class(WebService)
  {$ELSE}
    TCrossPlatformSoapService = class(TInvokableClass, ICrossPlatformSoapService)
  {$ENDIF}
De ASP.NET Web Service moet ook nog het attribuut [WebService] meekrijgen, met daarin de Namespace (anders wordt de default namespace http://tempuri.org gebruikt) en eventueel een Description.

Web Methods
Voor de individuele methoden moeten we ook een kleine aanpassing verrichten. De stdcall calling conventie heeft geen betekening meer onder .NET, en in plaats daarvan moet iedere methode die we als web service methode willen exporteren het attribuut [WebMethod] meekrijgen. Dat komt er dus als volgt uit te zien:

  public
    {$IFDEF CLR} [WebMethod] {$ENDIF}
    function echoEnum(const Value: TEnumTest): TEnumTest;
      {$IFNDEF CLR} stdcall; {$ENDIF}

    {$IFDEF CLR} [WebMethod] {$ENDIF}
    function echoDoubleArray(const Value: TDoubleArray): TDoubleArray;
      {$IFNDEF CLR} stdcall; {$ENDIF}

    {$IFDEF CLR} [WebMethod] {$ENDIF}
    function echoMyEmployee(const Value: TMyEmployee): TMyEmployee;
      {$IFNDEF CLR} stdcall; {$ENDIF}

    {$IFDEF CLR} [WebMethod] {$ENDIF}
    function echoDouble(const Value: Double): Double;
      {$IFNDEF CLR} stdcall; {$ENDIF}
  end;

Implementatie
En nu komt het mooiste: bij de implementatie van de web methods hoeven we alleen maar op de stdcall te letten. Je kan die netjes tussen

  {$IFNDEF CLR} stdcall; {$ENDIF}
neerzetten, of helemaal weghalen (de stdcall calling conventie werd immers al aangegeven bij de declaratie van de web methods in the class zelf, en dan mag je bij de implementatie zelfs de argumentenlijst weglaten, en zeker de calling conventie).
Behalve deze ene wijziging per web method hoef ik verder niks te doen aan de implementatie. In praktijk moet je hier nog wel even kijken of je binnen de implementatie van de web methods geen zaken gebruikt die uniek zijn voor Win32, en daarvoor dan een .NET specifieke versie gebruiken - met gebruik van {$IFDEF} dan weer.

Initialization
We zijn bijna klaar - met deze unit dan - alleen nog even de initialization sectie onder handen nemen. Die moet alleen bij de Delphi 7 versie uitgevoerd worden, voor een ASP.NET server is het niet nodig, dus dat wordt als volgt:

  {$IFNDEF CLR}
  initialization
    { Invokable classes must be registered }
    InvRegistry.RegisterInvokableClass(TCrossPlatformSoapService);
  {$ENDIF}
  end.

Interface
Nu dan de interface unit. Die bevat behalve de definitie van het ICrossPlatformSoapServer interface ook de custom types die we gebruiken in onze web service (zoals de TMyEmployee bijvoorbeeld). Dat is de enige reden dat we überhaubt nog iets te maken hebben met de xxxIntf.pas unit, anders zouden we hem helemaal links kunnen laten liggen. Om maar meteen met de deur in huis te vallen: het eindresultaat met {$IFDEFs} moet er als volgt uit komen te zien:

  { Invokable interface ICrossPlatformSoapService }

  unit CrossPlatformSoapServiceIntf;
  interface
  {$IFNDEF CLR}
  uses
    InvokeRegistry, Types, XSBuiltIns;
  {$ENDIF}

  type
    TEnumTest = (etNone, etAFew, etSome, etAlot);

    TDoubleArray = array of Double;

    TMyEmployee = class{$IFNDEF CLR}(TRemotable){$ENDIF}
    private
      FLastName: AnsiString;
      FFirstName: AnsiString;
      FSalary: Double;
    published
      property LastName: AnsiString read FLastName write FLastName;
      property FirstName: AnsiString read FFirstName write FFirstName;
      property Salary: Double read FSalary write FSalary;
    end;

  {$IFNDEF CLR}
    { Invokable interfaces must derive from IInvokable }
    ICrossPlatformSoapService = interface(IInvokable)
    ['{FD1BBEBC-382E-47D6-A3D1-81D91C935F2F}']

      { Methods of Invokable interface must not use the default }
      { calling convention; stdcall is recommended }
      function echoEnum(const Value: TEnumTest): TEnumTest; stdcall;
      function echoDoubleArray(const Value: TDoubleArray): TDoubleArray; stdcall;
      function echoMyEmployee(const Value: TMyEmployee): TMyEmployee; stdcall;
      function echoDouble(const Value: Double): Double; stdcall;
    end;
  {$ENDIF}

  implementation

  {$IFNDEF CLR}
  initialization
    { Invokable interfaces must be registered }
    InvRegistry.RegisterInterface(TypeInfo(ICrossPlatformSoapService));
  {$ENDIF}
  end.
Voor de ASP.NET versie zijn we dus alleen geïnteresseerd in de custom types, en niet in het ICrossPlatformSoapService interface of the initialization sectie.

ASMX
Er rest nog een ding: de ASP.NET code voor de web service ook daadwerkelijk als .asmx pagina aanbieden. Liefst zonder teveel moeite te hoeven doen, uiteraard. Daarvoor wil ik gebruik maken van Code Behind, door de implementatie van de web service als een .NET assembly beschikbaar te maken. Dat kan alleen maar door de unit header zodanig te veranderen dat we onder .NET een library (assembly) opleveren, en onder Win32 gewoon een unit. Dit heb ik als volgt gedaan:

  {$IFDEF CLR}
  library CrossPlatformSoapServiceImpl;
  {$ELSE}
  unit CrossPlatformSoapServiceImpl;
  interface
  {$ENDIF}
Ik kan nu CrossPlatformSoapServiceImpl.pas met dccil compileren tot een assembly, en deze in de bin directory van mijn scripts (virtual) directory op de web server neerzetten (lokaal is dat mijn C:\Inetpub\Scripts\bin directory).
Het enige wat nu nog nodig is, is een daadwerkelijk .asmx bestand (voor in de Scripts directory), dat slechts uit één regel hoeft te bestaan:
  <%@ WebService Class='CrossPlatformSoapServiceImpl.TCrossPlatformSoapService' %>
Als namespace de naam van de assembly, en als classname de naam van de web service zelf (nu nog met de T-prefix).

Deployment
De voorbeeld web service uit dit artikel is op mijn eBob42.com website terug te vinden (zodat je kunt zien dat ze allebei nog werken). De oorspronkelijke Delphi 7 Enterprise versie is te vinden als http://www.eBob42.com/cgi-bin/CrossPlatformSoapService.exe (met nog de default namespace):

http://www.eBob42.com/cgi-bin/CrossPlatformSoapService.exe

Terwijl de ASP.NET versie als http://www.eBob42.com/cgi-bin/CrossPlatformSoapService.asmx te vinden is:

http://www.eBob42.com/cgi-bin/CrossPlatformSoapService.asmx

Wie nog vragen of opmerkingen heeft, kan die altijd per e-mail aan mij kwijt, of kon natuurlijk ook de Conference to the Point van vrijdag 12 december bezoeken, waar ik dit in praktijk heb laten zien (met een wat uitgebreider voorbeeld).
Wie op de hoogte wil blijven van de laatste ontwikkelingen zou daarnaast zeker eens moeten overwegen om mijn Delphi for .NET clinic bij te wonen.


This webpage © 1999-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.