Defining ASP.NET Core Controller action constraint to match the correct action


This post is from Premier Developer consultants Gustavo Varo and Randy Patterson.


When developing Web API controllers many times we have situations where we need to define similar arguments in different methods. For example, a controller that has 2 methods that query users either by last name or date of birth. The name of the method is different but they both will receive a single argument of type string. When the routing engine can’t determine which action method it should call, a status code of 500 is returned.

Furthermore, sometimes even the proper routing does not fully help us, especially when dealing with external services that have their way of calling web APIs already established.

Recently I helped a customer integrate their Web API with Microsoft Graph; the service would just not work on the notification methods. They couldn’t figure out why Office 365 would not call their methods correctly. They were thinking that Microsoft was just not calling them.

In this post, we will discuss a very simple mechanism to identify why the controller is failing and a great way to fix this problem.

Let’s start by creating a simple ASP.NET core controller called Users

clip_image001

Run your project and using Fiddler make the request.

clip_image003

We can see on the images above that the request was performed and we received the expected result.

clip_image004

Now let’s add a new method to retrieve users by date of birth.

clip_image005

Build your project, run and perform the exact same request we did before, trying to retrieve the user based on last name.

clip_image007

We can see that the same request now returns a status code of 500 – Internal Server Error. So why is this happening? We are explicitly asking for a GET method passing a string parameter named lastname.

One thing that a lot developers forget is that if we are in a debug session with visual studio, we have the opportunity to see what is happening. Is Visual Studio receiving the request? If so, what is the issue?

With Visual Studio running and attached to the process, open the Output window and run the request again. You may be surprised how much information it can give you.

clip_image009

We can see on the output window that the request was made, however, request matched multiple actions resulting in ambiguity. In other words, the engine does not know which action should be executed.

You may be thinking: Wait a minute!!! This used to work on MVC WebAPI with ApiController.  That’s correct, on MVC, using ApiController, the controller used to be able to select the right method based on the query string. That was only correct for classes based on ApiController class.  For controllers that inherit from the Controller class, MVC did not resolve the right method based on the query string, same as the behavior we saw above.  In ASP.NET Core, Controller and ApiController classes were unified into a single Controller class. Microsoft decided to no longer provide a mechanism to attempt to find the right method based on the query string. In other words, the behavior above is not a BUG, it is by design!  Microsoft has built a back-compatibility porting change that can be found here, but keep in mind that we will drop support for it in the 3.0 time frame.

How can we solve this issue then?

This can be challenging, especially when using external services.  A good example would be if you are implementing your own OAuth controller to deal with Microsoft Account OAuth. By the OAuth flow, once a user enters the password within Microsoft, Microsoft will call your controller with an authorization code or with an error.  Both calls will have the same number of arguments and also potentially use the exact same types.

  • Success calls will be called with code (type string) and state (type string)
  • Unsuccessful calls will be called with error (string type) and description (string Type)

Now that we learned that we may be facing this issue quite often, let’s learn a way for us to solve the ambiguity.

The very first thing we need to do is to identify what makes one method different than the other method. They must have at least one thing different, otherwise we cannot solve the ambiguity.

In both examples above (the simple LastName/DOB and the Microsoft Authorization OAuth), we can see that the query string is different and could help us to identify the right method to be executed.  What we will do next is create a new class, I will call this class QueryStringConstraintAttribute, and make sure the class inherits from ActionMethodSelectorAttribute. The ActionMethodSelector class is a class that represents an attribute that is used to influence the selection of an action method.

This is an abstract class and we will need to implement the abstract method IsValidForRequest. The IsValidForRequest method is the place where we will add the logic to help the engine to determine if the current call matches the requirements to perform the action method the attribute is associated with. The IsValidForRequest method returns a Boolean value. If the method returns true, it tells the engine that the action should be executed, otherwise the engine will not consider that action method.

Keep in mind that after the engine analyzes all action methods, it should have a single action method ready to be executed or the engine will throw AmbiguousActionException.

clip_image010

In our example, we also added a constructor with 2 arguments, the name of the parameter provided on the query string and if it should be present on the call or not.

Once we have created our Attribute class, we can add the attribute to our methods. In our simple example, we want all calls made using last name as query string to be performed by GetUserByLastName, as seen in the image below.

clip_image011

Now that we have established that GetUserByLastName should only be executed when the call has lastname on the query string, we can try the request again on Fiddler.

clip_image013

Unfortunately, it seems the error is still happening. This is because while we have defined that the method will only execute if the query string was provided, the second method GetUserByDob has no constraint, which means it would still be a valid candidate, making the call still ambiguous.

For this scenario, we can do 2 things, either reject the call if lastname is provided as query string or require that dob is provided as query string. For this demo, we will provide both constraints to be explicit and make the code easier to read.

clip_image014

We will get an error that we are duplicating the attribute for the method. That’s because when we created our attribute we have not defined that it could be declared multiple times in the same method. Let’s go back there and add it.

We simply need to add the AttributeUsage attribute in the class as shown below.

clip_image015

Once the attribute is added, you should now be able to build your project.

clip_image016

Now it is time for the truth, go back to Fiddler and make a new request

clip_image018

As we can see the request is executed with success, HTTP 200 is returned. We can also now test providing date of birth and the call should also succeed.

clip_image020

In this post, we learned how to identify a method ambiguity call and use the ActionMethodSelectorAttribute class to help us resolve the issue.

I hope this post has helped you!

Comments (4)

  1. Paul says:

    You could handle this differently by having a single get action that takes in a viewmodel with both options. You can validaate the view model with IValidatableObject to ensure that it is valid (ie at least one value is present but perhaps not both) and then filter you results accordingly

    Or you could just make sure that actions do not use the same route by using the HttpGet overload that takes a route template.

    e.g. [Route(“GetUserByDateOfBirth”)

    1. Gustavo says:

      To use Route, you would need to modify the caller and as the post mentioned, those are for scenarios where you don’t have control on the caller. In your example the route defined would need to be part of the url.

  2. Tom says:

    You could easily solve this using routing attribute

    1. Gustavo says:

      The post mention that potentially you can achieve it with routing, but in some scenario the routing can be complex and hard to achieve. Keep in mind you cannot modify anything on the caller side, including the url that is being called. This is an alternative solution for such scenarios.

Skip to main content