前言:EFCore 到底是个啥

先记住一句话:

EFCore 就是一个翻译官,专门帮你把 C# 代码翻译成数据库能听懂的 SQL 语句。

你现在写程序,数据都是存在内存里的,程序一关就没了。做上位机、做数据采集,数据是要长期保存的,比如:

  • 设备台账(每台设备的编号、名称、型号)
  • 采集到的历史数据(温度、压力、产量)
  • 报警记录

这些数据都要存到数据库里。数据库只认一种语言,叫 SQL,长这样:

1
SELECT * FROM Products WHERE Price > 20;

这行 SQL 的意思是”把价格大于 20 的产品全查出来”。看着还行,但一旦查询变复杂,SQL 就很长、很难写、还容易拼错。

EFCore 干的事,就是让你不用写 SQL,直接用 C# 就能操作数据库。你写 C#,它在背后帮你翻译成 SQL 发给数据库。

打个比方:数据库是一台只会说”数据库黑话”的机器,EFCore 是站在你身边的翻译官。你说普通话(C#),它替你翻译(生成 SQL),再把结果拿回来给你。

用 EFCore 有三大好处:

  1. 不用手写 SQL 语句
  2. 用你熟悉的 C# 就能增删改查,写法跟操作 List 列表差不多
  3. 换数据库方便。你现在用 SQL Server,以后公司若换成 MySQL 或者其他数据库,代码基本不用改,只改一行配置就能切换

所以,学 EFCore 之前,先记住三句话:

  • EFCore 是连接 C# 程序和数据库的一座桥
  • 数据库里的一张表,在程序里对应一个”类”(叫模型类)
  • 操作数据库,就像操作一个 List 列表,记住最后要”保存”

接下来我们一步一步做,每一步你都能亲手跑起来,看到结果。


第零步:确认电脑准备好了

学 EFCore 需要三样东西:

  1. 装了 .NET SDK(你电脑上已经有了,版本是 9.0.308)
  2. 一个能写代码的工具(Visual Studio、VS Code、Rider 都行)
  3. 装了 SQL Server(你本机已经装好了,而且用 Windows 身份验证登录,下面的连接字符串都按这个情况来写)

先验证一下第一样,打开命令行(Windows 里按 Win 键,输入 cmd 回车,或者用 VS Code 自带的终端),输入:

1
dotnet --version

如果显示出一串版本号(比如 9.0.308),就说明 .NET 环境没问题。

SQL Server 你已经装在本机了,这一关算过。后面第四步写连接字符串的时候,会用到”本机”和”Windows 身份验证”这两个信息,你跟着写就行。


第一步:新建一个项目

我们从一个空的控制台项目开始,这样最干净,没有多余的东西干扰。

先找一个放代码的文件夹。比如就放在 D 盘,打开命令行,输入:

1
dotnet new console -n MyShop

这行命令的意思是:新建一个控制台项目,项目名叫 MyShop(商店的意思,因为我们后面要存”产品”数据)。

命令执行完,会出现一个叫 MyShop 的文件夹。进入它:

1
cd MyShop

现在先跑一下,确认项目是好的:

1
dotnet run

如果控制台输出 Hello, World! 就说明项目建成功了。

补充一句:dotnet new console 这个命令,-n 后面的名字你可以自己起,不一定要叫 MyShop。后面所有代码都假设你用了 MyShop 这个名字,如果你改了名,代码里的名字也要跟着改。


第二步:安装 EFCore 的包

EFCore 不是 .NET 自带的,需要我们从网上下载安装。因为我们要连 SQL Server,所以要装专门针对 SQL Server 的包。在命令行输入:

1
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.13

回车后会看到它下载安装的过程,最后出现 “已成功还原” 之类的提示就算好了。

这里要特别提醒两点:

第一,包名里的 SqlServer 不能错。EFCore 对不同数据库有不同的包,连 SQL Server 就装 Microsoft.EntityFrameworkCore.SqlServer。装错了(比如装成 Sqlite 的),后面会连不上。

第二,命令最后多出来的 --version 9.0.13 一定要加上,不能省。原因是你电脑装的是 .NET 9,如果不写版本号,它会自动去装最新版(现在是 10.x),而 10.x 要求 .NET 10 才能用,会直接报错装不上。加上 --version 9.0.13,就是指定装能和 .NET 9 配对的版本,前面的 9 正好对应你的 .NET 版本号。

这一行命令装的是什么?就是 EFCore 操作 SQL Server 数据库的零件。注意:装了它,就自动包含了 EFCore 的核心,不用再单独装别的包。

装完之后,项目文件夹里有个 MyShop.csproj 文件,用记事本打开看一眼,里面会多出类似这样的一行:

1
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.13" />

这就说明包装好了。


第三步:认识模型类,程序里的”表”

数据库里有一张张的”表”,比如产品表、设备表。在 EFCore 里,一张表对应一个 C# 类,这个类就叫”模型类”。

类里的每个属性,就是表里的一列。

我们先做一张”产品表”,保存产品信息。在 MyShop 文件夹里,新建一个文件,名字叫 Product.cs(用 VS 的话,右键项目,添加,类,起名 Product;用 VS Code 的话,直接右键新建文件输入名字)。

在 Product.cs 里写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace MyShop;

// 这是一个模型类,对应数据库里的"产品"表
public class Product
{
// Id 是主键,每条数据的唯一编号,EFCore 会自动处理
public int Id { get; set; }

// 产品名称
public string Name { get; set; } = "";

// 产品价格,decimal 是专门用来存钱的类型
public decimal Price { get; set; }

// 库存数量
public int Stock { get; set; }
}

逐行解释一下:

代码 意思
public class Product 定义一个类,叫 Product,它将来对应一张”产品”表
public int Id { get; set; } 属性 Id,是每条数据的编号,EFCore 看到名字是 Id,会自动把它当成主键
public string Name { get; set; } = “” 属性 Name,存产品名称。= “” 是先给个空字符串,避免出现黄色警告
public decimal Price { get; set; } 属性 Price,存价格。decimal 是存钱专用的类型,不会出现小数点误差
public int Stock { get; set; } 属性 Stock,存库存数量

先记住这个对应关系:

一个类,对应一张表;类的一个属性,对应表里的一列。

现在这个类还只是普通的 C# 类,它自己还不会变出表来。要让 EFCore 认识它,还需要下一步的”数据库上下文”。


第四步:认识数据库上下文,也就是翻译官

光有模型类还不够,我们得有一个”翻译官”来牵线。这个翻译官,在 EFCore 里叫”数据库上下文”,英文是 DbContext。

再新建一个文件,叫 AppDbContext.cs,写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Microsoft.EntityFrameworkCore;

namespace MyShop;

// 数据库上下文,就是翻译官,负责跟数据库打交道
public class AppDbContext : DbContext
{
// 这行表示:数据库里有一张"产品"表,对应 Product 类
public DbSet<Product> Products { get; set; }

// 这个方法告诉 EFCore:我们用哪个数据库
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// 连接本机的 SQL Server,数据库名叫 MyShop,用 Windows 身份验证登录
optionsBuilder.UseSqlServer("Server=localhost;Database=MyShop;Trusted_Connection=True;TrustServerCertificate=True;");
}
}

逐行解释:

代码 意思
public class AppDbContext : DbContext 定义类 AppDbContext,冒号后面表示”继承” DbContext。继承你可以先理解成”把它爹的本事全拿过来用”
public DbSet Products 声明一张表,表名叫 Products,它里面放的是 Product 类型的数据。以后操作这张表,就用 Products 这个名字
OnConfiguring 一个固定的方法,在里面告诉 EFCore 连哪个数据库
UseSqlServer(“…”) 用 SQL Server,括号里那一长串叫”连接字符串”,下面专门讲

连接字符串到底写了啥

SQL Server 和”一个文件就是一个数据库”那种数据库不一样。SQL Server 是一个一直在后台运行的服务,你的数据都存在这个服务里。所以要连上它,得告诉它三件事:服务器在哪、进哪个数据库、用谁的身份登录。

我们写的这串:

1
"Server=localhost;Database=MyShop;Trusted_Connection=True;TrustServerCertificate=True;"

拆开看:

部分 意思
Server=localhost 连本机的 SQL Server。localhost 就是”这台电脑自己”
Database=MyShop 进哪个数据库,这里叫 MyShop
Trusted_Connection=True 用 Windows 身份验证登录,也就是你说的”电脑开机账号直接登录,不用输密码”
TrustServerCertificate=True 信任本机服务器的证书。这句新手最容易漏,漏了会报证书错误

最后那句 TrustServerCertificate=True 要特别说一下:新版 .NET 默认要求加密连接,而本机 SQL Server 的证书是自己签的,系统不认,就会报错。加上这句,就是告诉程序”本机的证书我信了,别拦我”。

如果连不上,看这里

如果你运行时报错,说连不上服务器,多半是你装的是 SQL Server Express 版,它的实例名不是默认的。Express 版的实例名通常是 SQLEXPRESS,这时要把 Server=localhost 改成 Server=localhost\SQLEXPRESS。

注意,在 C# 代码里,反斜杠要写两个,写成:

1
optionsBuilder.UseSqlServer("Server=localhost\\SQLEXPRESS;Database=MyShop;Trusted_Connection=True;TrustServerCertificate=True;");

因为单个反斜杠在 C# 字符串里是特殊含义,写两个才代表一个真正的反斜杠。

那怎么确认自己到底要不要改?打开 SQL Server Management Studio(简称 SSMS)的登录窗口,看”服务器名称”那一栏默认显示的是什么。如果显示的是”计算机名”(不带反斜杠),就用 Server=localhost;如果显示的是”计算机名\SQLEXPRESS”这种带反斜杠的,就把连接字符串改成带 \SQLEXPRESS 的写法。

到这里,两个关键的东西都齐了:

  • Product:模型类,就是表的样子
  • AppDbContext:翻译官,负责建表、操作数据

第五步:让 EFCore 自动建出数据库

现在我们还没建数据库,也没建表。EFCore 可以帮你自动把数据库和表都建好,不用你手动去建。

打开 Program.cs,把里面原来的 Hello World 代码全删掉,换成:

1
2
3
4
5
6
7
8
9
10
using MyShop;

// 创建一个翻译官对象 db
using (var db = new AppDbContext())
{
// 让 EFCore 根据模型类,自动创建数据库和表
db.Database.EnsureCreated();

Console.WriteLine("数据库建好了");
}

解释一下这个代码:

代码 意思
new AppDbContext() 创建翻译官对象,起名 db
db.Database.EnsureCreated() 让 EFCore 检查 SQL Server 里有没有 MyShop 这个数据库,没有就自动创建,并且把表也建好
using (var db = …) 用完之后自动释放资源,先照抄,不影响理解

现在运行:

1
dotnet run

如果输出”数据库建好了”,恭喜你,数据库创建成功了。

怎么确认它真的建好了?打开 SSMS,连接本机,在左边展开”数据库”,你应该能看到一个叫 MyShop 的数据库,展开它,里面有一张 Products 表。

如果你现在不想开 SSMS 也没关系,直接进入第六步,用插入和查询代码,同样能验证数据真的存进去了。

到这里,你已经完成了最难的部分:把模型类、翻译官、数据库全部连起来了。接下来就是增删改查,反而简单。


第六步:往数据库里加数据(增)

存数据,在程序里就是:新建一个 Product 对象,塞给翻译官,然后”保存”。

把 Program.cs 改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using MyShop;

using (var db = new AppDbContext())
{
db.Database.EnsureCreated();

// 新建两个产品对象
var p1 = new Product { Name = "螺丝刀", Price = 15.5m, Stock = 100 };
var p2 = new Product { Name = "扳手", Price = 28m, Stock = 50 };

// 把两个对象加进"待保存"清单
db.Products.Add(p1);
db.Products.Add(p2);

// 真正写入数据库。这一步千万别漏
db.SaveChanges();

Console.WriteLine("两条数据存进去了");
}

运行:

1
dotnet run

输出”两条数据存进去了”就成功了。

这里有两个地方要重点记住:

第一,15.5m 这个 m 不能省。因为 Price 是 decimal 类型,而 C# 里直接写 15.5 默认是 double 类型,不匹配会报错。在小数后面加个 m,就是告诉 C#:这是 decimal。15.5m 读作”十五块五毛的 decimal”。

第二,SaveChanges() 千万不能忘。db.Products.Add(p1) 只是把 p1 记在”待保存清单”上,还没真正写进数据库。只有调用了 SaveChanges(),数据才真正落盘。忘了这行,程序跑完你会发现数据库里啥都没有。

把这三步连起来记:

Add(加入待保存清单)加 SaveChanges(真正写入)等于数据入库。


第七步:把数据读出来(查)

查数据就是把表里的数据读成 C# 对象,然后打印。

把 Program.cs 改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using MyShop;

using (var db = new AppDbContext())
{
db.Database.EnsureCreated();

// 把产品表里的所有数据读成一个 List
var list = db.Products.ToList();

// 一条一条打印出来
foreach (var p in list)
{
Console.WriteLine("编号:" + p.Id + ",名称:" + p.Name + ",价格:" + p.Price + ",库存:" + p.Stock);
}
}

运行:

1
dotnet run

你会看到上一节存进去的两条数据被打印出来了。

解释一下:db.Products.ToList() 的意思是”把产品表里的所有数据,读成一个 List 集合”。读出来之后,Products 里的每个元素就是一个 Product 对象,你就可以像操作普通 List 一样用 foreach 遍历它。

除了查全部,还有两种常用的查法。

按主键编号查一条,用 Find:

1
2
3
4
5
6
7
8
9
10
11
using MyShop;

using (var db = new AppDbContext())
{
db.Database.EnsureCreated();

// 查编号为 1 的那条数据
var p = db.Products.Find(1);

Console.WriteLine("查到了:" + p.Name + ",价格:" + p.Price);
}

按条件查,用 Where。这里就用到了你之前学过的 Lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using MyShop;

using (var db = new AppDbContext())
{
db.Database.EnsureCreated();

// 查出所有价格大于 20 的产品
var expensive = db.Products.Where(p => p.Price > 20).ToList();

foreach (var p in expensive)
{
Console.WriteLine("价格大于20的有:" + p.Name);
}
}

p => p.Price > 20 读作:对每条数据 p,判断它的价格是否大于 20。Where 会把满足条件的都挑出来。你看,之前学的 Lambda 在这里直接派上用场了。

记住三种查法:

写法 作用
db.Products.ToList() 查全部
db.Products.Find(编号) 按主键编号查一条
db.Products.Where(条件).ToList() 按条件查一批

第八步:改数据

改数据的套路是:先查出来,改它的属性,再保存。

把 Program.cs 改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using MyShop;

using (var db = new AppDbContext())
{
db.Database.EnsureCreated();

// 先查出编号为 1 的产品
var p = db.Products.Find(1);

// 修改它的价格
p.Price = 18m;

// 保存,改动才会真正写进数据库
db.SaveChanges();

Console.WriteLine("改好了,现在的价格是:" + p.Price);
}

运行:

1
dotnet run

输出”改好了,现在的价格是:18”就成功了。你可以用第七步的查询代码再跑一遍,验证一下价格真的变成了 18。

顺便说一句:运行这一步时,控制台可能会冒出几条黄色警告,内容大概是”可能出现空引用”。这是正常的,不影响结果,你先把程序跑通就行,不用管它。为什么会有这个警告,第十步的坑四会讲清楚。

改数据就这么简单,三步:查出来、改属性、SaveChanges。不用写任何 SQL 的 UPDATE 语句。


第九步:删数据

删数据的套路和改差不多:先查出来,再删,再保存。

把 Program.cs 改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using MyShop;

using (var db = new AppDbContext())
{
db.Database.EnsureCreated();

// 先查出编号为 2 的产品
var p = db.Products.Find(2);

// 从表里删掉它
db.Products.Remove(p);

// 保存
db.SaveChanges();

Console.WriteLine("删掉了");
}

运行:

1
dotnet run

输出”删掉了”就成功。再用查询代码跑一遍,你会发现编号 2 的”扳手”已经不见了。

这一步运行也会和改数据一样,冒出几条黄色警告,不用管,原因见坑四。

到这里,增删改查四个动作你就全部学会了。总结成一张表:

动作 核心代码 说明
db.Products.Add(对象); db.SaveChanges(); 先加入清单,再保存
db.Products.ToList() / Find(编号) / Where(条件) 读成 C# 对象
查到对象,改属性,db.SaveChanges() 改完要保存
db.Products.Remove(对象); db.SaveChanges() 删完要保存

看出来了吗?规律很简单:除了”查”不用保存,其余增、改、删,最后都要 SaveChanges()。


第十步:几个容易踩的坑

坑一:忘了 SaveChanges

这是新手最常犯的错。Add 了、改了、删了,结果数据库里没变化,十有八九是忘了 SaveChanges。

记住:Add、改属性、Remove 都只是改动内存里的对象,只有 SaveChanges() 才把改动真正写进数据库。

坑二:小数没加 m

1
2
3
4
5
// 错误:15.5 默认是 double,赋给 decimal 会报错
var p = new Product { Name = "螺丝刀", Price = 15.5, Stock = 100 };

// 正确:加 m 表示 decimal
var p = new Product { Name = "螺丝刀", Price = 15.5m, Stock = 100 };

坑三:改了模型类,旧数据库没跟着变

比如你后来给 Product 类加了一个属性,再跑程序会发现报错,说表里没这列。这是因为 SQL Server 里的 MyShop 数据库还是老结构,EnsureCreated 不会去改已经存在的库和表。

入门阶段最简单的解决办法:把旧的 MyShop 数据库删掉,再重新 dotnet run,让 EFCore 重新建一个。删除方法:打开 SSMS,连接本机,在左边找到 MyShop 数据库,右键删除;或者新建一个查询,执行下面这条 SQL:

1
DROP DATABASE MyShop;

(正式项目里,改数据库结构用的是”迁移”Migration,不用删库重建。那是进阶内容,等入门熟了再说。)

坑四:对空表用 Find 或 First 会报错

1
2
var p = db.Products.Find(1); // 如果表里没有编号 1 的数据,p 会是 null
Console.WriteLine(p.Name); // 这里就会报错

如果表里没有对应数据,Find 返回的是 null(空),再往下用 p.Name 就会崩。所以用之前最好先判断一下:

1
2
3
4
5
6
7
8
9
var p = db.Products.Find(1);
if (p != null)
{
Console.WriteLine(p.Name);
}
else
{
Console.WriteLine("没找到这条数据");
}

其实第八步、第九步运行时看到的那些黄色警告,根源就是这里:Find 有可能返回 null,编译器在提醒你小心。因为我们是先存了数据再去查,肯定查得到,所以那些警告可以放心忽略。以后写正式程序,建议都像上面这样先判断 p 是不是 null 再用。

坑五:字符串属性有黄色警告

如果 Product 类里的 Name 写成 public string Name { get; set; },编译器可能给你一条黄色警告,说这个属性可能为空。不影响运行,但看着烦。加上 = "" 给它个默认空字符串就好了。

坑六:连不上服务器,多半是实例名不对

这是 SQL Server 特有的坑。运行时报错说连不上服务器,最常见的原因有两个:

第一个,实例名不对。如果是 Express 版,实例名通常是 SQLEXPRESS,连接字符串里要写成 Server=localhost\SQLEXPRESS(注意两个反斜杠),而不是 Server=localhost。怎么确认,回第四步的”如果连不上,看这里”。

第二个,忘了写 TrustServerCertificate=True。漏了这句,会报一个和证书有关的错。把它加上就好。


小结

把今天学的东西串起来回顾一遍:

  1. EFCore 是连接 C# 程序和数据库的翻译官
  2. 一个模型类(比如 Product)对应数据库里的一张表
  3. 数据库上下文 AppDbContext 是翻译官,负责建表、操作数据
  4. 连接字符串告诉 EFCore:服务器在哪、进哪个库、用谁登录
  5. EnsureCreated 帮你自动建库建表
  6. 增删改查的套路:查出来、改对象、最后 SaveChanges

用一句话记住整个 EFCore 的入门核心:

把表当成类,把数据当成对象,把操作当成 List,最后记得保存。


下一步可以学什么

入门这一关过了之后,有两条路可以往下走:

第一条路,学”迁移”Migration。正式项目里,数据库结构会不断改,不能每次都删库重建。迁移能帮你安全地升级数据库结构,是实际工作中一定会用到的。

第二条路,学账号密码登录和远程连接。现在你用的是本机的 Windows 身份验证。以后连公司的 SQL Server,往往要改成用账号密码登录,连接字符串里的 Server 也要写成公司服务器的地址。这个等你有具体服务器了再学也不迟。

建议先反复把今天这几步多跑几遍,把增删改查写熟了,再往这两条路走。


练习题

自己动手写一写,写出来才算真的会。每道题都建一个新项目,或者接着用 MyShop 改。

练习一

定义一个”设备”模型类 Device,属性有:Id(编号)、DeviceName(设备名)、Temperature(温度,double 类型)、IsOnline(是否在线,bool 类型)。建一个对应的 DbContext,用 EnsureCreated 建出数据库。

练习二

往设备表里存两条数据,比如”一号风机,温度 65,在线”和”二号水泵,温度 40,离线”。存进去之后,用查询把两条都打印出来。

练习三

把二号水泵改成在线状态,再改一下它的温度,然后保存。查一遍验证改动生效了。

练习四

删掉其中一台设备,再查一遍,确认它没了。

练习五

用 Where 和 Lambda,挑出所有温度大于 50 的设备,打印出来。(提示:和价格大于 20 的写法一样,把 Price 换成 Temperature 就行。)


建议:把每一步的代码都亲手敲一遍,不要复制粘贴,敲完跑一次看看输出。EFCore 这东西,看十遍不如自己敲一遍。等你把增删改查写顺手了,以后做数据看板、做设备台账,脑子里就有底了。