28
2

当我将nextjs应用程序部署到azure web应用程序的时候报错:cannot find module ../server/require-hook。

我通过github-actions构建和部署成功完成,但是部署后,我在url 中收到错误,并且在azure日志流利发现这个错误。刚刚开始我不明白是什么原因造成的,因为“npm start”在我的本地机器上使用相同的 npm 版本构建和运行。

通过网上查找,发现是因为githhb的action默认不会把.next文件夹打包加入zip并且发布到azure。所以需要在默认的工作流基础上修改:

1、adding "startAzure": "./node_modules/next/dist/bin/next start" in package.json; 还需要在azure应用程序网站的配置立加上 the startup command of the azure web app like "npm run startAzure"

2、添加这个到azure的 pipeline (在创建 zip了之后)

- name: Zip artifact for deployment run: zip release.zip ./* -r
- name: Add .next folder to zip artifact for deployment run: zip -r release.zip ".next"
26
6

MiniProfiler工具介绍

0
归档:2024年6月分类:C#和.NET

在日常开发中,应用程序的性能是我们需要关注的一个重点问题。当然我们有很多工具来分析程序性能:如:Zipkin等;但这些过于复杂,需要单独搭建。

MiniProfiler就是一款简单,但功能强大的应用新能分析工具;可以帮助我们定位:SQL性能问题、响应慢等问题。

MiniProfiler是一款针对.NET, Ruby, Go and Node.js的性能分析的轻量级程序。可以对一个页面本身,及该页面通过直接引用、Ajax、Iframe形式访问的其它页面进行监控,监控内容包括数据库内容,并可以显示数据库访问的SQL(支持EF、EF CodeFirst等 )。并且以很友好的方式展现在页面上。

MiniProfiler官网:http://miniprofiler.com/

MiniProfiler的一个特别有用的功能是它与数据库框架的集成。除了.NET原生的 DbConnection类,MiniProfiler还内置了对实体框架(Entity Framework)以及LINQ to SQL、RavenDb和MongoDB的支持。任何执行的Step都会包括当时查询的次数和所花费的时间。为了检测常见的错误,如N+1反模式,profiler将检测仅有参数值存在差异的多个查询。

25
6

作者 Julie Lerman

你大概注意到了,自 2008 年以来,我写过许多关于 Entity Framework(即 Microsoft 对象关系映射器 (ORM))的文章,ORM 一直是主要的 .NET 数据访问 API。市面上还有许多其他 .NET ORM,但是有一个特殊类别因其强大的性能得到的关注最高,那就是微型 ORM。我听人提到最多的微型 ORM 是 Dapper。据不同的开发者说,他们使用 EF 和 Dapper 制定了混合解决方案,让每个 ORM 能够在单个应用程序中做它最擅长的事,这最终激发了我的兴趣,促使我在最近抽出时间来一探究竟。

在阅读大量文章和博客文章,与开发者聊过天并熟悉过 Dapper 后,我想与大家分享我的一些发现,尤其是和像我这样,可能听说过 Dapper 但并不知道它是什么或者并不知道它的工作原理的人分享,同时说说人们为什么这么喜欢它。需要提醒你的是,我根本不是什么专家。目前我只是为了满足我的好奇心而变得足够了解,并且希望激发你的兴趣,从而进一步探索。

为什么是 Dapper?
Dapper 的历史十分有趣,它是从你可能再熟悉不过的资源中衍生的: Marc Gravell 和 Sam Saffron 在研究 Stack Overflow,解决此平台的性能问题时构建了 Dapper。考虑到 Stack Overflow 是一个流量极高的站点,那么必然存在性能上的问题。根据 Stack Exchange About 网页,在 2015 年,Stack Overflow 拥有 57 亿的网页浏览量。在 2011 年,Saffron 撰写过一篇关于他和 Gravell 所做的工作的博客文章,名为“我如何学会不再担忧和编写我自己的 ORM”(bit.ly/),这篇文章介绍了 Stack 当时存在的性能问题,该问题源于 LINQ to SQL 的使用。他在文中详细介绍了为什么编写自定义 ORM,其中 Dapper 就是优化 Stack Overflow 上的数据访问的答案。五年后的今天,Dapper 已被广泛使用并已成为开源软件。Gravell 和 Stack 及团队成员 Nick Craver 继续在 github.com/StackExchange/dapper-dot-net 上积极地管理项目。

Dapper 简介
Dapper 主要能够让你练习你的 SQL 技能,按你认为的那样构建查询和命令。它接近于“金属”而非标准的 ORM,免除了解释查询的工作,例如将 LINQ to EF 解释为 SQL。Dapper 不具备炫酷的转换功能,比如打散传递到 WHERE IN 从句的列表。但在大多数情况下,你发送到 Dapper 的 SQL 已准备好运行,而查询可以更快地到达数据库。如果你擅长 SQL,那么你将有把握编写性能最高的命令。你需要创建某些类型的 IDbConnection 来执行查询,比如带有已知连接字符串的 SqlConnection。然后,Dapper 可以通过其 API 为你执行查询以及—假如查询结果的架构与目标类型的属性相匹配—自动实例化对象并向对象填充查询结果。此处还有另一个显著的性能优势: Dapper 能够有效缓存它获悉的映射,从而实现后续查询的极速反序列化。我将填充的类 DapperDesigner(如图 1 中所示)被定义用来管理构建整齐构架的设计器。

图 1 DapperDesigner 类

C#

复制
public class DapperDesigner
{
  public DapperDesigner() {
    Products = new List<Product>();
    Clients = new List<Client>();
  }
  public int Id { get; set; }
  public string LabelName { get; set; }
  public string Founder { get; set; }
  public Dapperness Dapperness { get; set; }
  public List<Client> Clients { get; set; }
  public List<Product> Products { get; set; }
  public ContactInfo ContactInfo { get; set; }
}

我执行查询的项目引用了我通过 NuGet 获取的 Dapper(安装包 dapper)。下面是从 Dapper 调用以为 DapperDesigners 表中所有行执行查询的示例:

C#

复制
var designers = sqlConn.Query<DapperDesigner>("select * from DapperDesigners");

需要注意的是,对于本文中的代码清单,当我希望使用表中的所有列时,我使用的是 select * 而非明确投影的查询列。sqlConn 连同其连接字符串是现有的实例化 SqlConnection 对象,但是尚未打开过。

Query 方法是 Dapper 提供的扩展方法。在此行执行时,Dapper 打开连接,创建 DbCommand,准确地按照我编写的内容执行查询,实例化结果中的每行的 DapperDesigner 对象并将值从查询结果推送到对象的属性。Dapper 可以通过几种方式将结果值与属性进行匹配,即使属性名称与列名称不相匹配,又或者即使属性的顺序与匹配的列的顺序不同。它不会读心术,所以别期望它弄清涉及的映射,例如列的顺序或名称和属性不同步的大量字符串值。我确实用它做了几个奇怪的实验,我想看看它如何响应,同时我也配置了控制 Dapper 如何推断映射的目标设置。

Dapper 和关系查询
我的 DapperDesigner 类型拥有多种关系,比如一对多(与产品)、一对一 (ContactInfo) 和多对多(客户端)。我已经试验过跨这些关系执行查询,而且 Dapper 能够处理这些关系。这绝对不像使用 Include 方法或投影表述 LINQ to EF 查询那么简单。我的 TSQL 技能被推到极限,这是因为 EF 在过去几年让我变得如此懒惰。

下面是使用我在数据库中使用 SQL 进行跨一对多关系的查询的示例:


C#

复制
var sql = @"select * from DapperDesigners D
           JOIN Products P
           ON P.DapperDesignerId = D.Id";
var designers= conn.Query<DapperDesigner, Product,DapperDesigner>
(sql,(designer, product) => { designer.Products.Add(product);
                              return designer; });

注意 Query 方法要求我指定两种必须构建的类型,并指示要返回的类型—由最终类型参数 (DapperDesigner) 表述。我首先使用多行匿名函数构建图表,将相关产品添加到其父设计器对象,然后将每个设计器返回到 Query 方法返回的 IEnumerable。

通过我对 SQL 的最佳尝试,这样做的不利之处在于结果是扁平的,就像使用 EF Include 方法时一样。每个产品我将获取一行并复制一下设计器。Dapper 拥有可以返回多个结果集的 MultiQuery 方法。与 Dapper 的 GridReader 组合,这些查询的性能肯定将胜过 EF Includes。

编码难度加大,执行速度变快
表述 SQL 并填充相关对象是我让 EF 在此背景中处理的任务,所以需要更多精力来编码。但是如果你要处理的数据量很大,那么运行时性能非常重要,这当然值得努力。在我的示例数据库中拥有 30,000 个设计器。仅有几个拥有产品。我做了一些简单的基准测试,确保我所做的是同类比较。在查看测试结果前,有些关于我如何测量的重点需要大家理解。

请记住,默认情况下,EF 的设计目的是跟踪为查询结果的对象。这意味着它创建了额外的跟踪对象(需要做一些工作),并且它也需要与这些跟踪对象互动。而 Dapper 只是将结果转储到内存。所以当进行性能对比时,让 EF 的更改跟踪不再循环非常重要。为此,我使用 AsNoTracking 方法定义我的所有 EF 查询。同时,当对比性能时,你需要应用大量的标准基准模式,比如给数据库热身、反复执行查询以及抛弃最慢时间和最快时间。你可以看到我如何在下载示例中构建我的基准测试的详情。我仍然认为这些测试是“轻量级”基准测试,此处只是为了展现差异。对于较高的基准,你需要多次迭代(500 次以上),而我只进行了 25 次,这是远远不够的,同时还需要将你运行的系统的性能考虑在内。我在笔记本上使用 SQL Server LocalDB 实例进行这些测试,所以我的结果仅用于比较。

我在测试中跟踪的的时间为执行查询和构建结果的时间。未计入实例化连接或 DbContexts 的时间。因为反复使用 DbContext,所以构建内存内模型的时间不计入内,因为每个应用程序示例仅构建一次,而不是每个查询都要构建。

图 2 显示了 Dapper 和 EF LINQ 查询的“select *”测试,从中你可以看到我的测试模式的基本构造。注意,除收集实际时间外,我还在收集每次迭代的时间并整理到列表(名为“时间”)中以供进一步分析。

图 2 查询所有 DapperDesigners 时 EF 与 Dapper 的对比测试

C#

复制
[TestMethod,TestCategory("EF"),TestCategory("EF,NoTrack")]
public void GetAllDesignersAsNoTracking() {
  List<long> times = new List<long>();
  for (int i = 0; i < 25; i++) {
    using (var context = new DapperDesignerContext()) {
      _sw.Reset();
      _sw.Start();
      var designers = context.Designers.AsNoTracking().ToList();
      _sw.Stop();
      times.Add(_sw.ElapsedMilliseconds);
      _trackedObjects = context.ChangeTracker.Entries().Count();
    }
  }
  var analyzer = new TimeAnalyzer(times);
  Assert.IsTrue(true);
}
[TestMethod,TestCategory("Dapper")
public void GetAllDesigners() {
  List<long> times = new List<long>();
  for (int i = 0; i < 25; i++) {
    using (var conn = Utils.CreateOpenConnection()) {
      _sw.Reset();
      _sw.Start();
      var designers = conn.Query<DapperDesigner>("select * from DapperDesigners");
      _sw.Stop();
      times.Add(_sw.ElapsedMilliseconds);
      _retrievedObjects = designers.Count();
    }
  }
  var analyzer = new TimeAnalyzer(times);
  Assert.IsTrue(true);
}

关于同类对比,还有一个问题。 Dapper 使用原始 SQL。默认情况下,使用 LINQ to EF 表述 EF 查询并且必须做一些工作才能为你构建 SQL。一旦构建好 SQL,即使是依靠参数的 SQL,它将被缓存到应用程序的内存,以减少重复工作。此外,EF 可以使用原始 SQL 执行查询,所以我考虑到了这两种方法。图 3 列出了四组测试的对比结果。下载包含更多测试。

图 3 基于 25 次迭代执行查询和填充对象的平均时间(以毫秒计),排除最快和最慢时间

AsNoTracking 查询 关系 LINQ to EF EF Raw SQL Dapper Raw SQL
所有设计器(3 万行) – 96 98 77
所有带产品的设计器(3 万行) 1 :
251 107 91
所有带客户端的设计器(3 万行) : 255 106 63
所有带联系人的设计器(3 万行) 1 : 1 322 122 116

在 图 3 显示的场景中,我们可以很容易地跨 LINQ to Entities 使用 Dapper 制作一个案例。但是原始 SQL 查询之间的细微差异可能不总是在使用 EF 的系统中为特定任务切换到 Dapper 的正当理由。理所当然,大家的需求各有不同,所以这可能影响 EF 查询和 Dapper 之间的差异程度。但是,在 Stack Overflow 等高流量系统中,甚至是每个查询保存的大量毫秒时间都可能至关重要。

用于其他暂留需求的 Dapper 和 EF
到目前为止,我测量了简单查询,并在其中从所返回类型的准确匹配属性的表中回拉所有列。如果你将查询投影到类型会如何呢? 只要结果构架与类型相匹配,Dapper 将无法观察到创建对象的差异。但是,如果投影结果与为属于模型一部分的类型不一致,EF 不得不多做些工作。

DapperDesignerContext 拥有一个针对 DapperDesigner 类型的 DbSet。在我的系统中有另一个名为 MiniDesigner 的类型,它拥有一个 DapperDesigner 属性的子集。

C#

复制
public class MiniDesigner {
    public int Id { get; set; }
    public string Name { get; set; }
    public string FoundedBy { get; set; }
  }

MiniDesigner 不属于我的 EF 数据模型,所以 DapperDesigner­Context 不知道这种类型。我发现与使用借用原始 SQL 的 EF 相比,使用 Dapper 查询所有这 30,000 行并将其投影到 30,000 个 MiniDesigner 对象要快 25%。我再次建议你做自己的性能分析,并为你自己的系统做出决策。

Dapper 也可用于将数据推送到数据库,其中包含允许你识别必须用于命令指定参数的属性的方法,不论你使用的是原始 INSERT 或 UPDATE 命令,还是对数据库执行函数或存储过程。我并没有对这些任务做任何性能对比。

现实世界中的混合 Dapper 和 EF
有许多将 Dapper 用于 100% 数据暂留的系统。但是回忆起来,我的兴趣是由谈论混合解决方案的开发者激起的。在某些情况下,还存在已有 EF 并希望微调特定问题区域的系统。在其他情况下,团队选择使用 Dapper 执行所有查询,使用 EF 执行所有保存。

有人回复了我在 Twitter 上发布的关于这方面的问题,答案千变万化。

@garypochron 告诉我他的团队“将 Dapper 用于高需区域并使用资源文件维护 SQL 的组织。“ 而热门 EF Reverse POCO Generator 的作者 Simon Hughes (@s1monhughes) 的习惯恰好相反—默认使用 Dapper,遇到棘手问题时则使用 EF,对此我感到很吃惊。他告诉我“只要可能,我都会使用 Dapper。如果是比较复杂的更新,我会使用 EF。”

我也见过各种混合方法是由于要分离关注点而非提高性能而推动的讨论。最常见的讨论是利用 EF 上的 ASP.NET Identity 的默认依赖性,然后在解决方案中使用 Dapper 进行其余存储。

除性能外,更直接地处理数据库还拥有其他优点。SQL Server 专家 Rob Sullivan (@datachomp) 和 Mike Campbell (@angrypets) 也对 Dapper 青睐有加。Rob 指出你可以利用 EF 不允许访问的数据库功能,比如全文搜索。从长期来看,特殊功能是关于性能的。

另一方面,有些任务只能使用 EF 完成,使用 Dapper 根本完成不了(更改跟踪除外)。一个很好的例子是我在构建为本文创建的解决方案时利用的功能—即使用 EF Code First Migrations 在模型更改时迁移数据库的能力。

Dapper 并不适合每一个人。@damiangray 告诉我 Dapper 不是他的解决方案之选,因为他需要能够将 IQueryables(不是真实数据)从系统的一部分返回到另一部分。这个推迟执行查询的主题已在 Dapper 的 GitHub 存储库中提出,如果你想详细了解此主题,请访问 bit.ly/22CJzJl。在设计混合系统时,使用 Command Query Separation (CQS) 是个不错的方法,你可以在其中为特定类型的交易设计独立的模型,至少我着迷于此。这样一来,你不必设法去构建普通的数据访问代码以使用 EF 和 Dapper,因为构建此代码通常会牺牲每个 ORM 的好处。在我创作这篇文章时,Kurt Dowswell 发布了一篇名为“Dapper、EF 和 CQS”(bit.ly/1LEjYvA) 的博文。对我来说得心应手,对你来说亦是如此。

对于那些期待 CoreCLR 和 ASP.NET Core 的人来说,Dapper 已演变为能够支持这些功能的软件。你可以在 Dapper 的 GitHub 存储库中的文章 (bit.ly/1T5m5Ko) 中找到更多信息。

最后,我看了看 Dapper。我认为怎么样?
我怎么样? 我很遗憾没能尽早正视 Dapper,同时也因最终实现了愿望而感到很高兴。我始终推荐 AsNoTracking 或建议使用数据库中的视图或过程缓解性能问题。它从未让我或我的客户失望过。但是现在我知道我还有另一招妙计要推荐给对从使用 EF 的系统中榨出更多性能感兴趣的开发者。这不是我们所谓的稳操胜券。我的建议将用来探索 Dapper、测量性能差异(大规模)以及找到性能与编码难度之间的平衡点。想想 StackOverflow 的显著用途:查询问题、注释和答案,然后连同一些元数据(编辑)和用户信息返回附有注释和答案的问题图表。它们反复执行相同类型的查询并标绘出相同形状的结果。Dapper 的设计更擅长这种类型的反复查询,并且每次都会变得更智能、更快速。即使你的系统中没有设计为供 Dapper 处理的海量交易,你也可能找到满足你需求的混合解决方案。

Julie Lerman是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 .NET 主题的演示。她的博客地址是 thedatafarm.com/blog。她是“Entity Framework 编程”及其 Code First 和 DbContext 版本(全都出版自 O’Reilly Media)的作者。通过 Twitter 关注她:@julielerman 并在 juliel.me/PS-Videos 上观看其 Pluralsight 课程。

衷心感谢以下 Stack Overflow 技术专家对本文的审阅: Nick Craver 和 Marc Gravell
Nick Craver (@Nick_Craver) 既是开发者,又是网站可靠性工程师,同时兼职 Stack Overflow 的 DBA。他擅长各层、总体系统体系结构和数据中心硬件的性能调节以及 Opserver 等开源项目的维护。访问 了解他的相关信息。

Marc Gravell 是 Stack Overflow 的开发者,主要专注于高性能库和 .NET 工具,尤其是数据访问、序列化、网络 API,为这些领域的一系列开源项目做出了贡献。

23
6

Entity Framework Core:一对多关系

0
归档:2024年6月分类:C#和.NET

当一个实体类型的任意数量的实体与相同或另一个实体类型的任意数量的实体相关联时,将使用多对多关系。 例如,Post 可以有多个关联的 Tags,并且每个 Tag 可以与任意数量的 Posts 关联。

了解多对多关系
多对多关系不同于一对多和一对一关系,因为它们不能仅使用外键以简单方式表示。 相反,需要其他实体类型来“联接”关系的两端。 这称为“联接实体类型”,并映射到关系数据库中的“联接表”。 此联接实体类型的实体包含外键值对,其中一对指向关系一端的实体,另一对指向关系另一端的实体。 因此,每个联接实体以及联接表中的每一行都表示关系中实体类型之间的一个关联。

EF Core 可以隐藏联接实体类型并在后台对其进行管理。 这允许以自然方式使用多对多关系的导航,从而根据需要在每一端添加或删除实体。 但是,了解后台发生的情况非常有用,以便其整体行为(尤其是映射关系数据库)有意义。 让我们从关系数据库架构设置开始,以表示帖子和标记之间的多对多关系:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

在此架构中,PostTag 是联接表。 它包含两列:PostsId(指向 Posts 表的主键的外键)和 TagsId(指向 Tags 表的主键的外键)。 因此,此表中的每一行都表示一个 Post 和一个 Tag 之间的关联。

EF Core 中此架构的简单映射由三种实体类型组成(每个表对应一个实体类型)。 如果其中每个实体类型都由 .NET 类表示,则这些类可能如下所示:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

请注意,在此映射中,没有多对多关系,而是存在两个一对多关系,一个用于联接表中定义的每个外键。 这不是映射这些表的不合理方法,但并不反映联接表的意图,即表示单个多对多关系,而不是两个一对多关系。

EF 允许通过引入两个集合导航实现更自然的映射:一个在包含其相关的 Tags 的 Post 上,另一个在包含其相关的 Posts 的 Tag 上。 例如:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

提示

这些新导航称为“跳过导航”,因为它们跳过联接实体以提供对多对多关系另一端的直接访问。

如下面的示例所示,可以通过这种方式映射多对多关系,即使用联接实体的 .NET 类,使用两个一对多关系的导航和跳过实体类型上公开的导航。 但是,EF 可以透明地管理联接实体,而无需为其定义 .NET 类,且无需两个一对多关系的导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

事实上,默认情况下,EF 模型生成约定会将此处显示的 Post 和 Tag 类型映射到本节顶部的数据库架构中的三个表。 此映射(不显式使用联接类型)通常是指术语“多对多”。

示例
以下部分包含多对多关系的示例,包括实现每个映射所需的配置。

提示

可在 ManyToMany.cs 中找到以下所有示例的代码。

基本多对多
在多对多的最基本情况下,关系两端的实体类型都具有集合导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

此关系按约定映射。 即使不需要,此关系的等效显式配置也以学习工具的形式显示如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts);
}

即使使用此显式配置,关系的许多方面仍按约定进行配置。 出于学习目的,更完整的显式配置是:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            "PostTag",
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
            j => j.HasKey("PostsId", "TagsId"));
}

重要

即使不需要,也不要尝试完全配置所有内容。 如上所示,代码很快就变得复杂,并且容易出错。 即使在上面的示例中,模型中有许多内容仍按约定进行配置。 认为 EF 模型中的所有内容始终可以完全显式配置是不现实的。

无论关系是按约定还是使用所示的任一显式配置生成,生成的映射架构(使用 SQLite)都是:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

使用 Database First 流从现有数据库搭建 DbContext 的基架时,EF Core 6 及更高版本会在数据库架构中查找此模式,并按本文档中所述搭建多对多关系的基架。 可以使用自定义 T4 模板更改此行为。 有关其他选项,请参阅无映射联接实体的多对多关系现已搭建基架。

重要

目前,EF Core 使用 Dictionary<string, object> 表示未配置 .NET 类的联接实体实例。 但是,为了提高性能,将来的 EF Core 版本中可能会使用另一种类型。 除非已显式配置,否则不要依赖于联接类型 Dictionary<string, object>。

具有命名联接表的多对多
在前面的示例中,联接表按约定命名为 PostTag。 可以使用 UsingEntity 为它指定显式名称。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity("PostsToTagsJoinTable");
}

映射的其他所有内容都保持不变,只更改了联接表的名称:

CREATE TABLE "PostsToTagsJoinTable" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostsToTagsJoinTable" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostsToTagsJoinTable_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostsToTagsJoinTable_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有联接表外键名称的多对多
在前面的示例中,还可以更改联接表中外键列的名称。 可通过两种方式来执行此操作。 第一种方式是在联接实体上显式指定外键属性名称。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostForeignKey"));
}

第二种方式是保留属性的按约定名称,但将这些属性映射到不同的列名。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.Property("PostsId").HasColumnName("PostForeignKey");
                j.Property("TagsId").HasColumnName("TagForeignKey");
            });
}

在任一情况下,映射都保持不变,只更改了外键列名称:

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

虽然此处未显示,但可以组合上述两个示例来映射更改联接表名称及其外键列名。

具有联接实体的类的多对多
到目前为止,在示例中,联接表已自动映射到共享类型实体类型。 这样就无需为实体类型创建专用类。 但是,拥有此类可能很有用,以便可以轻松引用它,尤其是在将导航或有效负载添加到类时,如后面的示例所示。 为此,除了 Post 和 Tag 的现有类型之外,首先为联接实体创建类型 PostTag:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

提示

类可以具有任何名称,但通常会在关系的任一端合并类型的名称。

现在,UsingEntity 方法可用于将其配置为关系的联接实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

PostId 和 TagId 作为外键自动选取,并配置为联接实体类型的复合主键。 对于与 EF 约定不匹配的情况,可以显式配置用于外键的属性。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
            r => r.HasOne<Post>().WithMany().HasForeignKey(e => e.PostId));
}

此示例中联接表的映射数据库架构在结构上与前面的示例等效,但具有一些不同的列名:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有指向联接实体的导航的多对多
在前面的示例中,现在有一个表示联接实体的类,因此可以轻松添加引用此类的导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

重要

如本示例中所示,除了多对多关系两端之间的跳过导航外,还可以使用指向联接实体类型的导航。 这意味着,跳过导航可用于以自然方式与多对多关系进行交互,而当需要更好地控制联接实体本身时,可以使用指向联接实体类型的导航。 从某种意义上说,此映射在简单的多对多映射和更明确匹配数据库架构的映射之间提供了两者的最佳效果。

无需在 UsingEntity 调用中更改任何内容,因为指向联接实体的导航是按约定选取的。 因此,此示例的配置与上一个示例的配置相同:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

对于无法按约定确定导航的情况,可以显式配置导航。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany(e => e.PostTags),
            r => r.HasOne<Post>().WithMany(e => e.PostTags));
}

在模型中包括导航不会影响映射的数据库架构:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有指向和来自联接实体的导航的多对多
前面的示例从多对多关系任一端的实体类型向联接实体类型添加了导航。 还可以在其他方向或两个方向添加导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

无需在 UsingEntity 调用中更改任何内容,因为指向联接实体的导航是按约定选取的。 因此,此示例的配置与上一个示例的配置相同:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

对于无法按约定确定导航的情况,可以显式配置导航。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags));
}

在模型中包括导航不会影响映射的数据库架构:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有导航和更改外键的多对多
前面的示例演示了具有指向和来自联接实体类型的导航的多对多。 此示例是相同的,只不过所使用的外键属性也会更改。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostForeignKey { get; set; }
    public int TagForeignKey { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

同样,UsingEntity 方法用于进行如下配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasForeignKey(e => e.PostForeignKey));
}

映射的数据库架构现在为:

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

单向多对多
备注

EF Core 7 中引入了单向多对多关系。 在早期版本中,可以将专用导航用作解决方法。

不必在多对多关系两端包含导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
}

EF 需要一些配置才能知道这应该是多对多关系,而不是一对多关系。 这是使用 HasMany 和 WithMany 完成的,但在没有导航的情况下,在一端不会传递参数。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany();
}

删除导航不会影响数据库架构:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有有效负载的多对多和联接表
到目前为止,在示例中,联接表仅用于存储表示每个关联的外键对。 但是,它还可用于存储有关关联的信息,例如,关联的创建时间。 在这种情况下,最好为联接实体定义类型,并将“关联有效负载”属性添加到此类型。 除了用于多对多关系的“跳过导航”之外,还经常创建指向联接实体的导航。 这些附加导航允许从代码中轻松引用联接实体,从而有助于读取和/或更改有效负载数据。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public DateTime CreatedOn { get; set; }
}

对有效负载属性使用生成的值也很常见,例如,插入关联行时自动设置的数据库时间戳。 这需要一些最小配置。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

结果映射到具有插入行时自动设置的时间戳的实体类型架构:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

此处显示的 SQL 适用于 SQLite。 在 SQL Server/Azure SQL 上,使用 .HasDefaultValueSql("GETUTCDATE()") 并用 TEXT 读取 datetime。

作为联接实体的自定义共享类型实体类型
前面的示例使用类型 PostTag 作为联接实体类型。 此类型特定于帖子-标记关系。 但是,如果有多个具有相同形状的联接表,则可以对所有这些表使用相同的 CLR 类型。 例如,假设所有联接表都有 CreatedOn 列。 可以使用映射为共享类型实体类型的 JoinType 类进行映射:

public class JoinType
{
    public int Id1 { get; set; }
    public int Id2 { get; set; }
    public DateTime CreatedOn { get; set; }
}

然后,可以通过多个不同的多对多关系将此类型引用为联接实体类型。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Blog
{
    public int Id { get; set; }
    public List<Author> Authors { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

public class Author
{
    public int Id { get; set; }
    public List<Blog> Blogs { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

然后,可以适当配置这些关系,以便将联接类型映射到每个关系的不同表:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<JoinType>(
            "PostTag",
            l => l.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
            r => r.HasOne<Post>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));

    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Authors)
        .WithMany(e => e.Blogs)
        .UsingEntity<JoinType>(
            "BlogAuthor",
            l => l.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
            r => r.HasOne<Blog>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

这会导致数据库架构中出现以下表:

CREATE TABLE "BlogAuthor" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_BlogAuthor" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_BlogAuthor_Authors_Id1" FOREIGN KEY ("Id1") REFERENCES "Authors" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_BlogAuthor_Blogs_Id2" FOREIGN KEY ("Id2") REFERENCES "Blogs" ("Id") ON DELETE CASCADE);

CREATE TABLE "PostTag" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_PostTag_Posts_Id2" FOREIGN KEY ("Id2") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_Id1" FOREIGN KEY ("Id1") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有备用键的多对多
到目前为止,所有示例都演示将联接实体类型中的外键约束为关系任一端的实体类型的主键。 可以改为将每个外键约束为备用键。 例如,假设某个模型中的 Tag 和 Post 具有备用键属性:

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
}

此模型的配置为:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
            r => r.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}

为清楚起见,生成的数据库架构还包括具有备用键的表:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostsAlternateKey" INTEGER NOT NULL,
    "TagsAlternateKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsAlternateKey", "TagsAlternateKey"),
    CONSTRAINT "FK_PostTag_Posts_PostsAlternateKey" FOREIGN KEY ("PostsAlternateKey") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsAlternateKey" FOREIGN KEY ("TagsAlternateKey") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

如果联接实体类型由 .NET 类型表示,则使用备用键的配置略有不同。 例如:

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

配置现在可以使用泛型 UsingEntity<> 方法:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey));
}

生成的架构为:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

具有单独主键的多对多和联接表
到目前为止,所有示例中的联接实体类型都具有由两个外键属性组成的主键。 这是因为这些属性的每个值组合最多可以出现一次。 因此,这些属性构成了自然主键。

备注

EF Core 不支持任何集合导航中的重复实体。

如果控制数据库架构,则联接表没有理由具有其他主键列。但是,现有联接表可能定义了主键列。 EF 仍可通过某些配置映射到此处。

最简单的方法也许是创建一个表示联接实体的类。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int Id { get; set; }
    public int PostId { get; set; }
    public int TagId { get; set; }
}

现在,此 PostTag.Id 属性按约定选取为主键,因此,需要的唯一配置是调用 PostTag 类型的 UsingEntity:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

联接表的结果架构为:

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

还可以将主键添加到联接实体,而无需为其定义类。 例如,仅使用 Post 和 Tag 类型:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

可以使用以下配置添加键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.IndexerProperty<int>("Id");
                j.HasKey("Id");
            });
}

这会生成具有单独主键列的联接表:

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

无需级联删除的多对多
在上面显示的所有示例中,在联接表和多对多关系的两端之间创建的外键都是使用级联删除行为创建的。 这非常有用,因为这意味着如果删除关系任一端的实体,则会自动删除该实体的联接表中的行。 或者,换句话说,当实体不再存在时,它与其他实体的关系也不再存在。

很难想象更改此行为何时有用,但如果需要,可以执行此操作。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
            r => r.HasOne(typeof(Post)).WithMany().OnDelete(DeleteBehavior.Restrict));
}

联接表的数据库架构对外键约束使用受限的删除行为:

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE RESTRICT,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE RESTRICT);

自引用多对多
可以在多对多关系的两端使用相同的实体类型;这称为“自引用”关系。 例如:

public class Person
{
    public int Id { get; set; }
    public List<Person> Parents { get; } = [];
    public List<Person> Children { get; } = [];
}

这映射到名为 PersonPerson 的联接表,其中两个外键都指向 People 表:

CREATE TABLE "PersonPerson" (
    "ChildrenId" INTEGER NOT NULL,
    "ParentsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PersonPerson" PRIMARY KEY ("ChildrenId", "ParentsId"),
    CONSTRAINT "FK_PersonPerson_People_ChildrenId" FOREIGN KEY ("ChildrenId") REFERENCES "People" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PersonPerson_People_ParentsId" FOREIGN KEY ("ParentsId") REFERENCES "People" ("Id") ON DELETE CASCADE);

对称自引用多对多
有时,多对多关系是自然对称的。 也就是说,如果实体 A 与实体 B 相关,则实体 B 也与实体 A 相关。这是使用单个导航自然建模的。 例如,假设人员 A 是人员 B 的朋友,则人员 B 是人员 A 的朋友:

public class Person
{
    public int Id { get; set; }
    public List<Person> Friends { get; } = [];
}

遗憾的是,这并不容易映射。 不能对关系的两端使用相同的导航。 最好是将其映射为单向多对多关系。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasMany(e => e.Friends)
        .WithMany();
}

但是,为了确保两个人彼此相关,需要手动将每个人添加到对方的 Friends 集合中。 例如:

ginny.Friends.Add(hermione);
hermione.Friends.Add(ginny);

直接使用联接表
上述所有示例都使用 EF Core 多对多映射模式。 但是,也可以将联接表映射到普通实体类型,并且仅对所有操作使用两个一对多关系。

例如,这些实体类型表示两个正常表和联接表的映射,而不使用任何多对多关系:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

这不需要特殊的映射,因为这些是具有正常一对多关系的正常实体类型。

21
6

Entity Framework Core:一对多关系

0
归档:2024年6月分类:C#和.NET

当单个实体与任意数量的其他实体关联时,将使用一对多关系。 例如,Blog 可以有多个关联的 Posts,但每个 Post 都只与一个 Blog 相关联。

本文档采用围绕大量示例展开的结构。 这些示例从常见情况着手,还引入了一些概念。 后面的示例介绍了不太常见的配置类型。 此处介绍了一个不错的方法,即了解前几个示例和概念,再根据特定需求转到后面的示例。 基于此方法,我们将从简单的“必需”和“可选”的一对多关系开始。

提示

可在 OneToMany.cs 中找到以下所有示例的代码。

必需的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

一对多关系由以下部分组成:

主体实体上的一个或多个主键或备用键属性,即关系的“一”端。 例如 Blog.Id。
依赖实体上的一个或多个外键属性,即关系的“多”端。 例如 Post.BlogId。
(可选)引用依赖实体的主体实体上的集合导航。 例如 Blog.Posts。
(可选)引用主体实体的依赖实体上的引用导航。 例如 Post.Blog。
因此,对于此示例中的关系:

外键属性 Post.BlogId 不可为空。 这会使关系成为“必需”关系,因为每个依赖实体 (Post) 必须与某个主体实体 (Blog) 相关,而其外键属性必须设置为某个值。
这两个实体都有指向关系另一端的相关实体的导航。
备注

必需的关系可确保每个依赖实体都必须与某个主体实体相关联。 但是,主体实体可以在没有任何依赖实体的情况下始终存在。 也就是说,必需的关系并不表示始终存在至少一个依赖实体。 无论是在 EF 模型,还是在关系数据库中,都没有确保主体实体与特定数量的依赖实体相关联的标准方法。 如果需要,则必须在应用程序(业务)逻辑中实现它。 有关详细信息,请参阅必需的导航。

提示

具有两个导航的关系(一个是从依赖实体到主体实体,一个是从主体实体到依赖实体)称为双向关系。

此关系按约定发现。 即:

Blog 作为关系中的主体实体被发现,Post 作为依赖实体被发现。
Post.BlogId 作为引用主体实体的 Blog.Id 主键的依赖实体的外键被发现。 由于 Post.BlogId 不可为空,所以发现这一关系是必需的。
Blog.Posts 作为集合导航被发现。
Post.Blog 作为引用导航被发现。
重要

使用 C# 可为空引用类型时,如果外键属性可为空,则引用导航必须不可为空。 如果外键属性不可为空,则引用导航可以为空,也可以不为空。 在这种情况下,Post.BlogId 和 Post.Blog 皆不可为空。 = null!; 构造用于将此标记为 C# 编译器的有意行为,因为 EF 通常会对 Blog 实例进行设置,并且对于完全加载的关系,它不能为空。 有关详细信息,请参阅使用可为空引用类型。

对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

在上面的示例中,关系的配置从 主体实体类型 (Blog) 上的 HasMany 开始,然后是 WithOne。 与所有关系一样,它完全等效于从依赖实体类型开始 (Post),然后依次使用 HasOne 和 WithMany。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(e => e.Blog)
        .WithMany(e => e.Posts)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

与另一个选项相比,这两个选项并没有什么优势:它们都会导致完全相同的配置。

提示

没有必要对关系进行两次配置,即先从主体实体开始,又从依赖实体开始。 此外,尝试单独配置关系的主体实体和依赖实体通常不起作用。 选择从一端或另一端配置每个关系,然后只编写一次配置代码。

可选的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; } // Optional foreign key property
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

这与上一个示例相同,只不过外键属性和到主体实体的导航现在可为空。 这会使关系成为“可选”关系,因为依赖实体 (Post) 可以在无需与任何主体实体 (Blog) 相关的情况下存在。

重要

使用 C# 可为空引用类型时,如果外键属性可为空,则引用导航必须不可为空。 在本例中,Post.BlogId 不可为空,因此 Post.Blog 也不得为空。 有关详细信息,请参阅使用可为空引用类型。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired(false);
}

具有阴影外键的必需的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

在某些情况下,你可能不需要模型中的外键属性,因为外键是关系在数据库中表示方式的详细信息,而在完全以面向对象的方式使用关系时,不需要外键属性。 但是,如果要序列化实体(例如通过网络发送),则当实体不采用对象形式时,外键值可能是保持关系信息不变的有用方法。 因此,为实现此目的,务实的做法是在 .NET 类型中保留外键属性。 外键属性可以是私有的,这是一个折中的办法,既可以避免公开外键,又允许其值随实体一起传输。

继前面的两个示例后,此示例从依赖实体类型中删除外键属性。 EF 因此会创建一个名为 BlogId 的 int 类型的阴影外键属性。

此处需要注意的一个要点是,正在使用 C# 可为空引用类型,因此引用导航的可为空性用于确定外键属性是否可为空,进而确定关系是可选的还是必需的。 如果未使用可为空引用类型,则默认情况下,阴影外键属性将为空,使关系默认为可选。 在这种情况下,使用 IsRequired 强制阴影外键属性为不可为空,并使关系成为必需关系。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("BlogId")
        .IsRequired();
}

具有阴影外键的可选一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

与前面的示例一样,外键属性已从依赖实体类型中移除。 EF 因此会创建一个名为 BlogId 的 int? 类型的阴影外键属性。 与前面的示例不同,这次外键属性创建为可为空,因为正在使用 C# 可为空引用类型,并且依赖实体类型的导航可为空。 这会使关系成为可选关系。

如果未使用 C# 可为空引用类型,则默认情况下,外键属性也将创建为可为空。 这意味着,与自动创建的阴影属性的关系默认可选。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("BlogId")
        .IsRequired(false);
}

无需导航到主体实体的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

在此示例中,外键属性已重新引入,但依赖实体上的导航已被移除。

提示

只有一个导航的关系(即从依赖实体到主体实体,或从主体实体到依赖实体,但只有其中一个)的被称单向关系。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

请注意,对 WithOne 的调用没有参数。 这是告知 EF 没有从 Post 到 Blog 的导航的方式。

如果从没有导航的实体开始配置,则必须使用泛型 HasOne<>() 调用显式指定关系另一端的实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne<Blog>()
        .WithMany(e => e.Posts)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

无需导航到主体实体且有阴影外键的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
}

此示例通过移除外键属性和依赖实体上的导航来合并上述两个示例。

此关系按约定作为可选关系被发现。 由于代码中没有任何内容可以用来指示它是必需的,因此需要使用 IsRequired 进行最小配置来创建必需关系。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .IsRequired();
}

可以使用更完整的配置来显式配置导航和外键名称,并根据需要对 IsRequired() 或 IsRequired(false) 进行适当的调用。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .HasForeignKey("BlogId")
        .IsRequired();
}

无需导航到依赖实体的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

前两个示例具有从主体实体到依赖实体的导航,但没有从依赖实体到主体实体的导航。 在接下来的几个示例中,将重新引入依赖实体上的导航,而主体上的导航将被移除。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(e => e.Blog)
        .WithMany()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

请注意,WithMany() 调用时没有参数,可指示此方向没有导航。

如果从没有导航的实体开始配置,则必须使用泛型 HasMany<>() 调用显式指定关系另一端的实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

无导航的一对多
有时,配置无导航的关系可能很有用。 此类关系只能通过直接更改外键值来操作。

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

此关系不会按约定发现,因为没有任何导航指示这两种类型是相关的。 可以在 OnModelCreating 中显式配置它。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne();
}

使用此配置时,按照约定,Post.BlogId 属性仍被检测为外键,并且关系是必需的,因为外键属性不可为空。 通过将外键属性设为“可为空”,可以使关系成为“可选”关系。

此关系的更完整显式配置是:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

具有备用键的一对多
在到目前为止的所有示例中,依赖实体上的外键属性被约束为主体实体上的主键属性。 外键可以改为被约束为不同的属性,该属性随后成为主体实体类型的备用键。 例如:

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public int AlternateId { get; set; } // Alternate key as target of the Post.BlogId foreign key
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

此关系不是按约定发现的,因为 EF 始终按照约定创建与主键的关系。 可以使用对 HasPrincipalKey 的调用在 OnModelCreating 中显式配置它。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey(e => e.AlternateId);
}

HasPrincipalKey 可与其他调用结合使用,以显式配置导航、外键属性和必需/可选性质。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey(e => e.AlternateId)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

具有复合外键的一对多
在到目前为止的所有示例中,主体实体的主键或备用键属性由单个属性组成。 利用多个属性也可以形成主键或备用键,这些键称为“组合键”。 当关系的主体实体具有组合键时,依赖实体的外键也必须是具有相同属性数的组合键。 例如:

// Principal (parent)
public class Blog
{
    public int Id1 { get; set; } // Composite key part 1
    public int Id2 { get; set; } // Composite key part 2
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId1 { get; set; } // Required foreign key property part 1
    public int BlogId2 { get; set; } // Required foreign key property part 2
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

此关系按约定发现。 但是,需要显式配置组合键本身:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasKey(e => new { e.Id1, e.Id2 });
}

重要

如果组合外键值的任何属性值为空,则认为其值为 null。 具有一个属性“空”和另一个属性“非空”的组合外键不会被视为与具有相同值的主键或备用键匹配。 两者都将被视为 null。

HasForeignKey 和 HasPrincipalKey 都可用于显式指定具有多个属性的键。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(
        nestedBuilder =>
        {
            nestedBuilder.HasKey(e => new { e.Id1, e.Id2 });

            nestedBuilder.HasMany(e => e.Posts)
                .WithOne(e => e.Blog)
                .HasPrincipalKey(e => new { e.Id1, e.Id2 })
                .HasForeignKey(e => new { e.BlogId1, e.BlogId2 })
                .IsRequired();
        });
}

提示

在上面的代码中,对 HasKey 和 HasMany 的调用已组合到嵌套生成器中。 使用嵌套生成器,无需为同一实体类型多次调用 Entity<>(),但在功能上等效于多次调用 Entity<>()。

无需级联删除的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

按照约定,必需关系配置为级联删除,这意味着,删除主体实体后,也会删除其所有依赖实体,因为依赖实体无法存在于每月主体实体的数据库中。 可以将 EF 配置为引发异常,而不是自动删除不再存在的依赖行:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .OnDelete(DeleteBehavior.Restrict);
}

自引用一对多
在前面的所有示例中,主体实体类型与依赖实体类型有所不同。 情况不一定如此。 例如,在下面的类型中,每个 Employee 都与另一个 Employees 相关。

public class Employee
{
    public int Id { get; set; }

    public int? ManagerId { get; set; } // Optional foreign key property
    public Employee? Manager { get; set; } // Optional reference navigation to principal
    public ICollection<Employee> Reports { get; } = new List<Employee>(); // Collection navigation containing dependents
}

此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .HasOne(e => e.Manager)
        .WithMany(e => e.Reports)
        .HasForeignKey(e => e.ManagerId)
        .IsRequired(false);
}
20
6

Entity Framework Core:一对一关系

0
归档:2024年6月分类:C#和.NET

当一个实体与最多一个其他实体关联时,将使用一对一关系。 例如,Blog 有一个 BlogHeader,并且 BlogHeader 属于单个 Blog。

本文档采用围绕大量示例展开的结构。 这些示例从常见情况着手,还引入了一些概念。 后面的示例介绍了不太常见的配置类型。 此处介绍了一个不错的方法,即了解前几个示例和概念,再根据特定需求转到后面的示例。 基于此方法,我们将从简单的“必需”和“可选”的一对一关系开始。

可在 OneToOne.cs 中找到以下所有示例的代码。

必需的一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

一对一关系由以下部分组成:

主体实体上的一个或多个主键或备用键属性。 例如 Blog.Id。
依赖实体上的一个或多个外键属性。 例如 BlogHeader.BlogId。
(可选)引用依赖实体的主体实体上的引用导航。 例如 Blog.Header。
(可选)引用主体实体的依赖实体上的引用导航。 例如,BlogHeader.Blog。
提示

一对一关系的哪一端应是主体实体,哪一端应是依赖实体,这并不总是显而易见的。 请注意以下事项:

如果两种类型的数据库表已存在,则具有外键列的表必须映射到依赖类型。
如果某个类型在逻辑上不能在没有另一个类型的情况下存在,则它通常是依赖类型。 例如,对于不存在的博客,它的标题是没有意义的,因此 BlogHeader 自然是依赖类型。
如果存在自然的父/子关系,则子级通常是依赖类型。
因此,对于此示例中的关系:

外键属性 BlogHeader.BlogId 不可为空。 这会使关系成为“必需”关系,因为每个依赖实体 (BlogHeader) 必须与某个主体实体 (Blog) 相关,而其外键属性必须设置为某个值。
这两个实体都有指向关系另一端的相关实体的导航。
备注

必需的关系可确保每个依赖实体都必须与某个主体实体相关联。 但是,主体实体可以在没有任何依赖实体的情况下始终存在。 也就是说,必需的关系并不表示始终存在依赖实体。 无论是在 EF 模型,还是在关系数据库中,都没有确保主体实体与依赖实体相关联的标准方法。 如果需要,则必须在应用程序(业务)逻辑中实现它。 有关详细信息,请参阅必需的导航。

提示

具有两个导航的关系(一个是从依赖实体到主体实体,一个是从主体实体到依赖实体)称为双向关系。

此关系按约定发现。 即:

Blog 作为关系中的主体实体被发现,BlogHeader 作为依赖实体被发现。
BlogHeader.BlogId 作为引用主体实体的 Blog.Id 主键的依赖实体的外键被发现。 由于 BlogHeader.BlogId 不可为空,所以发现这一关系是必需的。
Blog.BlogHeader 作为引用导航被发现。
BlogHeader.Blog 作为引用导航被发现。
重要

使用 C# 可为空引用类型时,如果外键属性可为空,则从依赖实体到主体实体的导航必须可为空。 如果外键属性不可为空,则导航可以为空,也可以不为空。 在这种情况下,BlogHeader.BlogId 和 BlogHeader.Blog 皆不可为空。 = null!; 构造用于将此标记为 C# 编译器的有意行为,因为 EF 通常会对 Blog 实例进行设置,并且对于完全加载的关系,它不能为空。 有关详细信息,请参阅使用可为空引用类型。

对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

在上面的示例中,关系的配置将启动主体实体类型 (Blog)。 与所有关系一样,它完全等效于从依赖实体类型 (BlogHeader) 开始。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogHeader>()
        .HasOne(e => e.Blog)
        .WithOne(e => e.Header)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

与另一个选项相比,这两个选项并没有什么优势:它们都会导致完全相同的配置。

提示

没有必要对关系进行两次配置,即先从主体实体开始,又从依赖实体开始。 此外,尝试单独配置关系的主体实体和依赖实体通常不起作用。 选择从一端或另一端配置每个关系,然后只编写一次配置代码。

可选的一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int? BlogId { get; set; } // Optional foreign key property
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

这与上一个示例相同,只不过外键属性和到主体实体的导航现在可为空。 这会使关系成为“可选”关系,因为依赖实体 (BlogHeader) 不能通过将其外键属性和导航设置为 null 来与任何主体 (Blog) 相关。

使用 C# 可为空引用类型时,如果外键属性可为空,则从依赖实体到主体实体的导航属性必须可为空。 在本例中,BlogHeader.BlogId 不可为空,因此 BlogHeader.Blog 也不得为空。 有关详细信息,请参阅使用可为空引用类型。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired(false);
}

具有主键到主键关系的必需的一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

与一对多关系不同,一对一关系的依赖端可以将其主键属性用作外键属性。 这通常称为 PK 到 PK 关系。 仅当主体类型和依赖类型具有相同的主键类型,并且始终需要生成的关系时才有可能,因为依赖类型的主键不可为空。

必须配置未按约定发现外键的任何一对一关系,以指示关系的主体端和依赖端。 这通常是使用对 HasForeignKey 的调用来完成的。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>();
}

HasPrincipalKey 也可以用于此目的,但这样做并不常见。

如果对 HasForeignKey 的调用中未指定任何属性,并且主键适用,则将其用作外键。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>(e => e.Id)
        .IsRequired();
}

具有阴影外键的必需的一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

在某些情况下,你可能不需要模型中的外键属性,因为外键是关系在数据库中表示方式的详细信息,而在完全以面向对象的方式使用关系时,不需要外键属性。 但是,如果要序列化实体(例如通过网络发送),则当实体不采用对象形式时,外键值可能是保持关系信息不变的有用方法。 因此,为实现此目的,务实的做法是在 .NET 类型中保留外键属性。 外键属性可以是私有的,这是一个折中的办法,既可以避免公开外键,又允许其值随实体一起传输。

继前面的示例后,此示例从依赖实体类型中删除外键属性。 但会指示 EF 创建一个名为 BlogId 的 int 类型的阴影外键属性,而不是使用主键。

此处需要注意的一个要点是,正在使用 C# 可为空引用类型,因此从依赖实体到主体实体的导航的可为空性用于确定外键属性是否可为空,进而确定关系是可选的还是必需的。 如果未使用可为空引用类型,则默认情况下,阴影外键属性将为空,使关系默认为可选。 在这种情况下,使用 IsRequired 强制阴影外键属性为不可为空,并使关系成为必需关系。

此关系再次需要一些配置来指示主体端和依赖端:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>("BlogId");
}

对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>("BlogId")
        .IsRequired();
}

具有阴影外键的可选一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

与前面的示例一样,外键属性已从依赖实体类型中移除。 但是,与前面的示例不同,这次外键属性创建为“可为空”,因为正在使用 C# 可为空引用类型,并且依赖实体类型的导航可为空。 这会使关系成为可选关系。

如果未使用 C# 可为空引用类型,则默认情况下,外键属性将创建为“可为空”。 这意味着,与自动创建的阴影属性的关系默认可选。

如前所述,此关系需要一些配置来指示主体端和依赖端:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>("BlogId");
}

对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>("BlogId")
        .IsRequired(false);
}

无需导航到主体实体的一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

在此示例中,外键属性已重新引入,但依赖实体上的导航已被移除。

只有一个导航的关系(即从依赖实体到主体实体,或从主体实体到依赖实体,但只有其中一个)称为单向关系。

此关系按约定发现,因为会发现外键,从而指示依赖端。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne()
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

请注意,对 WithOne 的调用没有参数。 这是告知 EF 没有从 BlogHeader 到 Blog 的导航的方式。

如果从没有导航的实体开始配置,则必须使用泛型 HasOne<>() 调用显式指定关系另一端的实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogHeader>()
        .HasOne<Blog>()
        .WithOne(e => e.Header)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

无需导航到主体实体且有阴影外键的一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
}

此示例通过移除外键属性和依赖实体上的导航来合并上述两个示例。

如前所述,此关系需要一些配置来指示主体端和依赖端:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne()
        .HasForeignKey<BlogHeader>("BlogId")
        .IsRequired();
}

可以使用更完整的配置来显式配置导航和外键名称,并根据需要对 IsRequired() 或 IsRequired(false) 进行适当的调用。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne()
        .HasForeignKey<BlogHeader>("BlogId")
        .IsRequired();
}

无需导航到依赖实体的一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

前两个示例具有从主体实体到依赖实体的导航,但没有从依赖实体到主体实体的导航。 在接下来的几个示例中,将重新引入依赖实体上的导航,而主体上的导航将被移除。

按照约定,EF 会将其视为一对多关系。 需要一些最小配置才能实现一对一:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogHeader>()
        .HasOne(e => e.Blog)
        .WithOne();
}

请注意,WithOne() 调用时没有参数,可指示此方向没有导航。

对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogHeader>()
        .HasOne(e => e.Blog)
        .WithOne()
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

如果从没有导航的实体开始配置,则必须使用泛型 HasOne<>() 调用显式指定关系另一端的实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne<BlogHeader>()
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

无导航的一对一
有时,配置无导航的关系可能很有用。 此类关系只能通过直接更改外键值来操作。

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

此关系不会按约定发现,因为没有任何导航指示这两种类型是相关的。 可以在 OnModelCreating 中显式配置它。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne<BlogHeader>()
        .WithOne();
}

使用此配置时,按照约定,BlogHeader.BlogId 属性仍被检测为外键,并且关系是“必需的”,因为外键属性不可为空。 通过将外键属性设为“可为空”,可以使关系成为“可选”关系。

此关系的更完整显式配置是:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne<BlogHeader>()
        .WithOne()
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

具有备用键的一对一
在到目前为止的所有示例中,依赖实体上的外键属性被约束为主体实体上的主键属性。 外键可以改为被约束为不同的属性,该属性随后成为主体实体类型的备用键。 例如:

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public int AlternateId { get; set; } // Alternate key as target of the BlogHeader.BlogId foreign key
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

此关系不是按约定发现的,因为 EF 始终按照约定创建与主键的关系。 可以使用对 HasPrincipalKey 的调用在 OnModelCreating 中显式配置它。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasPrincipalKey<Blog>(e => e.AlternateId);
}

HasPrincipalKey 可与其他调用结合使用,以显式配置导航、外键属性和必需/可选性质。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasPrincipalKey<Blog>(e => e.AlternateId)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

具有复合外键的一对一
在到目前为止的所有示例中,主体实体的主键或备用键属性由单个属性组成。 主键或备用键也可以形成多个属性,这些属性称为“组合键”。 当关系的主体实体具有组合键时,依赖实体的外键也必须是具有相同属性数的组合键。 例如:

// Principal (parent)
public class Blog
{
    public int Id1 { get; set; } // Composite key part 1
    public int Id2 { get; set; } // Composite key part 2
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId1 { get; set; } // Required foreign key property part 1
    public int BlogId2 { get; set; } // Required foreign key property part 2
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
此关系按约定发现。 但是,只有在已显式配置组合键时才会发现它,因为不会自动发现组合键。 例如:

```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity()
        .HasKey(e => new { e.Id1, e.Id2 });
}

如果组合外键值的任何属性值为空,则认为其值为 null。 具有一个属性“空”和另一个属性“非空”的组合外键不会被视为与具有相同值的主键或备用键匹配。 两者都将被视为 null。

HasForeignKey 和 HasPrincipalKey 都可用于显式指定具有多个属性的键。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity(
        nestedBuilder =>
        {
            nestedBuilder.HasKey(e => new { e.Id1, e.Id2 });

            nestedBuilder.HasOne(e => e.Header)
                .WithOne(e => e.Blog)
                .HasPrincipalKey(e => new { e.Id1, e.Id2 })
                .HasForeignKey(e => new { e.BlogId1, e.BlogId2 })
                .IsRequired();
        });
}

在上面的代码中,对 HasKey 和 HasOne 的调用已组合到嵌套生成器中。 使用嵌套生成器,无需为同一实体类型多次调用 Entity<>(),但在功能上等效于多次调用 Entity<>()。

无需级联删除的一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

根据约定,所需的关系配置为级联删除。 这是因为在删除主体实体后,数据库中就不能存在依赖实体。 可以将数据库配置为生成错误,通常会使应用程序崩溃,而不是自动删除不再存在的依赖行。 这需要一些配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .OnDelete(DeleteBehavior.Restrict);
}

自引用一对一
在前面的所有示例中,主体实体类型与依赖实体类型有所不同。 情况不一定如此。 例如,在下面的类型中,每个 Person 类型都可选择与另一个 Person 相关。

public class Person
{
    public int Id { get; set; }

    public int? HusbandId { get; set; } // Optional foreign key property
    public Person? Husband { get; set; } // Optional reference navigation to principal
    public Person? Wife { get; set; } // Reference navigation to dependent
}

此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity()
        .HasOne(e => e.Husband)
        .WithOne(e => e.Wife)
        .HasForeignKey(e => e.HusbandId)
        .IsRequired(false);
}

对于一对一自引用关系,由于主体实体类型和依赖实体类型相同,指定包含外键的类型不会阐明依赖端。 在本例中,从依赖实体到主体实体以 HasOne 点为单位指定的导航,以及从主体实体到依赖实体以 WithOne 点为单位指定的导航。

12
6

本教程提供了使用 SignalR 和 Blazor 生成实时应用的基本工作经验。 本文适用于已经熟悉 SignalR 并正在寻求了解如何在 SignalR 应用中使用 Blazor 的开发人员。 有关 SignalR 和 Blazor 框架的详细指南,请参阅以下参考文档集和 API 文档:

ASP.NET Core SignalR 概述
ASP.NET Core Blazor
.NET API 浏览器
了解如何:

创建 Blazor 应用
添加 SignalR 客户端库
添加 SignalR 集线器
添加 SignalR 服务和 SignalR 中心的终结点
添加用于聊天的 Razor 组件代码
在本教程结束时,你将拥有一个正常运行的聊天应用。

先决条件
Visual Studio
Visual Studio Code
.NET CLI
具有“ASP.NET 和 Web 开发”工作负载的 Visual Studio 2022 或更高版本

示例应用
本教程不需要下载教程的示例聊天应用。 示例应用是按照本教程的步骤生成的最终工作应用。

查看或下载示例代码(如何下载)

创建 Blazor Web 应用
按照所选工具的指南进行操作:

Visual Studio
Visual Studio Code
.NET CLI
备注

需要 Visual Studio 2022 或更高版本以及 .NET Core SDK 8.0.0 或更高版本。

创建新项目。

选择“Blazor Web 应用”模板。 选择“下一步”。

在“项目名称”字段中键入 BlazorSignalRApp。 确认“位置”条目正确无误或为项目提供位置。 选择下一步。

确认 Framework 是 .NET 8 或更高版本。 选择“创建”。

添加 SignalR 客户端库
Visual Studio
Visual Studio Code
.NET CLI
在“解决方案资源管理器”中,右键单击 BlazorSignalRApp 项目,然后选择“管理 NuGet 包” 。

在“管理 NuGet 包”对话框中,确认“包源”设置为“nuget.org” 。

选择“浏览”后,在搜索框中键入“Microsoft.AspNetCore.SignalR.Client”。

在搜索结果中,选择 Microsoft.AspNetCore.SignalR.Client 包的最新版本。 选择“安装” 。

如果出现“预览更改”对话框,则选择“确定”。

如果出现“许可证接受”对话框,如果你同意许可条款,请选择“我接受”。

添加 SignalR 集线器
创建 Hubs(复数)文件夹,并将以下 ChatHub 类 (Hubs/ChatHub.cs) 添加到应用的根目录:

C#

复制
using Microsoft.AspNetCore.SignalR;

namespace BlazorSignalRApp.Hubs;

public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
为 SignalR 中心添加服务和终结点
打开 Program 文件。

将 Microsoft.AspNetCore.ResponseCompression 和 ChatHub 类的命名空间添加到文件的顶部:

C#

复制
using Microsoft.AspNetCore.ResponseCompression;
using BlazorSignalRApp.Hubs;
添加响应压缩中间件服务:

C#

复制
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
在处理管道的配置顶部使用响应压缩中间件:

C#

复制
app.UseResponseCompression();
紧接在映射 Razor 组件 (app.MapRazorComponents()) 的行后面,为行添加一个终结点:

C#

复制
app.MapHub("/chathub");
添加用于聊天的 Razor 组件代码
打开 Components/Pages/Home.razor 文件。

将标记替换为以下代码:


@page "/"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable

Home


    @foreach (var message in messages) {
  • @message
  • }
@code { private HubConnection? hubConnection; private List messages = new List(); private string? userInput; private string? messageInput; protected override async Task OnInitializedAsync() { hubConnection = new HubConnectionBuilder() .WithUrl(Navigation.ToAbsoluteUri("/chathub")) .Build(); hubConnection.On("ReceiveMessage", (user, message) => { var encodedMsg = $"{user}: {message}"; messages.Add(encodedMsg); InvokeAsync(StateHasChanged); }); await hubConnection.StartAsync(); } private async Task Send() { if (hubConnection is not null) { await hubConnection.SendAsync("SendMessage", userInput, messageInput); } } public bool IsConnected => hubConnection?.State == HubConnectionState.Connected; public async ValueTask DisposeAsync() { if (hubConnection is not null) { await hubConnection.DisposeAsync(); } } }
12
6

Blazor server VS Blazor WebAssembly

0
归档:2024年6月分类:C#和.NET

Blazor WebAssembly
主要的 Blazor 托管模型在 WebAssembly 上的浏览器中运行客户端。 将 Blazor 应用、其依赖项以及 .NET 运行时下载到浏览器。 应用将在浏览器线程中直接执行。 UI 更新和事件处理在同一进程中进行。 应用资产作为静态文件部署到可为客户端提供静态内容的 Web 服务器或服务中。

file
Blazor WebAssembly:Blazor 应用在浏览器内部的 UI 线程上运行。

如果创建了 Blazor WebAssembly 应用进行部署,但没有后端 ASP.NET Core 应用来为其文件提供服务,那么该应用被称为独立 Blazor WebAssembly 应用。 如果创建了应用进行部署,但没有后端应用来为其文件提供服务,那么该应用被称为托管的 Blazor WebAssembly 应用。 托管的 Blazor WebAssembly Client 应用通常使用 Web API 调用或 SignalR(结合使用 ASP.NET Core SignalR 和 Blazor)通过网络与后端 Server 应用交互。

blazor.webassembly.js 脚本由框架和句柄提供:

下载 .NET 运行时、应用和应用依赖项。
初始化运行应用的运行时。
Blazor WebAssembly 托管模型具有以下优点:

没有 .NET 服务器端依赖项。 应用下载到客户端后即可正常运行。
可充分利用客户端资源和功能。
工作可从服务器转移到客户端。
无需 ASP.NET Core Web 服务器即可托管应用。 无服务器部署方案可行,例如通过内容分发网络 (CDN) 为应用提供服务的方案。
Blazor WebAssembly 托管模型具有以下局限性:

应用仅可使用浏览器功能。
需要可用的客户端硬件和软件(例如 WebAssembly 支持)。
下载项大小较大,应用加载耗时较长。
.NET 运行时和工具支持不够完善。 例如,.NET Standard 支持和调试方面存在限制。
若要创建 Blazor WebAssembly 应用,请参阅 用于 ASP.NET Core Blazor 的工具。

Blazor 托管应用模型支持 Docker 容器。 对于 Visual Studio 中的 Docker 支持,请右键单击托管的 Blazor WebAssembly 解决方案的 Server 项目,然后选择“添加” > “Docker 支持”。

Blazor Server
使用 Blazor Server 托管模型可从 ASP.NET Core 应用中在服务器上执行应用。 UI 更新、事件处理和 JavaScript 调用是通过 SignalR 连接进行处理。

浏览器通过 SignalR 连接与服务器上的应用(该应用托管在 ASP.NET Core 应用内部)进行交互。

ASP.NET Core 应用会引用应用的 Startup 类以添加以下内容:

服务器端服务。
用于请求处理管道的应用。
在客户端上,blazor.server.js 脚本与服务器建立 SignalR 连接。 脚本由 ASP.NET Core 共享框架中的嵌入资源提供给客户端应用。 客户端应用负责根据需要保持和还原应用状态。
file
Blazor Server 托管模型具有以下优点:

下载项大小明显小于 Blazor WebAssembly 应用,且应用加载速度快得多。
应用可充分利用服务器功能,包括使用任何与 .NET Core 兼容的 API。
服务器上的 .NET Core 用于运行应用,因此调试等现有 .NET 工具可按预期正常工作。
支持瘦客户端。 例如,Blazor Server 应用适用于不支持 WebAssembly 的浏览器以及资源受限的设备。
应用的 .NET/C# 代码库(其中包括应用的组件代码)不适用于客户端。
重要

Blazor Server 应用预呈现以响应第一个客户端请求,这会在服务器上创建 UI 状态。 客户端尝试创建 SignalR 连接时,“必须重新连接到同一服务器”。 使用多个后端服务器的 Blazor Server 应用应实现粘滞会话,从而建立 SignalR 连接。 有关更多信息,请参见连接到服务器一节。

Blazor Server 托管模型具有以下局限性:

通常延迟较高。 每次用户交互都涉及到网络跃点。
不支持脱机工作。 如果客户端连接失败,应用会停止工作。
如果具有多名用户,则应用扩缩性存在挑战。 服务器必须管理多个客户端连接并处理客户端状态。
需要 ASP.NET Core 服务器为应用提供服务。 无服务器部署方案不可行,例如通过内容分发网络 (CDN) 为应用提供服务的方案。
若要创建 Blazor Server 应用,请参阅 用于 ASP.NET Core Blazor 的工具。

Blazor Server 应用模型支持 Docker 容器。 对于 Visual Studio 中的 Docker 支持,请右键单击 Visual Studio 中的项目,然后选择“添加” > “Docker 支持” 。

与服务器呈现的 UI 进行比较
理解 Blazor Server 应用的一种方法是,了解其与使用 Razor 视图或 Razor Pages 在 ASP.NET Core 应用中呈现 UI 的惯用模型之间的差异。 这两种模型都使用 Razor 语言描述 HTML 内容,但两者在标记的呈现方式上差别显著。

呈现 Razor 页面或视图时,每行 Razor 代码都以文本形式发出 HTML。 呈现后,服务器会丢弃页面或视图实例,包括生成的任何状态。 出现另一个对该页面的请求时,例如,服务器验证失败并显示验证摘要时:

整个页面将再次重新呈现为 HTML 文本。
页面会发送到客户端。
Blazor 应用由 UI 的可重用元素组成,这些元素称为组件。 组件包含 C# 代码、标记和其他成分。 呈现组件时,Blazor 会生成所含组件的图,类似于 HTML 或 XML 文档对象模型 (DOM)。 此图包含属性和字段中保存的组件状态。 Blazor 会评估组件图,生成二进制形式的标记。 二进制格式可以:

转换为 HTML 文本(预呈现 † 期间)。
用于在定期呈现期间高效更新标记。
†预呈现:请求获取的 Razor 组件在服务器上编译为静态 HTML,并发送到客户端,然后呈现给用户。 客户端与服务器之间建立连接后,组件的静态预呈现元素会替换为交互式元素。 预呈现会使应用对用户的响应更加迅速。

Blazor 中的 UI 更新由以下内容触发:

用户交互,例如选中按钮。
应用触发器,例如计时器。
组件组已预呈现,且已计算 UI diff(差异)。 此差异是更新客户端上的 UI 所需的最小一组 DOM 编辑。 差异以二进制格式发送到客户端,并由浏览器应用。

用户在客户端上退出组件之后,组件会被丢弃。 用户与组件交互时,组件的状态(服务、资源)必须保存在服务器的内存中。 由于服务器可能同时保存多个组件的状态,因此必须解决内存不足的问题。 要了解如何创作 Blazor Server 应用以确保充分使用服务器内存,请参阅 ASP.NET Core Blazor Server 的威胁缓解指南。

线路
Blazor Server 应用基于 ASP.NET CoreSignalR 构建。 每个客户端通过一个或多个称为“线路”的 SignalR 连接与服务器通信。 线路是 Blazor 对可容忍短暂网络中断的 SignalR 连接的抽象。 Blazor 客户端发现 SignalR 连接已断开时,它会尝试使用新的 SignalR 连接来重新连接到服务器。

连接到 Blazor Server 应用的每个浏览器屏幕(浏览器标签页或 iframe)均使用 SignalR 连接。 与典型服务器呈现应用相比,这是另一个关键差异。 在服务器呈现应用的多个浏览器屏幕中打开同一应用通常不需要服务器上的其他资源。 在 Blazor Server 应用中,若服务器要管理浏览器屏幕,则每个浏览器屏幕均需要独立线路和组件状态的独立实例。

Blazor 将关闭浏览器标签页或访问外部 URL 视为正常终止。 如果正常终止,则会立即释放线路和关联的资源。 例如,由于网络中断,客户端也可能异常地断开连接。 Blazor Server 会将断开连接的路线存储一段时间(可配置),以便客户端重新连接。

Blazor Server 允许代码定义线路处理程序,后者允许在用户线路的状态发生更改时运行代码。 有关详细信息,请参阅 ASP.NET Core Blazor SignalR 指南。

UI 延迟
UI 延迟是指从启动操作到 UI 更新所需的时间。 要使应用对用户进行响应,需要 UI 延迟值较小。 在 Blazor Server 应用中,每个操作都会发送到服务器并进行处理,然后发回 UI 差异。 因此,UI 延迟是网络延迟和处理操作时的服务器延迟的总和。

如果是仅用于专用公司网络的商业应用程序,用户通常不易感受到因网络延迟而导致的延迟。 如果是通过 Internet 部署的应用,用户可能会更容易感受到延迟,用户分布广泛时感受尤为明显。

内存使用也会导致应用延迟。 内存使用率增大会导致垃圾收集频繁或内存分页到磁盘,两者均会降低应用性能,进而增大 UI 延迟。

Blazor Server 应用应降低网络延迟和内存使用率,从而优化以最大限度地降低 UI 延迟。 有关测量网络延迟的方法,请参阅 托管和部署 ASP.NET Core Blazor Server。 有关 SignalR 和 Blazor 的详细信息,请参阅以下内容:

托管和部署 ASP.NET Core Blazor Server
ASP.NET Core Blazor Server 的威胁缓解指南
连接到服务器
Blazor Server 应用需要与服务器建立有效的 SignalR 连接。 如果连接丢失,应用会尝试重新连接到服务器。 只要客户端的状态仍在服务器的内存中,客户端会话即可恢复,且不会失去状态。

Blazor Server 应用预呈现以响应第一个客户端请求,这会在服务器上创建 UI 状态。 客户端尝试创建 SignalR 连接时,必须重新连接到同一服务器。 使用多个后端服务器的 Blazor Server 应用应实现粘滞会话,从而建立 SignalR 连接。

我们建议将 Azure SignalR 服务用于 Blazor Server 应用。 该服务允许将 Blazor Server 应用扩展到大量并发 SignalR 连接。 可将服务的 ServerStickyMode 选项或配置值设置为 Required,从而为 Azure SignalR 服务启用粘滞会话。 有关详细信息,请参阅 托管和部署 ASP.NET Core Blazor Server。

使用 IIS 时,粘滞会话通过应用程序请求路由启用。 有关详细信息,请参阅使用应用程序请求路由实现 HTTP 负载均衡。

公告栏

欢迎大家来到我的博客,我是dodoro,希望我的博客能给你带来帮助。