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);
}
}
}
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.