Partager via


Liaison de modèle personnalisé dans ASP.NET Core

Par Kirk Larkin

La liaison de modèle permet aux actions du contrôleur de fonctionner directement avec des types de modèle (transmis en tant qu’arguments de méthode), plutôt que des requêtes HTTP. Le mappage entre les données de requête entrantes et les modèles d’application est géré par des classeurs de modèles. Les développeurs peuvent étendre la fonctionnalité de liaison de modèle intégrée en implémentant des classeurs de modèles personnalisés (bien que généralement, vous n’avez pas besoin d’écrire votre propre fournisseur).

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Limitations du classeur de modèles par défaut

Les classeurs de modèles par défaut prennent en charge la plupart des types de données .NET Core courants et doivent répondre à la plupart des besoins des développeurs. Ils s’attendent à lier directement une entrée basée sur du texte à partir de la requête aux types de modèle. Vous devrez peut-être transformer l’entrée avant de la lier. Par exemple, lorsque vous avez une clé qui peut être utilisée pour rechercher des données de modèle. Vous pouvez utiliser un classeur de modèles personnalisé pour extraire des données en fonction de la clé.

Types simples et complexes de liaison de modèle

La liaison de modèle utilise des définitions spécifiques pour les types sur lesquels elle opère. Un type simple est converti à partir d’une seule chaîne à l’aide TypeConverter ou d’une TryParse méthode. Un type complexe est converti à partir de plusieurs valeurs d’entrée. Le framework détermine la différence en fonction de l’existence d’un TypeConverter ou TryParse. Nous vous recommandons de créer un convertisseur de type ou d’utiliser TryParse pour une stringSomeType conversion qui ne nécessite pas de ressources externes ni plusieurs entrées.

Consultez les types simples pour obtenir la liste des types que le classeur de modèles peut convertir à partir de chaînes.

Avant de créer votre propre classeur de modèles personnalisé, il vaut la peine d’examiner la façon dont les classeurs de modèles existants sont implémentés. Considérez ce ByteArrayModelBinder qui peut être utilisé pour convertir des chaînes encodées en base64 en tableaux d’octets. Les tableaux d’octets sont souvent stockés sous forme de fichiers ou de champs BLOB de base de données.

Utilisation de ByteArrayModelBinder

Les chaînes encodées en base64 peuvent être utilisées pour représenter des données binaires. Par exemple, une image peut être encodée en tant que chaîne. L’exemple inclut une image sous forme de chaîne encodée en base64 dans Base64String.txt.

ASP.NET Core MVC peut prendre une chaîne encodée en base64 et l’utiliser ByteArrayModelBinder pour la convertir en tableau d’octets. Les ByteArrayModelBinderProvider arguments sont mappés byte[] à 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;
}

Lors de la création de votre propre classeur de modèles personnalisé, vous pouvez implémenter votre propre IModelBinderProvider type ou utiliser le ModelBinderAttributefichier .

L’exemple suivant montre comment utiliser ByteArrayModelBinder pour convertir une chaîne encodée en base64 en une byte[] et enregistrer le résultat dans un fichier :

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

Si vous souhaitez voir les commentaires de code traduits dans une langue autre que l’anglais, dites-le nous dans cette discussion GitHub.

Vous pouvez PUBLIER une chaîne encodée en base64 à la méthode api précédente à l’aide d’un outil tel que curl.

Tant que le classeur peut lier des données de requête à des propriétés ou arguments nommés de manière appropriée, la liaison de modèle réussit. L’exemple suivant montre comment utiliser ByteArrayModelBinder avec un modèle d’affichage :

[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; }
}

Exemple de classeur de modèles personnalisé

Dans cette section, nous allons implémenter un classeur de modèles personnalisé qui :

  • Convertit les données de requête entrantes en arguments clés fortement typés.
  • Utilise Entity Framework Core pour extraire l’entité associée.
  • Transmet l’entité associée en tant qu’argument à la méthode d’action.

L’exemple suivant utilise l’attribut ModelBinder sur le Author modèle :

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

Dans le code précédent, l’attribut ModelBinder spécifie le type de IModelBinder ce qui doit être utilisé pour lier Author des paramètres d’action.

La classe suivante AuthorEntityBinder lie un Author paramètre en récupérant l’entité à partir d’une source de données à l’aide d’Entity Framework Core et d’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;
    }
}

Note

La classe précédente AuthorEntityBinder est destinée à illustrer un classeur de modèles personnalisé. La classe n’est pas destinée à illustrer les meilleures pratiques pour un scénario de recherche. Pour la recherche, liez la base de données et interrogez-la authorId dans une méthode d’action. Cette approche sépare les échecs de liaison de modèle des NotFound cas.

Le code suivant montre comment utiliser la AuthorEntityBinder méthode d’action :

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

    return Ok(author);
}

L’attribut ModelBinder peut être utilisé pour appliquer les AuthorEntityBinder paramètres qui n’utilisent pas de conventions par défaut :

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

    return Ok(author);
}

Dans cet exemple, étant donné que le nom de l’argument n’est pas la valeur par défaut authorId, il est spécifié sur le paramètre à l’aide de l’attribut ModelBinder . Le contrôleur et la méthode d’action sont simplifiés par rapport à la recherche de l’entité dans la méthode d’action. La logique permettant d’extraire l’auteur à l’aide d’Entity Framework Core est déplacée vers le classeur de modèles. Cela peut être une simplification considérable lorsque vous avez plusieurs méthodes liées au Author modèle.

Vous pouvez appliquer l’attribut ModelBinder à des propriétés de modèle individuelles (par exemple, sur un viewmodel) ou à des paramètres de méthode d’action pour spécifier un certain classeur de modèles ou un nom de modèle pour ce type ou cette action uniquement.

Implémentation d’un ModelBinderProvider

Au lieu d’appliquer un attribut, vous pouvez implémenter IModelBinderProvider. Il s’agit de la façon dont les classeurs d’infrastructure intégrés sont implémentés. Lorsque vous spécifiez le type sur lequel fonctionne votre classeur, vous spécifiez le type d’argument qu’il produit, et non l’entrée que votre classeur accepte. Le fournisseur de classeurs suivant fonctionne avec le AuthorEntityBinder. Lorsqu’il est ajouté à la collection de fournisseurs de MVC, vous n’avez pas besoin d’utiliser l’attribut ModelBinder sur Author les paramètres typés ou Author-typés.

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

Remarque : Le code précédent retourne un BinderTypeModelBinder. BinderTypeModelBinder sert d’usine pour les classeurs de modèles et fournit l’injection de dépendances (DI). La AuthorEntityBinder demande d’accès EF Coreà l’aide de l’aide à l’aide de l' Utilisez BinderTypeModelBinder si votre classeur de modèles nécessite des services à partir d’une di.

Pour utiliser un fournisseur de classeur de modèles personnalisé, ajoutez-le dans ConfigureServices:

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

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

Lors de l’évaluation des classeurs de modèles, la collection de fournisseurs est examinée dans l’ordre. Le premier fournisseur qui retourne un classeur qui correspond au modèle d’entrée est utilisé. L’ajout de votre fournisseur à la fin de la collection peut donc entraîner l’appel d’un classeur de modèles intégré avant que votre classeur personnalisé ait une chance. Dans cet exemple, le fournisseur personnalisé est ajouté au début de la collection pour s’assurer qu’il est toujours utilisé pour Author les arguments d’action.

Liaison de modèle polymorphe

La liaison à différents modèles de types dérivés est appelée liaison de modèle polymorphe. La liaison de modèle personnalisé polymorphe est requise lorsque la valeur de la requête doit être liée au type de modèle dérivé spécifique. Liaison de modèle polymorphe :

  • N’est pas typique d’une REST API conçue pour interagir avec toutes les langues.
  • Rend difficile la raison des modèles liés.

Toutefois, si une application nécessite une liaison de modèle polymorphe, une implémentation peut ressembler au code suivant :

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,
            };
        }
    }
}

Recommandations et meilleures pratiques

Classeurs de modèles personnalisés :

  • Ne doit pas tenter de définir des codes d’état ou de retourner des résultats (par exemple, 404 Introuvable). Si la liaison de modèle échoue, un filtre d’action ou une logique dans la méthode d’action elle-même doit gérer l’échec.
  • Sont les plus utiles pour éliminer le code répétitif et les préoccupations croisées des méthodes d’action.
  • En règle générale, ne doit pas être utilisé pour convertir une chaîne en type personnalisé, une TypeConverter option est généralement meilleure.

Par Steve Smith

La liaison de modèle permet aux actions du contrôleur de fonctionner directement avec des types de modèle (transmis en tant qu’arguments de méthode), plutôt que des requêtes HTTP. Le mappage entre les données de requête entrantes et les modèles d’application est géré par des classeurs de modèles. Les développeurs peuvent étendre la fonctionnalité de liaison de modèle intégrée en implémentant des classeurs de modèles personnalisés (bien que généralement, vous n’avez pas besoin d’écrire votre propre fournisseur).

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Limitations du classeur de modèles par défaut

Les classeurs de modèles par défaut prennent en charge la plupart des types de données .NET Core courants et doivent répondre à la plupart des besoins des développeurs. Ils s’attendent à lier directement une entrée basée sur du texte à partir de la requête aux types de modèle. Vous devrez peut-être transformer l’entrée avant de la lier. Par exemple, lorsque vous avez une clé qui peut être utilisée pour rechercher des données de modèle. Vous pouvez utiliser un classeur de modèles personnalisé pour extraire des données en fonction de la clé.

Révision de liaison de modèle

La liaison de modèle utilise des définitions spécifiques pour les types sur lesquels elle opère. Un type simple est converti à partir d’une seule chaîne dans l’entrée. Un type complexe est converti à partir de plusieurs valeurs d’entrée. Le framework détermine la différence en fonction de l’existence d’un TypeConverter. Nous vous recommandons de créer un convertisseur de type si vous disposez d’un mappage simple string ,>SomeType qui ne nécessite pas de ressources externes.

Avant de créer votre propre classeur de modèles personnalisé, il vaut la peine d’examiner la façon dont les classeurs de modèles existants sont implémentés. Considérez ce ByteArrayModelBinder qui peut être utilisé pour convertir des chaînes encodées en base64 en tableaux d’octets. Les tableaux d’octets sont souvent stockés sous forme de fichiers ou de champs BLOB de base de données.

Utilisation de ByteArrayModelBinder

Les chaînes encodées en base64 peuvent être utilisées pour représenter des données binaires. Par exemple, une image peut être encodée en tant que chaîne. L’exemple inclut une image sous forme de chaîne encodée en base64 dans Base64String.txt.

ASP.NET Core MVC peut prendre une chaîne encodée en base64 et l’utiliser ByteArrayModelBinder pour la convertir en tableau d’octets. Les ByteArrayModelBinderProvider arguments sont mappés byte[] à 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;
}

Lors de la création de votre propre classeur de modèles personnalisé, vous pouvez implémenter votre propre IModelBinderProvider type ou utiliser le ModelBinderAttributefichier .

L’exemple suivant montre comment utiliser ByteArrayModelBinder pour convertir une chaîne encodée en base64 en une byte[] et enregistrer le résultat dans un fichier :

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

Vous pouvez PUBLIER une chaîne encodée en base64 à la méthode api précédente à l’aide d’un outil tel que curl.

Tant que le classeur peut lier des données de requête à des propriétés ou arguments nommés de manière appropriée, la liaison de modèle réussit. L’exemple suivant montre comment utiliser ByteArrayModelBinder avec un modèle d’affichage :

[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; }
}

Exemple de classeur de modèles personnalisé

Dans cette section, nous allons implémenter un classeur de modèles personnalisé qui :

  • Convertit les données de requête entrantes en arguments clés fortement typés.
  • Utilise Entity Framework Core pour extraire l’entité associée.
  • Transmet l’entité associée en tant qu’argument à la méthode d’action.

L’exemple suivant utilise l’attribut ModelBinder sur le Author modèle :

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

Dans le code précédent, l’attribut ModelBinder spécifie le type de IModelBinder ce qui doit être utilisé pour lier Author des paramètres d’action.

La classe suivante AuthorEntityBinder lie un Author paramètre en récupérant l’entité à partir d’une source de données à l’aide d’Entity Framework Core et d’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;
    }
}

Note

La classe précédente AuthorEntityBinder est destinée à illustrer un classeur de modèles personnalisé. La classe n’est pas destinée à illustrer les meilleures pratiques pour un scénario de recherche. Pour la recherche, liez la base de données et interrogez-la authorId dans une méthode d’action. Cette approche sépare les échecs de liaison de modèle des NotFound cas.

Le code suivant montre comment utiliser la AuthorEntityBinder méthode d’action :

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

L’attribut ModelBinder peut être utilisé pour appliquer les AuthorEntityBinder paramètres qui n’utilisent pas de conventions par défaut :

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

    return Ok(author);
}

Dans cet exemple, étant donné que le nom de l’argument n’est pas la valeur par défaut authorId, il est spécifié sur le paramètre à l’aide de l’attribut ModelBinder . Le contrôleur et la méthode d’action sont simplifiés par rapport à la recherche de l’entité dans la méthode d’action. La logique permettant d’extraire l’auteur à l’aide d’Entity Framework Core est déplacée vers le classeur de modèles. Cela peut être une simplification considérable lorsque vous avez plusieurs méthodes liées au Author modèle.

Vous pouvez appliquer l’attribut ModelBinder à des propriétés de modèle individuelles (par exemple, sur un viewmodel) ou à des paramètres de méthode d’action pour spécifier un certain classeur de modèles ou un nom de modèle pour ce type ou cette action uniquement.

Implémentation d’un ModelBinderProvider

Au lieu d’appliquer un attribut, vous pouvez implémenter IModelBinderProvider. Il s’agit de la façon dont les classeurs d’infrastructure intégrés sont implémentés. Lorsque vous spécifiez le type sur lequel fonctionne votre classeur, vous spécifiez le type d’argument qu’il produit, et non l’entrée que votre classeur accepte. Le fournisseur de classeurs suivant fonctionne avec le AuthorEntityBinder. Lorsqu’il est ajouté à la collection de fournisseurs de MVC, vous n’avez pas besoin d’utiliser l’attribut ModelBinder sur Author les paramètres typés ou Author-typés.

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

Remarque : Le code précédent retourne un BinderTypeModelBinder. BinderTypeModelBinder sert d’usine pour les classeurs de modèles et fournit l’injection de dépendances (DI). La AuthorEntityBinder demande d’accès EF Coreà l’aide de l’aide à l’aide de l' Utilisez BinderTypeModelBinder si votre classeur de modèles nécessite des services à partir d’une di.

Pour utiliser un fournisseur de classeur de modèles personnalisé, ajoutez-le dans 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);
}

Lors de l’évaluation des classeurs de modèles, la collection de fournisseurs est examinée dans l’ordre. Le premier fournisseur qui retourne un classeur est utilisé. L’ajout de votre fournisseur à la fin de la collection peut entraîner l’appel d’un classeur de modèles intégré avant que votre classeur personnalisé ait une chance. Dans cet exemple, le fournisseur personnalisé est ajouté au début de la collection pour s’assurer qu’il est utilisé pour Author les arguments d’action.

Liaison de modèle polymorphe

La liaison à différents modèles de types dérivés est appelée liaison de modèle polymorphe. La liaison de modèle personnalisé polymorphe est requise lorsque la valeur de la requête doit être liée au type de modèle dérivé spécifique. Liaison de modèle polymorphe :

  • N’est pas typique d’une REST API conçue pour interagir avec toutes les langues.
  • Rend difficile la raison des modèles liés.

Toutefois, si une application nécessite une liaison de modèle polymorphe, une implémentation peut ressembler au code suivant :

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,
            };
        }
    }
}

Recommandations et meilleures pratiques

Classeurs de modèles personnalisés :

  • Ne doit pas tenter de définir des codes d’état ou de retourner des résultats (par exemple, 404 Introuvable). Si la liaison de modèle échoue, un filtre d’action ou une logique dans la méthode d’action elle-même doit gérer l’échec.
  • Sont les plus utiles pour éliminer le code répétitif et les préoccupations croisées des méthodes d’action.
  • En règle générale, ne doit pas être utilisé pour convertir une chaîne en type personnalisé, une TypeConverter option est généralement meilleure.