When porting an old .NET MVC/API application to ASP .NET Core application things might not be as straightforward as you’d expect. A personal application that I was porting tumbled and even the most basic scenario wouldn’t work the way I expected (or the way the old .NET MVC used to work). This was nowhere on the massive Microsoft documentation website either. Read on, as I have figured out what was happening.

Let me tell you what the problem was to begin with.

The controller has two actions with two different method names. Both of the methods accept a boolean (with different names). The caller would pass the parameter name and the value of the parameter. The controller code would look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
[HttpPost]
public ActionResult DoSomething(bool real)
{
return Content("Something Done");
}

[HttpPost]
public ActionResult DoSomethingElse(bool unreal)
{
return Content("Something else done");
}
}

And the caller would invoke the endpoints like /my?real=true or /my?unreal=true. While this code works fine in the old asp.net WebAPI, this would not work in the new asp.net core application.

But why? There is a history…

So although this was working in my case (asp.net mvc/api) as I was using ApiController class, if you used the other Controller class it would not work. Why the descrepancy? I don’t know for sure, but probably because MVC and WebAPI was developed by two different teams and had different timelines and design decisions.

The design for ApiController pipeline was that it would match the action parameter names against the Query String parameters. The Controller class does not, which means you have to make it non-ambiguous (e.g. have one action per HTTP verb in a controller).

Okay, so it worked in ApiController, so why is it not working in ASP.NET Core? In the .NET Core, they merged the two frameworks into one (yay) but they had to make a decision as to which model they wanted to support and they went the MVC controller model (boo).

When you try to read the controller via my?real=true, you get the following exception:

1
2
3
4
AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied:

Controllers.MyController.DoSomething
Controllers.MyController.DoSomethingElse

The Fix

The error message should be clear: you need to make your actions less ambiguous. A way to make it less-ambiguous is to map the parameters to querystring parameters (like how the old ApiController did).

The solution would be to create a new attribute for your actions and inherit from ActionMethodSelectorAttribute so that it gets invoke when the request comes in and you get a say if the action is a right match for the incoming request. The attribute would look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class MapToQueryStingAttribute : ActionMethodSelectorAttribute
{
public string QueryStingName { get; set; }

public MapToQueryStingAttribute(string qname)
{
QueryStingName = qname;
}

public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
StringValues value;

routeContext.HttpContext.Request.Query.TryGetValue(QueryStingName, out value);

return !StringValues.IsNullOrEmpty(value);
}
}

and now you can decorate your actions like so.

1
2
3
4
5
6
7
8
9
10
11
12
13
[HttpPost]
[MapToQuerySting("real")]
public ActionResult DoSomething(bool real)
{
return Content("Something Done");
}

[HttpPost]
[MapToQuerySting("unreal")]
public ActionResult DoSomethingElse(bool unreal)
{
return Content("Something else done");
}

What we say here is that if the query string has that variable name (e.g. ‘real’ or ‘unreal’) match it against the action. Now is depending on your actions and the complexity (e.g. how many actions you have, if you have overloads that could cause overlaps and further ambiguity) this may not be enough. So you may need to further enhance the attribute to say: if the incoming request has this variable but doesn’t have that variable, then map it. In those cases you can use this version of the attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public string QueryStingName { get; set; }
public bool ShouldInclude { get; set; }

public MapToQueryStingAttribute(string qname, bool shouldInclude=true)
{
QueryStingName = qname;
ShouldInclude = shouldInclude;
}

public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
StringValues value;

routeContext.HttpContext.Request.Query.TryGetValue(QueryStingName, out value);

if (ShouldInclude)
{
return !StringValues.IsNullOrEmpty(value);
}

return StringValues.IsNullOrEmpty(value);
}

and now you can say mix the attribute to say the request should include and should not include what parameters.

1
2
3
4
5
6
7
[HttpGet]
[MapToQuerySting("real")]
[MapToQuerySting("unreal", ShouldInclude = false)]
public ActionResult DoSomething(bool real)
{
return Content("Something Done");
}

Happy porting!