Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics Dr.Bob's Delphi Courseware Manuals
 Dr.Bob Examines... #45
See Also: other Dr.Bob Examines columns or Delphi articles

This article was published in Hardcore Web Services (May 2003). Copyright Pinnacle Publishing, Inc. All rights reserved.

Consuming C# Web Services with Delphi 7 Professional
In this article, I'll show how web services written in C# can be consumed and used by Borland's Delphi Professional (on Windows) or even Kylix Professional (on Linux). Using a simple echo web service, we'll see how the Delphi and Kylix clients can connect and interact with the C# web service.

With an echo web service, I mean a web service that has many methods that all simply echo the value they receive.Although this may not sound useful, the main purpose of an echo web service is to be able to test the interoperability of different environments that implement web services (at the server side and the client side).And with this example, I will show you (or rather proof to you) that C# web services can be consumed by Borland Delphi and Kylix web service clients.

C# Echo Web Service
You don't need Visual C# .NET to write the Echo Web Service in C#, since it's simple in structure.In fact, I didn't even use code behind, so the C# source code is contained in a single file (wseBob42CSharp.asmx).The first line of this file specifies the language used to implement the web service as well as the class name eBob42CSharpWebService.eBobCSharp that I'm exporting.I only need to add two namespaces in the using clause: System (for DateTime) and System.Web.Services.The actual web service itself consists of methods that are all specific for a C# native type: from integers to floats, then boolean, string and char - the 16-bit Unicode character - and finally more advanced types such as enums, DateTime and even a complex structure.Each method takes a value argument of the specific type, and returns this value as well.Apart form that each method has an Echo prefix followed by the type that we're taking and returning. The complete source code can be seen below:

  <%@ 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;
      }
    }
  }
You may wonder what the purpose is of this echoing web service.When talking from a C# client to this C# web service, there is indeed little purpose.But when using a different development environment or programming language to consume (import and use) this C# web service, then you are suddenly using a testing web service, with the ability to see if each type that the "foreign" clients passes to a specific method is marshalled correctly into a SOAP envelope (by the "foreign" client), recognised correctly by the C# web service, returned by the C# web service (we can safely assume that will work just fine) and finally be marshalled back into a native "foreign" format that should be the same value that was passed to it.
An additional way to test the interoperability is to check the SOAP envelopes that are being sent back and forth between the client and web service.These SOAP envelopes should contain the language independent value and can be easily checked.
In order to deploy the C# web service, you need to place it in a directory that has scripting rights - by default the scripts directory of the c:\inetpub directory.Since my ISP also supports ASP.NET on his web server, I've also deployed it on the internet, and you can connect to it at http://www.eBob42.com/cgi-bin/wseBob42CSharp.asmx - note that this will provide you with a page where you can test the echo methods already.

Consuming with Delphi 7: Importing
Delphi 7 can import the WSDL (Web Service Definition Language) definition of a web service and turn it into a Delphi import unit - just like the wsdl command-line tool from the .NET Framework SDK.There are actually two tools that come with Delphi to do that: a command-line tool called WSDLImp.exe and the WSDL Importer from the Object Repository.WSDLImp takes the WSDL URL as command-line argument, as well as some options to control the generated code.You need to specify -P to generate Pascal code (for Delphi) or -C to generate C++ code (for Borland C++Builder).Unfortunately, when using the Delphi 7 version of WSDLImp with the -C option on my C# web service, it seems to hang after generating the header file (it works OK on most other web services that I've tried), but at least the -P option always terminates correctly and generates a Pascal import unit.
If you don't want to use the command-line tool but the visual WSDL Importer Wizard, you can start Delphi 7 and do File | New - Other, go to the Web Services tab of the Object Repository where you can find the WSDL Importer wizard.On the first page (see below) you can specify the URL for the WSDL which is http://www.eBob42.com/cgi-bin/wseBob42CSharp.asmx?wsdl in my example.

Delphi 7 WSDL Importer

If you go to the next page, you get a detailed preview of the types, interfaces and methods that are exposed by the web service (as defined in the WSDL document). Note the fact that weekday, char and complex are apparently "new" types for Delphi (noteworthy to mention), but DateTime and the other types are already known.For the type called complex, the structure shown below is indeed compatible to the C# struct that I defined in the C# listing.

Generating Methods and Types

If you click on Finish, then a new unit wseBob42CSharp.pas will be generated (the same one that can be obtained when using the command-line WSDLImp tool). The following few listings contain short snippets from this import unit that may be interesting to look at.
First of all, in the following listing we see the generated Delphi types for the original C# weekday, char and complex.The definition of char (a 16-bit Unicode character) raises an eyebrow, since I would have expected char to be equivalent to Delphi's 16-bit WideChar and not type word.Alas, the program runs just fine with char defined as type word, and if I manually change char to be of type WideChar the Delphi client compiles OK but throws an exception when calling the Echochar method.

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;
The following listing shows the generated eBobCSharpSoap interface, with the Delphi methods and the Delphi types of the arguments and return values.
  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;
Finally, the following listing shows a generated function called GeteBob42CSharpSoap that can be used to create an instance of the C# web service in a Delphi application.Since this function takes default arguments, we don't have to pass any and just call it to get the eBob42CSharpSoap interface back (defined earlier) and start calling methods.
Note that you can use the (default) arguments of this function to specify that the WSDL should be checked to locate the web service - instead of relying on the location of the web service at the time of importing it.You can also specify a new address in case you know it has moved (but can't or don't want to re-import it to generated the import unit), and finally you can pass a HTTPRIO component, which implements the actual connection to the web service and returns the eBob42CSharpSoap interface.I'll show you when this can be handy in just a moment.
  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;

Consuming with Delphi 7: Using
Once you have the import unit wseBob42CSharp.pas it's time to start building a Delphi application that uses this unit to get input from the user, send it to the C# web service, and compare it to the result (or in this example: present the resulting value in another editbox, so the user can compare from him/herself).
The code in the following listing shows the most "complex" example (pardon the pun) using the complex structure: create a new complex instance, assign values to the real and imag fields, call the Echocomplex method from the C# web service (passing the structure and assigning the results back to it), and finally displaying the results again.

  cm := complex.Create;
  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);
  cm.Free;
Like I said earlier, the fact that a value can be returned by the C# web service as the same value is one thing to test, but it's often also enlightening to see the soap message that was used to communicate.If you want to see the SOAP message that the client is sending to the C# server, you can use an explicit HTTPRIO component (from the Web Services tab of the Delphi Component Palette).Using this component, you can write some code in the OnBeforeExecute event handler (to see the MethodName and SoapRequest - the latter contains the complete SOAP envelope).Since this means you are now using an existing HTTPRIO component, the call to GeteBob42CSharp.Echocomplex must be changed to pass the HTTPRIO component as argument (replacing the empty default argument), as follows:
  cm := GeteBob42CSharpSoap(False,'',HTTPRIO1).Echocomplex(cm); // Call to C# server
This is actually the source code that you'll find in the MainForm.pas unit that implements the final version of the Delphi web service client.And in the event handler I put the MethodName in the statusbar as additional information which method of the C# server was actually called, as well as a call to ShowMessage that displays the SOAP envelope itself.

Delphi 7 Interoperability Client

Interoperability Results
As I expected, there are no problems when passing integers and floats from the Delphi client to the C# web service.When I tried to pass negative numbers to the unsigned types, I noticed that -1 was converted to 255 by Delphi, but sending -1 to Echoushort, Echouint or Echoulong resulted in an exception (from the C# web service) - that an incorrect value was received.
The funny thing was the Echochar again, since a char is mapped to a word in Delphi (and not a WideChar in Delphi).This is a strange situation, but I can cast a regular character in Delphi to a WideChar, and use the Ord operator to turn a character into its ordinal 16-bit value, and WideChar to turn it back into a 16-bit Unicode character.Passing char values from the Delphi side to C# and back seems to work fine anyway.Next time, I'll continue this interoperability test with a Delphi web service against a C# client, and then I'll also look some more into this char issue.
Strings work just fine, and enumerated types are also passed and received as correct values (note that the actual enum value is passed, but I'm displaying the ordinal value in the resulting editbox).The DateTime test was an interesting one: Delphi clients have had some interoperable issues with dates when communicating with web services that were built with the MS SOAP Toolkit 3.0 in the past, but using ASP.NET the problems are solved.For a DateTime test, I use the value of "Now" (a built-in Delphi function that returns the current date and time), assign it to a TXSDateTime structure which is then passed to the C# web service.The fact that we can use a TXSDateTime from the Delphi XSBuiltIns unit already illustrates that some work was done to support standard SOAP types (another example is the TXSDecimal type used for the C# decimal).The final test in my example involved the complex structure, and this was passed and received without any problems whatsoever.

As a final remark: testing types not only involves testing random values, but also the minimum and maximum ranges of the type (for which Delphi has handy built-in constants like MaxInt, MinInt, MaxDouble, etc.For this purpose, the type ranges are displayed in the statusbar of the Delphi client when you select a different type. Of course, once you've checked that values can be echoed correctly, a further test can consist of allowing the web service to actually operate on these values (for example adding 1 to the integer values, dividing the float values by 10, reversing the string, etc.).This is left as an exercise for the reader: full source code for the C# web service and Delphi client is included.

Next Time
Now that I've seen for myself that simple and more complex C# types in a C# web service can be used by a Delphi client, I also want to do the reverse next time: write an echoing web service using Delphi and consume this in a C# client (also taking a closer look at the character issue of this time).
Note that where I've been using Delphi 7 on Windows, the same functionality is available in Kylix 3 on Linux (as well as C++Builder 6 - albeit using the C++ syntax instead).And if you don't have access to Delphi, Kylix or C++Builder, you can download a free trial version from the Borland website to test the code from this article.


This webpage © 2003-2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.