|
| 1 | +# EntityFramework.Exceptions |
| 2 | + |
| 3 | +Typed exception handling for Entity Framework Core. Converts database-specific errors into strongly-typed .NET exceptions instead of generic `DbUpdateException`. |
| 4 | + |
| 5 | +## Build & Test |
| 6 | + |
| 7 | +```bash |
| 8 | +dotnet restore # Restore NuGet dependencies |
| 9 | +dotnet build --no-restore # Build all projects |
| 10 | +dotnet test --no-build # Run tests (requires Docker for Testcontainers) |
| 11 | +``` |
| 12 | + |
| 13 | +Tests use **Testcontainers** and require a running Docker daemon. Each database provider (SQL Server, PostgreSQL, MySQL, Oracle, SQLite) spins up its own container. |
| 14 | + |
| 15 | +## Project Structure |
| 16 | + |
| 17 | +The solution (`EntityFramework.Exceptions.slnx`) has two main layers: |
| 18 | + |
| 19 | +- **DbExceptionClassifier/** — Database-specific error code classification. Each provider implements `IDbExceptionClassifier` to map native error codes to a `DatabaseError` enum. |
| 20 | + - `Common/` — `IDbExceptionClassifier` interface |
| 21 | + - `SqlServer/`, `PostgreSQL/`, `MySQL/`, `MySQL.Pomelo/`, `Oracle/`, `Sqlite/` — Provider implementations |
| 22 | + |
| 23 | +- **EntityFramework.Exceptions/** — EF Core integration via interceptors. Catches `DbException`, classifies it, and throws a typed exception. |
| 24 | + - `Common/` — Base `ExceptionProcessorInterceptor<T>`, exception classes (`UniqueConstraintException`, `CannotInsertNullException`, `MaxLengthExceededException`, `NumericOverflowException`, `ReferenceConstraintException`), `ExceptionFactory` |
| 25 | + - `SqlServer/`, `PostgreSQL/`, `MySQL/`, `MySQL.Pomelo/`, `Oracle/`, `Sqlite/` — Provider-specific interceptors and `UseExceptionProcessor()` extension methods |
| 26 | + - `Tests/` — xUnit test suite using Testcontainers |
| 27 | + |
| 28 | +- **Directory.Build.props** — Shared build properties (target framework, version, NuGet metadata). All non-Common projects automatically reference their corresponding Common project. |
| 29 | + |
| 30 | +## Architecture |
| 31 | + |
| 32 | +1. **Interceptor pattern**: `ExceptionProcessorInterceptor<TProviderException>` implements `IDbCommandInterceptor` and `ISaveChangesInterceptor` |
| 33 | +2. **Classification**: Each database provider has an `IDbExceptionClassifier` that maps native error codes to `DatabaseError` enum values |
| 34 | +3. **Factory**: `ExceptionFactory` creates the appropriate typed exception |
| 35 | +4. **Extension methods**: Each provider exposes `UseExceptionProcessor()` on `DbContextOptionsBuilder` |
| 36 | + |
| 37 | +## Code Conventions |
| 38 | + |
| 39 | +- **C# / .NET 8.0** with file-scoped namespaces, primary constructors, nullable reference types, and implicit usings |
| 40 | +- **Naming**: `[Database]ExceptionClassifier`, `[Database]ExceptionProcessorInterceptor`, `ExceptionProcessorExtensions.UseExceptionProcessor()` |
| 41 | +- **Test naming**: `[Scenario]Throws[ExceptionType]` (e.g., `UniqueColumnViolationThrowsUniqueConstraintException`) |
| 42 | +- **MySQL.Pomelo** shares source files with **MySQL** via `<Link>` in .csproj and uses `#if POMELO` preprocessor directive |
| 43 | +- Exception classes follow a standard pattern: inherit `DbUpdateException`, provide all standard constructor overloads, and optionally expose `ConstraintName`, `ConstraintProperties`, and `SchemaQualifiedTableName` properties |
| 44 | + |
| 45 | +## Testing Notes |
| 46 | + |
| 47 | +- Base test class `DatabaseTests` defines ~12 virtual test methods; provider-specific test classes inherit and override as needed |
| 48 | +- **SQLite** does not populate `ConstraintName`/`ConstraintProperties` and does not enforce numeric overflow |
| 49 | +- **SQL Server** skips numeric overflow tests (`ArgumentException` from SqlClient) |
| 50 | +- **MySQL** primary key violations do not populate constraint properties |
| 51 | +- Test fixtures use `IAsyncLifetime` for container lifecycle management |
0 commit comments