1

I want to load all types from an assembly and register them for dependency injection, using the following reflection-heavy method:

using System.Reflection;

namespace Server
{
    public static class AddServices
    {
        public static IServiceCollection AddRepositories(this IServiceCollection services)
        {
            Assembly.Load("Services")
                .GetTypes()
                .Where(a => !a.IsAbstract && !a.IsInterface)
                .Select(a => new { assignedType = a, serviceTypes = a.GetInterfaces() })
                .ToList()
                .ForEach(typeToRegister =>
                {
                    if (typeToRegister.serviceTypes.Length != 1)
                        return;
                    services.AddScoped(typeToRegister.serviceTypes[0], typeToRegister.assignedType);
                });
            return services;
        }
    }
}

This ensures that I don't have to manually register lots of dependencies (and manually update the list every time I create a new dependency). It's true that this doesn't handle the scenario where a dependency implements multiple interfaces. For example, a dependency that implements both IRepository and IDisposable will be ignored by this method. But it's not difficult to add a denylist of interfaces that get ignored by this method (such as IDisposable).

My code doesn't have such dependencies, hence why it doesn't have the denylist.

The above code has worked perfectly for years, until I added a dependency with a generic method:

using Dto.Interfaces;

namespace Services
{
    public interface IRepository
    {
        public Task<decimal> Calculate<T>(List<T> list)
            where T : ICommonProps;
    }

    public class Repository : IRepository
    {
        public async Task<decimal> Calculate<T>(List<T> list)
            where T : ICommonProps
        {
            // implementation irrelevant
            await Task.Delay(100);

            foreach (var thing in list)
            {
                Console.WriteLine(thing);
            }

            return 0m;
        }
    }
}

Suddenly the program crashes during start-up with the following bizarre error message:

Cannot instantiate implementation type 'DotNetEightFeatureTesting.Server.Services.Repository+d__0`1[T]' for service type 'System.Runtime.CompilerServices.IAsyncStateMachine'

This is related to exactly how the Task framework works, and is irrelevant to API developers like myself. You can easily bypass this problem by changing the code to not use Tasks, but that's totally unsuitable for a production API.

I can easily bypass this problem by tweaking the AddRepositories method to ignore all generics:

.Where(a => !a.IsAbstract && !a.IsInterface && !a.IsGenericType)

But this is not suitable for anybody who actually has generic dependencies registered in their app.

How can I change the above AddRepositories method so that requests to both endpoints succeed, while preventing the fewest possible valid dependency injection scenarios (such as generic dependencies)?

I specifically do not want advice about how I could rearrange the API. This is a simplified example that's lacking all of my intermediate software layers. Also, try not to get lost in the weeds by speculating about all possible dependency injection scenarios; focus on the most common ones.

Here's the rest of the code, set up as a Web API solution. It uses 3 projects in one solution, named Server, Services, and Dto. The folder structure can be inferred from the namespaces. Server references Services and Dto, and Services references Dto (this is required for the minimum reproducible example, otherwise the AddRepositories method will find a bunch of extra rubbish).

Program:

using Server;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddRepositories()
    .AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Dto.Interfaces:

namespace Dto.Interfaces
{
    public interface ICommonProps
    {
        long Id { get; }

        string Name { get; }
    }
}

Dto:

using Dto.Interfaces;

namespace Dto
{
    public record Company(long Id, string Name, string TaxCode) : ICommonProps
    {
        public override string ToString() => $"{Id} {Name}: {TaxCode}";
    }

    public record Person(long Id, string Name, DateTimeOffset Birthday) : ICommonProps
    {
        public override string ToString() => $"{Id} {Name}: {Birthday}";
    }
}

Controllers:

using Microsoft.AspNetCore.Mvc;
using Dto;
using Services;

namespace Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class FlexibleController : ControllerBase
    {
        private readonly IRepository _repository;

        public FlexibleController(IRepository repository)
        {
            _repository = repository;
        }

        [HttpPost(nameof(CalculateForPeople))]
        public async Task<decimal> CalculateForPeople([FromForm] List<Person> people)
        {
            return await _repository.Calculate(people);
        }

        [HttpPost(nameof(CalculateForCompanies))]
        public async Task<decimal> CalculateForCompanies([FromForm] List<Company> companies)
        {
            return await _repository.Calculate(companies);
        }
    }
}
3
  • I also think you need to be careful about using this reflection code. For example, if an interface is added to one of your services, the call to GetInterfaces() might give you the wrong one. Also, any types in that assembly are being added to DI because you are not filtering at all. I strongly recommend filtering to only types that implement your service interface for example. Commented Jul 21 at 13:34
  • I guess you missed the if statement that ignores all types that don't have exactly one interface. If a type has exactly one, then the code will definitely find the right one. Commented Jul 21 at 13:46
  • So adding a second interface to a service will break your DI, even worse! Commented Jul 21 at 14:13

1 Answer 1

2

The reason for this exception is not that you registered IRepository <- Repository, but that you registered a class that is generated by Task<T>: IAsyncStateMachine <- Repository+d__0`1[T]. When the app is built by calling .Build(), it checks whether all registered implementation types can be instantiated, and the exception occurs. The solution is that you should ignore these compiler generated classes, for example since these generated classes are private, you can change .GetTypes() to .GetExportedTypes()

Sign up to request clarification or add additional context in comments.

7 Comments

To be clear, my code made no attempt to load that type, because none of it uses IAsyncStateMachine. Microsoft's Task code is the stuff that generates a bunch of helper classes that implement IAsyncStateMachine, which in turn get picked up by the call to .GetTypes() but not .GetExportedTypes().
Did you mean to edit in when the app is "started" as opposed to "built"?
At first, I thought this exception is caused by DI when resolving something. But the OP said the code didn't load that type, so after looking at the source code, I found that the registered service types will be checked when builder.Build() is invoked.
That depends on ServiceProviderOptions, which has different default values in Development environments. (andrewlock.net/…)
@JeremyLakeman The check occurs in the constructor of CallSiteFactory, so that doesn't depend on the options but on the service provider.
So that is still at runtime. Saying when the "app is built" implies compile time, hence why I asked.
Yes, the validation checks occur when constructing a service for the first time, or when ServiceProviderOptions.ValidateOnBuild is set. Which is implemented by CallSiteFactory in both cases.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.