For the last couple of weeks I've been playing with WCF, Windows Workflow, and other stuff while putting together a new app that we need.
It's been a steep learning curve - trying to keep things familiar (as in, the other guys on the team will still be able to follow the code, and follow generally the same architectural principals) while bringing in this new stuff. It's been extremely fun though :)
I thought I'd share what I've come up with as our solution for keeping the client and server in sync, while doing my best to not share business entities on both sides of the boundary. In a true service driven environment, this isn't an issue - you only want to match schemas. In proprietry systems, especially where it's the one team writing both sides of the service, you can be a bit more lax about this. Especially in the apps we write, where the web service is used to keep the systems distributed as opposed to service oriented, it's even easier to want to share business objects on both sides of the boundary. And we do. Some people have a problem with this, but I don't. So nyah :)
One thing I've wished, however, is that the server side logic of a shared entity didn't make it to the client - and the client side logic of the entity didn't also exist on the server. It's more annoying to me for the bloat rather than the anything else, and I've been meaning to do something about it for a while.
In this new app, I think I've found a pretty good way of still sharing the right schema, but being able to split the classes into client and server side version.
WCF is much more heavily interface driven than old .net web services ever have been, and it was this that made me realise what's needed. Given the fact that the service definition itself must be a shared interface, why not share the interfaces of the business objects too?
Sharing the service interface can be done in two ways - either the client autogenerates it based it on the wsdl, or you share the assembly that contains the definition between both the client and the server solutions. I (obviously) chose the second.
So I set up a shared assembly to define the interfaces that will dictate all business objects that can be passed to and from the service. An example:
Public Interface IBusinessObject1
Property One() As String
Property Two() As String
End Interface
I also wrote the service interface in this same assembly, but I wrote it as a generic interface - this way I didn't need to demand any particular class (and so therefore doesn't need to be shared in the shared assembly) but I could demand specific interfaces to be in use. eg:
<ServiceContract()> _
Public Interface ISampleService(Of BusinessObject1 As IBusinessObject1, BusinessObject2 As IBusinessObject2)
<OperationContract()> _
Function SampleCall1() As BusinessObject1
<OperationContract()> _
Function SampleCall2(ByVal businessObject As BusinessObject1) As BusinessObject2
End Interface
Then on both the client and the service, you just need to implement real server side business objects that implement these interfaces, inserting the desired extra logic along the way (In these examples, I only overrode ToString() - just to prove the point.
<DataContract(Namespace:="http://myApp/BusinessObject1")> _
Public Class BusinessObject1
Implements SharedInterfaces.IBusinessObject1
Private mOne As String
Private mTwo As String
<DataMember()> _
Public Property One() As String Implements SharedInterfaces.IBusinessObject1.One
Get
Return mOne
End Get
Set(ByVal value As String)
mOne = value
End Set
End Property
<DataMember()> _
Public Property Two() As String Implements SharedInterfaces.IBusinessObject1.Two
Get
Return mTwo
End Get
Set(ByVal value As String)
mTwo = value
End Set
End Property
Public Overrides Function ToString() As String
Return "Service entity 1 - " & One & " " & Two
End Function
End Class
And the client version:
<DataContract(Namespace:="http://myApp/BusinessObject1")> _
Public Class BusinessObject1
Implements SharedInterfaces.IBusinessObject1
Private mOne As String
Private mTwo As String
<DataMember()> _
Public Property One() As String Implements SharedInterfaces.IBusinessObject1.One
Get
Return mOne
End Get
Set(ByVal value As String)
mOne = value
End Set
End Property
<DataMember()> _
Public Property Two() As String Implements SharedInterfaces.IBusinessObject1.Two
Get
Return mTwo
End Get
Set(ByVal value As String)
mTwo = value
End Set
End Property
Public Overrides Function ToString() As String
Return "Client entity 1 - " & One & " " & Two
End Function
End Class
There's one very special peice of code here that makes it all work. WCF (specifically, the DataContractSerializer) is a smart cookie, but it's dumb at the same time. It knows if what you want (a deserialized client side object) is creatable from the XML that's come over the wire. If you went with all the defaults, a service side BusinessObject1 would not deserialize into a client side BusinessObject1. But by making sure you use the exact same namespace in the DataContractAttribute() on both ends, one happily turn magically into the other :)
We also need to define our contract interface in a concrete manner:
<ServiceContract()> _
Public Interface ISample
Inherits SharedInterfaces.ISampleService(Of BusinessObject1, BusinessObject2)
End Interface
The only other magic required is coming up with a client side proxy stub - just like is needed in plain web services. Another thing they've improved with WCF is writing them is a hell of a lot easier now that we have generics. To write a proxy by hand, you need to inherit from ClientBase, and implement your service interface (or something that looks like it). So on the client, you define a contract version of the service interface (like I did above for the service end, but this time using client side classes for the type params) and then:
Public Class SampleProxy
Inherits System.ServiceModel.ClientBase(Of ISample)
Implements ISample
Public Function SampleCall1() As BusinessObject1 Implements SharedInterfaces.ISampleService(Of BusinessObject1, BusinessObject2).SampleCall1
Return MyBase.InnerProxy.SampleCall1()
End Function
Public Function SampleCall2(ByVal businessObject As BusinessObject1) As BusinessObject2 Implements SharedInterfaces.ISampleService(Of BusinessObject1, BusinessObject2).SampleCall2
Return MyBase.InnerProxy.SampleCall2(businessObject)
End Function
Public Sub New()
MyBase.New()
End Sub
Public Sub New(ByVal endpointConfigurationName As String)
MyBase.New(endpointConfigurationName)
End Sub
Public Sub New(ByVal endpointConfigurationName As String, ByVal remoteAddress As String)
MyBase.New(endpointConfigurationName, remoteAddress)
End Sub
Public Sub New(ByVal endpointConfigurationName As String, ByVal remoteAddress As System.ServiceModel.EndpointAddress)
MyBase.New(endpointConfigurationName, remoteAddress)
End Sub
Public Sub New(ByVal binding As System.ServiceModel.Channels.Binding, ByVal remoteAddress As System.ServiceModel.EndpointAddress)
MyBase.New(binding, remoteAddress)
End Sub
End Class
The most annoying part of the proxy is that you have to reimplement the 5 different constructors. More than likely not all need to be done, but I've left them in for niceness.
After adding the magic bits to the app.config's on both the client and the service, it all just magically works.
It's especially hard to remember all the xml that needs to go into the app.config files. I generally create dummy projects, add a service reference the 'usual' way, and then copy the stuff out of the app.config's and paste it into my real app :)
I've put together a working sample for people that want to see it all put together and running. You can download it from here, and give a try. Remember, you'll need the Beta2 bits of WinFX - earlier versions won't work.
I'm especially interested in any comments others might have to make about this. I think it's really neat, but have I done anything here that is especially poor form?