This last week I've been exploring WCF, and my aim was building a prototype client-server application, .NET remoting style. The requirements that I had for this applications were:
- Serialize object graphs like you could do with .NET remoting (this application doesn't need platform independence).
- Share data types between client and server, because they will be using the same platform. Also share the service interface itself.
- Use http transport.
- Use user name and password authentication.
The information in this post has been gathered from a bunch of other articles/blog posts, which I'll try to cite, if I didn't forget to write them down as I ran across it. The default scenario that microsoft wants developers to use is SOA, not sharing types between client/server, but generating proxy classes on the client side (which are not very useful in my experience, except in the simplest of applications). So deviating from this preferred path leads to quite a bit of googling to make WCF behave to satisfied aforementioned requirements. Also I have to mention Ingo Rammer's From .NET Remoting to Windows Communication Foundation (WCF) article. It comes quite close to meeting the requirements above, but it still uses some client project class generation (only for the service interface), and it doesn't demonstrate authentication. Time to get started! The source code of the entire project can be downloaded from here.
Creating the shared data assembly
Ok, let's start at the bottom, the data classes that will be shared by client and server. Because they will be shared they need to be in a separate assembly: BusinessApp.Data.dll. Below is the source code for the Product and Category class.
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;
namespace BusinessApp.Data {
public class Product {
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
}
Note that use of Contract attributes (DataContract/DataMember) is not required since .NET 3.5 SP1. The Category class holds a Product collection and a CheapestProduct property.
using System;
using System.Collections.Generic;
using System.Text;
namespace BusinessApp.Data {
public class Category {
private List<Product> products = new List<Product>();
public string Name { get; set; }
public List<Product> Products {
get {
return products;
}
}
public Product CheapestProduct { get; set; }
}
}
The instance referenced by CheapestProduct is also referenced by the Products collection. So that means we need to be able to serialize object graphs. WCF's default behavior is to just serialize them separately, so you'd end up with two copies of the originally single Product instance. Next I'm defining a (nonsensical) interface for querying products and categories:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace BusinessApp.Data {
[ServiceContract(SessionMode = SessionMode.NotAllowed)]
public interface IService {
[OperationContract]
List<Product> GetAllProducts();
[OperationContract]
[CustomDataContractSerializer(true)]
List<Category> GetAllCategories();
}
}
The service interface is annotated with a ServiceContract attribute, and all operations are annotated with an OperationContract attribute. Method GetAllCategories is a bit special, because this method needs object graph serialization. Unfortunately there's no way you can configure this behavior in an app.config file. MS apparently wanted to discourage this practice by making the DataContractSerializer.PreserveObjectReferences property readonly (thus non-configurable). But, there is still a way to tell WCF to serialize object graphs in a convoluted manner, described here. This article shows how you create two classes CustomDataContractSerializerAttribute and CustomDataContractSerializerOperationBehavior to modify the DataContractSerializer behavior. The usage is pretty simple: just annotate the service interface method with [CustomDataContractSerializer(true)] and it will serialize object graphs properly (see method GetAllCategories above).
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Text;
namespace BusinessApp.Data {
/// <summary>
/// From
/// <see cref="http://blog.hill-it.be/post/2007/08/22/MaxItemsInObjectGraph-and-keeping-references-when-serializing-in-WCF"/>.
/// </summary>
public class CustomDataContractSerializerAttribute : Attribute, IOperationBehavior {
private bool _preserveReferences = false;
private int _maxItemsInObjectGraph = 65536;
public CustomDataContractSerializerAttribute(bool preserveReferences) {
_preserveReferences = preserveReferences;
}
public CustomDataContractSerializerAttribute(
bool preserveReferences,
int maxItemsInObjectGraph
) {
_preserveReferences = preserveReferences;
_maxItemsInObjectGraph = maxItemsInObjectGraph;
}
public void AddBindingParameters(
OperationDescription description,
BindingParameterCollection parameters
) {
}
public void ApplyClientBehavior(
OperationDescription description,
ClientOperation proxy
) {
IOperationBehavior innerBehavior =
new CustomDataContractSerializerOperationBehavior(
description,
_preserveReferences,
_maxItemsInObjectGraph
);
innerBehavior.ApplyClientBehavior(description, proxy);
}
public void ApplyDispatchBehavior(
OperationDescription description,
DispatchOperation dispatch
) {
IOperationBehavior innerBehavior =
new CustomDataContractSerializerOperationBehavior(
description,
_preserveReferences,
_maxItemsInObjectGraph
);
innerBehavior.ApplyDispatchBehavior(description, dispatch);
}
public void Validate(OperationDescription description) {
}
}
}
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel.Description;
using System.Text;
using System.Xml;
namespace BusinessApp.Data {
/// <summary>
/// From
/// <see cref="http://blog.hill-it.be/post/2007/08/22/MaxItemsInObjectGraph-and-keeping-references-when-serializing-in-WCF"/>.
/// </summary>
public class CustomDataContractSerializerOperationBehavior :
DataContractSerializerOperationBehavior
{
private bool _preserveReferences = false;
private int _maxItemsInObjectGraph = 65536;
public CustomDataContractSerializerOperationBehavior(
OperationDescription operationDescription,
bool preserveReferences,
int maxItemsInObjectGraph
) : base(operationDescription)
{
_preserveReferences = preserveReferences;
_maxItemsInObjectGraph = maxItemsInObjectGraph;
}
public override XmlObjectSerializer CreateSerializer(
Type type,
string name,
string ns,
IList<Type> knownTypes
) {
return new DataContractSerializer(
type,
name,
ns,
knownTypes,
this.MaxItemsInObjectGraph,
this.IgnoreExtensionDataObject,
_preserveReferences,
this.DataContractSurrogate
);
}
public override XmlObjectSerializer CreateSerializer(
Type type,
XmlDictionaryString name,
XmlDictionaryString ns,
IList<Type> knownTypes
) {
return new DataContractSerializer(
type,
name,
ns,
knownTypes,
_maxItemsInObjectGraph,
this.IgnoreExtensionDataObject,
_preserveReferences,
this.DataContractSurrogate
);
}
}
}
Creating the Server
The service is hosted in a server process, in this prototype a simple console application is used as a server. First the IService interface is implemented.
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using BusinessApp.Data;
namespace BusinessApp.WcfService {
public class Service : IService {
public List<Product> GetAllProducts() {
Console.WriteLine("GetAllProducts()");
List<Product> products = new List<Product>();
products.Add(new Product { Name = "Apple", Description = "Juicy apple", Price = 1.20m });
products.Add(new Product { Name = "Tomato", Description = "Red tomato", Price = 1.35m });
return products;
}
public List<Category> GetAllCategories() {
Console.WriteLine("GetAllCategories()");
List<Category> categories = new List<Category>();
Category foodCategory = new Category { Name = "Food" };
foodCategory.Products.AddRange(GetAllProducts());
foodCategory.CheapestProduct = foodCategory.Products[0];
categories.Add(foodCategory);
return categories;
}
}
}
Below is the server's main program:
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;
using BusinessApp.Data;
using BusinessApp.WcfService;
namespace Server {
class Program {
static void Main(string[] args) {
try {
ServiceHost host = new ServiceHost(typeof(Service));
host.Open();
Console.WriteLine("Press a key to exit.");
Console.ReadKey();
host.Close();
} catch (Exception e) {
Console.WriteLine("Exception caught: " + e.ToString());
Console.WriteLine("Press a key to exit.");
Console.ReadKey();
}
}
}
}
The service is configured using an app.config file, the WsHttpBinding is used for end-to-end security (ws-security):
<?xml version="1.0"?>
<configuration>
<system.serviceModel>
<services>
<service behaviorConfiguration="BusinessApp.WcfService.ServiceBehavior" name="BusinessApp.WcfService.Service">
<!-- Must add base address for host -->
<host>
<baseAddresses>
<add baseAddress="http://localhost:8081/Service"/>
</baseAddresses>
</host>
<endpoint address="http://localhost:8081/Service"
binding="wsHttpBinding"
contract="BusinessApp.Data.IService">
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="BusinessApp.WcfService.ServiceBehavior">
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
Creating the Client
In this prototype a simple Win Forms application is used as client. Visual Studio 2008 allows adding a 'Service Reference' to your project, and this normally creates proxy classes in your project that can communicate with your service. When the project knows the data types used by the service (i.e. has a reference to the assembly containing the data types), then it optionally will not generate proxy classes for the data types. It however will still generate a mirror for the service interface IService itself. Though tempting as 'Add Service Reference' is, the service can be used using interface IService just as easily without resorting to the evil 'Add Service Reference' as we'll see below (reference: Using WCF with Automatic Client Proxies). Most of the effort goes into creating the client configuration file (app.config):
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address="http://localhost:8081/Service"
binding="wsHttpBinding"
contract="BusinessApp.Data.IService"
name="WsHttpService" >
</endpoint>
</client>
</system.serviceModel>
</configuration>
The client code of the main form:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Windows.Forms;
using BusinessApp.Data;
namespace BusinessApp.WindowsFormsApplication {
public partial class MainForm : Form {
public MainForm() {
InitializeComponent();
}
private void getAllProductsToolStripMenuItem_Click(object sender, EventArgs e) {
ChannelFactory<IService> channelFactory = new ChannelFactory<IService>("WsHttpService");
channelFactory.Credentials.UserName.UserName = "john";
channelFactory.Credentials.UserName.Password = "P@ssword";
IService service = channelFactory.CreateChannel();
try {
List<Product> products = service.GetAllProducts();
if (products == null) {
textBox.Text += "null" + Environment.NewLine;
} else {
foreach (Product product in products) {
textBox.Text += "Product: " + product.Name + Environment.NewLine;
}
}
textBox.Select(textBox.Text.Length, 0);
textBox.ScrollToCaret();
} finally {
// Not needed, because the security context is disabled at the server's binding configuration.
//((IChannel)service).Close();
}
}
private void getAllCategoriesToolStripMenuItem_Click(object sender, EventArgs e) {
ChannelFactory<IService> channelFactory = new ChannelFactory<IService>("WsHttpService");
channelFactory.Credentials.UserName.UserName = "john";
channelFactory.Credentials.UserName.Password = "P@ssword";
IService service = channelFactory.CreateChannel();
try {
List<Category> categories = service.GetAllCategories();
if (categories == null) {
textBox.Text += "null" + Environment.NewLine;
} else {
foreach (Category category in categories) {
textBox.Text += "Category: " + category.Name + Environment.NewLine;
textBox.Text += "Cheapest product: " + (category.CheapestProduct == null ? "null" : category.CheapestProduct.Name) + Environment.NewLine;
foreach (Product product in category.Products) {
textBox.Text += "Product: " + product.Name + Environment.NewLine;
}
// Let's see if we got an object graph, if so the
// product instance is also part of the category.Products collection
// and also changed.
if (category.CheapestProduct != null) {
category.CheapestProduct.Name = "XYZ";
textBox.Text += "--- After change ---" + Environment.NewLine;
textBox.Text += "Category: " + category.Name + Environment.NewLine;
textBox.Text += "Cheapest product: " + (category.CheapestProduct == null ? "null" : category.CheapestProduct.Name) + Environment.NewLine;
foreach (Product product in category.Products) {
textBox.Text += "Product: " + product.Name + Environment.NewLine;
}
}
}
}
textBox.Select(textBox.Text.Length, 0);
textBox.ScrollToCaret();
} finally {
// Not needed, because the security context is disabled at the server's binding configuration.
//((IChannel)service).Close();
}
}
}
}
As one can see, making a service call is very easy: create a ChannelFactory, and then create a channel, which returns a proxy that implements the IService interface. Now we can actually demo something! Whooo! Here's a screenshot of how the client should look like after making the call and logging the result:

Figure 1: WCF client demo screenshot
Adding Authentication
To make it more useful in a real world business application, authentication has to be added. This prototype demonstrates use of Message authentication and use of an X509 certificate to encrypt the message. WCF doesn't allow username and password to be transported without any encryption. First let's make changes to the service configuration:
<?xml version="1.0"?>
<configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="MySecureBinding">
<security mode="Message">
<message clientCredentialType ="UserName" establishSecurityContext="false" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<services>
<service behaviorConfiguration="BusinessApp.WcfService.ServiceBehavior" name="BusinessApp.WcfService.Service">
<!-- Must add base address for host -->
<host>
<baseAddresses>
<add baseAddress="http://localhost:8081/Service"/>
</baseAddresses>
</host>
<endpoint address="http://localhost:8081/Service"
binding="wsHttpBinding"
bindingConfiguration="MySecureBinding"
contract="BusinessApp.Data.IService">
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="BusinessApp.WcfService.ServiceBehavior">
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
<serviceCredentials>
<!-- Create using this command: -->
<!-- makecert.exe -sr LocalMachine -ss My -a sha1 -n CN=MyServerCert -sky exchange -pe -->
<serviceCertificate
findValue="MyServerCert"
x509FindType="FindBySubjectName"
storeLocation="LocalMachine"
storeName="My" />
<userNameAuthentication
userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="BusinessApp.WcfService.CustomUserNameValidator, BusinessApp.WcfService"/>
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
The bindings element tells WCF to secure messages, and use 'UserName' as the credential type. The endpoint refers to this binding by this attribute: bindingConfiguration="MySecureBinding".
Note that I've sneakily added the very very import attribute establishSecurityContext="false" to the binding's security's message configuration. If this is not done, then the channel MUST be closed by the client after it's not needed anymore. If it is not closed, then it would result in a dangling session at the server. The default session limit at the server is 10, so the server would no longer accept new calls after just 10 non properly closed sessions, this can happen in a split second! The wsHttpBinding by default has transport-level sessions if it has security (default) or reliable messaging. The downside to disabling the security context is that messages become larger, as every message must be individually secured (thanks to Nicholas Allen for clarifying this).
Further the service behavior element has some new child elements. The userNameAuthentication pointsto a custom .NET class that will authenticate the user through his user name/password. The serviceCertificate points to the certificate used for message encryption. Details were stolen from here. Create the test certificate using this command:
makecert.exe -sr LocalMachine -ss My -a sha1 -n CN=MyServerCert -sky exchange –pe
The validator that is referred to by the configuration file is as follows (it only allows one user):
using System;
using System.Collections.Generic;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Web;
namespace BusinessApp.WcfService {
public class CustomUserNameValidator : UserNamePasswordValidator {
// This method validates users. It allows only user john with password P@ssword.
// This code is for illustration purposes only and
// MUST NOT be used in a production environment because it is NOT secure.
public override void Validate(string userName, string password) {
if (null == userName || null == password) {
throw new ArgumentNullException();
}
if (!(userName == "john" && password == "P@ssword")) {
throw new SecurityTokenException("Unknown Username or Password");
}
}
}
}
The client configuration file needs to be changed too:
<configuration>
<system.serviceModel>
<client>
<endpoint address="http://localhost:8081/Service"
behaviorConfiguration="myClientBehavior"
binding="wsHttpBinding"
bindingConfiguration="MySecureBinding"
contract="BusinessApp.Data.IService"
name="WsHttpService" >
<identity>
<dns value="MyServerCert"/>
</identity>
</endpoint>
</client>
<bindings>
<wsHttpBinding>
<binding name="MySecureBinding">
<security mode="Message">
<message clientCredentialType ="UserName" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="myClientBehavior">
<clientCredentials>
<serviceCertificate>
<authentication
certificateValidationMode="Custom"
customCertificateValidatorType="BusinessApp.WindowsFormsApplication.CustomX509CertificateValidator,BusinessApp.WindowsFormsApplication" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
New in the configuration is the wsHttpBinding specifying the clientCredentialType to be UserName. By default WCF doesn't like to use self signed certificates, so a small hack is needed to make WCF a bit more forgiving. This is done by creating a CustomX509CertificateValidator class:
using System;
using System.Collections.Generic;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Web;
namespace BusinessApp.WindowsFormsApplication {
/// <summary>
/// Need this class to accept self signed certificates.
/// See <see cref="http://www.devatwork.nl/index.php/2007/05/31/wcf-username-authentication/"/>.
/// </summary>
public class CustomX509CertificateValidator : X509CertificateValidator {
public override void Validate(X509Certificate2 certificate) {
// validate argument
if (certificate == null) {
throw new ArgumentNullException("certificate");
}
// check if the name of the certifcate matches
if (certificate.SubjectName.Name != "CN=MyServerCert") {
throw new SecurityTokenValidationException("Certificated was not issued by trusted issuer");
}
}
}
}
Now all that's left to be done is specify the username/password in the client code as the service call is made:
ChannelFactory<IService> channelFactory = new ChannelFactory<IService>("WsHttpService");
channelFactory.Credentials.UserName.UserName = "john";
channelFactory.Credentials.UserName.Password = "P@ssword";
IService service = channelFactory.CreateChannel();
List<Product> products = service.GetAllProducts();
That's it, mission completed!
Sniffing network traffic
I used to use the very nice tcptrace tool to put in between client and server and look at the http messages when anything went wrong. This no longer seemed to work when working with WCF, because WCF would check the service address, and it detected a mismatch in the address (port). So by default WCF is a bit more critical than .NET remoting used to be. But luckily there's a solution to this! Just change the AddressFilterMode to Any using the ServiceBehaviorAttribute. Add this attribute to your Service implementation class:
[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
public class Service : IService {
Version history
Nov 12, 2009: set establishSecurityContext to false to disable sessions for the wsHttpBinding.