Bob Swart (aka Dr.Bob)
SOAP & Web Services (2) met C# en Delphi

Dit artikel gaat verder waar mijn artikel over ASP.NET ophield: we richten ons nu uitsluiten op Web Services en real-world problemen die op kunnen treden bij het ontwerp, de bouw en het implementeren/deploy-en van Web Services. In het vorige deel gebruikte ik Delphi 7 Enterprise (om via BizSnap enkele Web Services te bouwen), en liet ik zien wat de aandachtspunten zijn bij het ontwerpen van je Web Service interface (naar de buitenwereld toe). Vervolgens bekeken we de cross-platform eigenschap van Web Services, door de bestaande Delphi 7 Web Service te importeren met C# onder .NET.
In dit deel zal ik een nieuwe Web Service (C#) bouwen onder .NET en die met Delphi 7 onder Win32 importeren. Interoperability en compatibiliteits problemen tussen Delphi en C# komen hierbij uitgebreid aan de orde.

Let op: Ik maak bij het schrijven van dit artikel gebruik van de Delphi for .NET preview command-line compiler met Update 3 (van februari 2003) op .NET Framework versie 1.0 met Service Pack 2. Dezelfde code draait inmiddels ook onder .NET Framework 1.1.

C# Server en Delphi 7 Client
Nu we eerst een Delphi 7 server aan een .NET / C# client hebben gekoppeld - met verrassende resultaten - is het nu tijd om het omgekeerde te doen: een C# web service met ASP.NET bouwen, en dan die vervolgens te importeren en gebruiken met Delphi 7. Als voorbeeld pak ik wederom een "echo" web service, omdat we daarmee goed kunnen zien wat er goed heen-en-weer gestuurd kan worden, en waar we gekke dingen kunnen verwachten (characters die strings worden) of welke zaken helemaal niet werken (TXSDateTime als onderdeel van een class).

C# Echo Web Service
De C# source code voor een web service met ASP.NET bestaat uit één enkele source file wseBob42CSharp.asmx waar ik de namespace eBob42CSharpWebService gebruik om de class eBobCSharp te exporteren. We hebben twee namespaces in de using clause nodig: System (voor DateTime) en System.Web.Services voor de WebService base class.

  <%@ WebService Language="C#" Class="eBob42CSharpWebService.eBob42CSharp" %>

  using System;
  using System.Web.Services;

  namespace eBob42CSharpWebService
  {
    [WebService(Namespace="http://www.eBob42.com")]
    public class eBob42CSharp: System.Web.Services.WebService
    {
      [WebMethod(Description="Echo a sbyte")]
      public sbyte Echosbyte(sbyte sbyteValue)
      {
        return sbyteValue;
      }
      [WebMethod(Description="Echo a short")]
      public short Echoshort(short shortValue)
      {
        return shortValue;
      }
      [WebMethod(Description="Echo an int")]
      public int Echoint(int intValue)
      {
        return intValue;
      }
      [WebMethod(Description="Echo a long")]
      public long Echolong(long longValue)
      {
        return longValue;
      }
      [WebMethod(Description="Echo a byte")]
      public byte Echobyte(byte byteValue)
      {
        return byteValue;
      }
      [WebMethod(Description="Echo an ushort")]
      public ushort Echoushort(ushort ushortValue)
      {
        return ushortValue;
      }
      [WebMethod(Description="Echo an uint")]
      public uint Echouint(uint uintValue)
      {
        return uintValue;
      }
      [WebMethod(Description="Echo an ulong")]
      public ulong Echoulong(ulong ulongValue)
      {
        return ulongValue;
      }
      [WebMethod(Description="Echo a float")]
      public float Echofloat(float floatValue)
      {
        return floatValue;
      }
      [WebMethod(Description="Echo a double")]
      public double Echodouble(double doubleValue)
      {
        return doubleValue;
      }
      [WebMethod(Description="Echo a decimal")]
      public decimal Echodecimal(decimal decimalValue)
      {
        return decimalValue;
      }
      [WebMethod(Description="Echo a bool")]
      public bool Echobool(bool boolValue)
      {
        return boolValue;
      }
      [WebMethod(Description="Echo a char")]
      public char Echochar(char charValue)
      {
        return charValue;
      }
      [WebMethod(Description="Echo a string")]
      public string Echostring(string stringValue)
      {
        return stringValue;
      }
      [WebMethod(Description="Echo a DateTime")]
      public DateTime EchoDateTime(DateTime DateTimeValue)
      {
        return DateTimeValue;
      }
      public enum weekday
      {
        Monday = 1,
        Tuesday = 2,
        Wednesday = 3,
        Thursday = 4,
        Friday = 5,
        Saturday = 6,
        Sunday = 7
      }
      [WebMethod(Description="Echo a weekday")]
      public weekday Echoweekday(weekday weekdayValue)
      {
        return weekdayValue;
      }
      public struct complex
      {
        public double real;
        public double imag;
      }

      [WebMethod(Description="Echo a complex")]
      public complex Echocomplex(complex complexValue)
      {
        return complexValue;
      }
    }
  }
Voor het deployen heb je een directory nodig met scripting rechten alsmede natuurlijk het .NET Framework en ASP.NET op die betreffende machine. Op mijn web server voor www.eBob42.com (hosted by TDMWeb) is dat allemaal geregeld (daar draait zelfs Delphi for .NET als scripting taal voor ASP.NET), dus heb ik deze web service ook op het web neergezet waar we hem kunnen testen als http://www.eBob42.com/cgi-bin/wseBob42CSharp.asmx (helaas kun je ASP.NET tests alleen uitvoeren op de lokale machine, dus om te zien of de echo goed in elkaar zit zul je hem eerst op localhost moeten deployen).

eBob42CSharp web service

Importeren met Delphi 7
Delphi 7 kan de WSDL van de eBob42CSharp web service importeren met de WSDLImp.exe (een command-line tool uit de Delphi7\bin directory) of met de WSDL Importer unit uit de Object Repository. WSDLImp kan zowel Pascal code (met de -P switch) als C++ code (met de -C switch) genereren, net als de .NET WSDL command-line tool C# of VB.NET kan genereren. Helaas is de Delphi 7 versie van WSDLImp niet in staat om C++ code voor C++Builder te genereren als ik mijn C# web service als argument meegeef, want de -C optie zorgt ervoor dat hij blijft hangen na de header file. Het werkt goed op andere web services, maar ik heb nog niet gevonden wat er in de eBob42CSharp service zo speciaal is. Gelukkig gaat de -P optie wel altijd goed, en kunnen we een Delphi import unit genereren.
De meeste mensen zullen echter gewoon de visuele WSDL Import Wizard van Delphi 7 gebruiken uit de Object Repository (ook in Delphi 7 Professional), waar de met de hand de WSDL kunnen opgeven - in dit geval is dat http://www.eBob42.com/cgi-bin/wseBob42CSharp.asmx?wsdl.

Delphi 7 WSDL Importer

In de volgende pagina van de wizard zien we een preview van de types, interfaces en methodes die door de web service geëxporteerd zijn (volgens het WSDL document). weekday, char en complex zijn kennelijk nieuw, maar DateTime en de andere types zijn al bekend dus hoeven niet meer vermeld te worden.

Generating Methods and Types

Als we uiteindelijk op de Finish knop klikken wordt de import unit wseBob42CSharp.pas gegenereerd (dezelfde versie die we kunnen krijgen als we de command-line tool WSDLImp gebruiken overigens). Er zijn enkele interessante onderdelen in de import unit te zien, zoals de C# types die in Delphi zijn overgenomen. Let op de weekday die correct is, maar let ook eens op de char die naar een word wordt vertaald (in C# zal het een 16-bits unicode character zijn, wat in Delphi dus eigenlijk WideChar had moeten worden). Een Delphi Char of WideChar werd eerder al een string in de WSDL en C#, en nu wordt een C# character een Word in Delphi. Het moet niet gekker worden, maar het komt er in ieder geval op neer dat ik maar geen characters meer ga gebruiken of terug ga geven in mijn web services!

  type
    weekday = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);

    char = type Word; { "http://microsoft.com/wsdl/types/" }

    complex = class(TRemotable)
    private
      Freal: Double;
      Fimag: Double;
    published
      property real: Double read Freal write Freal;
      property imag: Double read Fimag write Fimag;
    end;
Het volgende stukje code laat het eBob42CSharpSoap interface zien:
    eBob42CSharpSoap = interface(IInvokable)
    ['{0C5AA472-6601-F203-A8CC-AF812119AA0A}']
      function  Echosbyte(const sbyteValue: Shortint): Shortint; stdcall;
      function  Echoshort(const shortValue: Smallint): Smallint; stdcall;
      function  Echoint(const intValue: Integer): Integer; stdcall;
      function  Echolong(const longValue: Int64): Int64; stdcall;
      function  Echobyte(const byteValue: Byte): Byte; stdcall;
      function  Echoushort(const ushortValue: Word): Word; stdcall;
      function  Echouint(const uintValue: Cardinal): Cardinal; stdcall;
      function  Echoulong(const ulongValue: Int64): Int64; stdcall;
      function  Echofloat(const floatValue: Single): Single; stdcall;
      function  Echodouble(const doubleValue: Double): Double; stdcall;
      function  Echodecimal(const decimalValue: TXSDecimal): TXSDecimal; stdcall;
      function  Echobool(const boolValue: Boolean): Boolean; stdcall;
      function  Echochar(const charValue: char): char; stdcall;
      function  Echostring(const stringValue:  WideString): WideString; stdcall;
      function  EchoDateTime(const DateTimeValue: TXSDateTime): TXSDateTime; stdcall;
      function  Echoweekday(const weekdayValue: weekday): weekday; stdcall;
      function  Echocomplex(const complexValue: complex): complex; stdcall;
    end;
En uiteindelijk komen we dan bij een functie genaamd GeteBob42CSharpSoap die in staat is om een interface naar de web service terug te geven. De default argumenten zorgen ervoor dat hierbij geen gebruik wordt gemaakt van een WSDL (maar direct een SOAP action wordt uitgevoerd). Als we wel via de WSDL eerst willen ophalen waar de SOAP server te vinden is kunnen we dit via het eerste argument aangeven.
  function GeteBob42CSharpSoap(UseWSDL: Boolean; Addr: string; HTTPRIO: THTTPRIO): eBob42CSharpSoap;
  const
    defWSDL = 'http://www.eBob42.com/cgi-bin/wseBob42CSharp.asmx?wsdl';
    defURL  = 'http://www.ebob42.com/cgi-bin/wseBob42CSharp.asmx';
    defSvc  = 'eBob42CSharp';
    defPrt  = 'eBob42CSharpSoap';
  var
    RIO: THTTPRIO;
  begin
    Result := nil;
    if (Addr = '') then
    begin
      if UseWSDL then
        Addr := defWSDL
      else
        Addr := defURL;
    end;
    if HTTPRIO = nil then
      RIO := THTTPRIO.Create(nil)
    else
      RIO := HTTPRIO;
    try
      Result := (RIO as eBob42CSharpSoap);
      if UseWSDL then
      begin
        RIO.WSDLLocation := Addr;
        RIO.Service := defSvc;
        RIO.Port := defPrt;
      end else
        RIO.URL := Addr;
    finally
      if (Result = nil) and (HTTPRIO = nil) then
        RIO.Free;
    end;
  end;

Delphi 7 web service client
Met de import unit bij de hand kunnen we een Delphi toepassing bouwen met als doel kijken of de echo van een waarde ook daadwerkelijk terugkomt als dezelfde waarde (wat we eerder ook met een C# client voor de Delphi web service deden). De code is niet echt moeilijk, daarom beperk ik me even tot het meest "complexe" voorbeeld van deze keer: de complex class met de real en imag velden.

  cm := complex.Create;
  try
    cm.real := StrToFloatDef(edtInput.Text,0);
    edtInput.Text := FloatToStr(cm.real);
    cm.imag := StrToFloatDef(edtInput2.Text,0);
    edtInput2.Text := FloatToStr(cm.imag);
    cm := GeteBob42CSharpSoap.Echocomplex(cm);
    edtOutput.Text := FloatToStr(cm.real);
    edtOutput2.Text := FloatToStr(cm.imag);
  finally
    cm.Free;
  end
Behalve het ontvangen van dezelfde waarde die we erin stoppen is het vaak ook nuttig om te zien hoe de SOAP request (en response) eruit ziet die "over de lijn" gaat. Om dat te zien moeten we gebruik maken van een expliciete HTTPRIO component (te vinden op de Web Services tab van het Component Palette) omdat dit component een OnBeforeExecute event handler heeft waarin we de MethodName en het SoapRequest kunnen bekijken. Bij de aanroep van GeteBob42CSharp moeten we dan wel het "debug" HTTPRIO component meegeven om ervoor te zorgen dat we dat component gebruiken met de event handler. De aanroep wordt als volgt:
  cm := GeteBob42CSharpSoap(False,'',HTTPRIO1).Echocomplex(cm); // Call to C# server
En de code van de OnBeforeExecute event handler van het HTTPRIO1 component heb ik als volgt geschreven:
  procedure TForm1.HTTPRIO1BeforeExecute(const MethodName: String;
    var SOAPRequest: WideString);
  begin
    ShowMessage(SOAPRequest);
    StatusBar1.SimpleText := MethodName // + ': ' + SOAPRequest
  end;
De testtoepassing die ik heb geschreven ziet er uiteindelijk als volgt uit:

Delphi 7 Interoperability Client

Interoperability Resultaten
Met de Delphi 7 web service client kunnen we testen of alle types goed worden doorgestuurd van de Delphi 7 client naar de C# web service en ook weer goed terugkomen. Voor integers en floats is dit geen probleem, behalve toen ik een negatieve waarde bij de unsighed types probeerde mee te geven. De waarde -1 werd omgezet in 255 door Delphi voor een Byte, terwijl -1 als waarde voor Echoshort, Echounit en Echoulong een exception teruggaf van de C# web service:

Error

Een ander aandachtspunt is de Echochar weer. Omdat een C# char naar een Word in Delphi wordt omgezet (en geen WideChar) had ik Ord nodig om de juiste waarde door te geven.
Strings werken goed, en ook de enumerated types werken zonder problemen (ik laat als resultaat de Ord waarde zien in de editbox). En ook de complex class ging zonder probleem over de lijn en kwam als dezelfde waarde terug. Als laatste keek ik naar de DateTime test. En gelukkig ging deze goed, zodat ik nu in ieder geval zeker weet dat "losse" XML date time types goed van C# naar Delphi en terug kunnen worden doorgegeven. Iets wat met de MS SOAP Toolkit 3.0 in het verleden niet goed ging (voor meer interoperability resultaten van Delphi zie http://soap-server.borland.com/WebServices/Delphi/BaseResults/base_default.html), maar voor web services met ASP.NET zijn deze problemen dus opgelost.

Meer Informatie...
In dit artikel heb ik laten zien hoe we Delphi 7 web services kunnen bouwen en importeren in een .NET omgeving, en wat daarbij de compatibiliteits en interoperatbility problemen zijn die we tegen kunnen komen. Ook heb ik een C# web service met ASP.NET gebouwd en deze in Delphi 7 gebruikt, en laten zien waar we daarbij op moeten letten. Als allerlaatste test heb ik toen ook de C# web service "vertaald" naar Delphi for .NET en laten zien wat daarbij de issues zijn waar we op moeten letten..
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via . Wie op de hoogte wil blijven van de laatste ontwikkelingen zou zeker eens moeten overwegen om mijn Delphi for .NET clinic bij te wonen.


Dit artikel is gebasseerd op mijn SOAP sessie tijdens de CttM 2003 - mei 2003

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