How to build a custom WCF client using ChannelFactory
In "service" speak a client (or proxy) is just the code that handles all the communication details with the actual service. It allows you to call methods on a class (the client) as if they were located in your project. It abstracts you from most of the implementation details. This is a really good thing.
In .net there are a few different ways that you can create a WCF client to a service endpoint:
-Using Svcutil.exe from the command line (MSDN documentation here)
-Using Visual Studio's "Add Service Reference" (this is really just VS GUI-fying Svcutil.exe for you - documentation here)
-Use custom code that leverages the generic System.ServiceModel.ChannelFactory class (MSDN documentation here)
-Here we are going to discuss the third option where we create a client to communicate with the destination service.
This can be useful in scenarios where requiring client config is not ideal (i.e. a shared assembly)
or you have a need to have more control over the lifetime of some of the objects (Channel Factory is an expensive class to create).
For the below examples you can assume we import the System.ServiceModel namespace.
This is the interface that we will be using for the samples:
Public Interface IWCFTest
Function CrunchNumbers(ByVal value As Integer) As String
Function CalculateThings() As Boolean
End Interface
Here is a bare-bones way to wrap a service call using a custom client (you'll notice reference to "binding" and "endpoint" - these are defined at the end of this page as they are not critical to the example)
Public Function CrunchNumbers(ByVal proxyValue As Integer) As String
Dim client As IWCFTest = Nothing
Dim ret As String = Nothing
Try
'Create the client channel using the generic static (shared) ChannelFactory class
client = ChannelFactory(Of IWCFTest).CreateChannel(binding, endpoint)
ret = client.CrunchNumbers(proxyValue)
DirectCast(client, IClientChannel).Close() ' need to close the channel
Return ret
Catch ex As Exception
DirectCast(client, IClientChannel).Abort() ' need to abort since we weren't able to close properly
Throw
End Try
End Function
One thing you'll notice here is that there is really only a couple lines of code that are directly related to the code you probably care about.
The rest is WCF details that is just a necessity. This seems OK but has a few issues with it:
Creating a new ChannelFactory per call (that is what happens with the generic static method we are calling) is fairly expensive as .net has to generate a
dynamic proxy every time you do this.
This would get really ugly if you had to wrap a lot of methods as almost all of the above code would be repeated and identical in all the methods.
To solve the first problem of the overhead in creating the ChannelFactory you could simply cache this in a static variable and be done with that problem.
If that were done the channel creation would simply change like the below.
From: client = ChannelFactory(Of IWCFTest).CreateChannel(binding, endpoint)
To: client = myCachedFactory.CreateChannel() ' where myCachedFactory is a static variable, likely initialized in the static constructor
This is a pretty simple change that will improve performance but does nothing to address the second issue of having a bunch of boiler plate code in every method you need to proxy.
In order to directly address that we can leverage generics and lambda support.
The below method abstracts all the gory WCF details out of the actual method call and it lets us proxy methods very simply.
Private Function SafeServiceInvoke(Of TServiceType, TResult)(ByVal serviceCall As Func(Of TServiceType, TResult)) As TResult
Dim channel As TServiceType = myCachedFactory.CreateChannel()
Dim result As TResult
Try
result = serviceCall(channel) ' execute the function that is passed in
DirectCast(channel, IClientChannel).Close() ' close the channel when done
Return result
Catch ex As Exception
' since we got here there was an issue with the service call, the channel faulted so we need to abort instead of close it
DirectCast(channel, IClientChannel).Abort()
Throw ' would/could log first but need to propagate the exception so the client knows it didn't succeed (could also transition to a custom exception)
End Try
End Function
Using the above generic method the wrapped call now looks like:
Public Function CrunchNumbers(ByVal proxyValue As Integer) As String
Return SafeServiceInvoke(Function(s As IWCFTest) s.CrunchNumbers(proxyValue))
End Function
This allows us to take the original "CrunchNumbers" function from above which was 11 lines and turn it a single line - that's a huge improvement.
Aside from just being less repetitious code it is also far more readable and less likely to be a maintenance problem in the future.
Note that the above "SafeServiceInvoke" method returns a value (of type TResult),
if your service had a method that didn't return a value you could overload SafeServiceInvoke and have it take in an Action(Of TServiceType)
this will allow you to call it generically as well but doesn't have the return component to it.
You will also notice in "SafeServiceInvoke" we use "myCachedFactory" to create the channel.
For the purposes of this example we relied on myCachedFactory.CreateChannel() returning the same type as the generic TServiceType that was passed in.
If you had a requirement to support more than a single service (interface) you could change:
From: Dim channel As TServiceType = myCachedFactory.CreateChannel()
To: Dim channel As TServiceType = FactoryCache(Of TServiceType).CreateChannel()
Where "FactoryCache" supports some custom generic dictionary of ChannelFactories and manages them.
This would also guarantee that the result of "CreateChannel" was a "TServiceType".
*Note - The above sample uses a Cached Factory and Channel-Per-Call pattern. Depending on the usage of your service you may want to re-use the channel
(basically store it at the class level).
The downside to this is that now your client needs to implement ID imposable so the channel can be properly closed and this in turn requires your consumers to dispose of your proxy.
Channel creation is relatively cheap so channel-per-call should be favored in most scenarios.
Some example code (generic service invocation method, binding & endpoint) was taken directly from the MyApp.YearlyAccount assembly.
You can look at the actual code in TFS, the location is on the assembly's page.
====================================================================================
Related code from the above example
Creating an EndpointAddress, this is really simple you just need the URI to your service -
in a typical scenario you need to decide this based on some runtime value though.
The below is how the MyApp.YearlyAccount assembly determines the service address to use.
You'll note that it accesses the "GMIServer" environment variable directly - this is only done because it is a shared assembly and we didn't want it to take on a dependency.
You should use the properties in MyApp Core Library to get this info.
Private Function CreateEndpoint() As EndpointAddress
Dim environment As String = Nothing
Dim serviceAddress As String = Nothing
Dim appSetting As String = Nothing
Const endpointAppSetting As String = "MyApp.YearlyAccount-ServiceUrl"
appSetting = System.Configuration.ConfigurationManager.AppSettings(endpointAppSetting)
If Not String.IsNullOrEmpty(appSetting) Then
serviceAddress = appSetting
Else
environment = System.Environment.GetEnvironmentVariable("GMIServer")
' if environment doesn't exist we use prod since this could be used from a client machine
If String.IsNullOrEmpty(environment) OrElse environment.Equals("production", System.StringComparison.OrdinalIgnoreCase) Then
serviceAddress = "https://YearlyAccount.MyApp.com/YearlyAccountService.svc"
ElseIf "qa".Equals(serviceAddress, StringComparison.OrdinalIgnoreCase) Then
serviceAddress = "https://q.YearlyAccount.MyApp.com/YearlyAccountService.svc"
Else ' default to dev
serviceAddress = "https://d.YearlyAccount.MyApp.com/YearlyAccountService.svc"
End If
End If
Return New EndpointAddress(serviceAddress)
End Function
Creating the binding is also pretty simple, you just need to adjust for the type of binding you want and the attributes/behaviors it should contain
The below sample uses the BasicHttpBinding and windows security.
Private Shared Function CreateBinding(ByVal address As Uri) As Channels.Binding
Dim binding As New BasicHttpBinding()
binding.Name = "YearlyAccountBinding" ' for testing purposes we need to allow for both http & https
If address.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) Then
binding.Security.Mode = BasicHttpSecurityMode.Transport
Else
binding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly
End If
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows
Return binding
End Function