ASP.NET Core 透過反射自動注入服務

在 ASP.NET Core 中有內建依賴注入(Dependency Injection, DI),在 Program.cs 中註冊服務並指定生命週期,在其他類別的建構子就可以神奇的自動取得依賴。

範例 Service:
    
public class UserService
{
}
    

註冊服務:
    
builder.Services.AddScoped<UserService>();
    

在建構子中自動取得依賴:
    
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{

    private readonly UserService _userService;

    public UserController(UserService userService)
    {
        _userService = userService;
    }
}
    

如果沒有在 Program.cs 中註冊就會得到下面的例外:
    
System.InvalidOperationException: Unable to resolve service for type 'UserService' while attempting to activate 'UserController'.
    

假設有抽介面的話也可以這樣使用:
    
builder.Services.AddScoped<IUserService, UserService>();
    

    
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{

    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }
}
    



這樣由框架自動管理服務的生命週期非常方便,但是隨著專案的成長,在 Program.cs 就會有幾十甚至百餘行程式碼都是在註冊服務,那有沒有辦法自動註冊呢?

筆者在 GitHub 上的 fullstackhero/dotnet-webapi-starter-kit 專案中發現了一個非常厲害的寫法,透過建立分別代表 Singleton, Scoped, Transient 這三個生命週期的介面,放在其他服務的介面上,用來標記這些需要被註冊,然後在程式啟動時使用反射把全部有繼承自訂生命週期介面的類別找出來一次註冊。

建立生命週期代表的介面:
    
// 在一個請求(Request)中共用一個實例
public interface IScopedService
{
}

// 只有一個實例,整個應用程式共用
public interface ISingletonService
{
}

// 獨立的實例,不會共用
public interface ITransientService
{
}
    

透過反射找出全部有繼承自訂生命週期介面的介面並註冊:
    
public static class Startup
{
    public static IServiceCollection AddServices(this IServiceCollection services) =>
        services
            .AddServices(typeof(ISingletonService), ServiceLifetime.Singleton)
            .AddServices(typeof(IScopedService), ServiceLifetime.Scoped)
            .AddServices(typeof(ITransientService), ServiceLifetime.Transient);

    private static IServiceCollection AddServices(this IServiceCollection services, Type interfaceType,
        ServiceLifetime lifetime)
    {
        var interfaceTypes = AppDomain.CurrentDomain.GetAssemblies() // 取得所有組件
            .SelectMany(s => s.GetTypes())
            .Where(type => interfaceType.IsAssignableFrom(type) // 實現介面
                           && type.IsClass // 是類別
                           && !type.IsAbstract // 不是抽象類別
            )
            .Select(type => new
            {
                Service = type.GetInterfaces().FirstOrDefault(), // 取得介面
                Implementation = type, // 取得實作
            })
            .Where(t => t.Service is not null // 確認介面不為空
                        && interfaceType.IsAssignableFrom(t.Service) // 確認介面實作介面
            );

        foreach (var type in interfaceTypes)
        {
            services.AddService(type.Service!, type.Implementation, lifetime);
        }

        return services;
    }

    private static IServiceCollection AddService(this IServiceCollection services, Type serviceType,
        Type implementationType, ServiceLifetime lifetime) =>
        lifetime switch
        {
            ServiceLifetime.Singleton => services.AddSingleton(serviceType, implementationType),
            ServiceLifetime.Scoped => services.AddScoped(serviceType, implementationType),
            ServiceLifetime.Transient => services.AddTransient(serviceType, implementationType),
            _ => throw new ArgumentException("Invalid lifeTime", nameof(lifetime))
        };
}
    

在 Program.cs 中註冊一次:
    
builder.Services.AddServices();
    

這樣只要把 service 抽介面,然後介面上依照生命週期掛上自訂的生命週期介面,例如 Scoped 就是 IScopedService
    
public interface IUserService : IScopedService
{
    List<UserDto> Get();
}

public class UserService : IUserService
{
    private readonly ILogger<UserService> _logger;
    private readonly ApplicationDbContext _context;

    public UserService(ILogger<UserService> logger, ApplicationDbContext context)
    {
        _logger = logger;
        _context = context;
    }

    public List<UserDto> Get()
    {
        return _context.Users
            .Select(x => new UserDto(x.Id, x.UserName))
            .ToList();
    }
}

public record UserDto(string Id, string UserName);
    

使用時:
    
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{

    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }
    
     [HttpGet]
     public ActionResult Get()
     {
         return Ok(_userService.Get());
     }
}
    

這樣就不需要在手動註冊了!
唯一的小問題就是會強迫抽介面,不過這樣比較方便測試,或許平時沒抽介面是我們偷懶,可能應該都要抽介面。

參考資料:
StackOverflow - ASP.NET Core automatically resolve all dependencies
GitHub - fullstackhero/dotnet-webapi-starter-kit

留言

張貼留言

如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com