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

1

5

In .NET Core 3.1 and .NET 5, we had an Xunit test like the example below. It makes sure every Controller has an AuthorizeAttribute to prevent security leaks.

When upgrading our web project to ASP.NET Core 6's minimal hosting model, the Program and Startup classes are no longer needed. Everything works fine, except for the following:

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

Looking at the namespace Example.Web, I can't see any classes to load assemblies from, either. How can the Program.cs assembly be loaded in .NET 6?

Example from .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);
        }
    }
}
Goble answered 7/2, 2022 at 22:52 Comment(0)
R
13

The quick answer is that you can reference any (accessible) class in your application in order to get a reference to the assembly. It needn't be the Program or Startup class, and it needn't be in the root namespace.

You'll obviously want to choose a class you expect to be persistent, and not subject to being renamed or removed in later versions. Historically, the Startup class fit that criteria. With the ASP.NET Core 6 minimal hosting model, however, that's obviously not true anymore.

Given this, there are two approaches you can take here.

Option 1: Anchor your Assembly reference off of an application class

The first option is to anchor off of any arbitrary, public class from your application. For example, you could use one of your controllers. So long as it's compiled into the same assembly, the Assembly.GetTypes() call will yield the same results. This might look like e.g.:

using Example.Web.Controllers;

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

The main downside of this approach is that the class is completely arbitrary, and could potentially be moved or renamed in the future. Of course, if that did happen, you'll likely need to update your unit tests anyway, so it's not that big of a deal.

Option 2: Expose your Program class to your test assembly

Another option is to anchor your Assembly reference off of the class compiled from your Program.cs file, which is very similar to your previous approach. This requires understanding a bit about how this file is processed by the compiler.

When you use the ASP.NET Core 6 minimal hosting model, you're actually taking advantage of C# 9's top-level statements. The compiler automatically places any top-level statements into a class named Program, without a namespace.

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.

The problem is that this Program class is marked as internal, and thus not accessible to your unit test assembly.

You can work around that, however, by marking your ASP.NET Core assembly's internals as visible to your unit test assembly. This can be done by adding the following to e.g. your AssemblyInfo.cs:

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

Or, as @kal pointed out in the comments, by setting the following in your csproj file:

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

Once this is done, you can access your Program class using:

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

I'm not a big fan of exposing the internals of my assembly in this way, but it's fairly common practice with unit tests, so I'm including it as an option in case you're already doing this.

Ultimately, this really isn't any different from the first option—you're still anchoring your Assembly reference off of a different class—but it has the advantage of being anchored to a class we know will always be present, and not some arbitrary, application-specific class. That may also feel more intuitive when reading the code.

Retractile answered 7/2, 2022 at 23:53 Comment(2)
In case, we want to add the settings in .csproj file: <ItemGroup><InternalsVisibleTo Include="MyTestProject" /></ItemGroup>Anthe
@kal: Oh, thank you. I didn’t actually realize that was even an option, so I’m happy to have learned this. I’ve edited my answer to include that option as well.Retractile

© 2022 - 2024 — McMap. All rights reserved.