Getting All Controllers and Actions names in C#
Asked Answered
T

12

96

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?

Tadashi answered 5/2, 2014 at 16:46 Comment(2)
Related: #11300827Korean
You may find a better answer here: #44455884Commit
J
97

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)));
Johannesburg answered 5/2, 2014 at 16:52 Comment(10)
@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.Johannesburg
We can go further, all methods public methods with return type ActionResult or inherited from itSpellman
@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.Korean
what about public methods with return type void? Nevermind, I found this msdn article that shows action methods can be void msdn.microsoft.com/en-us/library/…Quote
what about getting the Area's name?Celik
Do you think this method could cause performance problems with many controllers ?Felicitasfelicitate
@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.Johannesburg
MethodInfo.IsDefined takes two arguments, per docs msdn.microsoft.com/en-us/library/…Bainbridge
@D-Jones There's the CustomAttributeExtensions.IsDefined extension which takes 1 arg: msdn.microsoft.com/en-us/library/hh138309(v=vs.110).aspxJohannesburg
@Johannesburg 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)));Kannan
H
128

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

Hargett answered 21/6, 2015 at 22:18 Comment(13)
with this answer, I can get the result immediately while the accepted one is hard to use!Tedtedd
what is this: 'MyWebDll.MvcApplication' is the model class?Sensitive
Is there a way to get the area value if it exists?Parrotfish
@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"Hargett
@David Létourneau see my postSensitive
Is there any way to get the view/partial view name returned by the Action method programatically?Uni
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?Interact
You can replace the MyWebDll line with var asm = Assembly.GetCallingAssembly() if you're inside your web project.Buddle
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.Apocalyptic
To @NathanKamenar I used GetCustomAttributesData to fix 'GetCustomAttributes' takes 0 arguments'Forzando
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://mcmap.net/q/217740/-getting-all-controllers-and-actions-names-in-c If it can help someone...Platinic
Such an amazing piece of code and works perfectly, thank you @Hargett !Ashjian
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.Apocalyptic
J
97

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)));
Johannesburg answered 5/2, 2014 at 16:52 Comment(10)
@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.Johannesburg
We can go further, all methods public methods with return type ActionResult or inherited from itSpellman
@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.Korean
what about public methods with return type void? Nevermind, I found this msdn article that shows action methods can be void msdn.microsoft.com/en-us/library/…Quote
what about getting the Area's name?Celik
Do you think this method could cause performance problems with many controllers ?Felicitasfelicitate
@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.Johannesburg
MethodInfo.IsDefined takes two arguments, per docs msdn.microsoft.com/en-us/library/…Bainbridge
@D-Jones There's the CustomAttributeExtensions.IsDefined extension which takes 1 arg: msdn.microsoft.com/en-us/library/hh138309(v=vs.110).aspxJohannesburg
@Johannesburg 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)));Kannan
C
12

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.

Commit answered 25/1, 2021 at 21:31 Comment(4)
For those using NSwag, leverage the plumbing you've already provided with: Program.CreateHostBuilder(new string[0]).Build().Services.GetRequiredService<IActionDescriptorCollectionProvider>()Hereon
This feels kinda cleaner than the Reflection based approachSwann
This answer is for asp.net core when the question was (most likely) for asp.net framework.Schonfield
Yes, but the answer is relevant to C# developers who want to achieve what the original poster asked for now, in 2023, and also, it does literally answer the question as stated.Frasier
S
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()
Sensitive answered 6/4, 2016 at 7:0 Comment(2)
I combined you code with this string extension: https://mcmap.net/q/143735/-get-string-between-two-strings-in-a-string 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 =)Parrotfish
var ind = method.DeclaringType?.Namespace?.IndexOf(".Areas.", StringComparison.InvariantCulture) ?? -1; Area = ind > -1 ? method.DeclaringType?.Namespace?.Substring(ind + ".Areas.".Length).Replace(".Controllers", "") : null;Internationale
P
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();
Paly answered 25/4, 2018 at 10:51 Comment(0)
P
5

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; }

Platinic answered 23/6, 2020 at 13:22 Comment(0)
G
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());
           }
      }
}
Granulate answered 14/10, 2015 at 5:35 Comment(0)
T
2

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

Assembly.GetExecutingAssembly()
.GetTypes()
.Where(type => typeof(Controller).IsAssignableFrom(type))
Townspeople answered 4/8, 2014 at 20:50 Comment(0)
E
2

Update:

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

https://mcmap.net/q/219187/-net-6-asp-net-core-6-0-get-startup-or-program-assembly-from-test-project

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);
        
    }
}
Eelpout answered 7/2, 2022 at 22:7 Comment(0)
S
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

Spellman answered 5/2, 2014 at 16:50 Comment(0)
A
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);
Abixah answered 6/7, 2018 at 14:10 Comment(2)
How can I get methods that have async Task<IActionResult>Hullo
you can use method.ReturnType == typeof(Task<IActionResult>) and remove other return types criteria.Abixah
C
0

You can also find all controllers and all of your actions inside your solution

(multiple controllers all together)

var controlleractionlist = asm.GetTypes()

.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();

var list = controlleractionlist.Where(a => a.Controller.Contains("Controller")).ToList();
Ceto answered 30/11, 2023 at 14:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.