loading...

[C# 8][Entity Framework Core] Play with Nullable reference types

masanori_msl profile image Masui Masanori ・6 min read

Intro

Because I have wanted to distinguish between nullable and non-null objects.
So I try "nullable reference types" that has been implemented in C# 8.0.

I use the project what I used last time.

Environments

  • .NET 5: ver.5.0.100-preview.7.20366.6
  • Microsoft.EntityFrameworkCore: ver.5.0.0-preview.7.20365.15
  • Microsoft.EntityFrameworkCore.Design: ver.5.0.0-preview.7.20365.15
  • Npgsql.EntityFrameworkCore.PostgreSQL: ver.5.0.0-preview7-ci.20200722t163648
  • Microsoft.EntityFrameworkCore.Abstractions: ver.5.0.0-preview.7.20365.15
  • Microsoft.EntityFrameworkCore.Relational: ver.5.0.0-preview.7.20365.15
  • Microsoft.AspNetCore.Mvc.NewtonsoftJson: ver.5.0.0-preview.7.20365.19

Prepare

To use "nullable reference types" I must change the project language version to 8.0 or more and set "Nullable" enable.

CodeFirstSample.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>preview</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>
...

After setting "Nullable" enable, I get many warnings.
So I change my codes.

About "nullable reference types"

Before C# 8, "value types" only can be nullable.

int nonNullNumber = 0;
nonNullNumber = null; // <- compile error
int? nullableNumber = 0;
nullableNumber = null; // <- OK

// Nullable types must be cast first to assign
nonNullNumber = (int)nullableNumber;

After c# 8, I can distinguish reference type instances maybe null or not.

using System;
using Models;

namespace CSharpEightSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // Nullable instance
            User? nullableUser = null;  // <- OK
            // Non-nullable instance
            User user = null;           // <- Warning
        }
    }
}

Unfortunately I only can get warning when I assign null into Non-nullable instance.
It's for backward compatibility.

IL

Are there any differences between Nullable instance and Non-nullable instance ?
I have a try with ILSpy Visual Studio Code Extension( https://github.com/icsharpcode/ilspy-vscode ).

.class /* 02000005 */ private auto ansi beforefieldinit CSharpEightSample.Program
    extends [System.Runtime]System.Object
{
    // Methods
    .method /* 06000005 */ private hidebysig static 
        void Main (
            string[] args
        ) cil managed 
    {
        .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
            01 00 01 00 00
        )
        // Method begins at RVA 0x2094
        // Code size 6 (0x6)
        .maxstack 1
        .entrypoint
        .locals /* 11000001 */ init (
            [0] class Models.User,
            [1] class Models.User
        )

        IL_0000: nop
        IL_0001: ldnull
        IL_0002: stloc.0
        IL_0003: ldnull
        IL_0004: stloc.1
        IL_0005: ret
    } // end of method Program::Main
...

For local instances, there are no any differences.
How about properties?

namespace CSharpEightSample
{
    public class Sample
    {
        public string Name { get; set; } = "";
        public string? NullableName {get; set; }
    }
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}
.class /* 02000005 */ public auto ansi beforefieldinit CSharpEightSample.Sample
    extends [System.Runtime]System.Object
{
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
        01 00 01 00 00
    )
    .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
        01 00 00 00 00
    )
    // Fields
    .field /* 04000003 */ private string '<Name>k__BackingField'
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )
    .field /* 04000004 */ private string '<NullableName>k__BackingField'
    .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
        01 00 02 00 00
    )
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )

    // Methods
    .method /* 06000005 */ public hidebysig specialname 
        instance string get_Name () cil managed 
    {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x2092
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */
        IL_0006: ret
    } // end of method Sample::get_Name

    .method /* 06000006 */ public hidebysig specialname 
        instance void set_Name (
            string 'value'
        ) cil managed 
    {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x209a
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: stfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */
        IL_0007: ret
    } // end of method Sample::set_Name

    .method /* 06000007 */ public hidebysig specialname 
        instance string get_NullableName () cil managed 
    {
        .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
            01 00 02 00 00
        )
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x20a3
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld string CSharpEightSample.Sample::'<NullableName>k__BackingField' /* 04000004 */
        IL_0006: ret
    } // end of method Sample::get_NullableName

    .method /* 06000008 */ public hidebysig specialname 
        instance void set_NullableName (
            string 'value'
        ) cil managed 
    {
        .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
            01 00 02 00 00
        )
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x20ab
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: stfld string CSharpEightSample.Sample::'<NullableName>k__BackingField' /* 04000004 */
        IL_0007: ret
    } // end of method Sample::set_NullableName

    .method /* 06000009 */ public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20b4
        // Code size 19 (0x13)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldstr "" /* 70000001 */
        IL_0006: stfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */
        IL_000b: ldarg.0
        IL_000c: call instance void [System.Runtime]System.Object::.ctor() /* 0A00000F */
        IL_0011: nop
        IL_0012: ret
    } // end of method Sample::.ctor

    // Properties
    .property /* 17000001 */ instance string Name()
    {
        .get instance string CSharpEightSample.Sample::get_Name()
        .set instance void CSharpEightSample.Sample::set_Name(string)
    }
    .property /* 17000002 */ instance string NullableName()
    {
        .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
            01 00 02 00 00
        )
        .get instance string CSharpEightSample.Sample::get_NullableName()
        .set instance void CSharpEightSample.Sample::set_NullableName(string)
    }

} // end of class CSharpEightSample.Sample

"NullableName" has "System.Runtime.CompilerServices.NullableAttribute".

default!

For example I have a non-nullable string instance.
I can assign empty string or "default!" as default value.

string sample = ""; // <- OK
string sample2 = default! // <- OK

But what is assigned into "sample2"?
It's null value.

So I can use "default!" only for avoiding warnings.

Generics

How about Generics?
I can't write like below.

...
    public class Sample<T> where T: class
    {
        public T Name { get; set; } = default!;
        public T? NullableName {get; set; } // <- compile error
    }
...

If "T" has class or struct constraints, I can use "T?".

...
    public class Sample<T> where T: class
    {
        public T Name { get; set; } = default!;
        public T? NullableName {get; set; } // <- OK
    }
...

Or I also can use "class ?" constraints.

...
    public class Sample<T> where T: class ?
    {
        public T Name { get; set; } = default!;
        public T NullableName {get; set; } = default!;
    }
    class Program
    {
        static void Main(string[] args)
        {
            var s = new Sample<string?>(); // <- OK
        }
    }
...

Or I can use attributes.

...
    public class Sample<T>
    {
        public T Name { get; set; } = default!;
        [AllowNull]
        public T NullableName {get; set; } = default!;
    }
    class Program
    {
        static void Main(string[] args)
        {
            var s = new Sample<string>();
            s.NullableName = null;          // <- OK
        }
...

I can use "[return:XXX]" attributes for methods.

...
    public class Sample<T>
    {
        public T Name { get; set; } = default!;
        [AllowNull]
        public T NullableName {get; set; } = default!;
        [return: MaybeNull]
        public T GetNull()
        {
            return default(T);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var s = new Sample<string>();
            s.NullableName = null;
            Console.WriteLine((s.GetNull() == null));          // <- true
        }
...

Entity FrameWork Core

DbContext

In DbContext class, I can use "Set<T>()" to avoid making "DbSet<T>" instances nullable.

using Microsoft.EntityFrameworkCore;

namespace Models
{
    public class CodeFirstSampleContext: DbContext
    {
        public CodeFirstSampleContext(DbContextOptions<CodeFirstSampleContext> options)
            : base(options)
        {

        }
        public DbSet<Workflow> Workflows => Set<Workflow>();
        public DbSet<WorkflowReader> WorkflowReaders => Set<WorkflowReader>();
    }
}

Model

How about Model classes?

There aren't any special methods to avoid making them nullable.
So I should determine nullable or non-nullable from Database tables.

Does nullable/not-nullable reference types affect for migrations?

The answer is yes.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Models
{
    public class Sample
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string? Name { get; set; } = "hello";
    }
}
...
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Samples",
                columns: table => new
                {
                    Id = table.Column<int>(type: "integer", nullable: false)
                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
                    Name = table.Column<string>(type: "text", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Samples", x => x.Id);
                });
        }
...

"Name" is nullable though I set default value.

I still don't know if all properties should be non-null types as much as possible.
But I will try it first :)

Resources

Discussion

pic
Editor guide