Skip to content

Instantly share code, notes, and snippets.

@yringler
Last active April 1, 2021 16:39
Show Gist options
  • Save yringler/eb10736533f2e63e54fcbac6a95d752e to your computer and use it in GitHub Desktop.
Save yringler/eb10736533f2e63e54fcbac6a95d752e to your computer and use it in GitHub Desktop.
MVC Core: Prefer action which best matches query parameters
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using MoreLinq;
using System;
using System.Linq;
namespace Api.Constraints
{
/// <summary>
/// Use amount of matching query parameters to find the right action.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RouteWithQueryParamatersAttribute : Attribute, IActionConstraint
{
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
var queryKeys = context.RouteContext.HttpContext.Request.Query.Keys;
var actionIdsAndCount = context.Candidates.Select(
candidate =>
{
// Get a list of all of the actions parameters which come from the query string.
var paramaters =
candidate.Action.Parameters
.Where(paramater => paramater.BindingInfo.BindingSource.Id == BindingSource.Query.Id)
.ToList();
return new
{
ParamaterCount = paramaters.Count,
// The number of parameters which can be provided by the current query string.
MatchingCount =
paramaters.Count(
paramater => queryKeys.Contains(paramater.Name)),
ParamaterId = candidate.Action.Id
};
}).ToList();
/*
* If none of the candidates are able to use the query string (perhaps[1] because there isn't a query string),
* accept an action which doesn't need any query parameters.
*
* [1] It would be simplest to say that an action with no query parameters only matches
* a request with no query parameters. This isn't robust, though, because a query string could be used eg for authentication.
* Therefor, the rule is that an action with no query parameters matches a request with no query parameters *relevant to the actions*,
* as defined above.
*/
if (actionIdsAndCount.All(action => action.MatchingCount == 0))
return actionIdsAndCount.First(action => action.ParamaterId == context.CurrentCandidate.Action.Id).ParamaterCount == 0;
// Get the actions which have the highest percentage of matched query parameters.
var highest = actionIdsAndCount
// If an action doesn't accept any query parameters, it only matches a request without any
// query parameters, and that was already handled earlier.
.Where(item => item.ParamaterCount > 0)
// Get the matches with the greatest percentage of used parameters.
.MaxBy(item => item.MatchingCount / item.ParamaterCount)
// If multiple actions have the same percentage, prefer the action with the highest number
// of used parameters.
// A common case would be if one controller accepts 2 parameters, and another accepts the same 2 and another 1, and
// the request has those 3 parameters - both actions have %100 of their parameters matching, so prefer the one which has
// 3 paramaters matching.
.MaxBy(item => item.MatchingCount);
// The current action is accepted if it is one of the highest.
return highest.Any(action => action.ParamaterId == context.CurrentCandidate.Action.Id);
}
}
}
@a4amaan
Copy link

a4amaan commented Apr 1, 2021

how to use it? i mean where to register it?

@yringler
Copy link
Author

yringler commented Apr 1, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment