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
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:
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
API Usage
FeatureCheckmay be placed on a static boolean property to indicate that it is a check for the referenced feature (represented by a type):FeatureGuardon a feature type may be used to express dependencies between features.This allows the property, or
RequiresFeatureAttributereferencing the feature type, to guard calls to APIs annotated as requiring that feature:FeatureGuardmay 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 onIsDynamicCodeCompiledcould be simplified to:For trimming support,
FeatureSwitchDefinitionmay be applied to the attribute type to give the feature a name:When the app is trimmed with the feature switch
"System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported"set tofalse, the properties are rewritten to returnfalse, and the guarded branches are removed.The initial implementation will not support
RequiresFeatureAttribute. Instead, analyzer warnings will initially be limited toRequiresUnreferencedCodeAttribute,RequiresDynamicCodeAttribute, andRequiresAssemblyFilesAttribute. It will still be possible to define an (otherwise unused) type for use inFeatureCheckandFeatureGuard, 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:
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
IsSupportedcheck serves the purpose that#ifserves, but at trim time.(compare to preprocessor symbols):
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
ReplacedChanged back laterFeatureGuardAttributewithFeatureDependsOnAttributeFeatureNametoSwitchNameinFeatureSwitchDefinitionRequiresFeatureAttributein the proposalFeatureDepndsOnAttributeback toFeatureGuardAttribute, allowed on properties or classes