.NET 6 (ASP.NET Core 6.0) 从测试项目中获取 Startup 或 Program Assembly

.NET 6 (ASP.NET Core 6.0) get Startup or Program Assembly from Test project

在 .NET Core 3.1 和 .NET 5 中,我们进行了 Xunit 测试,如下例所示。它确保每个 Controller 都有一个 AuthorizeAttribute 以防止安全漏洞。

将我们的 Web 项目升级到 ASP.NET Core 6 的最小托管模型时,不再需要 ProgramStartup 类。一切正常,但以下情况除外:

var types = typeof(Startup).Assembly.GetTypes();

查看命名空间 Example.Web,我也看不到任何要从中加载程序集的 类。如何在 .NET 6 中加载 Program.cs 程序集?

.NET 5 中的示例:

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace Example.Web.Tests.ControllerTests
{
    public class AuthorizeAttributeTest
    {
        [Fact]
        public void ApiAndMVCControllersShouldHaveAuthorizeAttribute()
        {
            var controllers = GetChildTypes<ControllerBase>();
            foreach (var controller in controllers)
            {
                var attribute = Attribute.GetCustomAttribute(controller, typeof(Microsoft.AspNetCore.Authorization.AuthorizeAttribute), true) as Microsoft.AspNetCore.Authorization.AuthorizeAttribute;
                Assert.NotNull(attribute);
            }
        }

        private static IEnumerable<Type> GetChildTypes<T>()
        {
            var types = typeof(Startup).Assembly.GetTypes();
            return types.Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract);
        }
    }
}

快速回答是您可以在您的应用程序中引用 any(可访问)class 以获得对程序集的引用。它不必是 ProgramStartup class,也不必在根命名空间中。

您显然希望选择 class 您希望持久的,并且不会在以后的版本中被重命名或删除。从历史上看,Startup class 符合该标准。然而,对于 ASP.NET Core 6 最小托管模型,这显然不再适用。

鉴于此,您可以采用两种方法。

选项 1:将您的 Assembly 引用锚定在应用程序之外 class

第一个选项是从您的应用程序中锚定任意 public class。例如,您可以使用您的控制器之一。只要它被编译到同一个程序集中,Assembly.GetTypes() 调用就会产生相同的结果。这可能看起来像:

using Example.Web.Controllers;

var types = typeof(ExampleController).Assembly.GetTypes();

这种方法的主要缺点是 class 完全是任意的,将来可能会被移动或重命名。当然,如果 确实 发生了,您可能无论如何都需要更新您的单元测试,所以这没什么大不了的。

选项 2:将您的 Program class 公开给您的测试程序集

另一种选择是将您的 Assembly 引用锚定在从您的 Program.cs 文件编译的 class 之外,这与您之前的方法非常相似。这需要了解编译器如何处理此文件。

当您使用 ASP.NET Core 6 最小托管模型时,您实际上是在利用 C# 9 的 top-level 语句。编译器自动将任何 top-level 语句放入名为 Program 的 class 中,没有名称空间。

Note: That happens to align with your use of Program.cs, but that's completely incidental; you could rename Program.cs to MyWebApplication.cs, but the class will still be named Program.

问题是这个Programclass被标记为internal,因此您的单元测试程序集无法访问。

不过,您可以通过将 ASP.NET 核心程序集的内部标记为对单元测试程序集可见来解决这个问题。这可以通过将以下内容添加到例如你的 AssemblyInfo.cs:

[assembly: InternalsVisibleTo("Example.Web.Tests")]

或者,正如 @kal 在评论中指出的那样,通过在 csproj 文件中设置以下内容:

<ItemGroup>
    <InternalsVisibleTo Include="Example.Web.Tests" />
</ItemGroup>

完成后,您可以使用以下方式访问您的 Program class:

var types = typeof(Program).Assembly.GetTypes();

我不太喜欢以这种方式公开我的程序集的内部结构,但这是单元测试中相当常见的做法,所以我将它作为一个选项包括在内,以防您已经这样做了。

最终,这与第一个选项并没有什么不同——你仍然将你的 Assembly 引用锚定在不同的 class 之外——但它的优点是锚定到我们知道的 class 将永远存在,而不是一些任意的 application-specific class。这样在阅读代码的时候可能也会感觉更直观。