4

I am migrating controllers from .NET Framework to .NET Core and I want to be compatibility with API calls from previous version. I have problem with handling multiple routes from Query Params.

My example controller:

[Route("/api/[controller]")]
[Route("/api/[controller]/[action]")]
public class StaticFileController : ControllerBase
{
    [HttpGet("{name}")]
    public HttpResponseMessage GetByName(string name)
    {
    }

    [HttpGet]
    public IActionResult Get()
    {
    }
}

Calling api/StaticFile?name=someFunnyName will lead me to Get() action instead of expected GetByName(string name).

What I want to achieve:

  • Calling GET api/StaticFile -> goes to Get() action
  • Calling GET api/StaticFile?name=someFunnyName -> goes to GetByName() action

My app.UseEndpoints() from Startup.cs have only these lines:

endpoints.MapControllers();
endpoints.MapDefaultControllerRoute();

If I use [HttpGet] everywhere and add ([FromQuery] string name) it gets me AmbiguousMatchException: The request matched multiple endpoints

Thank you for your time to helping me (and maybe others)

3
  • A bit off-topic, but keep in mind .NET Core 3.1 is out of support. If you can, migrate to .NET8 instead. Commented Feb 29, 2024 at 16:58
  • @julealgon: This is old question from 2020... Commented Mar 5, 2024 at 14:52
  • Oh, I actually completely missed that detail... thought this was brand new :D My bad, should've paid more attention. Commented Mar 5, 2024 at 15:06

4 Answers 4

5

What I want to achieve:

  • Calling GET api/StaticFile -> goes to Get() action
  • Calling GET api/StaticFile?name=someFunnyName -> goes to GetByName() action

To achieve above requirement of matching request(s) to expected action(s) based on the query string, you can try to implement a custom ActionMethodSelectorAttribute and apply it to your actions, like below.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class QueryStringConstraintAttribute : ActionMethodSelectorAttribute
{
    public string QueryStingName { get; set; }
    public bool CanPass { get; set; }
    public QueryStringConstraintAttribute(string qname, bool canpass)
    {
        QueryStingName = qname;
        CanPass = canpass;
    }
    public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
    {
        StringValues value;

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

        if (QueryStingName == "" && CanPass)
        {
            return true;
        }
        else
        {
            if (CanPass)
            {
                return !StringValues.IsNullOrEmpty(value);
            }

            return StringValues.IsNullOrEmpty(value);
        }
    }
}

Apply to Actions

[Route("api/[controller]")]
[ApiController]
public class StaticFileController : ControllerBase
{
    [HttpGet]
    [QueryStringConstraint("name", true)]
    [QueryStringConstraint("", false)]
    public IActionResult GetByName(string name)
    {
        return Ok("From `GetByName` Action");
    }

    [HttpGet]
    [QueryStringConstraint("name", false)]
    [QueryStringConstraint("", true)]
    public IActionResult Get()
    {
        return Ok("From `Get` Action");
    }
}

Test Result

enter image description here

Sign up to request clarification or add additional context in comments.

1 Comment

4

The parameter for HttpGet sets the route, not query string parameter name.

You should add FromQuery attribute for action parameter and use HttpGet without "{name}":

[HttpGet]
public HttpResponseMessage GetByName([FromQuery] string name)
{
    // ...
}

You can also set different name for query parameter:

[HttpGet]
public HttpResponseMessage GetByName([FromQuery(Name = "your_query_parameter_name")] string name)
{
    // ...
}

But now you have two actions matching same route so you will get exception. The only way to execute different logic based on query string part only (the route is the same) is to check query string inside action:

[HttpGet]
public IActionResult Get([FromQuery] string name)
{
    if (name == null)
    {
        // execute code when there is not name in query string
    }
    else
    {
        // execute code when name is in query string
    }
}

So you have only one action which handles both cases using same route.

4 Comments

AmbiguousMatchException: The request matched multiple endpoints and matches both GetByName and Get. This same if I have [HttpGet] and GetByName(string name) together
@Saibamen, yes, because you have two actions matching same route (route is same even if query string is different). You should handle this using only one action or specify different route for one of the actions
There is no solution for it? Like adding some options to .NET Core route mechanism? I really need to be backward compatibility with .NET Framework calls in new .NET Core project - there is more than 100 controllers. Thank you for your answers. I also edited my main question with API calls just to be more readable from beginning.
@Saibamen, it is not possible to have two actions matching same route (query string is not used to differentiate routes). You can use one action and check if there is query string in request url - if query string is not empty then proceed with one logic, if it is empty then proceed with another login. This is the only possible way to execute different login based only on query string (when the route is same). See edited answer
1

I got my solution from https://www.strathweb.com/2016/09/required-query-string-parameters-in-asp-net-core-mvc/

public class RequiredFromQueryAttribute : FromQueryAttribute, IParameterModelConvention
{
    public void Apply(ParameterModel parameter)
    {
        if (parameter.Action.Selectors != null && parameter.Action.Selectors.Any())
        {
            parameter.Action.Selectors.Last().ActionConstraints.Add(new RequiredFromQueryActionConstraint(parameter.BindingInfo?.BinderModelName ?? parameter.ParameterName));
        }
    }
}
public class RequiredFromQueryActionConstraint : IActionConstraint
{
    private readonly string _parameter;

    public RequiredFromQueryActionConstraint(string parameter)
    {
        _parameter = parameter;
    }

    public int Order => 999;

    public bool Accept(ActionConstraintContext context)
    {
        if (!context.RouteContext.HttpContext.Request.Query.ContainsKey(_parameter))
        {
            return false;
        }

        return true;
    }
}

For example, if using [RequiredFromQuery] in StaticFileController we are able to call /api/StaticFile?name=withoutAction and /api/StaticFile/GetByName?name=wAction but not /api/StaticFile/someFunnyName (?name= and /)

Workaround solution for that is to create separate controller action to handle such requests

2 Comments

Helped me out a lot. Here's a gist of the attribute: gist.github.com/SerenityInAllThings/…
@PetersonV: Your gist is not working for me - AmbiguousMatchException: The request matched multiple endpoints.
0

I have taken the answer by Fei Han and simplified it even more:

/// <summary>
/// This is an attribute that functions as a guard for query parameters. Input the query parameters that may pass where "" means no parameters.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class QueryParameterGuardAttribute(params string?[] whitelistedQueryParams) : ActionMethodSelectorAttribute
{

    public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
    {
        var requestQueryParams = routeContext.HttpContext.Request.Query;
        return (whitelistedQueryParams is [""] && requestQueryParams.Count == 0) || requestQueryParams.Keys.SequenceEqual(whitelistedQueryParams);
    }
}

You don't need the canpass attribute anymore, and you can define multiple query parameters.

[Route("api/[controller]")]
[ApiController]
public class StaticFileController : ControllerBase
{
    [HttpGet]
    [QueryStringConstraint("name", "lastname")]
    public IActionResult GetByName(string name, string lastname)
    {
        return Ok("From `GetByName` Action");
    }
    [HttpGet]
    [QueryStringConstraint("name")]
    public IActionResult GetByName(string name)
    {
        return Ok("From `GetByName` Action");
    }

    [HttpGet]
    [QueryStringConstraint("")]
    public IActionResult Get()
    {
        return Ok("From `Get` Action");
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.