Required query string parameters in ASP.NET Core MVC

Β· 795 words Β· 4 minutes to read

Today let’s have a look at two extensibility points in ASP.NET Core MVC - IActionConstraint and IParameterModelConvention. We’ll see how we can utilize them to solve a problem, that is not handled out of the box by the framework - creating an MVC action that has mandatory query string parameters.

Let’s have a look.

IActionConstraint extensibility point πŸ”—

ASP.NET Core MVC allows us to participate in the decision making regarding selecting an action suitable to handle the incoming HTTP request. We can do that through IActionConstraint extensibility point, which is a more powerful version of ActionMethodSelectorAttribute from “classic” ASP.NET MVC.

The interface is shown below.

    public interface IActionConstraint : IActionConstraintMetadata
    {
        /// <summary>
        /// The constraint order.
        /// </summary>
        /// <remarks>
        /// Constraints are grouped into stages by the value of <see cref="Order"/>. See remarks on
        /// <see cref="IActionConstraint"/>.
        /// </remarks>
        int Order { get; }

        /// <summary>
        /// Determines whether an action is a valid candidate for selection.
        /// </summary>
        /// 
        /// <returns>True if the action is valid for selection, otherwise false.</returns>
        bool Accept(ActionConstraintContext context);
    }

So, when creating a custom IActionConstraint, you effectively just have to handle one method - Accept, which should return true when the action is suitable for handling the current request, or false, if it isn’t. Naturally, the ActionConstraintContext object would give you access to the current HttpContext.

The framework itself uses this mechanism to extend action selection based on HTTP verb or based on request media type.

Building your own IActionConstraint πŸ”—

Now, in our example, we’d like to constraint based on query string.

Imagine the following action:

    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        [HttpGet("{id}")]
        public string Get(int id, string foo, string bar)
        {
            return id + " " + foo + " " + bar;
        }
    }

In this case, the action takes 3 parameters - id, foo and bar. However, only id is mandatory - because it’s part of the route, the latter ones are optional. This means that all 4 of the following URLs would be valid and lead to our action:

  • GET api/values/5
  • GET api/values/5?foo=a
  • GET api/values/5?bar=b
  • GET api/values/5?foo=a&bar=b

Now, let’s restrict these URLs to only the last one - by making our query strings mandatory. MVC comes with [FromQuery] attribute, which restricts binding of the data to query string only, but it still treats them as optional if we use it, so the code shown below still wouldn’t work like we want; it would simply stop looking at other (non-query string) binding sources for our foo and bar parameters.

    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        [HttpGet("{id}")]
        public string Get(int id, [FromQuery]string foo, [FromQuery]string bar)
        {
            return id + " " + foo + " " + bar;
        }
    }

The solution is to implement our own attribute, which we will get to in a second.

But first let’s create an IActionConstraint. The constraint will be pretty simple - we will be creating a single instance of a constraint for each mandatory parameter, and if a matching parameter is not found on the current request (in the query string, naturally) then we will return false from the Accept method.

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;
    }
}

We chose order with a high value to make sure our constraint runs last, especially after the built in framework constraints (some of which have order of 200).

The final piece is to apply this constraint to specific parameters.

Stitching it together via IParameterModelConvention πŸ”—

We could subclass the existing FromQueryAttribute (the one we originally deemed unsuitable for us), since it will force the correct binding source for us, and make sure that the constraint is applied to the parameter if that parameter is decorated with our attribute. This is shown next:

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));
        }
    }
}

We are able to achieve this via IParameterModelConvention - which gives us an option to add an extra constraint to each action by visiting all of the parameters of the discovered actions. There are also other ways of applying it - for example you could use IApplicationModelConvention too.

So now we could decorate our mandatory query string parameters with our new attribute, and voila!

[Route("api/[controller]")]
public class ValuesController : Controller
{
    [HttpGet("{id}")]
    public string Get(int id, [RequiredFromQuery]string foo, [RequiredFromQuery]string bar)
    {
        return id + " " + foo + " " + bar;
    }
}

Right now, only the following URL GET api/values/5?foo=a&bar=b would lead us into the action above - the other combinations of parameters would result in 404.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP