Rico Suter's blog.
 


After reading some articles about the new C# 8 feature Nullable Reference Types (abbr. NRT; specification on GitHub), one question still was unanswered: How does a compiled assembly with nullable types enabled look like and how can I access the nullability information via reflection? Because I couldn’t find an article with an answer, I had to figure it out myself.

First I wrote a simple class which I wanted to analyze:

class Test
{
    public string? Foo {get; set;}

    public void Bar(string? baz, string buz) 
    {

    }
}

With the help of sharplab.io I compiled it with C# 8 and looked at the resulting output (simplified):

internal class Test
{
    [Nullable]
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string k__BackingField;

    [Nullable]
    public string Foo
    {
        [CompilerGenerated]
        [return: Nullable]
        get
        {
            return k__BackingField;
        }
        [CompilerGenerated]
        [param: Nullable]
        set
        {
            k__BackingField = value;
        }
    }

    public void Bar([Nullable] string baz, string buz)
    {
    }
}

I noticed two important things:

  1. The compiler generates NullableAttributes on the properties, fields and parameters. This indicates that nullability is explicit and non-nullability is implicit (no attribute).
  2. There is a NonNullTypesAttribute on the module, which enables Roslyn’s nullability analyzer for the whole module (assembly). This attribute can also be used to disable the analyzer for some parts or enable it just for a class or other regions. The NonNullTypesAttribute is used to enable Roslyn’s nullability flow analysis which will produce warnings in Visual Studio and also specify the rules whether something is nullable or not.

The problem: The meaning of not having a NullableAttribute changes depending on the context (i.e. depending whether a NonNullTypesAttribute is active). And determining the current context is probably not trivial. This makes it very hard to do reflection just based on the existence of the NullableAttribute. Wouldn’t it be better to have a NotNullableAttribute instead so that “old” libraries are “nullable” by default and no context is needed when reflecting? This would greatly improve backward compatibility. But maybe this design has been chosen for performance reasons or because in the long run a NullableAttribute makes more sense (e.g. when NonNullTypesAttribute is enabled by default)…

If it’s staying as is, I’m wondering if there will also be a new reflection API to easily reflect over the nullability of properties, parameters and fields? If so, this API should come in an independent, widely usable package so that library authors can use it everywhere, also in “older” libraries…

If you know the answer to one of these questions please write a comment below…

Background: I’m writing libraries to generate JSON Schema (NJsonSchema) and OpenAPI/Swagger specs (NSwag). These libraries rely heavily on reflection. I looked into the Nullable Reference Types feature because I’d really like to use it in these generators…

Update 2019-03-31

The latest version of the C# 8 compiler changed dramatically and now the NullableAttribute is no longer context dependent but seems to be always generated when the Nullable Reference Types feature is enabled.

This class:

#nullable enable

using System;

class Test
{
    public void Bar(Tuple<String, String?, int, Nullable<int>> baz, string buz) 
    {

    }
}

… now compiles to:

internal class Test
{
    public void Bar([Nullable(new byte[] {
        1,
        1,
        2,
        0,
        0,
        0
    })] Tuple<string, string, int, int?> baz, [Nullable(1)] string buz)
    {
    }
}

As you can see, the numbers in the NullableAttribute describe the nullability of the flattened down type and its generic arguments. The meaning of the numbers is as follows:

  • 0 = Never null (e.g. not a reference type)
  • 1 = Not nullable
  • 2 = Nullable

A little bit unintuitive is the usage of Nullable<T> (e.g. int?) as it will be reported as a Nullable<T> which is never null with a generic argument of T which is also never null - and not as an int which is nullable.

I created a first version of an advanced .NET reflection library which incorporates all these rules: Namotion.Reflection

Update 2019-07-10

The output metadata has changed again, check out this page.



Discussion