Skip to content

Dependency Injection

Reflection Emit edited this page May 21, 2018 · 24 revisions

The dependency injection in Cauldron is very straight forward and was built with focus on speed. The features it offers is very small compared to other known DI frameworks.

What it can:

  • Configuration based on attributes
  • Multiple names can be assigned
  • Accepts static methods and properties as component constructor
  • Multiple constructors
  • Property injection
  • Field injection (private fields only)
  • Singletons
  • Transient
  • List injection
  • Hard coded conditions

What it can't

  • Custom lifetime management
  • Open generic
  • Constructor injection
  • Scope
  • Auto mocking

What it does not explicitly can, but still tries to do

  • Unregistered resolution - only works on non abstract and non interface types. This uses reflection and compiled expression, therefor much slower then the usual instance creation. This is however a pointless feature

Circular References

This is in most cases not a big deal for the Cauldron.Activator due to the fact that an AOP interceptor is used to inject the instances. This also means that injections only happen if the property or field is called. Avoiding calling injected objects from the constructor also lesser the probability of a circular reference.

Configuration

Registration

The registration is done by decorating the types with the ComponentAttribute.

    [Component(typeof(IStorePackage))]
    public class StorePackage : PackageBase, IStorePackage

It is also possible to assign multiple contract names to the type. If both component attributes defines a singleton then both contract names will share a single instance.

    [Component("Super Store Package")]
    [Component(typeof(IStorePackage))]
    public class StorePackage : PackageBase, IStorePackage

The component attribute can also define priority and lifetime of the component.
It is also possible to register a type using Factory.AddType.

Constructor

Cauldron Activator will automatically use the matching constructor based on the passed parameters. It is however possible to define a default constructor. The default constructor can be also a static method or a static property.

    [Component("Super Store Package")]
    [Component(typeof(IStorePackage))]
    public class StorePackage : PackageBase, IStorePackage
    {    
        [ComponentConstructor]
        public StorePackage(string name)
        {
        }
    }

It is also possible to define multiple default constructors.

    [Component("Super Store Package")]
    [Component(typeof(IStorePackage))]
    public class StorePackage : PackageBase, IStorePackage
    {    
        [ComponentConstructor]
        public StorePackage(string name)
        {
        }

        [ComponentConstructor]
        public static IStorePackage Create(int index, string name)
        {
        }

        [ComponentConstructor]
        public static IStorePackage Instance => new StorePackage();
    }

Injecting

The easiest way to inject an instance is by decorating a property or field with the InjectAttribute. The injection itself will only happen if the property or field is used. That also means that the property or field can be called during construction. The Inject attribute is implemented using the property interceptor from the Cauldron.BasicInterceptors package.

    [Component("Super Store Package")]
    [Component(typeof(IStorePackage))]
    public class StorePackage : PackageBase, IStorePackage
    {    
        [ComponentConstructor]
        public StorePackage(string name)
        {
            if(this.UserStuff.IsAdmin)
            {
            }
        }

        [Inject] // Property with injected value
        public IUserStuff UserStuff { get; }

        [Inject] // Field with injected list of "store" items
        private IEnumerable<IStoreItem> items;
    }

The above example injects a collection of "store" items to a field. The implementation of each item would look like the following:

    [Component(typeof(IStoreItem))]
    public class Cabbage : IStoreItem
    {
    }

    [Component(typeof(IStoreItem))]
    public class Carrot : IStoreItem
    {
    }

Internally the inject attribute uses Factory.Create or Factory.CreateMany to inject the values.
Besides from using the inject attribute, it is also possible to manually inject or directly interact with the instance without injecting.

    [Component(typeof(IStoreItem))]
    public class Cabbage : IStoreItem
    {
         public void GetPrice(bool withTax)
         {
              var mojoPrice = Factory.Create<IPriceList>().BasePrices.GetMojo(this);
              ...
         }
    }

Type selection / Type resolution

If there are multiple types registered to a contract name the Cauldron Activator requires a type resolver. The following example shows 2 very simple implementation of such a resolver.

    // Simpliest it can get
    Factory.Resolvers.Add("Super Store Package", typeof(StorePackage));

    // Implementation with delegate
    Factory.Resolvers.Add(typeof(IStorePackage), new Func<IFactoryTypeInfo>(() =>
    {
#if DEBUG
        return Factory.RegisteredTypes.FirstOrDefault(x => x.Type == typeof(StorePackageMock));
#else
        return Factory.RegisteredTypes.FirstOrDefault(x => x.Type == typeof(StorePackage));
#endif
    });

The precompiler directives are just examples. The conditions can be anything else like a configuration entry from your ASP.NET appsettings.json.

The resolver can be also added using the IFactoryExtension interface. The Factory loads all types that implements this interface during its initialization. This can come handy if you have a library that loads different implementations of a contract based on configuration.

        private static IFactoryTypeInfo logger;

        public void Initialize(IEnumerable<IFactoryTypeInfo> factoryInfoTypes)
        {
            Factory.Resolvers.Add(typeof(ILogging), new Func<IFactoryTypeInfo>(() =>
            {
                if (logger != null)
                    return logger;
                
                if(Configuration["Logging:Type"] == "azure")
                    logger = factoryInfoTypes.FirstOrDefault(x => x.Type == typeof(LoggerMicrosoftAzure));
                else
                    logger = factoryInfoTypes.FirstOrDefault(x => x.Type == typeof(LoggerAmazon));

                return logger;
            });
        }

XML or Json configuration

Not implemented yet.

Loading assemblies

Loading additional assemblies during runtime (plugins, environment based...) does not trigger the Factory to load the component information from that particular assembly. To trigger the Factory, it is neccessary to add the loaded assembly using Assemblies.AddAssembly to the known assemblies collection.
If you wish to load multiple assemblies at once from a defined folder, you can also use the Assemblies.LoadAssembly method.
Both methods triggers the Factory to load the component information to its dictionary.

Under the hood

The Cauldron Activator is very fast. Not relying on reflection is the main reason for this (although it is able to as last resort). The trick is that information of all types that are decorated with the component attribute are hard coded into the assembly. These hard-coded classes has a "CreateInstance" method, which contains all constructor with the explicitly defined constructor on top.
Example:
If the class is defined like this:

    [Component(typeof(IStoreItem))]
    public class Carrot : IStoreItem
    {
        public Carrot()
        {
        }

        public Carrot(string name)
        {
        }

        [ComponentConstructor]
        public Carrot(string name, int basePrice)
        {
        }
    }

The generated CreateInstance method will look like this:

	public object CreateInstance(object[] array)
	{
		if (array.Length == 2 && array[0] is string && array[1] is int)
		{
			var result = new Carrot(array[0] as string, (int)array[1]);
			Factory.OnObjectCreation(result, this);
			return result;
		}
		if (array == null || array.Length == 0)
		{
			var result = new Carrot();
			Factory.OnObjectCreation(result, this);
			return result;
		}
		if (array.Length == 1 && array[0] is string)
		{
			var result = new Carrot(array[0] as string);
			Factory.OnObjectCreation(result, this);
			return result;
		}
		var resultUsingReflection = typeof(Carrot).CreateInstance(array);
		Factory.OnObjectCreation(resultUsingReflection, this);
		return resultUsingReflection;
	}

Using the contract name the factory looks-up in its dictionary for the associated CreateInstance method and invokes it. So the only bottle-neck is the dictionary itself.