Windows Communication Foundation (WCF) is a framework from
Microsoft that helps developers build (web) services. WCF serves as an
abstraction layer and allows the developer to focus on _what_ to build, not on
all the how’s, if’s and but’s. Choosing transport, protocol and security is
done later [config time].
In this post, we will look into how to configure WCF programmatically
and postpone the choices to run-time. Note that the service needs to recycle in
order to obtain new config settings.
One challenge we meet in software development is
configurations sets that change from development, test and production
environments. Getting all the endpoints and settings correct in each
environment is time consuming and error prone.
One solution is to centralize the configuration and
introduce environmental variables to the server images. Let us say that we
create a windows server image and inject an appSettings value for environment
e.g. development. And we also might want to add a value that can resolve the
correct url’s for the services and endpoints needed in a given environment. Add
these settings to the machine.config file of your server image, this will make
the settings available to all services and applications running on the server.
<add key="Environment" value="Dev" >
<add key="UrlPrefix" value="Dev." > var endpointUrl=String.Format(“http://{0}MyService.MyCompany.com/Service.svc”,ConfigurationManager.AppSettings[“UrlPrefix”]); |
This neat little trick makes deployment easy and no
configuration transformation is needed. However, you are still stuck with the
wcf configuration from your development environment, unless you use config
transformations. Config transformations are still a static approach, changing
the settings means that you need to change the transformation and update the
config file in your service and the service clients.
This is where we will start to play with WCF and configuring
this runtime.
The service:
In this post we’ll only cover .svc hosted services. First
you need to create a service host factory, which is responsible for creating
the service host. During creation of the service host you can configure the
service and the available endpoints.
Please note that the sample code uses the really poor man’s
IoC (switch - new), in real life scenarios change this to your preferred IoC
container. In my production version of this, I use a custom lightweight IoC
container.
Service Host Factory
To be able to
configure the service at runtime you need to create a new service host factory.
Do this by overriding ServiceHostFactory’s CreateServiceHost method. In this
method you can control all aspects of the service you are hosting.
protected override ServiceHost CreateServiceHost(Type
serviceType, Uri[] baseAddresses)
{
var
serviceHost = new ServiceHost(serviceType, baseAddresses);
var
configurator = CreateConfigurator();
configurator.ConfigureServiceHost(serviceHost, serviceType);
SetBehavior(serviceHost);
return
serviceHost;
}
|
First you create the service host, just ‘new up’ the WCF
default implementation. We will add the endpoints to this later.
To prepare for more advanced scenarios in later posts we
will create an interface to handle the configuration of the endpoints. Create
an interface, IServiceConfiguration, with a method called ConfigureServiceHost.
After the endpoints have been added to the service host, we
want to set some behavior for the service, like open and close timeout values.
Building the endpoints
Each endpoint has its own binding, all bindings share the
same base type, Binding. This allows us to create an abstraction over this
using an interface, IBindingBuilder.
var bindingTypes =
ConfigurationManager.AppSettings["bindingTypes"].Split(';');
var
contractDescription =
ContractDescription.GetContract(serviceType.GetInterfaces()[0]);
contractDescription.Namespace = "http://demo.wcf.org/" +
serviceType.Name;
foreach (var
bindingType in bindingTypes)
{
var bindingBuilder =
GetBindingBuilder(bindingType);
var binding = bindingBuilder.CreateBinding();
var
endpoint = CreateEndpoint(binding, new
EndpointAddress(GetFormattedAddress(bindingType)), contractDescription);
serviceHost.AddServiceEndpoint(endpoint);
}
private static ServiceEndpoint CreateEndpoint(Binding binding,
EndpointAddress address, ContractDescription cd)
{
return new
ServiceEndpoint(cd, binding, address);
}
|
class BasicBindingBuilder : IBindingBuilder
{
public Binding
CreateBinding()
{
return new
BasicHttpBinding
{
Name =
"basic",
AllowCookies = false,
HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
MaxBufferPoolSize = int.MaxValue,
MaxReceivedMessageSize = int.MaxValue,
MessageEncoding = WSMessageEncoding.Mtom,
TextEncoding = Encoding.UTF8,
ReaderQuotas = XmlDictionaryReaderQuotas.Max,
BypassProxyOnLocal = true,
UseDefaultWebProxy = false
};
}
}
|
public Binding CreateBinding()
{
return new
WSHttpBinding(SecurityMode.None)
{
Name = "ws",
AllowCookies = false,
TransactionFlow = false,
HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
MaxBufferPoolSize = int.MaxValue,
MaxReceivedMessageSize =
int.MaxValue,
MessageEncoding = WSMessageEncoding.Text,
TextEncoding = Encoding.UTF8,
ReaderQuotas = XmlDictionaryReaderQuotas.Max,
BypassProxyOnLocal = true,
UseDefaultWebProxy = false
};
}
|
The values to configure the bindings with can be read from a
configuration store. I’ll post a simple version of a centralized configuration
store later.
Bringing it all
together
To enable the service to be configured by our new factory we
need to add one simple statement to the svc markup (marked in red).
<%@ ServiceHost Language="C#"
Debug="true" Service="Syrstad.Blog.WcfDemo.Service.DemoService"
CodeBehind="DemoService.svc.cs" Factory="Syrstad.Blog.WcfDemo.WcfConfigurationModule.HostFactory"
%>
|
When the development team is building both the service and
the client(s) it is a good thing to extract the interface and the data classes
in a separate assembly for easy distribution, through the build system, to the
projects that depend on it.
Configuring the client
The client also needs to be configured in the same way. We
want to ensure that resources used by WCF are properly cleaned, so we will
create a container for the proxy that we make disposable.
For ease of use we will create a factory for client proxy
containers, called ClientProxyFactory. This has one static method that returns
an initialized instance of the container. We implement the container as a
generic and uses the type parameter is the interface used in the service itself.
public static class ClientProxyFactory
{
public static
ServiceClientContainer
{
//Replace with
your IoC of choice
return new
ServiceClientContainer
}
}
|
The service container creates a new channel factory with the
appropriate settings collected from the interface and the config store. This factory
is used to create the actual proxy. The factory is kept as a instance field so
it will be a part of the dispose.
For binding creation, we reuse the BindingFactory used by
the service so that there is no possibility for configuration mismatch.
internal ServiceClientContainer()
{
ServiceName =
typeof(T).Name;
BindingType =
ConfigurationManager.AppSettings["useClientBinding"];
Url =
AddressHelper.GetFormattedAddress(BindingType);
}
public
ServiceClientContainer
{
ClientFactory
= CreateChannelFactory(ServiceName, Url);
return this;
}
internal
ChannelFactory
{
var builder =
GetProxyBuilder();
var
channelFactory = new
ChannelFactory
foreach
(OperationDescription op in channelFactory.Endpoint.Contract.Operations)
{
var
dataContractBehavior =
op.Behaviors[typeof(DataContractSerializerOperationBehavior)] as
DataContractSerializerOperationBehavior;
if
(dataContractBehavior != null)
{
dataContractBehavior.MaxItemsInObjectGraph = int.MaxValue;
}
}
return
channelFactory;
}
private static
IProxyBindingBuilder GetProxyBuilder()
{
return new
ProxyBindingBuilder();
}
|
Next we need a GetClient method that creates the proxy and
returns it to the client code.
public T GetClient()
{
if (Client ==
null) Client = ClientFactory.CreateChannel(new EndpointAddress(Url));
return Client;
}
|
This approach makes it easy and clean to create and use runtime
generated WCF proxies and ensure proper disposal of the underlying resources.
Download the demo solution and play with WCF. I apologize for
the use of regions in some of the source files; they are added as an effort to
make this post and the code easier to follow.
Please note that I have done some code cleaning (separated
some code into new methods) after I wrote this post.
The demo solution
Project
|
Description
|
Syrstad.Blog.WcfDemo
|
Contains WCF Service interface and data transfer objects for the
demo
|
Syrstad.Blog.WcfDemo.Client
|
The demo client
|
Syrstad.Blog.WcfDemo.Service
|
The demo service
|
Syrstad.Blog.WcfDemo.WcfConfigurationModule
|
The WCF configuration code. This will be updated in future posts
to include centralized config store and more binding builders
|
No comments:
Post a Comment