在 C # 中获取所有控制器和操作的名称

是否可以以编程方式列出所有控制器及其操作的名称?

我想为每个控制器和操作实现数据库驱动的安全性。作为一个开发人员,我知道所有的控制器和操作,并且可以将它们添加到数据库表中,但是有什么方法可以自动添加它们吗?

71328 次浏览

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

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

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

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

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

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

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

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

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

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.

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

}
}