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
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Progressive Web Apps with Angular
Impostare e validare il tipo dei parametri nei template delle pipeline di Azure DevOps
.NET Conference Italia 2020
Registrare un servizio generico nella dependency injection di ASP.NET Core
Creare un componente Button in Blazor per operazioni asincrone
Creare due extension method per serializzare un oggetto in JSON e viceversa utilizzando la libreria System.Text.Json
Eseguire lo shutdown pulito di un'applicazione ASP.NET Core
Avviare una registrazione audio e video in una applicazione della Universal Windows Platform
Graph API con .NET 5
Validare una pipeline YAML senza eseguirla in Azure DevOps
Montare una file share con Azure Container Instance
.NET Core <3 Azure Apps
I più letti di oggi
- ecco tutte le novità pubblicate sui nostri siti questa settimana: https://aspit.co/wkly buon week-end!
- Modificare automaticamente la Wiki da una pipeline YAML con Azure DevOps
- Gestione dei token negli input di testo con la Universal Windows Platform
- Utilizzare le proprietà Init-only per inizializzare una proprietà in C# 9
- Effettuare il redirect da HTTP a HTTPS con la Azure CDN
- Creare template HTML con Slim