Thursday, March 09, 2006

Doing remote procedure calls using WSE web service attachments

Note this is not an original idea. I'm sure I saw someone describe something similar on a blog a while back. I couldn't find the original source so I ended up re-implementing the same idea. WSE has a nice feature, web service attachments, that allows you to attach a binary file to a web service call. Now it doesn't take a big leap of imagination to see how you could use this to create a kind of remote procedure call. You'd serialize your parameters at the client and attach them to your standard call function. At the server end you retrieve the attachment, de-serialize your parameters, do whatever, serialize the return argument and return the call. Back at the client you de-serialize the return argument and it's done. Simple. Since the application I'm currently working on (the Civil Aviation Authority's Aircraft Register) describes all its service calls as interfaces it also means we can easily generate the client code. Now, you're thinking; "why doesn't he just use remoting?". That's a good point but this does give us a few benefits. We only have to maintain a single web method. We can use all that good stuff in WSE like security and routing. We could also implement compression easily by compressing the byte stream before we attach it. But most importantly we can still say to our managers, 'of course we're still using web services' so that they can live their SOA dream without it messing up our application design:) OK, now for an example. Let's use the canonical Add function, so our 'database layer' code looks like this (sorry they make me use VB.NET here... urgh):
Public Class MathService
    Implements AIS.Domain.Mock.IMathService

    Public Function Add(ByVal a As Integer, ByVal b As Integer) As Integer Implements Domain.Mock.IMathService.Add
        Return a + b
    End Function

End Class
Notice that it implements IMathService, our service interface. Our service client looks like this:
Public Class MathService
    Implements AIS.Domain.Mock.IMathService

    Private INTERFACE_TYPE As Type = GetType(AIS.Domain.Mock.IMathService)

    Public Function Add(ByVal a As Integer, ByVal b As Integer) As Integer Implements Domain.Mock.IMathService.Add

        Dim genericService As New AIS.Service.Generic.Client.GenericService
        Dim result As Object() = genericService.GenericFunction(INTERFACE_TYPE, "Add", New Object() {a, b})
        Return result(0)

    End Function

End Class
This also implements IMathService. It just takes the parameters and calls GenericService.GenericFunction passing in the interface, the method name, "Add" in this case and the parameters. GenericFunction looks like this:
Public Function GenericFunction(ByVal interfaceType As Type, ByVal methodName As String, ByVal parameters() As Object) As Object()

    Dim formatter As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
    Dim parameterStream As New System.IO.MemoryStream

    Dim serviceParameter As New serviceParameter(interfaceType.FullName, methodName, parameters)

    formatter.Serialize(parameterStream, serviceParameter)

    Dim returnStream As System.IO.Stream
    returnStream = InvokeAttachmentFunction(parameterStream)
    Return formatter.Deserialize(returnStream)

End Function
It wraps the interface name, method name and parameters into a ServiceParameter object, serializes it and passes it to InvokeAttachmentFunction, which looks like this:
Private Function InvokeAttachmentFunction(ByVal parameterStream As System.IO.Stream) As System.IO.Stream

    Dim proxy As New GenericServiceProxy
    proxy.Url = m_configuration.ProxyConfiguration.ServerUrl

    ' Create a new DimeAttachment class, and add the stream to that
    Dim attachment As New attachment("text/plain", parameterStream)

    ' make the call

    ' retrieve the return attachment
    ' test for attachments
    If proxy.ResponseSoapContext.Attachments.Count = 0 Then
        Throw New ApplicationException("No attachments detected")
    End If

    ' return the object stream
    Return proxy.ResponseSoapContext.Attachments(0).Stream

End Function
This creates a new GenericServiceProxy (generated by the WSDL.exe tool), attaches the parameter stream to the request, calls the web service, gets the returned attachment and returns it. Our web service looks like this:
<WebMethod()> _
Public Sub GenericAttachmentFunction()

    ' check there's at least one attachnent
    If RequestSoapContext.Current.Attachments.Count = 0 Then
        Throw New ApplicationException("No attachments detected")
    End If

    ' deserialize the attachment stream to a serviceParameter object
    Dim parameterStream As System.IO.Stream = RequestSoapContext.Current.Attachments(0).Stream
    Dim formatter As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
    Dim serviceParameter As serviceParameter = formatter.Deserialize(parameterStream)

    ' invoke the given method on the given interface with the given parameters
    Dim result() As Object = Invoke(serviceParameter.InterfaceFullName, serviceParameter.MethodName, serviceParameter.Parameters)

    ' serialize the result to a stream
    Dim resultByteStream As New System.IO.MemoryStream
    formatter.Serialize(resultByteStream, result)

    ' create an attachment with the stream
    Dim attachment As New attachment("text/plain", resultByteStream)

End Sub

Private Function Invoke(ByVal interfaceFullName As String, ByVal methodName As String, ByVal parameters() As Object) As Object()

    Dim service As Object = ServiceProvider.GetServiceInstance(interfaceFullName)

    Dim serviceType As Type = ServiceProvider.GetServiceInterfaceType(interfaceFullName)
    Dim method As System.Reflection.MethodInfo = serviceType.GetMethod(methodName)
    Dim result As Object = method.Invoke(service, parameters)
    Return New Object() {result}

End Function
The Invoke function calls a method GetServiceInstance on a class ServiceProvider. This returns an instance of a service provider of the given interface type defined in a config file. I'll go into this in another post. So once we've got our concrete instance we can simply use reflection to call the right method. Now have a look at the GenericAttachmentFunction. First it checks that an attachment was attached. Next it de-serializes the parameters to a ServiceParameter object which gives the interface and method that we want to call and the parameters to pass to it. Then we call Invoke as above which returns us an object array as a return value. The return value is then serialized, attached to the response and the function returns.

No comments: