Defining ASP.NET Core Controller action constraint to match the correct action
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
Run your project and using fiddler make the request.
We can see on the images above that the request was performed and we received the expected result.
Now let’s add a new method to retrieve users by date of birth.
Build your project, run and perform the exact same request we did before, trying to retrieve the user based on last name.
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 to you.
We can see on the output window that the request was made, however, request matched multiple actions resulting in ambiguity, in other words, 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 querystring. 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 querystring, 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 querystring. 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 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 using 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 represent 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 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.
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 seeing the image below.
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.
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.
We will get an error that we are duplicating the attribute for the method. That’s is 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.
Once the attribute was added, you should not be able to build your project.
Now it is time for the truth, go back to fiddler and make a new request
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.
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!