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);
}
}
}
CodePudding user response:
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 renameProgram.cs
toMyWebApplication.cs
, but the class will still be namedProgram
.
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")]
Then, 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.