Grace: The perfect DI IoC container [part 1] đ»đ€
GRACE
Dependency Inversion Principle
Weâre in 2020 and you really need to know a few programming principles if you want to be a winner. One of the most important is the Dependency Inversion principle. It says that we should get our dependencies from the outside. What does it mean? Let’s see it with one example. Imagine that you have a beautiful class that represents some algorithm:
class Algorithm { public int DoIt(int[] input) { var serviceA = new ServiceA(); var serviceB = new ServiceB(); ⊠} }
… youâre going to get into some serious troubles when you’re trying to test it. Moreover, youâll be having a hard time if the services A and B change their constructors because youâll have to supply the new parameters in every place they’re created! Moreover, youâre coding against implementations of your dependencies instead of interfaces, so dependencies wonât be replaced easily.
Thereâs a better way to implement the Algorithm class. How about this?
class Algorithm { private IServiceA serviceA; private IServiceB serviceB; public Algorithm(IServiceA serviceA, IServiceB serviceB) { this.serviceA = serviceA; this.serviceB = serviceB; } public int DoIt(int[] input) { ⊠} }
We simply moved the responsibility of newing up the dependencies (the services) to the caller, whoever the caller is, and that we now rely on interfaces instead of concrete implementations.
This allows for a lot of -ability words that will grant you glory and future satisfaction with your code. If youâre unfamiliar with this approach, you should start reading about the Dependency Inversion Principle right now đ, in addition to the rest of the SOLID principles.
Containers
There are some ways to put principles into practice. One of the most popular is to use a software artifact called DI IoC container (Dependency Injection/Inversion of Control container).
With this approach, the container acts like a âmagic boxâ. You tell it âhey, give me an instance of this typeâ, and the container is smart enough to provide you with an instance of that type.
The interesting thing is that the container will just know how to locate those instances. Sometimes it will reuse existing instances. Sometimes it will create new instances. The interesting and important things is that you donât have to create anything. It will do.
In order to get the advantage of a container, youâre supposed to design your classes well, and this involves applying the DI principle shown above.
How do I use DI IoC containers
Most containers are similar in terms of the basic usage. You usually register types with the container and later, you locate those types.
Container registration
For the container to work, you need to configure it. This is often called âregistrationâ.
The most basic registration is Interface to Type registration:
Letâs see it with an example:
container.Export<MyService>().As<IMyService>();
This is equivalent to saying âwhenever you need IMyService, use the MyService implementationâ
From now on, the container will use the given implementation when itâs asked for that interface.
Container location
Once the registration is done, we can ask the container:
var myService = container.Locate<IMyService>();
The container will provide an instance of MyService, because itâs configured to do so.
It looks too easy, doesnât it?
Letâs see a more complex example:
class One : IOne { public One(IAnother another) { ⊠} } class Another : IAnother { public Another() { ⊠} }
Now, One depends on Another.
Letâs configure our container
container.Export<One>().As<IOne>(); container.Export<Another>().As<IAnother>();
Finally, we are going to use the container to “locate” the type IOne
var one = container.Locate<IOne>();
You ask for an instance of One, but to create One, we need an instance implementing IAnother!
However, the container is smart and will analyze how instances are created and will supply the constructor parameters according to the registration (configuration).
Analyzing how the container works
Consider this call:
container.Locate<IOne>();
From the point of view of the container, we do the following:
- Oh, Iâm asked for an instance of type IOne. I know how to locate it! I should give an instance of type One
- Letâs create an instance of One
- Ouch, One needs an IAnother to be created! (itâs required in the constructor)
- Letâs create IAnother. I know how to locate it! Just create an instance of Since Another doesnât have dependencies, it can be constructed right away.
- Now that we have Another, we can now One!
- Letâs create the instance One by injecting the instance of Another in the constructor.
- Done! Letâs retrieve the instance of One to the caller
- Success!
As things get complex, the container becomes more and more useful because you only register the times you can to make it aware of, and associated interfaces to implementations.
So, from your programming point of view, you just design your classes making dependencies explicit (in the constructor) and later, the container will supply the dependencies automatically for you.
This is only the beginning. If you want to become a master with dependency inversion, thereâs a very good book on the matter: Dependency Injection in .NET, by Mark Seemann.
There are a lots of containers out there that you can use to max out the DI principle, but thereâs one that excels at itâs goal, and itâs Grace.
Meet Grace
Grace is the DI IoC container of our choice for a lot of reasons:
- Simplicity
- Elegance
- Low learning curve
- Itâs right to the point: no useless stuff, no confusing API, only high-quality bits inside
- It integrates with EasyRPC like a charm đ
This is how itâs configured and how a locate call look.
using Grace.DependencyInjection; var container = new DependencyInjectionContainer(); container.Configure(c => c.Export<BasicService>().As<IBasicService>()); var basicService = container.Locate<IBasicService>();
It’s as easy as create, configure, and locate.
It has an extensive set of useful features:
- Fluent interface or Attributes for configuration allowing for maximum flexibility
- Supports child containers and lightweight lifetime scopes
- Contextual binding support (similar to NInject)
- IDisposable objects created by the container will be tracked and disposed of by the container unless configured otherwise.
- Performance characteristics that make it one of the fastest containers available. (Benchmarks)
- Supports special types
- IEnumerable<T> – supports resolving collections as IEnumerable<T> as well as most other types of collections List<T>, ReadOnlyCollection<T>, T[] and any collection that implements ICollection<T>
- Func<T> – supports resolving Func<T> automatically
- Lazy<T> – when resolved a Lazy<T> will be created that resolves T from the scope it was created in
- Owned<T> – object resolved within an Owned<T> will have their disposal lifecycle tied to the Owned<T> (similar to Autofac)
- Meta<T> – objects resolved within a Meta<T> are resolved along with their metadata
- Custom Delegates – any delegate that returns a type can be automatically resolved.
- Custom interface factories with Grace.Factory
- Many LifeStyles supported including Singleton, SingletonPerScope, SingletonPerRequest (MVC4, MVC5 & WCF packages), SingletonPerObjectGraph, SingletonPerAncestor<T>, and WeakSingleton. If none of the provided lifestyles meet your need you can always implement your own ICompiledLifeStyle class.
- Built-in support for the decorator pattern
- Support for custom wrappers (Func<T> and Meta<T> are examples of built-in wrappers)
- ASP.Net Core support
Whatâs next
In the next part of our article, weâll show some of those wonderful features with working code and examples.
In the meanwhile, you can play around with it. Basic stuff is easy to do and it has nice documentation.
Useful links
Author: Ian Johnson
Written by: Jose Manuel Nieto, part of Idiwork’s team.