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)
proxy.RequestSoapContext.Attachments.Add(attachment)
' make the call
proxy.GenericAttachmentFunction()
' 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)
ResponseSoapContext.Current.Attachments.Add(attachment)
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.