In the previous section we tried to get a clear picture about what is meant by a decorator is and how decorator pattern helps in maintaining code integrity and helps extending an already implemented functionality without having to modifying the actual functionality itself, thereby satisfying the Open/Closed Principle in its core idea.
While theoretically the core idea is same for all ecosystems, implementations differ in each framework depending on their features and capabilities.
In this article let’s look at how we can create a simple Decorator implementation for an existing component using a Dependency Injection container.
We shall look at how it can be implemented in various DI providers such as Autofac and Ninject and finally look at how we do it with the built-in DI container that comes with ASP.NET Core
Various popular DI (Dependency Injection) containers such as Ninject and Autofac have different code syntaxes to implement decorator pattern.
How to implement a Decorator – Design
In theory, we inject a derivative of an interface as a constructor parameter to a wrapper component which itself is a derivative of the same interface.
public interface IDataReader
{
List<Data> ReadData();
}
// Implements the IDataReader
public class DataReader : IDataReader
{
public List<Data> ReadData()
{
// some domain logic
// returns a dataset
return data;
}
}
// Implements the IDataReader
public class FormattedDataReader : IDataReader
{
IDataReader reader;
public FormattedDataReader(IDataReader reader)
{
this.reader = reader;
}
public List<Data> ReadData()
{
List<Data> data = this.reader.ReadData();
// functionality to further process
// the fetched dataset happens here
// return the new processed dataset
return data;
}
}
In the above code snippet, both DataReader and FormattedDataReader implement the interface IDataReader and the FormattedDataReader receives a parameter of IDataReader for itself.
We pass an instance of DataReader into the FormattedDataReader and since DataReader implements IDataReader already, it is fully substitutable for IDataReader satisfying the Liskov Substitution principle.
The FormattedDataReader wraps an additional functionality that should work on the data that is otherwise returned by the DataReader (the original implementation).
One of the best use cases where the Decorator pattern really shines is the scenarios of implementing Caching layer over a TokenManager component where we can extend the TokenManager component to further cache an already produced response thereby reducing load on the server.
Register Decorator services in Autofac, Ninject and default DI containers –
In a DependencyInjection scenario, we would need to think in way in which we can safely register both the implementations DataReader and FormattedDataReader as substitutions for IDataReader type.
Ninject –
In a Ninject container, we register FormattedDataReader to be substituted for type IDataReader and within the FormattedDataReader constructor we pass in an instance of type DataReader by means of the WithConstructorArgument() chained method as below:
IKernel container = new StandardKernel();
container.Bind<IDataReader>()
.To<FormattedDataReader>()
.InSingletonScope()
.WithConstructorArgument<IDataReader>(container.Get<DataReader>());
the Ninject container now passes an instance of the DataReader class as parameter to the constructor of the type FormattedDataReader and it is substituted whenever an instance of type IDataReader is requested.
Autofac –
In Autofac, we implement the same as below:
IContainer container;
var builder = new ContainerBuilder();
builder.RegisterType<DataReader>()
.Named<IDataReader>("reader")
.SingleInstance();
builder.RegisterDecorator<IDataReader>(
(c, innerParam) => new FormattedDataReader(innerParam),
fromKey: "reader"
);
container = builder.Build();
Autofac offers a special method RegisterDecorator() which registers the passed type as a Decorator with additional options for specifying the inner params.
The container passes an instance of the class DataReader which is registered as a named type under key “reader” to the constructor of the type FormattedDataReader, whenever an instance of type IDataReader is requested.
default ASP.NET Core DI –
Implementing Decorator using the container built into ASP.NET Core is not a simple approach. Typically, we can register a service with only two type arguments – an abstract type and a substituting concrete type.
services.AddSingleton<IDataReader, FormattedDataReader>();
Hence if we register using the AddSingleton() method, we end up in a “circular reference runtime error”. The DI container recursively substitutes FormattedDataReader for a type IDataReader causing it into an unending loop, since the FormattedDataReader type expects an instance of type IDataReader in its constructor and IDataReader resolves to FormattedDataReader again and the loop continues.
To fix this, we make use of another overload of the method AddSingleton() which expects a single Type parameter and a predicate functionality in the parameter in which we can configure what to substitute for the specified abstract type.
services.AddSingleton<IDataReader>(
x => new FormattedDataReader(
new DataReader()
)
);
In this overload of the AddSingleton() method of the IServiceCollection, we explicitly specify the object instance to be passed when an instance of type IDataReader is requested, along with an instance of type DataReader as the constructor parameter.
Since we’re explicitly creating an object using the new keyword, we can further sophisticate this by replacing the new instantiation with dynamic instantiation.
services.AddSingleton<IDataReader>(
x => ActivatorUtilties.CreateInstance<FormattedDataReader>(x,
ActivatorUtilties.CreateInstance<DataReader>(x)
)
);
We can call CreateInstance() from the class ActivatorUtilities under the Microsoft.Extensions.DependencyInjection namespace which returns an instance of the specified type, similar to what a new keyword would do.
Found this article helpful? Please consider supporting!
The method also takes in an instance of type IServiceCollection, if needed to be used for resolution and any constructor parameters for that instance which is returned. Hence we can modify the above code snippet to use CreateInstance() as below: