Skip to content

Unable to use a enum as a key for keyed services in controller constructor #96140

@inayelle

Description

@inayelle

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Let's say we have the following services:

public enum WeatherProvider
{
    AccuWeather = 1,
    OpenWeatherMap = 2,
}

public interface IWeatherProvider
{
    string GetWeatherForCity(string city);
}

public sealed class AccuWeatherWeatherProvider : IWeatherProvider
{
    public string GetWeatherForCity(string city) => "AccuWeather";
}

public sealed class OpenWeatherMapWeatherProvider : IWeatherProvider
{
    public string GetWeatherForCity(string city) => "OpenWeather";
}

And their registration in the dependency container using enum keys:

var builder = WebApplication.CreateBuilder();

builder
   .Services
   .AddKeyedScoped<IWeatherProvider, OpenWeatherMapWeatherProvider>(WeatherProvider.OpenWeatherMap)
   .AddKeyedScoped<IWeatherProvider, AccuWeatherWeatherProvider>(WeatherProvider.AccuWeather);

Having this, we can resolve a weather provider by interface and key using method injection in a controller (which works fine):

    [HttpGet]
    public object GetWeather(
        [FromQuery] string city,
        [FromKeyedServices(WeatherProvider.OpenWeatherMap)] IWeatherProvider weatherProvider
    )
    {
        return new
        {
            City = city,
            Weather = weatherProvider.GetWeatherForCity(city),
        };
    }

However, if we wanted to inject a default weather provider via constructor, like:

[ApiController]
[Route("weather")]
public sealed class WeatherController : ControllerBase
{
    private readonly IWeatherProvider _defaultWeatherProvider;

    public WeatherController([FromKeyedServices(WeatherProvider.AccuWeather)] IWeatherProvider defaultWeatherProvider)
    {
        _defaultWeatherProvider = defaultWeatherProvider;
    }

    // actions omitted for brevity
}

We won't be able to query any of the actions of this controller due to a controller activation failure with an exception:

System.ArgumentException: Expression of type 'KeyedServicesExample.Api.Providers.WeatherProvider' cannot be used for parameter of type 'System.Object' of method 'System.Object GetService(System.IServiceProvider, System.Type, System.Type, Boolean, System.Object)' (Parameter 'arg4')
         at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
         at System.Linq.Expressions.Expression.Call(MethodInfo method, Expression arg0, Expression arg1, Expression arg2, Expression arg3, Expression arg4)
         at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable`1 arguments)
         at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.BuildFactoryExpression(ConstructorInfo constructor, Nullable`1[] parameterMap, Expression serviceProvider, Expression factoryArgumentArray)
         at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateFactoryInternal(Type instanceType, Type[] argumentTypes, ParameterExpression& provider, ParameterExpression& argumentArray, Expression& factoryExpressionBody)
         at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateFactory(Type instanceType, Type[] argumentTypes)
         at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.CreateActivator(ControllerActionDescriptor descriptor)
         at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.CreateControllerFactory(ControllerActionDescriptor descriptor)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvokerCache.GetCachedResult(ControllerContext controllerContext)
         at Microsoft.AspNetCore.Mvc.Routing.ControllerRequestDelegateFactory.<>c__DisplayClass12_0.<CreateRequestDelegate>b__0(HttpContext context)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
         at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
         at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
         at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

I believe it doesn't work because the key in this example is an enum, which is a value type. If we replace the enum key with a string key, it all works like a charm.

Expected Behavior

It should be possible to resolve keyed services using FromKeyedServices attribute via controller constructor.

Steps To Reproduce

Link to an example project: https://github.com/inayelle/keyed-services-api-example

Exceptions (if any)

System.ArgumentException: Expression of type 'KeyedServicesExample.Api.Providers.WeatherProvider' cannot be used for parameter of type 'System.Object' of method 'System.Object GetService(System.IServiceProvider, System.Type, System.Type, Boolean, System.Object)' (Parameter 'arg4')
         at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
         at System.Linq.Expressions.Expression.Call(MethodInfo method, Expression arg0, Expression arg1, Expression arg2, Expression arg3, Expression arg4)
         at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable`1 arguments)
         at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.BuildFactoryExpression(ConstructorInfo constructor, Nullable`1[] parameterMap, Expression serviceProvider, Expression factoryArgumentArray)
         at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateFactoryInternal(Type instanceType, Type[] argumentTypes, ParameterExpression& provider, ParameterExpression& argumentArray, Expression& factoryExpressionBody)
         at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateFactory(Type instanceType, Type[] argumentTypes)
         at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.CreateActivator(ControllerActionDescriptor descriptor)
         at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.CreateControllerFactory(ControllerActionDescriptor descriptor)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvokerCache.GetCachedResult(ControllerContext controllerContext)
         at Microsoft.AspNetCore.Mvc.Routing.ControllerRequestDelegateFactory.<>c__DisplayClass12_0.<CreateRequestDelegate>b__0(HttpContext context)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
         at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
         at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
         at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

.NET Version

8.0.100

Anything else?

.NET SDK:
 Version:           8.0.100
 Commit:            57efcf1350
 Workload version:  8.0.100-manifests.6c33ef20

Runtime Environment:
 OS Name:     arch
 OS Version:
 OS Platform: Linux
 RID:         linux-x64
 Base Path:   /home/inayelle/local/lib/dotnet/sdk/8.0.100/

.NET workloads installed:
 Workload version: 8.0.100-manifests.6c33ef20
There are no installed workloads to display.

Host:
  Version:      8.0.0
  Architecture: x64
  Commit:       5535e31a71

.NET SDKs installed:
  7.0.404 [/home/inayelle/local/lib/dotnet/sdk]
  8.0.100 [/home/inayelle/local/lib/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 7.0.14 [/home/inayelle/local/lib/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.0 [/home/inayelle/local/lib/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 7.0.14 [/home/inayelle/local/lib/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.0 [/home/inayelle/local/lib/dotnet/shared/Microsoft.NETCore.App]

Other architectures found:
  None

Environment variables:
  DOTNET_ROOT       [/home/inayelle/local/lib/dotnet]

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions