asp.net core

ASP.NET Core MVC Action Priority using ActionConstraints

When developing API Controllers in ASP.NET Core you may run into the following error when trying to use multiple HTTP verbs of the same type in a controller (or in a base controller if using inheritence)

AmbiguousMatchException: The request matched multiple endpoints.

Normally the solution is to modify one of the methods to have a different signature, such as expecting an additional parameter. For example..

[HttpGet]
public ActionResult Get()
{
   ...
}

[HttpGet]
public ActionResult Get(string query)
{
   ...
}

This isnt always an elegant approach if we dont actually want/need to change our method signature, for example we may have a base WebApi that handles CRUD operations but want to allow a developer to inherit from that base controller and override a particular action method, say to handle the HttpGet request differently.

For a concrete example we could have the two classes below, the standard 'ValuesController' generated with the default WebApi template which has been modified to inherit from a BaseController that also has an HttpGet verb applied to an Action method.

Modified 'ValuesController.cs'

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : BaseApiController
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }
    }

'BaseApiController'

    public class BaseApiController : Controller
    {
        [HttpGet] 
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value from base", "another value from base" };
        }

    }

The above code will compile successfully, however when we try to navigate to the endpoint '/api/values' we will get the ambiguous match error

AmbiguousMatchException: The request matched multiple endpoints. Matches: 

HttpRequestPriorityDemo.Controllers.ValuesController.Get (HttpRequestPriorityDemo)
HttpRequestPriorityDemo.Controllers.ValuesController.Get (HttpRequestPriorityDemo)

Enter ActionConstraints that provides functionality to support conditional logic to determine whether or not an associated action is valid to be selected for the given request.

You can read more on IActionConstraints here

In the example provided, Route Data is used to determine the action that should be used.

public class CountrySpecificAttribute : Attribute, IActionConstraint
{
    private readonly string _countryCode;

    public CountrySpecificAttribute(string countryCode)
    {
        _countryCode = countryCode;
    }

    public int Order
    {
        get
        {
            return 0;
        }
    }

    public bool Accept(ActionConstraintContext context)
    {
        return string.Equals(
            context.RouteContext.RouteData.Values["country"].ToString(),
            _countryCode,
            StringComparison.OrdinalIgnoreCase);
    }
}

In the above code you can see the the Accept method returns a boolean depending on the 'country' value in the RouteData collection.

In my use case however, I want to be able to select the action to use without having to modify the request in any way, such as a weighting or enum applied to the controller methods, as in this case the developer will know which should be resolved. I will use an attribute that accepts an enum defining the priority order in which to resolve the action method.

First up let's create our attribute

[AttributeUsage(AttributeTargets.Method)]
public class HttpRequestPriority : Attribute, IActionConstraint
    {
        public readonly Priority Priority;

        public HttpRequestPriority(Priority priority = Priority.First)
        {
            Priority = priority;
        }

        public int Order
        {
            get
            {
                return 0;
            }
        }

        public bool Accept(ActionConstraintContext context)
        {
            if (Priority == Priority.First || context.Candidates.Count == 1)
                return true;

            //check the other candidates
            foreach (var item in context.Candidates.Where(f => !f.Equals(context.CurrentCandidate)))
            {
                var attr = item.Action.ActionConstraints.FirstOrDefault(f=> f.GetType() == typeof(HttpRequestPriority));

                if (attr == null)
                {
                    return true;
                }
                else
                {
                    HttpRequestPriority httpPriority = attr as HttpRequestPriority;
                    if (httpPriority.Priority > Priority)
                        return false;
                }
            }

            return true;


        }
    }

You can see the attribute expects a Priority type and defaults to Priority.First if no value is provided. The Priority enum is as follows:

    public enum Priority
    {
        First = 3,
        Second = 2,
        Third = 1,
        Last = 0
    }

The Accept method on the HttpRequestPriority class, first checks to see if the Priority is First or its the only matching action, and if so returns true.

If there are multiple matching actions and its not the First, it will then find the other action method candidates, try to get the HttpRequestPriority attibute and if that exists get the priority. It can then determine if another priority is higher and should return false in that case.

Now we have our attribute, we can apply this to our controller action methods that we will want to override. In the case of the base controller, we will always want to return if there is no other derived class with the same method, so we will apply the attibute with the Priority.Last value. This means when we derive we can specify a higher Priority to override.

BaseController.cs

    [HttpGet] 
    [HttpRequestPriority(Priority.Last)]
    public ActionResult<IEnumerable<string>> Get()
    {
       return new string[] { "value from base", "another value from base" };
    }

Next we can apply the attribute to our derived class, this time applying Priority.First (or leaving the parameter empty as we default to First).

ValuesController.cs

  [HttpGet]
  [HttpRequestPriority(Priority.First)]
  public ActionResult<IEnumerable<string>> Get()
  {
      return new string[] { "value1", "value2" };
  }

When we now try loading our API we should no longer hit an exception, this time we should see the data returned specified in the derived ValuesController. If you try swapping the Priorities around (First on the base contoller and vise-versa) and hit the api endpoint again you should this time see the data returned from the base controller.

If you are using Swashbuckle to generate your Swagger API documention you will want the generated documentaton to use the correct Action Method.

The Swashbuckle API provides a ResolveConflictingActions extension to allow us to specifiy the correct method.

An example of how you could configure this could be as follows.

First create the following extension method that will return the correct ApiDescription based upon on HttpResultPriority attribute.

public static ApiDescription ResolveActionUsingAttribute(this IEnumerable<ApiDescription> apiDescriptions)
        {
            ApiDescription returnDescription = null;
            int currentPriority = 0;

            foreach (var item in apiDescriptions.Where(f => f.ActionDescriptor.ActionConstraints.Any(a => a.GetType() == typeof(HttpRequestPriority))))
            {
                //check the current HttpRequestPriority and return the highest
                var priority = (HttpRequestPriority)item.ActionDescriptor.ActionConstraints.FirstOrDefault(a => a.GetType() == typeof(HttpRequestPriority));

                if (priority != null && (int)priority.Priority > currentPriority)
                {
                    currentPriority = (int)priority.Priority;
                    returnDescription = item;
                }
            }

            if (returnDescription == null)
                returnDescription = apiDescriptions.First();

            return returnDescription;
        }

Next you can call this extension method from the ResolveConflictingActions option on your Swagger configuration in Startup.cs

 services.AddSwaggerGen(c =>
                    {
                        c.SwaggerDoc("v1", new Info { Title = appContext.App, Version = "v1" });
                        c.ResolveConflictingActions(apiDescriptions => apiDescriptions.ResolveActionUsingAttribute());
                    });

Your SwaggerAPI documention should now serve up the correct Action Method information based upon the action with the higest priority.

You can find an example project on my GitHub