Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
Consuming Delphi 7 Web Services with C#
In this article, I'll show how web services written in Borland's Delphi 7 (or Kylix) can be consumed and used in the .NET environment with C#.
Extending the simple echo web service example, we'll see how the Delphi web services on Windows (and Kylix web services on Linux) can connect and interact with C# clients on .NET.
Last time I built an echo web service in C#, showing the reason why we want to see which types and parameter values could be echoed by a Delphi client without problems.I encountered some minor issues with (UniCode) characters that were converted to Word in Delphi instead of WideChar.Other than that, everything went smoothly.This time, let's see if the same can be said for the interoperability between a Delphi 7 (of Kylix 3) web service and a C# client.
Delphi Echo Web Service
You need the Enterprise edition of Delphi 7 to participate with this month's example.Note that you can download a free trial edition of Delphi 7 Architect from http://www.borland.com/products/downloads/download_delphi.html so feel free to play along.The techniques in this article work with both Delphi 7 on Win32 and Kylix on Linux, and from no on when I mention Delphi you can read Kylix as well.
According to the W3C standard, SOAP message can be transported using the HTTP, FTP or even SMTP protocols.Delphi 7 currently offers HTTP support by creating SOAP servers as web server applications (a bit like ASP.NET, where web services are deployed as .asmx pages).To create a new web service with Delphi, you need to do File | New - Other and go to the WebServices tab of the Object Repository:
You need the first icon to create a new SOAP server application.
The third icon can be used to add new interfaces to an existing SOAP server application, and the last icon is used to import a WSDL definition (which I did last time).
For now, double-click on the SOAP server application icon, which first allows you to specify what kind of web server application you want to create as SOAP server:
The advantage of the Web App Debugger executable is that you can use that as stand-alone application (with a built-in HTTP server) that is very easy to debug using the Delphi integrated debugger.
For real-world deployment, you can of course select another target such as ISAPI, Apache or the plain CGI executable (easy to deploy).On Linux, you can select Apache dso or CGI executables as target (but obviously not ISAPI).I've selected a Web App Debugger executable at this time, but will "convert" this to a CGI executable later in this article.
As soon as you click on OK, a new SOAP server application is created, complete with web module, but a question pops up first ("Create Interface for SOAP module?"):
Before I answer the question, first take a look at the three components on the web module (behind the dialog).
The web module itself is responsible for receiving (and sending) HTTP requests (and responses).The HTTPSoapDispatch component receives incoming SOAP requests from the web module, and has to decide which SOAP object to dispatch the request to.The second component, HTTPSoapPascalInvoker, is used to invoke the specific method from the SOAP object, including the right parameter values.The result of this invocation is sent back through the HTTPSoapPascalInvoker to the HTTPSoapDispatcher which packs the response into a SOAP envelope again, and then uses the web module mechanism itself to return the SOAP envelope as part of the HTTP response.
The third component, WSDLHTMLPublish, is used to generate the WSDL based on the registered SOAP objects in the SOAP server application (I'll show you where this registration happens in a moment).
The three components on the web module define the interaction between the Delphi SOAP server and the SOAP clients, and as a Delphi developer you only have to focus on the new SOAP objects themselves, which can be added by the third icon of the first screenshot, or by answering Yes to the question we saw in the third screenshot.
Either way, the result is the Add New WebService dialog (see below), where you can enter the name of the service (D7Echo in my example) as well as some code generation options like comments or sample methods.The sample methods include an echoEnum, echoDoubleArray, echoMyEmployee and echoDouble.The echoDoubleArray is one that I didn't think of last time, when I only returned single values, enums and structs.
Note the service activation model, which can be set to "Per Request" or "Global".
The latter choice results in a global SOAP object (here at the server side) which will handle all incoming requests and generates all responses.The alternative (default) choice is to create a new instance of the SOAP object for every incoming request, which also enforces the fact that the SOAP server object is stateless.
Note that selecting the Global activation model has no effect at this time, and generates the same code as the Per Request activation model, since Borland forgot to ship the complete code template with Delphi 7 (see http://www.drbob42.com/SOAP/Delphi7.htm for a workaround).
Two more units will be created after you click on the OK button: D7EchoIntf.pas for the SOAP object interface, and D7EchoImpl.pas for the implementation.Unlike ASP.NET web services, Delphi likes to keep the interface definition and implementation separate.The interface definition unit is typically the same as the interface unit that you get when importing the WSDL definition using Delphi 7 (so if you are a Delphi developer, you can also copy the interface unit to your machine in order to create a client for the web service - you don't have to import the WSDL if you this interface unit is already available).
D7Echo Interface
The contents of D7EchoIntf.pas can be seen below.Note that if you want to add new methods to the ID7Echo interface, they need to be using the stdcall calling convention (instead of the default calling convention of Delphi, which is not supported by other languages).
{ Invokable interface ID7Echo } unit D7EchoIntf; interface uses InvokeRegistry, Types, XSBuiltIns; type TEnumTest = (etNone, etAFew, etSome, etAlot); TDoubleArray = array of Double; TMyEmployee = class(TRemotable) 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; { Invokable interfaces must derive from IInvokable } ID7Echo = interface(IInvokable) ['{EE9BFE38-D24C-4980-8A8A-1856342A02DF}'] { 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; implementation initialization { Invokable interfaces must be registered } InvRegistry.RegisterInterface(TypeInfo(ID7Echo)); end.Note the call to RegisterInterface in the initialization section, which will register the interface in the so-called Invokable Registry; an in-memory repository that will make sure that the interfaces, methods and parameters are "known" to the application, so the WSDLHTMLPublish component can produce the right WSDL.
TMyEmployee = class(TRemotable) private FLastName: AnsiString; FFirstName: AnsiString; FSalary: Double; FC: Char; // BS FWC: WideChar; // BS FDate: TXSDateTime; // BS published property LastName: AnsiString read FLastName write FLastName; property FirstName: AnsiString read FFirstName write FFirstName; property Salary: Double read FSalary write FSalary; property C: Char read FC write FC; // BS property WC: WideChar read FWC write FWC; // BS property Date: TXSDateTime read FDate write FDate; // BS end;
D7Echo Implementation
The contents of D7EchoImpl.pas can be seen in below.TD7Echo is derived from TInvokableClass and implements the ID7Echo interface which was defined in the D7EchoInft.pas unit.This means that all functions declared in the ID7Echo interface must be repeated in declaration of the TD7Echo class, and implemented in this unit as well.The generated ToDo-comment inside the echoEnum, echoDoubleArray and echoDouble seem to be unnecessary: the result is already assigned to the received argument value.This may put you on the wrong foot when looking at the implementation of the echoMyEmployee function: the TMyEmployee class is created and returned just fine.But... none of the incoming TMyEmployee field values are copied! So unless you write some additional lines of code (marked with the // BS comments in the previous listing), the echo of a TMyEmployee will appear to fail.
{ Invokable implementation File for TD7Echo which implements ID7Echo } unit D7EchoImpl; interface uses InvokeRegistry, Types, XSBuiltIns, D7EchoIntf; type { TD7Echo } TD7Echo = class(TInvokableClass, ID7Echo) 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 TD7Echo.echoEnum(const Value: TEnumTest): TEnumTest; stdcall; begin { TODO : Implement method echoEnum } Result := Value; end; function TD7Echo.echoDoubleArray(const Value: TDoubleArray): TDoubleArray; stdcall; begin { TODO : Implement method echoDoubleArray } Result := Value; end; function TD7Echo.echoMyEmployee(const Value: TMyEmployee): TMyEmployee; stdcall; begin { TODO : Implement method echoMyEmployee } Result := TMyEmployee.Create; Result.LastName := Value.LastName; // BS Result.FirstName := Value.FirstName; // BS Result.Salary := Value.Salary; // BS Result.C := Value.C; // BS Result.WC := Value.WC; // BS Result.Date := Value.Date.Clone; // BS end; function TD7Echo.echoDouble(const Value: Double): Double; stdcall; begin { TODO : Implement method echoDouble } Result := Value; end; initialization { Invokable classes must be registered } InvRegistry.RegisterInvokableClass(TD7Echo); end.Again you see a call to the Invokable Registry in the initialization section, this time to register the actual implementation class TD7Echo (so the HTTPSoapDispatcher and HTTPSoapPascalInvoker can dispatch the right object and invoke the right methods).
D7Echo Test
Time to test the WebApp Debugger executable.Compile and Run the application, which will show an empty main form (just to indicate that the WAD application is running).Now, start the WebApp Debugger itself from Delphi's Tools menu.This will look like this:
If you click on the Start button, the actual WebApp Debugger "engine" will start, and you can now click on the URL (which is only underlined and active if the engine is started), which will start the default browser and show a list of the registered WebApp Debugger applications - as shown below.
Select the D7EchoWAD.ECho42 application and click on the Go button to view this application.This will actually use the WebApp Debugger executable which is currently running (being debugged) in the Delphi IDE, so if you've set any breakpoints they will be triggered at this time.The actual starting page of the Delphi 7 Web Service can be seen below.
There are two so-called PortTypes available in the D7EchoWAD web service. The second on is the IWSDLPublish that is a result of using the WSDLHTMLPublish component on the web module.The first one is more interesting, and is the ID7Echo interface.You can see the four methods, and can click on the WSDL link to view the complete formal definition:
If you take a close look at the screenshot above, you may notice the targetNamespace attribute which is set to http://tempuri.org - the default target namespace of web services. Obviously, you want to change that into a more unique namespace for your web service.You can do this with the TargetNamespace property of the TWSDLHTMLPublish component on the web module (so the namespace is specified for the entire web service, which can actually implement more than one SOAP interface).I've changed the TargetNamespace to http://www.eBob42.org/ for the example project.
Echo42 Project Group
The URL to produce the WSDL is http://localhost:8081/D7EchoWAD.Echo42/wsdl/ID7Echo and I can use this URL with the wsdl command-line tool from the .NET Framework SDK in order to produce an import unit.However, since you may want to test this on your own machine as well, I've also deployed the web service on the web as a CGI application.For this, I needed to create a new web service skeleton, select CGI standalone executable as target, remove the web module from that web service and add the web module and D7Echo interface and implementation units to the new CGI project.The following screenshot shows the project group with both the WAD and the CGI project sharing the same units.
This means I can make changes to the D7Echo SOAP object and use the D7EchoWAD project to test it locally on my machine or the D7EchoCGI project to deploy it (and allow you to test it on your development machines).
Consuming with C#: Importing with WSDL
If you have the .NET Framework SDK installed (which also comes with Visual Studio .NET), then the command-line tool wsdl is available as well.This tool can turn a wsdl document or location into a C# of VB.NET import unit, Since I deployed the Delphi 7 web service on my web server at http://www.eBob42.com/cgi-bin/D7EchoCGI.exe, you can call wsdl as follows:
wsdl http://www.eBob42.com/cgi-bin/D7EchoCGI.exe/wsdl/ID7EchoThe resulting ID7Echoservice.cs can be compiled using Visual C# or using the C# command-line compiler with the following statement:
csc /t:library ID7Echoservice.csThe result is an ID7Echoservice.dll assembly that can be used by a new C# application to send an receive the echo arguments.
C# Import results
Now, before we actually start to use the imported web service, I first want to take a closer look at the C# source code that has been generated for the Delphi types.Especially the TEnumTest and TMyEmployee class, which can both be seen below:
///It strikes me as odd that both the C and WC fields (resp.Char and WideChar in Delphi) are imported as string, but apart from that, everything else looks fine.[System.Xml.Serialization.SoapTypeAttribute("TEnumTest", "urn:D7EchoIntf")] public enum TEnumTest { /// etNone, /// etAFew, /// etSome, /// etAlot, } /// [System.Xml.Serialization.SoapTypeAttribute("TMyEmployee", "urn:D7EchoIntf")] public class TMyEmployee { /// public string LastName; /// public string FirstName; /// public System.Double Salary; /// public string C; /// public string WC; /// public System.DateTime Date; }
csc /r:ID7Echoservice.dll UseD7Echo.csThe source code for the C# console application is as follows:
using System; namespace eBob42 { class UseD7Echo { static void Main(string[] args) { ID7Echoservice D7Echo = new ID7Echoservice(); // echoDouble System.Double value = 42.0; Console.WriteLine("D7Echo.echoDouble(42.0) = " + D7Echo.echoDouble(value).ToString()); // echoEnum TEnumTest MyEnum = TEnumTest.etSome; if (D7Echo.echoEnum(MyEnum) == TEnumTest.etSome) Console.WriteLine("enum OK"); else Console.WriteLine("enum failed"); // echoDoubleArray System.Double[] DAin = new System.Double[20]; for (int i=0; i < 20; i++) DAin[i] = i / 20.0; System.Double[] DAout = D7Echo.echoDoubleArray(DAin); for (int i=0; i < 20; i++) if (DAout[i] != DAin[i]) Console.WriteLine("EchoDoubleArray: " + DAout[i].ToString() + " != " + DAin[i].ToString()); // echoMyEmployee TMyEmployee MyEmployee = new TMyEmployee(); MyEmployee.LastName = "Swart"; MyEmployee.FirstName = "Bob"; MyEmployee.Salary = 42; MyEmployee.C = "C"; // string instead of char! MyEmployee.WC = "W"; // string instead of char! MyEmployee.Date = DateTime.Now; TMyEmployee Me = D7Echo.echoMyEmployee(MyEmployee); if (Me.LastName != MyEmployee.LastName) Console.WriteLine("LastName failed: [" + Me.LastName + "]"); if (Me.FirstName != MyEmployee.FirstName) Console.WriteLine("FirstName failed: [" + Me.FirstName + "]"); if (Me.Salary != MyEmployee.Salary) Console.WriteLine("Salary failed: " + Me.Salary.ToString()); if (Me.C != MyEmployee.C) Console.WriteLine("C failed = [" + Me.C + "]"); if (Me.WC != MyEmployee.WC) Console.WriteLine("W = [" + Me.WC + "]"); if (Me.Date != MyEmployee.Date) Console.WriteLine("Now = " + MyEmployee.Date.ToString() + " != " + Me.Date.ToString()); } } }Note that I have to assign string values to the C and WC properties of the TMyEmployee class (although the Delphi web service will end up with a Char and WideChar only).
Interoperability Results
The double value and enum were returned without problems, and so were most fields of the TMyEmployee class.Most fields, since the return value of the DateTime field was empty.Could this be related to the fact that the TXSDateTime is represented by a class in Delphi, and the class is used as property of another class?
To test this, I added a new method called echoDate to the interface and implementation of the D7Echo web service as follows:
function TD7Echo.echoDate(const Value: TXSDateTime): TXSDateTime; begin Result := Value.Clone; // BS end;I also added a few more lines to the C# test application to see if a simple DateTime - not as part of a class or structure - would be returned OK:
// echoDate DateTime nu = D7Echo.echoDate(DateTime.Now); Console.WriteLine("Date: " + nu.ToString());It turned out that a simple echo of a DateTime works fine, and so will other date arithmetic.As final test, I imported the Delphi 7 web service in Delphi itself, in order to see if the TXSDateTime field was perhaps left empty when the SOAP response was sent over the wire.Unfortunately, the Delphi 7 client received the TXSDateTime value just fine - even when it was part of the TMyEmployee class, so the TXSDateTime field itself is treated by Delphi as expected.The only conclusion I can draw is that the Delphi 7 web service seems to pack the TXSDateTime field (inside the TMyEmployee class) in a way that the C# client cannot correctly decode it.The Delphi client can decode it just fine, but the .NET client cannot use it anymore, and seems to receive an empty DateTime value.
Delphi 7 SOAP Attachments
Delphi 6 contained the first SOAP implementation by Borland, and Delphi 7 added more functionality with support for UDDI and SOAP Attachments.The latter is something that I've been using for a while now, in web server applications that return ZIP-archives as SOAP attachments.One of these examples is available on the web as http://www.eBob42.com/cgi-bin/UCCode.exe.Unfortunately, I was unable to import the WSDL of this web service with the wsdl importer of the .NET Framework SDK.The specific error message didn't help me much, I have to admit, and refers to the lack of a matching binding for the operation that returns the attachment.After some research, I finally found the reason: Delphi returns attachments as MIME multipart forms.And .NET uses DIME multipart forms instead of MIME.
In short: Delphi's TSOAPAttachment class can be used to send or receive any attachment, but these attachments are placed as additional parts in a MIME multipart form, and .NET doesn't use MIME, so it cannot import or use these web services.I have not heard if Delphi will also support DIME in the future (or if .NET will support MIME in the future).The latest .NET Framework 1.1 SDK was still unable to import the WSDL from my UCCode.exe web service, so for now it seems that attachments from Delphi web services cannot be received by .NET web service clients.
Summary
In this article, I've shown that Delphi 7 web services can be consume by C# clients on .NET with only a few minor surprises: characters will always be something special, and dates are not returned properly if they're part of another class (but a simple echoDate works just fine).Delphi's Attachments are sent as MIME multipart forms, which is not recognised by .NET, so they cannot be used (the wsdl importer even fails to import the entire web service if you use the Delphi TSOAPAttachment type).
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.