Associazione di modelli personalizzata in ASP.NET Core

Di Kirk Larkin

Mediante l'associazione di modelli le azioni dei controller possono operare direttamente con i tipi di modello (passati come argomenti dei metodi), anziché con le richieste HTTP. Il mapping tra i dati delle richieste in ingresso e i modelli applicativi è gestito dagli strumenti di associazione di modelli. Gli sviluppatori possono estendere la funzionalità di associazione di modelli predefinita implementando strumenti di associazione di modelli personalizzati (anche se in genere non è necessario creare un provider personalizzato).

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

Limiti degli strumenti di associazione di modelli predefiniti

Gli strumenti di associazione di modelli predefiniti supportano la maggior parte dei tipi di dati .NET Core comuni e soddisfano le esigenze della maggior parte degli sviluppatori. Tali strumenti associano direttamente l'input di testo della richiesta ai tipi di modello. Può essere necessario trasformare l'input prima dell'associazione. Un esempio è il caso in cui è presente una chiave che può essere usata per cercare i dati del modello. È possibile usare uno strumento di associazione di modelli personalizzato per il recupero di dati in base alla chiave.

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.

Per un elenco di tipi che il gestore di associazione di modelli può convertire da stringhe, vedere Tipi semplici.

Prima di creare uno strumento di associazione di modelli personalizzato, è utile esaminare le modalità di implementazione degli strumenti di associazione di modelli esistenti. Si consideri l'oggetto ByteArrayModelBinder che può essere usato per convertire stringhe con codifica Base64 in matrici di byte. Le matrici di byte vengono spesso archiviate come file o campi BLOB del database.

Uso di ByteArrayModelBinder

Le stringhe con codifica Base64 possono essere usate per rappresentare i dati binari. Ad esempio, un'immagine può essere codificata come stringa. L'esempio include un'immagine come stringa con codifica Base64 in Base64String.txt.

ASP.NET Core MVC può accettare una stringa con codifica base64 e usare un oggetto ByteArrayModelBinder per convertirla in una matrice di byte. Gli ByteArrayModelBinderProvider argomenti mappano byte[] a ByteArrayModelBinder:

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
        return new ByteArrayModelBinder(loggerFactory);
    }

    return null;
}

Quando si crea un gestore di associazione di modelli personalizzato, è possibile implementare il proprio IModelBinderProvider tipo o usare .ModelBinderAttribute

L'esempio indica come usare ByteArrayModelBinder per convertire una stringa con codifica base64 in un byte[] e salvare il risultato in un file:

[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}

Per visualizzare i commenti del codice tradotti in lingue diverse dall'inglese, segnalarlo in questo problema di discussione su GitHub.

È possibile pubblicare una stringa con codifica Base64 al metodo api precedente usando uno strumento come curl.

Se lo strumento di associazione riesce ad associare i dati della richiesta a proprietà o argomenti denominati in modo appropriato, l'associazione di modelli ha esito positivo. L'esempio seguente indica come usare ByteArrayModelBinder con un modello di visualizzazione:

[HttpPost("Profile")]
public void SaveProfile([FromForm] ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}

Esempio di strumento di associazione di modelli personalizzato

In questa sezione viene implementato uno strumento di associazione di modelli personalizzato che:

  • Converte i dati della richiesta in ingresso in argomenti chiave fortemente tipizzati.
  • Usa Entity Framework Core per recuperare l'entità associata.
  • Passa l'entità associata come argomento al metodo di azione.

L'esempio seguente usa l'attributo ModelBinder nel modello Author:

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}

Nel codice precedente l'attributo ModelBinder specifica il tipo di IModelBinder da usare per associare i parametri di azione Author.

La classe AuthorEntityBinder seguente associa un parametro Author recuperando l'entità da un'origine dati tramite Entity Framework Core e un authorId:

public class AuthorEntityBinder : IModelBinder
{
    private readonly AuthorContext _context;

    public AuthorEntityBinder(AuthorContext context)
    {
        _context = context;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        if (!int.TryParse(value, out var id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                modelName, "Author Id must be an integer.");

            return Task.CompletedTask;
        }

        // Model will be null if not found, including for
        // out of range id values (0, -3, etc.)
        var model = _context.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

Nota

La classe AuthorEntityBinder precedente è destinata a illustrare uno strumento di associazione di modelli personalizzato. La classe non ha lo scopo di illustrare procedure consigliate per uno scenario di ricerca. Per la ricerca, associare l'elemento authorId ed eseguire query nel database in un metodo di azione. Questo approccio separa gli errori di associazione di modelli dai casi NotFound.

L'esempio di codice seguente indica come usare AuthorEntityBinder in un metodo di azione:

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

L'attributo ModelBinder può essere usato per applicare gli elementi AuthorEntityBinder ai parametri che non usano convenzioni predefinite:

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

In questo esempio, dato che il nome dell'argomento non è il valore authorId predefinito, viene specificato nel parametro usando l'attributo ModelBinder. Sia il controller che il metodo di azione sono semplificati rispetto alla ricerca dell'entità nel metodo di azione. La logica per recuperare l'autore usando Entity Framework Core viene spostata nello strumento di associazione di modelli. Ciò può rappresentare una semplificazione notevole in presenza di vari metodi associati al modello Author.

È possibile applicare l'attributo ModelBinder a singole proprietà del modello (ad esempio a ViewModel) o a parametri del metodo di azione per specificare un determinato strumento di associazione di modelli o nome di modello solo per quel determinato tipo o azione.

Implementazione di un elemento ModelBinderProvider

Anziché applicare un attributo, è possibile implementare IModelBinderProvider. Gli strumenti di associazione del framework incorporati vengono implementati in questo modo. Quando si specifica il tipo sul quale opera lo strumento di associazione, si indica il tipo di argomento prodotto dallo strumento, non l'input accettato dallo strumento. Il provider strumento di associazione seguente funziona con l'elemento AuthorEntityBinder. Quando viene aggiunto alla raccolta di provider di MVC, non è necessario usare l'attributo ModelBinder sui parametri tipizzati Author o Author.

using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}

Nota: il codice precedente restituisce un elemento BinderTypeModelBinder. BinderTypeModelBinder funziona come factory per gli strumenti di associazione di modelli e implementa la funzionalità DI (Dependency Injection, Inserimento di dipendenze). Richiede l'inserimento AuthorEntityBinder delle dipendenze per accedere EF Corea . Usare BinderTypeModelBinder se lo strumento di associazione di modelli richiede servizi da DI (Dependency Injection, Inserimento di dipendenze).

Per usare un provider dello strumento di associazione di modelli personalizzato, aggiungerlo in ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AuthorContext>(options => options.UseInMemoryDatabase("Authors"));

    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

Durante la valutazione degli strumenti di associazione di modelli, la raccolta di provider viene esaminata in ordine. Viene usato il primo provider che restituisce un binder che corrisponde al modello di input. L'aggiunta del provider alla fine della raccolta può quindi comportare la chiamata di un gestore di associazione di modelli predefinito prima che il gestore di associazione personalizzato abbia la possibilità. In questo esempio, il provider personalizzato viene aggiunto all'inizio della raccolta per assicurarsi che venga sempre usato per Author gli argomenti dell'azione.

Associazione di modelli polimorfici

L'associazione a modelli diversi di tipi derivati è nota come associazione di modelli polimorfici. L'associazione di modelli personalizzati polimorfici è necessaria quando il valore della richiesta deve essere associato al tipo di modello derivato specifico. Associazione di modelli polimorfici:

  • Non è tipico per un'API REST progettata per interagire con tutti i linguaggi.
  • Rende difficile ragionare sui modelli associati.

Tuttavia, se un'app richiede l'associazione di modelli polimorfici, un'implementazione potrebbe essere simile al codice seguente:

public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

Suggerimenti e procedure ottimali

Gli strumenti di associazione di modelli personalizzati:

  • Non devono provare a impostare codici di stato o restituire risultati (ad esempio 404 Non trovato). Se si verifica un errore nell'associazione di modelli, l'errore deve essere gestito da un filtro azioni o da logica inclusa nel metodo di azione.
  • Sono particolarmente utili per eliminare codice ripetitivo e problemi di montaggio incrociato dai metodi di azione.
  • In genere non devono essere usati per convertire una stringa in un tipo personalizzato. Un elemento TypeConverter rappresenta solitamente una scelta migliore.

Di Steve Smith

Mediante l'associazione di modelli le azioni dei controller possono operare direttamente con i tipi di modello (passati come argomenti dei metodi), anziché con le richieste HTTP. Il mapping tra i dati delle richieste in ingresso e i modelli applicativi è gestito dagli strumenti di associazione di modelli. Gli sviluppatori possono estendere la funzionalità di associazione di modelli predefinita implementando strumenti di associazione di modelli personalizzati (anche se in genere non è necessario creare un provider personalizzato).

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

Limiti degli strumenti di associazione di modelli predefiniti

Gli strumenti di associazione di modelli predefiniti supportano la maggior parte dei tipi di dati .NET Core comuni e soddisfano le esigenze della maggior parte degli sviluppatori. Tali strumenti associano direttamente l'input di testo della richiesta ai tipi di modello. Può essere necessario trasformare l'input prima dell'associazione. Un esempio è il caso in cui è presente una chiave che può essere usata per cercare i dati del modello. È possibile usare uno strumento di associazione di modelli personalizzato per il recupero di dati in base alla chiave.

Analisi dell'associazione di modelli

L'associazione di modelli usa definizioni specifiche per i tipi sui quali opera. Un tipo semplice viene convertito da una stringa singola dell'input. Un tipo complesso viene convertito da più valori di input. Il framework determina la differenza in base all'esistenza di un elemento TypeConverter. È consigliabile creare un convertitore di tipi se si dispone di un semplice string>SomeType mapping che non richiede risorse esterne.

Prima di creare uno strumento di associazione di modelli personalizzato, è utile esaminare le modalità di implementazione degli strumenti di associazione di modelli esistenti. Si consideri l'oggetto ByteArrayModelBinder che può essere usato per convertire stringhe con codifica Base64 in matrici di byte. Le matrici di byte vengono spesso archiviate come file o campi BLOB del database.

Uso di ByteArrayModelBinder

Le stringhe con codifica Base64 possono essere usate per rappresentare i dati binari. Ad esempio, un'immagine può essere codificata come stringa. L'esempio include un'immagine come stringa con codifica Base64 in Base64String.txt.

ASP.NET Core MVC può accettare una stringa con codifica base64 e usare un oggetto ByteArrayModelBinder per convertirla in una matrice di byte. Gli ByteArrayModelBinderProvider argomenti mappano byte[] a ByteArrayModelBinder:

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        return new ByteArrayModelBinder();
    }

    return null;
}

Quando si crea un gestore di associazione di modelli personalizzato, è possibile implementare il proprio IModelBinderProvider tipo o usare .ModelBinderAttribute

L'esempio indica come usare ByteArrayModelBinder per convertire una stringa con codifica base64 in un byte[] e salvare il risultato in un file:

[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}

È possibile pubblicare una stringa con codifica Base64 al metodo api precedente usando uno strumento come curl.

Se lo strumento di associazione riesce ad associare i dati della richiesta a proprietà o argomenti denominati in modo appropriato, l'associazione di modelli ha esito positivo. L'esempio seguente indica come usare ByteArrayModelBinder con un modello di visualizzazione:

[HttpPost("Profile")]
public void SaveProfile([FromForm] ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}

Esempio di strumento di associazione di modelli personalizzato

In questa sezione viene implementato uno strumento di associazione di modelli personalizzato che:

  • Converte i dati della richiesta in ingresso in argomenti chiave fortemente tipizzati.
  • Usa Entity Framework Core per recuperare l'entità associata.
  • Passa l'entità associata come argomento al metodo di azione.

L'esempio seguente usa l'attributo ModelBinder nel modello Author:

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}

Nel codice precedente l'attributo ModelBinder specifica il tipo di IModelBinder da usare per associare i parametri di azione Author.

La classe AuthorEntityBinder seguente associa un parametro Author recuperando l'entità da un'origine dati tramite Entity Framework Core e un authorId:

public class AuthorEntityBinder : IModelBinder
{
    private readonly AppDbContext _db;

    public AuthorEntityBinder(AppDbContext db)
    {
        _db = db;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        if (!int.TryParse(value, out var id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                modelName, "Author Id must be an integer.");

            return Task.CompletedTask;
        }

        // Model will be null if not found, including for 
        // out of range id values (0, -3, etc.)
        var model = _db.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

Nota

La classe AuthorEntityBinder precedente è destinata a illustrare uno strumento di associazione di modelli personalizzato. La classe non ha lo scopo di illustrare procedure consigliate per uno scenario di ricerca. Per la ricerca, associare l'elemento authorId ed eseguire query nel database in un metodo di azione. Questo approccio separa gli errori di associazione di modelli dai casi NotFound.

L'esempio di codice seguente indica come usare AuthorEntityBinder in un metodo di azione:

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }
    
    return Ok(author);
}

L'attributo ModelBinder può essere usato per applicare gli elementi AuthorEntityBinder ai parametri che non usano convenzioni predefinite:

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

In questo esempio, dato che il nome dell'argomento non è il valore authorId predefinito, viene specificato nel parametro usando l'attributo ModelBinder. Sia il controller che il metodo di azione sono semplificati rispetto alla ricerca dell'entità nel metodo di azione. La logica per recuperare l'autore usando Entity Framework Core viene spostata nello strumento di associazione di modelli. Ciò può rappresentare una semplificazione notevole in presenza di vari metodi associati al modello Author.

È possibile applicare l'attributo ModelBinder a singole proprietà del modello (ad esempio a ViewModel) o a parametri del metodo di azione per specificare un determinato strumento di associazione di modelli o nome di modello solo per quel determinato tipo o azione.

Implementazione di un elemento ModelBinderProvider

Anziché applicare un attributo, è possibile implementare IModelBinderProvider. Gli strumenti di associazione del framework incorporati vengono implementati in questo modo. Quando si specifica il tipo sul quale opera lo strumento di associazione, si indica il tipo di argomento prodotto dallo strumento, non l'input accettato dallo strumento. Il provider strumento di associazione seguente funziona con l'elemento AuthorEntityBinder. Quando viene aggiunto alla raccolta di provider di MVC, non è necessario usare l'attributo ModelBinder sui parametri tipizzati Author o Author.

using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}

Nota: il codice precedente restituisce un elemento BinderTypeModelBinder. BinderTypeModelBinder funziona come factory per gli strumenti di associazione di modelli e implementa la funzionalità DI (Dependency Injection, Inserimento di dipendenze). Richiede l'inserimento AuthorEntityBinder delle dipendenze per accedere EF Corea . Usare BinderTypeModelBinder se lo strumento di associazione di modelli richiede servizi da DI (Dependency Injection, Inserimento di dipendenze).

Per usare un provider dello strumento di associazione di modelli personalizzato, aggiungerlo in ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("App"));

    services.AddMvc(options =>
        {
            // add custom binder to beginning of collection
            options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

Durante la valutazione degli strumenti di associazione di modelli, la raccolta di provider viene esaminata in ordine. Viene usato il primo provider che restituisce uno strumento di associazione. Se il provider personalizzato viene aggiunto alla fine della raccolta, è possibile che uno strumento di associazione di modelli incorporato venga chiamato prima dello strumento di associazione di modelli personalizzato. In questo esempio il provider personalizzato viene aggiunto all'inizio della raccolta, per garantire che venga usato per gli argomenti dell'azione Author.

Associazione di modelli polimorfici

L'associazione a modelli diversi di tipi derivati è nota come associazione di modelli polimorfici. L'associazione di modelli personalizzati polimorfici è necessaria quando il valore della richiesta deve essere associato al tipo di modello derivato specifico. Associazione di modelli polimorfici:

  • Non è tipico per un'API REST progettata per interagire con tutti i linguaggi.
  • Rende difficile ragionare sui modelli associati.

Tuttavia, se un'app richiede l'associazione di modelli polimorfici, un'implementazione potrebbe essere simile al codice seguente:

public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

Suggerimenti e procedure ottimali

Gli strumenti di associazione di modelli personalizzati:

  • Non devono provare a impostare codici di stato o restituire risultati (ad esempio 404 Non trovato). Se si verifica un errore nell'associazione di modelli, l'errore deve essere gestito da un filtro azioni o da logica inclusa nel metodo di azione.
  • Sono particolarmente utili per eliminare codice ripetitivo e problemi di montaggio incrociato dai metodi di azione.
  • In genere non devono essere usati per convertire una stringa in un tipo personalizzato. Un elemento TypeConverter rappresenta solitamente una scelta migliore.