DEV Community

Masui Masanori
Masui Masanori

Posted on

2 2

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

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>
...
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

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)
        {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
.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
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

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
    }
...
Enter fullscreen mode Exit fullscreen mode

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
    }
...
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
...
Enter fullscreen mode Exit fullscreen mode

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
        }
...
Enter fullscreen mode Exit fullscreen mode

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
        }
...
Enter fullscreen mode Exit fullscreen mode

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>();
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode
...
        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);
                });
        }
...
Enter fullscreen mode Exit fullscreen mode

"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

Heroku

Deploy with ease. Manage efficiently. Scale faster.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

Quickstart image

Django MongoDB Backend Quickstart! A Step-by-Step Tutorial

Get up and running with the new Django MongoDB Backend Python library! This tutorial covers creating a Django application, connecting it to MongoDB Atlas, performing CRUD operations, and configuring the Django admin for MongoDB.

Watch full video →

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay