91

Is it possible to list the names of all controllers and their actions programmatically?

I want to implement database driven security for each controller and action. As a developer, I know all controllers and actions and can add them to a database table, but is there any way to add them automatically?

johnnyRose
  • 7,310
  • 17
  • 40
  • 61
sagheer
  • 1,195
  • 1
  • 12
  • 19
  • 1
    Related: http://stackoverflow.com/questions/11300327/how-can-i-get-the-list-of-all-actions-of-mvc-controller-by-passing-controllernam – Nathan May 29 '14 at 15:06
  • You may find a better answer here: https://stackoverflow.com/questions/44455384/how-to-find-all-controller-and-action – Yennefer Jan 25 '21 at 21:34

11 Answers11

125

The following will extract controllers, actions, attributes and return types:

Assembly asm = Assembly.GetAssembly(typeof(MyWebDll.MvcApplication));

var controlleractionlist = asm.GetTypes()
        .Where(type=> typeof(System.Web.Mvc.Controller).IsAssignableFrom(type))
        .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
        .Where(m => !m.GetCustomAttributes(typeof( System.Runtime.CompilerServices.CompilerGeneratedAttribute), true).Any())
        .Select(x => new {Controller = x.DeclaringType.Name, Action = x.Name, ReturnType = x.ReturnType.Name, Attributes = String.Join(",", x.GetCustomAttributes().Select(a => a.GetType().Name.Replace("Attribute",""))) })
        .OrderBy(x=>x.Controller).ThenBy(x => x.Action).ToList();

If you run this code in linqpad for instance and call

controlleractionlist.Dump();

you get the following output:

enter image description here

AVH
  • 1,725
  • 1
  • 13
  • 8
  • 1
    with this answer, I can get the result immediately while the accepted one is hard to use! – hakuna1811 Jan 29 '16 at 09:06
  • what is this: 'MyWebDll.MvcApplication' is the model class? – Lucian Bumb Feb 04 '16 at 12:09
  • Is there a way to get the area value if it exists? – David Létourneau Apr 01 '16 at 20:38
  • 5
    @LucianBumb - "MyWebDll" is just a placeholder for the main dll of your web app (or whichever namespace MvcApplication class is in). Look in your Global.asax.cs file and you'll see something similar to "public class MvcApplication : System.Web.HttpApplication". Replace the "MyWebDll" portion in the example with the name of your web application dll or namespace (check your project properties window or the bin folder if you're unsure of the assembly name). For instance if my project generates a dll named "AmosCo.Enterprise.Website.dll", then I'd use "AmosCo.Enterprise.Website.MvcApplication" – AVH Apr 06 '16 at 06:50
  • 1
    @David Létourneau see my post – Lucian Bumb Apr 06 '16 at 07:01
  • Is there any way to get the view/partial view name returned by the Action method programatically? – umsateesh Oct 05 '16 at 04:50
  • I am getting No overload for method 'GetCustomAttributes' takes 0 arguments for x.GetCustomAttributes() on the second to last line, any idea why or what should go there? – Nathan Kamenar Feb 09 '17 at 14:45
  • 1
    You can replace the MyWebDll line with var asm = Assembly.GetCallingAssembly() if you're inside your web project. – WhiteleyJ Jun 09 '17 at 19:53
  • 4
    To get this query working in LinqPad, hit F4 to open the Query Properties > Browse > go to your app's bin/debug/MyApp.dll (or release folder) and select. Also do the same for `System.Web.Mvc.dll` from the same folder. Then change the `MyWebDll` to `MyApp` (name of your app dll). Don't forget to add the dump statement. – Alex Jun 30 '17 at 20:58
  • To @NathanKamenar I used GetCustomAttributesData to fix 'GetCustomAttributes' takes 0 arguments' – Barry McDermid Nov 20 '17 at 15:10
  • Perfect Answer -So easy to use !! – Greg Foote Jul 12 '19 at 16:43
  • your solution is amazing! Tnx – Ivan Tikhonov Dec 20 '19 at 12:20
  • Thank you, it helps me a lot. I've used your solution and adapt it to my needs, then post a full sample of what I did : https://stackoverflow.com/a/62535813/1866810 If it can help someone... – AlexB Aug 26 '20 at 09:54
  • Such an amazing piece of code and works perfectly, thank you @AVH ! – Ivan Yurchenko Jul 22 '21 at 08:56
  • 1
    If you run into this exception `Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.` with `System.Net.Http` (or similar DLL), try downloading Linqpad 4. The issue is with .NET Framework and Standard. – Alex Jul 22 '21 at 10:15
95

You can use reflection to find all Controllers in the current assembly, and then find their public methods that are not decorated with the NonAction attribute.

Assembly asm = Assembly.GetExecutingAssembly();

asm.GetTypes()
    .Where(type=> typeof(Controller).IsAssignableFrom(type)) //filter controllers
    .SelectMany(type => type.GetMethods())
    .Where(method => method.IsPublic && ! method.IsDefined(typeof(NonActionAttribute)));
dcastro
  • 66,540
  • 21
  • 145
  • 155
  • @ArsenMkrt good point, I thought that methods had to be marked with the `Action` attribute. It turns out that all public methods are actions, unless decorated with the `NonAction` attribute. I've updated my answer. – dcastro Feb 05 '14 at 17:03
  • We can go further, all methods public methods with return type ActionResult or inherited from it – Arsen Mkrtchyan Feb 05 '14 at 18:24
  • 3
    @ArsenMkrt Action methods are not required to return a derivative of ActionResult. For example, MVC is perfectly happy to execute an action method that returns `string` in its signature. – Nathan May 29 '14 at 15:10
  • what about public methods with return type void? Nevermind, I found this msdn article that shows action methods can be void http://msdn.microsoft.com/en-us/library/system.web.mvc.emptyresult(v=vs.100).aspx – Dean Jul 25 '14 at 02:03
  • what about getting the Area's name? – systempuntoout Oct 04 '14 at 18:52
  • Do you think this method could cause performance problems with many controllers ? – RPDeshaies Nov 08 '14 at 21:06
  • 1
    @Tareck117 if you're calling it on every request, possibly. But this doesn't seem like something you'd need to call that often. For most cases, calling it once on startup is enough - the perf hit will be negligible. And if you load new assemblies dynamically at runtime, then scan those individually, once. – dcastro Nov 08 '14 at 21:20
  • MethodInfo.IsDefined takes two arguments, per docs https://msdn.microsoft.com/en-us/library/system.reflection.memberinfo.isdefined(v=vs.110).aspx – djones Oct 08 '16 at 20:55
  • @D-Jones There's the `CustomAttributeExtensions.IsDefined` extension which takes 1 arg: https://msdn.microsoft.com/en-us/library/hh138309(v=vs.110).aspx – dcastro Oct 11 '16 at 10:33
  • @dcastro thanks. to avoid getting get/set methods for properties, static and methods in the base classes I changed it to `Assembly.GetExecutingAssembly().GetTypes().Where(type => typeof(ApiController).IsAssignableFrom(type)).SelectMany(type => type.GetMethods()).Where(method => method.IsPublic && !method.IsSpecialName && !method.IsStatic && (typeof(ApiController) != method.DeclaringType) && (typeof(Object) != method.DeclaringType) && !method.IsDefined(typeof(NonActionAttribute)));` – Andy Nugent Jul 14 '20 at 11:59
9

All these answers rely upon reflection, and although they work, they try to mimic what the middleware does.

Additionally, you may add controllers in different ways, and it is not rare to have the controllers shipped in multiple assemblies. In such cases, relying on reflection requires too much knowledge: for example, you have to know which assemblies are to be included, and when controllers are registered manually, you might choose a specific controller implementation, thus leaving out some legit controllers that would be picked up via reflection.

The proper way in ASP.NET Core to get the registered controllers (wherever they are) is to require this service IActionDescriptorCollectionProvider.

The ActionDescriptors property contains the list of all the actions available. Each ControllerActionDescriptor provides details including names, types, routes, arguments and so on.

var adcp = app.Services.GetRequiredService<IActionDescriptorCollectionProvider>();
var descriptors = adcp.ActionDescriptors
                      .Items
                      .OfType<ControllerActionDescriptor>();

For further information, please see the MSDN documentation.

Edited You may find more information on this SO question.

Jacob Foshee
  • 2,704
  • 2
  • 29
  • 48
Yennefer
  • 5,704
  • 7
  • 31
  • 44
8

I was looking for a way to get Area, Controller and Action and for this I manage to change a little the methods you post here, so if anyone is looking for a way to get the AREA here is my ugly method (which I save to an xml):

 public static void GetMenuXml()
        {
       var projectName = Assembly.GetExecutingAssembly().FullName.Split(',')[0];

        Assembly asm = Assembly.GetAssembly(typeof(MvcApplication));

        var model = asm.GetTypes().
            SelectMany(t => t.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
            .Where(d => d.ReturnType.Name == "ActionResult").Select(n => new MyMenuModel()
            {
                Controller = n.DeclaringType?.Name.Replace("Controller", ""),
                Action = n.Name,
                ReturnType = n.ReturnType.Name,
                Attributes = string.Join(",", n.GetCustomAttributes().Select(a => a.GetType().Name.Replace("Attribute", ""))),
                Area = n.DeclaringType.Namespace.ToString().Replace(projectName + ".", "").Replace("Areas.", "").Replace(".Controllers", "").Replace("Controllers", "")
            });

        SaveData(model.ToList());
    }

Edit:

//assuming that the namespace is ProjectName.Areas.Admin.Controllers

 Area=n.DeclaringType.Namespace.Split('.').Reverse().Skip(1).First()
Lucian Bumb
  • 2,821
  • 5
  • 26
  • 39
  • 1
    I combined you code with this string extension: http://stackoverflow.com/a/17253735/453142 to get the area. So you can get the area like that: Area = n.DeclaringType.Namespace.ToString().Substring("Areas.", ".Controllers") You'll need to update the extension to return string.empty instead of an exception and that it's. It's a little less ugly =) – David Létourneau Apr 19 '16 at 15:34
  • var ind = method.DeclaringType?.Namespace?.IndexOf(".Areas.", StringComparison.InvariantCulture) ?? -1; Area = ind > -1 ? method.DeclaringType?.Namespace?.Substring(ind + ".Areas.".Length).Replace(".Controllers", "") : null; – dima_horror Mar 19 '21 at 09:53
5
var result = Assembly.GetExecutingAssembly()
            .GetTypes()
            .Where(type => typeof(ApiController).IsAssignableFrom(type))
            .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
            .Where(m => !m.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), true).Any())
            .GroupBy(x => x.DeclaringType.Name)
            .Select(x => new { Controller = x.Key, Actions = x.Select(s => s.Name).ToList() })
            .ToList();
Mohammad Fazeli
  • 648
  • 10
  • 13
4

If it may helps anyone, I improved @AVH's answer to get more informations using recursivity.
My goal was to create an autogenerated API help page :

 Assembly.GetAssembly(typeof(MyBaseApiController)).GetTypes()
        .Where(type => type.IsSubclassOf(typeof(MyBaseApiController)))
        .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
        .Where(m => !m.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), true).Any())
        .Select(x => new ApiHelpEndpointViewModel
        {
            Endpoint = x.DeclaringType.Name.Replace("Controller", String.Empty),
            Controller = x.DeclaringType.Name,
            Action = x.Name,
            DisplayableName = x.GetCustomAttributes<DisplayAttribute>().FirstOrDefault()?.Name ?? x.Name,
            Description = x.GetCustomAttributes<DescriptionAttribute>().FirstOrDefault()?.Description ?? String.Empty,
            Properties = x.ReturnType.GenericTypeArguments.FirstOrDefault()?.GetProperties(),
            PropertyDescription = x.ReturnType.GenericTypeArguments.FirstOrDefault()?.GetProperties()
                                        .Select(q => q.CustomAttributes.SingleOrDefault(a => a.AttributeType.Name == "DescriptionAttribute")?.ConstructorArguments ?? new List<CustomAttributeTypedArgument>() )
                                        .ToList()
        })
        .OrderBy(x => x.Controller)
        .ThenBy(x => x.Action)
        .ToList()
        .ForEach(x => apiHelpViewModel.Endpoints.Add(x)); //See comment below

(Just change the last ForEach() clause as my model was encapsulated inside another model).
The corresponding ApiHelpViewModel is :

public class ApiHelpEndpointViewModel
{
    public string Endpoint { get; set; }
    public string Controller { get; set; }
    public string Action { get; set; }
    public string DisplayableName { get; set; }
    public string Description { get; set; }
    public string EndpointRoute => $"/api/{Endpoint}";
    public PropertyInfo[] Properties { get; set; }
    public List<IList<CustomAttributeTypedArgument>> PropertyDescription { get; set; }
}

As my endpoints return IQueryable<CustomType>, the last property (PropertyDescription) contains a lot of metadatas related to CustomType's properties. So you can get the name, type, description (added with a [Description] annotation) etc... of every CustomType's properties.

It goes further that the original question, but if it can help someone...


UPDATE

To go even further, if you want to add some [DataAnnotation] on fields you can't modify (because they've been generated by a Template for example), you can create a MetadataAttributes class :

[MetadataType(typeof(MetadataAttributesMyClass))]
public partial class MyClass
{
}

public class MetadataAttributesMyClass
{
    [Description("My custom description")]
    public int Id {get; set;}

    //all your generated fields with [Description] or other data annotation
}

BE CAREFUL : MyClass MUST be :

  • A partial class,
  • In the same namespace as the generated MyClass

Then, update the code which retrieves the metadatas :

Assembly.GetAssembly(typeof(MyBaseController)).GetTypes()
        .Where(type => type.IsSubclassOf(typeof(MyBaseController)))
        .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
        .Where(m => !m.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), true).Any())
        .Select(x =>
        {
            var type = x.ReturnType.GenericTypeArguments.FirstOrDefault();
            var metadataType = type.GetCustomAttributes(typeof(MetadataTypeAttribute), true)
                .OfType<MetadataTypeAttribute>().FirstOrDefault();
            var metaData = (metadataType != null)
                ? ModelMetadataProviders.Current.GetMetadataForType(null, metadataType.MetadataClassType)
                : ModelMetadataProviders.Current.GetMetadataForType(null, type);

            return new ApiHelpEndpoint
            {
                Endpoint = x.DeclaringType.Name.Replace("Controller", String.Empty),
                Controller = x.DeclaringType.Name,
                Action = x.Name,
                DisplayableName = x.GetCustomAttributes<DisplayAttribute>().FirstOrDefault()?.Name ?? x.Name,
                Description = x.GetCustomAttributes<DescriptionAttribute>().FirstOrDefault()?.Description ?? String.Empty,
                Properties = x.ReturnType.GenericTypeArguments.FirstOrDefault()?.GetProperties(),
                PropertyDescription = metaData.Properties.Select(e =>
                {
                    var m = metaData.ModelType.GetProperty(e.PropertyName)
                        .GetCustomAttributes(typeof(DescriptionAttribute), true)
                        .FirstOrDefault();
                    return m != null ? ((DescriptionAttribute)m).Description : string.Empty;
                }).ToList()
            };
        })
        .OrderBy(x => x.Controller)
        .ThenBy(x => x.Action)
        .ToList()
        .ForEach(x => api2HelpViewModel.Endpoints.Add(x));

(Credit to this answer)

and update PropertyDescription as public List<string> PropertyDescription { get; set; }

AlexB
  • 7,302
  • 12
  • 56
  • 74
3
Assembly assembly = Assembly.LoadFrom(sAssemblyFileName)
IEnumerable<Type> types = assembly.GetTypes().Where(type => typeof(Controller).IsAssignableFrom(type)).OrderBy(x => x.Name);
foreach (Type cls in types)
{
      list.Add(cls.Name.Replace("Controller", ""));
      IEnumerable<MemberInfo> memberInfo = cls.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public).Where(m => !m.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), true).Any()).OrderBy(x => x.Name);
      foreach (MemberInfo method in memberInfo)
      {
           if (method.ReflectedType.IsPublic && !method.IsDefined(typeof(NonActionAttribute)))
           {
                  list.Add("\t" + method.Name.ToString());
           }
      }
}
Shahzad Barkati
  • 2,532
  • 6
  • 25
  • 33
Gyan
  • 121
  • 1
  • 1
  • 3
2

Update:

For .NET 6 minimal hosting model see this answer on how to replace Startup in the code below

https://stackoverflow.com/a/71026903/3850405

Original:

In .NET Core 3 and .NET 5 you can do it like this:

Example:

public class Example
{
    public void ApiAndMVCControllers()
    {
        var controllers = GetChildTypes<ControllerBase>();
        foreach (var controller in controllers)
        {
            var actions = controller.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public);
        }
    }

    private static IEnumerable<Type> GetChildTypes<T>()
    {
        var types = typeof(Startup).Assembly.GetTypes();
        return types.Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract);
        
    }
}
Ogglas
  • 62,132
  • 37
  • 328
  • 418
1

Use Reflection, enumerate all types inside the assembly and filter classes inherited from System.Web.MVC.Controller, than list public methods of this types as actions

Arsen Mkrtchyan
  • 49,896
  • 32
  • 148
  • 184
1

Or, to whittle away at @dcastro 's idea and just get the controllers:

Assembly.GetExecutingAssembly()
.GetTypes()
.Where(type => typeof(Controller).IsAssignableFrom(type))
Mark Schultheiss
  • 32,614
  • 12
  • 69
  • 100
Don Rolling
  • 2,301
  • 4
  • 30
  • 27
1

@decastro answer is good. I add this filter to return only public actions those have been declared by the developer.

        var asm = Assembly.GetExecutingAssembly();
        var methods = asm.GetTypes()
            .Where(type => typeof(Controller)
                .IsAssignableFrom(type))
            .SelectMany(type => type.GetMethods())
            .Where(method => method.IsPublic 
                && !method.IsDefined(typeof(NonActionAttribute))
                && (
                    method.ReturnType==typeof(ActionResult) ||
                    method.ReturnType == typeof(Task<ActionResult>) ||
                    method.ReturnType == typeof(String) ||
                    //method.ReturnType == typeof(IHttpResult) ||
                    )
                )
            .Select(m=>m.Name);
freedomn-m
  • 27,664
  • 8
  • 35
  • 57
Bellash
  • 7,560
  • 6
  • 53
  • 86