Skip to content

How is Autofac used in Catalyst.Node

nshCore edited this page Oct 10, 2019 · 1 revision

Dependency injection (DI)

Dependency injection is a very common way to introduce inversion of control in the architecture of a piece of software. Although it might appear overly complicated at first, inversion of control tends to result in architectures that are more modular, extensible, and easier to test than their procedural counterparts... Well, at least that is what we read on the web:

https://en.wikipedia.org/wiki/Inversion_of_control
https://martinfowler.com/articles/injection.html

Throughout the Catalyst.Node solution, the popular Autofac for .Net framework is used to provide dependency injection.

How is it done in Catalyst.Node

Catalyst.Node is composed of a bunch of mandatory components without which the node would basically be unable to function, as well as a bunch of modules, which are meant to be replaceable depending on your particular needs. For instance, it makes sense to think of a node running with a local file system versus an interplanetary one, with an in-memory mempool or with an SQL based one, but it is much harder to think of it with many different but compatible messaging protocols.
These modules are therefore treated as independent services and rely on their own separate configuration files.

Main (mandatory) components

Most of the DI configuration for mandatory components can be found in the components.json file. Here is how a basic configuration looks like:

"defaultAssembly": "Catalyst.Node.Core",
"components": [
{
   "type": "Catalyst.Node.Core.CatalystNode",
   "services": [
   {
       "type": "Catalyst.Node.Common.Interfaces.ICatalystNode, Catalyst.Node.Common"
   }]
},

components -> type : refers to the name of the type that should be used to satisfy the implementation of the various services.
services : this represents the list of interfaces that should be resolved using the concrete type defined above.
defaultAssembly : can be used to avoid having to explicitly specify the assembly in which a type should be found. in the example above, the type Catalyst.Node.Core.CatalystNode should be found in the Catalyst.Node.Core assembly. However Catalyst.Node.Common.Interfaces.ICatalystNode should be found in Catalyst.Node.Common.

A lot more information about Autofac configuration can be found here.

It is in that same components.json file that we can find links to the individual module configuration.

"modules": [
   {
     "type": "Catalyst.Node.Core.Modules.JsonConfiguredModule",
     "parameters": {
       "configFilePath": "Config/Modules/dfs.json"
     }
   },

module : groups a set of dependencies that only make sense in the context of a higher level functionality. For instance, a Dfs (Distributed File System) module might need an INetworkConnector that is not needed by, and might not even make sense to, a Cryptography module only supposed to group a bunch of mathematical functions used to encrypt messages.
type : is a type inheriting from Autofac.Module which defines how the module registers its dependencies. In our case, a JsonConfiguredModule has been implemented to allow a given module to read its own configuration, from a separate JSON file found at configFilePath

More information about Autofac modules configuration can be found here.

Modules configuration

As explained above, modules are meant to represent isolate and interchangeable sets of related functionalities. In Catalyst.Node, they rely on a given configuration file, which is read through the JsonConfiguredModule class. This is not mandatory and different modules could be implemented and configured differently. However, we decided to use this mechanism to allow different users of the node to swap the implementation of a given module simply by changing its configuration.

The source code contains an example of such a configuration change in the class MempoolIntegrationTests where a first mempool is configured to use an in-memory storage mechanism, while a second one is configured to use an XML file based store.

[Fact]
[Trait(Traits.TestType, Traits.IntegrationTest)]
public async Task Mempool_with_InMemoryRepo_can_save_and_retrieve()
{
    var fi = new FileInfo(Path.Combine(Constants.ConfigSubFolder, Constants.ModulesSubFolder,
        "mempool.inmemory.json"));
    await Mempool_can_save_and_retrieve(fi);
}

[Fact]
[Trait(Traits.TestType, Traits.IntegrationTest)]
public async Task Mempool_with_XmlRepo_can_save_and_retrieve()
{
    var mempoolConfigFile = "mempool.xml.json";
    var resultFile = await PointXmlConfigToLocalTestFolder(mempoolConfigFile);
    await Mempool_can_save_and_retrieve(new FileInfo(resultFile));
}

Autofac documentation

This article is only meant to explain briefly how Autofac is used in the Catalyst.Node solution, but it is advised to read the Autofac documentation which contains more in-depth information and helpful examples.