NOTE: this first solution based on ControllerActionInvokerCache is tested in asp.net core 2.2 and worked fine. However in later versions, looks like that class is made internal and not accessible anymore, this solution will not help. Try the second solution introduced later.
The invoked controller is cached before in a ControllerActionInvokerCache (which is available via DI). The cache key is an instance of ControllerContext which is not equally compared by reference (so you can instantiate a new instance as long as the wrapped ActionContext is the current one). Actually ControllerActionInvokerCache is a composite cache.
You can see the code below for detail:
public static class RazorViewExtensions
{
public static Controller GetInvokedController(this RazorPage view)
{
var serviceProvider = view.Context.RequestServices;
var controllerCache = serviceProvider.GetRequiredService<ControllerActionInvokerCache>();
//ViewContext here is also an ActionContext
var controllerContext = new ControllerContext(view.ViewContext);
var cacheEntry = controllerCache.GetCachedResult(controllerContext).cacheEntry;
return cacheEntry == null ? null : cacheEntry.ControllerFactory(controllerContext) as Controller;
}
}
For convenience, we declare an extension method as above. To use it inside your controller's view:
var controller = this.GetInvokedController();
You can base on that to write a similar extension method for using inside a Razor Page (the base page is Page instead of RazorPage).
Actually the ControllerActionInvokerCacheEntry is passed through to ControllerActionDescriptor.CacheEntry. However that CacheEntry property is internal (and of course not documented). We can see that in the source code. So basically you can use reflection to obtain that cache entry. However it requires reflection so the code block is even longer than the first solution we use above.
Here's another solution by extracting the controller instance from the ActionExecutingContext.Controller. This may be a little bit faster than the first solution but we need a separate class for the custom IActionFilter to capture the controller instance into a feature shared via HttpContext.Features. The code is of course a bit longer, like this:
//define the feature types
public interface IInvokedControllerFeature
{
Controller Controller { get; }
}
public class InvokedControllerFeature : IInvokedControllerFeature
{
public InvokedControllerFeature(Controller controller)
{
Controller = controller;
}
public Controller Controller { get; }
}
//an extension class to contain several convenient extension methods
//to setup the feature and get the controller instance later
public static class InvokedControllerFeatureExtensions
{
public static Controller GetInvokedController(this HttpContext httpContext)
{
return httpContext.Features.Get<IInvokedControllerFeature>()?.Controller;
}
public static Controller GetInvokedController(this RazorPage view)
{
return view.Context.GetInvokedController();
}
public static IServiceCollection AddInvokedControllerFeature(this IServiceCollection services)
{
return services.Configure<MvcOptions>(o => {
o.Filters.Add<InvokedControllerFeatureActionFilter>();
});
}
class InvokedControllerFeatureActionFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context) {}
public void OnActionExecuting(ActionExecutingContext context)
{
//share the controller instance via a feature
context.HttpContext.Features.Set<IInvokedControllerFeature>(new InvokedControllerFeature(context.Controller as Controller));
}
}
}
//register the feature inside Startup.ConfigureServices
services.AddInvokedControllerFeature();
The usage inside a controller's view is just the same as in the first solution.