Grace: The perfect DI IoC container [part 2] 💻🤓

GRACE

Overview

As we already explained in the previous post, Grace is a feature rich dependency injection container. It’s our pick at IdiWork, and that’s not by coincidence: It has proven to be the best container we’ve tried for years. It’s easy to configure and use and it’s FAST.

We used it in real life projects and our experience couldn’t be better.

Let’s go with Grace!

We know you did your homework since our last post on the topic and you already know what’s a DI IoC Container like Grace. But, in case you missed it, please read it here.

NOTICE

Before you go ahead, please, ensure that you are familiarized with Dependency Injection. If you still don’t know what’s a Composition Root, or don’t know what “constructor injection” is all about, please read about the topic. A very good read is Dependency Injection in .NET, by Mark Seemann.

For those that already know what’s the deal with DI and containers, let’s go straight to the point.

First steps

NOTE: In order to use Grace, you will have to install the Grace NuGet Package.

Everything in Grace gravitates around the dependency injection container itself. That’s the class called DependencyInjectionContainer. Surprise!

Normally, inside your Composition Root, you will create an instance of DependencyInjectionContainer and configure it to your needs.

This is the most basic code to do it:

var container = new DependencyInjectionContainer();
container.Configure(x => x.Export<SomeType>().As<ISomeInterface>());

The Configure method is used to configure the container. It can contain multiple exports registrations:

container.Configure(c =>
	{
		c.Export<ServiceA>().As<IServiceA>();
		c.Export<ServiceB>().As<IServiceB>();
		c.Export<ServiceC>().As<IServiceC>();
	});

Also, you can invoke this method more than once, but it’s not necessary most of times.

Easy, uh?

Now, let’s look at some of the most common use cases.

Common Use Cases

In our experience, we have learnt that dependency injection should be as transparent for the developer as it could be. So, we should try to keep everything simple (that’s a good recommendation for every development process, hence the KISS principle). Regarding configuration, we should absolutely stick to simplicity as much as we can.

When type registration starts becoming unreadable and too complex, it can be a good indicator that your design can be improved.

The are many very common scenarios that almost every application will need to successfully apply container-based DI.

Here is a list that intends to cover most of them. Use this section as a reference 😊 It contains a lot of recipes ready to use.

Tip: All the code you’ll find below can be run directly on LINQPad (be sure to install the  Grace NuGet Package)

Factory method

You can tell the container how to locate your instance supplying a custom factory. The container will provide you with the dependencies you need using the appropriate overload (up to 5 dependencies! That should be more than enough 😄). In the sample, with explicitly say that our factory will require IDependency. Please, notice how the dep argument is filled by Grace automatically.

void Main()
{
	var container = new DependencyInjectionContainer();
	var random = new Random();

	container.Configure(c =>
		{
			c.ExportFactory<IDependency, Service>(dep => new Service(random.NextDouble(), dep));
		});
		
	var service = container.Locate<Service>();
}

public interface IDependency { }
public class Dependency { }

public class Service
{
	public Service(double v, IDependency dependency)
	{
	}
}

Open generic

You can export an open generic type. The container will provide the correct instance of the closed type when needed.

void Main()
{
	var container = new DependencyInjectionContainer();
	container.Configure(x =>
	{
		x.Export(typeof(GenericService<>)).As(typeof(IGenericService<int>));
	});	
	
	var service = container.Locate(typeof(IGenericService<int>));
}

interface IGenericService<T> {}

class GenericService<T> : IGenericService<T> {}

Batch export by interface

Useful to export a set of instances implementing a given interface. Please, notice that in this sample the MyService will have an enumerable with all the exported components.

void Main()
{
	var container = new DependencyInjectionContainer();
	var assembly = Assembly.GetExecutingAssembly();
	
	container.Configure(c =>
	{
		c.ExportAssemblies(new[] { assembly })
			.ByInterface<IComponent>();
	});	
	
	var service = container.Locate<MyService>();
}

interface IComponent { }
public class ComponentA : IComponent {}
public class ComponentB : IComponent {}

class MyService
{	
	public MyService(IEnumerable<IComponent> components)
	{		
	}
}

Marker interface multiexport

You can mark your dependencies with a marker interface to export them as the interfaces they implement. In this example, IDeserveAService is our marker interface.

void Main()
{
	var container = new DependencyInjectionContainer();
	var assembly = Assembly.GetExecutingAssembly();
	
	container.Configure(c =>
	{
		c.ExportAssemblies(new[] { assembly })
			.Where(y => typeof(IDeserveAService).IsAssignableFrom(y))
			.ByInterfaces();
	});	
	
	var service = container.Locate<MyService>();
}

interface IDeserveAService { }
interface ISomeServiceA { }
interface ISomeServiceB { }
public class ServiceA : ISomeServiceA, IDeserveAService {}
public class ServiceB : ISomeServiceB, IDeserveAService {}

class MyService
{	
	public MyService(ISomeServiceA serviceA, ISomeServiceB serviceB)
	{		
	}
}

Types based on given base, as the concrete type

You can export all the types derived from a base type. Later, you can get them injected via their concrete type.

void Main()
{
	var container = new DependencyInjectionContainer();
	var assembly = Assembly.GetExecutingAssembly();
	
	container.Configure(c =>
	{
		c.ExportAssemblies(new[] { assembly })
			.BasedOn<ViewModel>()
			.ByType();
	});	
	
	var service = container.Locate<MyService>();
}

public abstract class ViewModel { }
public class ViewModelA : ViewModel { }
public class ViewModelB : ViewModel { }


class MyService
{
	public MyService(ViewModelA viewModelA, ViewModelB viewModelB)
	{
	}
}

Types derived from a given type, as base type

This is used to export all types derived from another, but not as concrete types, but using the base type. This way, you could get a list of all the derived classes like the MyService class in the sample.

void Main()
{
	var container = new DependencyInjectionContainer();
	var assembly = Assembly.GetExecutingAssembly();
	
	container.Configure(c =>
	{
		c.ExportAssemblies(new[] { assembly })
			.BasedOn<ViewModel>()
			.ByTypes(x => new[] { x.BaseType });
	});	
	
	var service = container.Locate<MyService>();
}

public abstract class ViewModel { }
public class ViewModelA : ViewModel { }
public class ViewModelB : ViewModel { }

class MyService
{
	public MyService(IEnumerable<ViewModel> viewModels)
	{
	}
}

Semi-automatic injection (extra data injection)

You can provide extra data that will be used to build the dependency. I call this “semi-automatic” because a part of the construction relies on the container and another part relies on our code. This can be super useful when you need your dependencies to receive a parameter that varies according to some algorithm. In addition to the below sample you can check this use case.

void Main()
{
	var container = new DependencyInjectionContainer();

	container.Configure(c =>
	{
		c.Export<OtherDependency>().As<IOtherDependency>();
	});

	var service = container.Locate<Service>("injected string");
}

class Service
{
	public Service(string parameter, IOtherDependency otherDependency)
	{		
	}
}

interface IOtherDependency { }
class OtherDependency : IOtherDependency { }

Factory injection (Func<T>)

If your class declares a Func<T> dependency, Grace will provide automatically a delegate which with you can use to resolve the type T. It’s in fact, a factory method injection. You don’t need to create an interface and register it. It’s done by Grace for you! Check the sample 😊

void Main()
{
	var container = new DependencyInjectionContainer();

	container.Configure(c =>
	{
		c.Export<Dependency>().As<IDependency>();		
	});

	var service = container.Locate<Service>();
}

class Service
{
	public Service(Func<IDependency> factory)
	{		
		var dependency = factory();
	}
}

interface IDependency { }
class Dependency : IDependency { }

Metadata (for multiple registration)

Grace also supports a rather useful feature: Exports Metadata. You can attach metadata to your exports. The metadata can be retrieved using MetaData<T> instead of T in your constructors. Grace will wrap your dependency inside a MetaData<T> that will contain not only the metadata, but also the requested instance. Awesome, don’t you think?

void Main()
{
	var container = new DependencyInjectionContainer();
	var assembly = Assembly.GetExecutingAssembly();

	container.Configure(c =>
	{
		c.ExportAssemblies(new[] { assembly })
			.ByInterface<IDependency>()
			.ExportAttributedTypes();
	});

	var service = container.Locate<Service>();
}

class Service
{
	public Service(Meta<IDependency> dependency)
	{		
		var name = (string)dependency.Metadata["Name"];
		var order = (int)dependency.Metadata["SortOrder"];
	}
}

interface IDependency { }

[Metadata("Name", "My dependency")]
[Metadata("SortOrder", 1)]
public class Dependency : IDependency { }

Metadata (for single registration – not using attributes)

Attaches metadata to an export. This method doesn’t require to decorate the classes with the Metadata attribute.

void Main()
{
	var container = new DependencyInjectionContainer();

	container.Configure(c =>
	{
		c.Export<A>().WithMetadata("Hello", "World");
	});

	var service = container.Locate<Service>();
}

class A { }

Decorator (to import decorated instances)

The Decorator Pattern can be very useful. Grace provides you a super simple way to configure the automatic creation of decorators with the ExportDecorator method.

void Main()
{
	var container = new DependencyInjectionContainer();
	var assembly = Assembly.GetExecutingAssembly();

	container.Configure(c =>
	{
		c.ExportAssemblies(new[] { assembly })
			.ByInterface<IShape>();
			
		c.ExportDecorator<IShape>(shape => new ShapeDecorator(shape));
	});

	var service = container.Locate<Service>();
}

class Service
{
	public Service(IEnumerable<IShape> shapes) 
	{		
	}
}

Stay up to date!



Leave a comment