In this post we will add support for a more advanced
configuration management and really take control of WCF. We will also add
support for REST service endpoints.
Centralized configuration – the simple version
For our “late configuration” of WCF, we want to add a shared
configuration store that all components in our system utilizes. In this
example, we will use XML and xpath to create a versioned configuration store
with inheritance. With inheritance, we mean that data is inherited from the
parent data set unless it is overridden in the children. A configuration set
represents a runtime environment like dev, test or prod.
The basic structure
The xml file consists of one or more ConfigurationSet, which
may contain as many services as needed. A service consist of one or more
endpoints with the appropriate binding configuration settings.
When we are reading from the file we use the value from the
first set that has a value in the requested node.
public string GetValue(string expression)
{
return
GetValueFromVersion(expression, Version);
}
private string GetValueFromVersion(string expression, string
version)
{
if
(string.IsNullOrEmpty(version)) return null;
var expr =
CreatExpression(expression, version);
var iterator =
Navigator.Select(expr);
if
(iterator.MoveNext())
return
iterator.Current.Value;
if (expression
== "ParentSet") return null;
return
GetValueFromVersion(expression, GetParent(version));
}
public IEnumerable
{
var expr =
CreatExpression(expression, Version);
var
iterator=Navigator.Select(expr);
var list=new
List
while
(iterator.MoveNext())
{
list.Add(iterator.Current.Value);
}
return list;
}
private XPathExpression CreatExpression(string expression,
string version)
{
var expr =
Navigator.Compile(FormatExpression(expression, version));
return expr;
}
//Creates an xpath expression by combining an incoming
expression part with the set selector query
private static string FormatExpression(string expression, string
version)
{
var expr =
string.Format("ConfigurationSets/ConfigurationSet[SetName='{0}']/{1}",
version, expression);
return expr;
}
|
To make it easy to create the needed expressions we create
our own little expression builder.
public static class ExpressionBuilder
{
public static
string GetEndpointNames(string serviceName)
{
return
string.Format("Services/EndpointConfig[ServiceName='{0}']//Endpoint/EndpointName",
serviceName);
}
public static
string GetEndpointConfigValue(string serviceName, string bindingName, string
attributeName)
{
return
string.Format("Services/EndpointConfig[ServiceName='{0}']//Endpoint[EndpointName='{1}']/{2}",
serviceName, bindingName, attributeName);
}
public static
string GetClientEndpoint(string serviceName)
{
return
string.Format("Services/EndpointConfig[ServiceName='{0}']/ActiveEndpoint",
serviceName);
}
}
|
With this, we have the necessary code to read config
settings and use it in our configuration code.
First we create a
helper class that loads and initializes our configuration store. Note that this
helper should include some kind of caching mechanism that allows the
application to get new settings without restart.
internal static class ConfigurationReader
{
internal static
VersionedDocumentReader Reader;
static
ConfigurationReader()
{
Reader = new
VersionedDocumentReader();
Reader.Load(ConfigurationManager.AppSettings["ConfigPath"]);
Reader.SetVersion(ConfigurationManager.AppSettings["ConfigSet"]);
}
}
|
To make it easier to configure and name the services we add
a new attribute that sets the service name on the interface. We access this
attribute instance at runtime using reflection and read the service name. If
the service interface doesn’t have this attribute set we use the interface name
as the service name.
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Interface, AllowMultiple = false, Inherited = true)]
public sealed class
ServiceNameAttribute : Attribute
{
public string
ServiceName { get; private set; }
public
ServiceNameAttribute(string serviceName)
{
ServiceName =
serviceName;
}
}
|
Changes to the code.
We need to pass the service name to the binding creators so
they can find the correct settings to use during configuration of the binding.
public interface IBindingBuilder
{
Binding
CreateBinding(string serviceName);
}
|
Then we add a helper method to the binding builders to read
data from the configuration store and inject the binding name.
private static string GetValue(string serviceName,string
valueName)
{
return
ConfigurationReader.Reader.GetValue(ExpressionBuilder.GetEndpointConfigValue(serviceName,
"basic", valueName));
}
|
public Binding CreateBinding(string serviceName)
{
return new
BasicHttpBinding
{
Name =
"basic",
AllowCookies = false,
HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
MaxBufferPoolSize =
int.Parse(GetValue(serviceName,"MaxBufferPoolSize")),
MaxReceivedMessageSize = int.Parse(GetValue(serviceName,
"MaxReceivedSize")),
MessageEncoding =
(WSMessageEncoding)Enum.Parse(typeof(WSMessageEncoding),
GetValue(serviceName,"MessageFormat")),
TextEncoding = Encoding.GetEncoding(GetValue(serviceName,
"TextEncoding")),
ReaderQuotas
= XmlDictionaryReaderQuotas.Max,
BypassProxyOnLocal = true,
UseDefaultWebProxy = false
};
}
|
And we add a new class for creating a REST binding
internal class RestBindingBuilder:IBindingBuilder
{
public Binding
CreateBinding(string serviceName)
{
var binding =
new WebHttpBinding(WebHttpSecurityMode.None)
{
AllowCookies = false,
HostNameComparisonMode =
HostNameComparisonMode.StrongWildcard,
MaxBufferPoolSize = int.Parse(GetValue(serviceName,
"MaxBufferPoolSize")),
MaxReceivedMessageSize = int.Parse(GetValue(serviceName,
"MaxReceivedSize"))
};
return
binding;
}
private static
string GetValue(string serviceName, string valueName)
{
return
ConfigurationReader.Reader.GetValue(ExpressionBuilder.GetEndpointConfigValue(serviceName,
"rest", valueName));
}
}
|
For the WebHttpBinding to work we need to do some additional
changes to the code, I will comment this when we get to these parts later.
We also need to read the address from the configuration
store by changing the AddressHelper class to take the service name and use this
to look for the value in the xml file.
internal static string GetFormattedAddress(string
bindingType,string serviceName)
{
return
String.Format(ConfigurationReader.Reader.GetValue(ExpressionBuilder.GetEndpointConfigValue(serviceName,
bindingType, "Address"))) + "/" + bindingType;
}
|
Update ServiceConfigurationImp
to get the service name and pass it to the binding builders.
Add a method called GetServiceName. You need to get the service
interface from the service type. I have done this the quick and dirty way her
and assuming that the serviceType only implements one service interface. It the
interface is decorated with the ServiceName attribute we use the value from it,
else we use the name of the serviceType.
private string GetServiceName(Type serviceType)
{
var attrib =
serviceType.GetInterfaces()[0].GetAttribute
if (attrib ==
null) return serviceType.Name;
return
attrib.ServiceName;
}
|
Update the GetBindingTypes to read from the configuration
store and not the config file as in the previous post.
private static IEnumerable
{
var
bindingTypes =
ConfigurationReader.Reader.GetValues(ExpressionBuilder.GetEndpointNames(serviceName));
return
bindingTypes;
}
|
Update CreateEndpoint to support WebHttpBinding, where we
set the default response format to json.
private static ServiceEndpoint CreateEndpoint(Binding binding,
EndpointAddress address, ContractDescription cd)
{
if (binding is
WebHttpBinding)
return new
WebHttpEndpoint(cd, address)
{
HelpEnabled = true,
DefaultOutgoingResponseFormat = WebMessageFormat.Json,
Binding = binding
};
return new
ServiceEndpoint(cd, binding, address);
}
|
This is basically the changes needed for the service side of
WCF.
On the client side we only need to do one small change in ServiceClientContainer.
It also needs to get the service name from the attribute and we need to get the
active client binding from the configuration store.
private string GetServiceName()
{
var
serviceType = typeof(T);
var attrib =
serviceType.GetAttribute
if (attrib ==
null) return serviceType.Name;
return
attrib.ServiceName;
}
|
internal ServiceClientContainer()
{
ServiceName =
GetServiceName();
BindingType =
GetBindingType();
Url =
AddressHelper.GetFormattedAddress(BindingType,ServiceName);
}
|
public ServiceClientContainer
{
ClientFactory
= CreateChannelFactory(ServiceName, Url);
if
(ClientFactory.Endpoint.Binding is WebHttpBinding)
ClientFactory.Endpoint.Behaviors.Add(new WebHttpBehavior());
return this;
}
|
Updating the config
files
There are just a few things we need to change in the config
files for the client and service. We need to add references to the location of
the xml configuration file and which set to use.
<add
key="ConfigPath" value="C:\Settings\sentralConfiguration.xml"/>
<add
key="ConfigSet" value="Demo"/>
|
Download the code here: http://jmp.sh/b/k0b3LRA0jxOz83L4baQ8
No comments:
Post a Comment