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.

Keine Kommentare:

Kommentar posten