Using EntityFramework with IDBContext in .NET 9.0

Entity Framework (EF) Core is a powerful tool for managing databases in .NET applications. However, using DbContext directly in repositories or services can create strong dependencies and make unit testing harder. A better approach is to use an abstraction like IDbContext to keep the architecture clean and separate concerns properly.

In this blog post, we'll discuss how to implement IDbContext in .NET 9.0, enabling database interactions while keeping the code flexible and easy to test.

Why Use IDbContext?

  • Separation of Concerns: Keeps repository logic separate from EF Core.
  • Better Testability: This makes it easier to mock the database context for unit tests.
  • Encapsulation: Restricts direct access to DbContext, ensuring best practices.
  • Loose Coupling: Minimizes dependencies between services and the data layer.

The source code can be downloaded from GitHub

Implement IDBConext

Entity Framework Power Tools is a Visual Studio extension that offers helpful design-time features for EF Core. It allows developers to visualize, generate, and manage EF Core models with ease.

1. Define IDbContext Interface: We begin by defining an interface that abstracts EF Core DbContext operations.

public interface IDbContext { DbSet<T> Set<T>() where T : class; Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); Task ExecuteStoredProcedureAsync(string storedProcName, object parameters = null); }

2.  Implement the Interface in Your DbContext: Extend your DbContext to implement the IDbContext interface

public partial class EmployeeContext : DbContext,IDbContext
{
    public EmployeeContext(DbContextOptions<EmployeeContext> options)
        : base(options)
    {
    }

    public virtual DbSet<EmployeeSalary> EmployeeSalaries => Set<EmployeeSalary>();

    public Task ExecuteStoredProcedureAsync(string storedProcName, object parameters = null)
    {
        throw new NotImplementedException();
    }

    async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return await base.SaveChangesAsync(cancellationToken);
    }
    

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<EmployeeSalary>(entity =>
        {
            entity
                .HasNoKey()
                .ToTable("EmployeeSalary", "Employee");

            entity.Property(e => e.EmpName)
                .HasMaxLength(10)
                .IsUnicode(false);
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

3. Register IDbContext Interface: Register the IDbConext with the service container

public static class DataInjections
{
    public static void AddDataServices(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddDbContext<IDbContext, EmployeeContext>(options =>
        {
            options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
        });
        services.AddScoped<IEmployeeRepository, EmployeeRepository>();
    }
}

4. Consume IDbContext in the repositories: Inject IDbContext into the repositories to promote loose coupling and testability

public class EmployeeRepository(
    IDbContext dbContext
    ) : IEmployeeRepository
{
    public async Task<List<DomainModel.EmployeeSalary>> GetEmployeeAsync()
    {
        var result = await dbContext.EmployeeSalaries.ToListAsync();
        return result.MapToDomainEmployeeSalaries();
    }
}

I have incorporated Scalar as an alternative to Swagger.

Execute the code, and you will see the below screen.

Execution of the code

Conclusion

Implementing IDbContext in .NET 9.0 with EF Core 9 follows the same approach as previous versions, promoting clean architecture and improved testability. Stay up-to-date with the latest features and breaking changes in EF Core 9 to make the most of its capabilities.

Happy Coding!