Search code examples
c#dependency-injectionservice-provider

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


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?


Solution

  • 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.