EntityFramework.Exceptions 3.1.1 - Support for Entity Framework Core 3 and Improved API
In the previous article I introduced EntityFramework.Exceptions, a library which simplifies handling exceptions in Entity Framework Core but the library had one important limitation: In order to
use it you had to inherit your custom DbContext from ExceptionProcessorContextBase
class. This means that if you wanted to use some other base class for your DbContext you were out of luck. The
latest version of the library solves this issue by replacing one of the internal services used by entity framework core with a custom implementation and also adds support for Entity Framework Core 3.1.1
The service that needs to be replaced is
IStateManager and is used by the ChangeTracker. The custom
implementation of IStateManager
interface inherits from the built in
StateManager class and overrides SaveChanges
and SaveChangesAsync
methods. Let’s see how it works:
public abstract class ExceptionProcessorStateManager<T> : StateManager where T : DbException
{
private static readonly Dictionary<DatabaseError, Func<DbUpdateException, Exception>> ExceptionMapping = new Dictionary<DatabaseError, Func<DbUpdateException, Exception>>
{
{DatabaseError.MaxLength, exception => new MaxLengthExceededException("Maximum length exceeded", exception.InnerException) },
{DatabaseError.UniqueConstraint, exception => new UniqueConstraintException("Unique constraint violation", exception.InnerException) },
{DatabaseError.CannotInsertNull, exception => new CannotInsertNullException("Cannot insert null", exception.InnerException) },
{DatabaseError.NumericOverflow, exception => new NumericOverflowException("Numeric overflow", exception.InnerException) },
{DatabaseError.ReferenceConstraint, exception => new ReferenceConstraintException("Reference constraint violation", exception.InnerException) }
};
protected ExceptionProcessorStateManager(StateManagerDependencies dependencies) : base(dependencies)
{
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
try
{
return base.SaveChanges(acceptAllChangesOnSuccess);
}
catch (DbUpdateException originalException)
{
var exception = GetException(originalException);
if (exception != null)
{
throw exception;
}
throw;
}
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())
{
try
{
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
return result;
}
catch (DbUpdateException originalException)
{
var exception = GetException(originalException);
if (exception != null)
{
throw exception;
}
throw;
}
}
private Exception GetException(DbUpdateException ex)
{
if (ex.GetBaseException() is T dbException && GetDatabaseError(dbException) is DatabaseError error && ExceptionMapping.TryGetValue(error, out var ctor))
{
return ctor(ex);
}
return null;
}
protected abstract DatabaseError? GetDatabaseError(T dbException);
}
The abstract ExceptionProcessorStateManager
class catches any database exception thrown during SaveChanges
call and tries to translate it into one of the supported exception instances. If it succeeds it throws the new exception and if it doesn’t it simply rethrows the original exception. The GetDatabaseError
is overriden in database specific projects and returns DatabaseError
based on the
specific DbException
that was thrown:
class SqlServerExceptionProcessorStateManager: ExceptionProcessorStateManager<SqlException>
{
public SqlServerExceptionProcessorStateManager(StateManagerDependencies dependencies) : base(dependencies)
{
}
private const int ReferenceConstraint = 547;
private const int CannotInsertNull = 515;
private const int CannotInsertDuplicateKeyUniqueIndex = 2601;
private const int CannotInsertDuplicateKeyUniqueConstraint = 2627;
private const int ArithmeticOverflow = 8115;
private const int StringOrBinaryDataWouldBeTruncated = 8152;
protected override DatabaseError? GetDatabaseError(SqlException dbException)
{
switch (dbException.Number)
{
case ReferenceConstraint:
return DatabaseError.ReferenceConstraint;
case CannotInsertNull:
return DatabaseError.CannotInsertNull;
case CannotInsertDuplicateKeyUniqueIndex:
case CannotInsertDuplicateKeyUniqueConstraint:
return DatabaseError.UniqueConstraint;
case ArithmeticOverflow:
return DatabaseError.NumericOverflow;
case StringOrBinaryDataWouldBeTruncated:
return DatabaseError.MaxLength;
default:
return null;
}
}
}
In order to actually replace IStateManager
with the custom implementation you need to install either
SQL Server,
PostgreSQL or
MySQL nuget package and call UseExceptionProcessor
method of the ExceptionProcessorExtensions
from the database specific package:
class DemoContext : DbContext, IDemoContext
{
public DbSet<Product> Products { get; set; }
public DbSet<ProductSale> ProductSale { 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=(localdb)\ProjectsV13;Initial Catalog=Test;Integrated Security=True;Connect Timeout=30;")
.UseExceptionProcessor();
}
}
The UseExceptionProcessor
method is very simple and all it does is a call to
DbContextOptionsBuilder.ReplaceService<TService,TImplementation> method:
public static class ExceptionProcessorExtensions
{
public static DbContextOptionsBuilder UseExceptionProcessor(this DbContextOptionsBuilder self)
{
self.ReplaceService<IStateManager, SqlServerExceptionProcessorStateManager>();
return self;
}
public static DbContextOptionsBuilder<TContext> UseExceptionProcessor<TContext>(this DbContextOptionsBuilder<TContext> self) where TContext : DbContext
{
self.ReplaceService<IStateManager, SqlServerExceptionProcessorStateManager>();
return self;
}
}
Once you have done it you will start getting database specific exceptions instead of DbUpdateException
:
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.
}
}
The full source code of the library is available on GitHub: EntityFramework.Exceptions If you have questions or suggestions feel free to leave a comment, create an issue and star the repository.