Getting All Controllers and Actions names in C#

前端 未结 9 1635
耶瑟儿~
耶瑟儿~ 2020-11-27 10:56

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.

相关标签:
9条回答
  • 2020-11-27 11:27

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

    Assembly.GetExecutingAssembly()
    .GetTypes()
    .Where(type => typeof(Controller).IsAssignableFrom(type))
    
    0 讨论(0)
  • 2020-11-27 11:32

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

    0 讨论(0)
  • 2020-11-27 11:33

    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

    0 讨论(0)
  • 2020-11-27 11:34
    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();
    
    0 讨论(0)
  • 2020-11-27 11:35

    @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);
    
    0 讨论(0)
  • 2020-11-27 11:40

    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()
    
    0 讨论(0)
提交回复
热议问题