Skip to content

[API Proposal]: Attribute model for feature APIs #96859

@sbomer

Description

@sbomer

Background and motivation

.NET has feature switches which can be set to turn on/off areas of functionality in our libraries, with optional support for removing unused features when trimming or native AOT compiling.

Feature switches suffer from a poor user experience:

  • trimming support requires embedding an unintuitive XML file into the library, and
  • there is no analyzer support

This document proposes an attribute-based model for feature switches that will significantly improve the user experience, by removing the need for this XML and enabling analyzer support.

More detail and discussion in dotnet/designs#305.
The attribute model is heavily inspired by the capability-based analyzer draft.

API Proposal

namespace System.Diagnostics.CodeAnalysis;

[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class FeatureCheckAttribute : Attribute
{
    public Type FeatureType { get; }
    public FeatureCheckAttribute(Type featureType)
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public sealed class FeatureGuardAttribute : Attribute
{
    public Type FeatureType { get; }
    public FeatureGuardAttribute(Type featureType)
}

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class FeatureSwitchDefinitionAttribute : Attribute
{
    public string SwitchName { get; }
    public FeatureSwitchDefinitionAttribute(string switchName)
}

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited=false, AllowMultiple=true)]
public sealed class RequiresFeatureAttribute : Attribute
{
    public Type FeatureType { get; }
    public RequiresFeatureAttribute(Type featureType)
}

API Usage

FeatureCheck may be placed on a static boolean property to indicate that it is a check for the referenced feature (represented by a type):

namespace System.Runtime.CompilerServices;

public static class RuntimeFeature
{
    [FeatureCheck(typeof(RequiresDynamicCodeAttribute))]
    public static bool IsDynamicCodeSupported { get; } = AppContext.TryGetSwitch("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", out bool isDynamicCodeSupported) ? isDynamicCodeSupported : true;

    [FeatureCheck(typeof(DynamicCodeCompilation))]
    public static bool IsDynamicCodeCompiled => IsDynamicCodeSupported;
}

FeatureGuard on a feature type may be used to express dependencies between features.

namespace System.Diagnostics.CodeAnalysis;

[FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
internal static class DynamicCodeCompilation { }

This allows the property, or RequiresFeatureAttribute referencing the feature type, to guard calls to APIs annotated as requiring that feature:

if (RuntimeFeature.IsDynamicCodeSupported) {
    APIWhichRequiresDynamicCode(); // No warning, thanks to FeatureCheck
}

if (RuntimeFeature.IsDynamicCodeCompiled) {
    APIWhichRequiresDynamicCodeCompilation(); // No warning, thanks to FeatureCheck
    APIWhichRequiresDynamicCode(); // No warning, thanks to FeatureCheck/FeatureGuard
}

[RequiresDynamicCode("Does something with dynamic codegen")]
static void APIWhichRequiresDynamicCode() {
    // ...
}

[RequiresFeature(typeof(DynamicCodeCompilation))]
static void APIWhichRequiresDynamicCodeCompilation() {
    // ...
    APIWhichRequiresDynamicCode(); // No warning, thanks to RequiresFeature/FeatureGuard
}

FeatureGuard may also be placed directly on a static boolean property as a shorthand, to define a simple guard without a separate feature type. So the annotations on IsDynamicCodeCompiled could be simplified to:

namespace  System.Diagnostics.CodeAnalysis;

public static class RuntimeFeature
{
    // ...

    [FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
    public static bool IsDynamicCodeCompiled => IsDynamicCodeSupported;
}
if (RuntimeFeature.IsDynamicCodeCompiled)
    APIWhichRequiresDynamicCode(); // No warning, thanks to FeatureGuard

For trimming support, FeatureSwitchDefinition may be applied to the attribute type to give the feature a name:

namespace System.Diagnostics.CodeAnalysis;

[FeatureSwitchDefinition("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported")]
public sealed class RequiresDynamicCodeAttribute
{
    // ...
}

When the app is trimmed with the feature switch "System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported" set to false, the properties are rewritten to return false, and the guarded branches are removed.

The initial implementation will not support RequiresFeatureAttribute. Instead, analyzer warnings will initially be limited to RequiresUnreferencedCodeAttribute, RequiresDynamicCodeAttribute, and RequiresAssemblyFilesAttribute. It will still be possible to define an (otherwise unused) type for use in FeatureCheck and FeatureGuard, to influence branch elimination.

Applications

Aside from the analysis for RequiresUnreferencedCode, RequiresDynamicCode, RequiresAssemblyFiles, these semantics work well for protecting usage of hardware intrinsics or crypto hash algorithms: #96859 (comment).

Alternative Designs

Separate type to represent feature

There could be a level of indirection, so that the feature is represented not by the attribute type, but by a separate type that is linked to the attribute type:

[FeatureAttribute(typeof(RequiresFeatureAttribute))]
[FeatureSwitchDefinition("MyLibrary.Feature.IsSupported")]
class Feature {
    [FeatureCheck(typeof(Feature))]
    public static bool IsSupported => ...;

    [RequiresFeature(typeof(Feature))]
    public static void DoSomething() { ... }
}

class RequiresFeatureAttribute : Attribute { ... }

The current proposal allows attribute or non-attribute types.

String-based API

The attributes could instead use strings, making the usage slightly more analogous to preprocessor symbols. The difference is that callsites or code blocks within a method can't be annotated directly, so the IsSupported check serves the purpose that #if serves, but at trim time.

class Feature {
    [FeatureCheck("MY_LIBRARY_FEATURE")]
    public static bool IsSupported => ...;

    [RequiresFeature("MY_LIBRARY_FEATURE")]
    public static void DoSomething() { ... }
}

class Consumer {
    static void Main() {
        if (Feature.IsSupported)
            Feature.DoSomething();
    }
}

(compare to preprocessor symbols):

class Library {
#if MY_LIBRARY_FEATURE
    public static void DoSomething() { ... }
#endif
}

class Consumer {
    static void Main() {
#if MY_LIBRARY_FEATURE
        Feature.DoSomething();
#endif
    }
}

Risks

The proposed API doesn't cover every possible pattern that might be useful for feature switches. We are aiming to start with a small, well-defined set of behavior, but need to ensure this doesn't lock us out of future extensions. We can extend these by adding extra constructor parameters to the attributes in the future, as discussed in dotnet/designs#305.

Updates

  • Replaced FeatureGuardAttribute with FeatureDependsOnAttribute Changed back later
  • Lifted restriction that feature type must be an attribute type
  • Updated FeatureName to SwitchName in FeatureSwitchDefinition
  • Included RequiresFeatureAttribute in the proposal
  • Changed FeatureDepndsOnAttribute back to FeatureGuardAttribute, allowed on properties or classes

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions