Reputation: 648
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:
api/StaticFile
-> goes to Get()
actionapi/StaticFile?name=someFunnyName
-> goes to GetByName()
actionMy 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)
Upvotes: 4
Views: 5154
Reputation: 155
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");
}
}
Upvotes: 0
Reputation: 648
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
Upvotes: 1
Reputation: 27793
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
Upvotes: 5
Reputation: 12171
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.
Upvotes: 4