Entity Framework Core and Nullable

There is one thing that has been bothering me for some time, and that is how it is possible to combine an Entity Framework Core database model with enabled null-checks. If you set up the standard EF tutorial model like this:

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;

namespace ModelTest;

[Table("Blog")]
public class Blog
{
    public string Id { get; set; }
    public string Name { get; set; }
    public virtual Uri SiteUri { get; set; }

    public ICollection<Post> Posts { get; }
}

[Table("Post")]
public class Post
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTimeOffset PublishedOn { get; set; }
    public bool Archived { get; set; }

    public string BlogId { get; set; }
    public Blog Blog { get; set; }
}

internal class MyContext : DbContext
{
    public MyContext(DbContextOptions<MyContext> options) : base(options)
    { }

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasMany(blog => blog.Posts)
            .WithOne(post => post.Blog)
            .HasForeignKey(post => post.BlogId)
            .HasPrincipalKey(blog => blog.Id);
    }
}

And a simple ASP.NET Core Web API like this:

using Microsoft.EntityFrameworkCore;
using ModelTest;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<MyContext>(opt => opt.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=ModelTest;Integrated Security=True;"));

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI();

app.MapGet("/blogs", async (MyContext dbContext) =>
    await dbContext.Blogs.ToListAsync());
app.MapGet("/blogs/{id}", async (MyContext dbContext, string id) =>
    await dbContext.Blogs.SingleOrDefaultAsync(blog => blog.Id == id));
app.MapGet("/blogs/{id}/posts", async (MyContext dbContext, string id) =>
    await dbContext.Posts.Where(post => post.BlogId == id).ToListAsync());
app.MapGet("/posts/{id}", async (MyContext dbContext, string id) =>
    await dbContext.Posts.SingleOrDefaultAsync(post => post.Id == id));

app.Run();

There will be no less than eleven warnings (CS8618 “Non-nullable property xxx must contain a non-null value when exiting constructor. Consider declaring the property as nullable”). One way would be to simply suppress this in the model classes using

#pragma warning disable CS8618
...
#pragma warning restore CS8618

But it is kind of unsatisfactory and can lead to bugs. If we take Post as an example, no properties will actually be null except Blog, which can be seen by making a request to /blogs/{id}/posts:

[
  {
    "id": "1",
    "title": "Post 1",
    "content": "Hello",
    "publishedOn": "2023-04-03T00:00:00+02:00",
    "archived": false,
    "blogId": "1",
    "blog": null
  },
  {
    "id": "2",
    "title": "Post 2",
    "content": "Another post",
    "publishedOn": "2023-04-03T00:00:00+02:00",
    "archived": false,
    "blogId": "1",
    "blog": null
  }
]

Sidetrack: To make the blog property appear in each post, we must include the Blog property in our query (and at the same time prevent cycles):

builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
});

app.MapGet("/blogs/{id}/posts", async (MyContext dbContext, string id) =>
    await dbContext.Posts.Where(post => post.BlogId == id).Include(post => post.Blog).ToListAsync());

Ok, other than disabling the warning, could we do something better? Maybe do as the warning says?

public class Post
{
    public string? Id { get; set; }
    public string? Title { get; set; }
    public string? Content { get; set; }
    public DateTimeOffset PublishedOn { get; set; }
    public bool Archived { get; set; }

    public string? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

The problem with this is that Entity Framework now wants to alter the columns to be nullable, which we didn’t want since we didn’t define the properties as nullable in the first place. This can be seen by creating a test migration:

 dotnet ef migrations add Test

So if we instead create a constructor that takes all property values as parameters?

public class Blog
{
    public Blog(string id, string name, Uri siteUri, ICollection<Post> posts)
    {
        Id = id;
        Name = name;
        SiteUri = siteUri;
        Posts = posts;
    }
...
}

public class Post
{
    public Post(string id, string title, string content, DateTimeOffset publishedOn, bool archived, string blogId, Blog blog)
    {
        Id = id;
        Title = title;
        Content = content;
        PublishedOn = publishedOn;
        Archived = archived;
        BlogId = blogId;
        Blog = blog;
    }
...
}

Doesn’t work:

System.InvalidOperationException: No suitable constructor was found for entity type 'Blog'. The following constructors had parameters that could not be bound to properties of the entity type:
    Cannot bind 'posts' in 'Blog(string id, string name, Uri siteUri, ICollection<Post> posts)'
Note that only mapped properties can be bound to constructor parameters. Navigations to related entities, including references to owned types, cannot be bound.

What we can do is to exclude the navigation property from the constructor.

public class Blog
{
    public Blog(string id, string name, Uri siteUri)
    {
        Id = id;
        Name = name;
        SiteUri = siteUri;
    }
...
}

public class Post
{
    public Post(string id, string title, string content, DateTimeOffset publishedOn, bool archived, string blogId)
    {
        Id = id;
        Title = title;
        Content = content;
        PublishedOn = publishedOn;
        Archived = archived;
        BlogId = blogId;
    }
...
}

This means we only have to warnings left:

Warning	CS8618	Non-nullable property 'Posts' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Warning	CS8618	Non-nullable property 'Blog' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

This is actually quite in order – if we don’t include those properties in the LINQ query, they are going to be null, so it can be argued that the most sensible thing is to declare them as nullable:

[Table("Blog")]
public class Blog
{
    public Blog(string id, string name, Uri siteUri)
    {
        Id = id;
        Name = name;
        SiteUri = siteUri;
    }

    public string Id { get; set; }
    public string Name { get; set; }
    public Uri SiteUri { get; set; }
    public ICollection<Post>? Posts { get; }
}

[Table("Post")]
public class Post
{
    public Post(string id, string title, string content, DateTimeOffset publishedOn, bool archived, string blogId)
    {
        Id = id;
        Title = title;
        Content = content;
        PublishedOn = publishedOn;
        Archived = archived;
        BlogId = blogId;
    }

    public string Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTimeOffset PublishedOn { get; set; }
    public bool Archived { get; set; }
    public string BlogId { get; set; }
    public Blog? Blog { get; set; }
}

Could we take this one step further, eliminating the setters? No, unfortunately not, but we can change them to init:

[Table("Blog")]
public class Blog
{
    public Blog(string id, string name, Uri siteUri)
    {
        Id = id;
        Name = name;
        SiteUri = siteUri;
    }

    public string Id { get; init; }
    public string Name { get; init; }
    public Uri SiteUri { get; init; }
    public ICollection<Post>? Posts { get; init; }
}

[Table("Post")]
public class Post
{
    public Post(string id, string title, string content, DateTimeOffset publishedOn, bool archived, string blogId)
    {
        Id = id;
        Title = title;
        Content = content;
        PublishedOn = publishedOn;
        Archived = archived;
        BlogId = blogId;
    }

    public string Id { get; init; }
    public string Title { get; init; }
    public string Content { get; init; }
    public DateTimeOffset PublishedOn { get; init; }
    public bool Archived { get; init; }
    public string BlogId { get; init; }
    public Blog? Blog { get; init; }
}

In fact, we can take it one step further and use records instead. Navigation properties must still be standard properties with get and init:

[Table("Blog")]
public record class Blog(string Id, string Name, Uri SiteUri)
{
    public ICollection<Post>? Posts { get; init; }
}

[Table("Post")]
public record class Post(string Id, string Title, string Content, DateTimeOffset PublishedOn, bool Archived, string BlogId)
{
    public Blog? Blog { get; init; }
}

Entity Framework Update Database without Source Code

If you, as part of your Team Foundation Server (TFS) release, wish to perform automatic migration of your database using Entity Framework Core, there is a problem. If you try the standard PowerShell command Update-Database or the CLI equivalent dotnet ef database update you will receive the error message No project was found. Change the current working directory or use the –project option. This is because you’re published application consists of binaries only. You got two options:

  1. Package the source code as part of your release (and hope it won’t get installed on the production server).
  2. Use the following hack.

It turns out you can get away with the undocumented dotnet exec command like the following example (assuming the web application is called WebApp):

set rootDir=$(System.DefaultWorkingDirectory)\WebApp\drop\WebApp.Web
set efPath=C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.entityframeworkcore.tools\2.1.1\tools\netcoreapp2.0\any\ef.dll
dotnet exec --depsfile "%rootDir%\WebApp.deps.json" --additionalprobingpath %USERPROFILE%\.nuget\packages --additionalprobingpath "C:\Program Files\dotnet\sdk\NuGetFallbackFolder" --runtimeconfig "%rootDir%\WebApp.runtimeconfig.json" "%efpath%" database update --verbose --prefix-output --assembly "%rootDir%\AssemblyContainingDbContext.dll" --startup-assembly "%rootDir%\AssemblyContainingStartup.dll" --working-dir "%rootDir%"

Note that the Working Directory (hidden under Advanced) of this run command line task must be set to where the artifacts are (rootDir above).