Introducing EntityFramework.Exceptions

When using Entity Framework Core for data access all database exceptions are wrapped in DbUpdateException. If you need to know whether the exception was caused by a unique constraint, value being too long or value missing for a required column you need to dig into the concrete DbException subclass instance and check the error number to determine the exact cause.

EntityFramework.Exceptions simplifies this by handling all the database specific details and throwing different exceptions for different cases. All you have to do is

inherit your DbContext from ExceptionProcessorContext and handle the exception(s) (such as UniqueConstraintException, CannotInsertNullException, MaxLengthExceededException, NumericOverflowException) you need.

The Problem with Entity Framework Exceptions

Let’s say we have a Product table which has Name column with a unique index and Price column. Entity Framework context will look like this:

class DemoContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder builder) 
    { 
        builder.Entity<Product>().Property(b => b.Price).HasColumnType("decimal(5,2)").IsRequired();
        builder.Entity<Product>().Property(b => b.Name).IsRequired().HasMaxLength(15);
        builder.Entity<Product>().HasIndex(u => u.Name).IsUnique(); 
    } 

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 
    { 
        optionsBuilder.UseSqlServer(@"Data Source=localhost;Initial Catalog=Test;Integrated Security=True;Connect Timeout=30;"); 
    }
}

If we try to insert two records with the same name we will get DbUpdateException. As we have already mentioned DbUpdateException is thrown every time when saving changes to the database fails. In order to check that the exception was caused by the unique index we need to get the specific database DbException subclass instance and check the error number. In case of SQL Server we can do it like this:

using (var demoContext = new DemoContext())
{
    demoContext.Products.Add(new Product
    {
        Name = "Moon Lamp",
        Price = 1
    });

    demoContext.Products.Add(new Product
    {
        Name = "Moon Lamp",
        Price = 10
    });

    try
    {
        demoContext.SaveChanges();
    }
    catch (DbUpdateException e)
    {
        var sqlException = e.GetBaseException() as SqlException;
        //2601 is error number of unique index violation
        if (sqlException != null && sqlException.Number == 2601)
        {
            //Unique index was violated. Show corresponding error message to user.
        }
    }
}

While this works it has several disadvantages. First of all it is repetitive to write this code every time we try to save changes to database. Secondly, there are other database errors to handle such as trying to insert null in non-null column or trying to insert longer value than the column allows. Finally, the error numbers are different for different database servers.

To avoid these issues I have created a library EntityFramework.Exceptions which handles database specific errors and throws different exceptions for different database errors.

EntityFramework.Exceptions – Easy Way to Handle Exceptions

With EntityFramework.Exceptions we can rewrite the above code like this:

using (var demoContext = new DemoContext())
{
    demoContext.Products.Add(new Product
    {
        Name = "Moon Lamp",
        Price = 1
    });

    demoContext.Products.Add(new Product
    {
        Name = "Moon Lamp",
        Price = 10
    });

    try
    {
        demoContext.SaveChanges();
    }
    catch (UniqueConstraintException e)
    {
        //Unique index was violated. Show corresponding error message to user.
    }
}

As you can see we no longer have to deal with database specific exception and error numbers. Instead UniqueConstraintException is thrown when either a unique index or unique constraint is violated. As a result our code is cleaner, shorter and easier to understand. What’s more the library provides other exceptions such as CannotInsertNullException, MaxLengthExceededException, NumericOverflowException. For example if we try insert a product with Name longer than 15 characters we will get MaxLengthExceededException exception:

using (var demoContext = new DemoContext())
{
    demoContext.Products.Add(new Product
    {
        Name = "Moon Lamp Change 3 Colors",
        Price = 1
    });

    try
    {
        demoContext.SaveChanges();
    }
    catch (MaxLengthExceededException e)
    {
        //Max length of Name column exceeded. Show corresponding error message to user.
    }
}

Apart from this EntityFrameworkCore.Exceptions supports other database servers such as PostgreSQL and MySQL so if you ever switch to different database server your exception handling code will stay the same.

To get started with EntityFramework.Exceptions all you need to do is install either SQL Server, PostgreSQL or MySQL nuget package and inherit your DbContext from ExceptionProcessorContext:

class DemoContext : ExceptionProcessorContext
{
    public DbSet<Product> Products { get; set; }
}

How Does EntityFramework.Exceptions Work ?

The implementation is pretty straightforward. There is an ExceptionProcessorContextBase class in EntityFramework.Exceptions.Common project which inherits from DbContext, overrides SaveChanges and handles any exception that occurs. It gets the database specific exception instance and asks derived classes to tell which exception it should throw for the DbException that occurred. For more details please check the GitHub repository. If you find the library useful don’t forget to Star the repository. If you have any questions or suggestions you are welcome to submit an issue or send a PR.

Avatar
Giorgi Dalakishvili
World-Class Software Engineer

Related