Semplificare la gestione degli array in querystring in ASP.NET Core

di Marco De Sanctis, in ASP.NET Core,

Nella sua implementazione di default, ASP.NET Core gestisce array in querystring tramite ripetizioni della stessa chiave:

Questa notazione può risultare alle volte verbosa e poco leggibile, e magari può avere senso modificare leggermente la gestione del parsing della richiesta per supportare qualcosa di più conciso, come una sequenza separata da virgole, per esempio:

Fondamentalmente, esistono due tipologie di oggetti che vengono sfruttati dal runtime per trasformare un valore in querystring, in un oggetto C# che venga poi passato al nostro controller:

  • i ValueProvider, che hanno il compito di leggere il contenuto string dalla request, in diversi formati e sezioni (per es. il body di una form, la querystring, un file in upload, ecc.);
  • i ModelBinder, che invece sfruttano i primi per recuperare questo valore "raw" in formato stringa, e trasformarlo in un vero e proprio oggetto C#.

Quale strumento usare?

Volendo personalizzare il modo in cui vengono interpretati gli array (o, genericamente le collection), un possibile approccio potrebbe essere quello di creare un ModelBinder personalizzato in grado di interpretare una lista di stringhe separata da virgole. Questo approccio ha il vantaggio di essere molto mirato - infatti, grazie all'attribute ModelBinder possiamo di volta in volta indicare dove vogliamo usarlo.

public string Get(
  [FromQuery][ModelBinder(typeof(MyCustomArrayBinder))]string[] values)

Tuttavia, nel momento in cui vogliamo supportare diversi tipi di array (string, int, DateTime, ecc.) o diverse tipologie di collection (IEnumerable, IList, List, ecc.) ci accorgiamo che il lavoro da svolgere è tutt'altro che banale: il framework ASP.NET Core, infatti, ha una complessa gerarchia di binders che si occupano di gestire tutte queste casistiche, che dovremmo riscriverci più o meno da capo.

Un custom ValueProvider è la risposta

Un differente approccio, allora, è quello di scrivere un custom ValueProvider che interpreti in maniera differente parametri in cui sia presente la virgola, restituendo invece che un unico valore, una collection di string, che poi sarà interpretata dall'infrastruttura standard di ModelBinding, con tutti i vantaggi e l'affidabilità che questo comporta.

Un custom ValueProvider non è altro che una classe che implementa l'interfaccia IValueProvider. Nel nostro caso, però, invece che implementarlo da zero, ci limiteremo a modificare il comportamento dell'oggetto QueryStringValueProvider già esistente:

public class QueryStringCommaSeparatedValueProvider : QueryStringValueProvider
{
  public QueryStringCommaSeparatedValueProvider(
    BindingSource bindingSource,
    IQueryCollection values,
    CultureInfo culture) : base(bindingSource, values, culture)
  { }

  public override ValueProviderResult GetValue(string key)
  {
    var result = base.GetValue(key);

    if (result == ValueProviderResult.None)
    {
      return result;
    }

    var values = result.SelectMany(r => r.Split(","));

    return new ValueProviderResult(
      new StringValues(values.ToArray()),
      this.Culture);
  }
}

Nel codice in alto abbiamo definito un nuovo QueryStringCommaSeparatedValueProvider ed effettuato l'override del metodo GetValue. Dopo aver recuperato il risultato della classe base, lo trasformiamo tramite l'operatore SelectMany, così da restituire eventuali stringhe contenenti virgole come un array di valori separati.

Questo farà sì che tutti i model binder invocati successivamente gestiscano questa collection di valori in maniera del tutto standard, senza accorgersi che il formato della querystring è effettivamente diverso.

Come utilizzare questo provider?

Per poter essere utilizzato, abbiamo bisogno di una corrispondente classe factory che analizzi la richiesta e ne restituisca un'istanza nel caso in cui il nostro provider sia idoneo a gestirla. Questa classe dovrà ereditare da IValueProviderFactory:

public class QueryStringCommaSeparatedValueProviderFactory : IValueProviderFactory
{
  public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
  {
    if (context == null)
    {
      throw new ArgumentNullException(nameof(context));
    }

    var query = context.ActionContext.HttpContext.Request.Query;
    if (query != null && query.Count > 0)
    {
      var valueProvider = new QueryStringCommaSeparatedValueProvider(
        BindingSource.Query,
        query,
        CultureInfo.InvariantCulture);

        context.ValueProviders.Add(valueProvider);
    }

    return Task.CompletedTask;
  }
}

L'implementazione è piuttosto semplice, e verifica semplicemente che sia presente una querystring nella richiesta HTTP, per poi costruire un'istanza di QueryStringCommaSeparatedValueProvider.

Per attivare questa factory, sarà sufficiente registrarla nella classe di Startup:

public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc(options => 
  {
    options.ValueProviderFactories.Insert(0,
      new QueryStringCommaSeparatedValueProviderFactory());
  });

  // altro codice qui..
}

A questo punto siamo finalmente in grado di usarlo, e il bello di questa soluzione è che tutto avviene senza modificare il codice nei nostri controller:

[HttpGet]
public string Get([FromQuery]IEnumerable<int> values)
{
  return string.Join(" - ", values);
}

Nell'esempio in alto, stiamo utilizzando un IEnumerable di int, ma ovviamente il tutto funzionerà con qualsiasi collection, sia generiche che non, e supporterà anche la gestione standard degli errori nel ModelState.

Un possibile problema di questa soluzione è che si applica a tutte le richieste, cosa che potrebbe generare degli effetti collaterali nel caso in cui abbiamo parametri contenenti virgole, che non sono però array. In un prossimo script vedremo un paio di differenti approcci per rendere la nostra soluzione più affidabile.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi