Monday, October 20, 2014

Registering runtime types on DryIoc

DryIoc is missing an API for registering a runtime type

Hence we can't automate this WCF registration..

_container.RegisterDelegate<TheServiceContracts.IProductService>(x => 
    (TheServiceContracts.IProductService) ServiceModelHelper.CreateService(
        typeof(TheServiceContracts.IProductService),  
        System.ServiceModel.BasicHttpBinding(), 
        new System.ServiceModel.EndpointAddress("http://localhost:1337/ProductService.svc")
    )
, Reuse.InResolutionScope);
    
_container.RegisterDelegate<TheServiceContracts.IMemberService>(x => 
    (TheServiceContracts.IMemberService) ServiceModelHelper.CreateService(
        typeof(TheServiceContracts.IMemberService), 
        new System.ServiceModel.BasicHttpBinding(), 
        new System.ServiceModel.EndpointAddress("http://localhost:1337/MemberService.svc")
    )
, Reuse.InResolutionScope);

_container.RegisterDelegate<TheServiceContracts.IOrderService>(x => 
    (TheServiceContracts.IOrderService) ServiceModelHelper.CreateService(
        typeof(TheServiceContracts.IOrderService), 
        new System.ServiceModel.BasicHttpBinding(), 
        new System.ServiceModel.EndpointAddress("http://localhost:1337/OrderService.svc")
    )
, Reuse.InResolutionScope);


..into this:

_container.Register<System.ServiceModel.BasicHttpBinding>(DryIoc.Reuse.Singleton, x => x.GetConstructor(new Type[] { }));

foreach (var contractType in typeof(TheServiceContracts.IMemberService).Assembly.GetTypes())
{
    _container.RegisterDelegate(contractType,
        r =>
        {
            var binding = r.Resolve<System.ServiceModel.BasicHttpBinding>();

            var svcAddress = "http://localhost:1337/" + contractType.Name.Substring(1) + ".svc";
            var endpointAddress = new System.ServiceModel.EndpointAddress(svcAddress);


            return ServiceModelHelper.CreateService(contractType, new System.ServiceModel.BasicHttpBinding(), endpointAddress);
        }, Reuse.InResolutionScope);

}   


And this available API in DryIoc can't be used, parameters in generics and casting must be determined at compile-time. Hence we can't pass the runtime type contractType variable as a parameter to generics and casting
_container.Register<System.ServiceModel.BasicHttpBinding>(DryIoc.Reuse.Singleton, x => x.GetConstructor(new Type[] { }));

foreach(var contractType in typeof(TheServiceContracts.IProductService).Assembly.GetTypes())
{
    var binding = new System.ServiceModel.BasicHttpBinding();
    var endpointAddress = new System.ServiceModel.EndpointAddress("http://localhost:1337/" + contractType.SubString(1) + ".svc"); // removes the first letter I
    _container.RegisterDelegate<contractType>(r => 
        {
            var binding = r.Resolve<System.ServiceModel.BasicHttpBinding>();

            var svcAddress = "http://localhost:1337/" + contractType.Name.Substring(1) + ".svc";
            var address = new System.ServiceModel.EndpointAddress(svcAddress);
            
            return (contractType) ServiceModelHelper.CreateService(contractType, binding, endpointAddress)
        }, Reuse.InResolutionScope);
}


To fix that, we need to simulate RegisterDelegate<T> during runtime


First step, we should design how the additional API should look like. It should feel as close as possible to the convention of the original API:
_container.RegisterDelegate<TheServiceContracts.IOrderService>(x => 
    (TheServiceContracts.IOrderService) ServiceModelHelper.CreateService(
        typeof(TheServiceContracts.IOrderService), 
        new System.ServiceModel.BasicHttpBinding(), 
        new System.ServiceModel.EndpointAddress("http://localhost:1337/OrderService.svc")
    )
, Reuse.InResolutionScope);


The additional API should be like this:
_container.RegisterDelegate(contractType, x => 
    ServiceModelHelper.CreateService(
        contractType, 
        new System.ServiceModel.BasicHttpBinding(), 
        new System.ServiceModel.EndpointAddress("http://localhost:1337/OrderService.svc")
    )
, Reuse.InResolutionScope);


Then our API should be an extension method of the DryIoc.IResolver:
public static class ResolverExtensionHelper
{
    public static void RegisterDelegate(this DryIoc.IResolver resolver,
         Type contractType,
         Func<DryIoc.IResolver, object> objectConstructor,
         DryIoc.IReuse reuse = null, DryIoc.FactorySetup setup = null, string named = null)
    {
        ResolverBuilder.Build(container, contractType, objectConstructor, reuse, setup, named);
    }
}


You maybe wondering why we need to use two classes, ResolverBuilder and ResolverExtensionHelper, why not just do everything in ResolverExtensionHelper?

We need to have the following code, we need to cast the objects generated(e.g., WCF proxy objects) in a delegate back to its dependency-injected type(e.g., is an interface) during runtime, which is not possible with the original RegisterDelegate<contractTypeHere>. So we need to have a mechanism that can cast runtime types:
TService ConstructObjectThenCast<TService>(IResolver resolver)
{
    var svc = (TService) this._objectConstructorDelegate(resolver);
    return svc;
}


And we cannot add lambda parameter (any parameter for that matter) on that method:
TService ConstructObjectThenCast<TService>(IResolver resolver, 
    Func<IResolver, object> objectConstructorDelegate)
{
    var svc = (TService) objectConstructorDelegate(resolver);
    return svc;
}


Adding that parameter would cause runtime error when doing a reflection call on original RegisterDelegate method. ConstructObjectThenCast method is the substitute method for the original RegisterDelegate's lambda parameter
Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type.


Looking at the original RegisterDelegate method signature shows why:
public static void RegisterDelegate<TService>(
     this IRegistrator registrator, Func<IResolver, TService> lambda, 
     IReuse reuse = null, FactorySetup setup = null, string named = null);

The lambda delegate we want to hook on has one parameter only, IResolver only. Hence if Func<IResolver, object> objectConstructorDelegate is added to parameters of ConstructObjectThenCast, it results to the error above


However, since we cannot add another parameter on the substitute method(ConstructObjectThenCast) to the lambda, the only way to pass the user-defined lambda to the original RegisterDelegate is to create a class instance, which is not possible on static class, hence the need for two classes. So when the original RegisterDelegate method calls our substitute method for original RegisterDelegate's lambda, we can still call the user-defined lambda in turn. The user-defined lambda is stored in a non-static class, it's a field on that class: Func<DryIoc.IResolver,object> _objectConstructorDelegate;


ConstructObjectThenCast is the method that will cast an object to its dependency-injected type(e.g., in an interface), that's the method we will substitute to RegisterDelegate's lambda parameter. In order for that substitute method to be able to call our user-defined lambda, we will call the user-defined lamba from the class instance. this._objectConstructorDelegate is where the user-defined lambda is stored
TService ConstructObjectThenCast<TService>(IResolver resolver)
{
    var svc = (TService) this._objectConstructorDelegate(resolver);
    return svc;
}




Here's the complete code of the additional API. This also shows how to use DryIoc on ASP.NET MVC and WCF:
using System;
using System.Linq;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Routing;

using DryIoc;
using AspNetMvcUseWcf.Controllers;

namespace AspNetMvcUseWcf
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            
            ControllerBuilder.Current.SetControllerFactory(new DryIocDependencyResolver());   
        }
    }

    class DryIocDependencyResolver : System.Web.Mvc.DefaultControllerFactory
    {
        DryIoc.Container _container;

        public DryIocDependencyResolver()
        {
            _container = new DryIoc.Container();
            RegisterTheIocs();
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            System.Web.Mvc.IController ic = controllerType == null
                ? null
                : (System.Web.Mvc.IController)_container.Resolve(controllerType);

            // _container.ResolvePropertiesAndFields(ic);  // uncomment this if you want to use DI on controller's properties

            return ic;
        }

        void RegisterTheIocs()
        {
            RegisterMvcControllers();
            RegisterWcfServices();
        }

        void RegisterWcfServices()
        {
            _container.Register<System.ServiceModel.BasicHttpBinding>(DryIoc.Reuse.Singleton, x => x.GetConstructor(new Type[] { }));


            foreach (var contractType in typeof(TheServiceContracts.IMemberService).Assembly.GetTypes())
            {                
                _container.RegisterDelegate(contractType,
                    r =>
                    {
                        var binding = r.Resolve<System.ServiceModel.BasicHttpBinding>();

                        var svcAddress = "http://localhost:1337/" + contractType.Name.Substring(1) + ".svc";
                        var endpointAddress = new System.ServiceModel.EndpointAddress(svcAddress);


                        return ServiceModelHelper.CreateService(contractType, binding, endpointAddress);
                    }, Reuse.InResolutionScope);

            }
        }

        void RegisterMvcControllers()
        {
            System.Reflection.Assembly assembly = typeof(HomeController).Assembly;

            foreach (var controller in assembly.GetTypes().Where(t => typeof(Controller).IsAssignableFrom(t)))
            {
                _container.Register(controller, DryIoc.Reuse.InResolutionScope);
            }
        }


    } // class DryIocDependencyResolver






    public static class ResolverExtensionHelper
    {
        public static void RegisterDelegate(this DryIoc.IResolver container,
             Type contractType, Func<DryIoc.IResolver, object> objectConstructorDelegate,
             DryIoc.IReuse reuse = null, DryIoc.FactorySetup setup = null, string named = null)
        {
            ResolverBuilder.Build(container, contractType, objectConstructorDelegate, reuse, setup, named);
        }
    }

    public class ResolverBuilder
    {
        
        Func<DryIoc.IResolver,object> _objectConstructorDelegate;


        public static void Build(DryIoc.IResolver container, Type contractType, Func<IResolver, object> objectConstructor, IReuse reuse, FactorySetup setup, string named)
        {
            new ResolverBuilder(container, contractType, objectConstructor, reuse, setup, named);
        }

        ResolverBuilder(
            DryIoc.IResolver resolver, Type contractType, Func<DryIoc.IResolver, object> objectConstructorDelegate,
            DryIoc.IReuse reuse = null, DryIoc.FactorySetup setup = null, string named = null
            )
        {
            _objectConstructorDelegate = objectConstructorDelegate;

            Delegate lambda;
            {
                System.Reflection.MethodInfo constructObjectThenCastMethodInfo = Create_ConstructObjectThenCast_MethodInfo_For_ContractType(contractType);
                lambda = Create_ConstructObjectThenCast_Lambda_For_ContractType(this, constructObjectThenCastMethodInfo, contractType);
            }


            // Invoke the original RegisterDelegate<TService>            
            {
                // public static void RegisterDelegate<TService>(this DryIoc.IRegistrator registrator, Func<DryIoc.IResolver, TService> lambda, 
                //                                                     DryIoc.IReuse reuse = null, DryIoc.FactorySetup setup = null, string named = null);

                // obj is null, means RegisterDelegate is a static method
                // resolver comes from _container
                // contractType is the TService in the original _container.RegisterDelegate<TService>
                System.Reflection.MethodInfo registerDelegateMethodInfo = typeof(DryIoc.Registrator).GetMethod("RegisterDelegate").MakeGenericMethod(contractType);
                registerDelegateMethodInfo.Invoke(/*obj*/ null, new object[] { resolver, lambda, reuse, setup, named });
            }
        }

        static System.Reflection.MethodInfo Create_ConstructObjectThenCast_MethodInfo_For_ContractType(Type contractType)
        {

            System.Reflection.MethodInfo constructObjectThenCastMethodInfo =
                // typeof(IResolver) is the resolver parameter of: TService ConstructObjectThenCast<TService>(IResolver resolver)
               typeof(ResolverBuilder).GetMethod(
                   name: "ConstructObjectThenCast",
                   types: new[] { typeof(IResolver) },
                   bindingAttr: System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
                   binder: null,
                   modifiers: null
                   )
                // contractType is the TService of ConstructObjectThenCast<TService>
               .MakeGenericMethod(contractType);
            return constructObjectThenCastMethodInfo;
        }


        // Create a lambda out of this class method: TService ConstructObjectThenCast<TService>(IResolver resolver)
        static Delegate Create_ConstructObjectThenCast_Lambda_For_ContractType(ResolverBuilder resolverBuilder,  System.Reflection.MethodInfo constructObjectThenCastMethodInfo, Type contractType)
        {
            // Create a Func<IResolver,TService> delegate type
            // This will be used as the lambda parameter on the original RegisterDelegate static method.                                          
            Type lambdaDelegateType = typeof(Func<,>).MakeGenericType(typeof(DryIoc.IResolver), contractType);
            // The above corresponds to this example: Func<IResolver, TheServiceContracts.IProductService>


            // Cannot use Activator.CreateInstance on delegate type as delegates don't have constructor
            // ConstructObjectThenCast method has a signature same as lambdaDelegateType 
            // Create a lambda out of ConstructObjectThenCast method info
            Delegate lambda = Delegate.CreateDelegate(lambdaDelegateType, resolverBuilder, constructObjectThenCastMethodInfo);

            return lambda;
        }

     
        TService ConstructObjectThenCast<TService>(IResolver resolver)
        {
            var svc = (TService) this._objectConstructorDelegate(resolver);
            return svc;
        }


    }// class ResolverBuilder


    public static class ServiceModelHelper
    {
        public static object CreateService(
            Type contractType,
            System.ServiceModel.BasicHttpBinding basicHttpBinding,
            System.ServiceModel.EndpointAddress endpointAddress
            )
        {
            var binding = new System.ServiceModel.BasicHttpBinding();
            //Get the address of the service from configuration or some other mechanism - Not shown here

            //dynamic factory generation
            object factory =
                Activator.CreateInstance(typeof(System.ServiceModel.ChannelFactory<>)
                .MakeGenericType(contractType), binding, endpointAddress);

            System.Reflection.MethodInfo createFactory = factory.GetType().GetMethod("CreateChannel", new Type[] { });
            //now dynamic proxy generation using reflection
            return createFactory.Invoke(factory, null);
        }
    }
    
}//namespace



Happy Coding!

1 comment:

  1. The API for registering delegates and instances with runtime type is available in upcoming DryIoc v2.0 (currently in prerelease NuGet packages or dev source branch).
    It was added in scope of: #28: Add RegisterInstance method that accepts a System.Type parameter.

    So you can write:

    container.RegisterDelegate(contractType, r => ServiceModelHelper.CreateService(...), Reuse.Singleton);

    If you need to provide custom parameter value, e.g. endpointAddress, there is other way in v2.0:

    container.Register(contractType, concreteServiceType, setup: Setup.With(
    parameters: Parameters.All.And("endpointAddress", r => GetEndpointAddressFromContractType(r.ServiceType))));

    Check, how "r" (Request) parameter is used to get access to context.
    The API with parameters is not so elegant at the moment, so I appreciate any suggestions. But it is tested and can be used as of now.

    ReplyDelete