在.NET 6之后,Minimal API成为构建轻量级HTTP服务的主流方式,而EF Core作为微软官方ORM,在Minimal API里并不需要控制器也能完成完整的增删改查。核心思路是把DbContext注册为服务,然后在路由委托中直接声明参数来获取实例。
一、基础集成:注册与注入
第一步是在Program.cs中用AddDbContext把上下文加入到依赖注入容器。这里推荐使用连接字符串配置,并从appsettings.json读取,避免硬编码。注册完成后,上下文的生命周期默认是Scoped,也就是每个请求一个实例,正好匹配Minimal API的请求处理方式。
下面代码展示了一个最基础的注册与查询接口。我们在MapGet的委托里直接写AppDbContext ctx作为参数,框架会自动从当前请求作用域解析它。这种方式比构造函数注入更直观,也更符合Minimal API极简的风格。
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlite(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
app.MapGet("/users", async (AppDbContext ctx) =>
{
var list = await ctx.Users.ToListAsync();
return Results.Ok(list);
});
app.Run();
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
上述写法在单请求内完全没问题,但当我们需要在后台任务或并行查询里使用上下文时,Scoped生命周期会带来麻烦。因为每个DbContext都不是线程安全的,跨线程共享会抛出异常。
二、使用AddDbContextFactory应对并行场景
如果接口内部要并发访问数据库,例如同时查两张表再聚合,就应该用AddDbContextFactory代替AddDbContext。工厂模式允许你在任意地方创建独立的上下文实例,而不依赖请求作用域。每个using块里的上下文都是全新且线程隔离的。
下面的示例演示了在Minimal API中通过工厂并行查询。注意我们使用CreateDbContext方法,并在using中释放。这样即使接口逻辑复杂,也不会因为作用域错乱导致“无法解析Scoped服务”的报错。
builder.Services.AddDbContextFactory<AppDbContext>(opt =>
opt.UseSqlite(builder.Configuration.GetConnectionString("Default")));
app.MapGet("/stats", async (IDbContextFactory<AppDbContext> factory) =>
{
using var ctx1 = factory.CreateDbContext();
using var ctx2 = factory.CreateDbContext();
var task1 = ctx1.Users.CountAsync();
var task2 = ctx2.Users.Where(u => u.Name.StartsWith("A")).CountAsync();
await Task.WhenAll(task1, task2);
return Results.Ok(new { Total = task1.Result, StartWithA = task2.Result });
});
工厂方式代价是你需要手动管理上下文生命周期,但换来的是更清晰的边界。对于报表类、聚合类接口,强烈建议采用这种写法。
三、写操作与事务处理
Minimal API里做新增或修改同样直接,在POST委托中拿上下文,构造实体后SaveChangesAsync即可。如果多个写操作要保证原子性,可以用上下文自带的Database.BeginTransactionAsync。事务在Minimal API中和普通控制台程序没有区别,只是作用域更短。
以下代码展示了一个带事务的批量插入接口。我们先开启事务,循环添加,再统一提交。若中途异常,事务回滚保证数据一致。注意在Minimal API中异常如果不捕获,框架会返回500,所以这里用try包裹更符合生产要求。
app.MapPost("/users/batch", async (AppDbContext ctx, List<User> input) =>
{
using var tx = await ctx.Database.BeginTransactionAsync();
try
{
foreach (var u in input)
{
ctx.Users.Add(u);
}
await ctx.SaveChangesAsync();
await tx.CommitAsync();
return Results.Ok("inserted " + input.Count);
}
catch (Exception ex)
{
await tx.RollbackAsync();
return Results.Problem(ex.Message);
}
});
写接口建议配合模型验证。可以在委托参数前加Claims或自定义过滤器,不过Minimal API通常用显式if判断,反而更直白。事务不要长期持有,避免锁表影响其他请求。
四、过滤、分页与投影
列表接口几乎都要分页。EF Core的Skip和Take配合Minimal API的query参数非常自然。我们从ctx里取IQueryable,再按前端传的page与size裁剪。投影用Select转成匿名类,能减少返回字段,提升网络效率。
下面例子接收page和size两个查询参数,返回分页数据与总数。用Select投影只取Id和Name,避免把整个User表行全量序列化。这种写法在用户量大的表里效果明显。
app.MapGet("/users/page", async (AppDbContext ctx, int page = 1, int size = 10) =>
{
if (page < 1) page = 1;
var query = ctx.Users.OrderBy(u => u.Id);
var total = await query.CountAsync();
var items = await query
.Skip((page - 1) * size)
.Take(size)
.Select(u => new { u.Id, u.Name })
.ToListAsync();
return Results.Ok(new { total, items });
});
分页查询里要小心先查全表再内存分页的错误写法,一定要让Skip和Take翻译成SQL。上面代码由于使用IQueryable链式调用,EF Core会生成带OFFSET FETCH的语句,性能可控。
五、常见误区与总结
新手常把DbContext当成单例在服务里缓存,结果遇到“上下文已被释放”或并发报错。记住EF Core上下文是轻量但非线程安全的,Minimal API里要么用Scoped注入,要么用工厂按需创建。另一个误区是在Map委托里写太长业务逻辑,这会让路由文件膨胀,建议把复杂逻辑抽到Service类,通过注入进委托。
总体看,EF Core与Minimal API集成非常顺滑:注册服务、注入上下文、写委托三步走。只要理清作用域与生命周期,就能用极少代码交付一套完整的数据库驱动API。
EF_CoreMinimal_API依赖注入修改时间:2026-08-05 22:54:44