Liaison de données personnalisée dans ASP.NET CoreCustom Model Binding in ASP.NET Core

Par Steve SmithBy 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.Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. 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.Mapping between incoming request data and application models is handled by model binders. 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).Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).

Afficher ou télécharger un exemple depuis GitHubView or download sample from GitHub

Limitations du classeur de modèles par défautDefault model binder limitations

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.The default model binders support most of the common .NET Core data types and should meet most developers' needs. Ils sont censés lier directement les entrées textuelles de la requête aux types de modèle.They expect to bind text-based input from the request directly to model types. Vous devrez peut-être transformer l’entrée avant de la lier.You might need to transform the input prior to binding it. Par exemple, quand vous avez une clé qui peut être utilisée pour rechercher des données de modèle.For example, when you have a key that can be used to look up model data. Vous pouvez utiliser un classeur de modèles personnalisé pour récupérer (fetch) les données en fonction de la clé.You can use a custom model binder to fetch data based on the key.

Vérification de la liaison de donnéesModel binding review

La liaison de données utilise des définitions spécifiques pour les types sur lesquels elle opère.Model binding uses specific definitions for the types it operates on. Un type simple est converti à partir d’une seule chaîne dans l’entrée.A simple type is converted from a single string in the input. Un type complexe est converti à partir de plusieurs valeurs d’entrée.A complex type is converted from multiple input values. Le framework détermine la différence en fonction de l’existence de TypeConverter.The framework determines the difference based on the existence of a 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.We recommended you create a type converter if you have a simple string -> SomeType mapping that doesn't require external resources.

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.Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. Prenons l’exemple de ByteArrayModelBinder, qui permet de convertir des chaînes encodées au format base64 en tableaux d’octets.Consider the ByteArrayModelBinder which can be used to convert base64-encoded strings into byte arrays. Les tableaux d’octets sont souvent stockés sous forme de fichiers ou de champs BLOB de base de données.The byte arrays are often stored as files or database BLOB fields.

Utilisation de ByteArrayModelBinderWorking with the ByteArrayModelBinder

Les chaînes encodées au format Base64 peuvent être utilisées pour représenter des données binaires.Base64-encoded strings can be used to represent binary data. Par exemple, l’image suivante peut être encodée sous forme de chaîne.For example, the following image can be encoded as a string.

dotnet botdotnet bot

Une petite partie de la chaîne encodée est affichée dans l’image suivante :A small portion of the encoded string is shown in the following image:

dotnet bot encodédotnet bot encoded

Suivez les instructions du fichier README de l’exemple pour convertir la chaîne encodée au format base64 en fichier.Follow the instructions in the sample's README to convert the base64-encoded string into a file.

ASP.NET Core MVC peut accepter une chaîne encodée au format base64 et utiliser ByteArrayModelBinder pour la convertir en tableau d’octets.ASP.NET Core MVC can take a base64-encoded string and use a ByteArrayModelBinder to convert it into a byte array. Le ByteArrayModelBinderProvider qui implémente IModelBinderProvider mappe les arguments byte[] à ByteArrayModelBinder :The ByteArrayModelBinderProvider which implements IModelBinderProvider maps byte[] arguments to 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 ModelBinderAttribute.When creating your own custom model binder, you can implement your own IModelBinderProvider type, or use the 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 :The following example shows how to use ByteArrayModelBinder to convert a base64-encoded string to a byte[] and save the result to a file:

// POST: api/image
[HttpPost]
public void Post(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 à cette méthode d’API à l’aide d’un outil tel que Postman :You can POST a base64-encoded string to this api method using a tool like Postman:

postmanpostman

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.As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. L’exemple suivant montre comment utiliser ByteArrayModelBinder avec un modèle de vue :The following example shows how to use ByteArrayModelBinder with a view model:

[HttpPost("Profile")]
public void SaveProfile(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éCustom model binder sample

Dans cette section, nous allons implémenter un classeur de modèles personnalisé qui :In this section we'll implement a custom model binder that:

  • Convertit les données de requête entrantes en arguments clés fortement typés.Converts incoming request data into strongly typed key arguments.
  • Utilise Entity Framework Core pour récupérer (fetch) l’entité associée.Uses Entity Framework Core to fetch the associated entity.
  • Passe l’entité associée en tant qu’argument à la méthode d’action.Passes the associated entity as an argument to the action method.

L’exemple suivant utilise l’attribut ModelBinder pour le modèle Author :The following sample uses the ModelBinder attribute on the Author model:

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

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.In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind Author action parameters.

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 :The following AuthorEntityBinder class binds an Author parameter by fetching the entity from a data source using Entity Framework Core and an 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;
        }

        int id = 0;
        if (!int.TryParse(value, out 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é.The preceding AuthorEntityBinder class is intended to illustrate a custom model binder. La classe n’est pas destinée à illustrer les bonnes pratiques pour un scénario de recherche.The class isn't intended to illustrate best practices for a lookup scenario. Pour la recherche, liez authorId et interrogez la base de données dans une méthode d’action.For lookup, bind the authorId and query the database in an action method. Cette approche sépare les échecs de liaison des modèles des cas NotFound.This approach separates model binding failures from NotFound cases.

Le code suivant montre comment utiliser AuthorEntityBinder dans une méthode d’action :The following code shows how to use the AuthorEntityBinder in an action method:

[HttpGet("get/{authorId}")]
public IActionResult Get(Author author)
{
    return Ok(author);
}

L’attribut ModelBinder permet d’appliquer AuthorEntityBinder aux paramètres qui n’utilisent pas les conventions par défaut :The ModelBinder attribute can be used to apply the AuthorEntityBinder to parameters that don't use default conventions:

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")]Author author)
{
    if (author == null)
    {
        return NotFound();
    }
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    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.In this example, since the name of the argument isn't the default authorId, it's specified on the parameter using the ModelBinder attribute. 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.Both the controller and action method are simplified compared to looking up the entity in the action method. 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.The logic to fetch the author using Entity Framework Core is moved to the model binder. Cela peut représenter une simplification considérable quand vous avez plusieurs méthodes qui sont liées au modèle Author.This can be a considerable simplification when you have several methods that bind to the Author model.

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.You can apply the ModelBinder attribute to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.

Implémentation de ModelBinderProviderImplementing a ModelBinderProvider

Au lieu d’appliquer un attribut, vous pouvez implémenter IModelBinderProvider.Instead of applying an attribute, you can implement IModelBinderProvider. C’est ainsi que les classeurs de framework intégrés sont implémentés.This is how the built-in framework binders are implemented. 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.When you specify the type your binder operates on, you specify the type of argument it produces, not the input your binder accepts. Le fournisseur de classeurs suivant fonctionne avec AuthorEntityBinder.The following binder provider works with the 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.When it's added to MVC's collection of providers, you don't need to use the ModelBinder attribute on Author or Author-typed parameters.

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.Note: The preceding code returns a BinderTypeModelBinder. BinderTypeModelBinder sert de fabrique pour les classeurs de modèles et permet l’injection de dépendances.BinderTypeModelBinder acts as a factory for model binders and provides dependency injection (DI). AuthorEntityBinder a besoin de l’injection de dépendances pour accéder à EF Core.The AuthorEntityBinder requires DI to access EF Core. Utilisez BinderTypeModelBinder, si votre classeur de modèles nécessite des services liés à l’injection de dépendances.Use BinderTypeModelBinder if your model binder requires services from DI.

Pour utiliser un fournisseur de classeurs de modèles personnalisé, ajoutez-le à ConfigureServices :To use a custom model binder provider, add it in ConfigureServices:

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

    services.AddMvc(options =>
    {
        // add custom binder to beginning of collection
        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.When evaluating model binders, the collection of providers is examined in order. Le premier fournisseur qui retourne un classeur est utilisé.The first provider that returns a binder is used.

L’image suivante illustre les classeurs de modèles par défaut du débogueur.The following image shows the default model binders from the debugger.

classeurs de modèles par défautdefault model binders

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é.Adding your provider to the end of the collection may result in a built-in model binder being called before your custom binder has a chance. 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.In this example, the custom provider is added to the beginning of the collection to ensure it's used for Author action arguments.

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

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

Liaison de modèle polymorphePolymorphic model binding

La liaison à différents modèles de types dérivés porte le nom de liaison de modèle polymorphe.Binding to different models of derived types is known as polymorphic model binding. La liaison de modèle personnalisé polymorphe est requise lorsque la valeur de la demande doit être liée au type de modèle dérivé spécifique.Polymorphic custom model binding is required when the request value must be bound to the specific derived model type. Liaison de modèle polymorphe :Polymorphic model binding:

  • N’est pas typique d’une API REST conçue pour interagir avec toutes les langues.Isn't typical for a REST API that's designed to interoperate with all languages.
  • Rend difficile la raison des modèles liés.Makes it difficult to reason about the bound models.

Toutefois, si une application requiert une liaison de modèle polymorphe, une implémentation peut se présenter comme le code suivant :However, if an app requires polymorphic model binding, an implementation might look like the following code:

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] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

Recommandations et bonnes pratiquesRecommendations and best practices

Les classeurs de modèles personnalisés :Custom model binders:

  • Ne doivent pas tenter de définir des codes d’état ou de retourner des résultats (par exemple, 404 Introuvable).Shouldn't attempt to set status codes or return results (for example, 404 Not Found). 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.If model binding fails, an action filter or logic within the action method itself should handle the failure.
  • Sont surtout utiles pour éliminer le code répétitif et les problèmes transversaux des méthodes d’action.Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
  • 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.Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.