Skip to content

API Proposal: FromServiceKeyAttribute(object) to support nullable (for unkeyed) and to use current service key #113585

Closed
@stephentoub

Description

@stephentoub

Background and motivation

Note that issue #99084 is also addressed by the API changes below, which allows a constructor parameter to be resolved using the key that resolved the current service. Please see that other issue for details. This is achieved here by using the empty constructor on the attribute. The rest of this description is about allowing null.

Keyed services was added in v8 along with FromKeyedServices(object key) to be applied to a constructor argument so that a literal key, such as a string, can be specified as the key for that parameter. That key was marked non-nullable and now there is case to have that nullable.

At the time keyed services was implemented, [FromKeyedServices] was not considered useful for the pre-existing "unkeyed" services model (which uses the parameter's Type as the key) since every parameter by default is considered injectable and also do not need any extra "key" information since the parameter Type is used as the key. Note that although every parameter by default is required to be injected when using DI, there are ways to get around that by using a factory approach specified during registration or by using ActivatorUtilities.CreateInstance().

However, per the discussion in this issue, [FromKeyedServices] is also used in some systems (outside of the standard DI implementation) as a way to flag parameters that are to be injected but are unkeyed. This can be accomplished today by specifying null for the key and that approach is consistent with other (non-attribute) keyed service APIs called at runtime that take a null key to indicate that they are unkeyed.

By making [FromKeyedServices(object key)] non-nullable we can avoid compiler warnings of using [FromKeyedServices(null)] and avoid the workaround of [FromKeyedServices(null!)].

This is a compile-time breaking change for nullable-enabled consumers of FromServiceKeyAttribute.Key.

API Proposal

namespace Microsoft.Extensions.DependencyInjection;

[AttributeUsage(AttributeTargets.Parameter)]
public sealed class FromKeyedServicesAttribute : Attribute
{
-    public FromKeyedServicesAttribute(object key);
    
    // A null key means no key.
+    public FromKeyedServicesAttribute(object? key);

    // Use "inherit key" mode. Key will be null.
+    public FromKeyedServicesAttribute();

-   public object Key { get; }
+   public object? Key { get; }

    // Support a way to determine which of the 3 modes we are in.
+   public ServiceKeyLookupMode LookupMode { get; }
}

// A light-weight alternative is to add a bool "InheritKey" instead of an enum. The Key property would be null when true.
+ public enum ServiceKeyLookupMode
+ {
+    NoKey,
+    ExplicitKey,
+    InheritKey
+ }

API Usage

public class MyType1
{
    // Declare we need to inject `IMyService` with the appropriate registered type.
    // For our DI implementation, this attribute is a no-op, since a null key means no key.
    public MyType1([FromKeyedServices(null)] IMyService someService)
    {
    }
}

public class MyType2
{
    // The 'IMyService' is resolved by using the same key that was used to find this class ('MyType2').
    public MyType2([FromKeyedServices] IMyService someService)
    {
    }
}

Alternative Designs

Add a new attribute, such as the [FromServices] that already exists in AspNetCore.Mvc so we would need a different name.

Original PR Description

FromKeyedServicesAttribute is annotated to prohibit null keys:

[AttributeUsage(AttributeTargets.Parameter)]
public class FromKeyedServicesAttribute : Attribute
{
/// <summary>
/// Creates a new <see cref="FromKeyedServicesAttribute"/> instance.
/// </summary>
/// <param name="key">The key of the keyed service to bind to.</param>
public FromKeyedServicesAttribute(object key) => Key = key;
/// <summary>
/// The key of the keyed service to bind to.
/// </summary>
public object Key { get; }
}

but IKeyedServiceProvider's GetService and GetRequiredService methods allow null keys:

object? GetKeyedService(Type serviceType, object? serviceKey);
/// <summary>
/// Gets service of type <paramref name="serviceType"/> from the <see cref="IServiceProvider"/> implementing
/// this interface.
/// </summary>
/// <param name="serviceType">An object that specifies the type of service object to get.</param>
/// <param name="serviceKey">The <see cref="ServiceDescriptor.ServiceKey"/> of the service.</param>
/// <returns>A service object of type <paramref name="serviceType"/>.
/// Throws an exception if the <see cref="IServiceProvider"/> cannot create the object.</returns>
object GetRequiredKeyedService(Type serviceType, object? serviceKey);

Is this inconsistency by design? Should the ctor parameter and Keys property on FromKeyedServicesAttribute be modified to be nullable?

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions