Liaison de données personnalisée dans ASP.NET Core

Par Kirk Larkin

La liaison de données permet aux actions du contrôleur de fonctionner directement avec des types de modèle (passés en tant qu’arguments de méthode), plutôt qu’avec des requêtes HTTP. Le mappage entre les données de requête entrantes et les modèles d’application est pris en charge par les classeurs de modèles. Les développeurs peuvent étendre la fonctionnalité de liaison de données intégrée en implémentant des classeurs de modèles personnalisés (même si, en règle générale, 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 usuels et doivent répondre aux besoins de la majorité des développeurs. Ils sont censés lier directement les entrées textuelles de la requête aux types de modèle. Vous devrez peut-être transformer l’entrée avant de la lier. Par exemple, quand 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 récupérer (fetch) les données en fonction de la clé.

Types simples et complexes de liaison de modèle

La liaison de données 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 en utilisant TypeConverter ou une méthode TryParse. Un type complexe est converti à partir de plusieurs valeurs d’entrée. Le framework détermine la différence en fonction de l’existence de TypeConverter ou TryParse. Nous vous recommandons de créer un convertisseur de type ou d’utiliser TryParse pour une string conversion vers SomeType qui ne nécessite pas de ressources externes ou plusieurs entrées.

Consultez 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é, vérifiez la façon dont les classeurs de modèles existants sont implémentés. Prenons l’exemple de ByteArrayModelBinder qui permet de convertir des chaînes encodées au format 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 au format Base64 peuvent être utilisées pour représenter des données binaires. Par exemple, une image peut être encodée sous forme de chaîne. L’exemple inclut une image sous forme de chaîne encodée en base64 dans Base64String.txt.

ASP.NET Core MVC peut accepter une chaîne encodée au format base64 et utiliser ByteArrayModelBinder pour la convertir en tableau d’octets. Les ByteArrayModelBinderProvider arguments mappent 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;
}

Quand vous créez votre propre classeur de modèles personnalisé, vous pouvez implémenter votre propre type IModelBinderProvider, ou utiliser le ModelBinderAttribute.

L’exemple suivant montre comment utiliser ByteArrayModelBinder pour convertir une chaîne encodée au format base64 en byte[], et comment 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 envoyer (POST) une chaîne encodée au format base64 à la méthode d’API précédente à l’aide d’un outil tel que curl.

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

[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 récupérer (fetch) l’entité associée.
  • Passe l’entité associée en tant qu’argument à la méthode d’action.

L’exemple suivant utilise l’attribut ModelBinder pour le modèle 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; }
    }
}

Dans le code précédent, l’attribut ModelBinder spécifie le type de IModelBinder à utiliser pour lier les paramètres d’action de Author.

La classe AuthorEntityBinder suivante est utilisée pour lier un paramètre Author en récupérant (fetch) l’entité à partir d’une source de données via Entity Framework Core et 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;
    }
}

Notes

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

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

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

    return Ok(author);
}

L’attribut ModelBinder permet d’appliquer AuthorEntityBinder aux paramètres qui n’utilisent pas les 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, comme le nom de l’argument n’est pas le authorId par défaut, il est spécifié dans 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 de récupérer (fetch) l’auteur à l’aide d’Entity Framework Core est déplacée vers le classeur de modèles. Cela peut représenter une simplification considérable quand vous avez plusieurs méthodes qui sont liées au modèle Author.

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

Implémentation de ModelBinderProvider

Au lieu d’appliquer un attribut, vous pouvez implémenter IModelBinderProvider. C’est ainsi que les classeurs de framework intégrés sont implémentés. Quand vous spécifiez le type sur lequel votre classeur opère, 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 AuthorEntityBinder. Quand il est ajouté à la collection de fournisseurs de MVC, vous n’avez pas besoin d’utiliser l’attribut ModelBinder sur Author ou sur les paramètres typés de 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;
        }
    }
}

Remarque : Le code précédent retourne BinderTypeModelBinder. BinderTypeModelBinder sert de fabrique pour les classeurs de modèles et permet l’injection de dépendances. AuthorEntityBinder a besoin de l’injection de dépendances pour accéder à EF Core. Utilisez BinderTypeModelBinder, si votre classeur de modèles nécessite des services liés à l’injection de dépendances.

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

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

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

Durant l’évaluation des classeurs de modèles, la collection de fournisseurs est examinée dans un certain 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 alors entraîner l’appel d’un classeur de modèles intégré avant votre classeur personnalisé. Dans cet exemple, le fournisseur personnalisé est ajouté au début de la collection afin qu’il soit toujours utilisé pour les arguments d’action Author.

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 pour une REST API conçue pour interagir avec tous les langages.
  • 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 bonnes pratiques

Les classeurs de modèles personnalisés :

  • Ne doivent pas tenter de définir des codes d’état ou de retourner des résultats (par exemple, 404 Introuvable). En cas d’échec de la liaison de données, un filtre d’action ou une logique située dans la méthode d’action elle-même doit prendre en charge l’erreur.
  • Sont surtout utiles pour éliminer le code répétitif et les problèmes transversaux des méthodes d’action.
  • Ne doivent pas être utilisés pour convertir une chaîne en type personnalisé. En règle générale, TypeConverter est une meilleure option.

Par Steve Smith

La liaison de données permet aux actions du contrôleur de fonctionner directement avec des types de modèle (passés en tant qu’arguments de méthode), plutôt qu’avec des requêtes HTTP. Le mappage entre les données de requête entrantes et les modèles d’application est pris en charge par les classeurs de modèles. Les développeurs peuvent étendre la fonctionnalité de liaison de données intégrée en implémentant des classeurs de modèles personnalisés (même si, en règle générale, 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 usuels et doivent répondre aux besoins de la majorité des développeurs. Ils sont censés lier directement les entrées textuelles de la requête aux types de modèle. Vous devrez peut-être transformer l’entrée avant de la lier. Par exemple, quand 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 récupérer (fetch) les données en fonction de la clé.

Vérification de la liaison de données

La liaison de données 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 de TypeConverter. Nous vous recommandons de créer un convertisseur de type si vous disposez d’un mappage string –>SomeType simple qui ne nécessite pas de ressources externes.

Avant de créer votre propre classeur de modèles personnalisé, vérifiez la façon dont les classeurs de modèles existants sont implémentés. Prenons l’exemple de ByteArrayModelBinder qui permet de convertir des chaînes encodées au format 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 au format Base64 peuvent être utilisées pour représenter des données binaires. Par exemple, une image peut être encodée sous forme de chaîne. L’exemple inclut une image sous forme de chaîne encodée en base64 dans Base64String.txt.

ASP.NET Core MVC peut accepter une chaîne encodée au format base64 et utiliser ByteArrayModelBinder pour la convertir en tableau d’octets. Les ByteArrayModelBinderProvider arguments mappent 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;
}

Quand vous créez votre propre classeur de modèles personnalisé, vous pouvez implémenter votre propre type IModelBinderProvider, ou utiliser le ModelBinderAttribute.

L’exemple suivant montre comment utiliser ByteArrayModelBinder pour convertir une chaîne encodée au format base64 en byte[], et comment 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 envoyer (POST) une chaîne encodée au format base64 à la méthode d’API précédente à l’aide d’un outil tel que curl.

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

[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 récupérer (fetch) l’entité associée.
  • Passe l’entité associée en tant qu’argument à la méthode d’action.

L’exemple suivant utilise l’attribut ModelBinder pour le modèle 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; }
    }
}

Dans le code précédent, l’attribut ModelBinder spécifie le type de IModelBinder à utiliser pour lier les paramètres d’action de Author.

La classe AuthorEntityBinder suivante est utilisée pour lier un paramètre Author en récupérant (fetch) l’entité à partir d’une source de données via Entity Framework Core et 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;
    }
}

Notes

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

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

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

L’attribut ModelBinder permet d’appliquer AuthorEntityBinder aux paramètres qui n’utilisent pas les 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, comme le nom de l’argument n’est pas le authorId par défaut, il est spécifié dans 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 de récupérer (fetch) l’auteur à l’aide d’Entity Framework Core est déplacée vers le classeur de modèles. Cela peut représenter une simplification considérable quand vous avez plusieurs méthodes qui sont liées au modèle Author.

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

Implémentation de ModelBinderProvider

Au lieu d’appliquer un attribut, vous pouvez implémenter IModelBinderProvider. C’est ainsi que les classeurs de framework intégrés sont implémentés. Quand vous spécifiez le type sur lequel votre classeur opère, 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 AuthorEntityBinder. Quand il est ajouté à la collection de fournisseurs de MVC, vous n’avez pas besoin d’utiliser l’attribut ModelBinder sur Author ou sur les paramètres typés de 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;
        }
    }
}

Remarque : Le code précédent retourne BinderTypeModelBinder. BinderTypeModelBinder sert de fabrique pour les classeurs de modèles et permet l’injection de dépendances. AuthorEntityBinder a besoin de l’injection de dépendances pour accéder à EF Core. Utilisez BinderTypeModelBinder, si votre classeur de modèles nécessite des services liés à l’injection de dépendances.

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

Durant l’évaluation des classeurs de modèles, la collection de fournisseurs est examinée dans un certain 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 votre classeur personnalisé. Dans cet exemple, le fournisseur personnalisé est ajouté au début de la collection afin qu’il soit utilisé pour les arguments d’action Author.

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 pour une REST API conçue pour interagir avec tous les langages.
  • 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 bonnes pratiques

Les classeurs de modèles personnalisés :

  • Ne doivent pas tenter de définir des codes d’état ou de retourner des résultats (par exemple, 404 Introuvable). En cas d’échec de la liaison de données, un filtre d’action ou une logique située dans la méthode d’action elle-même doit prendre en charge l’erreur.
  • Sont surtout utiles pour éliminer le code répétitif et les problèmes transversaux des méthodes d’action.
  • Ne doivent pas être utilisés pour convertir une chaîne en type personnalisé. En règle générale, TypeConverter est une meilleure option.