不会 SQL 也能先把业务做成 C# 类?这篇用 MyBords 项目,按小白节奏讲清:Entity Framework 是什么、实体怎么写、主键 / 默认值怎么配,以及一对一、一对多、多对多到底怎么落地。适合刚接触 .NET 8 + EF Core 的同学收藏对照。
- 项目:MyBords(仿 Azure Boards 的工作项看板)
- 技术栈:.NET 8 Minimal API + EF Core + SQL Server LocalDB
- 本阶段目标:先把「实体、主键、默认值、三种关系」建模清楚,API 接口后面再写
一、什么是 Entity Framework?它在干什么?

写业务时我们面对的是 C# 类 (WorkItem、User),数据库里面对的是 表和字段。两边格式不一样,手写 SQL 很累,也容易出错。
Entity Framework Core(EF Core) 就是中间的「翻译官」:
| 你写的 C# 代码 | EF Core 自动帮你翻译成 |
|---|---|
WorkItem 类 |
WorkItems 数据表 |
workItem.State = "Done" |
UPDATE WorkItems SET State = 'Done'... |
context.WorkItems.Add(...) |
INSERT INTO ... |
一句话:用面向对象的方式操作数据库,少手写 SQL。
工作流程可以记成三步:
- 写实体类(C# 业务模型)
- 写
DbContext(告诉 EF:有哪些表、实体之间怎么关联) - 使用
DbContext做增删改查(EF 自动生成 SQL 发给数据库)
二、目标项目思路:我们要建什么?
MyBords 不是博客系统,而是一个工作项看板:
- 用户(
User)可以创建工作项(WorkItem) - 工作项可以有评论(
Comment)、标签(Tag) - 用户有一个地址(
Address)
关系简图(先画关系图,再写代码,防止外键写乱)
User ─────1:1───── Address
│
│ 1 : N
▼
WorkItem ─────1:N───── Comment
│
│ N : N(中间表 WorkItemTag)
▼
Tag
三、创建 Minimal API 项目
Minimal API 就是「少文件、少样板代码」的 Web API 写法,核心入口在 Program.cs。
本阶段我们做 3 件核心配置:
- 启用 Swagger(方便接口调试)
- 注册
MyBoardsContext(数据库上下文,交给依赖注入 DI) - 配置 SQL Server LocalDB 数据库连接
核心代码(Program.cs)
cs
// 把你的数据库上下文 `MyBoardsContext` 注册到[ASP.NET](https://ASP.NET) Core 的 DI 依赖注入容器
builder.Services.AddDbContext<MyBoardsContext>(option =>
option.UseSqlServer(
builder.Configuration.GetConnectionString("MyBoardsConnectionString")));

连接字符串放在 appsettings.json,指向 MyBordsDb。
cs
"ConnectionStrings": {
"MyBoardsConnectionString": "Server=(localdb)\\mssqllocaldb;Database=MyBordsDb;Trusted_Connection=True;"
},

✅ 当前阶段重点:模型 + 实体关系配置优先 ,业务接口 MapGet/MapPost 可以后面再写。
四、数据库实体创建(Entity)
实体 = 一张数据库表的「C# 代码形式」。
工作项实体
cs
namespace MyBords.Entities
{
public class WorkItem
{
public int Id { get; set; }
public string State { get; set; }
public string Area { get; set; }
public string IterationPath { get; set; }
public int Priority { get; set; } = 0;
// 史诗(大需求,大模块)
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public decimal Efford { get; set; } // 预估工作量
public string Activity { get; set; } // 活动类型
public decimal RemainingWork { get; set; } // 剩余工作量
public string Type { get; set; } // 任务类型
public List<Comment> Comments { get; set; } = new List<Comment>();
public User Author { get; set; }
public Guid AuthorId { get; set; }
public List<Tag> Tags { get; set; }
}
}
注意点:
- 普通属性:映射到数据库表的列
- 导航属性 (如
Author、Comments):用来表达实体之间的关系,不会单独生成数据库列 AuthorId:外键字段 ,存在数据库里;Author:导航属性,用来直接拿到关联的 User 对象
用户实体
cs
namespace MyBords.Entities
{
public class User
{
// Guid 类型的 Id 属性,表示用户的唯一标识符
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public Address Address { get; set; }
public List<WorkItem> WorkItems { get; set; } = new List<WorkItem>();
}
}
Tag实体
cs
namespace MyBords.Entities
{
public class Tag
{
public int Id { get; set; }
public string Value { get; set; }
public List<WorkItem> WorkItems { get; set; }
}
}
评论实体
cs
namespace MyBords.Entities
{
public class Comment
{
public int Id { get; set; }
public string Message { get; set; }
public string Author { get; set; }
public DateTime CreateDate { get; set; }
public DateTime? UpdateDate { get; set; }
// 导航属性,表示评论所属的工作项
public WorkItem WorkItem { get; set; }
// 外键属性,表示评论所属的工作项的Id
public int WorkItemId { get; set; }
}
}
中间表实体
cs
namespace MyBords.Entities
{
// 关联表,表示工作项和标签之间的多对多关系
public class WorkItemTag
{
public WorkItem WorkItem { get; set; }
public int WorkItemId { get; set; }
public Tag Tag { get; set; }
public int TagId { get; set; }
// 发布日期
public DateTime PublicationDate { get; set; }
}
}
实体清单:WorkItem、User、Comment、Address、Tag、WorkItemTag(多对多中间表)
五、DbContext 与配置
DbContext 理解成「数据库会话入口 / 数据库管家」:
DbSet<T>:告诉 EF,这个 C# 类对应数据库一张表OnModelCreating:重写这个方法,用 Fluent API 写详细规则(必填、字段长度、实体关系、默认值等)
cs
public class MyBoardsContext : DbContext
{
public DbSet<WorkItem> WorkItems { get; set; }
public DbSet<Comment> Comments { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 在这里集中配置实体规则、表关系
}
}
全部代码
cs
using Microsoft.EntityFrameworkCore;
namespace MyBords.Entities
{
public class MyBoardsContext : DbContext
{
//构造函数
public MyBoardsContext(DbContextOptions<MyBoardsContext> options) : base(options)
{
}
public DbSet<WorkItem> WorkItems { get; set; }
public DbSet<Comment> Comments { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Address> Addresses { get; set; }
// 模型创建时的配置 重写父类DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<WorkItem>(eb =>
{
eb.Property(x => x.State)
.IsRequired();
eb.Property(x => x.Area)
.HasColumnType("nvarchar(100)");
eb.HasMany(x => x.Comments)
.WithOne(x => x.WorkItem)
.HasForeignKey(x => x.WorkItemId);
eb.HasOne(x => x.Author)
.WithMany(x => x.WorkItems)
.HasForeignKey(x => x.AuthorId);
eb.HasMany(x => x.Tags)
.WithMany(x => x.WorkItems)
.UsingEntity<WorkItemTag>(
// 参数1:第一个lambda:配置中间实体 ↔ Tag 的关系
w => w.HasOne(wt => wt.Tag)
.WithMany()
.HasForeignKey(wt => wt.TagId),
// 参数2:第二个lambda:配置中间实体 ↔ WorkItem 的关系
w => w.HasOne(wt => wt.WorkItem)
.WithMany()
.HasForeignKey(wt => wt.WorkItemId),
// 参数3:第三个lambda:配置中间实体本身(主键、字段默认值、字段类型等)
w =>
{
w.HasKey(wt => new { wt.TagId, wt.WorkItemId });
w.Property(wt => wt.PublicationDate)
.HasDefaultValueSql("getutcdate()");
});
});
modelBuilder.Entity<Comment>(eb =>
{
eb.Property(x => x.CreateDate).HasDefaultValueSql("getutcdate()");
eb.Property(x => x.UpdateDate).ValueGeneratedOnUpdate();
});
modelBuilder.Entity<User>()
.HasOne(u => u.Address)
.WithOne(a => a.User)
.HasForeignKey<Address>(a => a.UserId);
}
}
}
记忆口诀:
实体类 = 表结构草稿 DbContext = 正式说明书(有哪些表、表之间怎么关联、字段约束)
六、主键(Primary Key)
主键用来唯一标识一行数据,相当于这条记录的身份证。
项目里 3 种主键写法:
| 主键类型 | 示例 | 说明 |
|---|---|---|
| 自增 int | WorkItem.Id、Comment.Id、Tag.Id |
EF 默认约定:属性名叫Id自动识别为主键 |
| Guid | User.Id、Address.Id |
适合分布式系统,ID 不容易被猜测 |
| 复合主键 | WorkItemTag 的 { TagId, WorkItemId } |
多对多中间表常用,代表「两个字段组合不能重复」 |
中间表复合主键配置代码:
cs
w.HasKey(wt => new { wt.TagId, wt.WorkItemId });
主键 = 这行数据的身份证号;没有主键,EF / 数据库无法精准更新、删除某一行。
七、实体配置(Fluent API)
EF 有默认约定,简单场景可以自动识别;复杂规则,统一写在OnModelCreating里。
项目典型配置示例:
cs
eb.Property(x => x.State).IsRequired(); // 字段必填(NOT NULL)
eb.Property(x => x.Area).HasColumnType("nvarchar(100)"); // 指定数据库字段类型与长度
实体之间的关系,同样使用 FluentAPI:HasMany / WithOne / HasForeignKey。
两种配置方式对比:
| 配置方式 | 优点 | 缺点 |
|---|---|---|
数据注解 [Required] |
写在属性上方,一眼看到 | 实体类被特性注解 "污染" |
| Fluent API(本文使用) | 所有配置集中在 Context,实体干净 | 需要到OnModelCreating中查找配置 |
八、默认值(Default Value)
定义:新增数据时,如果代码没有手动赋值,数据库自动填充预设的值。
1)C# 代码层默认值(应用程序侧)
cs
public int Priority { get; set; } = 0;
2)数据库层默认值(推荐,数据库侧兜底)
cs
// 评论创建时间,新增记录自动填充UTC时间
eb.Property(x => x.CreateDate).HasDefaultValueSql("getutcdate()");
// 工作项打标签的时间,新增关联记录自动填充
w.Property(wt => wt.PublicationDate).HasDefaultValueSql("getutcdate()");
九、一对一关系(1 : 1)
业务例子:一个用户对应一个地址
实体代码:
cs
// User实体
public Address Address { get; set; }
// Address实体
public User User { get; set; }
public Guid UserId { get; set; }
FluentAPI 配置
cs
modelBuilder.Entity<User>()
.HasOne(u => u.Address)
.WithOne(a => a.User)
.HasForeignKey<Address>(a => a.UserId);
HasOne+WithOne= 一对一- 外键放在依赖方 (这里放在 Address 表,
Address.UserId)
生活类比:一个用户登记一个收货地址;该地址只属于这一个用户。
十、一对多关系(1 : N)
项目两处典型一对多场景:
场景 1:一个用户 → 多个工作项
cs
eb.HasOne(x => x.Author)
.WithMany(x => x.WorkItems)
.HasForeignKey(x => x.AuthorId);
- 一方:
User(1) - 多方:
WorkItem(N),外键AuthorId写在 WorkItem
场景 2:一个工作项 → 多条评论
cs
eb.HasMany(x => x.Comments)
.WithOne(x => x.WorkItem)
.HasForeignKey(x => x.WorkItemId);
小白口诀:
HasMany+WithOne= 一对多- 外键永远放在「多」的那一张表里
十一、多对多关系(N : N)
业务例子:工作项和标签
一个工作项可以打多个标签;一个标签,可以贴在多个不同工作项上。
EF Core 支持两种多对多写法:
- 只需要关联关系:EF 自动生成隐式中间表,不用手动写中间实体
- 中间表需要额外字段(时间、备注等) :必须手动创建显式中间实体
WorkItemTag
我们的场景:需要记录打标签的时间 PublicationDate,所以使用显式中间实体。
中间实体代码:
cs
public class WorkItemTag
{
public int WorkItemId { get; set; }
public WorkItem WorkItem { get; set; }
public int TagId { get; set; }
public Tag Tag { get; set; }
public DateTime PublicationDate { get; set; }
}
多对多核心配置:
cs
eb.HasMany(x => x.Tags)
.WithMany(x => x.WorkItems)
.UsingEntity<WorkItemTag>(
// ① 中间实体关联Tag
w => w.HasOne(wt => wt.Tag)
.WithMany()
.HasForeignKey(wt => wt.TagId),
// ② 中间实体关联WorkItem
w => w.HasOne(wt => wt.WorkItem)
.WithMany()
.HasForeignKey(wt => wt.WorkItemId),
// ③ 中间实体自身配置(主键、字段默认值)
w =>
{
w.HasKey(wt => new { wt.TagId, wt.WorkItemId });
w.Property(wt => wt.PublicationDate)
.HasDefaultValueSql("getutcdate()");
}
);
💡重点:UsingEntity 接收3 个独立 lambda 参数,逗号分隔,不能全部塞进同一个大括号,这是新手高频踩坑点!
判断标准:
| 业务需求 | 实现方案 |
|---|---|
| 仅需要两个表关联,中间无额外信息 | 隐式中间表,不用新建实体 |
| 中间还要保存额外字段(时间、备注等) | 显式中间实体 + UsingEntity |