Sunday, November 22, 2009

WCF Service in pure IronPython with config file

I was wrong when I wrote in the last post that the IronPython service cannot be saved into an assembly. It can. Which opens a way to use .config file to configure the service.

This is a simple config file for the service:

ConfigService.exe.config

<?xml version="1.0"?>
<configuration>
<system.serviceModel>
    <services>
      <service name="ConfigService.myService">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:9000/myWcfService"/>
          </baseAddresses>
        </host>
        <endpoint address=""
            binding="basicHttpBinding"
            contract="TestServiceInterface.ImyService"/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

The interface is the same as in the previous version. The only difference in the service to the previous version is in the ServiceHost initialization - we omit the service configuration parameters because they are in the .config file. I also changed the clr namespace:

ConfigService.py

import clr
import clrtype
clr.AddReference('System.ServiceModel')
from TestServiceInterface import ImyService
from System import Console, Uri
from System.ServiceModel import (ServiceHost,
        BasicHttpBinding, ServiceBehaviorAttribute,
        InstanceContextMode)

class myService(ImyService):
    __metaclass__ = clrtype.ClrClass
    _clrnamespace = "ConfigService"
    _clrclassattribs = [ServiceBehaviorAttribute]

    def GetData(self, value):
        return "IronPython config service: You entered: %s" % value

sh = ServiceHost(myService)
sh.Open()
Console.WriteLine("Press  to terminate\n")
Console.ReadLine()
sh.Close()

If you want to run this script, you must save the ConfigService.exe.config as ipy.exe.config to the folder with the IronPython interpreter ipy.exe.

To save the service as an assembly, run the following command:

C:\IronPython-2.6\ipy.exe C:\IronPython-2.6\Tools\Scripts\pyc.py  /out:ConfigService /target:exe /main:ConfigService.py clrtype.py TestServiceInterface.py

The ConfigService.dll and ConfigService.exe are created. Add the ConfigService.exe.config to the same folder and when you run ConfigService.exe, the service starts. Note you also need all IronPython .dlls in the same folder.

You can adjust the .config file to expose a MEX endpoint (ConfigService.mex.exe.config) but I don't see a big point in it because svcutil.exe generates C# or VB code. Anyway - here are the generated files: myService.cs, myService.config

You can run the old TestClient.py and it will successfully retrieve value from the service. But the old TestClient.py does not use .config file. If we want to use .config file for the client, we have to rewrite the WCF client. First, here is the sample client .config file:

ConfigClient.exe.config

<?xml version="1.0"?>
<configuration>
  <system.serviceModel>
    <client>
        <endpoint address="http://localhost:9000/myWcfService"
            binding="basicHttpBinding"
            contract="TestServiceInterface.ImyService"/>
    </client>
  </system.serviceModel>
</configuration>

You can see it is very similar to the generated one. We do not specify details of the binding but we specify full name of the contract interface.

If you check the generated client proxy class by svcutil.exe, you see it is based on System.ServiceModel.ClientBase and the interface ImyService. There are some empty constructors and all methods from ImyService interface return result of the same method name call on Channel property. That's why I have created WcfClient helper function. The client source then looks like the following:

ConfigService.py

import clr
clr.AddReference('System.ServiceModel')
import System.ServiceModel
from TestServiceInterface import ImyService

def WcfClient(interface):

    class WcfClientBase(System.ServiceModel.ClientBase[interface]):

        def __getattr__(self, name):
            # if name is method from interface, return the Channel method
            if name in (k[0] for k in interface.emitted_methods.keys()):
                return getattr(self.Channel, name)

    return WcfClientBase()

wcfcli = WcfClient(ImyService)
print "WCF config client returned:\n%s" % wcfcli.GetData(11)

The WcfClient helper function returns an instance of class based on System.ServiceModel.ClientBase. The __getattr__ checks if the requested attribute name is interface method and if so, it returns the Channel's method with the same name. Which is the same behavior as the generated client proxy class in couple of lines of code.

To save the client as an assembly, run the following command:

C:\IronPython-2.6\ipy.exe C:\IronPython-2.6\Tools\Scripts\pyc.py /out:ConfigClient /target:exe /main:ConfigClient.py clrtype.py TestServiceInterface.py

The ConfigClient.dll and ConfigClient.exe are created. Add the ConfigClient.exe.config to the same folder and when you run ConfigClient.exe, the client calls the service.

Having this I think there is only a small step to use the IronPython WCF services in IIS. Unfortunately, I do not know how to do it...

2 comments:

Mike said...

This was really helpful, thanks!

Unknown said...

You've saved me heaps of time, fantastic!

A couple of minor points:

1) The ConfigService.exe.config shown on your web page has a case typo: the opening tag should match the closing tag's camel case:



It's correct in the downloadable file.

The ConfigService.exe was throwing an exception when I had the ConfigService.exe.config in its directory with that case difference. Curiously, the ConfigService.exe worked fine if I simply removed the faulty ConfigService.exe.config file--I suppose it reverts to relying upon default settings?

The ConfigClient.exe will only work if the ConfigClient.exe.config file is present--i.e. it cannot rely upon defaults.

2) I later realised that the reason why I couldn't get your code to run as a script rather than via the .exes was because there's a typo in your comment to "save the ConfigService.exe.config as ipy.exe.config to the folder with the IronPython interpreter ipy.exe."

It should instead be the ConfigClient.exe.config file that's saved to that location.

Cheers!