Gérer les paramètres de query string sans valeur en ASP.NET Core
Anatomie d'une query string
Les query strings sont généralement constituées d'une séquence de paires clé-valeur, comme ?foo=hello&bar=world…
. Cependant, si on regarde la RFC 3986, on voit que les query strings sont spécifiées de façon assez vague. Il est mentionné que
query components are often used to carry identifying information in the form of "key=value" pairs
(en français : les composants de la query sont souvent utilisés pour porter des informations d'identification sous la forme de paires "clé=valeur")
Mais c'est juste une observation, pas une règle (les RFC utilisent habituellement un langage très spécifique pour les règles, avec des mots comme MUST, SHOULD, etc.). Donc en gros, une query string peut contenir à peu près n'importe quoi, ce n'est pas du tout standardisé. L'utilisation de paires clé-valeur séparées par &
est juste une convention, pas une obligation.
Et il se trouve qu'il n'est pas rare de voir des URLs avec des query strings comme ceci : ?foo
, c'est-à-dire une clé sans valeur. La façon dont ça doit être interprété dépend de l'implémentation, mais dans la plupart des cas, ça veut probablement dire la même chose que ?foo=true
: la simple présence du paramètre est interprétée comme une valeur true
implicite.
Les query strings en ASP.NET Core
Malheureusement, en ASP.NET Core MVC, cette forme de query string n'est pas supportée, à part pour des paramètres de type string
. Si vous avez une action de contrôleur comme celle-ci :
[HttpGet("search")] public IActionResult Search( [FromQuery] string term, [FromQuery] bool ignoreCase) { … }
Le model binder par défaut attend pour le paramètre ignoreCase
une valeur explicite true
ou false
, par exemple ignoreCase=true
. Si on omet la valeur, elle sera considérée comme vide, et le binding du modèle échouera :
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "|53613c25-4767e032425dfb92.", "errors": { "ignoreCase": [ "The value '' is invalid." ] } }
Ce n'est pas un très gros problème, mais c'est agaçant… Voyons donc comment y remédier !
Créer son propre model binder
Par défaut, le model binding d'un paramètre booléen est fait par la classe SimpleTypeModelBinder
, qui est utilisée pour la plupart des types primitifs. Ce model binder utilise le TypeConverter
du type cible pour convertir une valeur de type string
vers le type du paramètre. Dans ce cas, le convertisseur est un BooleanConverter
, qui ne reconnait pas une valeur vide…
Il faut donc créer notre propre model binder, qui va interpréter la présence d'une clé sans valeur comme un true
implicite :
class QueryBooleanModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (result == ValueProviderResult.None) { // Le paramètre est absent, interpréter comme false bindingContext.Result = ModelBindingResult.Success(false); } else { bindingContext.ModelState.SetModelValue(bindingContext.ModelName, result); var rawValue = result.FirstValue; if (string.IsNullOrEmpty(rawValue)) { // La valeur est absente, interpréter comme true bindingContext.Result = ModelBindingResult.Success(true); } else if (bool.TryParse(rawValue, out var boolValue)) { // La valeur est un booléen valide, utiliser cette valeur bindingContext.Result = ModelBindingResult.Success(boolValue); } else { // La valeur est autre chose, échec bindingContext.ModelState.TryAddModelError( bindingContext.ModelName, "Value must be false, true, or empty."); } } return Task.CompletedTask; } }
Pour pouvoir utiliser ce model binder, il nous faut aussi un ModelBinderProvider
:
class QueryBooleanModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context.Metadata.ModelType == typeof(bool) && context.BindingInfo.BindingSource != null && context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Query)) { return new QueryBooleanModelBinder(); } return null; } }
Celui-ci va renvoyer notre model binder si le type du paramètre est bool
et si la source du binding est la query string. Il suffit maintenant d'ajouter notre provider à la liste des providers d'ASP.NET MVC :
// Dans Startup.ConfigureServices services.AddControllers(options => { options.ModelBinderProviders.Insert( 0, new QueryBooleanModelBinderProvider()); });
Note : Ce code est pour un projet ASP.NET Core 3 Web API.
- Si votre projet utilise aussi des vues ou des pages, remplacez
AddControllers
parAddControllersWithViews
ouAddRazorPages
, selon les cas. - Si vous utilisez ASP.NET Core 2, remplacez
AddControllers
parAddMvc
.
Remarquez qu'on doit insérer notre provider au début de la liste. Si on l'ajoute à la fin, un autre provider prendra la main avant, et le notre ne sera même pas appelé.
Et c'est tout : on peut maintenant appeler notre API avec une query string comme ?term=foo&ignoreCase
, sans spécifier explicitement true
comme valeur de ignoreCase
.
Une amélioration possible de notre model binder serait d'accepter également 0
ou 1
comme des valeurs valides pour les paramètres booléens. Je laisse ça comme exercice pour le lecteur !
Commentaires