I’ll show two approaches you can choose from:
Approach A (recommended for most reads/writes): Use EF Core for schema, migrations, and most CRUD; use Dapper (Npgsql) for fast raw/complex queries. (Dapper uses its own IDbConnection.)
Approach B (when you need a single transaction across EF + Dapper): Reuse the
DbConnection/transaction insideDbContextso Dapper executes on the same connection/transaction as EF Core.
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.
}
Comments
Join the conversation! Your thoughts help the community grow.