6

I'm trying to get the Attribute tag from an API Controller so that I can see what HTTP verb it allows at runtime. From the sample code below, I want to be able to get the [HttpGet] tag.

[HttpGet]
public void MyResource()
{
    // Controller logic
}

I am currently using System.Reflection to gather other information about my API while it is running, but so far I have been unable to retrieve the [HttpGet tag and other Http verb tags. I've tried each of the solutions below with no luck:

public void GetControllerMethodHttpAttribute()
{
    MethodInfo controllerMethod = typeof(TestController).GetMethods().First();

    // Solution 1
    var attrs = controllerMethod.Attributes;

    // Solution 2
    var httpAttr = Attribute.GetCustomAttributes(typeof(HttpGetAttribute));

    // Solution 3
    var httpAttr2 = Attribute.GetCustomAttribute(controllerMethod, typeof(HttpGetAttribute));

    // Solution 4
    var httpAttr3 = Attribute.IsDefined(controllerMethod, typeof(HttpGetAttribute));
}

All of the previous questions that I've researched about this topic only related to Custom Attribute tags and pulling values out of those, but I couldn't find any information about getting the framework-included Attribute tags.

Does anyone know how I can get the [HttpGet] Attribute tag?

Thanks!

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Luke T Brooks
  • 545
  • 1
  • 8
  • 25
  • Do you need "this action supports get/post/etc", or the full information inside of the attributes (order, template, etc)? – gunr2171 Oct 12 '17 at 15:45
  • 1
    Did you verify that `typeof(TestController).GetMethods().First()` returns the expected method? – thehennyy Oct 12 '17 at 15:47
  • @gunr2171 the full information would be preferred, but most importantly I want to be able to tell what verbs each method supports. – Luke T Brooks Oct 12 '17 at 15:48
  • @thehennyy that line is a simplification of what my app is actually doing. I have a larger piece of code that retrieves all the methods from a given controller class and iterates through each method; so my actual code will undoubtedly come across a method that has an `Http` tag – Luke T Brooks Oct 12 '17 at 15:50
  • Solution 3 works for me. – Chris Oct 12 '17 at 16:40
  • There are different Attribute classes for API Controllers and for MVC controllers with the same name but in different namespaces. You might be mixing up the two. See my answer for more details. – NineBerry Oct 12 '17 at 17:05

2 Answers2

8

The proposed solutions 3 and 4 work.

You must look out that you are referencing the correct HttpGetAttribute. There is one in System.Web.Http.HttpGetAttribute and there is one in System.Web.Mvc.HttpGetAttribute.

The following code lists the public methods of the ValuesController API controller with the information on whether they have HttpGet or HttpPost attributes.

var methods = typeof(ValuesController).GetMethods();
string infoString = "";

foreach(var method in methods)
{
    // Only public methods that are not constructors
    if(!method.IsConstructor && method.IsPublic)
    {
        // Don't include inherited methods
        if(method.DeclaringType == typeof(ValuesController))
        {

            infoString += method.Name;

            if(Attribute.IsDefined(method, typeof(System.Web.Http.HttpGetAttribute)))
            {
                infoString += " GET ";
            }
            if(Attribute.IsDefined(method, typeof(System.Web.Http.HttpPostAttribute)))
            {
                infoString += " POST ";
            }


            infoString += Environment.NewLine;
        }
    }
}

You need to exchange System.Web.Http. with System.Web.Mvc. when the controller is an MVC controller instead of an API controller.

NineBerry
  • 26,306
  • 3
  • 62
  • 93
2

This is how I ended up doing it:

public static IEnumerable<Attribute> GetSupportedVerbsForAction<T>(
    Expression<Func<T, IActionResult>> expression)
    where T : Controller
{
    //only consider a list of attributes
    var typesToCheck = new[] { typeof(HttpGetAttribute), typeof(HttpPostAttribute),
        typeof(HttpPutAttribute), typeof(HttpDeleteAttribute), typeof(HttpPatchAttribute)};

    var method = ((MethodCallExpression)expression.Body).Method;

    var matchingAttributes = typesToCheck
        .Where(x => method.IsDefined(x))
        .ToList();

    //if the method doesn't have any of the attributes we're looking for,
    //assume that it does all verbs
    if (!matchingAttributes.Any())
        foreach(var verb in typesToCheck)
            yield return verb;

    //else, return all the attributes we did find
    foreach (var foundAttr in matchingAttributes)
    {
        yield return method.GetCustomAttribute(foundAttr);
    }
}

Say you have the following Controller and Actions:

public class HomeController : Controller
{
    // implicit get
    public IActionResult Index() => View();

    // explicit get
    [HttpGet]
    public IActionResult GetAction() => View();

    // extra info
    [HttpPost(Name = "some name", Order = 5)]
    public IActionResult PostAction() => View();

    [HttpPut]
    [HttpDelete]
    [HttpPatch("my template", Name = "patch name", Order = 333)]
    public IActionResult MultiAction() => View();
}

You can call them like this:

var indexVerbs = GetSupportedVerbsForAction<HomeController>(x => x.Index()).ToList();
var getVerbs = GetSupportedVerbsForAction<HomeController>(x => x.GetAction()).ToList();
var postVerbs = GetSupportedVerbsForAction<HomeController>(x => x.PostAction()).ToList();
var multiVerbs = GetSupportedVerbsForAction<HomeController>(x => x.MultiAction()).ToList();

This will return the full attribute that you've set in case you've added any custom data.

enter image description here

Two things to note:

  1. You'll need to supply parameters to the function call. They don't need to be valid as the method itself won't be called, but I couldn't figure out how to allow you to do GetSupportedVerbsForAction<HomeController>(x => x.Index).
  2. The method will pass back a list of Attribute, so use the is keyword to figure out if it's the type you want.
gunr2171
  • 16,104
  • 25
  • 61
  • 88
  • I may be wrong but I thought if it had no attributes that meant it would accept anything, not just get? – Chris Oct 12 '17 at 16:34
  • @Chris nope, the default is GET: http://www.tutorialsteacher.com/mvc/actionverbs-in-mvc. Here's a slightly [better link](https://forums.asp.net/t/1928761.aspx?What+is+default+http+verb+for+action+method), but I'm still searching for official documentation. – gunr2171 Oct 12 '17 at 16:38
  • https://stackoverflow.com/questions/11767911/mvc-httppost-httpget-for-action is a relevant stack overflow question. As you can see from the tests (and supported by the documentation https://msdn.microsoft.com/en-us/library/system.web.mvc.httpgetattribute(v=vs.118).aspx ) the attribute restricts what is allowed. With no attribute there is no restirction. I suspect in the case of that example you linked that MVC is clever enough to say if one is unrestricted and one is post only then if its a post then use the more restricted post only one. – Chris Oct 12 '17 at 16:44
  • 1
    @Chris Yeah, sounds like you're right about that. In that case instead of returning one `GET` attribute you would just return all the attributes. – gunr2171 Oct 12 '17 at 16:45