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
}
}
...
- Constraints on type parameters - C# Programming Guide | Microsoft Docs
- null 許容参照型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
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 :)
Top comments (0)