Wednesday, September 11, 2013

How to programmatically configure WCF services and clients at runtime pt. 2.


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 GetValues(string expression)
        {
            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 GetBindingTypes(string serviceName)
        {
            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);
        }

 To enable support for rest we change the Initialize method

public ServiceClientContainer Initialize()
        {
            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: