IoC principle-use reflection/Emit to implement the simplest IoC container

IoC principle-use reflection/Emit to implement the simplest IoC container

From Unity to Spring.Net, to Ninject, several IoC frameworks have been used in succession over the past few years. Although I can use it, I have not carefully studied the process of IoC implementation. Recently, I took some time to download the source code of Ninject and studied it, which was quite rewarding. Next, I want to implement one of the simplest IoC containers, so that small dishes like me can better understand what the IoC framework does for us.

What is IoC

IoC is the abbreviation of Inversion of Control in English. We generally call it "inversion of control". IoC technology is a technology used to solve a major principle of object-oriented design that relies on inversion. Can better realize the interface-oriented programming to decouple the various components.

The realization principle of IoC

There are generally two types of .NET IoC containers, one is reflection, and the other is to use Emit to write IL directly.

Not much nonsense, if you want to know more about IoC, please Google.

About realization

First on a class diagram

1. Define IioCConfig interface

  public interface IIoCConfig
    {
        void AddConfig<TInterface,TType>();

        Dictionary<Type, Type> ConfigDictionary {get;}
    }

2. Define IoCConfig implementation

   public class IoCConfig:IIoCConfig
    {
       ///<summary>
       ///The dictionary object that stores the configuration, KEY is the interface type, and VALUE is the type that implements the interface
       ///</summary>
        private Dictionary<Type, Type> _configDictionary=new Dictionary<Type, Type>();

       ///<summary>
       ///Add configuration
       ///</summary>
       ///<typeparam name="TInterface">Interface</typeparam>
       ///<typeparam name="TType">implement the type of interface</typeparam>
        public void AddConfig<TInterface, TType>()
        {
           //Determine whether TType implements TInterface
            if (typeof(TInterface).IsAssignableFrom(typeof(TType)))
            {
                _configDictionary.Add(typeof(TInterface), typeof(TType));
            }
            else
            {
                throw new Exception("Type does not implement interface");
            }
        }

        public Dictionary<Type, Type> ConfigDictionary
        {
            get
            {
                return _configDictionary;
            }
        }
    }

Use a dictionary to store the correspondence between Interface and Class. Here is the configuration method imitating Ninject, using code to configure. The advantage of this configuration method is that it won't make mistakes, because there is an IDE to check spelling mistakes for you. Don't underestimate this benefit. When you have hundreds of injected objects, spelling errors are easy to occur when you use Unity's XML to configure the correspondence. Such errors are often difficult to find.

Of course, it is very easy to implement a class that sets the corresponding relationship according to the XML configuration file, and it is not implemented here.

3. Define IioCContainer container interface

public interface IIoCContainer
    {
       ///<summary>
       ///Return the corresponding instance according to the interface
       ///</summary>
       ///<typeparam name="TInterface"></typeparam>
       ///<returns></returns>
        TInterface Get<TInterface>();
    }

4. Use reflection to implement IoC container

public class ReflectionContainer:IIoCContainer
    {
       ///<summary>
       ///Configuration example
       ///</summary>
        private IIoCConfig _config;

       ///<summary>
       ///Constructor
       ///</summary>
       ///<param name="config">ioc configuration</param>
        public ReflectionContainer(IIoCConfig config)
        {
            _config = config;
        }

       ///<summary>
       ///Get the instance object according to the interface
       ///</summary>
       ///<typeparam name="TInterface">Interface</typeparam>
       ///<returns></returns>
        public TInterface Get<TInterface>()
        {
            Type type;
            var can = _config.ConfigDictionary.TryGetValue(typeof(TInterface), out type);
            if (can)
            {
              //Reflect to instantiate the object
                return (TInterface)Activator.CreateInstance(type);
            }
            else
            {
                throw new Exception("The corresponding type was not found");
            }
        }
    }

The reflection code is too simple, everyone will use it.

5. Use Emit to implement IoC container

 public class EmitContainer:IIoCContainer
    {
       ///<summary>
       ///Configuration example
       ///</summary>
        private IIoCConfig _config;

        public EmitContainer(IIoCConfig config)
        {
            _config = config;
        }

       ///<summary>
       ///Get instance
       ///</summary>
       ///<typeparam name="TInterface">Interface</typeparam>
       ///<returns></returns>
        public TInterface Get<TInterface>()
        {
            Type type;
            var can = _config.ConfigDictionary.TryGetValue(typeof(TInterface), out type);
            if (can)
            {
                BindingFlags defaultFlags = BindingFlags.Public | BindingFlags.Instance;
                var constructors = type.GetConstructors(defaultFlags);//Get the default constructor
                var t = (TInterface)this.CreateInstanceByEmit(constructors[0]);
                return t;
            }
            else
            {
                throw new Exception("The corresponding type was not found");
            }
        }

       ///<summary>
       ///EMIT for instantiating objects
       ///</summary>
       ///<typeparam name="T"></typeparam>
       ///<param name="constructor"></param>
       ///<returns></returns>
        private Object CreateInstanceByEmit(ConstructorInfo constructor)
        {
           //Dynamic method
            var dynamicMethod = new DynamicMethod(Guid.NewGuid().ToString("N"), typeof(Object), new[] {typeof(object[]) }, true);
           //Method IL
            ILGenerator il = dynamicMethod.GetILGenerator();
           //Instantiate the command
            il.Emit(OpCodes.Newobj, constructor);
           //If it is a value type box
            if (constructor.ReflectedType.IsValueType)
                il.Emit(OpCodes.Box, constructor.ReflectedType);
           //return
            il.Emit(OpCodes.Ret);
           //Use FUNC to associate method
            var func = (Func<Object>)dynamicMethod.CreateDelegate(typeof(Func<Object>));
           //Execution method
            return func.Invoke();
        }
    }

The realization of Emit is copied from the realization of Ninject. Here is actually writing IL manually. A simple way to write IL is to first write the code in C#, then use a decompiler tool such as Reflector to view the generated IL, and then change it to Emit code.

6. Implement IoCContainerManager

 public class IoCContainerManager
    {
       ///<summary>
       ///container
       ///</summary>
        private static IIoCContainer _container;

       ///<summary>
       ///Get the IOC container
       ///</summary>
       ///<param name="config">ioc configuration</param>
       ///<returns></returns>
        public static IIoCContainer GetIoCContainer(IIoCConfig config)
        {
           
                if (_container==null)
                {
                   //Reflection method
                    _container = new ReflectionContainer(config);
                   //EMIT method
                  //_container=new EmitContainer(config);
                }
                return _container;
            
        }
    }
The code is too simple, not much to say.

7. Use

 public interface ITest
    {
        void DoWork();
    }

    public class Test:ITest
    {
        public void DoWork()
        {
           Console.WriteLine("do work!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IIoCConfig config = new IoCConfig();
            config.AddConfig<ITest, Test>();//Add configuration
           //Get the container
            IIoCContainer container = IoCContainerManager.GetIoCContainer(config);
           //According to the ITest interface to obtain the corresponding instance
            ITest test = container.Get<ITest>();

            test.DoWork();

            Console.Read();
        }
    }

Output:

Here we manually use the IoC container to obtain the corresponding instance objects, and we can also cooperate with features to make the code simpler. It's not implemented here.

8. Summary

Through such a few short lines of code. We have implemented one of the simplest IoC containers. It can implement constructor injection (no parameters by default). But this has revealed the most essential thing of the IoC framework: reflection or EMIT to instantiate objects. Then we can add caching, or some strategies to control the life cycle of the object, such as whether it is a singleton object or a new object is generated every time.

 Source code

Reference: https://cloud.tencent.com/developer/article/1013356 Principles of IoC-Use reflection/Emit to implement the simplest IoC container-Cloud + Community-Tencent Cloud