在.NET生态里,EF Core固然是官方主推的ORM框架,但不少开发者在面对复杂查询、多数据库切换、动态条件拼接这些场景时,还是会觉得写起来束手束脚。FreeSql作为一款国产开源的ORM框架,以流畅的链式API、极低的接入成本和丰富的功能特性赢得了大量用户。这篇文章将从零开始,带你完整走一遍c#中使用FreeSql的流程,并配上可以直接复用到项目里的代码。

一、FreeSql的安装与初始化配置
使用FreeSql的第一步是通过NuGet安装对应数据库的提供程序。FreeSql的核心包是FreeSql,但真正决定连接哪种数据库的是各个Provider包,比如FreeSql.Provider.MySql、FreeSql.Provider.SqlServer、FreeSql.Provider.Sqlite等。你只需要安装目标数据库的Provider包,它会自动带上核心依赖。
安装完成之后,最关键的一点是IFreeSql必须声明为单例。FreeSql内部带有表达式解析缓存、连接池管理,如果每次请求都new一个实例,不仅浪费内存,还会导致缓存失效、性能急剧下降。推荐在Program.cs中完成初始化,然后注册到依赖注入容器里。
var fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.MySql,
"Data Source=127.0.0.1;Port=3306;User ID=root;Password=123456;Database=testdb;Charset=utf8mb4;")
.UseAutoSyncStructure(true) // 自动同步实体结构到数据库,开发阶段好用,生产环境建议关闭
.UseMonitorCommand(cmd => Console.WriteLine(cmd.CommandText)) // 打印SQL,便于调试
.Build();
builder.Services.AddSingleton(fsql);上面的代码中,UseAutoSyncStructure是一个很贴心的功能,程序启动时会自动比对实体和表结构,缺表就建表,缺字段就加字段。但要注意,生产环境尽量不要开启,因为自动迁移可能带来意外的表结构变更风险,正式环境推荐使用CodeFirst比较工具或手写迁移脚本。
二、实体定义与CRUD基本操作
FreeSql的实体定义非常简单,一个普通的c#类加上特性标注就能映射到数据库表。常用的特性包括Table(指定表名)、Column(指定列名和类型)、key(主键自增)等。下面定义一个订单实体作为示例。
[FreeSql.DataAnnotations.Table(Name = "t_order")]
public class Order
{
[FreeSql.DataAnnotations.Column(IsPrimary = true, IsIdentity = true)]
public long Id { get; set; }
public string OrderNo { get; set; }
public decimal Amount { get; set; }
public DateTime CreateTime { get; set; }
public bool IsDeleted { get; set; }
}有了实体之后,增删改查都可以通过fsql.Repository或fsql.Insert、fsql.Select等方法完成。Repository模式适合带工作单元语义的业务逻辑,而直接使用IInsert、ISelect则更轻量灵活。看一组常见的写法。
// 插入:返回自增主键
var order = new Order { OrderNo = "SO202401001", Amount = 199.00m, CreateTime = DateTime.Now };
var id = fsql.Insert(order).ExecuteIdentity();
// 批量插入,内部会拆分成批量SQL,性能远高于循环单条插入
fsql.Insert(orders).ExecuteAffrows();
// 更新:只更新指定字段
fsql.Update<Order>(id).Set(o => o.Amount, 299.00m).ExecuteAffrows();
// 删除
fsql.Delete<Order>(id).ExecuteAffrows();
// 查询单条
var one = fsql.Select<Order>(id).First();这里有个细节值得注意:ExecuteAffrows返回的是受影响的行数,而ExecuteIdentity返回插入后的自增主键,两者适用的场景不同。批量插入时FreeSql会根据数据库类型自动优化成批量语句,比如MySQL的批量INSERT,几千条数据的插入速度比逐条执行快一个数量级以上。
三、条件查询、分页与动态筛选
实际业务中最常见的诉求是动态条件查询加分页。FreeSql的ISelect支持链式拼接Where,配合条件判断可以做到有值就过滤、没值就跳过,不需要像拼SQL字符串那样小心翼翼地处理and和1=1。
var query = fsql.Select<Order>();
if (!string.IsNullOrEmpty(keyword))
query = query.Where(o => o.OrderNo.Contains(keyword));
if (minAmount.HasValue)
query = query.Where(o => o.Amount >= minAmount.Value);
if (startTime.HasValue)
query = query.Where(o => o.CreateTime >= startTime.Value);
// 分页查询:总数与当前页数据一次拿到
var total = query.Count();
var list = query
.OrderByDescending(o => o.CreateTime)
.Page(pageIndex, pageSize)
.ToList();Page方法的第一个参数是页码从1开始,第二个是每页条数,底层会翻译成对应数据库的分页语法,比如MySQL的LIMIT OFFSET、SQL Server的OFFSET FETCH。如果你需要更高效的分页,还可以使用ToListAsync加上CountAsync的异步版本,避免阻塞线程。
除了常规查询,FreeSql还提供了很多实用函数,比如WhereIf方法可以直接把条件判断压缩进链式调用:query.WhereIf(!string.IsNullOrEmpty(keyword), o => o.OrderNo.Contains(keyword)),一行顶上面四行,代码可读性更好。聚合查询则可以使用WithSql或者直接调用Count、Sum等方法。
四、事务处理与导航属性实战
涉及多表写入的业务必须使用事务保证一致性。FreeSql的事务用法清晰,支持普通事务和嵌套传播式事务(配合UnitOfWorkManager实现类似Spring的@Transactional效果)。
using (var uow = fsql.CreateUnitOfWork())
{
var repo = uow.GetRepository<Order>();
var detailRepo = uow.GetRepository<OrderItem>();
var order = new Order { OrderNo = "SO202401002", Amount = 500m, CreateTime = DateTime.Now };
repo.Insert(order);
detailRepo.Insert(new List<OrderItem>
{
new OrderItem { OrderId = order.Id, Product = "键盘", Price = 300m },
new OrderItem { OrderId = order.Id, Product = "鼠标", Price = 200m }
});
uow.Commit(); // 任意一步抛异常,都不会提交
}导航属性是FreeSql的另一大亮点。在Order里配置一个OneToMany的Items属性后,查询订单时用Include(o => o.Items)就能一次把明细带出来,框架内部会自动生成关联查询或二次查询,不需要手写JOIN,也不需要像EF那样担心懒加载的N+1陷阱,因为FreeSql明确要求你显式Include,控制权完全在自己手里。
整体来看,FreeSql的学习成本很低,会写Lambda表达式就能上手,同时它还具备读写分离、分表分库、CodeFirst迁移等进阶能力,等基础用法熟悉之后再逐步深入这些特性即可。建议新项目可以先在某个模块试点接入,跑通初始化、CRUD、分页、事务这条完整链路,再决定是否全面推广。