Allow IEnumerable<T> to resolve items lazily in MS.DI

Viewed 87

I've got a very big list of ISample registrations in my MS.DI container, which I inject as an IEnumerable<ISample>. But at runtime I typically need a very few. MS.DI, however, always creates all items of the collection immediately, which causes a performance problem in my application.

Is there a way to allow items of an injected IEnumerable<T> to be loaded lazily, making it function as a stream, rather than a pre-populated array?

Here's my sample code that demonstrates the problem:

services.AddTransient<ISample, SampleA>();
services.AddTransient<ISample, SampleB>();
services.AddTransient<ISample, SampleC>();

public class SampleA : ISample 
{
    public Guid Id  = FirstGuid;
    public SampleA()
    {
        Console.WriteLine("SampleA created.");
    }
}
public class SampleB : ISample 
{
    public Guid Id  = SecondGuid;
    public SampleB()
    {
        Console.WriteLine("SampleB created.");
    }
}
public class SampleC : ISample 
{
    public Guid Id  = ThirdGuid;
    public SampleC()
    {
        Console.WriteLine("SampleC created.");
    }
}

In other class I use service provider for creating an instance of any of these classes.

public ISample GetInstance(Guid Id)
{
     return _serviceProvider.GetServices<ISample>().FirstOrDefault(d => d.Id==Id);
}

What's the best way to prevent all items to be pre-populated?

3 Answers

For solve this issue I would suggest using Dictionary<Guid, Type> and add all your implementations to it by their Ids, this what I've tried and works fine:

 public interface ISample
    {
        public Guid Id { get; }
    }
    public class SampleA : ISample
    {
        public Guid Id => Guid.Parse("3f30ae05-b88e-4abf-85b5-22e7ce4b639f");
        public SampleA()
        {
            Console.WriteLine("SampleA created.");
        }
    }
    public class SampleB : ISample
    {
        public Guid Id => Guid.Parse("c4a5b853-433b-4889-af41-cb99a8c71c4a");
        public SampleB()
        {
            Console.WriteLine("SampleB created.");
        }
    }
    public class SampleC : ISample
    {
        public Guid Id => Guid.NewGuid();
        public SampleC()
        {
            Console.WriteLine("SampleC created.");
        }
    }

and in your startup register them like this:

services.AddTransient<SampleA>();
services.AddTransient<SampleB>();
services.AddTransient<SampleC>();

var dic = new Dictionary<Guid, Type>()
{
 {Guid.Parse("3f30ae05-b88e-4abf-85b5-22e7ce4b639f"), typeof(SampleA)},
 {Guid.Parse("c4a5b853-433b-4889-af41-cb99a8c71c4a"), typeof(SampleB)},
 {Guid.Parse("c4a5b853-433b-4889-af41-cb99a8c71c4a"), typeof(SampleC)},

};

//you will inject this
services.AddScoped<Func<Guid, ISample>>
(provider => (id) => provider.GetService(dic[id]) as ISample);

and I you class inject it in this way:

public class UseDI
    {
        private readonly Func<Guid, ISample> _funcSample;

        public UseDI(Func<Guid, ISample> funcSample)
        {
            _funcSample= funcSample;
        }


        public ISample GetInstance(Guid Id)
        {
            return _funcSample(Id);
        }
    }

I have tested this var sampleB=GetInstance(Guid.Parse("c4a5b853-433b-4889-af41-cb99a8c71c4a")) and only SampleB constructor run.

The default behavior of MS.DI is to resolve the complete collection. This is done both when you call GetServices or when you inject an IEnumerable<T>. Under the covers the enumerable is an array of eagerly loaded instances.

Although you could refactor your code to use factories of some sort, you can also register an enumerable that truly acts as a stream. This, however, is not something that is supported out of the box with MS.DI. But this can be achieved using the following code:

public static void AddStream<TService>(
    this IServiceCollection services, params Type[] implementations)
{
    foreach (var implementation in implementations)
    {
        // TODO: Check if implementation implements TService
        services.AddTransient(implementation, implementation);
    }

    services.AddTransient(c =>
        implementations.Select(c.GetRequiredService).OfType<TService>());
}

When called, this extension method registers all supplied implementations as transient inside the container and registers an IEnumerable<T> that starts resolving instances when its iterated.

The AddStream extension method can be called as follows:

services.AddStream<ISample>(typeof(SampleA), typeof(SampleB), typeof(SampleC));

Depending on your needs, it will get more complicated though, because the above example does not support:

  • Registering generic types (and it would be quite cumbersome to add a fully functional implementation that allows registering types for generic abstractions that support streaming)
  • O(n) access when using .Last() or .ElementAt(x). This would require implementing returning a special ICollection<T> that allow LINQ extension methods to work on. This would also be much more cumbersome to write. Switching to a different DI Container that supports streaming OOTB would in that case be a better option.

No note though that even with this lazy approach, you will still be initializing half of the list on average (meaning a performance of O(n/2). If you want O(1) performance, consider using either a dictionary instead.

It depends on the nature of was is really done in // do some thing.

If this work is always the same regardless of the instance created, it could be initialized once with a static constructor.

Otherwise, your intuition to add an Initialize method in ISample will guide you in the right direction. My only concern is that calling this method inside GetInstance will not change anything performance wise because you just deferred the same work from the constructor to an initialization method and will introduce a temporal coupling between the constructor and this method. That is, forgetting to call this method after the construction and the returned instance will be in a corrupted state (this is even worse than before).

If you want to fully go down the road, you need to ask yourself how to defer this complex code from the object's construction (this is considered as an anti-pattern) to a subsequent method call. But again, without seeing what is really done in the constructor and where and how the initialized part is used, I can't really say anything as how to do it.

Related