Home > OS >  .NET Core: Two GET actions, one with `[RequiredFromQuery]` attribute gives `The method 'get
.NET Core: Two GET actions, one with `[RequiredFromQuery]` attribute gives `The method 'get

Time:02-05

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)

  •  Tags:  
  • Related