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.