Rendere sicura una form di edit con ViewModel e AutoMapper in ASP.NET Core MVC

di Marco De Sanctis, in ASP.NET Core,

Quando realizziamo una form di edit in ASP.NET Core MVC, grazie al model binding, possiamo facilmente implementare un metodo che accetti la entity di dominio, come quello in basso, che verrà automaticamente istanziata e popolata dal runtime in base ai dati ricevuti in POST.

public async Task<IActionResult> Edit(int id, Person person)
{
  if (ModelState.IsValid)
  {
     _context.Update(person);
     await _context.SaveChangesAsync();
     return RedirectToAction("Index");
  }
  return View(person);
}

Bisogna però fare molta attenzione alle implicazioni di sicurezza che questa funzionalità comporta nel caso in cui ci siano proprietà che non vogliamo esporre. Immaginiamo per esempio che Person abbia questa definizione:

public class Person
{
  public int Id { get; set; }
  public string Name { get; set; }
  public bool HasSpecialDiscount { get; set; }
}

Anche se nella form di edit non fosse presente un field per HasSpecialDiscount, un utente malintenzionato a conoscenza dell'object model, non dovrebbe far altro che inviare in POST un valore per anche questa proprietà per far sì che venga aggiornato.

Il modo più comune per risolvere questo problema è utilizzare l'attributo Bind per elencare una whitelist di campi che il model binder dovrà aggiornare:

public async Task<IActionResult> Edit(int id, [Bind("Id,Name")] Person person)
{
  ..
}

Tuttavia questo modo di procedere è piuttosto scomodo a lungo andare, perchè per ogni action dobbiamo produrre questa lista di stringhe e manutenerla nel tempo, nel caso in cui le form dovessero cambiare e diventare più complesse.

Una pratica migliore è quella di avvalersi dei ViewModel, ossia degli oggetti accessori che siano 1:1 con la view che li utilizza. L'idea è quella di creare quindi una classe specifica per la view di Person da utilizzare in luogo dell'entità di dominio. Questa classe avrà solo le proprietà effettivamente presenti nella view.

public class PersonViewModel
{
  public int Id { get; set; }
  public string Name { get; set; }
}

Possiamo poi sfruttare una libreria come AutoMapper (https://github.com/AutoMapper/AutoMapper), disponibile anche su NuGet, per mappare facilmente il ViewModel sulla entità e viceversa. Tipicamente, possiamo configurarlo all'interno della classe Startup.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
  // ... altro codice qui ...

  this.ConfigureAutoMapper();
}

private void ConfigureAutoMapper()
{
    Mapper.Initialize(cfg => 
    {
        cfg.CreateMap<Person, PersonViewModel>().ReverseMap();
    });

    Mapper.AssertConfigurationIsValid();
}

Il metodo Initialize ci consente di creare tutti i mapper che vogliamo all'interno della lambda expression. Nel nostro caso, ne abbiamo creato uno tra Person e PersonViewModel. Dato che le proprietà che vogliamo copiare hanno lo stesso nome, non dobbiamo specificare alcun dettaglio. Tramite ReverseMap, poi, abbiamo indicato che vogliamo che il mapping funzioni in entrambi i versi, ossia quando vogliamo passare da Person a PersonViewModel e viceversa.

A questo punto possiamo riformulare le nostre action e le view per utilizzare PersonViewModel invece di Person:

public async Task<IActionResult> Edit(int? id)
{
  var person = await _context.Person.SingleAsync(m => m.Id == id);
    
  PersonViewModel model = Mapper.Map<PersonViewModel>(person);
  return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, PersonViewModel model)
{
  if (ModelState.IsValid)
  {
    var person = await _context.Person.SingleAsync(x => x.Id == id);
    Mapper.Map(model, person);

    await _context.SaveChangesAsync();
    return RedirectToAction("Index");
  }

  return View(model);
}

Nella action di GET, dopo aver recuperato la Person tramite l'id, costruiamo un'istanza di PersonViewModel tramite il metodo Mapper.Map. Questo oggetto sarà l'effettivo model che invieremo alla view.

In maniera simile, nella action di POST, recuperiamo ancora la Person tramite l'id e poi usiamo sempre Mapper.Map, ma con un overload diverso, che ci consente di sovrascrivere le proprietà di un oggetto esistente. A questo punto, non dobbiamo fare altro che invocare SaveChangesAsync per persistere le modifiche sul database.

In conclusione, abbiamo scritto leggermente più codice di quanto necessario con l'attributo Bind, ma abbiamo il pregio di avere ora a disposizione un oggetto che rappresenta l'effettivo model su cui la view deve operare, completamente disaccoppiato dalla entity di dominio, che potrebbe variare senza che dobbiamo per questo mettere mano alle view.

Grazie ad AutoMapper, il codice necessario per mappare le proprietà tra di loro è estremamente conciso, e siamo comunque riusciti nell'intento di mantenere sicura la nostra form di Edit, anche a fronte di modifiche future.

Un simile approccio è utilizzabile, ovviamente, anche nei controller di Web API.

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