Accessing Controller instance from View in .net core 5

Viewed 866

I've been accessing base controller instance from the view through following line, ASP.NET

BaseController baseController = ViewContext.Controller as BaseController;

I've new project in ASP.NET Core 5.0. I would like to access base controller for some reason, but it seems now it is not similar like previous version of MVC.

Is there any solution or alternative to achieve same?

NOTE: I want to access completely initialized instance of the controller. I tried to get instance through Dependency Injection using GetService() method. It gives new instance of controller, but not fully initialized, such as HttpContext, User etc properties are null.

1 Answers

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.

Related