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 8 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 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 8 di questo articolo.

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

Che cos'è l'associazione di modelli

I controller e Razor le pagine funzionano con i 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 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 di modelli cerca di trovare i valori per i tipi di destinazioni 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; }

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 oggetto 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; }
    
        // ...
    }
    
  • 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, ad esempio, potrebbe essere un'intestazione con un segno meno 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 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 factory del 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, 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:

Eseguire l'associazione 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 IParsable<TSelf>.TryParse metodo per associare .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 TryParsedi , 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 pubblico 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 esamina le origini per il modello di 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 di Instructor.Name modelli si aspetta perché Instructor.Id è stata usata 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 cerca prima di tutto 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 di modelli cerca prima di tutto 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 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<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 gli indici 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"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Associazione del 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, 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 impostazioni cultura invarianti.
  • Si prevede che gli URL siano invarianti delle impostazioni cultura.

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 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. Viene associato RequestAborted che segnala quando la connessione sottostante la 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. Gli overload 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(
    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 inserimento delle dipendenze. Lo scopo è fornire un'alternativa all'inserimento 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 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

I controller e Razor le pagine funzionano con i 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 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 di modelli cerca di trovare i valori per i tipi di destinazioni 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; }

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 oggetto 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; }
    
        // ...
    }
    
  • 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, ad esempio, potrebbe essere un'intestazione con un segno meno 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 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 factory del 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, 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:

Eseguire l'associazione 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 IParsable<TSelf>.TryParse metodo per associare .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 TryParsedi , 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 pubblico 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 esamina le origini per il modello di 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 di Instructor.Name modelli si aspetta perché Instructor.Id è stata usata 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 cerca prima di tutto 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 di modelli cerca prima di tutto 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 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; }

    // ...
}

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 gli indici 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"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Associazione del 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, 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 impostazioni cultura invarianti.
  • Si prevede che gli URL siano invarianti delle impostazioni cultura.

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 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. Viene associato RequestAborted che segnala quando la connessione sottostante la 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. Gli overload 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(
    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 inserimento delle dipendenze. Lo scopo è fornire un'alternativa all'inserimento 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 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

I controller e Razor le pagine funzionano con i 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 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.

Target

L'associazione di modelli cerca di trovare i valori per i tipi di destinazioni 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 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; }
    
        // ...
    }
    
  • 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, ad esempio, potrebbe essere un'intestazione con un segno meno 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 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 factory del 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 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 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, 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 pubblico 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 esamina le origini per il modello di 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 di Instructor.Name modelli si aspetta perché Instructor.Id è stata usata 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 cerca prima di tutto 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 di modelli cerca prima di tutto 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 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; }

    // ...
}

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 gli indici 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"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Associazione del 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, 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 impostazioni cultura invarianti.
  • Si prevede che gli URL siano invarianti delle impostazioni cultura.

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 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. Viene associato RequestAborted che segnala quando la connessione sottostante la 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. Gli overload 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(
    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 inserimento delle dipendenze. Lo scopo è fornire un'alternativa all'inserimento 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 null sia 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 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:

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

L'associazione di modelli cerca di trovare i valori per i tipi di destinazioni 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 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 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, ad esempio, potrebbe essere un'intestazione con un segno meno 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 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; }

    [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 riprodurre 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 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 pubblico 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 modello di 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 cerca prima di tutto 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 di modelli cerca prima di tutto 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:

  • [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 gli indici 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"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Associazione del 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.

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);

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;
   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 impostazioni cultura invarianti.
  • Si prevede che gli URL siano invarianti delle impostazioni cultura.

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.

CancellationToken

Le azioni possono facoltativamente associare un oggetto CancellationToken come parametro. Viene associato RequestAborted che segnala quando la connessione sottostante la 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 chiamare 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 System.Text.Jsonformattatore di input basato su per comprendere un tipo personalizzato denominato ObjectId.

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

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. 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 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 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. Gli overload 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.

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 inserimento delle dipendenze. Lo scopo è fornire un'alternativa all'inserimento del costruttore per quando è necessario un servizio solo se viene chiamato un metodo specifico.

Risorse aggiuntive