I’ll show two approaches you can choose from:

Key libraries used: Dapper , Npgsql (Postgres ADO.NET driver), and Npgsql.EntityFrameworkCore.PostgreSQL (EF Core provider).

1. Create project & add packages

  
    dotnet new webapi -n DapperEfPgDemo
cd DapperEfPgDemo

dotnet add package Dapper
dotnet add package Npgsql
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
  

(Install dotnet-ef tool if you need CLI migrations: dotnet tool install --global dotnet-ef .)

2. Connection string (appsettings.json)

  
    {
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=dapperefpg;Username=postgres;Password=your_password"
  }
}
  

3. POCO models

Models/Product.cs

  
    public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
}
  

Models/Category.cs

  
    public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
}
  

4. EF Core DbContext

Data/AppDbContext.cs

  
    using Microsoft.EntityFrameworkCore;

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

    public DbSet<Product> Products { get; set; } = null!;
    public DbSet<Category> Categories { get; set; } = null!;
}
  

Register DbContext in Program.cs (below).

5. Program.cs (DI & EF + optional Dapper factory)

Program.cs (minimal)

  
    using Npgsql;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var conn = builder.Configuration.GetConnectionString("DefaultConnection");

// EF Core
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(conn)
);

// OPTIONAL: register an IDbConnection factory for Dapper (Approach A)
builder.Services.AddScoped<System.Data.IDbConnection>(_ => new NpgsqlConnection(conn));

// register repositories, controllers, swagger...
builder.Services.AddScoped<IProductRepository, ProductRepository>();

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.MapControllers();
app.Run();
  

You can either inject IDbConnection (a new NpgsqlConnection per scope) for Dapper, or use the DbContext ’s connection (shown below) when you need transaction sharing. npgsql.org

6. Create the DB (migrations)

  
    dotnet ef migrations add InitialCreate
dotnet ef database update
  

(Use EF migrations for schema management — easier than hand-written SQL for most workflows.) npgsql.org

7. Repositories — two patterns

Pattern A — Dapper using its own IDbConnection (no shared transaction)

Good when you only need quick reads/writes and don't need EF & Dapper to share a single transaction.

Repositories/ProductDapperRepository.cs

  
    using Dapper;
using System.Data;

public class ProductDapperRepository : IProductRepository
{
    private readonly IDbConnection _db; // injected NpgsqlConnection (scoped)

    public ProductDapperRepository(IDbConnection db) => _db = db;

    public async Task<IEnumerable<Product>> GetAllAsync()
    {
        var sql = "SELECT id AS Id, name AS Name, price AS Price, categoryid AS CategoryId FROM products";
        if (_db.State != ConnectionState.Open) await _db.OpenAsync();
        return await _db.QueryAsync<Product>(sql);
    }

    public async Task<Product?> GetByIdAsync(int id)
    {
        var sql = "SELECT id, name, price, categoryid FROM products WHERE id = @Id";
        if (_db.State != ConnectionState.Open) await _db.OpenAsync();
        return await _db.QueryFirstOrDefaultAsync<Product>(sql, new { Id = id });
    }

    // Create/Update/Delete can use ExecuteAsync / ExecuteScalarAsync etc.
}
  

This is the simplest pattern and performs well for read-heavy, SQL-optimized queries.

Pattern B — Use EF DbContext's connection so EF & Dapper share the same connection/transaction

Use this when you need both EF and Dapper operations in the same transaction (commit/rollback together).

Repositories/ProductCombinedRepository.cs

  
    using Dapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using System.Data;

public class ProductCombinedRepository
{
    private readonly AppDbContext _db;

    public ProductCombinedRepository(AppDbContext db) => _db = db;

    // Dapper read using DbContext's connection (don't dispose the connection EF owns)
    public async Task<IEnumerable<Product>> GetAllWithDapperAsync()
    {
        var conn = _db.Database.GetDbConnection(); // returns DbConnection
        if (conn.State != ConnectionState.Open) await conn.OpenAsync();

        var sql = "SELECT id AS Id, name AS Name, price AS Price, categoryid AS CategoryId FROM products";
        // Dapper's QueryAsync accepts DbConnection since it implements IDbConnection
        return await conn.QueryAsync<Product>(sql);
    }

    // Example: an operation that uses Dapper + EF in one transaction
    public async Task<int> CreateProductAndLogAsync(Product product, string auditMessage)
    {
        // Start EF transaction
        await using var efTx = await _db.Database.BeginTransactionAsync();
        try
        {
            // 1) run a Dapper insert on the same connection/transaction
            var conn = _db.Database.GetDbConnection();
            if (conn.State != ConnectionState.Open) await conn.OpenAsync();

            var insertSql = @"
                INSERT INTO products (name, price, categoryid)
                VALUES (@Name, @Price, @CategoryId)
                RETURNING id;
            ";

            // Get underlying ADO.NET transaction to pass to Dapper
            var dbTransaction = efTx.GetDbTransaction(); // IDbTransaction
            var newId = await conn.ExecuteScalarAsync<int>(insertSql, product, transaction: dbTransaction);

            // 2) use EF Core to write to some tracked audit entity (or any other EF operation)
            var audit = new AuditLog { Message = auditMessage, ProductId = newId, CreatedAt = DateTime.UtcNow };
            _db.Add(audit);
            await _db.SaveChangesAsync();

            await efTx.CommitAsync();
            return newId;
        }
        catch
        {
            await efTx.RollbackAsync();
            throw;
        }
    }
}
  

Notes

8. Example controller (using combined repo)

Controllers/ProductsController.cs

  
    [ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ProductCombinedRepository _repo;

    public ProductsController(ProductCombinedRepository repo) => _repo = repo;

    [HttpGet]
    public async Task<IActionResult> GetAll() => Ok(await _repo.GetAllWithDapperAsync());

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] Product p)
    {
        var id = await _repo.CreateProductAndLogAsync(p, $"Created via API at {DateTime.UtcNow:O}");
        return CreatedAtAction(nameof(GetAll), new { id }, new { Id = id });
    }
}
  

9. Important best-practices & gotchas

10. Quick checklist / commands

References/reading