Getting All Controllers and Actions names in C#

C#asp.net Mvcasp.net Mvc-Controller

C# Problem Overview


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?

C# Solutions


Solution 1 - C#

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

Solution 2 - C#

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

Solution 3 - C#

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

Solution 4 - C#

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

Solution 5 - C#

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

Solution 6 - C#

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

Solution 7 - C#

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 to get the registered controllers (wherever they are) is to require this service IActionDescriptorCollectionProvider.

The ActionDescriptors property contains the list of all the controllers available, including routes, arguments and so on.

For further information, please see the MSDN documentation.

Edited You may find more information on this SO question.

Solution 8 - C#

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

Solution 9 - C#

@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);

Solution 10 - C#

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

Solution 11 - C#

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

Assembly.GetExecutingAssembly()
.GetTypes()
.Where(type => typeof(Controller).IsAssignableFrom(type))

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionsagheerView Question on Stackoverflow
Solution 1 - C#AVHView Answer on Stackoverflow
Solution 2 - C#dcastroView Answer on Stackoverflow
Solution 3 - C#Lucian BumbView Answer on Stackoverflow
Solution 4 - C#Mohammad FazeliView Answer on Stackoverflow
Solution 5 - C#GyanView Answer on Stackoverflow
Solution 6 - C#AlexBView Answer on Stackoverflow
Solution 7 - C#YenneferView Answer on Stackoverflow
Solution 8 - C#Arsen MkrtchyanView Answer on Stackoverflow
Solution 9 - C#BellashView Answer on Stackoverflow
Solution 10 - C#OgglasView Answer on Stackoverflow
Solution 11 - C#Don RollingView Answer on Stackoverflow