
Anyone who’s done much in the way of programming in Windows Communications Foundation (WCF) will be well aware of the problems that can occur due to even minor configuration inconsistencies between client and service endpoint settings. The fact those settings are normally defined within application configuration files means that a typo, or one or more settings that are different between files can grind connectivity to a halt.
This can especially be an issue where different developers are programming the web service host and the client code that is consuming it.
After many frustrating hours investigating issues with connectivity between service and client endpoints in new WCF services that had just been developed, I decided to look at alternative approaches to solve the frequent problems that arose. It turns out that the WCF configuration that is normally hosted in configuration files can also be defined programmatically and associated with the service at runtime in the same way the XML-based configuration does. Essentially, that is all WCF is doing anyway – importing the XML configuration into memory and storing it as objects.
Let’s define a base-class that can be used to define per-service settings at design time, eradicating the need for XML-based configuration in application configuration files. This will mean we have strongly typed configurations, defined in assemblies that can be shared directly between web service and client endpoints to avoid any possibility for mismatch.
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
namespace My.Wcf.Utilities
{
/// <summary>
/// Defines a base class for describing WCF configurations in code.
/// </summary>
public abstract class WcfConfigBase
{
/// <summary>Gets the authentication behaviour the web service uses.</summary>
public ServiceAuthorizationBehavior Authentication { get; protected set; }
/// <summary>Gets the web service's core behaviours.</summary>
public BehaviorsSection Behaviours { get; protected set; }
/// <summary>
/// Gets the communications binding the web service should use.
/// </summary>
public Binding Binding { get; protected set; }
/// <summary>
/// Gets the web service debug behaviours that should be configured.
/// </summary>
public ServiceDebugBehavior Debug { get; protected set; }
/// <summary>
/// Gets the maximum size a data object being transmitted between endpoints
/// can be.
/// </summary>
public int MaxItemsInObjectGraph { get; protected set; }
/// <summary>Gets the web service metadata settings.</summary>
public ServiceMetadataBehavior Metadata { get; protected set; }
/// <summary>Gets the web service throttling behaviour.</summary>
public ServiceThrottlingBehavior Throttling { get; protected set; }
}
/// <summary>
/// Provides methods to simplify initialisation of WCF endpoints.
/// </summary>
public static class WcfInit
{
/// <summary>
/// Returns a new factory that can be used to instantiate WCF
/// communication channels from client to server.
/// </summary>
/// <param name="host">
/// The name or IP-address of the machine hosting the web service.
/// </param>
/// <param name="port">
/// The port number the web service will be hosted on.
/// (A value in the range 49152 - 65535 recommended.)
/// </param>
/// <param name="addressSuffix">
/// The web service address suffix (if required.)
/// </param>
/// <param name="endpointName">The name of the endpoint being targeted.</param>
/// <param name="config">The web service's configuration settings.</param>
/// <returns>A WCF channel factory.</returns>
/// <exception cref="System.ArgumentException">
/// Thrown if the supplied parameters are invalid in some way.
/// </exception>
public static ChannelFactory<T> CreateClientFactory<T>(
string host, int port, string addressSuffix, string endpointName,
WcfConfigBase config)
{
if (string.IsNullOrEmpty(host) || host.Trim() == "")
throw new ArgumentException("A host name must be provided.");
if (string.IsNullOrEmpty(endpointName) || endpointName.Trim() == "")
throw new ArgumentException("An endpoint name must be provided.");
if (config == null || config.Binding == null)
{
throw new ArgumentException(
"Service configuration settings must be provided.");
}
// Make sure the port number is valid
// (if required - named pipes don't use ports and HTTP connections
// may implicitly use the default port (80, or a port below the
/// 'private range' like 8000))
if (!(config.Binding is NetNamedPipeBinding) &&
!config.Binding.GetType().Name.StartsWith("Http") &&
(port < 49152 || port > 65535))
{
throw new ArgumentException(
"The supplied port number is invalid - port numbers in the " +
"range 49152-65535 are recommended for private services.");
}
if (addressSuffix == null) addressSuffix = "";
addressSuffix = addressSuffix.Trim();
if (addressSuffix != "" &&
!addressSuffix.StartsWith("/"))
{
addressSuffix = "/" + addressSuffix;
}
string address = "";
if (config.Binding is BasicHttpBinding ||
config.Binding is NetMsmqBinding)
{
address = string.Format(
"http://{0}{1}{2}/{3}",
host,
port > 0 ? ":" + port : "",
addressSuffix,
endpointName);
}
else if (config.Binding is NetTcpBinding)
{
address = string.Format(
"net.tcp://{0}:{1}{2}/{3}",
host,
port,
addressSuffix,
endpointName);
}
else if (config.Binding is NetNamedPipeBinding)
{
address = string.Format(
"net.pipe://{0}{1}/{2}",
host,
addressSuffix,
endpointName);
}
else
{
throw new ArgumentException(string.Format(
"The WCF binding type '{0}' was not recognised.",
config.Binding.GetType().FullName));
}
EndpointAddress endpoint = new EndpointAddress(address);
ChannelFactory<T> factory =
new ChannelFactory<T>(config.Binding, endpoint);
return factory;
}
/// <summary>
/// Returns a new <see cref="System.ServiceModel.ServiceHost"/> based on
/// the supplied configuration.
/// </summary>
/// <param name="host">
/// The name or IP-address of the machine hosting the service.
/// </param>
/// <param name="port">
/// The port number the web service will be hosted on.
/// (A value in the range 49152 - 65535 recommended.)
/// </param>
/// <param name="addressSuffix">
/// The web service address suffix (if required.)
/// </param>
/// <param name="endpointName">The name of the endpoint being hosted.</param>
/// <param name="config">The web service's configuration settings.</param>
/// <param name="contract">
/// The specific <see cref="System.Type"/> of the interface that forms the
/// contract between server and client endpoints.
/// </param>
/// <param name="implementation">
/// The specific <see cref="System.Type"/> of the class that implements
/// the contract.
/// </param>
/// <returns>
/// A configuration <see cref="System.ServiceModel.ServiceHost"/> object.
/// </returns>
/// <exception cref="System.ArgumentException">
/// Thrown if the supplied parameters are invalid in some way.
/// </exception>
public static ServiceHost CreateServiceHost(
string host, int port, string addressSuffix, string endpointName,
WcfConfigBase config, Type contract, Type implementation)
{
if (string.IsNullOrEmpty(host) || host.Trim() == "")
throw new ArgumentException("A host name must be provided.");
if (string.IsNullOrEmpty(endpointName) || endpointName.Trim() == "")
throw new ArgumentException("An endpoint name must be provided.");
if (config == null || config.Binding == null)
{
throw new ArgumentException(
"Service configuration settings must be provided.");
}
if (contract == null || !contract.IsInterface)
{
throw new ArgumentException(
"A service contract (interface type) must be provided.");
}
if (implementation == null)
{
throw new ArgumentException(
"A contract implementation class must be provided.");
}
// Make sure the port number is valid
// (if required - named pipes don't use ports and HTTP connections
// may implicitly use the default port (80, or a port below the
/// 'private range' like 8000))
if (!(config.Binding is NetNamedPipeBinding) &&
!config.Binding.GetType().Name.StartsWith("Http") &&
(port < 49152 || port > 65535))
{
throw new ArgumentException(
"The supplied port number is invalid - port numbers in the " +
"range 49152-65535 are recommended for private services.");
}
if (addressSuffix == null) addressSuffix = "";
addressSuffix = addressSuffix.Trim();
if (addressSuffix != "" && !addressSuffix.StartsWith("/"))
{
addressSuffix = "/" + addressSuffix;
}
Uri address = null;
if (config.Binding is BasicHttpBinding ||
config.Binding is NetMsmqBinding)
{
address = new Uri(string.Format(
"http://{0}{1}{2}",
host,
port > 0 ? ":" + port : "",
addressSuffix));
}
else if (config.Binding is NetTcpBinding)
{
address = new Uri(string.Format(
"net.tcp://{0}:{1}{2}",
host,
port,
addressSuffix));
}
else if (config.Binding is NetNamedPipeBinding)
{
address = new Uri(string.Format(
"net.pipe://{0}{1}",
host,
addressSuffix));
}
else
{
throw new ArgumentException(string.Format(
"The WCF binding type '{0}' was not recognised.",
config.Binding.GetType().FullName));
}
ServiceHost service = new ServiceHost(implementation, address);
ServiceEndpoint endpoint = service.AddServiceEndpoint(
contract, config.Binding, endpointName);
ServiceBehaviorAttribute serialiser = new ServiceBehaviorAttribute();
if (service.Description.Behaviors.Contains(serialiser.GetType()))
{
serialiser =
service.Description.Behaviors[typeof(ServiceBehaviorAttribute)]
as ServiceBehaviorAttribute;
service.Description.Behaviors.Remove(typeof(ServiceBehaviorAttribute));
}
serialiser.MaxItemsInObjectGraph = config.MaxItemsInObjectGraph;
service.Description.Behaviors.Add(serialiser);
if (service.Description.Behaviors.Contains(
config.Authentication.GetType()))
{
service.Description.Behaviors.Remove(config.Authentication.GetType());
}
if (service.Description.Behaviors.Contains(
config.Debug.GetType()))
{
service.Description.Behaviors.Remove(config.Debug.GetType());
}
if (service.Description.Behaviors.Contains(
config.Metadata.GetType()))
{
service.Description.Behaviors.Remove(config.Metadata.GetType());
}
if (service.Description.Behaviors.Contains(
config.Throttling.GetType()))
{
service.Description.Behaviors.Remove(config.Throttling.GetType());
}
service.Description.Behaviors.Add(config.Authentication);
service.Description.Behaviors.Add(config.Debug);
service.Description.Behaviors.Add(config.Metadata);
service.Description.Behaviors.Add(config.Throttling);
return service;
}
}
}
Code language: C# (cs)
Now we’ve defined the scaffolding through which we’ll expose our configuration and the code to host and consume web services, let’s look at an example of using those resources…
For a specific project, we need to develop a web service to provide some capability. We create a new Visual Studio solution, with a class library project that we’ll use for defining the contract, WCF settings, and a client wrapper class to make consumption of the service even easier. For example:
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using My.Wcf.Utilities;
namespace My.WcfService
{
/// <summary>
/// Provides a default set of WCF configuration settings, using TCP bindings.
/// </summary>
public sealed class WcfSettings : WcfConfigBase
{
/// <summary>
/// Creates a new instance of the class.
/// </summary>
public WcfSettings()
{
// 500MB max pool size
const int poolSize = 524288000;
// 50MB object graph to cater for large document files
const int maxItemsInGraph = 52428800;
MaxItemsInObjectGraph = maxItemsInGraph;
// Behaviours
Behaviours = new BehaviorsSection();
DataContractSerializerElement serialising =
new DataContractSerializerElement {
MaxItemsInObjectGraph = maxItemsInGraph
};
EndpointBehaviorElement endpointBehaviour =
new EndpointBehaviorElement("MyEndpointBehaviour");
endpointBehaviour.Add(serialising);
Behaviours.EndpointBehaviors.Add(endpointBehaviour);
ServiceBehaviorElement serviceBehaviour =
new ServiceBehaviorElement("MyServiceBehaviour");
ServiceDebugElement debug = new ServiceDebugElement
{
HttpsHelpPageEnabled = false,
HttpHelpPageEnabled = false,
IncludeExceptionDetailInFaults = true
};
ServiceAuthorizationElement security = new ServiceAuthorizationElement
{
ImpersonateCallerForAllOperations = true;
};
ServiceMetadataPublishingElement metadata =
new ServiceMetadataPublishingElement();
serviceBehaviour.Add(serialising);
serviceBehaviour.Add(debug);
serviceBehaviour.Add(security);
serviceBehaviour.Add(metadata);
Behaviours.ServiceBehaviors.Add(serviceBehaviour);
// Debugging
this.Debug = new ServiceDebugBehavior
{
HttpHelpPageEnabled = false,
HttpsHelpPageEnabled = false,
IncludeExceptionDetailInFaults = true
};
// Service metadata
this.Metadata = new ServiceMetadataBehavior();
// Authentication
this.Authentication = new ServiceAuthorizationBehavior
{
PrincipalPermissionMode = PrincipalPermissionMode.UseWindowsGroups,
ImpersonateCallerForAllOperations = true
};
// Throttling
this.Throttling = new ServiceThrottlingBehavior();
// Bindings
NetTcpBinding tcpBinding = new NetTcpBinding
{
Name = BindingConfigurationName,
MaxBufferPoolSize = poolSize,
MaxBufferSize = MaxObjectsInGraph,
MaxConnections = 100,
MaxReceivedMessageSize = MaxObjectsInGraph,
PortSharingEnabled = true
};
tcpBinding.ReaderQuotas.MaxStringContentLength = MaxObjectsInGraph;
tcpBinding.ReaderQuotas.MaxArrayLength = MaxObjectsInGraph;
tcpBinding.ReaderQuotas.MaxBytesPerRead = MaxObjectsInGraph;
tcpBinding.ReaderQuotas.MaxNameTableCharCount = MaxObjectsInGraph;
tcpBinding.ReaderQuotas.MaxDepth = 1024;
this.Binding = tcpBinding;
}
}
/// <summary>
/// Defines the methods the WCF service supports.
/// </summary>
[ServiceContract]
public interface IServiceMethods
{
/// <summary>
/// Executes the 'do this' command.
/// </summary>
[OperationContract]
void DoThis();
/// <summary>
/// Executes the 'do this as well' command.
/// </summary>
/// <param name="from">
/// The source of the request (e.g. application's or user's name).
/// </param>
/// <param name="number">The number to set.</param>
/// <returns>The generated object.</returns>
[OperationContract]
object DoThisAsWell(string from, int number);
}
/// <summary>
/// Provides an implementation of <see cref="IServiceMethods"/> that simplifies
/// the task of creating clients to consume the WCF service.
/// </summary>
public sealed class ClientComms() : IServiceMethods
{
private ChannelFactory<IServiceMethods> _factory = null;
/// <summary>
/// Creates a new instance of the class.
/// </summary>
/// <param name="host">The name of the host, or IP address.</param>
/// <param name="port">The port number to use for communication.</param>
/// <param name="addressSuffix">The URI address suffix, if required.</param>
/// <param name="endpointName">The name of the endpoint being accessed.</param>
public ClientComms(
string host, int port, string addressSuffix, string endpointName)
{
_factory = WcfInit.CreateClientFactory<IServiceMethods>(
host, port, addressSuffix, endpointName, new WcfSettings());
}
#region IServiceMethods Members
void IServiceMethods.DoThis()
{
IServiceMethods target = null;
try
{
target = _factory.CreateChannel();
target.DoThis();
}
finally
{
if (target != null)
{
// If closing the channel fails it'll cause a faulted
// exception so just force abort the channel in this case!
try
{
((IChannel)target).Close();
}
catch
{
((IChannel)target).Abort();
}
}
}
}
object IServiceMethods.DoThisAsWell(string whoFrom, int number)
{
IServiceMethods target = null;
try
{
target = _factory.CreateChannel();
return target.DoThisAsWell(whoFrom, number);
}
finally
{
if (target != null)
{
// If closing the channel fails it'll cause a faulted
/// exception so just force abort the channel in this case!
try
{
((IChannel)target).Close();
}
catch
{
((IChannel)target).Abort();
}
}
}
}
#endregion
}
}
Code language: C# (cs)
Now the web service definitions have been defined we add a new project to the solution (e.g. a Windows Service project) to expose the service capability…
using System;
using System.Collections.Generic;
using System.ServiceProcess;
using System.ServiceModel;
using My.Wcf.Utilities;
namespace My.WcfService.Host
{
public sealed class TheService : IServiceMethods
{
// Methods and code bodies omitted for brevity.
}
public class MainService : ServiceBase
{
private ServiceHost host = null;
protected override void OnStart(string[] args)
{
host = WcfInit.CreateServiceHost(
"myMachine", 49887, "", "SheHasHerMethods", new WcfSettings(),
typeof(IServiceMethods), typeof(TheService));
host.Open();
}
protected override void OnStop()
{
if (host != null)
{
try
{
host.Close();
}
catch
{
host.Abort();
}
}
}
}
}
Code language: C# (cs)
That’s all there is to establishing the web service host. A few simple lines of code in the last class. In a real scenario you would define your host name, port, etc. within either an application configuration file (e.g. appSettings section) or within an environment-specific database, but this simple example demonstrates the concept. You may be thinking that we’ve still potentially got settings being defined in the application configuration file in that case… Well, yes, potentially we may have but it’ll be a LOT easier to declare, manage and maintain a few entries in the appSettings section of such a file than trying to synchronise the myriad settings associated with WCF in them.
Now that the web service host has been developed, all that is left is to consume it. In the client project we’d need to reference the WCF utilities assembly and the definitions assembly (and System.ServiceModel, obviously.) Then we can connect to the remote web service simply by doing…
IServiceMethods service = new ClientComms("myMachine", 49887, "", "SheHasHerMethods");
service.DoThis();
object outcome = service.DoThisAsWell("me", 6551);
Code language: C# (cs)
Again, the client configuration could be simplified in a real scenario by moving the host name, port, etc. into an application configuration file section, or to a database table, and then retrieving the settings data from there. If this was done then the ClientComms class could be modified to pull settings direct from the source so that it wasn’t necessary to have a parameterised constructor at all.
As you can see, we’ve now pushed the complex problem of configuring WCF endpoints into pre-baked classes and also reduced the day-to-day coding required to host or consume web services to a few lines of code.