Have you ever wanted a programmatic way of discovering the controller actions available in your ASP.NET MVC Core app? Or what Razor Pages are available? Maybe you would want to build something dynamically depending on the actions available.

One user on Stack Overflow wanted to know this, and you can see my answer here: Stack Overflow.

Turns out MVC Core has a built in mechanism for this.

If you don't like reading or explanations, here is the repo with my test project: GitHub repo.

IActionDescriptorCollectionProvider

To discover the controller actions and Razor Pages in your app, you must inject an IActionDescriptorCollectionProvider. The IActionDescriptorCollectionProvider provides the action descriptor collection as the name says.

What are action descriptors? Well, a ControllerActionDescriptor contains details about a single controller action, and a PageActionDescriptor has the details for a Razor page.

Discovering controller actions

Here is a small example of discovering all controller actions:

public class TestController : ControllerBase
{
    private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;

    public TestController(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
    {
        _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
    }

    public IActionResult GetActions()
    {
        return Ok(_actionDescriptorCollectionProvider
            .ActionDescriptors
            .Items
            .OfType<ControllerActionDescriptor>()
            .Select(a => new
            {
                a.DisplayName,
                a.ControllerName,
                a.ActionName,
                AttributeRouteTemplate = a.AttributeRouteInfo?.Template,
                HttpMethods = string.Join(", ", a.ActionConstraints?.OfType<HttpMethodActionConstraint>().SingleOrDefault()?.HttpMethods ?? new string[] { "any" }),
                Parameters = a.Parameters?.Select(p => new
                {
                    Type = p.ParameterType.Name,
                    p.Name
                }),
                ControllerClassName = a.ControllerTypeInfo.FullName,
                ActionMethodName = a.MethodInfo.Name,
                Filters = a.FilterDescriptors?.Select(f => new
                {
                    ClassName = f.Filter.GetType().FullName,
                    f.Scope //10 = Global, 20 = Controller, 30 = Action
                }),
                Constraints = a.ActionConstraints?.Select(c => new
                {
                    Type = c.GetType().Name
                }),
                RouteValues = a.RouteValues.Select(r => new
                {
                    r.Key,
                    r.Value
                }),
            }));
    }
}

Here is an example of the returned JSON:

{
  "displayName": "AspNetCoreActionDiscovery.Controllers.TestController.C (AspNetCoreActionDiscovery)",
  "controllerName": "Test",
  "actionName": "C",
  "attributeRouteTemplate": "api/test/c",
  "httpMethods": "POST",
  "parameters": [
    {
      "type": "Int32",
      "name": "param"
    }
  ],
  "controllerClassName": "AspNetCoreActionDiscovery.Controllers.TestController",
  "actionMethodName": "C",
  "filters": [
    {
      "className": "Microsoft.AspNetCore.Mvc.ViewFeatures.SaveTempDataAttribute",
      "scope": 10
    },
    {
      "className": "Microsoft.AspNetCore.Mvc.ModelBinding.UnsupportedContentTypeFilter",
      "scope": 10
    }
  ],
  "constraints": [ { "type": "HttpMethodActionConstraint" } ],
  "routeValues": [
    {
      "key": "action",
      "value": "C"
    },
    {
      "key": "controller",
      "value": "Test"
    },
    {
      "key": "page",
      "value": null
    }
  ]
}

Information shown here:

  • A display name used in some places in the framework (e.g. logging)
  • Name of the controller class without the Controller suffix
  • Name of the action method (name of function or another name if overridden with an attribute)
  • The complete route template from RouteAttributes (controller-level route is combined with action-level route)
  • HTTP methods accepted by the action (gotten from the constraints)
  • Parameters of the action (name + type)
  • Controller class
  • Method on the class
  • Filters that apply to this action (scope: 10 = global, 20 = controller, 30 = action)
  • Constraints that affect action selection (here a HttpPostAttribute)
  • Route values that map to this action

Note this is not all of the info available. I picked the more interesting data points. Feel free to explore the descriptors under a debugger.

You can see the complete JSON for my sample app here: GetActions.json.

Discovering Razor Pages

There is not so much info available for Razor Pages. But we can still discover them with something like this:

public IActionResult GetPages()
{
    return Ok(_actionDescriptorCollectionProvider
        .ActionDescriptors
        .Items
        .OfType<PageActionDescriptor>()
        .Select(a => new
        {
            a.DisplayName,
            a.ViewEnginePath,
            a.RelativePath,
        }));
}

A sample of the resulting JSON:

{
  "displayName": "Page: /Index",
  "viewEnginePath": "/Index",
  "relativePath": "/Pages/Index.cshtml"
}

The view engine path tells you what URL will lead to the page.

The relative path tells you which file it maps to.

You can see the complete JSON for my sample app here: GetPages.json.

Conclusions

Thanks for reading! Hopefully this will be useful.

Feel free to comment if you have any questions :)

Link to sample app: GitHub repo.