Gracefully Handling Entity Framework Exceptions with EntityFramework.Exceptions

Working with databases can sometimes be daunting, mainly when errors occur. These errors or exceptions can be due to many reasons, such as constraint violations, connection issues, or syntax errors. Entity Framework throws a generic DbException or DbUpdateException for most of these database issues. But we cand get more specific exceptions based on the concrete "problem"! That's where EntityFramework.Exceptions comes in.

The way it works

EntityFramework.Exceptions sites in the middle between you and Entity Framework and wraps the exceptions depending on various conditions in a new exception with a meaningful name. For example if you have a IsRequired property that unfortunately is null, you will get an CannotInsertNullException. Not only makes it debugging easier but you could also catch specific exceptions and handle them gracefully.

Setup

The setup is quite easy, add the NuGet package for the provider you need. Here is a complete overview. For example:

dotnet add package EntityFrameworkCore.Exceptions.SqlServer
dotnet add package EntityFrameworkCore.Exceptions.PostgreSQL
...

And so on. In my case I will use Sqlite with the in-memory database to quickly showcase how it works. Then you have to add the "middleware":

public class BlogContext : DbContext
{
    public DbSet<BlogPost> BlogPosts { get; set; } = default!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlite("Filename=:memory:")
            .UseExceptionProcessor();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BlogPost>().Property(b => b.Title)
            .IsRequired();
    }
}

UseExceptionProcessor is the interesting part that comes from the package.

An example

The example above shows, that my Title has to be required - so it can not be null. But what if we do?

await using var context = new BlogContext();
context.Database.OpenConnection();
context.Database.EnsureCreated();

var blogPostWithNullTitle = new BlogPost
{
    Title = null,
    Content = "Content"
};

context.BlogPosts.Add(blogPostWithNullTitle);
await context.SaveChangesAsync();

Now here is the exception without the middleware:

/Users/stgi/repos/BlogExamples/EntityFrameworkExceptions/bin/Debug/net7.0/EntityFrameworkExceptions
Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
 ---> Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'NOT NULL constraint failed: BlogPosts.Title'.
   at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
   at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()
...

And here with:

/Users/stgi/repos/BlogExamples/EntityFrameworkExceptions/bin/Debug/net7.0/EntityFrameworkExceptions
Unhandled exception. EntityFramework.Exceptions.Common.CannotInsertNullException: Cannot insert null
 ---> Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'NOT NULL constraint failed: BlogPosts.Title'.
   at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
   at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()

So only a thin layer on top that encapsulate the original exception in a more meaningful one!

That allows you to do stuff like this:

try
{
    await context.SaveChangesAsync();
}
catch (CannotInsertNullException e)
{
    // Do something specific to this exception
}

Resources

  • Source code to this blog post: here
  • All my sample code is hosted in this repository: here
9
An error has occurred. This application may no longer respond until reloaded. Reload x