Introduction
Tired of writing host and client (proxy) code for your common Windows Communication Foundation (WCF) services hosted by a Windows Service using standard binding and transport in code rather than error prone configuration files? I was. Not now. This article presents a solution to the problem: a generic WCF Windows Service host and client proxy class library that will host and allow you to use any WCF service contract you write. You write the service contract and data contract in one class library and implement the operations contract in another. Then modify a simplified configuration section on the generic service and your client, and restart the service. No fuss, no muss.
Background
The Visual Studio project templates and config tool can make writing WCF services easier, but more and more I found I was avoiding those tools and writing it all by hand in order to get exactly what I wanted without all the heavy, sometimes problematic, configuration files. And every time I did that, I realized the steps were identical and tedious. And isn't this why we write code, to avoid repetitive tedium?
So I decided to create a reusable framework for hosting my services. Here's what I wanted:
- Fast net.tcp binding
- Windows credentials and authentication
- Encrypted and signed transport (no packet sniffing allowed)
- Simplified configuration (hide everything I don't want to see)
- Windows Service host that behaves like a console app when I'm debugging
- Dynamic loading of the service (no changes to the host code to add a new service)
- Generic client so I don't have to write or generate proxy code
- Client that is truly
IDisposable
(hide Abort vs. Close for me) - Long timeout in DEBUG mode so I can really take my time while debugging
- Inclusion of exception details in DEBUG mode only
- Base service class with a simple
Authorize
method to support multiple Windows groups - Support for multiple Windows group authorization
- Identical configuration for server and client
- Cached resolution of service plug-in service and contract types
- Minimal number of assemblies (projects) in the solution
- Keep the implementation of the service hidden from the client
- (Probably some I've forgotten to mention)
Using the Code
Once you have defined your contracts, implemented your service operations, and written a tiny bit of configuration, the real fun begins: using the generic client. You use the WcfServiceClient<T>.Create
method, passing in the string key for the service you want to use. Then you just use the Instance
property of the returned object to use the interface of the service.
namespace WcfSvcTest
{
class Program
{
static void Main(string[] args)
{
using (var client1 = WcfServiceClient<IThatOneService>.Create("test1"))
{
var name = client1.Instance.GetName("seed");
Console.WriteLine(name);
int addressId = 8;
var address = client1.Instance.GetAddress(addressId);
Console.WriteLine("{0}", address.City);
}
using (var client2 = WcfServiceClient<IMyOtherService>.Create("test2"))
{
try
{
Console.WriteLine(client2.Instance.GetName("newseed"));
var per = client2.Instance.GetPerson(7);
Console.WriteLine("{0}, {1}", per.Name, per.Title);
}
catch (FaultException<WcfServiceFault> fault)
{
Console.WriteLine(fault.ToString());
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
Console.ReadLine();
}
}
}
To save space, I wrapped the use of the client in a try catch
on the second test service.
The Configuration
One of the critical parts of this generic WCF service host and client is the custom configuration section class WcfServiceConfigurationSection
. You can download the code to review it, but this class allows us to define a single, identical config section for both the server and the client. Here's the simplified config with two services configured. The configuration is identical for the service host and the client, greatly simplifying deployment and reducing configuration errors.
="1.0"="utf-8"
<configuration>
<configSections>
<section name="wcfServices"
type="WcfServiceCommon.WcfServiceConfigurationSection, WcfServiceCommon" />
</configSections>
<appSettings/>
<connectionStrings/>
<wcfServices consoleMode="On">
<services>
<add key="test1"
serviceAddressPort="localhost:2981"
endpointName="Test1EndPoint"
authorizedGroups="WcfServiceClients,someOtherGoup"
hostType="Test1Service.ThatOneService, Test1Service"
contractType="Test1Common.IThatOneService, Test1Common" />
<add key="test2"
serviceAddressPort="localhost:2981"
endpointName="Test2EndPoint"
authorizedGroups="WcfServiceClients,someOtherGoup"
hostType="Test2Service.MyOtherService, Test2Service"
contractType="Test2Common.IMyOtherService, Test2Common" />
</services>
</wcfServices>
</configuration>
Installing the Windows Service
You will note that the <wcfServices>
node in the configuration has an attribute called "consoleMode
" which tells the generic host to run in console mode when the value is "On
" and to otherwise run as a Windows Service. This makes for easy debug sessions. Just set your Visual Studio solution to start the host service before your client starts, and you can debug straight through to your implementation.
To install the service as a Windows Server, you will need to use the InstallUtil.exe in your .NET Framework directory (e.g., C:\Windows\Microsoft.NET\Framework64\v4.0.30319). For help with this utility, see the MSDN article on installutil.
The Service Contract and Implementation
Here's the simple service and data contract:
namespace Test1Common
{
[ServiceContract]
public interface IThatOneService
{
[OperationContract, FaultContract(typeof(WcfServiceFault))]
string GetName(string seed);
[OperationContract, FaultContract(typeof(WcfServiceFault))]
Address GetAddress(int id);
}
[DataContract]
public class Address
{
[DataMember]
public string Line1 { get; set; }
[DataMember]
public string Line2 { get; set; }
[DataMember]
public string City { get; set; }
[DataMember]
public string State { get; set; }
[DataMember]
public string Zip { get; set; }
}
}
And here's the implementation of the service. It really is simple, but note that it inherits from WcfServiceBase
(see below).
namespace Test1Service
{
public class ThatOneService : WcfServiceBase, IThatOneService
{
public string GetName(string seed)
{
return "Mr. " + seed.ToUpper();
}
public Address GetAddress(int id)
{
return new Address
{
Line1 = "100 Main Street",
Line2 = "P.O. Box 100",
City = "MallTown",
State = "TX",
Zip = "12345"
};
}
}
}
The WcfServiceBase Class
The base class WcfServiceBase
provides two very important common requirements for the service implementation. First, you get a ServiceBehavior
attribute tacked on with IncludeExceptionDetailInFaults
set to true
when in DEBUG build. The second is the Authorize
method to make authorizing the calling client against the list of authorized groups in the configuration very easy. If the calling client is found in one of those groups, the Authorize
method does nothing. Otherwise, an exception is thrown.
namespace WcfServiceCommon
{
#if(DEBUG)
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall,
MaxItemsInObjectGraph = 131072, IncludeExceptionDetailInFaults = true)]
#else
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall,
MaxItemsInObjectGraph = 131072, IncludeExceptionDetailInFaults = false)]
#endif
public abstract class WcfServiceBase
{
public void Authorize()
{
string[] groups = null;
Type serviceType = this.GetType();
var configItem = Config.GetServiceConfig(serviceType);
if (null != configItem)
{
groups = configItem.Item.AuthorizedGroups.Split(',');
}
if (null != groups)
{
PrincipalPermission[] pps = new PrincipalPermission[groups.Length];
for (int i = 0; i < groups.Length; i++)
{
pps[i] = new PrincipalPermission(null, groups[i]);
}
PrincipalPermission pp = pps[0];
if (groups.Length > 0)
{
for (int i = 1; i < groups.Length; i++)
{
pp = (PrincipalPermission)pp.Union(pps[i]);
}
}
pp.Demand();
}
else
throw new SecurityException("Group is null");
}
}
}
Conclusion
I hope you find this WCF plug-in architecture and the accompanying code useful. I will certainly be using it. If you do use it, please let me know how it goes.
Points of Interest
Note: You have to remember that the WcfServiceHost requires the "common" and the "service" assemblies of your dynamically loaded services in its bin folder. The client (see the WcfSvcTest project in the solution) will also need a copy of the "common" assemblies in its bin folder. You'll find I'm doing that for the test using post-build commands (copy $(TargetPath) $(SolutionDir)WcfServiceHost\bin\debug\). And of course, both need to have identical config sections as shown in the code.
More Important Note: The service host project does not reference your "common" or "service" implementations. They are dynamically loaded using the information in the config file. Your client project will need a reference to the WcfServiceCommon assembly as well as your "common" contracts assembly, of course, but the WcfServiceCommon's generic WcfServiceClient will dynamically load your "common" contracts assembly using the config file information, so the WcfServiceCommon assembly does not require a reference to your "common" contract assembly.
Update: On Making Asynchronous Calls
A user asked about asynchronous usage. Download v2 of the source. Take a look at the updated WcfServiceClient<T>
class with an eye to the AsyncBegin
method and the AsyncCompleted
event. For a more generalized version of this, check out my new blog post: generic asynchronous wrapper.
History
- 5/3/2010 - Some spelling corrections and one or two grammar and heading fixes in the text. Thanks to my reviewer and editor Charles W.
- 5/13/2010 - Updates with v2 of source which includes the generic client's asynchronous support.