Using Entity Framework with IDbContext in .NET 9.0

By FoxLearn 2/21/2025 8:08:05 AM   27
Entity Framework (EF) Core is a powerful ORM for managing databases in .NET applications. While EF Core provides ease in interacting with databases, directly using DbContext in services or repositories can create tight coupling and make unit testing more challenging.

A cleaner solution is to abstract DbContext using an interface, like IDbContext, to improve testability and separation of concerns. This post will explore how to implement IDbContext in .NET 9.0, promoting flexibility and testability when working with EF Core.

Why Use IDbContext?

  1. Separation of Concerns: Keeps repository logic distinct from EF Core operations.
  2. Better Testability: Easier to mock the database context, improving unit test reliability.
  3. Encapsulation: Restricts direct access to DbContext, ensuring good practices.
  4. Loose Coupling: Reduces the dependency between your services and data layer.

Implementing IDbContext in .NET 9.0

Follow these steps to implement IDbContext with EF Core for the Customer entity:

1. Define the IDbContext Interface

First, define an interface that abstracts the database operations, allowing interaction with EF Core without directly depending on it.

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

Now, extend your DbContext to implement the IDbContext interface. In this case, we will implement it in an ApplicationContext for handling Customer data.

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

    public virtual DbSet<Customer> Customers => Set<Customer>();

    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<Customer>(entity =>
        {
            entity
                .HasKey(c => c.CustomerId)
                .ToTable("Customers", "Sales");

            entity.Property(c => c.Name)
                .HasMaxLength(100)
                .IsUnicode(false);
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

3. Register IDbContext in the Service Container

Next, register the IDbContext in the dependency injection container, so that it can be injected wherever needed.

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

4. Consume IDbContext in Repositories

Inject the IDbContext into your repository to ensure loose coupling and make testing easier.

public class CustomerRepository : ICustomerRepository
{
    private readonly IDbContext _dbContext;

    public CustomerRepository(IDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<List<Customer>> GetCustomersAsync()
    {
        var result = await _dbContext.Customers.ToListAsync();
        return result;
    }

    public async Task<Customer> GetCustomerByIdAsync(int id)
    {
        return await _dbContext.Customers
                               .FirstOrDefaultAsync(c => c.CustomerId == id);
    }
}

In this example, Scalar is utilized to document and manage API endpoints, offering an alternative to Swagger.

Once you've set up the code, you can run the application to interact with the Customer data using IDbContext, ensuring clean separation of concerns, flexibility, and ease of testing.

Implementing IDbContext in .NET 9.0 with EF Core allows for a clean and maintainable architecture.