惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Fortinet All Blogs
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
小众软件
小众软件
有赞技术团队
有赞技术团队
雷峰网
雷峰网
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
AWS News Blog
AWS News Blog
C
Cisco Blogs
美团技术团队
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
W
WeLiveSecurity
D
DataBreaches.Net
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
T
The Blog of Author Tim Ferriss
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Vercel News
Vercel News
月光博客
月光博客
T
Tailwind CSS Blog
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
P
Proofpoint News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Spread Privacy
Spread Privacy
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
V
V2EX
WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
Recent Announcements
Recent Announcements

博客园 - 东邪独孤

【ASP.NET Core】在 Windows Forms 中运行 Web 服务器 【EF Core】级联删除行为 【EF Core】继承策略——TPC 【EF Core】继承策略——TPT 【EF Core】继承策略——TPH 【EF Core】使用自定义的值比较器 【EF Core】值转换器 【EF Core】直接更新数据 【EF Core】实体追踪——Entry中记录的数据 【EF Core】实体状态与变更追踪 【EF Core】将一个实体映射到多个表的正确方法 【EF Core】“Code First”方案下以编程方式生成迁移 【EF Core】“DB First”方案下用编程方式生成数据库模型代码 【EF Core】三种方法记录生成的 SQL 语句 【EF Core】未定义实体类的数据库模型 【EF Core】“多对多”关系与跳跃导航 【EF Core】FromExpression 方法有什么用? 【EF Core】通过 DbContext 选项扩展框架 【EF Core】框架底层的数据库连接管理 【EF Core】再谈普通实体关系与 Owned 关系的区别 【EF Core】实体类的依赖注入 【EF Core】使用外部 Model 【EF Core】聊聊“复合”属性 【EF Core】为 DatabaseFacade 扩展“创建”与“删除”数据表功能 【EF Core】带主键实体与无主键实体 【EF Core】框架是如何识别实体类的属性和主键的
【EF Core】优化后的模型
东邪独孤 · 2025-09-01 · via 博客园 - 东邪独孤

本文所讲述内容,大伙伴们不必要完全掌握,毕竟,dotnet ef 工具会帮助咱们生成相关代码。不过,为了让各位能够真正了解它,老周会做一次纯手搓代码。

所谓优化后的模型,相当于把实体的配置进行“硬编码”,程序会执行更少的代码来提升效率。放到实际代码编写上,这个活叫运行时模型,对应的类就是 RuntimeModel。与运行时模型相关的类型都是以“Runtime”开头的,对应着模型各个部分。例如:

1、实体—— RuntimeEntityType 类;

2、属性—— RuntimeProperty 类;

3、主键—— RuntimeKey 类;

4、导航—— RuntimeNavigation 类。导航属性使用专门的类,而不归类于 RuntimeProperty;

5、外键—— RuntimeForeignKey 类;

6、索引—— RuntimeIndex 类;

7、其他,这里就不全列出了。

有大伙伴可能会想:都定义实体类了,为什么还要构建 Model 呢?因为这体类本身无法提供额外的信息,比如谁是主键,映射到哪个表哪个列,有哪些列是索引……而且,ef core 还要跟踪每个实体对象的状态,以便在必要时更新数据库。有太多的附加信息要处理,所以得有专门的类型去封装这些信息。

除了上面列出的专用运行时类型,EF Core 框架还用到另一个重要的东西去描述实体——Annotations(批注,或者叫注释)。EF Core 框架还定义了一个 Annotation 类,专门保存批注信息。批注集合是一个字典,故一条批注是以 Name-Value 的形式存在的。Name 是字符串,Value 是任意类型。咱们在代码中能访问到的常用名称由 RelationalAnnotationNames 公开,如 TableName 字段可以返回表示表名称的字符串。另外,不同数据库提供者也可以公开专用的批注名称,如 SQL Server 数据库公开了 SqlServerAnnotationNames,不过,此类位于 Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal 命名空间,帮助文档上是查不到的,属于留给内部使用的。当然,咱们在代码中是可以访问的。

批注名称是用冒号来分隔层次的,有点像 ASP.NET Core 的配置树。比如,表示 SQL Server 标识列的批注是这样的:SqlServer:Identity,它的值是字符串,格式为 1,1 表示从1开始,增量为1。如果从1000开始增量为5,就是 1000,5(用逗号分隔)。

现在说回 RuntimeModel 类。实例化后,通过对应的方法调用来添加模型:

1、AddEntityType 方法,向模型添加实体,返回 RuntimeEntityType 实例;

2、调用 RuntimeEntityType 实例的 AddProperty 方法添加属性,返回 RuntimeProperty 实例;

3、调用 RuntimeEntityType 实例的 AddKey 方法创建主键,返回 RuntimeKey 实例,随后要调用 SetPrimaryKey 方法设置主键;调用 AddForeignKey 方法添加外键,返回 RuntimeForeignKey 实例。

……

总结一下思路:创模型,添实体,加属性,设主键,置外键,最后补批注

上面就是本文所需的知识,运行时模型在建立时所调用的方法成员参数较多,而且不是所有参数都会用到,一般建议写上参数名。用命名参数就不必按顺序传参。这些参数等下面实际演练时再解释。

===============================================================================================

好了,理论上准备得差不多,接下来老周将纯手动打造一个运行时模型。由于运行时模型是不能创建数据库和表的,所以咱们采用 DB First 方案,先创建数据库。本次老周使用 SQL Server,使用以下脚本创建数据库(建议把脚本存为文件,后面写代码时可以参考):

USE master;
GO

-- 创建数据库
CREATE DATABASE ToyDB;
GO
USE ToyDB;
GO

-- 创建表
CREATE TABLE dbo.tb_boys
(
    boy_id INT IDENTITY NOT NULL,
    [name] NVARCHAR(20) NOT NULL,
    [nick] NVARCHAR(20) NULL,
    CONSTRAINT [PK_boy_id] PRIMARY KEY (boy_id ASC)
);
GO

CREATE TABLE dbo.tb_toys
(
    toy_id INT IDENTITY(100, 1) NOT NULL,
    toy_desc NVARCHAR(50) NOT NULL,
    toy_cate NVARCHAR(10) NOT NULL,
    f_boy_id INT NULL,      -- 外键
    CONSTRAINT [PK_toy_id] PRIMARY KEY (toy_id ASC),
    CONSTRAINT [FK_toy_boy] FOREIGN KEY (f_boy_id) REFERENCES dbo.tb_boys (boy_id) ON DELETE CASCADE
);
GO

tb_boys 表代表某男孩,tb_toys 表代表他拥有的玩具,每个 boy 在小时候都有自己喜欢的 toy,正如每个进城的农民工都怀揣着梦想一般;随着 boy 们的长大,许多 toy 逐渐被淡忘;随着时光的流逝,农民工们的梦已被风雨打碎,留下千丝万缕的遗恨与怅惘。

咱们程序中定义的实体类与数据表的名称是不相同的,所以后期要做映射。

public class Boy
{
    public int ID { get; set; }
    public string Name { get; set; } = string.Empty;
    public string? Nick { get; set; }

    // 导航属性
    public ICollection<Toy> MyToys { get; set; } = new List<Toy>();
}

public class Toy
{
    public int ToyID { get; set; }
    public string Desc { get; set; } = string.Empty;
    public string Cate { get; set; } = "经典玩具";

    // 导航属性的反向引用,外键隐藏
    public Boy? TheBoy { get; set; }
}

两个类都有导航属性,每个 Boy 的童年不可能只有一件玩具的,每件玩具专属于喜欢它的主人,因此这是一对多的关系。

好了,下面是重头戏,看看怎么构建 RuntimeModel。

1、实例化模型。

RuntimeModel model = new(
    skipDetectChanges: false,   // 开启更改跟踪
    modelId: Guid.NewGuid(),    // 模型ID
    entityTypeCount: 2,         // 模型中有两个实体类型
    typeConfigurationCount: 0   // 未使用自定义类型转换
);

skipDetectChanges:99%选 false,因为我们希望跟踪模型的变更。modelId:模型的ID,GUID类型,随便分配,不会重复的。entityTypeCount:这个模型包含几个实体类型,这里明确指出(属于是硬编码了)。typeConfigurationCount:保留默认0即可,实体的类型映射,这里咱们用不上,98%的情形也是用不上的。

2、添加 Boy 实体。

var boyEnt = model.AddEntityType(
    name: "DB.Boy",     // 一般包含命名空间
    type: typeof(Boy),  // CLR 类型
    baseType: null,     // 没有基类(指除Object以外)
    propertyCount: 3,   // 属性个数,不含导航属性
    navigationCount: 1, // 有一个导航属性
    keyCount: 1         // 有一个主键
);

name 表示实体名称,一般用实体类的完整类型名(含命名空间+类名)。type 表示实体的类型 Type。baseType:如果 Boy 有基类,要附上基类型。这里说的基类是除 Object 以外,此处,Boy 可以认为无基类型。sharedClrType = false,不是共享的实体类型(多个实体共用一个CLR类型)。propertyCount 表示此实体包含属性个数,应为3,不包括导航属性,导航属性有家族背景,需要特殊对待。navigationCount 表示包含多少个导航属性,这里是一个。keyCount 表示实体有几个主键,这里一个。

3、添加 Boy 实体的属性。注意,属性是加在 RuntimeEntity 中。

var boyIdProp = boyEnt.AddProperty(
    name: nameof(Boy.ID),   // 属性名
    clrType: typeof(int),   // 属性类型
    nullable: false,        // 不为null
                            // 引用CLR属性成员
    propertyInfo: typeof(Boy).GetProperty("ID", BindingFlags.
Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
    // 无字段成员
    fieldInfo: null,
    // 优先使用属性来设置值
    propertyAccessMode: PropertyAccessMode.PreferProperty,
    // 插入时生成值
    valueGenerated: ValueGenerated.OnAdd,
    // 实体存入数据库后不允许修改此属性
    afterSaveBehavior: PropertySaveBehavior.Throw,
    // 默认值0
    sentinel: 0
);
// SQL Server 值生成配置-标识列
boyIdProp.AddAnnotation(SqlServerAnnotationNames.
ValueGenerationStrategy, SqlServerValueGenerationStrategy.
IdentityColumn);
var nameProp = boyEnt.AddProperty(
    name: nameof(Boy.Name),
    clrType: typeof(string),
    nullable: false,
    propertyInfo: typeof(Boy).GetProperty("Name", BindingFlags.
Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
    maxLength: 20,          // 长度限制
    propertyAccessMode: PropertyAccessMode.PreferProperty
);
var nickProp = boyEnt.AddProperty(
    name: nameof(Boy.Nick),
    clrType: typeof(string),
    nullable: true,
    maxLength: 20,
    propertyInfo: typeof(Boy).GetProperty(nameof(Boy.Nick), 
BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.
Instance),
    propertyAccessMode: PropertyAccessMode.PreferProperty
);

name:属性名,一般与 CLR 类型的属性名相同。clrType:属性值的类型。propertyInfo:引用CLR属性的成员(通过反射来设置或读取值),设置值时要求用 set 访问器。fieldInfo:存储属性值的字段成员,没有的话也可以为 null。propertyAccessMode:表示 EF Core 如何访问属性,这是一个 PropertyAccessMode 枚举,主要值有:

A、Field:必须通过字段来读写属性。使用该方式,实体类的属性必须定义隐藏字段。

B、FieldDuringConstruction:只在实体实例化(调用构造器)时才通字段访问属性。

C、Property:直接读写属性本身。

D、PreferField:先尝试通过字段来读写属性,如果没有,直接访问属性。

E:PreferFieldDuringConstruction:仅在实例构造时,尝试通过字段访问属性,若不行改为直接访问属性。

F:PreferProperty:通过属性直接访问。如果没有属性,或 get/set 不存在(通常不会这样),就改为通过字段读写。

上述枚举值要根据你的 propertyInfo 和 fieldInfo 参数来配置。

nullable:表示此属性是否可为 null。valueGenerated:值生成,如果是标识列,应选择 ValueGenerated.OnAdd,插入数据时生成。beforeSaveBehavior 和 afterSaveBehavior:指示属性值在写入数据库前后是否允许修改。对于标识列,afterSaveBehavior 参数可以设置为 Throw,即如果数据已存入数据库,那么后面就不让你修改(不能改生成的主键)。maxLength:一般用于字符类型,最大长度。unicode:一般用于字符类型,是否使用 Unicode。precision 和 scale:一般用于浮点数,指定精度和位数。sentinel:如果属性未设置,给它一个默认值。

注意:boyid是标识列,要用批注表明它是标识列。

boyIdProp.AddAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy, SqlServerValueGenerationStrategy.IdentityColumn);

4、给 Boy 添加主键。

// 配置主键
var boyKey = boyEnt.AddKey([boyIdProp]);
boyEnt.SetPrimaryKey(boyKey);
// 主键名称
boyKey.AddAnnotation(RelationalAnnotationNames.Name, "PK_boy_id");

主键的名称通过批注来设置。AddKey 方法引用一个属性列表,即哪些属性设置为键。

5、 Toy 实体的配置差不多。

var toyEnt = model.AddEntityType(
    name: "DB.Toy",
    type: typeof(Toy),
    baseType: null,
    propertyCount: 3,           // 属性数量不含导航属性
    navigationCount: 1,         // 导航属性个数
    keyCount: 1,                // 主键个数
    foreignKeyCount: 1          // 包含外键
);

var toyidProp = toyEnt.AddProperty(
    name: nameof(Toy.ToyID),
    clrType: typeof(int),
    nullable: false,
    valueGenerated: ValueGenerated.OnAdd,
    afterSaveBehavior: PropertySaveBehavior.Throw,
    propertyAccessMode: PropertyAccessMode.PreferProperty,
    propertyInfo: typeof(Toy).GetProperty(nameof(Toy.ToyID), 
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
    sentinel: 0
);

// 配置标识列
toyidProp.AddAnnotation(SqlServerAnnotationNames.
ValueGenerationStrategy, SqlServerValueGenerationStrategy.
IdentityColumn);
var toydescProp = toyEnt.AddProperty(
    name: nameof(Toy.Desc),
    clrType: typeof(string),
    nullable: false,
    maxLength: 50,
    propertyInfo: typeof(Toy).GetProperty(nameof(Toy.Desc), 
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
    propertyAccessMode: PropertyAccessMode.PreferProperty
);

var toycateProp = toyEnt.AddProperty(
    name: nameof(Toy.Cate),
    clrType: typeof(string),
    maxLength: 10,
    propertyInfo: typeof(Toy).GetProperty(nameof(Toy.Cate), 
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
    propertyAccessMode: PropertyAccessMode.PreferProperty
);
// 以下属性为影子属性,映射到外键
var toytoboyIdProp = toyEnt.AddProperty(
    name: "boy_id",
    clrType: typeof(int),
    propertyInfo: null,     // 影子属性不需要引用属性成员
    nullable: true
);

boy_id 是影子属性,因为不需要与 CLR 类型映射,所以,propertyInfo 和 fieldInfo 参数不需要。虽然这货没有在实体类中定义,但 EF Core 内部会用一个字典来管理它。实际上 EF Core 为实体类存了不止一份值,比如“快照”。即当数据从数据库查询出来时会做一个快照,后面会用实体的值跟快照比较,以确定实体是否被修改了。

6、给 Toy 实体设置主键和外键。

 // 设置主键
 var toyKey = toyEnt.AddKey([toyidProp]);
 // 主键名
 toyKey.AddAnnotation(RelationalAnnotationNames.Name, "PK_toy_id");
 toyEnt.SetPrimaryKey(toyKey);
 // 设置外键
 var toyforekey = toyEnt.AddForeignKey(
     properties: [toytoboyIdProp],
     principalKey: boyKey,
     principalEntityType: boyEnt,
     deleteBehavior: DeleteBehavior.Cascade,
     unique: false               // 一对多,这里不唯一
 );
 // 外键名称
 toyforekey.AddAnnotation(RelationalAnnotationNames.Name, 
"FK_toy_boy");

由于 Toy 表中会出现重复的 Boy ID,所以,AddForeignKey 方法的 unique 参数为 false。

7、导航属性,不管是 Boy 中的导航属性还是 Toy 中的,共用一个外键信息。

var boytotoyNav = boyEnt.AddNavigation(
    name: nameof(Boy.MyToys),
    foreignKey: toyforekey,
    onDependent: false,
    clrType: typeof(ICollection<Toy>),
    propertyInfo: typeof(Boy).GetProperty(nameof(Boy.MyToys), 
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
    fieldInfo: null,
    propertyAccessMode: PropertyAccessMode.PreferProperty
);

var toytoboyNav = toyEnt.AddNavigation(
    name: nameof(Toy.TheBoy),
    foreignKey: toyforekey,
    onDependent: true,
    clrType: typeof(Boy),
    propertyInfo: typeof(Toy).GetProperty(nameof(Toy.TheBoy), 
BindingFlags.Public | BindingFlags.Instance | BindingFlags.
DeclaredOnly),
    fieldInfo: null,
    propertyAccessMode: PropertyAccessMode.PreferProperty
);

导航属性的添加和一般属性差不多。注意 onDependent 参数的设置,对于 Boy 来说,它应该是“父”,即主体(被 Toy 的外键引用),要设置为 false;而对于 Toy 而言为“子”,即依赖于主体,故 onDependent 为 true。

8、由于实体名和属性名都与数据库不同,所以还要做表、列的映射。这些也是通过批注完成的。

// 1、架构
boyEnt.AddAnnotation(RelationalAnnotationNames.Schema, "dbo");
toyEnt.AddAnnotation(RelationalAnnotationNames.Schema, "dbo");
// 2、表名
boyEnt.AddAnnotation(RelationalAnnotationNames.TableName, "tb_boys");
toyEnt.AddAnnotation(RelationalAnnotationNames.TableName, "tb_toys");
// 3、列名
boyIdProp.AddAnnotation(RelationalAnnotationNames.ColumnName, 
"boy_id");
nameProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "name");
nickProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "nick");

toyidProp.AddAnnotation(RelationalAnnotationNames.ColumnName, 
"toy_id");
toydescProp.AddAnnotation(RelationalAnnotationNames.ColumnName, 
"toy_desc");
toycateProp.AddAnnotation(RelationalAnnotationNames.ColumnName, 
"toy_cate");
toytoboyIdProp.AddAnnotation(RelationalAnnotationNames.ColumnName, 
"f_boy_id");

好了,模型就配好了。下面是完整代码。老周用了一个静态类来封装(你也可以像 dotnet-ef 工具那样,从 RuntimeModel 类派生)。

#pragma warning disable EF1001 // Internal EF Core API usage.
public static class ModelHelpers
{
    private static readonly RuntimeModel _model;

    static ModelHelpers()
    {
        _model = BuildRuntimeModel();
    }

    public static RuntimeModel DemoModel => _model;

    private static RuntimeModel BuildRuntimeModel()
    {
        RuntimeModel model = new(
            skipDetectChanges: false,   // 开启更改跟踪
            modelId: Guid.NewGuid(),    // 模型ID
            entityTypeCount: 2,         // 模型中有两个实体类型
            typeConfigurationCount: 0   // 未使用自定义类型转换
        );

        // 配置第一个实体
        var boyEnt = model.AddEntityType(
            name: "DB.Boy",     // 一般包含命名空间
            type: typeof(Boy),  // CLR 类型
            baseType: null,     // 没有基类(指除Object以外)
            propertyCount: 3,   // 属性个数,不含导航属性
            navigationCount: 1, // 有一个导航属性
            keyCount: 1         // 有一个主键
        );
        // 添加属性
        var boyIdProp = boyEnt.AddProperty(
            name: nameof(Boy.ID),   // 属性名
            clrType: typeof(int),   // 属性类型
            nullable: false,        // 不为null
                                    // 引用CLR属性成员
            propertyInfo: typeof(Boy).GetProperty("ID", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
            // 无字段成员
            fieldInfo: null,
            // 优先使用属性来设置值
            propertyAccessMode: PropertyAccessMode.PreferProperty,
            // 插入时生成值
            valueGenerated: ValueGenerated.OnAdd,
            // 实体存入数据库后不允许修改此属性
            afterSaveBehavior: PropertySaveBehavior.Throw,
            // 默认值0
            sentinel: 0
        );
        // SQL Server 值生成配置-标识列
        boyIdProp.AddAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy, SqlServerValueGenerationStrategy.IdentityColumn);

        var nameProp = boyEnt.AddProperty(
            name: nameof(Boy.Name),
            clrType: typeof(string),
            nullable: false,
            propertyInfo: typeof(Boy).GetProperty("Name", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
            maxLength: 20,          // 长度限制
            propertyAccessMode: PropertyAccessMode.PreferProperty
        );

        var nickProp = boyEnt.AddProperty(
            name: nameof(Boy.Nick),
            clrType: typeof(string),
            nullable: true,
            maxLength: 20,
            propertyInfo: typeof(Boy).GetProperty(nameof(Boy.Nick), BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance),
            propertyAccessMode: PropertyAccessMode.PreferProperty
        );

        // 配置主键
        var boyKey = boyEnt.AddKey([boyIdProp]);
        boyEnt.SetPrimaryKey(boyKey);
        // 主键名称
        boyKey.AddAnnotation(RelationalAnnotationNames.Name, "PK_boy_id");

        // 添加第二个实体
        var toyEnt = model.AddEntityType(
            name: "DB.Toy",
            type: typeof(Toy),
            baseType: null,
            propertyCount: 3,           // 属性数量不含导航属性
            navigationCount: 1,         // 导航属性个数
            keyCount: 1,                // 主键个数
            foreignKeyCount: 1          // 包含外键
        );
        // 添加属性
        var toyidProp = toyEnt.AddProperty(
            name: nameof(Toy.ToyID),
            clrType: typeof(int),
            nullable: false,
            valueGenerated: ValueGenerated.OnAdd,
            afterSaveBehavior: PropertySaveBehavior.Throw,
            propertyAccessMode: PropertyAccessMode.PreferProperty,
            propertyInfo: typeof(Toy).GetProperty(nameof(Toy.ToyID), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
            sentinel: 0
        );
        // 配置标识列
        toyidProp.AddAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy, SqlServerValueGenerationStrategy.IdentityColumn);

        var toydescProp = toyEnt.AddProperty(
            name: nameof(Toy.Desc),
            clrType: typeof(string),
            nullable: false,
            maxLength: 50,
            propertyInfo: typeof(Toy).GetProperty(nameof(Toy.Desc), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
            propertyAccessMode: PropertyAccessMode.PreferProperty
        );

        var toycateProp = toyEnt.AddProperty(
            name: nameof(Toy.Cate),
            clrType: typeof(string),
            maxLength: 10,
            propertyInfo: typeof(Toy).GetProperty(nameof(Toy.Cate), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
            propertyAccessMode: PropertyAccessMode.PreferProperty
        );
        // 以下属性为影子属性,映射到外键
        var toytoboyIdProp = toyEnt.AddProperty(
            name: "boy_id",
            clrType: typeof(int),
            propertyInfo: null,     // 影子属性不需要引用属性成员
            nullable: true
        );

        // 设置主键
        var toyKey = toyEnt.AddKey([toyidProp]);
        // 主键名
        toyKey.AddAnnotation(RelationalAnnotationNames.Name, "PK_toy_id");
        toyEnt.SetPrimaryKey(toyKey);
        // 设置外键
        var toyforekey = toyEnt.AddForeignKey(
            properties: [toytoboyIdProp],
            principalKey: boyKey,
            principalEntityType: boyEnt,
            deleteBehavior: DeleteBehavior.Cascade,
            unique: false               // 一对多,这里不唯一
        );
        // 外键名称
        toyforekey.AddAnnotation(RelationalAnnotationNames.Name, "FK_toy_boy");

        // 建立导航关系
        var boytotoyNav = boyEnt.AddNavigation(
            name: nameof(Boy.MyToys),
            foreignKey: toyforekey,
            onDependent: false,
            clrType: typeof(ICollection<Toy>),
            propertyInfo: typeof(Boy).GetProperty(nameof(Boy.MyToys), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
            fieldInfo: null,
            propertyAccessMode: PropertyAccessMode.PreferProperty
        );
        var toytoboyNav = toyEnt.AddNavigation(
            name: nameof(Toy.TheBoy),
            foreignKey: toyforekey,
            onDependent: true,
            clrType: typeof(Boy),
            propertyInfo: typeof(Toy).GetProperty(nameof(Toy.TheBoy), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
            fieldInfo: null,
            propertyAccessMode: PropertyAccessMode.PreferProperty
        );

        // 表、列映射
        // 1、架构
        boyEnt.AddAnnotation(RelationalAnnotationNames.Schema, "dbo");
        toyEnt.AddAnnotation(RelationalAnnotationNames.Schema, "dbo");
        // 2、表名
        boyEnt.AddAnnotation(RelationalAnnotationNames.TableName, "tb_boys");
        toyEnt.AddAnnotation(RelationalAnnotationNames.TableName, "tb_toys");
        // 3、列名
        boyIdProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "boy_id");
        nameProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "name");
        nickProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "nick");

        toyidProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "toy_id");
        toydescProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "toy_desc");
        toycateProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "toy_cate");
        toytoboyIdProp.AddAnnotation(RelationalAnnotationNames.ColumnName, "f_boy_id");

        // 完事,收工

        return model;
    }
}
#pragma warning restore EF1001 // Internal EF Core API usage.

下面定义上下文类。

public class DemoContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder ob)
    {
        ob.UseSqlServer(@"server=你的服务器; database=你的数据库")
            .LogTo(o => Console.WriteLine(o))
            .EnableSensitiveDataLogging(true)
            .UseModel(ModelHelpers.DemoModel);
    }

    public DbSet<Boy> BoySet { get; set; }
    public DbSet<Toy> ToySet { get; set; }
}

熟悉的配方,不用多解释了。

下面是测试代码。

using DemoContext dc = new();

if (!dc.BoySet.Any())
{
    Boy b1 = new()
    {
        Name = "小张",
        Nick = "灰太狼"
    };
    b1.MyToys.Add(new()
    {
        Desc = "电动风车",
        Cate = "电动玩具"
    });
    b1.MyToys.Add(new()
    {
        Desc = "米老鼠高压水枪",
        Cate = "气动玩具"
    });
    dc.BoySet.Add(b1);

    Boy b2 = new Boy
    {
        Nick = "哈巴狗",
        Name = "小李"
    };
    b2.MyToys.Add(new() { Desc = "库洛牌", Cate = "卡牌" });
    b2.MyToys.Add(new() { Desc = "yoyo", Cate = "光电玩具" });
    dc.BoySet.Add(b2);

    // 更新
    dc.SaveChanges();
}

插入的数据如下:

image

image

================================================================================================

上面内容是不实用的,只是方便学习,下面老周演示一下如何用 dotnet-ef 工具生成运行时模型。这才是咱们在实际项目中要用的(除非特殊需求,要自己去写)。

1、新建一个控制台项目。

dotnet new console -n Demo -o .

2、添加要用的包。

dotnet add package microsoft.entityframeworkcore
dotnet add package microsoft.entityframeworkcore.sqlserver

3、要使用 dotnet-ef 工具,还得添加设计时包。

dotnet add package microsoft.entityframeworkcore.design

添加完毕后,项目应该引用了三个库:

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
  </ItemGroup>

设计时库被设置为“私有资产”,你在代码中无法访问。如果你在代码要用,最简单方法是把 IncludeAssets、PrivateAssets 节点干掉。

4、定义实体类。

public class Light
{
    /// <summary>
    /// 主键
    /// </summary>
    public Guid LightID { get; set; }
    /// <summary>
    /// 灯光颜色
    /// </summary>
    public string[] Color { get; set; } = [""];
    /// <summary>
    /// 功率(瓦)
    /// </summary>
    public float Power { get; set; }
    /// <summary>
    /// 是否为 RGB LED 灯
    /// </summary>
    public bool? IsRGBLed { get; set; }
}

5、定义上下文类。

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
    {

    }

    // 配置模型
    protected override void OnModelCreating(ModelBuilder mb)
    {
        mb.Entity<Light>().HasKey(e => e.LightID).HasName("PK_light_id");
        mb.Entity<Light>().Property(g => g.LightID)
            .ValueGeneratedOnAdd();
        mb.Entity<Light>().Property(x => x.Color)
            .HasMaxLength(200).IsRequired();
        mb.Entity<Light>().Property(s => s.Power)
            .HasPrecision(5, 2);
        mb.Entity<Light>().ToTable("tb_lights", "dbo", tb =>
        {
            tb.Property(a => a.LightID).HasColumnName("light_id");
            tb.Property(a => a.Color).HasColumnName("light_colors");
            tb.Property(a => a.Power).HasColumnName("light_pwr");
            tb.Property(a => a.IsRGBLed).HasColumnName("is_rgbled");
        });
    }

    public DbSet<Light> Lights { get; set; }
}

6、定义一个类,实现 IDesignTimeDbContextFactory 接口。在执行 dotnet ef 命令时可以用来创建 MyDbContext 实例(设计阶段专用)。

public class CustDesigntimeFactory : IDesignTimeDbContextFactory<MyDbContext>
{
    public MyDbContext CreateDbContext(string[] args)
    {
        // 创建选项
        DbContextOptions<MyDbContext> options = new DbContextOptionsBuilder<MyDbContext>()
            .UseSqlServer(@"server=你的服务器; database=你的数据库")
            .LogTo(s => Console.WriteLine(s))
            .Options;
        // 实例化上下文对象
        return new MyDbContext(options);
    }
}

代码不用往下写了,这个时候,就可以用 ef 工具生成优化的模型了。

dotnet ef dbcontext optimize -c MyDbContext -n DB -o DB

-c 指定要用到的 DbContext 子类,这里可以省略,工具会自动搜索项目中的 MyDbContext 类。-n 表示生成代码使用的命名空间,这里指定了 DB。-o 表示生成的代码文件存放到哪个目录中,这里存到 DB 目录下(相对于项目目录)。

工具先编译一下项目,然后实例化 MyDbContext 类,执行一遍模型配置,最终生成相关代码。

image

 MyDbContextModel.cs 和 MyDbContextModelBuilder.cs 都是模型类相关。

namespace DB
{
    [DbContext(typeof(MyDbContext))]
    public partial class MyDbContextModel : RuntimeModel
    {
        private static readonly bool _useOldBehavior31751 =
            System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751;

        static MyDbContextModel()
        {
            var model = new MyDbContextModel();

            if (_useOldBehavior31751)
            {
                model.Initialize();
            }
            else
            {
                var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024);
                thread.Start();
                thread.Join();

                void RunInitialization()
                {
                    model.Initialize();
                }
            }

            model.Customize();
            _instance = (MyDbContextModel)model.FinalizeModel();
        }

        private static MyDbContextModel _instance;
        public static IModel Instance => _instance;

        partial void Initialize();

        partial void Customize();
    }
}

注意,MyDbContextModel 类是 partial 的,意味着你可以自定义扩展它,只要写在其他文件中,若模型修改了,重新运行 ef dbcontext 命令也不会被覆盖。

此类应用了 DbContextAttribute 特性类,表明此模型与 MyDbContext 关联。

LightEntityType.cs 文件就是配置 Light 实体的代码了,看看里面有啥。

    public partial class LightEntityType
    {
        public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
        {
            var runtimeEntityType = model.AddEntityType(
                "DataBS.Light",
                typeof(Light),
                baseEntityType,
                propertyCount: 4,
                keyCount: 1);

            var lightID = runtimeEntityType.AddProperty(
                "LightID",
                typeof(Guid),
                propertyInfo: typeof(Light).GetProperty("LightID", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
                fieldInfo: typeof(Light).GetField("<LightID>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
                valueGenerated: ValueGenerated.OnAdd,
                afterSaveBehavior: PropertySaveBehavior.Throw,
                sentinel: new Guid("00000000-0000-0000-0000-000000000000"));

            var overrides = new StoreObjectDictionary<RuntimeRelationalPropertyOverrides>();
            var lightIDTb_lights = new RuntimeRelationalPropertyOverrides(
                lightID,
                StoreObjectIdentifier.Table("tb_lights", "dbo"),
                true,
                "light_id");
            overrides.Add(StoreObjectIdentifier.Table("tb_lights", "dbo"), lightIDTb_lights);
            lightID.AddAnnotation("Relational:RelationalOverrides", overrides);

            lightID.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None);

            var color = runtimeEntityType.AddProperty(
                "Color",
                typeof(string[]),
                propertyInfo: typeof(Light).GetProperty("Color", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
                fieldInfo: typeof(Light).GetField("<Color>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
                maxLength: 200);

            var overrides0 = new StoreObjectDictionary<RuntimeRelationalPropertyOverrides>();
            var colorTb_lights = new RuntimeRelationalPropertyOverrides(
                color,
                StoreObjectIdentifier.Table("tb_lights", "dbo"),
                true,
                "light_colors");
            overrides0.Add(StoreObjectIdentifier.Table("tb_lights", "dbo"), colorTb_lights);
            color.AddAnnotation("Relational:RelationalOverrides", overrides0);

            color.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None);

            var isRGBLed = runtimeEntityType.AddProperty(
                "IsRGBLed",
                typeof(bool?),
                propertyInfo: typeof(Light).GetProperty("IsRGBLed", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
                fieldInfo: typeof(Light).GetField("<IsRGBLed>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
                nullable: true);

            var overrides1 = new StoreObjectDictionary<RuntimeRelationalPropertyOverrides>();
            var isRGBLedTb_lights = new RuntimeRelationalPropertyOverrides(
                isRGBLed,
                StoreObjectIdentifier.Table("tb_lights", "dbo"),
                true,
                "is_rgbled");
            overrides1.Add(StoreObjectIdentifier.Table("tb_lights", "dbo"), isRGBLedTb_lights);
            isRGBLed.AddAnnotation("Relational:RelationalOverrides", overrides1);

            isRGBLed.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None);

            var power = runtimeEntityType.AddProperty(
                "Power",
                typeof(float),
                propertyInfo: typeof(Light).GetProperty("Power", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
                fieldInfo: typeof(Light).GetField("<Power>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
                precision: 5,
                scale: 2,
                sentinel: 0f);

            var overrides2 = new StoreObjectDictionary<RuntimeRelationalPropertyOverrides>();
            var powerTb_lights = new RuntimeRelationalPropertyOverrides(
                power,
                StoreObjectIdentifier.Table("tb_lights", "dbo"),
                true,
                "light_pwr");
            overrides2.Add(StoreObjectIdentifier.Table("tb_lights", "dbo"), powerTb_lights);
            power.AddAnnotation("Relational:RelationalOverrides", overrides2);

            power.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None);

            var key = runtimeEntityType.AddKey(
                new[] { lightID });
            runtimeEntityType.SetPrimaryKey(key);
            key.AddAnnotation("Relational:Name", "PK_light_id");

            return runtimeEntityType;
        }

        public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
        {
            runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
            runtimeEntityType.AddAnnotation("Relational:Schema", "dbo");
            runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
            runtimeEntityType.AddAnnotation("Relational:TableName", "tb_lights");
            runtimeEntityType.AddAnnotation("Relational:ViewName", null);
            runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);

            Customize(runtimeEntityType);
        }

        static partial void Customize(RuntimeEntityType runtimeEntityType);
    }

是不是和咱们前文中自己手写的很像?所以,实际开发中咱们是不用自己动手写的,用工具生成即可。

MyDbContextAssemblyAttributes.cs 文件有一个程序集级别的特性应用。

[assembly: DbContextModel(typeof(MyDbContext), typeof(MyDbContextModel))]

有了这个特性,EF Core 就能自己查找 RuntimeModel,不需要调用 DbContextOptionsBuilder 的 UseModel 方法来添加外部模型了。

顺便总结一下,EF Core 框架是按这个顺序查找模型的:

1、UseModel 方法指定的外部模型(请看上一篇水文),如果从选项类设置了,就用这个模型;

2、如果选项类没有用 UseModel 方法设置外部模型,那就找一下有没 dotnet-ef 工具生成的模型(就是咱们刚刚做的事)。如果有,就用它,原理是根据程序集上应用的 DbContextModelAttribute 特性找到生成的模型类,如本例中的 MyDbContextModel。接着查找模型类中名为 Instance 的静态属性,读取这个属性的值,就能获取模型实例了。显然,运行时模型实例是静态的,即只实例化一次。

可以看看源代码:

 static IModel? FindCompiledModel(Type contextType)
 {
     var contextAssembly = contextType.Assembly;
     IModel? model = null;
     foreach (var modelAttribute in contextAssembly.
GetCustomAttributes<DbContextModelAttribute>())
     {
         if (modelAttribute.ContextType != contextType)
         {
             continue;
         }
         var modelType = modelAttribute.ModelType;
         var instanceProperty = modelType.GetProperty("Instance", 
BindingFlags.Public | BindingFlags.Static);
         if (instanceProperty == null
             || instanceProperty.PropertyType != typeof(IModel))
         {
             throw new InvalidOperationException(CoreStrings.
CompiledModelMissingInstance(modelType.DisplayName
()));
         }
         if (model != null)
         {
             throw new InvalidOperationException(
                 CoreStrings.CompiledModelDuplicateAttribute(
                     contextAssembly.FullName, contextType.
DisplayName()));
         }
         model = (IModel)instanceProperty.GetValue(null)!;
     }
     return model;
 }

3、如果找不到 ef 工具生成的运行时模型,就调用内部模型构建,即调用 DbContext 的 OnModelCreating 方法以设计时形态构建模型。构建完毕后,通过预置约定 RuntimeModelConvention 生成 RuntimeModel。当然了,前面多次提到,如果要在代码中创建数据库或迁移,是不使用运行时模型的,而是直接跑 OnModelCreating 方法的配置代码,当然,预置约定也会全部跑一遍。

=========================================================================================

上周六,老周作为外三路辅助招聘大师,和两家企业的内部招聘人员交流。他们抱怨说现在的码农怎么回事,技术水平咋感觉一代不如一代,是不是现在工具太好用了,还有 AI 辅助,反而让他变菜了?就此老周也发表了自己的荒唐观点:

过度依赖 AI 以及其他工具并不是他们菜的原因,而是他们菜的结果。工具本身没啥,爱用就用,不爱用就不用,就是辅助的,真正干事的还是人类。但核心事件是——他们就是菜,从骨子里透出来的菜,而且,菜还不练!整天做出一副要改革职场的屌样。你们又不是缺人缺到项目写不动的程度,真要是那么缺人,要不分一点给我,我帮你做。既然招不到人项目还是能继续干的,那就不着急,总能找到不菜的人。我不相信国内的程序猿全都那么菜。

如果你想老周分析一下,现在很多码仔那么菜的原因,那对不起了,老周又要说你们不爱听的话了。那些人就是被当今网络环境忽悠成菜鸟的。如果你在十几年前关注过一些培训班,不管培训钢琴、古筝、编程啥的,它们很喜欢一句口号——“XXX 内速成”,比如一个月内速成,三个月内速成。有脑子都知道是P话,但架不住许多驴子和家长会相信。后来央视曝光过,现在“速成”二字很少见了。但是,这些骗子仍旧屎心不改,换个模式接着忽悠。于是出现了学 XXX 拿 YYY 万高薪。不出意外,也有许多驴子相信了。当初还有些大学森问过老周,报不报这些?老周说:“你学校没得学吗?图书馆地震了吗?没开课吗?何必呢?那些‘老师’估计比你们学校的老师还菜”。要是你听了它们的鬼话,估计连面试都过不了,还想什么高薪。

在网上曾看到过一个笑话,A说:上次有个哥们来我们这面试,问了一个 C++ 虚函数表的问题,那家伙直跑了。简历上还写着“精通C++”。然后下面,B网友说“那是北大青X出来的吧”。

现在,坑人模式升级了,不,准确来说没升级,2005 年前后网上就有这些货了,只是那时候没有短视频,也没那么多人上网。现在短视频里教别人编程的,老周可以不客气的说:全是坑人的(包括老周自己录的,也是忽悠你的)。

在老周心中,什么样的视频教程是合格的?如果各位和老周一样足够老的话,一定看过孙鑫老师的 C++ 视频,20集,时长惊人,含金量不用多说。说简单点,视频教程要达到这种层次,才是有观看价值的。你看看现在各种短视频里面,有这种水平的吗?

还是看书好,就算有些书内容不怎么样,但至少内容是相对全面系统化的,视频、博文、贴子可以作为辅助学习。

王经理说老周面试时太仁慈,只问些基础知识,从来不问项目经验。我就给王经理解释了一通。实际应用中,个个项目是不同的,就是有个别的书本会讲项目上的事,但对于实际工作中还是没多大意义的。工作经验是靠总结,每个人的想法都可能不一样,没办法作为知识点让你一条一条去学,学了也没用。其实项目经验这东西,把你放项目里面呆几个月基本就有经验了,不用学的,自然就会。

可是,技术和基础知识则不同,这些都必须去学的。你得先把这一关过了,项目上要安排你做什么你随机应变就行,你只要技术够硬,就马上就能想到这个东西要用什么解决。比如这里是不是要用 Web API 实现,那里是不是要用 SHAXXX 加密一下,那里是不是要用到压缩数据,某个窗口是不是要开后台线程处理,某个工序是不是要创建一个队列来排单……

要是你基础知识没学好,我叫你改一下那个页面,登录加密加个下拉表列,可以选 MD5、SHA1、SHA256 算法,然后提交前用对应的算法 Hash 一下密码。这里你完全可以用反射来做,.NET 的 MD5 等类,都有一个静态的 Create 方法……懂我的意思了吧。但是,如果你连反射是啥都不知道,自然不会想到这样解决,可能会用多 if 语句处理。虽说不是不行,但不如反射来得通用且简便。

说多了,其实就是现在很多人不愿意去实践,学了不去试,懒得动手,等真正要用的时候就不知所措了。