Search code examples
c#asp.net.netasp.net-coreasp.net-core-webapi

Handle multiple endpoints in .NET Core 3.1 Web API by Query Params


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)


Solution

  • 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