Thursday, April 27, 2017

Creating an IOC Container for ASP.NET Core

I recently added support for ASP.NET Core to my Tact.NET IOC Container, and I thought that I would share some of the interesting requirements I discovered while doing so. I had originally expected the default ASP.NET Core container would follow the same rules as the official Microsoft Unity container, but that turned out not to be the case!

1) Register is first in win.

The Unity container was last in win, and it would completely disregard the original registration. With ASP.NET Core you need to preserve the original registration and then treat all subsequent registrations as addition registrations that will only be resolved when ResolveAll is invoked, similar to keyed registrations in Unity. Which brings us to our next difference...

public class RegisterIsFirstInWinTests
{
    [Fact]
    public void Unity()
    {
        var log = new EmptyLog();
        using (var container = new TactContainer(log))
        {
            container.RegisterSingleton<IExample, ExampleA>();
            container.RegisterSingleton<IExample, ExampleB>();
 
            var example = container.Resolve<IExample>();
            Assert.IsType<ExampleB>(example);
        }
    }
 
    [Fact]
    public void AspNetCore()
    {
        var log = new EmptyLog();
        using (var container = new AspNetCoreContainer(log))
        {
            container.RegisterSingleton<IExample, ExampleA>();
            container.RegisterSingleton<IExample, ExampleB>();
 
            var example = container.Resolve<IExample>();
            Assert.IsType<ExampleA>(example);
        }
    }
 
    public interface IExample
    {
        string Name { get; }
    }
 
    public class ExampleA : IExample
    {
        public string Name => nameof(ExampleA);
    }
 
    public class ExampleB : IExample
    {
        public string Name => nameof(ExampleB);
    }
}

2) ResolveAll includes single registrations.

Unity has the concept of keyed registrations, where you can register the same type multiple times with distinct keys (strings) and then resolve them individually by key or as an IEnumerable with ResolveAll. In ASP.NET Core there is no concept of keyed registration, and ResolveAll will return all registrations made for a given type. This means that registering a single type will cause ResolveAll to return an IEnumerable with one value.

public class ResolveAllIncludesSingleRegistrations
{
    [Fact]
    public void Unity()
    {
        var log = new EmptyLog();
        using (var container = new TactContainer(log))
        {
            container.RegisterSingleton<IExample, ExampleA>();
            container.RegisterSingleton<IExample, ExampleB>();
 
            var examples = container.ResolveAll<IExample>();
            Assert.Equal(0, examples.Count());
        }
    }
 
    [Fact]
    public void AspNetCore()
    {
        var log = new EmptyLog();
        using (var container = new AspNetCoreContainer(log))
        {
            container.RegisterSingleton<IExample, ExampleA>();
            container.RegisterSingleton<IExample, ExampleB>();
 
            var examples = container.ResolveAll<IExample>();
            Assert.Equal(2, examples.Count());
            }
    }
 
    public interface IExample
    {
        string Name { get; }
    }
 
    public class ExampleA : IExample
    {
        public string Name => nameof(ExampleA);
    }
 
    public class ExampleB : IExample
    {
        public string Name => nameof(ExampleB);
    }
}

3) Generic resolution is required.

This was a neat feature that I had not used before! You can register generic types without any generic parameters, and then the container will fill in the generic parameters based on the resolve request. So, for example, I could register IRepository<> to type DataRepository<>, and then when I call container.Resolve<IRepository<User>>, the IOC container will automatically try to construct a DataRepository<User>

public class TactContainerTests
{
    [Fact]
    public void GenericResolution()
    {
        var log = new EmptyLog();
        using (var container = new TactContainer(log))
        {
            container.RegisterSingleton(typeof(IRepository<>), typeof(Repository<>));
 
            var userRepository = container.Resolve<IRepository<User>>();
            Assert.IsType<Repository<User>>(userRepository);
        }
    }
 
    public interface IRepository<T>
    {
        string ClassName { get; }
        string GenericName { get; }
    }
 
    public class Repository<T> : IRepository<T>
    {
        public string ClassName => nameof(Repository<T>);
        public string GenericName => typeof(T).Name;
    }
 
    public class User
    {
    }
}

4) You have to convert IServiceCollection to your container.

I like how the new .NET separates their service registration from the container itself. In your ASP.NET Core wire up you will work with an IServiceCollection, which will then be converted into a container after you are done filling in your registrations. This means that if you want to create your own container, you will need to map the service collection into your container.

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics