Freitag, 24. November 2017

PantherDI - Part 3: Singletons, Generics and Reflection

Singletons

A typical scenario for dependency injection that I saw is that most dependencies that get injected are only instantiated once. Thus PantherDI must offer a way to mark a registration as singleton, so only one instance will be created and then this instance will be returned on subsequent resolutions.
This is achieved by appending a flag to the resolution, which PantherDI can read. However, it also means that this flag needs to be transported down to the created providers.

Now I can think of two ways to implement that in the container. The first would be similar to how they were implemented in KittyDI: If it's a singleton, the provider will hold onto the first instance it creates and return this on subsequent calls without calling the providers for its dependencies. The other option would be for the container to hold on to the singletons it created and not even call the provider anymore, but instead use that singleton-cache.

Since I plan on each container disposing the singletons it created (but not the ones its parent container created) I'll opt for the second approach. So a container will hold the singleton-cache. For singletons the container will wrap the original provider into a SingletonProvider which will only call the original provider if the singleton-cache does not hold an instance. That way the container can quickly access all generated singletons during disposal.

This disposable behavior is also included in the changes made during this post.

Generics

I aim to make PantherDI handle certain generics differently when there is nothing actually registered for them. This will be done by using additional resolvers. The following generics are planned:
  • IEnumerable<T>
    Returns instances of all registered type that fulfill the given contracts and can be casted to the type T
  • Lazy<T>
    Doesn't instantiate the dependency immediately
  • Lazy<T, TMetadata>
    Also contains metadata that has been provided during registration
    This will be part of a different blog post
  • Func<T>
    Returns a function that, when executed, will create a new instance of the registered type.
    Except of course for singletons - there it will always return the same instance.
  • Func<T1,..., TOut>
    Like Func<T> only that it can be used to resolve types with dependencies unresolvable by the container.
    This can be used as factory-function for sub-viewmodels by simply adding the model to the constructors parameters and resolving Func<TModel, TViewModel>
    For now only the one-parameter version will be implemented
Also those generics should be nest-able, so when resolving an IEnumerable<Lazy<T,TMetaData>>, you should get an enumeration of not yet instantiated types that fulfill the given contracts and can be casted to the type T, including the registered metadata. This is a good thing when lazy loading plugins for example.

Implementation notes

  • Due to the same troubles as mentioned in the previous post, it was not possible to use Set operations and GroupBy to implement the EnumerableResolver, even though there is an IEqualityComparer for dependencies and one for sets that forces ISet<T>.SetEquals to be used - their Equals-Method is not used however although their GetHashCode returns the same value for both instances. Instead a workaround is employed. Again I ask for help on why this doesn't work and how to make it work with LINQ and Set-Operations.
  • Upon implementing the resolver for Lazy<T>, I extracted a helper class which does the pre-check and instantiates an inner resolver with the same type parameters as the generic that is to be resolved. So to create a resolver for MyType<T1,T2>, I just have to create a MyTypeResolver<T1,T2> and a proxy which inherits from GenericResolver and registers typeof(MyType<,>) and typeof(MyTypeResolver<,>) to the generic resolver.

Reflection

The topic of reflection falls apart into two areas: Automagic registration via reflection and using that to enable a non-strict behavior of a container. Automagic registration means the container will use reflection to determine what is registered and which contracts it has, the non-strict behavior enables types to be resolved without registering them at all, just like it was possible in KittyDI.

Registration

In order to perform registration via reflection, each element which is used to create a catalog, as well as the catalog itself will get versions that use reflection to set themselves up. To do so, a set of attributes will be added.

ConstructorFactory

The ConstructorFactory represents the constructor of a type and will create the type by invoking its constructor. It can be created by passing it the ConstructorInfo that Reflection will return. Each constructor parameter will be treated as a dependency. The contracts each dependency requires will be read from the ContractAttribute, if present. If no ContractAttribute is present, or if a ContractAttribute has no contract set, the type of the constructor parameter is used as contract.

TypeRegistration

The TypeRegistration registers a single type. It will contain a factory for each constructor of the type unless the constructor is decorated with the IgnoreAttribute. The fulfilled contracts will be
  • read from the ContractAttributes decorating the type itself. If one of these attributes is given without a contract, the type itself is used as contract.
  • read from the ContractAttributes on all implemented interfaces an up the type hierarchy
  • if no ContractAttribute is found at all, the type itself will be used as only contract
If the SingletonAttribute is present on the registered type, it will be registered as a singleton.

TypeCatalog

The TypeCatalog is a fast way to add TypeRegistrations to the container. As it implements methods to add types quickly which will generate a TypeRegistration for each added type.

AssemblyCatalog

The AssemblyCatalog scans a whole assembly and adds a TypeRegistration for each type which is decorated with a ContractAttribute, implements an interface decorated with a ContractAttribute or inherits from a type decorated with a ContractAttribute.

MergedCatalog

Not really related to reflection, the MergedCatalog will contain all registrations of all the catalogs it contains. If two contained catalogs contain the same registered type, the registration will be merged. This means the new registration will contain the union of the fulfilled contracts and the factories. It will be registered as a singleton as soon as one of the catalogs registers the type as singleton.

Non-Strict behavior

Non-Strict behavior will be achieved by the ReflectionResolver which will try to resolve any Dependency. It will do so by iterating over all required type contracts (and the expected type) to find a type which can be instantiated and fulfills all given contracts. To do so it will use the TypeRegistration to scan each type.
If a type is found, the algorithm to recursively resolve the dependencies of that TypeRegistration's factories is called from the container construction. Only this time it uses the function handed to the resolver for recursive resolution.
For this the function has been made static and returns the found Providers that it created. Adding to the knowledge base while container construction will be done in the calling function.

Code

The code after implementing this blogpost can be found on GitHub under the Tag Blogpost3: Link

Glossary

This chapter contains a cumulative list of terms that are used within this project.

Container
The central element of PantherDI. 
It offers a way to retrieve instances of objects with all their transitive dependencies resolved.

Registration
Information about a single type, enabling a container to create it, resolve its dependencies and use it as dependency.

Contract
Any object that serves as the promise that a registration can be used as dependency. A registration can be retrieved only by contract, so the consumer does not need to know its actual type.

Factory
Basically the object representation of a function which will create an instance of a registered type.

Dependency (of a Factory or Provider, see below)
A single dependency of a factory is the object representation of one a function parameter. It includes meta-information, for example the contracts the instance passed needs to fulfil.


Provider
Created from a factory by resolving all the dependencies that can be resolved using the given registrations.

Knowledge Base (KB)
The knowledge base contains all providers that can be used by the container.

Resolver
An algorithm that utilizes the knowledge base to return all providers for a given dependency.
It may create new providers from the ones given in the knowledge base, depending on its purpose.

Mittwoch, 8. November 2017

PantherDI - Part 2: Constructing a container

Basic principle

The basic idea of container construction is that there is a data structure storing the unprocessed registrations which the construction algorithm will take out of the data structure, process and then add the generated providers to the knowledge base.
Processing the unprocessed registrations would basically work as follows:
  1. Get the next entry and remove it from the unprocessed registrations
  2. Resolve its dependencies
  3. For all possible combinations of resolved dependencies create a provider and add it to the knowledge base.

Dependency resolution during container construction

Requirements to the "unprocessed items" data structure

To ensure that the knowledge base contains all registrations needed for dependency resolution of the factory to convert, the algorithm needs to first process all unprocessed registrations for each of the dependencies' contracts first. This means that the data structure to store the unprocessed registrations needs a way of retrieving all registrations that fulfill a contract.

The chosen solution for the data sctructure which stores the unprocessed registrations is to use a dictionary that maps a fulfilled contract to all registrations fulfilling it. This does mean that removal is a bit costly as removing a registration means removing it from all dictionary entries. However the main scenario is that each type only fulfils one contract (which is the expected return type), so this overhead will only come into play in complex configurations.

Resolving the dependencies of the factory

In order to turn a factory into a provider, the construction algorithm needs to resolve its dependencies. For this it has a list of strategies which it will execute and use the concatenated results.
Each of these strategies will be called "Resolver".
While the default resolver is rather trivial, resolvers will also be used to handle certain "automagic" generics like IEnumerable, Lazy and Func. For now the most important part is that a resolver takes the a dependency as well as a function to resolve a dependency (which will run the list of resolvers again) and returns an enumeration of providers for the given dependency.

Accessing the knowlegde base

In its implementation it will store all providers in a dictionary that maps a single contract to an enumeration of the providers that fulfil it. The rest of the constraints will be checked by the resolver.
For encapsulation the knowledge base is a resolver in itself which will only rely on its contents.

Additional Remarks on the chosen implementation

Container

  • The container contains an internal resolution function which is passed to the registered resolvers. This will become interesting in the next blog post when we add implicit handling of certain generics.
  • The container already handles types not resolved by the knowledge base by adding their resolutions to the knowledge base. This is actually work done for the next blog post.

ContainerBuilder

  • While processing the registrations, the container builder will pull a registration from its to-do-list, remove it from that list and then process this single entry until the to-do-list is empty
  • While processing a factory, the builder will encounter dependencies of the factory. Before it tries to resolve those using the registered resolvers, it processes all registrations that fulfill the first contract of the dependency.
    Why only the first contract? Because all registrations that fulfill the other contracts but not the first can't be considered as dependencies, so all dependencies must be in the knowledge base as soon as the first contract has been processed.
  • After resolving all dependencies of a factory, each possible combination of providers for each dependency is converted into a provider, so if there is a partial resolution (=provider with dependencies) for a dependency, there also will be a partial resolution for the currently processed factory.
  • Unlike the container, the ContainerBuilder does not yet handle resolutions done by resolvers other than the knowledge base differently. This is due to the fact that it doesn't support adding custom resolvers yet (unlike the container).

Dependency

  • The dependency has a custom equality comparer. Still I need to manually invoke it to find an entry in a Dictionary<IDependency, T> or ISet<IDependency>. I would be happy if someone looked into that, so I could use the methods provided by the builtin dictionary and set structures.

ManualRegistration

  • For now the only way to register a type is by creating a ManualRegistration. However this series of blog posts will also handle the desired ways to create a catalog. These will mainly be via a builder with a fluent interface and via reflection.

Code

The code can be found on GitHub:

Glossary

This chapter contains a cumulative list of terms that are used within this project.

Container
The central element of PantherDI. 
It offers a way to retrieve instances of objects with all their transitive dependencies resolved.

Registration
Information about a single type, enabling a container to create it, resolve its dependencies and use it as dependency.

Contract
Any object that serves as the promise that a registration can be used as dependency. A registration can be retrieved only by contract, so the consumer does not need to know its actual type.

Factory
Basically the object representation of a function which will create an instance of a registered type.

Dependency (of a Factory or Provider, see below)
A single dependency of a factory is the object representation of one a function parameter. It includes meta-information, for example the contracts the instance passed needs to fulfil.


Provider
Created from a factory by resolving all the dependencies that can be resolved using the given registrations.

Knowledge Base (KB)
The knowledge base contains all providers that can be used by the container.

Resolver
An algorithm that utilizes the knowledge base to return all providers for a given dependency.
It may create new providers from the ones given in the knowledge base, depending on its purpose.