Wednesday, March 26, 2008

Source code for the amendments to the Oran's Spring.NET WCF integration

This entry is providing the source code for the amendments described here. Basically, this is for enabling the "duplicate" object types in the same Spring.NET configuration when some of them are used via the WCF DI provider.

First the code for the instance provider itself:

public class DependencyInjectionInstanceProvider : IInstanceProvider
{
    private string objectName; 

    public DependencyInjectionInstanceProvider(){} 

    public DependencyInjectionInstanceProvider(string objectName)
    {
        this.objectName = objectName;
    } 

    public object GetInstance(InstanceContext instanceContext)
    {
        return GetInstance(instanceContext, null);
    }
    public object GetInstance(InstanceContext instanceContext, Message message)
    {
        return ContextRegistry.GetContext().GetObject(objectName);
        //TODO: (SD) Review compatibility of the instancing modes
    }
    public void ReleaseInstance(System.ServiceModel.InstanceContext instanceContext, 
        object instance) 
    { 
        //TODO: (SD) see what Bruno and Mark got in here, to have more than IDisposable release
    }
}
Service behavior follows:
    public class DependencyInjectionServiceBehavior : IServiceBehavior
    {
        private IDictionary<Type, string> serviceTypeMappings = new Dictionary<Type, string>();
        private IDictionary<string, string> serviceTypeStringMappings = new Dictionary<string, string>();

        internal IDictionary<string, string> ServiceTypeStringMappings
        {
            get { return serviceTypeStringMappings; }
        }
        public DependencyInjectionServiceBehavior(){}

        public void ApplyDispatchBehavior(
            ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
            {
                ChannelDispatcher cd = cdb as ChannelDispatcher;

                if (cd != null) //TODO: (SD) Assert for it
                {
                    string objectName = ResolveObjectName(serviceDescription);

                    foreach (EndpointDispatcher ed in cd.Endpoints)
                    {
                        
                        
                        ed.DispatchRuntime.InstanceProvider =
                            new DependencyInjectionInstanceProvider(objectName);
                    }
                }
            }
        }

        private string ResolveObjectName(ServiceDescription serviceDescription)
        {
            string objectName = null;
            // Find the object name to create when the end point is called
            // This is either the service type class name when used without
            // the service type mapping or service type mapping object name.
            if (!serviceTypeMappings.TryGetValue(serviceDescription.ServiceType,
                out objectName))
            {
                // Apply the convention over configuration approach
                // There should be exactly one object name as validated in the Validate method
                return ContextRegistry.GetContext().GetObjectNamesForType(
                    serviceDescription.ServiceType)[0];
            }
            return objectName;
        }
        public void AddBindingParameters(ServiceDescription serviceDescription,
            ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
            BindingParameterCollection bindingParameters) { }

        public void Validate(ServiceDescription serviceDescription,
            ServiceHostBase serviceHostBase) 
        {
            // Validate that all mapping types are available, if there is an 
            // issue with the type, at least this is shown during activation
            foreach (string key in serviceTypeStringMappings.Keys)
            {
                serviceTypeMappings.Add(Type.GetType(key, true), serviceTypeStringMappings[key]);
            }

            string objectName;

            if (!serviceTypeMappings.TryGetValue(serviceDescription.ServiceType,
                out objectName))
            {
                //Validate for the convention over configuration approach
                string[] objectNames = ContextRegistry.GetContext().GetObjectNamesForType(serviceDescription.ServiceType);

                if (objectNames.Length != 1)
                {
                    throw new InvalidOperationException(
                        string.Format(CultureInfo.InvariantCulture,
                        "Exactly one <object> definition for the {0} service with type {1} is required unless you define" +
                       " the service type mapping explicitely via the DependencyInjectionElement. Found <object>s: {2}",
                       serviceDescription.ServiceType.Name, serviceDescription.ServiceType.FullName,
                       objectNames.Aggregate(String.Empty, (acc, next) => (acc += next + ",")).TrimEnd(',')));

                }
            }
            else
            {
                if (!ContextRegistry.GetContext().ContainsObject(objectName))
                {
                    throw new InvalidOperationException(
                        string.Format(CultureInfo.InvariantCulture,
                        "No <object> definition found in the spring.net configuration for the object with name {0}" +
                        " to which the service {1} of type {2} is mapped! Create the <object> definition or change the mapping.",
                        objectName, serviceDescription.ServiceType.Name, serviceDescription.ServiceType.FullName));

                }
            }
        }
    }

Main focus of a service behavior is actually around the Validate method where we try to catch as many things as possible so we know about what is configured wrong at the service activation time. The reason for using the string keyed dictionary of types in addition to the type keyed one is actually the need to drag this conversion between the types named in the mapping configuration and real types to the Validate method. A little price though for being compliant with the WCF validation during service activation.

Already mentioned mappings code to follow:
[Serializable]
    public class ServiceTypeMappingConfigElement : ConfigurationElement
    {
        #region Properties


        [ConfigurationProperty("type",
        IsRequired = true, IsKey=true)]
        [XmlAttribute()]
        public string Type
        {
            get
            {
                return (string)this["type"];
            }
            set
            {
                this["type"] = value;
            }
        }
        [ConfigurationProperty("objectName",
            IsRequired = true, IsKey = false)]
        [XmlAttribute()]
        public string ObjectName
        {
            get
            {
                return (string)this["objectName"];
            }
            set
            {
                this["objectName"] = value;
            }
        }


        #endregion Properties

        #region Constructors

        public ServiceTypeMappingConfigElement()
        {
        }
        public ServiceTypeMappingConfigElement(string type)
        {
            Type = type;
        }
        
        #endregion Constructors

        
        public override string ToString()
        {
            return base.ToString() + " type=" + Type + ", objectName=" + ObjectName;
        }
    }
And the configuration element collection:
    public class ServiceTypeMappingConfigElementCollection : ConfigurationElementCollection
    {
        public ServiceTypeMappingConfigElementCollection()
        {
        }

        public override
            ConfigurationElementCollectionType CollectionType
        {
            get
            {
                return
                    ConfigurationElementCollectionType.AddRemoveClearMap;
            }
        }

        protected override
            ConfigurationElement CreateNewElement()
        {
            return new ServiceTypeMappingConfigElement();
        }


        protected override
            ConfigurationElement CreateNewElement(
            string elementName)
        {
            return new ServiceTypeMappingConfigElement(elementName);
        }


        protected override Object
            GetElementKey(ConfigurationElement element)
        {
            return ((ServiceTypeMappingConfigElement)element).Type;
        }


        public new string AddElementName
        {
            get
            { return base.AddElementName; }

            set
            { base.AddElementName = value; }

        }

        public new string ClearElementName
        {
            get
            { return base.ClearElementName; }

            set
            { base.AddElementName = value; }

        }

        public new string RemoveElementName
        {
            get
            { return base.RemoveElementName; }


        }

        public new int Count
        {

            get { return base.Count; }

        }


        public ServiceTypeMappingConfigElement this[int index]
        {
            get
            {
                return (ServiceTypeMappingConfigElement)BaseGet(index);
            }
            set
            {
                if (BaseGet(index) != null)
                {
                    BaseRemoveAt(index);
                }
                BaseAdd(index, value);
            }
        }

        new public ServiceTypeMappingConfigElement this[string Name]
        {
            get
            {
                return (ServiceTypeMappingConfigElement)BaseGet(Name);
            }
        }

        public int IndexOf(ServiceTypeMappingConfigElement objectEndPoint)
        {
            return BaseIndexOf(objectEndPoint);
        }

        public void Add(ServiceTypeMappingConfigElement objectEndPoint)
        {
            BaseAdd(objectEndPoint);

            // Add custom code here.
        }

        protected override void
            BaseAdd(ConfigurationElement element)
        {
            BaseAdd(element, false);
            // Add custom code here.
        }

        public void Remove(ServiceTypeMappingConfigElement objectEndPoint)
        {
            if (BaseIndexOf(objectEndPoint) >= 0)
                BaseRemove(objectEndPoint.Type);
        }

        public void RemoveAt(int index)
        {
            BaseRemoveAt(index);
        }

        public void Remove(string name)
        {
            BaseRemove(name);
        }

        public void Clear()
        {
            BaseClear();
            // Add custom code here.
        }
    }
And the last one in this configuration food chain is the Behaviour extension element:
public class DependencyInjectionElement : BehaviorExtensionElement
    {
        #region BehaviorExtensionElement

        public override Type BehaviorType
        {
            get { return typeof(DependencyInjectionServiceBehavior); }
        }

        protected override object CreateBehavior()
        {
            DependencyInjectionServiceBehavior behavior = new DependencyInjectionServiceBehavior();

            foreach (ServiceTypeMappingConfigElement el in Mappings)
            {
                behavior.ServiceTypeStringMappings.Add(el.Type, el.ObjectName);
            }
            return behavior;
        }

        #endregion

        #region Properties

        [ConfigurationProperty("mappings",
            IsDefaultCollection = true, IsRequired = true)]
        public ServiceTypeMappingConfigElementCollection Mappings
        {

            get
            {
                return base["mappings"] as ServiceTypeMappingConfigElementCollection;
            }
        }


        #endregion
    }

This is it for a code, now configuration:

Setup a behaviour extension:
    <extensions>
      <behaviorExtensions>
        <add name="dependencyInjection" type="[YourNamespace].DependencyInjectionElement, YourAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<!-- Use the strong name type naming convention here even if you would not have your assembly strong named --> </behaviorExtensions> </extensions>
Setup the behaviour itself:
      <serviceBehaviors  >
        ....
<behavior name="DependencyInjectionServiceBehavior"> <dependencyInjection> <mappings> <add type="YourTypeFQN, YourAssembly" objectName="YourObjectName" /> </mappings> </dependencyInjection> </behavior> </serviceBehaviors>

Use it in your services configuration:
<services>
.... <service behaviorConfiguration="DependencyInjectionServiceBehavior" name="YourTypeFQN, YourAssembly"> <host> <!-- setup the base address if not hosted under IIS --> </host> <endpoint name="rest2" binding="webHttpBinding" bindingConfiguration="AjaxBinding" contract="YourInterfaceOrContractNamespace.IYourContractInterface" behaviorConfiguration="AjaxBehavior" />
</service> </services>

Of course, your service setup details can be totally different, so the only important detail is to match the behavior configuration name and your implementation type (highlighted in the xml). Once this is done you can have the multiple objects setup of the same type but distinct object names:
      <object name="YourObjectName" type="YourTypeFQN, YourAssembly" singleton="false">
        ...      </object>
<object name="YourObjectNameForTheSameTypeThatDoesntConflictWithTheAboveOneAnymore" type="YourTypeFQN, YourAssembly" singleton="false"> ... </object>

This is it for today. Code is still subject to refactorings and enhancements, but should give the idea and hopefully help if you are coping with the same subject :).

No comments: