Leggere in inglese

Condividi tramite


Associazione di modelli in ASP.NET Core

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Questo articolo illustra cos'è l'associazione di modelli, come funziona e come personalizzarne il comportamento.

Che cos'è l'associazione di modelli

Sia i controller che le pagine Razor funzionano con i dati provenienti da richieste HTTP. Ad esempio, i dati della route possono fornire una chiave per il record, mentre i campi di un modulo inviato possono offrire valori per le proprietà del modello. La scrittura di codice per recuperare tutti i valori e convertirli da stringhe in tipi .NET sarebbe noiosa e soggetta a errori. L'associazione di modelli consente di automatizzare questo processo. Il sistema di associazione di modelli:

  • Recupera i dati da diverse origini, ad esempio i dati di route, i campi modulo e le stringhe di query.
  • Fornisce i dati ai controller e Razor alle pagine nei parametri del metodo e nelle proprietà pubbliche.
  • Converte dati stringa in tipi .NET.
  • Aggiorna le proprietà dei tipi complessi.

Esempio

Si supponga di avere il metodo di azione seguente:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

E l'app riceve una richiesta con questo URL:

https://contoso.com/api/pets/2?DogsOnly=true

L'associazione di modelli esegue i passaggi seguenti dopo che il sistema di routing seleziona il metodo di azione:

  • Trova il primo parametro di GetById, un intero denominato id.
  • Esamina le origini disponibili nella richiesta HTTP e trova id = "2" nei dati di route.
  • Converte la stringa "2" nell'intero 2.
  • Trova il parametro successivo di GetById, un valore booleano denominato dogsOnly.
  • Esamina le origini e trova "DogsOnly=true" nella stringa di query. Per la corrispondenza dei nomi non viene applicata la distinzione tra maiuscole e minuscole.
  • Converte la stringa "true" nel valore booleano true.

Il framework chiama quindi il metodo GetById, passando 2 per il parametro id e true per il parametro dogsOnly.

Nell'esempio precedente, le destinazioni di associazione del modello sono parametri di metodo che sono tipi semplici . Le destinazioni possono essere anche le proprietà di un tipo complesso. Dopo l'associazione di ogni proprietà, viene eseguita la convalida dei modelli per la proprietà. Il record dei dati associati al modello e di eventuali errori di associazione o convalida viene archiviato in ControllerBase.ModelState oppure PageModel.ModelState. Per scoprire se questo processo ha esito positivo, l'app controlla il flag ModelState.IsValid.

Target

L'associazione dei modelli cerca di trovare i valori per i seguenti tipi di destinazione:

  • Parametri del metodo di azione del controller a cui viene indirizzata una richiesta.
  • Parametri del metodo del Razor gestore Pages a cui viene indirizzata una richiesta.
  • Proprietà pubbliche di un controller o una classe PageModel, se specificate dagli attributi.

Attributo [BindProperty]

Può essere applicato a una proprietà pubblica di un controller o una classe PageModel per fare in modo che l'associazione di modelli usi tale proprietà come destinazione:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Attributo [BindProperties]

Può essere applicato a un controller o una classe PageModel per indicare all'associazione del modello di usare tutte le proprietà pubbliche della classe come destinazione:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Associazione di modelli per le richieste HTTP GET

Per impostazione predefinita, le proprietà non vengono associate per le richieste HTTP GET. In genere, per una richiesta GET è sufficiente un parametro ID record. L'ID record viene usato per cercare l'elemento nel database. Pertanto, non è necessario associare una proprietà che contiene un'istanza del modello. Negli scenari in cui si vogliono associare le proprietà ai dati dalle richieste GET, impostare la proprietà SupportsGet su true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

Associazione di modelli tipi semplici e complessi

L'associazione di modelli usa definizioni specifiche per i tipi sui quali opera. Un tipo semplice viene convertito da una singola stringa utilizzando TypeConverter o un TryParse metodo . Un tipo complesso viene convertito da più valori di input. Il framework determina la differenza in base all'esistenza di un TypeConverter o TryParse. È consigliabile creare un convertitore di tipi o usare TryParse per una string conversione da a SomeType che non richiede risorse esterne o più input.

Origini

Per impostazione predefinita, l'associazione di modelli ottiene i dati sotto forma di coppie chiave-valore dalle origini seguenti in una richiesta HTTP:

  1. Campi del modulo
  2. Il corpo della richiesta (per i controller con l'attributo [ApiController].)
  3. Dati route
  4. Parametri della stringa di query
  5. File caricati

Per ogni parametro o proprietà di destinazione, le origini vengono analizzate nell'ordine indicato nell'elenco precedente. Esistono tuttavia alcune eccezioni:

  • I dati di route e i valori delle stringhe di query vengono usati solo per i tipi semplici .
  • I file caricati vengono associati solo ai tipi di destinazione che implementano IFormFile o IEnumerable<IFormFile>.

Se l'origine predefinita non è corretta, usare uno degli attributi seguenti per specificare l'origine:

  • [FromQuery] : ottiene i valori dalla stringa di query.
  • [FromRoute] - Ottiene i valori dai dati di route.
  • [FromForm] - Ottiene i valori dai campi modulo inseriti.
  • [FromBody] - Ottiene i valori dal corpo della richiesta.
  • [FromHeader] - Ottiene i valori dalle intestazioni HTTP.

Questi attributi:

  • Vengono aggiunti alle proprietà del modello singolarmente e non alla classe del modello, come nell'esempio seguente:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Accettare facoltativamente un valore per il nome del modello nel costruttore. Questa opzione è disponibile nel caso in cui il nome della proprietà non corrisponda al valore nella richiesta. Il valore nella richiesta potrebbe, ad esempio, essere un'intestazione il cui nome contiene un trattino, come nell'esempio seguente:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Attributo [FromBody]

Applicare l'attributo [FromBody] a un parametro per popolare le relative proprietà dal corpo di una richiesta HTTP. Il runtime di ASP.NET Core delega la responsabilità di leggere il corpo in un formattatore di input. I formattatori di input sono descritti più avanti in questo articolo.

Quando [FromBody] viene applicato a un parametro di tipo complesso, tutti gli attributi di origine dell'associazione applicati alle relative proprietà vengono ignorati. Ad esempio, l'azione seguente Create specifica che il relativo pet parametro viene popolato dal corpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

La Pet classe specifica che la relativa Breed proprietà viene popolata da un parametro della stringa di query:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

Nell'esempio precedente:

  • L'attributo [FromQuery] viene ignorato.
  • La Breed proprietà non viene popolata da un parametro di stringa di query.

I formattatori di input leggono solo il corpo e non comprendono gli attributi dell'origine di associazione. Se nel corpo viene trovato un valore appropriato, tale valore viene usato per popolare la Breed proprietà.

Non applicare [FromBody] a più di un parametro per ogni metodo di azione. Quando il flusso di richiesta viene letto da un formattatore di input, non è più disponibile per la lettura per l'associazione di altri [FromBody] parametri.

Origini aggiuntive

I dati di origine vengono forniti al sistema di associazione di modelli dai provider di valori. È possibile scrivere e registrare i provider di valori personalizzati che recuperano i dati per l'associazione di modelli da altre origini. Ad esempio, potrebbe essere necessario recuperare dati dai cookie o dallo stato della sessione. Per ottenere dati da una nuova origine:

  • Creare una classe che implementi IValueProvider.
  • Creare una classe che implementi IValueProviderFactory.
  • Registrare la classe factory in Program.cs.

L'esempio include un provider di valori e un esempio di factory che ottiene i valori dai cookie. Registrare le fabbriche dei provider di valori personalizzati in Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

Il codice precedente inserisce il provider di valori personalizzato dopo tutti i provider di valori predefiniti. Per renderlo il primo nell'elenco, chiamare Insert(0, new CookieValueProviderFactory()) invece di Add.

Nessuna origine per una proprietà del modello

Per impostazione predefinita, non viene creato un errore di stato del modello se non viene trovato alcun valore per una proprietà del modello. La proprietà viene impostata su Null o su un valore predefinito:

  • I tipi semplici nullable sono impostati su null.
  • I tipi valore non nullable vengono impostati su default(T). Ad esempio, un parametro int id viene impostato su 0.
  • Per i tipi complessi, l'associazione di modelli crea un'istanza usando il costruttore predefinito, senza impostare proprietà.
  • Le matrici vengono impostate su Array.Empty<T>(), ad eccezione delle matrici byte[] che vengono impostate su null.

Se lo stato del modello deve essere invalidato quando non viene trovato nulla nei campi modulo per una proprietà del modello, usare l'attributo [BindRequired] .

Si noti che questo [BindRequired] comportamento si applica all'associazione di modelli dai dati del modulo pubblicati, non dai dati JSON o XML in un corpo della richiesta. I dati del corpo della richiesta vengono gestiti dai formattatori di input.

Errori di conversione dei tipi

Se viene trovata un'origine ma non può essere convertita nel tipo di destinazione, lo stato del modello viene contrassegnato come non valido. Il parametro o la proprietà di destinazione viene impostato su Null o su un valore predefinito, come indicato nella sezione precedente.

In un controller API con l'attributo [ApiController], uno stato del modello non valido genera una risposta HTTP 400 automatica.

In una Razor pagina, visualizzare nuovamente la pagina con un messaggio di errore.

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Quando la pagina viene riprodotta dal codice precedente, l'input non valido non viene visualizzato nel campo modulo. Questo avviene perché la proprietà del modello è stata impostata su Null o su un valore predefinito. L'input non valido viene visualizzato in un messaggio di errore. Se si desidera riprodurre nuovamente i dati non valido nel campo modulo, è consigliabile impostare la proprietà del modello su una stringa ed eseguire manualmente la conversione dei dati.

La stessa strategia è consigliata se si vuole evitare che gli errori di conversione del tipo causino errori di stato del modello. In tal caso, impostare la proprietà del modello come stringa.

Tipi semplici

Per una spiegazione dei tipi semplici e complessi, vedere Associazione di modelli di tipi semplici e complessi.

I tipi semplici in cui lo strumento di associazione di modelli può convertire le stringhe di origine includono i seguenti:

Associa con IParsable<T>.TryParse

L'API IParsable<TSelf>.TryParse supporta i valori dei parametri di azione del controller di associazione:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

La classe seguente DateRange implementa IParsable<TSelf> per supportare l'associazione di un intervallo di date:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

Il codice precedente:

  • Converte una stringa che rappresenta due date in un DateRange oggetto
  • Lo strumento di associazione di modelli usa il metodo IParsable<TSelf>.TryParse per associare il DateRange.

L'azione controller seguente usa la DateRange classe per associare un intervallo di date:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

La classe seguente Locale implementa IParsable<TSelf> per supportare l'associazione a CultureInfo:

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

L'azione controller seguente usa la Locale classe per associare una CultureInfo stringa:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

L'azione controller seguente usa le DateRange classi e Locale per associare un intervallo di date a CultureInfo:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

L'app di esempio per le API in GitHub mostra l'esempio precedente per un controller API.

Associare con TryParse

L'API TryParse supporta i valori dei parametri di azione del controller di associazione:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse è l'approccio consigliato per l'associazione di parametri perché, a differenza di TryParse, non dipende dalla reflection.

La classe seguente DateRangeTP implementa TryParse:

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

L'azione controller seguente usa la DateRangeTP classe per associare un intervallo di date:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Tipi complessi

Un tipo complesso deve avere un costruttore predefinito pubblico e proprietà scrivibili pubbliche per essere associato. Quando si verifica il binding del modello, la classe viene istanziata usando il costruttore predefinito pubblico.

Per ogni proprietà del tipo complesso, l'associazione di modelli esamina le origini per il modellodi nome prefix.property_name. Se non viene trovato, cerca semplicemente nome_proprietà senza il prefisso. La decisione di usare il prefisso non viene presa per ogni proprietà. Ad esempio, con una query contenente ?Instructor.Id=100&Name=foo, associata al metodo OnGet(Instructor instructor), l'oggetto risultante di tipo Instructor contiene:

  • Id impostato su 100.
  • Name impostato su null. L'associazione dei modelli si aspetta Instructor.Name perché nel parametro di query precedente è stato usato Instructor.Id.

Per l'associazione a un parametro, il prefisso è il nome del parametro. Per l'associazione a una proprietà pubblica PageModel, il prefisso è il nome della proprietà pubblica. Alcuni attributi hanno una proprietà Prefix che consente di eseguire l'override dell'utilizzo predefinito del nome di un parametro o una proprietà.

Ad esempio, si supponga che il tipo complesso sia la classe Instructor seguente:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefisso = nome del parametro

Se il modello da associare è un parametro denominato instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

L'associazione di modelli inizia esaminando le fonti per la chiave instructorToUpdate.ID. Se non viene trovata, cerca ID senza prefisso.

Prefisso = nome della proprietà

Se il modello da associare è una proprietà denominata Instructor del controller o della classe PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

L'associazione di modelli inizia esaminando le origini per trovare la chiave Instructor.ID. Se non viene trovata, cerca ID senza prefisso.

Prefisso personalizzato

Se il modello da associare è un parametro denominato instructorToUpdate e un attributo Bind specifica Instructor come prefisso:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

Il binding del modello inizia cercando nelle fonti la chiave Instructor.ID. Se non viene trovata, cerca ID senza prefisso.

Attributi per gli obiettivi di tipo complesso

Sono disponibili vari attributi predefiniti per controllare l'associazione di modelli di tipi complessi:

Avviso

Questi attributi influiscono sul modello di associazione quando i dati di modulo inviati sono l'origine dei valori. Non influiscono sui formattatori di input, che elaborano i corpi delle richieste JSON e XML pubblicati. I formattatori di input sono descritti più avanti in questo articolo.

Attributo [Bind]

Può essere applicato a una classe o a un parametro di metodo. Specifica quali proprietà di un modello devono essere incluse nell'associazione di modelli. [Bind]non influisce sui formattatori di input.

Nell'esempio seguente vengono associate solo le proprietà specificate del modello Instructor quando viene chiamato qualsiasi gestore o metodo di azione:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

Nell'esempio seguente vengono associate solo le proprietà specificate del modello Instructor quando viene chiamato il metodo OnPost:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

L'attributo [Bind] può essere usato per evitare l'overposting negli scenari di creazione. Non funziona bene negli scenari di modifica perché le proprietà escluse vengono impostate su Null o su un valore predefinito anziché rimanere inalterate. Per la protezione dall'overposting, è consigliabile usare i modelli di visualizzazione anziché l'attributo [Bind] . Per maggiori informazioni, vedere Nota sulla sicurezza relativa all'overposting.

Attributo [ModelBinder]

ModelBinderAttribute può essere applicato a tipi, proprietà o parametri. Consente di specificare il tipo di associazione modello usato per associare una specifica istanza o un tipo. Ad esempio:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder<MyInstructorModelBinder>] Instructor instructor)

L'attributo [ModelBinder] può essere usato anche per modificare il nome di una proprietà o di un parametro quando è associato al modello:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Attributo [BindRequired]

L'associazione di modelli induce un errore nello stato del modello se non è possibile eseguire l'associazione per una proprietà del modello. Ecco un esempio:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Vedere anche la discussione relativa all'attributo [Required] in Convalida del modello.

Attributo [BindNever]

Può essere applicato a una proprietà o a un tipo. Impedisce all'associazione di modelli di modificare una proprietà del modello. Se applicato a un tipo, il sistema di associazione di modelli esclude tutte le proprietà definite dal tipo. Ecco un esempio:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Raccolte

Per le destinazioni che sono raccolte di tipi semplici, l'associazione dei modelli cerca le corrispondenze per nome_parametro oppure nome_proprietà. Se non viene trovata alcuna corrispondenza, viene cercato uno dei formati supportati senza il prefisso. Ad esempio:

  • Si supponga che il parametro da associare sia una matrice denominata selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • I dati di modulo o di stringhe di query possono essere in uno dei formati seguenti:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evitare l'associazione di un parametro o di una proprietà denominata index o Index se è adiacente a un valore della raccolta. L'associazione di modelli tenta di usare index come indice per la raccolta, che potrebbe comportare un'associazione errata. Si consideri ad esempio l'azione seguente:

    public IActionResult Post(string index, List<Product> products)
    

    Nel codice precedente, il index parametro della stringa di query viene associato al parametro del index metodo e viene usato anche per associare la raccolta di prodotti. La ridenominazione del parametro o l'uso di un attributo di associazione di modelli per configurare l'associazione index evita questo problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • Il formato seguente è supportato solo nei dati di modulo:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Per tutti i formati di esempio precedenti, l'associazione di modelli passa una matrice di due elementi al parametro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Per i formati di dati che usano numeri con indice (... [0]... [1]...) è necessario assicurarsi che la numerazione sia progressiva a partire da zero. Se sono presenti eventuali interruzioni nella numerazione con indice, tutti gli elementi dopo l'interruzione vengono ignorati. Ad esempio, se i pedici sono 0 e 2 anziché 0 e 1, il secondo elemento viene ignorato.

Dizionari

Per le destinazioni Dictionary, l'associazione di modelli cerca le corrispondenze per nome_parametro oppure nome_proprietà. Se non viene trovata alcuna corrispondenza, viene cercato uno dei formati supportati senza il prefisso. Ad esempio:

  • Si supponga che il parametro di destinazione sia un elemento Dictionary<int, string> denominato selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • I dati di modulo o di stringhe di query inviati possono essere simili a uno degli esempi seguenti:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Per tutti i formati di esempio precedenti, l'associazione di modelli passa un dizionario di due elementi al parametro selectedCourses:

    • selectedCourses["1050"]="Chimica"
    • selectedCourses["2000"]="Economia"

Associazione del costruttore e tipi di record

L'associazione di modelli richiede che i tipi complessi abbiano un costruttore senza parametri. Sia i formattatori di input basati su System.Text.Json che quelli basati su Newtonsoft.Json supportano la deserializzazione delle classi che non hanno un costruttore senza parametri.

I tipi di record sono un ottimo modo per rappresentare in modo conciso i dati in rete. ASP.NET Core supporta l'associazione di modelli e la convalida dei tipi di record con un singolo costruttore:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

<label>Name: <input asp-for="Name" /></label>
<br />
<label>Age: <input asp-for="Age" /></label>

Quando si convalidano i tipi di record, il runtime cerca i metadati di associazione e convalida in modo specifico sui parametri anziché sulle proprietà.

Il framework consente l'associazione ai tipi di record e la convalida:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Per il corretto funzionamento del precedente, il tipo deve:

  • Essere un tipo di record.
  • Avere esattamente un costruttore pubblico.
  • Contenere parametri con una proprietà con lo stesso nome e tipo. I nomi non devono essere diversi in base alle maiuscole e minuscole.

PoCO senza parametri costruttori

I POCO che non dispongono di costruttori senza parametri non possono essere associati.

Il codice seguente genera un'eccezione che indica che il tipo deve avere un costruttore senza parametri:

public class Person {
    public Person(string Name) { }
}
public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0)
    {
    }
}

Tipi di record con costruttori scritti manualmente

Tipi di record con costruttori creati manualmente che hanno un aspetto simile al funzionamento dei costruttori primari

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipi di record, convalida e metadati di associazione

Per i tipi di record, vengono usati i metadati di convalida e associazione per i parametri. Tutti i metadati sulle proprietà vengono ignorati

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validazione e metadati

La convalida usa i metadati nel parametro, ma usa la proprietà per leggere il valore. Nel caso comune con i costruttori primari, i due sarebbero identici. Tuttavia, ci sono modi per sconfiggerla:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel non aggiorna i parametri in un tipo di record

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

In questo caso, MVC non tenterà di eseguire di nuovo l'associazione Name . Tuttavia, è consentito aggiornare Age.

Comportamento di globalizzazione dei dati di route di associazione di modelli e stringhe di query

I provider di valori di route di ASP.NET Core e i provider di valori delle stringhe di query:

  • Trattare i valori come cultura invariata.
  • Si prevede che gli URL siano invarianti rispetto alle impostazioni culturali.

Al contrario, i valori provenienti dai dati del modulo vengono sottoposti a una conversione sensibile alla cultura. Questa operazione è progettata per rendere gli URL condivisibili tra le diverse aree geografiche.

Per fare in modo che il provider di valori di route ASP.NET Core e il provider di valori stringa di query subiscano una conversione sensibile alle impostazioni di cultura:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipi di dati speciali

Esistono alcuni tipi di dati speciali che l'associazione di modelli può gestire.

IFormFile e IFormFileCollection

Un file caricato incluso nella richiesta HTTP. È anche supportato IEnumerable<IFormFile> per più file.

CancellationToken

Le azioni possono facoltativamente associare un oggetto CancellationToken come parametro. Questo lega RequestAborted, segnalando quando la connessione alla base della richiesta HTTP viene interrotta. Le azioni possono usare questo parametro per annullare operazioni asincrone a esecuzione prolungata eseguite come parte delle azioni del controller.

FormCollection

Usato per recuperare tutti i valori dai dati di modulo inviati.

Formattatori di input

I dati nel corpo della richiesta possono essere in formato JSON, XML o in altri formati. Per analizzare questi dati, l'associazione di modelli usa un formattatore di input configurato in modo da gestire un particolare tipo di contenuto. Per impostazione predefinita, ASP.NET Core include i formattatori di input basati su JSON per la gestione dei dati JSON. È possibile aggiungere altri formattatori per altri tipi di contenuto.

ASP.NET Core consente di selezionare i formattatori di input in base all'attributo Consumes. Se non è presente alcun attributo, viene usata l'intestazione Content-Type.

Per usare i formattatori di input XML predefiniti:

Personalizzare l'associazione di modelli con formattatori di input

Un formattatore di input assume la piena responsabilità di leggere i dati dal corpo della richiesta. Per personalizzare questo processo, configurare le API usate dal formattatore di input. Questa sezione descrive come personalizzare il System.Text.Jsonformattatore di input basato su per comprendere un tipo personalizzato denominato ObjectId.

Si consideri il modello seguente, che contiene una proprietà personalizzata ObjectId :

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Per personalizzare il processo di associazione del modello quando si usa System.Text.Json, creare una classe derivata da JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Per usare un convertitore personalizzato, applicare l'attributo JsonConverterAttribute al tipo . Nell'esempio seguente il ObjectId tipo è configurato con ObjectIdConverter come convertitore personalizzato:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Per altre informazioni, vedere Come scrivere convertitori personalizzati.

Escludere i tipi specificati dall'associazione di modelli

Il comportamento dei sistemi di associazione e convalida del modello è basato su ModelMetadata. È possibile personalizzare ModelMetadata mediante l'aggiunta di un provider di dettagli a MvcOptions.ModelMetadataDetailsProviders. Sono disponibili provider di dettagli predefiniti per la disabilitazione dell'associazione di modelli o la convalida per i tipi specificati.

Per disabilitare l'associazione di modelli per tutti i modelli di un tipo specificato, aggiungere un ExcludeBindingMetadataProvider in Program.cs. Ad esempio, per disabilitare l'associazione di modelli per tutti i modelli di tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Per disabilitare la convalida per le proprietà di un tipo specificato, aggiungere un SuppressChildValidationMetadataProvider in Program.cs. Ad esempio, per disabilitare la convalida per le proprietà di tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Strumenti di associazione di modelli personalizzati

È possibile estendere l'associazione di modelli scrivendo uno strumento di associazione di modelli personalizzato e usando l'attributo [ModelBinder] per selezionarlo per una determinata destinazione. Altre informazioni sull'associazione di modelli personalizzata.

Associazione di modelli manuale

L'associazione di modelli può essere richiamata manualmente usando il metodo TryUpdateModelAsync. Il metodo è definito in entrambe le classi ControllerBase e PageModel. I sovraccarichi del metodo consentono di specificare il prefisso e il provider di valori da utilizzare. Il metodo restituisce false se l'associazione di modelli non riesce. Ecco un esempio:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa provider di valori per ottenere dati dal corpo del modulo, dalla stringa di query e dai dati di route. TryUpdateModelAsync è in genere:

  • Usato con Razor app Pages e MVC che utilizzano controller e viste per prevenire l'eccessivo invio di dati.
  • Non usato con un'API Web, a meno che non venga utilizzato dai dati del modulo, dalle stringhe di query e dai dati di route. Gli endpoint API Web che utilizzano JSON usano formattatori di input per deserializzare il corpo della richiesta in un oggetto .

Per altre informazioni, vedere TryUpdateModelAsync.

Attributo [FromServices]

Il nome di questo attributo segue il modello degli attributi di associazione di modelli che specificano un'origine dati. Non si tratta però dell'associazione di dati da un provider di valori. Ottiene un'istanza di un tipo dal contenitore di iniezione delle dipendenze. Lo scopo è fornire un'alternativa all'iniezione del costruttore per quando è necessario un servizio solo se viene chiamato un metodo specifico.

Se un'istanza del tipo non è registrata nel contenitore di inserimento delle dipendenze, l'app genera un'eccezione quando si tenta di associare il parametro. Per rendere facoltativo il parametro, usare uno degli approcci seguenti:

  • Rendere il parametro nullable.
  • Impostare un valore predefinito per il parametro .

Per i parametri nullable, assicurarsi che il parametro non sia null prima di accedervi.

Risorse aggiuntive

Questo articolo illustra cos'è l'associazione di modelli, come funziona e come personalizzarne il comportamento.

Che cos'è l'associazione di modelli

Controller e pagine Razor lavorano con dati provenienti da richieste HTTP. Ad esempio, i dati di route possono fornire una chiave del record e i campi modulo inviati possono fornire valori per le proprietà del modello. La scrittura di codice per recuperare tutti i valori e convertirli da stringhe in tipi .NET sarebbe noiosa e soggetta a errori. L'associazione di modelli consente di automatizzare questo processo. Il sistema di associazione di modelli:

  • Recupera i dati da diverse origini, ad esempio i dati di route, i campi modulo e le stringhe di query.
  • Fornisce i dati ai controller e alle pagine Razor nei parametri e nelle proprietà pubbliche del metodo.
  • Converte dati stringa in tipi .NET.
  • Aggiorna le proprietà dei tipi complessi.

Esempio

Si supponga di avere il metodo di azione seguente:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

E l'app riceve una richiesta con questo URL:

https://contoso.com/api/pets/2?DogsOnly=true

L'associazione di modelli esegue i passaggi seguenti dopo che il sistema di routing seleziona il metodo di azione:

  • Trova il primo parametro di GetById, un intero denominato id.
  • Esamina le origini disponibili nella richiesta HTTP e trova id = "2" nei dati di route.
  • Converte la stringa "2" nell'intero 2.
  • Trova il parametro successivo di GetById, un valore booleano denominato dogsOnly.
  • Esamina le origini e trova "DogsOnly=true" nella stringa di query. Per la corrispondenza dei nomi non viene applicata la distinzione tra maiuscole e minuscole.
  • Converte la stringa "true" nel valore booleano true.

Il framework chiama quindi il metodo GetById, passando 2 per il parametro id e true per il parametro dogsOnly.

Nell'esempio precedente, le destinazioni di associazione del modello sono parametri di metodo che sono tipi semplici . Le destinazioni possono essere anche le proprietà di un tipo complesso. Dopo l'associazione di ogni proprietà, viene eseguita la convalida dei modelli per la proprietà. Il record dei dati associati al modello e di eventuali errori di associazione o convalida viene archiviato in ControllerBase.ModelState oppure PageModel.ModelState. Per scoprire se questo processo ha esito positivo, l'app controlla il flag ModelState.IsValid.

Target principali

Il binding dei modelli cerca di trovare i valori per i seguenti tipi di target:

  • Parametri del metodo di azione del controller a cui viene indirizzata una richiesta.
  • Parametri del metodo del Razor gestore Pages a cui viene indirizzata una richiesta.
  • Proprietà pubbliche di un controller o una classe PageModel, se specificate dagli attributi.

Attributo [BindProperty]

Può essere applicato a una proprietà pubblica di un controller o una classe PageModel per fare in modo che l'associazione di modelli usi tale proprietà come destinazione:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Attributo [BindProperties]

Può essere applicato a un controller o una classe PageModel per indicare all'associazione del modello di usare tutte le proprietà pubbliche della classe come destinazione:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Associazione di modelli per le richieste HTTP GET

Per impostazione predefinita, le proprietà non vengono associate per le richieste HTTP GET. In genere, per una richiesta GET è sufficiente un parametro ID record. L'ID record viene usato per cercare l'elemento nel database. Pertanto, non è necessario associare una proprietà che contiene un'istanza del modello. Negli scenari in cui si vogliono associare le proprietà ai dati dalle richieste GET, impostare la proprietà SupportsGet su true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

Associazione di modelli tipi semplici e complessi

L'associazione di modelli usa definizioni specifiche per i tipi sui quali opera. Un tipo semplice viene convertito da una singola stringa utilizzando TypeConverter o un TryParse metodo . Un tipo complesso viene convertito da più valori di input. Il framework determina la differenza sull'esistenza di un TypeConverter o TryParse. È consigliabile creare un convertitore di tipi o usare TryParse per una string conversione da a SomeType che non richiede risorse esterne o più input.

Origini

Per impostazione predefinita, l'associazione di modelli ottiene i dati sotto forma di coppie chiave-valore dalle origini seguenti in una richiesta HTTP:

  1. Campi del modulo
  2. Il corpo della richiesta (per i controller con l'attributo [ApiController].)
  3. Dati di itinerario
  4. Parametri della stringa di query
  5. File caricati

Per ogni parametro o proprietà di destinazione, le origini vengono analizzate nell'ordine indicato nell'elenco precedente. Esistono tuttavia alcune eccezioni:

  • I dati di route e i valori delle stringhe di query vengono usati solo per i tipi semplici .
  • I file caricati vengono associati solo ai tipi di destinazione che implementano IFormFile o IEnumerable<IFormFile>.

Se l'origine predefinita non è corretta, usare uno degli attributi seguenti per specificare l'origine:

  • [FromQuery] : ottiene i valori dalla stringa di query.
  • [FromRoute] - Ottiene i valori dai dati di route.
  • [FromForm] - Ottiene i valori dai campi modulo inseriti.
  • [FromBody] - Ottiene i valori dal corpo della richiesta.
  • [FromHeader] - Ottiene i valori dalle intestazioni HTTP.

Questi attributi:

  • Vengono aggiunti alle proprietà del modello singolarmente e non alla classe del modello, come nell'esempio seguente:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Accetta facoltativamente un valore per il nome del modello nel costruttore. Questa opzione è disponibile nel caso in cui il nome della proprietà non corrisponda al valore nella richiesta. Il valore nella richiesta potrebbe essere, ad esempio, un'intestazione con un trattino nel nome, come nell'esempio seguente:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Attributo [FromBody]

Applicare l'attributo [FromBody] a un parametro per popolare le relative proprietà dal corpo di una richiesta HTTP. Il runtime di ASP.NET Core delega la responsabilità di leggere il corpo a un formattatore di input. I formattatori di input sono descritti più avanti in questo articolo.

Quando [FromBody] viene applicato a un parametro di tipo complesso, tutti gli attributi di origine dell'associazione applicati alle relative proprietà vengono ignorati. Ad esempio, l'azione seguente Create specifica che il relativo pet parametro viene popolato dal corpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

La Pet classe specifica che la relativa Breed proprietà viene popolata da un parametro della stringa di query:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

Nell'esempio precedente:

  • L'attributo [FromQuery] viene ignorato.
  • La Breed proprietà non viene popolata da un parametro di stringa di query.

I formattatori di input leggono solo il corpo e non comprendono gli attributi dell'origine di associazione. Se nel corpo viene trovato un valore appropriato, tale valore viene usato per popolare la Breed proprietà.

Non applicare [FromBody] a più di un parametro per ogni metodo di azione. Quando il flusso di richiesta viene letto da un formattatore di input, non è più disponibile per la lettura per l'associazione di altri [FromBody] parametri.

Origini aggiuntive

I dati di origine vengono forniti al sistema di associazione di modelli dai provider di valori. È possibile scrivere e registrare i provider di valori personalizzati che recuperano i dati per l'associazione di modelli da altre origini. Ad esempio, potrebbe essere necessario recuperare dati dai cookie o dallo stato della sessione. Per ottenere dati da una nuova origine:

  • Creare una classe che implementi IValueProvider.
  • Creare una classe che implementi IValueProviderFactory.
  • Registrare la classe factory in Program.cs.

L'esempio include un provider di valori e un esempio di factory che ottiene i valori dai cookie. Registra le fabbriche dei provider di valori personalizzati in Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

Il codice precedente inserisce il provider di valori personalizzato dopo tutti i provider di valori predefiniti. Per renderlo il primo nell'elenco, chiamare Insert(0, new CookieValueProviderFactory()) invece di Add.

Nessuna origine per una proprietà del modello

Per impostazione predefinita, non viene creato un errore di stato del modello se non viene trovato alcun valore per una proprietà del modello. La proprietà viene impostata su Null o su un valore predefinito:

  • I tipi semplici nullable sono impostati su null.
  • I tipi valore non nullable vengono impostati su default(T). Ad esempio, un parametro int id viene impostato su 0.
  • Per i tipi complessi, l'associazione di modelli crea un'istanza usando il costruttore predefinito, senza impostare proprietà.
  • Le matrici vengono impostate su Array.Empty<T>(), ad eccezione delle matrici byte[] che vengono impostate su null.

Se lo stato del modello deve essere invalidato quando non viene trovato nulla nei campi modulo per una proprietà del modello, usare l'attributo [BindRequired] .

Si noti che questo comportamento [BindRequired] si applica all'associazione di modelli dai dati di moduli inviati e non ai dati JSON o XML nel corpo di una richiesta. I dati del corpo della richiesta vengono gestiti dai formattatori di input.

Errori di conversione dei tipi

Se viene trovata un'origine ma non può essere convertita nel tipo di destinazione, lo stato del modello viene contrassegnato come non valido. Il parametro o la proprietà di destinazione viene impostato su Null o su un valore predefinito, come indicato nella sezione precedente.

In un controller API con l'attributo [ApiController], uno stato del modello non valido genera una risposta HTTP 400 automatica.

In una Razor pagina riprodurre la pagina con un messaggio di errore:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Quando la pagina viene riprodotta dal codice precedente, l'input non valido non viene visualizzato nel campo modulo. Questo avviene perché la proprietà del modello è stata impostata su Null o su un valore predefinito. L'input non valido viene visualizzato in un messaggio di errore. Se si desidera riprodurre nuovamente i dati non valido nel campo modulo, è consigliabile impostare la proprietà del modello su una stringa ed eseguire manualmente la conversione dei dati.

La stessa strategia è consigliata se si vuole evitare che gli errori di conversione del tipo causino errori di stato del modello. In tal caso, imposta la proprietà del modello su tipo stringa.

Tipi semplici

Per una spiegazione dei tipi semplici e complessi, vedere Associazione di modelli di tipi semplici e complessi.

I tipi semplici in cui lo strumento di associazione di modelli può convertire le stringhe di origine includono i seguenti:

Associa con IParsable<T>.TryParse

L'API IParsable<TSelf>.TryParse supporta i valori dei parametri di azione del controller di associazione:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

La classe seguente DateRange implementa IParsable<TSelf> per supportare l'associazione di un intervallo di date:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

Il codice precedente:

  • Converte una stringa che rappresenta due date in un DateRange oggetto
  • Lo strumento di associazione di modelli usa il metodo IParsable<TSelf>.TryParse per associare il DateRange.

L'azione controller seguente usa la DateRange classe per associare un intervallo di date:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

La classe seguente Locale implementa IParsable<TSelf> per supportare l'associazione a CultureInfo:

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

L'azione controller seguente usa la Locale classe per associare una CultureInfo stringa:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

L'azione controller seguente usa le DateRange classi e Locale per associare un intervallo di date a CultureInfo:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

L'app di esempio per le API in GitHub mostra l'esempio precedente per un controller API.

Eseguire l'associazione con TryParse

L'API TryParse supporta i valori dei parametri di azione del controller di associazione:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse è l'approccio consigliato per l'associazione di parametri perché, a differenza di TryParse, non dipende dalla Reflection.

La classe seguente DateRangeTP implementa TryParse:

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

L'azione controller seguente usa la DateRangeTP classe per associare un intervallo di date:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Tipi complessi

Un tipo complesso deve avere un costruttore predefinito pubblico e proprietà scrivibili pubbliche per il binding. Al momento dell'associazione dei modelli, la classe viene istanziata utilizzando il costruttore predefinito pubblico.

Per ogni proprietà del tipo complesso, il model binding esamina le origini alla ricerca del pattern di nomeprefix.property_name. Se non viene trovato, cerca semplicemente nome_proprietà senza il prefisso. La decisione di usare il prefisso non viene presa per ogni proprietà. Ad esempio, con una query contenente ?Instructor.Id=100&Name=foo, associata al metodo OnGet(Instructor instructor), l'oggetto risultante di tipo Instructor contiene:

  • Id impostato su 100.
  • Name impostato su null. Il binding dei modelli si aspetta Instructor.Name perché Instructor.Id è stato usato nel parametro di query precedente.

Per l'associazione a un parametro, il prefisso è il nome del parametro. Per l'associazione a una proprietà pubblica PageModel, il prefisso è il nome della proprietà pubblica. Alcuni attributi hanno una proprietà Prefix che consente di eseguire l'override dell'utilizzo predefinito del nome di un parametro o una proprietà.

Ad esempio, si supponga che il tipo complesso sia la classe Instructor seguente:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefisso = nome del parametro

Se il modello da associare è un parametro denominato instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

L'associazione di modelli inizia cercando la chiave instructorToUpdate.ID nelle fonti. Se non viene trovata, cerca ID senza prefisso.

Prefisso = nome della proprietà

Se il modello da associare è una proprietà denominata Instructor del controller o della classe PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

L'associazione dei modelli inizia cercando la chiave Instructor.ID nelle origini. Se non viene trovata, cerca ID senza prefisso.

Prefisso personalizzato

Se il modello da associare è un parametro denominato instructorToUpdate e un attributo Bind specifica Instructor come prefisso:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

L'associazione di modelli cerca prima di tutto la chiave Instructor.ID nelle origini. Se non viene trovata, cerca ID senza prefisso.

Attributi per le destinazioni di tipo complesso

Sono disponibili vari attributi predefiniti per controllare l'associazione di modelli di tipi complessi:

Avviso

Questi attributi influiscono sul modello di associazione quando i dati di modulo inviati sono l'origine dei valori. Non influiscono sui formattatori di input, che elaborano i corpi delle richieste JSON e XML pubblicati. I formattatori di input sono descritti più avanti in questo articolo.

Attributo [Bind]

Può essere applicato a una classe o a un parametro di metodo. Specifica quali proprietà di un modello devono essere incluse nell'associazione di modelli. [Bind] non influisce sui formattatori di input.

Nell'esempio seguente vengono associate solo le proprietà specificate del modello Instructor quando viene chiamato qualsiasi gestore o metodo di azione:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

Nell'esempio seguente vengono associate solo le proprietà specificate del modello Instructor quando viene chiamato il metodo OnPost:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

L'attributo [Bind] può essere usato per evitare l'overposting negli scenari di creazione. Non funziona bene negli scenari di modifica perché le proprietà escluse vengono impostate su Null o su un valore predefinito anziché rimanere inalterate. Per la protezione dall'overposting, è consigliabile usare i modelli di visualizzazione anziché l'attributo [Bind] . Per ulteriori informazioni, consultare la Nota sulla sicurezza riguardante l'overposting.

Attributo [ModelBinder]

ModelBinderAttribute può essere applicato a tipi, proprietà o parametri. Consente di specificare il tipo di binder del modello usato per vincolare l'istanza o il tipo specifico. Ad esempio:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

L'attributo [ModelBinder] può essere usato anche per modificare il nome di una proprietà o di un parametro quando è associato al modello:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Attributo [BindRequired]

Con questo attributo l'associazione di modelli aggiunge un errore di stato del modello se non è possibile eseguire l'associazione per una proprietà del modello. Ecco un esempio:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Vedere anche la discussione relativa all'attributo [Required] in Convalida del modello.

Attributo [BindNever]

Può essere applicato a una proprietà o a un tipo. Impedisce all'associazione di modelli di impostare una proprietà del modello. Se applicato a un tipo, il sistema di associazione di modelli esclude tutte le proprietà definite dal tipo. Ecco un esempio:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Raccolte

Per le destinazioni che sono raccolte di tipi semplici, l'associazione di modelli cerca le corrispondenze per nome_parametro oppure nome_proprietà. Se non viene trovata alcuna corrispondenza, viene cercato uno dei formati supportati senza il prefisso. Ad esempio:

  • Si supponga che il parametro da associare sia una matrice denominata selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • I dati di modulo o di stringhe di query possono essere in uno dei formati seguenti:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evitare l'associazione di un parametro o di una proprietà denominata index o Index se è adiacente a un valore della raccolta. L'associazione di modelli tenta di usare index come indice per la raccolta, che potrebbe comportare un'associazione errata. Si consideri ad esempio l'azione seguente:

    public IActionResult Post(string index, List<Product> products)
    

    Nel codice precedente, il index parametro della stringa di query viene associato al parametro del index metodo e viene usato anche per associare la raccolta di prodotti. La ridenominazione del parametro o l'uso di un attributo di associazione di modelli per configurare l'associazione index evita questo problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • Il formato seguente è supportato solo nei dati di modulo:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Per tutti i formati di esempio precedenti, l'associazione di modelli passa una matrice di due elementi al parametro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Per i formati di dati che usano numeri con indice (... [0]... [1]...) è necessario assicurarsi che la numerazione sia progressiva a partire da zero. Se sono presenti eventuali interruzioni nella numerazione con indice, tutti gli elementi dopo l'interruzione vengono ignorati. Ad esempio, se i pedici sono 0 e 2 anziché 0 e 1, il secondo elemento viene ignorato.

Dizionari

Per le destinazioni Dictionary, l'associazione di modelli cerca le corrispondenze per nome_parametro oppure nome_proprietà. Se non viene trovata alcuna corrispondenza, viene cercato uno dei formati supportati senza il prefisso. Ad esempio:

  • Si supponga che il parametro di destinazione sia un elemento Dictionary<int, string> denominato selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • I dati di modulo o di stringhe di query inviati possono essere simili a uno degli esempi seguenti:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Per tutti i formati di esempio precedenti, l'associazione di modelli passa un dizionario di due elementi al parametro selectedCourses:

    • selectedCourses["1050"]="Chimica"
    • selectedCourses["2000"]="Economia"

Associazione al costruttore e tipi di record

L'associazione di modelli richiede che i tipi complessi abbiano un costruttore senza parametri. I System.Text.Json formattatori di input basati e Newtonsoft.Json supportano la deserializzazione delle classi che non hanno un costruttore senza parametri.

I tipi di record sono un ottimo modo per rappresentare in modo conciso i dati in rete. ASP.NET Core supporta l'associazione di modelli e la convalida dei tipi di record con un singolo costruttore:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

<label>Name: <input asp-for="Name" /></label>
<br />
<label>Age: <input asp-for="Age" /></label>

Quando si convalidano i tipi di record, il runtime cerca i metadati di associazione e convalida in modo specifico sui parametri anziché sulle proprietà.

Il framework consente l'associazione ai tipi di record e la convalida:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Per il corretto funzionamento dell'oggetto precedente, il tipo deve:

  • Essere un tipo di record.
  • Avere esattamente un costruttore pubblico.
  • Contenere parametri con una proprietà con lo stesso nome e tipo. I nomi non devono essere diversi in base alle maiuscole e minuscole.

PoCO senza parametri costruttori

I POCO che non dispongono di costruttori senza parametri non possono essere associati.

Il codice seguente genera un'eccezione che indica che il tipo deve avere un costruttore senza parametri:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Tipi di record con costruttori creati manualmente

Tipi di record con costruttori creati manualmente che hanno un aspetto simile al funzionamento dei costruttori primari

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipi di record, convalida e metadati di associazione

Per i tipi di record, vengono usati i metadati di convalida e associazione per i parametri. Tutti i metadati sulle proprietà vengono ignorati

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Convalida e metadati

La convalida usa i metadati nel parametro, ma usa la proprietà per leggere il valore. Nel caso comune con i costruttori primari, i due sarebbero identici. Tuttavia, ci sono modi per sconfiggerla:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel non aggiorna i parametri in un tipo di record

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

In questo caso, MVC non tenterà di eseguire di nuovo l'associazione Name . Tuttavia, è consentito aggiornare Age

Comportamento di globalizzazione dell'associazione dei modelli con i dati di route e le stringhe di query

Il provider di valore di route di ASP.NET Core e il provider di valore di stringa di query:

  • Trattare i valori come cultura invariante.
  • Si prevede che gli URL siano invarianti delle impostazioni culturali.

Al contrario, i valori provenienti dai dati del modulo vengono sottoposti a una conversione sensibile al contesto culturale. Questa operazione è progettata in modo che gli URL siano condivisibili tra le impostazioni locali.

Per fare in modo che il provider di valori di route di ASP.NET Core e il provider di valori delle stringhe di query siano soggetti a una conversione sensibile alla cultura:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipi di dati speciali

Esistono alcuni tipi di dati speciali che l'associazione di modelli può gestire.

IFormFile e IFormFileCollection

Un file caricato incluso nella richiesta HTTP. È anche supportato IEnumerable<IFormFile> per più file.

CancellationToken

Le azioni possono facoltativamente associare un oggetto CancellationToken come parametro. L'operazione associa RequestAborted che segnala quando la connessione alla base della richiesta HTTP viene interrotta. Le azioni possono usare questo parametro per annullare operazioni asincrone a esecuzione prolungata eseguite come parte delle azioni del controller.

FormCollection

Usato per recuperare tutti i valori dai dati di modulo inviati.

Formattatori di input

I dati nel corpo della richiesta possono essere in formato JSON, XML o in altri formati. Per analizzare questi dati, l'associazione di modelli usa un formattatore di input configurato in modo da gestire un particolare tipo di contenuto. Per impostazione predefinita, ASP.NET Core include i formattatori di input basati su JSON per la gestione dei dati JSON. È possibile aggiungere altri formattatori per altri tipi di contenuto.

ASP.NET Core consente di selezionare i formattatori di input in base all'attributo Consumes. Se non è presente alcun attributo, viene usata l'intestazione Content-Type.

Per usare i formattatori di input XML predefiniti:

Personalizzare l'associazione di modelli con formattatori di input

Un formattatore di input assume la piena responsabilità di leggere i dati dal corpo della richiesta. Per personalizzare questo processo, configurare le API usate dal formattatore di input. Questa sezione descrive come personalizzare il formattatore di input basato su System.Text.Json per comprendere un tipo personalizzato denominato ObjectId.

Si consideri il modello seguente, che contiene una proprietà personalizzata ObjectId :

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Per personalizzare il processo di associazione del modello quando si usa System.Text.Json, creare una classe derivata da JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Per usare un convertitore personalizzato, applicare l'attributo JsonConverterAttribute al tipo . Nell'esempio seguente il ObjectId tipo è configurato con ObjectIdConverter come convertitore personalizzato:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Per altre informazioni, vedere Come scrivere convertitori personalizzati.

Escludere i tipi specificati dall'associazione di modelli

Il comportamento dei sistemi di associazione e convalida del modello è basato su ModelMetadata. È possibile personalizzare ModelMetadata mediante l'aggiunta di un provider di dettagli a MvcOptions.ModelMetadataDetailsProviders. Sono disponibili provider di dettagli predefiniti per la disabilitazione dell'associazione di modelli o la convalida per i tipi specificati.

Per disabilitare l'associazione di modelli per tutti i modelli di un tipo specificato, aggiungere un ExcludeBindingMetadataProvider in Program.cs. Ad esempio, per disabilitare l'associazione di modelli per tutti i modelli di tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Per disabilitare la convalida per le proprietà di un tipo specificato, aggiungere un SuppressChildValidationMetadataProvider in Program.cs. Ad esempio, per disabilitare la convalida per le proprietà di tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Strumenti di associazione di modelli personalizzati

È possibile estendere l'associazione di modelli scrivendo uno strumento di associazione di modelli personalizzato e usando l'attributo [ModelBinder] per selezionarlo per una determinata destinazione. Altre informazioni sull'associazione di modelli personalizzata.

Associazione manuale di modelli

L'associazione di modelli può essere richiamata manualmente usando il metodo TryUpdateModelAsync. Il metodo è definito in entrambe le classi ControllerBase e PageModel. I sovraccarichi del metodo consentono di specificare il prefisso e il fornitore di valori da usare. Il metodo restituisce false se l'associazione di modelli non riesce. Ecco un esempio:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa provider di valori per ottenere dati dal corpo del modulo, dalla stringa di query e dai dati di route. TryUpdateModelAsync è in genere:

  • Usato con Razor le app Pages e MVC usando controller e visualizzazioni per evitare l'over-post.
  • Non usato con un'API Web, a meno che non venga utilizzato dai dati del modulo, dalle stringhe di query e dai dati di route. Gli endpoint API Web che utilizzano JSON usano formattatori di input per deserializzare il corpo della richiesta in un oggetto .

Per altre informazioni, vedere TryUpdateModelAsync.

Attributo [FromServices]

Il nome di questo attributo segue il modello degli attributi di associazione di modelli che specificano un'origine dati. Non si tratta però dell'associazione di dati da un provider di valori. Ottiene un'istanza di un tipo dal contenitore di iniezione delle dipendenze. Lo scopo è fornire un'alternativa all'iniezione tramite costruttore quando si ha bisogno di un servizio solo se viene chiamato un metodo specifico.

Se un'istanza del tipo non è registrata nel contenitore di inserimento delle dipendenze, l'app genera un'eccezione quando si tenta di associare il parametro. Per rendere facoltativo il parametro, usare uno degli approcci seguenti:

  • Rendere il parametro nullable.
  • Impostare un valore predefinito per il parametro .

Per i parametri nullable, assicurarsi che il parametro non null sia prima di accedervi.

Risorse aggiuntive

Questo articolo illustra cos'è l'associazione di modelli, come funziona e come personalizzarne il comportamento.

Che cos'è l'associazione di modelli

Le pagine e i controller funzionano con i dati che provengono dalle richieste HTTP. Ad esempio, i dati di route possono fornire una chiave del record e i campi modulo inviati possono fornire valori per le proprietà del modello. La scrittura di codice per recuperare tutti i valori e convertirli da stringhe in tipi .NET sarebbe noiosa e soggetta a errori. L'associazione di modelli consente di automatizzare questo processo. Il sistema di associazione di modelli:

  • Recupera i dati da diverse origini, ad esempio i dati di route, i campi modulo e le stringhe di query.
  • Fornisce i dati ai controller e Razor alle pagine nei parametri del metodo e nelle proprietà pubbliche.
  • Converte dati stringa in tipi .NET.
  • Aggiorna le proprietà dei tipi complessi.

Esempio

Si supponga di avere il metodo di azione seguente:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

E l'app riceve una richiesta con questo URL:

https://contoso.com/api/pets/2?DogsOnly=true

L'associazione di modelli esegue i passaggi seguenti dopo che il sistema di routing seleziona il metodo di azione:

  • Trova il primo parametro di GetById, un intero denominato id.
  • Esamina le origini disponibili nella richiesta HTTP e trova id = "2" nei dati di route.
  • Converte la stringa "2" nell'intero 2.
  • Trova il parametro successivo di GetById, un valore booleano denominato dogsOnly.
  • Esamina le origini e trova "DogsOnly=true" nella stringa di query. Per la corrispondenza dei nomi non viene applicata la distinzione tra maiuscole e minuscole.
  • Converte la stringa "true" nel valore booleano true.

Il framework chiama quindi il metodo GetById, passando 2 per il parametro id e true per il parametro dogsOnly.

Nell'esempio precedente le destinazioni dell'associazione di modelli sono parametri di metodo che sono tipi semplici. Le destinazioni possono essere anche le proprietà di un tipo complesso. Dopo l'associazione di ogni proprietà, viene eseguita la convalida dei modelli per la proprietà. Il record dei dati associati al modello e di eventuali errori di associazione o convalida viene archiviato in ControllerBase.ModelState oppure PageModel.ModelState. Per scoprire se questo processo ha esito positivo, l'app controlla il flag ModelState.IsValid.

Obiettivi

Il binding dei modelli cerca di trovare i valori per i tipi di target seguenti:

  • Parametri del metodo di azione del controller a cui viene indirizzata una richiesta.
  • Parametri del metodo del Razor gestore Pages a cui viene indirizzata una richiesta.
  • Proprietà pubbliche di un controller o una classe PageModel, se specificate dagli attributi.

Attributo [BindProperty]

Può essere applicato a una proprietà pubblica di un controller o una classe PageModel per fare in modo che l'associazione di modelli usi tale proprietà come destinazione:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Attributo [BindProperties]

Può essere applicato a un controller o una classe PageModel per indicare all'associazione del modello di usare tutte le proprietà pubbliche della classe come destinazione:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Associazione di modelli per le richieste HTTP GET

Per impostazione predefinita, le proprietà non vengono associate per le richieste HTTP GET. In genere, per una richiesta GET è sufficiente un parametro ID record. L'ID record viene usato per cercare l'elemento nel database. Pertanto, non è necessario associare una proprietà che contiene un'istanza del modello. Negli scenari in cui si vogliono associare le proprietà ai dati dalle richieste GET, impostare la proprietà SupportsGet su true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

Origini

Per impostazione predefinita, l'associazione di modelli ottiene i dati sotto forma di coppie chiave-valore dalle origini seguenti in una richiesta HTTP:

  1. Campi del modulo
  2. Il corpo della richiesta (per i controller con l'attributo [ApiController].)
  3. Dati del percorso
  4. Parametri della stringa di query
  5. File caricati

Per ogni parametro o proprietà di destinazione, le origini vengono analizzate nell'ordine indicato nell'elenco precedente. Esistono tuttavia alcune eccezioni:

  • I dati di route e i valori delle stringhe di query vengono usati solo per i tipi semplici.
  • I file caricati vengono associati solo ai tipi di destinazione che implementano IFormFile o IEnumerable<IFormFile>.

Se l'origine predefinita non è corretta, usare uno degli attributi seguenti per specificare l'origine:

  • [FromQuery] : ottiene i valori dalla stringa di query.
  • [FromRoute] - Ottiene i valori dai dati di route.
  • [FromForm] - Ottiene i valori dai campi modulo inseriti.
  • [FromBody] - Ottiene i valori dal corpo della richiesta.
  • [FromHeader] - Ottiene i valori dalle intestazioni HTTP.

Questi attributi:

  • Vengono aggiunti alle proprietà del modello singolarmente e non alla classe del modello, come nell'esempio seguente:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Accettare facoltativamente un valore per il nome del modello nel costruttore. Questa opzione è disponibile nel caso in cui il nome della proprietà non corrisponda al valore nella richiesta. Il valore nella richiesta, ad esempio, potrebbe essere un'intestazione con un trattino nel nome, come nell'esempio seguente:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Attributo [FromBody]

Applicare l'attributo [FromBody] a un parametro per popolare le relative proprietà dal corpo di una richiesta HTTP. Il runtime di ASP.NET Core delega la responsabilità di leggere il corpo a un formattatore di input. I formattatori di input sono descritti più avanti in questo articolo.

Quando [FromBody] viene applicato a un parametro di tipo complesso, tutti gli attributi di origine dell'associazione applicati alle relative proprietà vengono ignorati. Ad esempio, l'azione seguente Create specifica che il relativo pet parametro viene popolato dal corpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

La Pet classe specifica che la relativa Breed proprietà viene popolata da un parametro della stringa di query:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

Nell'esempio precedente:

  • L'attributo [FromQuery] viene ignorato.
  • La Breed proprietà non viene popolata da un parametro di stringa di query.

I formattatori di input leggono solo il corpo e non comprendono gli attributi dell'origine di associazione. Se nel corpo viene trovato un valore appropriato, tale valore viene utilizzato per popolare la proprietà Breed.

Non applicare [FromBody] a più di un parametro per ogni metodo di azione. Quando il flusso di richiesta viene letto da un formattatore di input, non è più disponibile per la lettura per l'associazione di altri [FromBody] parametri.

Origini aggiuntive

I dati di origine vengono forniti al sistema di associazione di modelli dai provider di valori. È possibile scrivere e registrare i provider di valori personalizzati che recuperano i dati per l'associazione di modelli da altre origini. Ad esempio, potrebbe essere necessario recuperare dati dai cookie o dallo stato della sessione. Per ottenere dati da una nuova origine:

  • Creare una classe che implementi IValueProvider.
  • Creare una classe che implementi IValueProviderFactory.
  • Registrate la classe factory in Program.cs.

L'esempio include un provider di valori e un esempio di factory che ottiene i valori dai cookie. Registrare le factory per provider di valori personalizzati in Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

Il codice precedente inserisce il provider di valori personalizzato dopo tutti i provider di valori predefiniti. Per renderlo il primo nell'elenco, chiamare Insert(0, new CookieValueProviderFactory()) invece di Add.

Nessuna origine per una proprietà del modello

Per impostazione predefinita, non viene creato un errore di stato del modello se non viene trovato alcun valore per una proprietà del modello. La proprietà viene impostata su Null o su un valore predefinito:

  • I tipi semplici nullable sono impostati su null.
  • I tipi valore non-null sono impostati su default(T). Ad esempio, un parametro int id viene impostato su 0.
  • Per i tipi complessi, l'associazione di modelli crea un'istanza usando il costruttore predefinito, senza impostare proprietà.
  • Le matrici vengono impostate su Array.Empty<T>(), ad eccezione delle matrici byte[] che vengono impostate su null.

Se lo stato del modello deve essere invalidato quando non viene trovato nulla nei campi modulo per una proprietà del modello, usare l'attributo [BindRequired] .

Si noti che questo comportamento [BindRequired] si applica all'associazione di modelli dai dati di moduli inviati e non ai dati JSON o XML nel corpo di una richiesta. I dati del corpo della richiesta vengono gestiti dai formattatori di input.

Errori di conversione dei tipi

Se viene trovata un'origine ma non può essere convertita nel tipo di destinazione, lo stato del modello viene contrassegnato come non valido. Il parametro o la proprietà di destinazione viene impostato su Null o su un valore predefinito, come indicato nella sezione precedente.

In un controller API con l'attributo [ApiController], uno stato del modello non valido genera una risposta HTTP 400 automatica.

Nella Razor pagina, visualizzare nuovamente la pagina con un messaggio di errore.

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Quando la pagina viene riprodotta dal codice precedente, l'input non valido non viene visualizzato nel campo modulo. Questo avviene perché la proprietà del modello è stata impostata su Null o su un valore predefinito. L'input non valido viene visualizzato in un messaggio di errore. Se si desidera riprodurre nuovamente i dati non valido nel campo modulo, è consigliabile impostare la proprietà del modello su una stringa ed eseguire manualmente la conversione dei dati.

La stessa strategia è consigliata se si vuole evitare che gli errori di conversione del tipo causino errori di stato del modello. In tal caso, impostare la proprietà del modello come stringa.

Tipi semplici

I tipi semplici in cui lo strumento di associazione di modelli può convertire le stringhe di origine includono i seguenti:

Tipi complessi

Un tipo complesso deve avere un costruttore predefinito pubblico e da associare proprietà scrivibili pubbliche. Quando si verifica l'associazione di modelli, vengono create istanze della classe usando il costruttore predefinito pubblico.

Per ogni proprietà del tipo complesso, il binding del modello cerca nelle fonti per trovare il pattern del nomeprefix.property_name. Se non viene trovato, cerca semplicemente nome_proprietà senza il prefisso. La decisione di usare il prefisso non viene presa per ogni proprietà. Ad esempio, con una query contenente ?Instructor.Id=100&Name=foo, associata al metodo OnGet(Instructor instructor), l'oggetto risultante di tipo Instructor contiene:

  • Id impostato su 100.
  • Name impostato su null. Il model binding si aspetta Instructor.Name poiché Instructor.Id è stato usato nel parametro della query precedente.

Per l'associazione a un parametro, il prefisso è il nome del parametro. Per l'associazione a una proprietà pubblica PageModel, il prefisso è il nome della proprietà pubblica. Alcuni attributi hanno una proprietà Prefix che consente di eseguire l'override dell'utilizzo predefinito del nome di un parametro o una proprietà.

Ad esempio, si supponga che il tipo complesso sia la classe Instructor seguente:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefisso = nome del parametro

Se il modello da associare è un parametro denominato instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

L'associazione di modelli inizia cercando la chiave instructorToUpdate.ID nelle fonti. Se non viene trovata, cerca ID senza prefisso.

Prefisso = nome della proprietà

Se il modello da associare è una proprietà denominata Instructor del controller o della classe PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

Il processo di binding dei modelli inizia guardando attraverso le sorgenti per trovare la chiave Instructor.ID. Se non viene trovata, cerca ID senza prefisso.

Prefisso personalizzato

Se il modello da associare è un parametro denominato instructorToUpdate e un attributo Bind specifica Instructor come prefisso:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

L'associazione di modelli inizia cercando la chiave Instructor.ID nelle origini. Se non viene trovata, cerca ID senza prefisso.

Attributi per obiettivi di tipo complesso

Sono disponibili vari attributi predefiniti per controllare l'associazione di modelli di tipi complessi:

Avviso

Questi attributi influiscono sul modello di associazione quando i dati di modulo inviati sono l'origine dei valori. Non influiscono sui formattatori di input, che elaborano i corpi delle richieste JSON e XML pubblicati. I formattatori di input sono descritti più avanti in questo articolo.

Attributo [Bind]

Può essere applicato a una classe o a un parametro di metodo. Specifica quali proprietà di un modello devono essere incluse nell'associazione di modelli. [Bind] non influisce sui formattatori di input.

Nell'esempio seguente vengono associate solo le proprietà specificate del modello Instructor quando viene chiamato qualsiasi gestore o metodo di azione:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

Nell'esempio seguente vengono associate solo le proprietà specificate del modello Instructor quando viene chiamato il metodo OnPost:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

L'attributo [Bind] può essere usato per evitare l'overposting negli scenari di creazione. Non funziona bene negli scenari di modifica perché le proprietà escluse vengono impostate su Null o su un valore predefinito anziché rimanere inalterate. Per la protezione dall'overposting, è consigliabile usare i modelli di visualizzazione anziché l'attributo [Bind] . Per altre informazioni, vedere Nota sulla sicurezza relativa all'overposting.

Attributo [ModelBinder]

ModelBinderAttribute può essere applicato a tipi, proprietà o parametri. Consente di specificare il tipo di binder di modello usato per associare un'istanza o un tipo specifico. Ad esempio:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

L'attributo [ModelBinder] può essere usato anche per modificare il nome di una proprietà o di un parametro quando è associato al modello:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Attributo [BindRequired]

Con questo attributo il binding del modello aggiunge un errore nello stato del modello se non è possibile eseguire l'associazione per una proprietà del modello. Ecco un esempio:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Vedere anche la discussione relativa all'attributo [Required] in Convalida del modello.

Attributo [BindNever]

Può essere applicato a una proprietà o a un tipo. Impedisce al binding del modello di settare una proprietà del modello. Se applicato a un tipo, il sistema di associazione di modelli esclude tutte le proprietà definite dal tipo. Ecco un esempio:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Raccolte

Per le destinazioni che sono raccolte di tipi semplici, l’associazione dei modelli cerca le corrispondenze per nome_parametro oppure nome_proprietà. Se non viene trovata alcuna corrispondenza, viene cercato uno dei formati supportati senza il prefisso. Ad esempio:

  • Si supponga che il parametro da associare sia una matrice denominata selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • I dati di modulo o di stringhe di query possono essere in uno dei formati seguenti:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evitare l'associazione di un parametro o di una proprietà denominata index o Index se è adiacente a un valore della raccolta. L'associazione di modelli tenta di usare index come indice per la raccolta, che potrebbe comportare un'associazione errata. Si consideri ad esempio l'azione seguente:

    public IActionResult Post(string index, List<Product> products)
    

    Nel codice precedente, il index parametro della stringa di query viene associato al parametro del index metodo e viene usato anche per associare la raccolta di prodotti. La ridenominazione del parametro o l'uso di un attributo di associazione di modelli per configurare l'associazione index evita questo problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • Il formato seguente è supportato solo nei dati di modulo:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Per tutti i formati di esempio precedenti, l'associazione di modelli passa una matrice di due elementi al parametro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Per i formati di dati che usano numeri con indice (... [0]... [1]...) è necessario assicurarsi che la numerazione sia progressiva a partire da zero. Se sono presenti eventuali interruzioni nella numerazione con indice, tutti gli elementi dopo l'interruzione vengono ignorati. Ad esempio, se i pedici sono 0 e 2 anziché 0 e 1, il secondo elemento viene ignorato.

Dizionari

Per le destinazioni Dictionary, l'associazione di modelli cerca le corrispondenze per nome_parametro oppure nome_proprietà. Se non viene trovata alcuna corrispondenza, viene cercato uno dei formati supportati senza il prefisso. Ad esempio:

  • Si supponga che il parametro di destinazione sia un elemento Dictionary<int, string> denominato selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • I dati di modulo o di stringhe di query inviati possono essere simili a uno degli esempi seguenti:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Per tutti i formati di esempio precedenti, l'associazione di modelli passa un dizionario di due elementi al parametro selectedCourses:

    • selectedCourses["1050"]="Chimica"
    • selectedCourses["2000"]="Economia"

Associazione del costruttore e tipi di record

L'associazione di modelli richiede che i tipi complessi abbiano un costruttore senza parametri. I formattatori di input basati su System.Text.Json e Newtonsoft.Json supportano la deserializzazione delle classi che non hanno un costruttore senza parametri.

I tipi di record sono un ottimo modo per rappresentare in modo conciso i dati in rete. ASP.NET Core supporta l'associazione di modelli e la convalida dei tipi di record con un singolo costruttore:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

<label>Name: <input asp-for="Name" /></label>
<br />
<label>Age: <input asp-for="Age" /></label>

Quando si convalidano i tipi di record, il runtime cerca i metadati di associazione e convalida in modo specifico sui parametri anziché sulle proprietà.

Il framework consente l'associazione ai tipi di record e la convalida:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Per il funzionamento di quanto precede, il tipo deve:

  • Essere un tipo di record.
  • Avere esattamente un costruttore pubblico.
  • Contenere parametri con una proprietà con lo stesso nome e tipo. I nomi non devono essere diversi in base alle maiuscole e minuscole.

PoCO senza parametri costruttori

I POCO che non dispongono di costruttori senza parametri non possono essere associati.

Il codice seguente genera un'eccezione che indica che il tipo deve avere un costruttore senza parametri:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Tipi di record con costruttori creati manualmente

Tipi di record con costruttori creati manualmente che hanno un aspetto simile al funzionamento dei costruttori primari

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipi di record, convalida e metadati di associazione

Per i tipi di record, vengono usati i metadati di convalida e associazione per i parametri. Tutti i metadati sulle proprietà vengono ignorati

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Convalida e metadati

La convalida usa i metadati nel parametro, ma usa la proprietà per leggere il valore. Nel caso comune con i costruttori primari, i due sarebbero identici. Tuttavia, ci sono modi per sconfiggerla:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel non aggiorna i parametri in un tipo di record

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

In questo caso, MVC non tenterà di eseguire di nuovo l'associazione Name . Tuttavia, Age può essere aggiornato

Comportamento di globalizzazione dei dati di route dell'associazione dei modelli e delle stringhe di query

Il provider di parametri di route di ASP.NET Core e il provider di parametri della stringa di query:

  • Trattare i valori come cultura invariante.
  • Ci si aspetta che gli URL siano invarianti rispetto alla cultura.

Al contrario, i valori provenienti dai dati del modulo vengono sottoposti a una conversione sensibile alla cultura. Questa operazione è progettata in modo che gli URL siano condivisibili tra le località.

Per fare in modo che il provider di valori di route core ASP.NET e il provider di valori stringa di query subisca una conversione sensibile alle impostazioni cultura:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipi di dati speciali

Esistono alcuni tipi di dati speciali che l'associazione di modelli può gestire.

IFormFile e IFormFileCollection

Un file caricato incluso nella richiesta HTTP. È anche supportato IEnumerable<IFormFile> per più file.

Token di annullamento

Le azioni possono facoltativamente associare un oggetto CancellationToken come parametro. Questo associa RequestAborted che segnala quando la connessione sottostante alla richiesta HTTP viene interrotta. Le azioni possono usare questo parametro per annullare operazioni asincrone a esecuzione prolungata eseguite come parte delle azioni del controller.

FormCollection

Usato per recuperare tutti i valori dai dati di modulo inviati.

Formattatori di input

I dati nel corpo della richiesta possono essere in formato JSON, XML o in altri formati. Per analizzare questi dati, l'associazione di modelli usa un formattatore di input configurato in modo da gestire un particolare tipo di contenuto. Per impostazione predefinita, ASP.NET Core include i formattatori di input basati su JSON per la gestione dei dati JSON. È possibile aggiungere altri formattatori per altri tipi di contenuto.

ASP.NET Core consente di selezionare i formattatori di input in base all'attributo Consumes. Se non è presente alcun attributo, viene usata l'intestazione Content-Type.

Per usare i formattatori di input XML predefiniti:

Personalizzare l'associazione di modelli con formattatori di input

Un formattatore di input assume la piena responsabilità di leggere i dati dal corpo della richiesta. Per personalizzare questo processo, configurare le API usate dal formattatore di input. Questa sezione descrive come personalizzare il formattatore di input basato su System.Text.Json per comprendere un tipo personalizzato denominato ObjectId.

Si consideri il modello seguente, che contiene una proprietà personalizzata ObjectId :

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Per personalizzare il processo di associazione del modello quando si usa System.Text.Json, creare una classe derivata da JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Per usare un convertitore personalizzato, applicare l'attributo JsonConverterAttribute al tipo . Nell'esempio seguente il ObjectId tipo è configurato con ObjectIdConverter come convertitore personalizzato:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Per altre informazioni, vedere Come scrivere convertitori personalizzati.

Escludere i tipi specificati dall'associazione di modelli

Il comportamento dei sistemi di associazione e convalida del modello è basato su ModelMetadata. È possibile personalizzare ModelMetadata mediante l'aggiunta di un provider di dettagli a MvcOptions.ModelMetadataDetailsProviders. Sono disponibili provider di dettagli integrati per disabilitare l'associazione di modelli o la convalida per tipi specificati.

Per disabilitare l'associazione di modelli per tutti i modelli di un tipo specificato, aggiungere un ExcludeBindingMetadataProvider in Program.cs. Ad esempio, per disabilitare l'associazione di modelli per tutti i modelli di tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Per disabilitare la convalida per le proprietà di un tipo specificato, aggiungere un SuppressChildValidationMetadataProvider in Program.cs. Ad esempio, per disabilitare la convalida per le proprietà di tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Strumenti di associazione di modelli personalizzati

È possibile estendere l'associazione di modelli scrivendo uno strumento di associazione di modelli personalizzato e usando l'attributo [ModelBinder] per selezionarlo per una determinata destinazione. Altre informazioni sull'associazione di modelli personalizzata.

Associazione manuale di modelli

L'associazione di modelli può essere richiamata manualmente usando il metodo TryUpdateModelAsync. Il metodo è definito in entrambe le classi ControllerBase e PageModel. I sovraccarichi del metodo consentono di specificare il prefisso e il fornitore di valori da usare. Il metodo restituisce false se l'associazione di modelli non riesce. Ecco un esempio:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa provider di valori per ottenere dati dal corpo del modulo, dalla stringa di query e dai dati di route. TryUpdateModelAsync è in genere:

  • Usato con Razor le app Pages e MVC usando controller e visualizzazioni per evitare l'over-post.
  • Non utilizzato con un'API Web, a meno che non venga consumato da dati di modulo, stringhe di query, e dati di percorso. Gli endpoint API Web che utilizzano JSON usano formattatori di input per deserializzare il corpo della richiesta in un oggetto .

Per altre informazioni, vedere TryUpdateModelAsync.

Attributo [FromServices]

Il nome di questo attributo segue il modello degli attributi di associazione di modelli che specificano un'origine dati. Non si tratta però dell'associazione di dati da un provider di valori. Ottiene un'istanza di un tipo dal contenitore di iniezione delle dipendenze. Lo scopo è fornire un'alternativa all'iniezione del costruttore nel caso in cui sia necessario un servizio solo se viene chiamato un metodo specifico.

Se un'istanza del tipo non è registrata nel contenitore di inserimento delle dipendenze, l'app genera un'eccezione quando si tenta di associare il parametro. Per rendere facoltativo il parametro, usare uno degli approcci seguenti:

  • Rendere il parametro nullable.
  • Impostare un valore predefinito per il parametro .

Per i parametri nullable, assicurarsi che il parametro non sia null prima di accedervi.

Risorse aggiuntive

Questo articolo illustra cos'è l'associazione di modelli, come funziona e come personalizzarne il comportamento.

Visualizzare o scaricare il codice di esempio (procedura per il download).

Che cos'è l'associazione di modelli

I controller e Razor le pagine funzionano con i dati provenienti da richieste HTTP. Ad esempio, i dati del percorso possono fornire una chiave del record e i campi modulo inviati possono fornire valori per le proprietà del modello. La scrittura di codice per recuperare tutti i valori e convertirli da stringhe in tipi .NET sarebbe noiosa e soggetta a errori. L'associazione di modelli consente di automatizzare questo processo. Il sistema di associazione di modelli:

  • Recupera i dati da diverse origini, ad esempio i dati di route, i campi modulo e le stringhe di query.
  • Fornisce i dati ai controller e alle pagine Razor nei parametri e nelle proprietà pubbliche del metodo.
  • Converte dati stringa in tipi .NET.
  • Aggiorna le proprietà dei tipi complessi.

Esempio

Si supponga di avere il metodo di azione seguente:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

E l'app riceve una richiesta con questo URL:

http://contoso.com/api/pets/2?DogsOnly=true

L'associazione di modelli esegue i passaggi seguenti dopo che il sistema di routing seleziona il metodo di azione:

  • Trova il primo parametro di GetById, un intero denominato id.
  • Esamina le origini disponibili nella richiesta HTTP e trova id = "2" nei dati di route.
  • Converte la stringa "2" nell'intero 2.
  • Trova il parametro successivo di GetById, un valore booleano denominato dogsOnly.
  • Esamina le origini e trova "DogsOnly=true" nella stringa di query. Per la corrispondenza dei nomi non viene applicata la distinzione tra maiuscole e minuscole.
  • Converte la stringa "true" nel valore booleano true.

Il framework chiama quindi il metodo GetById, passando 2 per il parametro id e true per il parametro dogsOnly.

Nell'esempio precedente le destinazioni dell'associazione di modelli sono parametri di metodo che sono tipi semplici. Le destinazioni possono essere anche le proprietà di un tipo complesso. Dopo l'associazione di ogni proprietà, viene eseguita la convalida dei modelli per la proprietà. Il record dei dati associati al modello e di eventuali errori di associazione o convalida viene archiviato in ControllerBase.ModelState oppure PageModel.ModelState. Per scoprire se questo processo ha esito positivo, l'app controlla il flag ModelState.IsValid.

Target obiettivi

Il binding dei modelli cerca di trovare i valori per i tipi di target seguenti:

  • Parametri del metodo di azione del controller a cui viene indirizzata una richiesta.
  • Parametri del metodo del Razor gestore Pages a cui viene indirizzata una richiesta.
  • Proprietà pubbliche di un controller o una classe PageModel, se specificate dagli attributi.

Attributo [BindProperty]

Può essere applicato a una proprietà pubblica di un controller o una classe PageModel per fare in modo che l'associazione di modelli usi tale proprietà come destinazione:

public class EditModel : InstructorsPageModel
{
    [BindProperty]
    public Instructor Instructor { get; set; }

Attributo [BindProperties]

Disponibile in ASP.NET Core 2.1 e versioni successive. Può essere applicato a un controller o una classe PageModel per indicare all'associazione del modello di usare tutte le proprietà pubbliche della classe come destinazione:

[BindProperties(SupportsGet = true)]
public class CreateModel : InstructorsPageModel
{
    public Instructor Instructor { get; set; }

Associazione di modelli per le richieste HTTP GET

Per impostazione predefinita, le proprietà non vengono associate per le richieste HTTP GET. In genere, per una richiesta GET è sufficiente un parametro ID record. L'ID record viene usato per cercare l'elemento nel database. Pertanto, non è necessario associare una proprietà che contiene un'istanza del modello. Negli scenari in cui si vogliono associare le proprietà ai dati dalle richieste GET, impostare la proprietà SupportsGet su true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string ApplicationInsightsCookie { get; set; }

Origini

Per impostazione predefinita, l'associazione di modelli ottiene i dati sotto forma di coppie chiave-valore dalle origini seguenti in una richiesta HTTP:

  1. Campi del modulo
  2. Il corpo della richiesta (per i controller con l'attributo [ApiController].)
  3. Dati percorso
  4. Parametri della stringa di query
  5. File caricati

Per ogni parametro o proprietà di destinazione, le origini vengono analizzate nell'ordine indicato nell'elenco precedente. Esistono tuttavia alcune eccezioni:

  • I dati di route e i valori delle stringhe di query vengono usati solo per i tipi semplici.
  • I file caricati vengono associati solo ai tipi di destinazione che implementano IFormFile o IEnumerable<IFormFile>.

Se l'origine predefinita non è corretta, usare uno degli attributi seguenti per specificare l'origine:

  • [FromQuery] : ottiene i valori dalla stringa di query.
  • [FromRoute] - Ottiene i valori dai dati di route.
  • [FromForm] - Ottiene i valori dai campi modulo inseriti.
  • [FromBody] - Ottiene i valori dal corpo della richiesta.
  • [FromHeader] - Ottiene i valori dalle intestazioni HTTP.

Questi attributi:

  • Vengono aggiunti singolarmente alle proprietà del modello (non alla classe del modello), come nell'esempio seguente:

    public class Instructor
    {
        public int ID { get; set; }
    
        [FromQuery(Name = "Note")]
        public string NoteFromQueryString { get; set; }
    
  • Accettano facoltativamente un valore di nome di modello nel costruttore. Questa opzione è disponibile nel caso in cui il nome della proprietà non corrisponda al valore nella richiesta. Il valore nella richiesta potrebbe essere, ad esempio, un'intestazione con un trattino nel suo nome, come nell'esempio seguente:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

[FromBody] Attributo

Applicare l'attributo [FromBody] a un parametro per popolare le relative proprietà dal corpo di una richiesta HTTP. Il runtime di ASP.NET Core delega la responsabilità di leggere il corpo a un formattatore di input. I formattatori di input sono descritti più avanti in questo articolo.

Quando [FromBody] viene applicato a un parametro di tipo complesso, tutti gli attributi di origine dell'associazione applicati alle relative proprietà vengono ignorati. Ad esempio, l'azione seguente Create specifica che il relativo pet parametro viene popolato dal corpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

La Pet classe specifica che la relativa Breed proprietà viene popolata da un parametro della stringa di query:

public class Pet
{
    public string Name { get; set; }

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; }
}

Nell'esempio precedente:

  • L'attributo [FromQuery] viene ignorato.
  • La Breed proprietà non viene popolata da un parametro di stringa di query.

I formattatori di input leggono solo il corpo e non comprendono gli attributi dell'origine di associazione. Se nel corpo viene trovato un valore appropriato, tale valore viene usato per popolare la Breed proprietà.

Non applicare [FromBody] a più di un parametro per ogni metodo di azione. Quando il flusso di richiesta viene letto da un formattatore di input, non è più disponibile per la lettura per l'associazione di altri [FromBody] parametri.

Origini aggiuntive

I dati di origine vengono forniti al sistema di associazione di modelli dai provider di valori. È possibile scrivere e registrare i provider di valori personalizzati che recuperano i dati per l'associazione di modelli da altre origini. Ad esempio, potrebbe essere necessario recuperare dati dai cookie o dallo stato della sessione. Per ottenere dati da una nuova origine:

  • Creare una classe che implementi IValueProvider.
  • Creare una classe che implementi IValueProviderFactory.
  • Registrare la classe factory in Startup.ConfigureServices.

L'app di esempio include un esempio di provider di valori e factory che ottiene i valori dai cookie. Ecco il codice di registrazione in Startup.ConfigureServices:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

Il codice illustrato posiziona il provider di valori personalizzati dopo tutti i provider di valori predefiniti. Per renderlo il primo nell'elenco, chiamare Insert(0, new CookieValueProviderFactory()) invece di Add.

Nessuna origine per una proprietà del modello

Per impostazione predefinita, non viene creato un errore di stato del modello se non viene trovato alcun valore per una proprietà del modello. La proprietà viene impostata su Null o su un valore predefinito:

  • I tipi semplici nullable vengono impostati su null.
  • I tipi valore non nullable vengono impostati su default(T). Ad esempio, un parametro int id viene impostato su 0.
  • Per i tipi complessi, l'associazione di modelli crea un'istanza usando il costruttore predefinito, senza impostare proprietà.
  • Le matrici vengono impostate su Array.Empty<T>(), ad eccezione delle matrici byte[] che vengono impostate su null.

Se lo stato del modello deve essere invalidato quando non viene trovato nulla nei campi modulo per una proprietà del modello, usare l'attributo [BindRequired] .

Si noti che questo comportamento [BindRequired] si applica all'associazione di modelli dai dati di moduli inviati e non ai dati JSON o XML nel corpo di una richiesta. I dati del corpo della richiesta vengono gestiti dai formattatori di input.

Errori di conversione dei tipi

Se viene trovata un'origine ma non può essere convertita nel tipo di destinazione, lo stato del modello viene contrassegnato come non valido. Il parametro o la proprietà di destinazione viene impostato su Null o su un valore predefinito, come indicato nella sezione precedente.

In un controller API con l'attributo [ApiController], uno stato del modello non valido genera una risposta HTTP 400 automatica.

In una Razor pagina, visualizzare di nuovo la pagina con un messaggio di errore:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _instructorsInMemoryStore.Add(Instructor);
    return RedirectToPage("./Index");
}

La convalida lato client rileva la maggior parte dei dati non valido che altrimenti verrebbero inviati a un Razor modulo Pages. Questa convalida rende difficile attivare il codice sopra evidenziato. L'app di esempio include un pulsante Submit with Invalid Date (Invia con data non valida) che inserisce dati non validi nel campo Hire Date (Data assunzione) e invia il modulo. Questo pulsante illustra il funzionamento del codice per visualizzare di nuovo la pagina quando si verificano errori di conversione dei dati.

Quando la pagina viene nuovamente visualizzata dal codice precedente, l'input non valido non viene visualizzato nel campo modulo. Questo avviene perché la proprietà del modello è stata impostata su Null o su un valore predefinito. L'input non valido viene visualizzato in un messaggio di errore. Ma se si vogliono visualizzare di nuovo i dati non validi nel campo modulo, è consigliabile impostare la proprietà del modello come stringa ed eseguire manualmente la conversione dei dati.

La stessa strategia è consigliata se si vuole evitare che gli errori di conversione del tipo causino errori di stato del modello. In tal caso, impostare la proprietà del modello a stringa.

Tipi semplici

I tipi semplici in cui lo strumento di associazione di modelli può convertire le stringhe di origine includono i seguenti:

Tipi complessi

Un tipo complesso deve avere un costruttore predefinito pubblico e proprietà scrivibili pubbliche da associare. Quando si verifica l'associazione di modelli, vengono create istanze della classe usando il costruttore predefinito pubblico.

Per ogni proprietà del tipo complesso, l'associazione di modelli cerca il pattern del nome prefisso.nome_proprietà nelle origini. Se non viene trovato, cerca semplicemente nome_proprietà senza il prefisso.

Per l'associazione a un parametro, il prefisso è il nome del parametro. Per l'associazione a una proprietà pubblica PageModel, il prefisso è il nome della proprietà pubblica. Alcuni attributi hanno una proprietà Prefix che consente di eseguire l'override dell'utilizzo predefinito del nome di un parametro o una proprietà.

Ad esempio, si supponga che il tipo complesso sia la classe Instructor seguente:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefisso = nome del parametro

Se il modello da associare è un parametro denominato instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

L'associazione di modelli inizia cercando la chiave instructorToUpdate.ID nelle origini. Se non viene trovata, cerca ID senza prefisso.

Prefisso = nome della proprietà

Se il modello da associare è una proprietà denominata Instructor del controller o della classe PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

L'associazione dei modelli inizia cercando la chiave Instructor.ID tra le fonti. Se non viene trovata, cerca ID senza prefisso.

Prefisso personalizzato

Se il modello da associare è un parametro denominato instructorToUpdate e un attributo Bind specifica Instructor come prefisso:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

L'associazione di modelli inizia cercando la chiave Instructor.ID all'interno delle fonti. Se non viene trovata, cerca ID senza prefisso.

Attributi per obiettivi di tipo complesso

Sono disponibili vari attributi predefiniti per controllare l'associazione di modelli di tipi complessi:

  • [Bind]
  • [BindRequired]
  • [BindNever]

Avviso

Questi attributi influiscono sul modello di associazione quando i dati di modulo inviati sono l'origine dei valori. Non influiscono sui formattatori di input, che elaborano i corpi delle richieste JSON e XML pubblicati. I formattatori di input sono descritti più avanti in questo articolo.

Attributo [Bind]

Può essere applicato a una classe o a un parametro di metodo. Specifica quali proprietà di un modello devono essere incluse nell'associazione di modelli. [Bind] non influisce sui formattatori di input.

Nell'esempio seguente vengono associate solo le proprietà specificate del modello Instructor quando viene chiamato qualsiasi gestore o metodo di azione:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

Nell'esempio seguente vengono associate solo le proprietà specificate del modello Instructor quando viene chiamato il metodo OnPost:

[HttpPost]
public IActionResult OnPost([Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

L'attributo [Bind] può essere usato per evitare l'overposting negli scenari di creazione. Non funziona bene negli scenari di modifica perché le proprietà escluse vengono impostate su Null o su un valore predefinito anziché rimanere inalterate. Per la protezione dall'overposting, è consigliabile usare i modelli di visualizzazione anziché l'attributo [Bind] . Per altre informazioni, vedere Nota sulla sicurezza relativa all'overposting.

Attributo [ModelBinder]

ModelBinderAttribute può essere applicato a tipi, proprietà o parametri. Consente di specificare il tipo di gestore di associazione di modelli usato per associare l'istanza o il tipo specifico. Ad esempio:

[HttpPost]
public IActionResult OnPost([ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

L'attributo [ModelBinder] può essere usato anche per modificare il nome di una proprietà o di un parametro quando è associato al modello:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    public string Name { get; set; }
}

Attributo [BindRequired]

Può essere applicato solo alle proprietà del modello e non ai parametri di metodo. Con questo attributo l'associazione di modelli aggiunge un errore di stato del modello se non è possibile eseguire l'associazione per una proprietà del modello. Ecco un esempio:

public class InstructorWithCollection
{
    public int ID { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Hire Date")]
    [BindRequired]
    public DateTime HireDate { get; set; }

Vedere anche la discussione relativa all'attributo [Required] in Convalida del modello.

Attributo "[BindNever]"

Può essere applicato solo alle proprietà del modello e non ai parametri di metodo. Impedisce all'associazione di modelli di impostare una proprietà del modello. Ecco un esempio:

public class InstructorWithDictionary
{
    [BindNever]
    public int ID { get; set; }

Raccolte

Per le destinazioni che sono raccolte di tipi semplici, l'associazione di modelli cerca le corrispondenze per nome_parametro oppure nome_proprietà. Se non viene trovata alcuna corrispondenza, viene cercato uno dei formati supportati senza il prefisso. Ad esempio:

  • Si supponga che il parametro da associare sia una matrice denominata selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • I dati di modulo o di stringhe di query possono essere in uno dei formati seguenti:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evitare l'associazione di un parametro o di una proprietà denominata index o Index se è adiacente a un valore della raccolta. L'associazione di modelli tenta di usare index come indice per la raccolta, che potrebbe comportare un'associazione errata. Si consideri ad esempio l'azione seguente:

    public IActionResult Post(string index, List<Product> products)
    

    Nel codice precedente, il index parametro della stringa di query viene associato al parametro del index metodo e viene usato anche per associare la raccolta di prodotti. La ridenominazione del parametro o l'uso di un attributo di associazione di modelli per configurare l'associazione index evita questo problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • Il formato seguente è supportato solo nei dati di modulo:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Per tutti i formati di esempio precedenti, l'associazione di modelli passa una matrice di due elementi al parametro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Per i formati di dati che usano numeri con indice (... [0]... [1]...) è necessario assicurarsi che la numerazione sia progressiva a partire da zero. Se sono presenti eventuali interruzioni nella numerazione con indice, tutti gli elementi dopo l'interruzione vengono ignorati. Ad esempio, se i pedici sono 0 e 2 anziché 0 e 1, il secondo elemento viene ignorato.

Dizionari

Per le destinazioni Dictionary, l'associazione di modelli cerca le corrispondenze per nome_parametro oppure nome_proprietà. Se non viene trovata alcuna corrispondenza, viene cercato uno dei formati supportati senza il prefisso. Ad esempio:

  • Si supponga che il parametro di destinazione sia un elemento Dictionary<int, string> denominato selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • I dati di modulo o di stringhe di query inviati possono essere simili a uno degli esempi seguenti:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Per tutti i formati di esempio precedenti, l'associazione di modelli passa un dizionario di due elementi al parametro selectedCourses:

    • selectedCourses["1050"]="Chimica"
    • selectedCourses["2000"]="Economia"

Associazione del costruttore e tipi di record

L'associazione di modelli richiede che i tipi complessi abbiano un costruttore senza parametri. Sia i formattatori di input basati su System.Text.Json sia su Newtonsoft.Json supportano la deserializzazione delle classi che non hanno un costruttore senza parametri.

C# 9 introduce i tipi di record, un ottimo modo per rappresentare in modo conciso i dati in rete. ASP.NET Core aggiunge il supporto per l'associazione di modelli e la convalida dei tipi di record con un singolo costruttore:

public record Person([Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
   public IActionResult Index() => View();

   [HttpPost]
   public IActionResult Index(Person person)
   {
       ...
   }
}

Person/Index.cshtml:

@model Person

<label>Name: <input asp-for="Name" /></label>
...
<label>Age: <input asp-for="Age" /></label>

Quando si convalidano i tipi di record, il runtime cerca i metadati di associazione e convalida in modo specifico sui parametri anziché sulle proprietà.

Il framework consente l'associazione ai tipi di record e la convalida:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Affinché quanto sopra possa funzionare, il tipo deve:

  • Essere un tipo di record.
  • Avere esattamente un costruttore pubblico.
  • Contenere parametri con una proprietà con lo stesso nome e tipo. I nomi non devono essere diversi in base alle maiuscole e minuscole.

PoCO senza parametri costruttori

I POCO che non dispongono di costruttori senza parametri non possono essere associati.

Il codice seguente genera un'eccezione che indica che il tipo deve avere un costruttore senza parametri:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
   public Person(string Name) : this (Name, 0);
}

Tipi di record con costruttori scritti manualmente

Tipi di record con costruttori creati manualmente che hanno un aspetto simile al funzionamento dei costruttori primari

public record Person
{
   public Person([Required] string Name, [Range(0, 100)] int Age) => (this.Name, this.Age) = (Name, Age);

   public string Name { get; set; }
   public int Age { get; set; }
}

Tipi di record, convalida e metadati di associazione

Per i tipi di record, vengono usati i metadati di convalida e associazione per i parametri. Tutti i metadati sulle proprietà vengono ignorati

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Convalida e metadati

La convalida usa i metadati nel parametro, ma usa la proprietà per leggere il valore. Nel caso comune con i costruttori primari, i due sarebbero identici. Tuttavia, ci sono modi per sconfiggerla:

public record Person([Required] string Name)
{
   private readonly string _name;
   public Name { get; init => _name = value ?? string.Empty; } // Now this property is never null. However this object could have been constructed as `new Person(null);`
}

TryUpdateModel non aggiorna i parametri in un tipo di record

public record Person(string Name)
{
   public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

In questo caso, MVC non tenterà di eseguire di nuovo l'associazione Name . Tuttavia, Age è consentito aggiornare

Comportamento di globalizzazione dei dati di route di associazione di modelli e stringhe di query

Provider di valori di route core ASP.NET e provider di valori stringa di query:

  • Considerare i valori come cultura invariante.
  • Ci si aspetta che gli URL siano invarianti rispetto alle impostazioni culturali.

Al contrario, i valori provenienti dai dati del modulo vengono sottoposti a una conversione sensibile alle impostazioni cultura. Questa operazione è progettata in modo che gli URL siano condivisibili tra le impostazioni locali.

Per fare in modo che il provider di valori di route core ASP.NET e il provider di valori stringa di query subisca una conversione sensibile alle impostazioni cultura:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        var index = options.ValueProviderFactories.IndexOf(
            options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().Single());
        options.ValueProviderFactories[index] = new CulturedQueryStringValueProviderFactory();
    });
}
public class CulturedQueryStringValueProviderFactory : 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 QueryStringValueProvider(
                BindingSource.Query,
                query,
                CultureInfo.CurrentCulture);

            context.ValueProviders.Add(valueProvider);
        }

        return Task.CompletedTask;
    }
}

Tipi di dati speciali

Esistono alcuni tipi di dati speciali che l'associazione di modelli può gestire.

IFormFile e IFormFileCollection

Un file caricato incluso nella richiesta HTTP. È anche supportato IEnumerable<IFormFile> per più file.

Token di Cancellazione

Le azioni possono facoltativamente associare un oggetto CancellationToken come parametro. Associa RequestAborted che segnala quando la connessione sottostante alla richiesta HTTP viene interrotta. Le azioni possono usare questo parametro per annullare operazioni asincrone a esecuzione prolungata eseguite come parte delle azioni del controller.

FormCollection

Usato per recuperare tutti i valori dai dati di modulo inviati.

Formattatori di input

I dati nel corpo della richiesta possono essere in formato JSON, XML o in altri formati. Per analizzare questi dati, l'associazione di modelli usa un formattatore di input configurato in modo da gestire un particolare tipo di contenuto. Per impostazione predefinita, ASP.NET Core include i formattatori di input basati su JSON per la gestione dei dati JSON. È possibile aggiungere altri formattatori per altri tipi di contenuto.

ASP.NET Core consente di selezionare i formattatori di input in base all'attributo Consumes. Se non è presente alcun attributo, viene usata l'intestazione Content-Type.

Per usare i formattatori di input XML predefiniti:

  • Installare il pacchetto NuGet Microsoft.AspNetCore.Mvc.Formatters.Xml.

  • In Startup.ConfigureServices, chiama AddXmlSerializerFormatters o AddXmlDataContractSerializerFormatters.

    services.AddRazorPages()
        .AddMvcOptions(options =>
    {
        options.ValueProviderFactories.Add(new CookieValueProviderFactory());
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(System.Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
    })
    .AddXmlSerializerFormatters();
    
  • Applicare l'attributo Consumes alle classi controller o ai metodi di azione che devono aspettarsi XML nel corpo della richiesta.

    [HttpPost]
    [Consumes("application/xml")]
    public ActionResult<Pet> Create(Pet pet)
    

    Per altre informazioni, vedere Introduzione alla serializzazione XML.

Personalizzare l'associazione di modelli con formattatori di input

Un formattatore di input assume la piena responsabilità di leggere i dati dal corpo della richiesta. Per personalizzare questo processo, configurare le API usate dal formattatore di input. Questa sezione descrive come personalizzare il formattatore di input basato su System.Text.Json per gestire un tipo personalizzato denominato ObjectId.

Si consideri il modello seguente, che contiene una proprietà personalizzata denominata ObjectIdId:

public class ModelWithObjectId
{
    public ObjectId Id { get; set; }
}

Per personalizzare il processo di associazione del modello quando si usa System.Text.Json, creare una classe derivata da JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return new ObjectId(JsonSerializer.Deserialize<int>(ref reader, options));
    }

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
    {
        writer.WriteNumberValue(value.Id);
    }
}

Per usare un convertitore personalizzato, applicare l'attributo JsonConverterAttribute al tipo . Nell'esempio seguente il ObjectId tipo è configurato con ObjectIdConverter come convertitore personalizzato:

[JsonConverter(typeof(ObjectIdConverter))]
public struct ObjectId
{
    public ObjectId(int id) =>
        Id = id;

    public int Id { get; }
}

Per altre informazioni, vedere Come scrivere convertitori personalizzati.

Escludere i tipi specificati dall'associazione di modelli

Il comportamento dei sistemi di associazione e convalida del modello è basato su ModelMetadata. È possibile personalizzare ModelMetadata mediante l'aggiunta di un provider di dettagli a MvcOptions.ModelMetadataDetailsProviders. Provider di dettagli integrati sono disponibili per disabilitare l'associazione dei modelli o la convalida per i tipi specificati.

Per disabilitare l'associazione di modelli per tutti i modelli di un tipo specificato, aggiungere un ExcludeBindingMetadataProvider in Startup.ConfigureServices. Ad esempio, per disabilitare l'associazione di modelli per tutti i modelli di tipo System.Version:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

Per disabilitare la convalida per le proprietà di un tipo specificato, aggiungere un SuppressChildValidationMetadataProvider in Startup.ConfigureServices. Ad esempio, per disabilitare la convalida per le proprietà di tipo System.Guid:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

Strumenti di associazione di modelli personalizzati

È possibile estendere l'associazione di modelli scrivendo uno strumento di associazione di modelli personalizzato e usando l'attributo [ModelBinder] per selezionarlo per una determinata destinazione. Altre informazioni sull'associazione di modelli personalizzata.

Associazione manuale di modelli

L'associazione di modelli può essere richiamata manualmente usando il metodo TryUpdateModelAsync. Il metodo è definito in entrambe le classi ControllerBase e PageModel. I sovraccarichi del metodo consentono di specificare il prefisso e il provider di valori da usare. Il metodo restituisce false se l'associazione di modelli non riesce. Ecco un esempio:

if (await TryUpdateModelAsync<InstructorWithCollection>(
    newInstructor,
    "Instructor",
    i => i.FirstMidName, i => i.LastName, i => i.HireDate))
{
    _instructorsInMemoryStore.Add(newInstructor);
    return RedirectToPage("./Index");
}
PopulateAssignedCourseData(newInstructor);
return Page();

TryUpdateModelAsync usa provider di valori per ottenere dati dal corpo del modulo, dalla stringa di query e dai dati di route. TryUpdateModelAsync è in genere:

  • Usato con Razor le app Pages e MVC usando controller e visualizzazioni per evitare l'over-post.
  • Non usato con un'API Web, a meno che non venga utilizzato dai dati del modulo, dalle stringhe di query e dai dati di route. Gli endpoint API Web che utilizzano JSON usano formattatori di input per deserializzare il corpo della richiesta in un oggetto .

Per altre informazioni, vedere TryUpdateModelAsync.

[FromServices] attributo

Il nome di questo attributo segue il modello degli attributi di associazione di modelli che specificano un'origine dati. Non si tratta però del collegamento di dati da un fornitore di valori. Ottiene un'istanza di un tipo dal contenitore di inserimento delle dipendenze. Lo scopo è fornire un'alternativa all'iniezione del costruttore quando hai bisogno di un servizio solo se viene chiamato un metodo specifico.

Risorse aggiuntive