In this post I describe application parts and how ASP.NET Core uses them to find the controllers in your app. I then show how you can retrieve the list at runtime for debugging purposes.
Debugging a missing controller
A while ago I was converting an ASP.NET application to ASP.NET Core. The solution had many class library projects, where each project represented a module or vertical slice of the application. These modules contained everything for that feature: the database code, the domain, and the Web API controllers. There was then a "top level" application that referenced all these modules and served the requests.
As the whole solution was based on Katana/Owin and used Web API controllers exclusively, it wasn't too hard to convert it to ASP.NET Core. But, of course, there were bugs in the conversion process. One thing that had me stumped for a while was why the controllers from some of the modules didn't seem to be working. All of the requests to certain modules were returning 404s.
There were a few possibilities in my mind for what was going wrong:
- There was a routing issue, so requests meant for the controllers were not reaching them
- There was a problem with the controllers themselves, meaning they were generating 404s
- The ASP.NET Core app wasn't aware of the controllers in the module at all.
My gut feeling was the problem was either 1 or 3, but I needed a way to check. The solution I present in this post let me rule out point 3, by listing all the ApplicationPart
s and controllers the app was aware of.
What are application parts?
An Application Part is an abstraction over the resources of an MVC app. Application Parts allow ASP.NET Core to discover controllers, view components, tag helpers, Razor Pages, razor compilation sources, and more
Application Parts allow you to share the same resources (controllers, Razor Pages etc) between multiple apps. If you're familiar with Razor Class Libraries, then think of application parts as being the abstraction behind it.
One application part implementation is an AssemblyPart
which is an application part associated with an assembly. This is the situation I had in the app I described previously - each of the module projects was compiled into a separate Assembly
, and then added to the application as application parts.
You can add application parts in ConfigureServices
when you configure MVC. The current assembly is added automatically, but you can add additional application parts too. The example below adds the assembly that contains TestController
(which resides in a different project) as an application part.
public void ConfigureServices(IServiceCollection services)
{
services
.AddControllers()
.AddApplicationPart(typeof(TestController).Assembly);
}
Note that in ASP.NET Core 3.x, when you compile an assembly that references ASP.NET Core, an assembly attribute is added to the output,
[ApplicationPart]
. ASP.NET Core 3.x apps look for this attribute on referenced assemblies and registers them as application parts automatically, so the code above isn't necessary.
We've covered how you register application parts, but how do we debug when things go wrong?
Providing features with the ApplicationPartManager
When you add an application part (or when ASP.NET Core adds it automatically), it's added to the ApplicationPartManager
. This class is responsible for keeping track of all the application parts in the app, and for populating various features based on the registered parts, in conjunction with registered feature providers.
There are a variety of features used in MVC, such as the ControllerFeature
and ViewsFeature
for example. The ControllerFeature
(shown below) contains a list of all the controllers available to an application, across all of the registered application parts.
public class ControllerFeature
{
public IList<TypeInfo> Controllers { get; } = new List<TypeInfo>();
}
The list of controllers is obtained by using the ControllerFeatureProvider
. This class implements the IApplicationFeatureProvider<T>
interface, which, when given a list of application parts, populates an instance of ControllerFeature
with all the controllers it finds.
public class ControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
// Loop through all the application parts
foreach (var part in parts.OfType<IApplicationPartTypeProvider>())
{
// Loop through all the types in the application part
foreach (var type in part.Types)
{
// If the type is a controller (and isn't already added) add it to the list
if (IsController(type) && !feature.Controllers.Contains(type))
{
feature.Controllers.Add(type);
}
}
}
}
protected virtual bool IsController(TypeInfo typeInfo) => { /* Elided for brevity*/ }
}
The ApplicationPartManager
exposes a PopulateFeature
method which calls all the appropriate feature providers for a given feature:
public class ApplicationPartManager
{
// The list of application parts
public IList<ApplicationPart> ApplicationParts { get; } = new List<ApplicationPart>();
// The list of feature providers for the various possible features
public IList<IApplicationFeatureProvider> FeatureProviders { get; } =
new List<IApplicationFeatureProvider>();
// Populate the feature of type TFeature
public void PopulateFeature<TFeature>(TFeature feature)
{
foreach (var provider in FeatureProviders.OfType<IApplicationFeatureProvider<TFeature>>())
{
provider.PopulateFeature(ApplicationParts, feature);
}
}
That covers all the background for ApplicationPartManager
and features.
Listing all the Application parts and controllers added to an application
To quickly work out whether my 404
problem was due to routing or missing controllers, I needed to interrogate the ApplicationPartManager
. If the application parts and controllers for the problematic modules were missing, then that was the problem; if they were present, then it was probably some sort of routing issue!
To debug the issue I wrote a quick IHostedService
that logs the application parts added to an application, along with all of the controllers discovered.
I used an
IHostedService
because it runs after application part discovery, and only executes once on startup.
The example below takes an ILogger
and the ApplicationPartManager
as dependencies. It then lists the names of the application parts, populates an instance of the ControllerFeature
, and lists all the controllers known to the app. These are written to a log message which can be safely inspected
A similar example in the documentation exposes this information via a Controller, which seems like a bit of a bad idea to me!
public class ApplicationPartsLogger : IHostedService
{
private readonly ILogger<ApplicationPartsLogger> _logger;
private readonly ApplicationPartManager _partManager;
public ApplicationPartsLogger(ILogger<ApplicationPartsLogger> logger, ApplicationPartManager partManager)
{
_logger = logger;
_partManager = partManager;
}
public Task StartAsync(CancellationToken cancellationToken)
{
// Get the names of all the application parts. This is the short assembly name for AssemblyParts
var applicationParts = _partManager.ApplicationParts.Select(x => x.Name);
// Create a controller feature, and populate it from the application parts
var controllerFeature = new ControllerFeature();
_partManager.PopulateFeature(controllerFeature);
// Get the names of all of the controllers
var controllers = controllerFeature.Controllers.Select(x => x.Name);
// Log the application parts and controllers
_logger.LogInformation("Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'",
string.Join(", ", applicationParts), string.Join(", ", controllers));
return Task.CompletedTask;
}
// Required by the interface
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
All that remains is to register the hosted service in Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHostedService<ApplicationPartsLogger>();
}
The example log message below is taken from the sample code, in which an API project (ApplicationPartsDebugging.Api) references a class library (ApplicationPartsDebugging.Controllers) which contains a controller, TestController
.
info: ApplicationPartsDebugging.Api.ApplicationPartsLogger[0]
Found the following application parts: 'ApplicationPartsDebugging.Api, ApplicationPartsDebugging.Controllers'
with the following controllers: 'WeatherForecastController, TestController'
Both the API app and the class library are referenced as application parts, and controllers from both application parts are available.
And yes, this was exactly the problem I had during my conversion, I'd failed to register one of the modules as an application part, shown by it's absence from my log message!
Summary
In this post I described a problem I faced when converting an application to ASP.NET Core - the controllers from a referenced project could not be found. ASP.NET Core looks for controllers, views, and other features in application parts that it knows about. You can add additional application parts to an ASP.NET Core manually, though ASP.NET Core 3.x will generally handle this for you automatically. To debug my problem I created an ApplicationPartsLogger
that lists all the registered application parts for an app. This allows you to easily spot when an expected assembly is missing.
Top comments (0)