Entity Framework  

How Do I Implement a Hybrid Data Access Layer Using Both Dapper and EF Core in a .NET Application?

Introduction

In modern .NET applications, developers often face a common challenge — choosing between performance and productivity when working with databases. As we discussed earlier, Dapper is fast and efficient, while Entity Framework Core (EF Core) is easy to use and reduces development effort.

But what if you don’t want to choose just one?

This is where a hybrid data access approach comes in. It allows you to use both Dapper and EF Core together in the same application, taking advantage of the strengths of each.

In this article, we will understand how to design and implement a hybrid data access layer in simple terms, step by step.

What is a Hybrid Data Access Layer?

A hybrid data access layer means:

  • Using EF Core for standard operations like Create, Update, Delete

  • Using Dapper for performance-critical or complex queries

Instead of replacing one with the other, you combine both tools in a smart way.

Why Use a Hybrid Approach?

Let’s understand the reasoning in simple terms.

Benefits of EF Core

  • Faster development

  • Clean and readable code

  • Built-in features like migrations and tracking

Benefits of Dapper

  • High performance

  • Full control over SQL

  • Better for complex queries

Combined Advantage

By using both:

  • You write less code (EF Core)

  • You get better performance where needed (Dapper)

When Should You Use Hybrid Approach?

You should consider this approach when:

  • Your application has both simple and complex queries

  • Performance matters in some parts of the system

  • You are building scalable APIs or microservices

  • You want flexibility in database operations

Architecture Overview

In a hybrid setup, your project is usually structured like this:

  • Controllers → Handle HTTP requests

  • Services → Business logic

  • Data Access Layer → EF Core + Dapper

You separate responsibilities clearly so that both tools can coexist without confusion.

Step-by-Step Implementation

Let’s go step by step to implement this in a .NET application.

Step 1: Install Required Packages

You need both EF Core and Dapper:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Dapper

Step 2: Configure EF Core DbContext

Create your DbContext as usual:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }
}

Register it in Program.cs:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

Step 3: Create a Dapper Context

Dapper works with IDbConnection, so we create a helper class:

public class DapperContext
{
    private readonly IConfiguration _configuration;

    public DapperContext(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public IDbConnection CreateConnection()
        => new SqlConnection(_configuration.GetConnectionString("DefaultConnection"));
}

Register it:

builder.Services.AddSingleton<DapperContext>();

Step 4: Create Repository or Service Layer

Now we use both EF Core and Dapper in the same service.

public class UserService
{
    private readonly AppDbContext _context;
    private readonly DapperContext _dapper;

    public UserService(AppDbContext context, DapperContext dapper)
    {
        _context = context;
        _dapper = dapper;
    }

    // EF Core for simple operation
    public async Task<User> GetUserById(int id)
    {
        return await _context.Users.FindAsync(id);
    }

    // Dapper for high-performance query
    public async Task<IEnumerable<User>> GetActiveUsers()
    {
        using var connection = _dapper.CreateConnection();
        var query = "SELECT * FROM Users WHERE IsActive = 1";
        return await connection.QueryAsync<User>(query);
    }
}

Step 5: Use in Controller

[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
    private readonly UserService _service;

    public UsersController(UserService service)
    {
        _service = service;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        var user = await _service.GetUserById(id);
        return Ok(user);
    }

    [HttpGet("active")]
    public async Task<IActionResult> GetActive()
    {
        var users = await _service.GetActiveUsers();
        return Ok(users);
    }
}

Best Practices for Hybrid Approach

To avoid confusion and maintain clean architecture, follow these best practices:

  • Keep EF Core and Dapper usage clearly separated

  • Use EF Core for writes (Insert, Update, Delete)

  • Use Dapper for read-heavy queries

  • Avoid mixing both in a single method unnecessarily

  • Keep SQL queries organized (stored procedures or query files)

Common Mistakes to Avoid

  • Overusing Dapper everywhere (losing EF Core benefits)

  • Writing complex SQL when EF Core can handle it easily

  • Mixing responsibilities without clear structure

  • Not managing connections properly

Performance Consideration

Hybrid approach improves performance because:

  • EF Core handles routine operations efficiently

  • Dapper optimizes heavy queries

This balance helps in building scalable applications without sacrificing developer productivity.

Summary

A hybrid data access layer in .NET combines the strengths of Entity Framework Core and Dapper by using EF Core for standard database operations and Dapper for performance-critical queries. This approach helps developers achieve both productivity and performance, making applications more efficient, scalable, and easier to maintain when implemented with proper structure and best practices.