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
}