设计时工具体系结构

设计时工具是 EF 的一部分,用于启动设计时作,例如搭建模型或管理迁移。 它们负责实例化 DbContext 对象,以便在设计时使用。

概要工具流 Mermaid 关系图。

有两个主要入口点:dotnet-ef 和 NuGet 包管理器控制台 (PMC) EF Core 工具。 这两者都负责收集有关用户项目的信息,编译它们,然后调用 ef.exe,而 ef.exe 最终会调用 EFCore.Design.dll 中的设计时入口点。

dotnet-ef

dotnet-ef 是一个 .NET 工具。 dotnet- prefix 允许它作为主 dotnet 命令的一部分调用:dotnet ef

此命令有两个主要输入:启动项目和目标项目。 dotnet-ef 负责读取有关这些项目的信息,然后编译它们。

dotnet-ef 流 Mermaid 图。

它通过注入 MSBuild .targets 文件并调用自定义 MSBuild 目标来读取有关项目的信息。 .targets 文件作为嵌入资源集成到 dotnet-ef 中。 源位于 src/dotnet-ef/Resources/EntityFrameworkCore.targets

它的开头有一些用于处理多目标项目的逻辑。 从本质上讲,它只需选取第一个目标框架并重新调用自身。 确定单个目标框架后,它会获取多个 MSBuild 属性,例如 AssemblyName、OutputPath、RootNamespace 等。

收集项目信息后,我们将编译启动项目。 我们假设目标项目也将传递性地进行编译。

然后 dotnet-ef 调用 ef.exe。

PMC 工具

PMC 工具执行与 dotnet-ef 类似的功能,但使用 Visual Studio API 而不是 MSBuild。 它们以 Microsoft.EntityFrameworkCore.Tools 软件包的形式发布。 它们是一个特殊的 VS 感知 PowerShell 模块,通过 init.ps1 自动加载到 NuGet 程序包管理器控制台中。 与 dotnet-ef 一样,每个命令采用两个主要输入:启动项目和目标项目。 但这些默认值取自 IDE。 目标项目默认为在程序包管理器控制台中指定为“默认项目”的项目。 启动项目默认为在解决方案资源管理器中(通过“设置为启动项目”)指定为启动项目的项目。

PMC 工具流 Mermaid 图。

PMC 工具尽可能通过 EnvDTE API 收集有关项目的信息。 有时,它需要下降到通用项目系统 (CPS) 或 MSBuild API。 GitHub 上的 dotnet/project-system 项目下提供了新式 C# 项目系统实现源。

收集信息后,它将生成整个解决方案。

提示

#9716 是关于将其更新为仅生成启动项目的问题。

然后,与 dotnet-ef 一样,它调用 ef.exe。 PMC 工具在调用 ef.exe 打开由命令创建的文件后,增加了一些额外的逻辑,以提供更集成的体验。

ef.exe

ef.exe(由于缺乏更好的名称)有时被称为内部人员,作为 dotnet-ef 和 PMC 工具的一部分以一组二进制文件的形式提供。 不同的目标框架和平台有不同的相应二进制文件。

  • tools/
    • net461/
      • any/
        • ef.exe
      • win-x86/
        • ef.exe
      • win-arm64/
        • ef.exe
    • netcoreapp2.0/
      • any/
        • ef.dll

只会为 EF Core 3.1 项目和面向 .NET Framework 的早期项目而调用 .NET Framework 程序集。 根据设计,可以在使用旧版 EF 的项目上使用最新版本的工具。 没有专门的 x64 版本,因为在某个目录下的程序集目标是 AnyCPU 平台,这个平台在 Windows 的 x64 和 arm64 版本上都会以 x64 模式运行。

.NET Core 2.0 程序集用于面向 .NET Core 或 .NET 5 及更新的项目。

ef.exe 的主要责任是加载启动项目的输出程序集,并在 EFCore.Design.dll内调用设计时入口点。

在 .NET Framework 上,我们使用单独的 AppDomain 加载项目程序集,传递要遵循的项目 App/Web.config 文件并绑定由 NuGet 或用户添加的重定向。

在 .NET Core/5+ 上,我们使用项目的 .deps.json 和 .runtimeconfig.json 文件调用 ef.dll,以模拟项目的实际运行时和程序集加载行为。

dotnet exec ef.dll --depsfile startupProject.deps.json --runtimeconfig startupProject.runtimeconfig.json

提示

问题 #18840 主要是使用 AssemblyLoadContext 而不是 dotnet exec 来加载用户的程序集。 这应该使这些工具能够处理更多项目类型,包括面向 Android 和 iOS 的项目类型。

设置好所有要加载的内容后,ef.exe 通过反射和 Activator.CreateInstance(或 .NET Framework 上的 AppDomain.CreateInstance)调用 EFCore.Design.dll。

EFCore.Design.dll

EFCore.Design.dll或者更具体地说,Microsoft.EntityFrameworkCore.Design.dll 包含 EF Core 的所有设计时逻辑。 所有入口点都在 OperationExecutor 类内。 此类设计中的许多奇特之处(如 MarshallByRefObject、嵌套类型等)是因为需要在 .NET Framework 上的不同应用程序域之间调用它。 如果删除此要求,很多事情可以变得更简单。 所有签名都是弱类型化的,以便实现与工具的向前和向后兼容性。 请记住,不同的工具版本可用于使用不同版本的 EF 调用项目。

除了执行程序,DbContextActivator 是此程序集中的另一个重要类型。 某些 ASP.NET Web 工具组件使用它在设计时实例化用户的 DbContext。

创建 DbContext

在运行任何特定的设计时逻辑之前,通常需要 DbContext 实例。 用户可以为 DbContext 指定简单或完全限定的不区分大小写的类型名称,或者如果只有单个 DbContext 类型,则不能指定名称。 无论哪种方式,我们需要在将其缩小到单一类型之前识别所有 DbContext 类型。 用于发现 DbContext 类型的逻辑位于 DbContextOperations的 FindContextTypes 方法中。

我们使用各种源查找 DbContext 类型。

  • 由启动程序集中的 IDesignTimeDbContextFactory<T> 实现引用。
  • 添加到应用程序服务提供程序的 DbContext。 若要获取所有上下文类型的列表,我们将获取注册为 DbContextOptions 的所有内容,并查看 ContextType 属性。 (请参阅下文,了解如何获取应用程序服务提供商。
  • 启动程序集和目标程序集中派生自 DbContext 的类型

我们还使用各种方法来实例化类型。 此处按优先级顺序排列。

  1. 使用 IDesignTimeDbContextFactory<T> 实现
  2. 使用应用程序服务提供程序中的 IDbContextFactory<T>
  3. 使用 ActivatorUtilities.CreateInstance

查找应用程序服务

为了获得对运行时行为的高保真度,我们尝试直接从应用程序服务提供程序获取 DbContext 实例。 我们将此逻辑与 ASP.NET 核心工具共享。 它作为 GitHub 上 dotnet/runtime 项目的一部分保留在 Microsoft.Extensions.HostFactoryResolver 目录下。

简言之,下面是它使用的一些策略。

  • 查找程序集入口点旁边的名为 BuildWebHost、CreateWebHostBuilder 或 CreateHostBuilder 的方法
    • 生成主机并从 Services 属性获取服务
  • 调用程序集入口点
    • 在生成主机时截获服务并在实际启动主机之前终止

设计时服务

除了应用程序服务和内部 DbContext 服务外,还有第三组设计时服务。 这些服务不会添加到内部服务提供程序,因为在运行时从不需要它们。 设计阶段服务由 DesignTimeServicesBuilder构建。 有两个主要路径-一个包含上下文实例,一个没有。 在搭建新的 DbContext 时,主要使用未包含上下文实例的主路径。 此处有多个扩展点,可让用户、提供程序和扩展重写和自定义服务。

用户可以通过将 IDesignTimeServices 的实现添加到启动程序集来自定义服务。

提供商可以通过将 DesignTimeProviderServices 属性添加到其程序集来自定义服务。 这指向 IDesignTimeServices 的实现。

扩展可以通过向目标程序集或启动程序集添加 DesignTimeServicesReference 属性来自定义服务。 如果该属性指定提供程序,则仅当该提供程序正在使用时,才会添加该提供程序。

日志记录和异常

实例化 DbContext 后,我们将其日志记录连接到工具的输出。 这允许从运行时生成输出。 任何未经处理的异常也将写入输出。 有一种特殊的异常类型OperationException,可以用于优雅地终止工具,并显示简单的错误消息,而无需显示堆栈跟踪。