Samstag, 23. Dezember 2017

PantherDI - Part 6: Metadata, Factories

Metadata


Apart from the metadata that PantherDI will hold internally, in this blogpost I will implement functionality which allows custom metadata to be registered for a type.
This metadata can then be used to further determine which resolved item will be instantiated for example when creating a dynamic plugin system.

Internal storage

Custom metadata are stored in a key-value store for each type where the key is a string and the value is an object supplied by the registration. It will not be possible to supply different metadata for each registered factory, the metadata will be attached to the registered type (=the registration) itself. Also it will be possible to request metadata which is not registered for a given type or to have metadata registered which is not be retrieved.
In other words: Registering and retrieving metadata is completely optional.

Registration by reflection

Metadata can be either registered manually (by adding it to the IRegistration) or - if reflection is used - registered with the means of attributes. Since the manual registration is rather trivial, I will mainly go over the registration via attributes as there are three ways to add metadata to a type: Attributing the type, marking a static property or field as metadata and metadata inheritance.
Both ways can be mixed, however a key must be unique for a type. PantherDI will throw an exception when a key is present more than once for the metadata of a single type. The only case where the presence of a metadata attribute with an already known key is when the key is added via metadata inheritance.

Attributing the type

The attribute Metadata can be used on a registered type to add an entry to the metadata of its registration. This attribute expects the key as well as the value of the entry. You can add as many of these attributes to a type. It is also possible to create your own attribute that inherits from the MetadataAttribute so the key, which is a magic string does not need to be supplied manually.

Attributing a static property or field

If the MetadataAttribute is added to a static property or field, the value of this property or field is read at the time of resolving the metadata (more on that later). While this allows the metadata to be changed during the lifetime of the container, it is advised to set the metadata statically and never change it afterwards.

When supplying metadata through a field or property, the key of the MetadataAttribute can be ommitted. This will cause PantherDI to use the name of the property or field as key for the entry. However if a key is supplied in the attribute, it is used instead of the property or field name.

Metadata supplied as fields or properties will always override metadata supplied on the type itself.

Inheriting metadata

Metadata on a type will also be set on types it inherits from. These inherited entries can be overriden on the derived type.
Thus the normal inheritance mechanism can't be used: PantherDI needs to know which entry comes from which type, so overriding is done correctly.

Retrieval

To retrieve the metadata of a type, Lazy<T, TMetadata> needs to be resolved. PantherDI will create a new instance of TMetadata and fill all its public, non readoly properties with the metadata.
The rules for doing so are as follows:

  • If the property is decorated with a MetadataAttribute, the key from it will be used to retrieve the entry. This is why the MetadataAttribute can be instantiated without value. However if a value is supplied, it will be ignored.
  • If the attribute is missing, the name of the property is used as a key
  • If the metadata of the type contains an entry which is not reflected in a property of TMetadata, it is simply ignored.
  • If a property of TMetadata can't be matched to an entry in the metadata, its setter will never be called.

This allows the consumer of metadata to easily specify which entries are of interest.

Creating your own MetadataAttribute types

If you create a type that inherits from MetadataAttribute, PantherDI will also use it to gather metadata or mark properties to be filled with metadata. That way you can create your own attribute and use that instead of the key.

Missing registration of instances via properties and fields

During implementation of registration of instances I forgot to implement registration via static properties and fields. Now that metadata can be registered in a similar way, I will fix that bug, using the same mechanisms to detect these an assembly is scanned. 
The field or property needs to be decorated with the ContractAttribute and just like decorating a type, the set of ContractAttributes will determine the fulfilled contracts of the registered instance.
If a contract attribute is present without a contract, the name of the property and its type will be used.
Instances registered in that manner can not have any metadata (for PantherDI would be unable to distinguish whether you want the property or field to be the value of a metadata entry or whether the metadata should be annotating the property or field).

To enable registration of instances with their own contracts, factories now can have a set of fulfilled contracts too. These contracts are only fulfilled when the respective factory is used to create the instance.

FactoryAttribute for registration of static methods as factories

Instead of just using the constructors, PantherDI supports registration of static methods as factories. If a static method is annotated with the FactoryAttribute, it will be considered a factory for the return type. ContractAttributes can be used to further annotate the method and add contracts that will only be considered as fulfilled when the factory is used.



Code

The code after implementing this blogpost can be found on GitHub under the Tag v1.2.0: 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.

Processing
Often means converting a Factory into a Provider by resolving all its dependencies that can be resolved.

Samstag, 16. Dezember 2017

PantherDI - Part 5: Instance registration, Late processing, IgnoreAttribute

License and readme

First up a rather smaller change: The project now features a license file and a readme.
I chose the creative commons Attribution 4.0 international license, because it empowers others to build upon my work and use it in commercial projects too.

Instance registration

One of the features still missing is the ability to hand an instance of an object to the container and have it return the instance whenever a request matches its registration. The basis for the implementation is rather simple for it is just a factory that declares no dependencies and returns the given instance.

In the ContainerBuilder however there will be two new methods: WithInstance and RegisterInstance.
While WithInstance simply registers the instance and returns the ContainerBuilder, providing chainability, RegisterInstance returns an object which allows to modify the registration.

Late processing

The major philosophy of PantherDI is to put the time consuming stuff up front which stems from the philosophy found in medical projects. This ensures that it is always well known when the work will be done and that user interaction can't create a package of workload while the application runs and maybe even while time critical processes are executed.
Usually during normal application development this is not a requirement and thus this default behavior which leads to a slower application startup actually counteracts what the developer wants to achieve.
Thus I want to implement a way to revert that behavior and process registrations only when a type is requested that the registration handles. This removes processing time from the application startup (or container creation) and spreads it out over resolution requests. If a registration is never requested, it won't be processed at all.

The implementation of late processing is rather simple. First of all the container no longer holds the knowledge base. Instead it "only" holds a cache, which contains the result of resolutions for a specific request. If the cache already contains a result for the request, it will be used.
During normal operation the KB will be handed to the container as first resolver. As it was before, only if it does not contain a solution for the given request, the other resolvers are actually triggered. But instead of adding their results to the KB, they will also just be added to the cache. This will only be a slight change when using the default configuration, but when late processing should be used, the first resolver won't be a pre-filled KB, but a resolver that first processes any registrations that fulfil the first requested contract and then query the KB. This is the same behavior as it was in the ContainerBuilder when requesting to process a specific contract.

As you can see, separating the ContainerBuilder from the logic that does the actual processing was already a step towards implementing this feature: The change will be that the RegistrationConverter will be put into a resolver.

IgnoreAttribute

The IgnoreAttribute is meant to tell PantherDI to not process items via reflection it would otherwise process. Its detailed semantic changes depending where it is placed:

  • Used on a type, the Container won't contain this type even though it implements an interface or inherits from an abstract class marked as contract.
  • Used on a constructor, it will not be used as a factory for that type
  • Used on a parameter of a constructor, the parameter won't be resolved, even if it could, rendering the type only resolvable by Func<TIn, TOut> when using this constructor.
    This enables to create constructors for view-models that take the corresponding model, even when in non-strict mode and thus the model could be resolved.
While implementing ignoring a constructor parameter, I noticed a bug in the GenericResolver base class: The filter condition for when the GenericResolver was used always chose to use the resolver when the requested type was not a generic at all. system tests were added and the condition now does not use the GenericResolver when either the requested type is not a generic or the generic type definition does not match the implemented GenericResolver.

Code

The code after implementing this blogpost can be found on GitHub under the Tag v1.1.0: 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.


Processing
Often means converting a Factory into a Provider by resolving all its dependencies that can be resolved.


Mittwoch, 13. Dezember 2017

PantherDI - Part 4: The first release

Okay, so here it is. The blogpost with which the first released version of PantherDI will be published.
But still this does not mean that PantherDI is finished. The roadmap can be viewed in the GitHub-Issues by filtering by the "New Feature"-Tag.

Switching to DotNetStandard

The easiest part in this blogpost will be the switch to DotNetStandard. A colleague actually triggered me trying it after having had issues with KittyDI and the test project. Well, what can I say? I simply created a new Project in the solution, targeted DotNetStandard 1.0, copied all files to it and changed the reference in the test project. Basically the only changes I needed to to was switching between TypeInfo and Type in several places and replacing a.IsInstanceOfType(b) by b.IsAssignableFrom(a). 
After that all the projects compiled again and the tests ran green.

DirectoryCatalog

This one actually got lost while adding the reflection stuff.
The DirectoryCatalog simply scans a directory for assemblies, tries to load them and if it succeeds will create an AssemblyCatalog for each found assembly. The DirectoryCatalog itself will return the merged result of these AssemblyCatalogs. Since loading an assembly from a file is not supported in DotNetStandard (yet?), the DirectoryCatalog will be part of an extension that targets the regular .NET-Framework.

Fluent Syntax for ContainerBuilder

This is mainly convenience for the developer using the library. The goal is to give the developer an opportunity to do all registrations that PantherDI offers by calling methods on the ContainerBuilder.
Also the code that converts registrations to the knowledge base is moved out of the ContainerBuilder itself.

The ContainerBuilder now knows three ways to add something to the configuration which will then produce the DI-container:

  • Using a version of the Add-Method
    This enables the use of the collection initializer syntax too.
  • Using one of the With*-Methods
    These return the ContainerBuilder itself, so they can be chained.
  • Using one of the Register*-Methods
    These return a different object, which enables to change the registration using a fluent interface.

Since the collection initializer syntax is only available, when an object implements ICollection, the ContainerBuilder will enumerate over all catalogs, registrations and resolvers that it contains when enumerating its contents.

In addition a helper class for RegisterType has been introduced that enables configuring how the type should behave. By default it will neither use reflection to determine which contracts the type providdes nor use the constructors of the type as factories. This behavior can be changed however.

Also the With*-Methods contain convenience-methods, so the setup can be more concise when using the encouraged configuration. For example the method WithGenericResolvers will add all resolvers for generics that ship with PantherDI. If all but one should be used, the method WithoutResolver<T> can be used to remove the resolver again.

Adding CI/CD

Now it's getting real: I'm going to add the repository to Appveyor which will add Continous Integration to the project. Also I want to configure it that upon pushing a tag it will build a release, package it with NuGet and publish that in the Appveyour-NuGet-Stream that you get with registering a project. This means PantherDI is technically ready to be used.

During implementation of the CI/CD using AppVeyor, I decided to change how or specifically when a new release will be created: AppVeyor is set up to build each PullRequest in GitHub and upon merging them, push the resulting packages to NuGet. This means that for each PullRequest the version number in the AppVeyor-configuration needs to be updated.
Since AppVeyor is unable to change the version of a dependency, the required version of PantherDI needs to be updated in the PantherDI.DNF-Package whenever the major version of PantherDI is changed. Version numbering should be according to "Semantic Versioning" (www.semver.org)

Roadmap

This does neither conclude the development of PantherDI, nor this series of blogposts. Instead all features I want to see in PantherDI are reflected in the issues on GitHub.

Code

The code after implementing this blogpost can be found on GitHub under the Tag v1.0.0: 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.


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.

Samstag, 28. Oktober 2017

PantherDI - Part I: Interfaces

After developing KittDI with a rather naive approach (not thinking much prior, doing refactorings as I went) I decided to start another, more heavyweight library for dependency injection.


The first thing I want to talk about is the Interface of the library. To do this I am going to assume a usual workflow:

  1. Setting up the configuration for the new container
  2. Creating the container using that configuration
  3. Resolving what you need.

Container setup

Basically the setup of a container is a set of registrations. Upon construction, the container will use these to construct resolution strategies for the registered types. 


But what is a registration?
At first a registration is done for a specific type, so the data structure used for registrations needs to provide the type that is registered.
Next the registered type usually fulfils contracts, so for each registration we need to supply which contracts are fulfilled.
Then for each registered type there must be at least one way to create an instance of that type, so the registration needs an enumeration of factories for that type.
Each of these factories can have parameters (as in parameters of a function) which would be treated as dependencies of the registered type, when constructed using that factory.
Each dependency needs an expected type and a list of contracts that need to be fulfilled in order to satisfy the dependency. In the most cases the contract will be the expected type which both are the type of some interface, but any object can serve as contract.
Thus a dependency of a factory is modeled as a separate entity.
Also the factory has an execute-method which takes the resolved dependencies in the order given by the dependency list as an array of objects.

Container creation

To create a container it should be supplied with a catalog that contains the setup for the container. The algorithm that creates the container from a given catalog is the heart of PantherDI and will thus be within its own class for testability. It will create, what I will call the "knowledge base (KB)" of the container in future references. The KB contains an entry for each contract that can be fulfilled within the container. Each of these entries provides all ways to fulfill the given contract. A way to fulfill the contract is called a provider and just like the factories it too can have parameters, but when a provider in the KB does have parameters this means that during container creation this parameter was not resolvable using the catalog. This means that the KB basically contains a version of the factories which already know how to resolve the dependencies that actually can be resolved. Apart from that it it also contains the actual returned type, a full list of contracts fulfilled by this provider and additional metadata used by the container or set via registration.
The KB thus is a dictionary that maps contracts to a enumeration of providers. 

Resolving a registered type

To resolve a registration, the container needs an enumeration which contains at least one contract and a return type. A provider matches the request if it provides all the requested contracts and its actual type can be assigned to the requested return type. Since the typical call will be the return type being the contract, a parameterless call is allowed, causing the return type to be used as contract.

Test first (somewhat)

With that in mind the interfaces can already be created and with the interfaces already there, we can write tests for the behavior. But while I won't work test driven (meaning always implement the minimal solution to satisfy a test), I will write these tests up front and then use them to check if my implementation actually does what it should.
The following behavior should be covered by the tests (for now):

  • When no type is registered, trying to resolve by type or contract fails
  • When a type is registered, it can be resolved by its type and the registered contract type.
    The registered factory is called for each resolution
  • The registered type can also be used as contract
  • When a factory declares a dependency, the factory of that dependency is also called.
  • When multiple factories are registered that fulfil the contract requested, an exception is thrown
  • When there is a circular dependency, an exception is thrown
In order to write the tests without instantiating mocks for each interface, a first implementation of each interface is given, where all properties can be read and written, but without any internal logic.

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.

Code

The state of the code after this blogpost was written can be found under the tag "Blogpost1" in the GitHub-Repository: https://github.com/MarkusPalcer/PantherDI/tree/Blogpost1

Sonntag, 24. September 2017

KittyDI or "How I wrote my own dependency injection container"

Since KittyDI is rather well developed (I mainly just miss the NuGet packaging and publishing stuff and can't find the motivtion to actually do the PRISM-Integration), this blogpost will be more of a retrospective than a developers diary. I still file it as such, because it is planned as the first part in a series about me creating two DI-Container implementations.
But first things first:

How it all began

In the beginning there was... a project at Zühlke. my employer. While I can't disclose much of the project, I can say that we worked on a complicated enterprise application using Autofac and PRISM. At one point we used Autofac to populate a list with ViewModels for the items to be displayed. I thought that to be a rather nice approach as we only had to resolve a function that takes the model as an argument and returns the viewmodel. Autofac then takes care of selecting a constructor where it can resolve all dependencies but the model with types registered to the container and put the model in as last missing dependency.
This all worked pretty well, but it took a long time for the list of viewmodels to fill (with about 100 items in the list of models). So I wondered aloud why it takes so long to create 100 instances of a type and a coworker replied "Well that's obvious. Autofac needs to go through all the strategies of creating the viewmodel for each of the 100 instances."
My first thought was "Really? Can't it just re-use the strategy it used the last time?" Of course I dismissed that thought as being too naive and instead considered implementing my own dependency injection container just to learn the pitfalls of doing so. I just never got around doing that.
In my current project we're using the lightweight Java dependency injection container "Feather" to create an Android project. Inspired by the simple idea that Feather implements (which was basically the same idea I had back then) I decided to finally start that. So yes, KittyDI is inspired by Feather, but no I never used their code as an actual example.
Why I chose KittyDI as name? Because I viewed this DI container as an experiment. A first, naive, maybe even childish try to implement a DI container and to mature by doing so. The relation to felines should be obvious while reading this blog. If it isn't, check the archives.

The goal

My main goal of course was to learn, maybe to fail and see why, to see where the complexety lies and to see if writing a DI container really is as hard as I thought and as people made me think it was. (Spoiler: It isn't.)

For dependency injection my goals were to offer a mixture of the features MEF, AutoFac and feather offer. This means:

  • Being able to resolve a type which is not yet known by the container 
  • Telling the container which type to resolve when an interface is requested 
  • Registering types as singletons 
  • Resolving all registered implementations of an Interface 
  • Resolving a factory for a type 
  • Resolving a factory that takes parameters 
  • Putting containers inside each other 
  • Creating and resolving containers 
  • Automatic disposal of container contents on container disposal 
  • Resolving generics
  • And of course remembering how a type got resolved last time
Of these features I only scrapped resolving all registered implementations of an Interface. 

The work

I implemented KittyDI in an incremental way. 
The first functionality that got added was simple resolving of an unknown type. The approach was to check the constructors of the requested type and selecting the one that fits. There was the first decision: how do I choose which constructor to use. I decided for a simple approach that doesn't force the user to add an attribute to the constructor of each type he wants to resolve (like MEF does). Instead I decided that KittyDI always uses a parameterless constructor if it is there. If there is no parameterless constructor but only a single constructor with parameters, it is used and the parameters are in turn resolved. Only if there is no parameterless constructor and instead multiple constructors taking parameters, the attribute is needed to tell KittyDI which one to use. This does two things at once: it enables the user to simply resolve types without the danger of missing the attribute and getting cryptic exceptions and it makes it possible to use types defined in libraries that don't know about KittyDI. This was a major flaw I personally saw in MEF. After KittyDI decided how to resolve a type, it wraps that in a function and stores it in a dictionary so it doesn't have to search for the right constructor again. 
In order to prevent endless resolution loops KittyDI hands the resolution stack (meaning the types of all factories currently in the call stack) to an internal factory and if a factory is called where it's type is already on the stack we are in a resolution loop and throw an exception notifying the caller of the circular dependency. 

The next step was rather simple. After refactoring out the functionality which creates the factory for a type if it's not yet in the dictionary, I added the possibility to resolve that exact factory by providing a special function for that. In fact normal resolution turned into "resolve the factory and then execute it". Spoiler: I removed that function after I added generic resolving and turned resolution of a factory into a generic resolved. 

Singletons are handled by KittyDI either by setting an optional boolean parameter to true when registering the type of by adding an attribute to the type. If the type should be a singleton, it's factory is wrapped by one that on first call executed the inner factory and then just returns the result of that call on subsequent calls. 

The next rather important point was the ability to tell KittyDI which type to use to resolve an interface. This was also pretty naïvely implemented: First the function which ensures that the factory for a given type had been resolved is called for the implementing type and then it is put into the dictionary of known factories as factory for the"contract type". This way it is not just possible to do that for interfaces but for all types provided the implementing type actually inherits from our implements the contract. 

Next up was generic resolution. This means that I created a list of "generic resolvers" which intercept requests and instead of searching for constructors on the generic type perform their own logic. This is now used to resolve factories but it was also used to resolve IEnumerable<T> - something that I removed after I also added a generic resolver for Lazy<T>. I wanted nesting of generics to be allowed and IEnumerable proved troublesome here with the architecture KittyDI evolved into. This I scraped resolution of enumerables and moved that to PantherDI where I aim to give the architecture a bit more thought instead of letting it evolve. 

Now adding the possibility to resolve a factory which takes parameters was rather easy. I just needed to register a generic resolver which all have full access to the container and trigger resolution with the tires given in the parameters already set. The change I had to make was to add another parameter to my internal factories which is a dictionary of all types provided by the caller. While doing so, I decided to move all the information passed into an internal factory into an object instead of having to change the signature of functions all over KittyDI each time I added something. 

To prepare for more complex scenarios, the container supports adding whole containers. This enables the concept of scoping. Each step that is taken to resolve a type (checking the dictionary of known resolutions, checking if a generic type can be resolved and registering the type as new type) will search the DI-Containers that have been added to the container on which the original request was made. Only if the step did not yield any result when performed on the whole tree of containers, then the next step will be started.

Creating child containers then was rather easy. The child container is an empty container which searches its parent before failing each step. After creation, Types can be registered to the child, but they won't be known to the parent.
At the same time resolving a container (resolving the types "DependencyContainer" and "IDependencyContainer") yields a new child on each resolution.
This enables services or view models to register types only to the child and then resolve a helper that uses those.

Last but not least the container supports disposal. If it is disposed, it will dispose all instances of singletons that it has created as well as all child containers too.

Conclusion

As I already said writing a DI container is not that hard. But my main takeaway is - and I think that is a general rule in software development - that you should think beforehand about how you want to achieve your goal unless you want your code to become more and more messy and complicated to read over time.
Even though the PRISM integration isn't done, I see KittyDI as done and the experiment a success (KittyDI can be used in projects) and have many ideas - also on what to think about beforehand - for the mature version "PantherDI" (by now the source of the name should be obvious to the reader).

Further links