Skip to content

Expose top-level nullability information from reflection #29723

Closed

Description

@terrajobst

With C# 8, developers will be able to express whether a given reference type can be null:

public void M(string? nullable, string notNull, IEnumerable<string?> nonNullCollectionOfPotentiallyNullEntries);

(Please note that existing code that wasn't compiled using C# 8 and nullable turned on is considered to be unknown.)

This information isn't only useful for the compiler but also attractive for reflection-based tools to provide a better experience. For example:

  • MVC

    • Provides a way to automatically deserialize inputs to controller methods ("model binding")
    • Would like to provide model validation so that the existing pattern would allow code to bail early
    • Without it, customers would have to apply a custom attribute, such as [Required], or resort to additional null-checks
    • Only needs top-level annotations, i.e. string? but not nested, such as IEnumerable<string?>
  • EF

    • Provides a way to generate database schemas from user classes ("code first")
    • Would like use nullable information to infer whether columns should be null or non-null (they already do that for nullable value types).
    • Without it, customers would have to apply a custom attribute to repeat that information.
    • Also only needs top-level annotations

The nullable information is persisted in metadata using custom attributes. In principle, any interested party can already read the custom attributes without additional work from the BCL. However, this is not ideal because the encoding is somewhat non-trivial:

  • Custom attribute might be generated. The custom attribute might have been generated (meaning is embedded in the user's assembly) or might use the to-be-provided attribute.
  • Encoded as a byte array. The tri-state is encoded as a linearized version of the constructed generic type.
  • Compressed. Right now, each member will have the attribute when nullability is turned on but this causes metadata bloat. We're working on a proposal that allows the containing member, type, or assembly to state a default to reduce the number of attribute applications.

It's tempting to think of nullable information as additional information on System.Type. However, we can't just expose an additional property on Type because at runtime there is no difference between string (unknown), string? (nullable), and string (non-null). So we'd have to expose some sort of API that allows consumers to walk the type structure and getting information.

Unifying nullable-value types and nullable-reference types

It was suggested that these APIs also return NullableState.MaybeNull for nullable value types, which seems desirable indeed. Boxing a nullable value type causes the non-nullable representation to be boxed. Which also means you can always cast a boxed non-nullable value type to its nullable representation. Since the reflection API surface is exclusively around object it seems logical to unify these two models. For customers that want to differentiate the two, they can trivially check the top-level type to see whether it's a reference type or not.

API proposal

namespace System.Reflection
{
    public sealed class NullabilityInfoContext
    {
        public NullabilityInfo Create(ParameterInfo parameterInfo);
        public NullabilityInfo Create(PropertyInfo propertyInfo);
        public NullabilityInfo Create(EventInfo eventInfo);
        public NullabilityInfo Create(FieldInfo parameterInfo);
    }

    public sealed class NullabilityInfo
    {
        public Type Type { get; }
        public NullableState ReadState { get; }
        public NullableState WriteState { get; }
        public NullabilityInfo? ElementType { get; }
        public ReadOnlyCollection<NullabilityInfo>? GenericTypeArguments { get; }
    }

    public enum NullableState
    {
        Unknown,
        NotNull,
        MaybeNull
    }
}

Sample usage

Getting top-level nullability information

private NullabilityInfoContext _nullabilityContext = new NullabilityInfoContext();

private void DeserializePropertyValue(PropertyInfo p, object instance, object? value)
{
    if (value == null)
    {
        var nullabilityInfo = _nullabilityContext.Create(p);
        var allowsNull = nullabilityInfo.WriteState != NullableState.NotNull;
        if (!allowsNull)
            throw new MySerializerException($"Property '{p.GetType().Name}.{p.Name}'' cannot be set to null.");
    }

    p.SetValue(instance, value);
}

Getting nested nullability information

class Data
{
    public string?[] ArrayField;
    public (string?, object) TupleField;
}
private void Print()
{
    Type type = typeof(Data);
    FieldInfo arrayField = type.GetField("ArrayField");
    FieldInfo tupleField = type.GetField("TupleField");

    NullabilityInfoContext context = new ();

    NullabilityInfo arrayInfo = context.Create(arrayField);
    Console.WriteLine(arrayInfo.ReadState);         // NotNull
    Console.WriteLine(arrayInfo.Element.ReadState); // MayBeNull

    NullabilityInfo tupleInfo = context.Create(tupleField);
    Console.WriteLine(tupleInfo.ReadState);                        // NotNull
    Console.WriteLine(tupleInfo.GenericTypeArgument[0].ReadState); // MayBeNull
    Console.WriteLine(tupleInfo.GenericTypeArgument[1].ReadState); // NotNull
}

Custom Attributes

The following custom attributes in System.Diagnostics.CodeAnalysis are processed and combined with type information:

  • [AllowNull]
  • [DisallowNull]
  • [MaybeNull]
  • [NotNull]

The following attributes aren't processed because they don't annotate static state but information related to dataflow:

  • [DoesNotReturn]
  • [DoesNotReturnIf]
  • [MaybeNullWhen]
  • [MemberNotNull]
  • [MemberNotNullWhen]
  • [NotNullIfNotNull]
  • [NotNullWhen]

@dotnet/nullablefc @dotnet/ldm @dotnet/fxdc @rynowak @divega @ajcvickers @roji @steveharter

Activity

  1. rynowak commented on May 31, 2019

    @rynowak
    Contributor

    What's the behaviour with a value type? Does this include reflecting the effect of Nullable<> as well?

  2. mattwar commented on May 31, 2019

    @mattwar

    Are these really static methods? Shouldn't they have a parameter then?

  3. safern commented on May 31, 2019

    @safern
    Member

    For, PropertyInfo, we can have a NullableState on the setter and on the getter depending if we have an attribute. I.e

    [NotNull] string? Property { get; set; }

    That will have a nullable input but a non-null output.

    Also what about generic constraints or interface implementations? Those can also have nullability information and to when getting the generic constraints or interface implementations we get an array of Type.

  4. terrajobst commented on May 31, 2019

    @terrajobst
    ContributorAuthor

    @rynowak

    What's the behaviour with a value type? Does this include reflecting the effect of Nullable<> as well?

    They would return Unknown right now. We could add an enum member NotApplicable that we return for non-reference types or throw InvalidOperationException. We could also handle those, but this feels wrong because nullable value types are actually different types.

    @mattwar

    Are these really static methods? Shouldn't they have a parameter then?

    Sigh. No, I wrote up a sample project to test this and I used extension methods and then I didn't fix it when copy and pasting. Fixed.

    @safern

    We need to think about the higher-level attributes. I only thought about NullableAttribute so far.

    For, PropertyInfo, we can have a NullableState on the setter and on the getter depending if we have an attribute.

    This wouldn't change the API shape. People would have to go to PropertyInfo.GetGetMethod() and ask the corresponding MethodInfo directly.

    Also what about generic constraints or interface implementations? Those can also have nullability information and to when getting the generic constraints or interface implementations we get an array of Type.

    See intro. I've deliberately scoped them out.

  5. chucker commented on May 31, 2019

    @chucker

    We could also handle those, but this feels wrong because nullable value types are actually different types.

    They are, but on a source level, C# abstracts the difference away. Maybe introduce something like ValueNull and ValueNotNull as additional enum values. That way, the distinction is still prominent, but it’s also not a runtime surprise (a.k.a. exception) that these won’t be handled.

  6. MichalStrehovsky commented on May 31, 2019

    @MichalStrehovsky
    Member

    Why not just make these extension methods and stash them away somewhere in the System.Diagnostics.CodeAnalysis namespace? Or better yet, in a NuGet package shipped out of the Roslyn repo - since their meaning is defined by the C# compiler?

    These annotations don't have a meaning to the runtime and reflection itself doesn't respect them in MethodBase.Invoke or FieldInfo.SetValue - it feels odd to burn knowledge of custom attributes that are only meaningful to the C# compiler into the runtime reflection stack. These are not that different from e.g. the custom attributes for System.ComponentModel - they're just guidelines.

    E.g. we added a IsByRefLike property to System.Type, for ref struct because byref-like types have a meaning to the runtime. We didn't add any API for unmanaged struct, because that constraint only means something to the C# compiler.

  7. roji commented on May 31, 2019

    @roji
    Member

    For, PropertyInfo, we can have a NullableState on the setter and on the getter depending if we have an attribute.

    See dotnet/csharplang#2384 for this at the language level. @terrajobst users could call PropertyInfo.GetGetMethod() and check there, but what would be the result for a call to GetNullableState() directly on a PropertyInfo which has different nullability settings for getter and setter?

    it feels odd to burn knowledge of custom attributes that are only meaningful to the C# compiler into the runtime reflection stack.

    I think the idea here is that these attributes are meaningful to various reflection-based consumers at runtime (EF, ASP.NET, 3rd-party products). If it were only for the compiler an API would probably not be needed at all.

    These are not that different from e.g. the custom attributes for System.ComponentModel - they're just guidelines.

    They are different in that they are emitted by the compiler and interpreted by it. They are also potentially much more complex to interpret (a context-based scheme may be introduced to combat the bloat they introduce), hence the desire to encapsulate that interpretation logic.

  8. tannergooding commented on May 31, 2019

    @tannergooding
    Member

    These annotations don't have a meaning to the runtime and reflection itself doesn't respect them in MethodBase.Invoke or FieldInfo.SetValue - it feels odd to burn knowledge of custom attributes that are only meaningful to the C# compiler into the runtime reflection stack. These are not that different from e.g. the custom attributes for System.ComponentModel - they're just guidelines.

    These attributes are not only going to be respected by the C# compiler, they are also going to be understood and consumed by other nullable aware languages (such as F#).

    I would also say that, regardless of whether or not the runtime treats these attributes specially, the overarching ecosystem ultimately will, which I believe is the important point here. These attributes (and others like the unmanaged constraint) convey additional metadata that is generally (but not always) enforced by some user code in the method and it is useful to have an easy/logical way of querying it so that you can perform the appropriate checks when/if needed.

    We are already burning this information into all of our assemblies, because of its perceived benefit and exposing that metadata in a well-defined way in the reflection type system will only help fulfill that story and make it easier for users who want to utilize them.

  9. MichalStrehovsky commented on May 31, 2019

    @MichalStrehovsky
    Member

    I would also say that, regardless of whether or not the runtime treats these attributes specially, the overarching ecosystem ultimately will, which I believe is the important point here

    To be clear, my question is not whether we need an API that provides this functionality - but whether such API should be on ParameterInfo and friends. Since this is adding non-virtual methods (not properties), my expectation is that this API is not going to do any caching - so it doesn't really need to be an instance method.

    We could easily stash this API away in e.g. System.Reflection.TypeExtensions and even make it available on lower versions of NetStandard (doesn't have to be exclusive to .NET Core 3.0).

  10. tannergooding commented on May 31, 2019

    @tannergooding
    Member

    We could easily stash this API away in e.g. System.Reflection.TypeExtensions and even make it available on lower versions of NetStandard (doesn't have to be exclusive to .NET Core 3.0).

    Nullable reference types is a C# 8 feature which is only officially supported on .NET Standard 2.1 and higher.

    I think discussion on where in the reflection stack it goes is reasonable, although I think most people would expect and look for it to be an instance (or extension) method

  11. MichalStrehovsky commented on May 31, 2019

    @MichalStrehovsky
    Member

    Nullable reference types is a C# 8 feature which is only officially supported on .NET Standard 2.1 and higher.

    Not that I want to make this my main argument (my main argument is to not pollute the instance member surface area with convenience APIs), but runtime reflection is not the only flavor of reflection. A .NET Standard 2.1 assembly can be loaded for inspection in all sorts of ways on downlevel runtimes (reflection only load is one way, and things like our own System.Reflection.MetadataLoadContext is another). Some people might find this API in the form of an extension method that runs downlevel still valuable.

  12. terrajobst commented on May 31, 2019

    @terrajobst
    ContributorAuthor

    @chucker

    They are, but on a source level, C# abstracts the difference away. Maybe introduce something like ValueNull and ValueNotNull as additional enum values. That way, the distinction is still prominent, but it’s also not a runtime surprise (a.k.a. exception) that these won’t be handled.

    Syntactically, you could make the argment that they look similar. But C# doesn't abstract them away. int? and int are different types and this has real observable language behavior. For example, this is valid:

    public void M(int x) {}
    public void M(int? x) {}

    While this is not:

    public void M(string x) {}
    public void M(string? x) {}

    In other words, you can't overload based on nullability of reference types (because they are fundamentally the same type at runtime). Also, the types have different members. So no, I don't think we should make the argument that "C# abstracts the differences away".

    That being said, that doesn't mean we cannot expose the nullability information for nullable types. Boxing a nullable value type causes the non-nullable representation to be boxed. Which also means you can always cast a boxed non-nullable value type to its nullable representation. Given that the reflection API surface is exclusively around object, this might actually work just fine.

    So I think I'm coming around agreeing with you 😄

  13. terrajobst commented on May 31, 2019

    @terrajobst
    ContributorAuthor

    @MichalStrehovsky

    Why not just make these extension methods and stash them away somewhere in the System.Diagnostics.CodeAnalysis namespace? Or better yet, in a NuGet package shipped out of the Roslyn repo - since their meaning is defined by the C# compiler?

    But they aren't specific to C#. In order for the C in CLR to be meaningful, other languages, so they support nullability, should use the same underlying encoding. Otherwise exchanging types no longer works. Hence, the reflection API can be considered general purpose.

    These annotations don't have a meaning to the runtime and reflection itself doesn't respect them in MethodBase.Invoke or FieldInfo.SetValue - it feels odd to burn knowledge of custom attributes that are only meaningful to the C# compiler into the runtime reflection stack. These are not that different from e.g. the custom attributes for System.ComponentModel - they're just guidelines.

    Reflection is about inspecting the program at runtime. While I understand that there good reasons to separate concerns and C# semantics and runtime semantics are different, it totally makes sense to expose static information as people have an expectation that they can access that at runtime. And they already can, but it's error prone. As my examples above show, I'd argue these aren't contrived scenarios but fairly compelling and very much in line with expectation for the .NET development experience.

    I don't like shipping core functionality like this on NuGet as this is yet another thing that can go out of sync between the various pieces. Also, given our strategy to stronger align C# with the release cycle of the .NET Core platform, this is also against what we want to achieve as a product.

    To be clear, my question is not whether we need an API that provides this functionality - but whether such API should be on ParameterInfo and friends. Since this is adding non-virtual methods (not properties), my expectation is that this API is not going to do any caching - so it doesn't really need to be an instance method.

    That is a good point -- I should probably mark these APIs as virtual so that other implementations (such as the new metadata based reflection layer) can provide specialized implementations.

    We could easily stash this API away in e.g. System.Reflection.TypeExtensions and even make it available on lower versions of NetStandard (doesn't have to be exclusive to .NET Core 3.0).

    I'd rather not. We don't want to optimize for out-of-band or downlevel. We want to foremost optimize for what's right for the .NET platform moving forward. I'd argue making them part of the reflection model to encapsulate them in a central way is the right thing to do.

    Should we also see a need for this functionality downlevel, we could also ship a bridge pack that defines them as extension methods. But let's not default to frankendesigns just because.

    Not that I want to make this my main argument (my main argument is to not pollute the instance member surface area with convenience APIs), but runtime reflection is not the only flavor of reflection

    I'd argue that the reverse is true today: the reflection APIs are polluted with a ton of plumbing APIs when the vast majority of the consumers want simple methods to get stuff done. And moving forward I think reasoning about declared nullability will be important to many consumers of reflection.

  14. terrajobst commented on May 31, 2019

    @terrajobst
    ContributorAuthor

    @roji

    For, PropertyInfo, we can have a NullableState on the setter and on the getter depending if we have an attribute.

    See dotnet/csharplang#2384 for this at the language level. @terrajobst users could call PropertyInfo.GetGetMethod() and check there, but what would be the result for a call to GetNullableState() directly on a PropertyInfo which has different nullability settings for getter and setter?

    Right now, this issue is still open so there is no spec. In general, at the metadata level the property itself has a signature (and thus a return type). Similar, the C# compiler emits the nullable attribute for the property itself, as well as for all accessors:

    // public string? Name { get; set; }
    
    [Nullable(1)]
    public string Name
    {
        [return: Nullable(1)]
        get
        {
            return this.<Name>k__BackingField;
        }
        [param: Nullable(1)]
        set
        {
            this.<Name>k__BackingField = value;
        }
    }

    So in other words I don't think exposing this on the property is a bad idea; depending on context it might just not be sufficient for the consumer.

  15. chucker commented on May 31, 2019

    @chucker

    you can't overload based on nullability of reference types (because they are fundamentally the same type at runtime)

    That’s an interesting edge case, yes.

    But I stand by my assertion — the syntax of string? wasn’t just picked because other languages do that, but also because, by and large, it expresses a sufficiently similar intent to int?. And if someone were to design .NET today, those subtle behavioral differences would have been avoided. They exist by and large for legacy reasons.

    (IANA C# language designer, though.)

    I'd argue that the reverse is true today: the reflection APIs are polluted with a ton of plumbing APIs when the vast majority of the consumers want simple methods to get stuff done.

    Yup. I think higher-level additions to Reflection are welcome.

  16. 118 remaining items

  17. bartonjs commented on Jun 10, 2021

    @bartonjs
    Member

    Video

    • [MaybeNullWhen] should count as a "maybe null" return answer.
    • We changed NullabilityInfo.GenericTypeArguments to a non-nullable array to match standard reflection API practices.
      • We discussed the caching strategy, and since NullabilityInfo objects are expected to be freshly returned each time there's no parallel-caller mutation concern.
    • We renamed the MaybeNull member to Nullable
    • Once we renamed the member the type name for the enum seemed wrong, so we renamed that to NullabilityState.
    namespace System.Reflection
    {
        public sealed class NullabilityInfoContext
        {
            public NullabilityInfo Create(ParameterInfo parameterInfo);
            public NullabilityInfo Create(PropertyInfo propertyInfo);
            public NullabilityInfo Create(EventInfo eventInfo);
            public NullabilityInfo Create(FieldInfo parameterInfo);
        }
    
        public sealed class NullabilityInfo
        {
            public Type Type { get; }
            public NullabilityState ReadState { get; }
            public NullabilityState WriteState { get; }
            public NullabilityInfo? ElementType { get; }
            public NullabilityInfo[] GenericTypeArguments { get; }
        }
    
        public enum NullabilityState
        {
            Unknown,
            NotNull,
            Nullable
        }
    }
  18. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Jun 10, 2021
  19. danmoseley commented on Jun 10, 2021

    @danmoseley
    Contributor

    Nit, it still says NullableState above.

  20. RikkiGibson commented on Jun 10, 2021

    @RikkiGibson
    Member

    I'm listening to the recording and I feel compelled to mention that if you want to handle this in combination with Nullable<T>, then the scenario [NotNull] int? Prop { get; } should give "not null" for its ReadState and [DisallowNull] int? Prop { get; set; } should give "not null" for its WriteState.

  21. terrajobst commented on Jun 10, 2021

    @terrajobst
    ContributorAuthor

    Nit, it still says NullableState above.

    Fixed, thanks!

  22. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Jun 30, 2021
  23. davidfowl commented on Jul 6, 2021

    @davidfowl
    Member

    Is this happening in .NET 6? We'd like to take a dependency on it.

  24. jeffhandley commented on Jul 6, 2021

    @jeffhandley
    Member

    Yes, #54985 is intended to be included in Preview 7.

  25. removed their assignment
    on Jul 8, 2021
  26. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Jul 13, 2021
  27. ghost locked as resolved and limited conversation to collaborators on Aug 12, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.ReflectionblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions