Background and motivation
Follow up to #128161
Reflection-based components need to be able to identify the set of derived types of a closed type. See #125449.
We discussed how to best make this information available in cc-list-subtypes.md. When we met about this, we leaned against adding a reflection API like Type.GetSameAssemblySubtypes(), in part due to the relatively narrow use case and C#-specific purpose of that API. Instead we settled on listing the derived types in an attribute on the closed type.
API Proposal
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class IsClosedTypeAttribute : Attribute
{
public IsClosedTypeAttribute() { }
+ public Type[] DerivedTypes { get; set; }
}
}
API Usage
[IsClosedType] cannot be used explicitly in C#. It is used by the compiler in the following way:
public closed class Animal;
public class Dog : Animal;
public class Cat : Animal;
public class CalicoCat : Cat;
// generates the following metadata:
[IsClosedType(DerivedTypes = [typeof(Dog), typeof(Cat)])]
public class Animal;
public class Dog : Animal;
public class Cat : Animal;
public class CalicoCat : Cat;
// In generic cases, the attribute lists the definitions of the derived types
public closed class C<T>;
public class D1<T> : C<T>;
public class D2<T, U> : C<Dictionary<T, U>>;
public class D3 : C<string>;
// generates the following metadata:
[IsClosedType(DerivedTypes = [typeof(D1<>), typeof(D2<,>), typeof(D3)])]
public class C<T>;
public class D1<T> : C<T>;
public class D2<T, U> : C<Dictionary<T, U>>;
public class D3 : C<string>;
If this API proposal is accepted, then the compiler will always set a value for this property, and the compiler will guarantee that the value will be consistent with the TypeDef table for the containing module. i.e. it will contain the same set of subtype definitions that we would have discovered by scanning the TypeDef table. Note that this doesn't protect us against post-build tools adding new derived types and failing to update the attribute. See also Risks.
Alternative Designs
We might want to make it so tools which use this attribute, are required to specify a value for DerivedTypes. We could consider one of the following to try to emphasize the need to specify a value:
- Put
required modifier on the attribute property. I didn't see any pre-existing example of a public required property in the core libraries, and, this isn't wouldn't be especially "binding" for post-compilation tools, so I didn't suggest this in the main proposal.
- Change the constructor signature to take a (possibly
params) Type[] derivedTypes, and do not include the DerivedTypes.set accessor.
Risks
Quoting cc-list-subtypes.md:
We resisted doing this until now, because we felt the information was redundant. The TypeDef table is already a source of truth for this, and theoretically a type using this encoding could have differences between the table and the attribute.
The risk of having multiple sources of truth for this is: different tools which operate in a similar domain, may end up using different sources for the information. For example, maybe STJ uses the attribute to find the subtypes, and Newtonsoft uses the TypeDef table. This creates an interoperability issue which may or may not amount to a security bug.
Although we assume that the compiler is going to provide a correct list of subtypes in the attribute, post-build rewriting tools might not. The possibility of divergence might further motivate runtime enforcement. For example, throwing InvalidProgramException at runtime, when a subtype is loaded, and its base type has [IsClosedType] attribute, and the attribute doesn't include the subtype.
Moving forward with this API proposal, may motivate us to add a runtime enforcement when a type is loaded, when the base type is a closed type, that:
- the derived type is listed in the
IsClosedTypeAttribute.DerivedTypes on the base type.
- the derived type is declared in the same module as the base type.
Regarding runtime enforcement, there was also a corner case pointed out with type forwarding. It doesn't seem valid/sane to forward a derived type of a closed type to another assembly, because it seems to require declaring the derived type in a different assembly. It feels reasonable to block that.
Background and motivation
Follow up to #128161
Reflection-based components need to be able to identify the set of derived types of a closed type. See #125449.
We discussed how to best make this information available in cc-list-subtypes.md. When we met about this, we leaned against adding a reflection API like
Type.GetSameAssemblySubtypes(), in part due to the relatively narrow use case and C#-specific purpose of that API. Instead we settled on listing the derived types in an attribute on the closed type.API Proposal
namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class IsClosedTypeAttribute : Attribute { public IsClosedTypeAttribute() { } + public Type[] DerivedTypes { get; set; } } }API Usage
[IsClosedType]cannot be used explicitly in C#. It is used by the compiler in the following way:If this API proposal is accepted, then the compiler will always set a value for this property, and the compiler will guarantee that the value will be consistent with the TypeDef table for the containing module. i.e. it will contain the same set of subtype definitions that we would have discovered by scanning the TypeDef table. Note that this doesn't protect us against post-build tools adding new derived types and failing to update the attribute. See also Risks.
Alternative Designs
We might want to make it so tools which use this attribute, are required to specify a value for
DerivedTypes. We could consider one of the following to try to emphasize the need to specify a value:requiredmodifier on the attribute property. I didn't see any pre-existing example of a public required property in the core libraries, and, this isn't wouldn't be especially "binding" for post-compilation tools, so I didn't suggest this in the main proposal.params)Type[] derivedTypes, and do not include theDerivedTypes.setaccessor.Risks
Quoting
cc-list-subtypes.md:Moving forward with this API proposal, may motivate us to add a runtime enforcement when a type is loaded, when the base type is a closed type, that:
IsClosedTypeAttribute.DerivedTypeson the base type.Regarding runtime enforcement, there was also a corner case pointed out with type forwarding. It doesn't seem valid/sane to forward a derived type of a closed type to another assembly, because it seems to require declaring the derived type in a different assembly. It feels reasonable to block that.