从零理解 EF Core:用 Minimal API 给「工作项看板」建模(小白笔记)

不会 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。

工作流程可以记成三步:

  1. 写实体类(C# 业务模型)
  2. 写 DbContext(告诉 EF:有哪些表、实体之间怎么关联)
  3. 使用 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 件核心配置:

  1. 启用 Swagger(方便接口调试)
  2. 注册 MyBoardsContext(数据库上下文,交给依赖注入 DI)
  3. 配置 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 支持两种多对多写法:

  1. 只需要关联关系:EF 自动生成隐式中间表,不用手动写中间实体
  2. 中间表需要额外字段(时间、备注等) :必须手动创建显式中间实体 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
相关推荐
深圳老胡1 小时前
STM32 MCU 国产替代型号简介
笔记·stm32·单片机·代码规范
做cv的小昊3 小时前
【大模型算法自学笔记01】NLP基础知识(1.1 自注意力)
人工智能·笔记·算法·自然语言处理·大模型·llm
深圳老胡4 小时前
STM32 在 VSCode 下 -O0 与 -Os 编译条件的区别
笔记·stm32·单片机·代码规范
陈卫军老师13 小时前
陈卫军语录全集总结:12句话,一条主线
经验分享·笔记
2501_9269783317 小时前
给自指系统接一个外部锚 —— 一个关于「用 AI 观察自己」的方法
人工智能·经验分享·笔记·机器学习·ai写作
AOI小白新手上路21 小时前
anomalib 缺陷检测复现笔记:从跑通库到 EfficientAD 落地
人工智能·笔记·机器学习
xxwl58521 小时前
RabbitMQ 学习笔记
笔记·学习·rabbitmq
加油努力有饭吃1 天前
DLMS/COSEM 蓝皮书解读(二十六):Auto connect(class_id = 29)—— 自动拨号与主动连接(出站管理)
笔记
CarIise1 天前
Java实训阶段查漏补缺复习笔记1
java·开发语言·笔记