I'm upgrading backend from .NET Framework to .NET Core 6.
We want to be old-style API endpoints compatible (we don't want to change anything in Client code base), so in our BaseApiController we are using:
[ApiController]
[Route("/api/[controller]")]
[Route("/api/[controller]/[action]")]
public class BaseApiController : ControllerBase
In StaticFileController we have two GET methods:
[HttpGet("GetByName")] // Together with [RequiredFromQuery] it is working for api/StaticFile/GetByName?name=xx, but not for api/StaticFile?name=xx
//[HttpGet("{name}")] // Not working: api/StaticFile/GetByName?name=xx redirects to Get() method
//[HttpGet] // Swagger exception: InvalidOperationException: The method 'get' on path '/api/StaticFile' is registered multiple time
[ResponseType(typeof(FileActionResult))]
public HttpResponseMessage GetByName([RequiredFromQuery] string name)
{}
[HttpGet]
public IActionResult Get()
{}
And in Client we have API call:
return GetData("StaticFile/?name=" name);
Thanks to [RequiredFromQuery] attribute, we can call this endpoint with success:
api/StaticFile/GetByName?name=working
But with this solution we can't call old API endpoints like this (this call will always call Get() method, not GetByName()):
api/StaticFile?name=not_working_redirects_to_get_method
How to achieve this goal?
Endpoints configuration in Startup.cs is default:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapDefaultControllerRoute();
});
CodePudding user response:
One solution could be to add filtering to the generic Get().
For example:
[HttpGet]
public IActionResult Get([FromQuery, Required = False] string name)
{
if (!string.IsNullOrWhitespace(name)
{
return GetByName(name);
}
// Whatever the Get() method normally does.
}
This isn't the cleanest solution, but should be simple to implement.
CodePudding user response:
I fixed it by using two solutions together from Handle multiple endpoints in .NET Core 3.1 Web API by Query Params
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class QueryStringConstraintAttribute : ActionMethodSelectorAttribute
{
public string QueryStringName { get; set; }
public bool CanPass { get; set; }
public QueryStringConstraintAttribute(string queryStringName, bool canPass)
{
QueryStringName = queryStringName;
CanPass = canPass;
}
/// <summary>
/// Simplified from Original (from StackOverflow) for using one action attribute in controller
/// </summary>
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
StringValues value;
routeContext.HttpContext.Request.Query.TryGetValue(QueryStringName, out value);
if (!string.IsNullOrEmpty(QueryStringName) && !StringValues.IsNullOrEmpty(value))
{
return CanPass;
}
return !CanPass;
}
}
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;
}
}
Usage in my controller:
[HttpGet]
[ResponseType(typeof(FileActionResult))]
[QueryStringConstraint("name", true)]
public HttpResponseMessage GetByName([RequiredFromQuery] string name)
{}
[Microsoft.AspNetCore.Mvc.ApiExplorerSettings(IgnoreApi = true)]
[HttpGet]
[QueryStringConstraint("name", false)]
public IActionResult Get()
{}
Ignoring Get() from Swagger documentation was needed, because without it, Swagger will throw exception (duplicated GET actions)
