종속성 주입에서 키 지정된 서비스 지원(DI) 관련 정보 공유

제가 2년전만 해도 주로 4.7버전 mvc4에서 작업을 했습니다. 그러다 보니 닷넷에서 DI관련 정보를 거의 접하지 못했네요.
3년전에는 스프링부트 2.x를 이용해서 개발 경험이 있었는데 DI를 위한 설정이나 주입법이 다양한 것을 보았습니다.
그래서 닷넷에서 DI는 많이 접하지 못했는데 오늘 새로운 8(9아님)의 새로운 기능을 확인 하다 보니 키 기반 주입 내용이 있더라구요. 반가워서 공유합니다.
버전이 올라가면서 하나하나 추가되다 보니 DI 기능이 어디까지 지원하는지 알 수가 없는데 혹시 관련정보가 모여 있다면 정보 공유 부탁 드립니다.

-정의

키 서비스는 키를 사용하여 DI(종속성 주입) 서비스를 등록하고 검색하는 메커니즘을 나타냅니다. 서비스는 키를 등록하기 위해 호출 AddKeyedSingleton (또는 AddKeyedScoped AddKeyedTransient )하여 키와 연결됩니다. 특성을 사용하여 키를 [FromKeyedServices] 지정하여 등록된 서비스에 액세스합니다. 다음 코드는 키 지정된 서비스를 사용하는 방법을 보여 줍니다.

  • 대상코드
// ICache 다형성을 부여받은 두개의 서비스 등록
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

// 각 엔드포인트 마다 ICache 다형성을 부여받은 선택적 주입
app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
                                                               smallCache.Get("date"));
  • 전체 코드
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
                                                               smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
    [HttpGet("big-cache")]
    public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
    {
        return cache.Get("data-mvc");
    }
}

public class MyHub : Hub
{
    public void Method([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}
  • 원문링크
2개의 좋아요

FromKeyedServiceAttirubte 가 생긴 건 참 좋은데 이게 참 계륵같은게

const 형태의 고정 키값이 들어가야만 합니다.

결국 비즈니스 로직에서 동적으로 할당해야하는 부분들에서는

기존처럼 IServiceProvider 에서 꺼내와야 하는건 마찬가지더라구요

1개의 좋아요

닷넷의 기본 서비스 컨테이너의 약점이 8.0 버전에 이르러서야 비로소 해소된 것입니다.

KeyedService 기능이 없었을 때는(7.0 이하일 때는), 다형성에 기반한 종속성 주입이 어려웠습니다.

아래와 같이 파생 객체를 여러 개 등록하면,

// ICache 다형성을 부여받은 두개의 서비스 등록
builder.Services.AddSingleton<ICache, BigCache>();
builder.Services.AddSingleton<ICache, SmallCache>();

등록된 모든 서비스를 전부 주입받거나,

app.MapGet("/big", (IEnumerable<ICache> caches) => // ...

마지막에 등록된 하나만 주입 받는 것만 가능했습니다.

app.MapGet("/big", (ICache cache) => // chche is SmallCache

호스트(의 서비스 컨테이너)를 설정하는 것은 비지니스 로직이 생성되기도 전에 실행됩니다.

그래서, 비지니스 로직의 생성 과정 중 종속성을 주입받을 때는 키 값은 상수일 수 밖에 없습니다.

예전에는 특성의 값이 리터럴이어야 했지만, C# 11부터는 nameof 를 쓸 수 있게 되었습니다.

app.MapGet("/big", 
 ([FromKeyedServices(nameof(BigCache))] ICache bigCache) => // ...

const에는 nameof 가 허락되기 때문에, 키를 아래와 같이 지정할 수도 있고,

// ICache 다형성을 부여받은 두개의 서비스 등록
builder.Services.AddKeyedSingleton<ICache, BigCache>(nameof(BigCache));
builder.Services.AddKeyedSingleton<ICache, SmallCache>(nameof(SmallCache));

C# 10 부터는 const에 상수 식에 기반한 문자열 보간도 허락되어, 리펙토링 문제도 없다고 볼 수 있습니다.

const string BigCache1 = $"{nameof(BigCache)}1";
const string BigCache2 = $"{nameof(BigCache)}2";
3개의 좋아요

IModelBinder를 이용해서 무언가 개발한 경험이 있는데 이것을 이용해서 동적 주입방법을 작성해보았습니다.
응용하시면 여러가지 방법을 만드실 수 있을 겁니다.

- 주요코드

  • 콘트롤러 진입전 요청으로 부터 모델을 생성하는 코드
    using Microsoft.AspNetCore.Mvc.ModelBinding;

    public class CustomModelBinder : IModelBinder
    {
        private readonly IServiceProvider _serviceProvider;

        public CustomModelBinder(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var parameterValue = bindingContext.ValueProvider.GetValue("type").FirstValue;

            IService service = parameterValue switch
            {
                "A" => _serviceProvider.GetService<ServiceA>(),
                "B" => _serviceProvider.GetService<ServiceB>(),
                _ => throw new InvalidOperationException("Invalid parameter value")
            };

            bindingContext.Result = ModelBindingResult.Success(service);
            return Task.CompletedTask;
        }
    }
  • 특정 controller action의 파라메터 타입에 대한 모델바인더 처리가 매핑
    using Microsoft.AspNetCore.Mvc.ModelBinding;
    using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;

    public class CustomModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context.Metadata.ModelType == typeof(IService))
            {
                return new BinderTypeModelBinder(typeof(CustomModelBinder));
            }
            return null;
        }
    }
  • 싱글톤 객체 및 모델바인더 등록
builder.Services.AddSingleton<ServiceA>();
builder.Services.AddSingleton<ServiceB>();

builder.Services.AddControllersWithViews(options =>
{
    options.ModelBinderProviders.Insert(0, new CustomModelBinderProvider());
});
  • Action에 ModelBinder를 이용한 실행시간 주입 결정
[HttpGet]
public IActionResult Get([ModelBinder] IService service)
{
    return Ok(service.GetData());
}
  • Test 방법

localhost:7018/api/ditest?type=A
localhost:7018/api/ditest?type=B

2개의 좋아요

덜덜덜이네요… 전부 주입 받을 수 도 있다니.

1개의 좋아요

주입받을 수 있는게 아니라 어쩔 수 없이 받을 수 밖에 없었다고 표현하는 게 맞을 것 같습니다.

서비스를 식별하는 기능이 없었으니까요.

이런 경우, 보통은 다형성이 아니라 구체 클래스를 그대로 사용하는 코드를 많이 적었습니다.

builder.Services.AddSingleton<BigCache>();
builder.Services.AddSingleton<SmallCache>();
app.MapGet("/big", (BigCache cache) => // ...
app.MapGet("/small", (SamllCache cache) => // ...

서비스 컨테이너가 다형성을 제한하는 웃긴 상황이었죠.