Enlace de modelos personalizado en ASP.NET Core

De Kirk Larkin

Con el enlace de modelos, las acciones de controlador pueden funcionar directamente con tipos de modelos (pasados como argumentos de método), en lugar de con solicitudes HTTP. La asignación entre los datos de solicitudes entrantes y los modelos de aplicaciones se controla por medio de enlazadores de modelos. Los desarrolladores pueden ampliar la funcionalidad integrada de enlace de modelos implementando enlazadores de modelos personalizados (si bien, por lo general, no es necesario escribir un proveedor propio).

Vea o descargue el código de ejemplo (cómo descargarlo)

Limitaciones de los enlazadores de modelos predeterminados

Los enlazadores de modelos predeterminados admiten la mayoría de los tipos de datos de .NET Core comunes y deberían cubrir las necesidades de casi cualquier desarrollador. Esperan enlazar entradas basadas en texto desde la solicitud directamente a tipos de modelos. Puede que sea necesario transformar la entrada antes de enlazarla. Es el caso, por ejemplo, si tiene una clave que se puede usar para buscar datos del modelo. Se puede usar un enlazador de modelos personalizado para capturar datos en función de la clave.

Tipos simples y complejos de enlace de modelos

El enlace de modelos usa definiciones específicas de los tipos con los que funciona. Un tipo simple se convierte a partir de una sola cadena mediante un método TypeConverter o TryParse. mientras que un tipo complejo se convierte a partir de varios valores de entrada. El marco establece la diferencia basándose en la existencia de un TypeConverter oTryParse. Se recomienda crear un convertidor de tipos o usar TryParse para una string para una conversión SomeType que no requiera recursos externos ni varias entradas.

Consulte Tipos simples para obtener una lista de tipos que el enlazador de modelos puede convertir de cadenas.

Antes de crear su propio enlazador de modelos personalizado, no está de más que repase cómo se implementan los enlazadores de modelos existentes. Hay que considerar el uso de ByteArrayModelBinder, que sirve para convertir cadenas codificadas con base64 en matrices de bytes. Las matrices de bytes se suelen almacenar como archivos o como campos de tipo BLOB de base de datos.

Trabajar con ByteArrayModelBinder

Las cadenas codificadas con base64 se pueden usar para representar datos binarios. Por ejemplo, una imagen se puede codificar como una cadena. En el ejemplo se incluye una imagen como una cadena codificada con base64 en Base64String.txt.

ASP.NET Core MVC toma cadenas codificadas con base64 y usa un ByteArrayModelBinder para convertirlas en una matriz de bytes. ByteArrayModelBinderProvider asigna argumentos 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;
}

Cuando cree su propio enlazador de modelos personalizado, puede implementar su tipo IModelBinderProvider particular o usar ModelBinderAttribute.

En el siguiente ejemplo se indica cómo usar ByteArrayModelBinder para convertir una cadena codificada con base64 en un byte[] y guardar el resultado en un archivo:

[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 quiere que los comentarios de código se traduzcan en más idiomas además del inglés, háganoslo saber en este problema de debate de GitHub.

Puede PUBLICAR una cadena codificada en base64 en el método de API anterior mediante una herramienta como curl.

Siempre y cuando el enlazador pueda enlazar datos de la solicitud a argumentos o propiedades con el nombre adecuado, el enlace de modelos se realizará correctamente. En el siguiente ejemplo se muestra cómo usar ByteArrayModelBinder con un modelo de vista:

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

Ejemplo de enlazador de modelos personalizado

En esta sección implementaremos un enlazador de modelos personalizado que haga lo siguiente:

  • Convertir los datos de solicitud entrantes en argumentos clave fuertemente tipados
  • Usar Entity Framework Core para capturar la entidad asociada
  • Pasar la entidad asociada como argumento al método de acción

En el siguiente ejemplo se usa el atributo ModelBinder en el modelo 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; }
    }
}

En el código anterior, el atributo ModelBinder especifica el tipo de IModelBinder que se debe emplear para enlazar parámetros de acción de Author.

La clase AuthorEntityBinder siguiente enlaza un parámetro Author capturando la entidad de un origen de datos por medio de Entity Framework Core y un elemento 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 clase AuthorEntityBinder anterior está diseñada para ilustrar un enlazador de modelos personalizado. La clase no está pensada para ilustrar los procedimientos recomendados para un escenario de búsqueda. Para la búsqueda, enlace el valor authorId y consulte la base de datos en un método de acción. Este enfoque permite diferenciar y separar los errores de enlace de modelo de los casos NotFound.

En el siguiente código se indica cómo usar AuthorEntityBinder en un método de acción:

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

    return Ok(author);
}

El atributo ModelBinder se puede usar para aplicar AuthorEntityBinder a los parámetros que no usan convenciones predeterminadas:

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

    return Ok(author);
}

En este ejemplo, como el nombre del argumento no es el authorId predeterminado, se especifica en el parámetro por medio del atributo ModelBinder. Tanto el controlador como el método de acción se simplifican, en contraste con tener que buscar la entidad en el método de acción. La lógica para capturar el autor a través de Entity Framework Core se traslada al enlazador de modelos. Esto puede reducir enormemente la complejidad cuando existen varios métodos que se enlazan al modelo Author.

Puede aplicar el atributo ModelBinder a propiedades de modelo individuales (como en un modelo de vista) o a parámetros del método de acción para especificar un determinado nombre de modelo o enlazador de modelos que sea exclusivo de ese tipo o acción en particular.

Implementar un ModelBinderProvider

En lugar de aplicar un atributo, puede implementar IModelBinderProvider. Así es como se implementan los enlazadores de marco integrados. Cuando se especifica el tipo con el que el enlazador funciona, lo que se está indicando es el tipo de argumento que ese enlazador genera, no la entrada que acepta. El siguiente proveedor de enlazador funciona con AuthorEntityBinder. Cuando se agrega a la colección de proveedores de MVC, no es necesario usar el atributo ModelBinder en los parámetros con tipo 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: El código anterior devuelve un BinderTypeModelBinder. BinderTypeModelBinder actúa como una fábrica de enlazadores de modelos y proporciona la inserción de dependencias. AuthorEntityBinder requiere que la inserción de dependencias tenga acceso a EF Core. Use BinderTypeModelBinder si su enlazador de modelos necesita servicios de inserción de dependencias.

Para usar un proveedor de enlazadores de modelos personalizado, agréguelo a ConfigureServices:

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

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

Al evaluar enlazadores de modelos, la colección de proveedores se examina en orden. Se usa el primer proveedor que devuelve un enlazador que coincide con el modelo de entrada. Agregar a su proveedor al final de la colección, puede provocar que se llame a un enlazador de modelos integrado antes que al suyo. En este ejemplo, el proveedor personalizado se agrega al principio de la colección para asegurar que se use siempre en los argumentos de acción de Author.

Enlace de modelos polimórfico

El enlace a diferentes modelos de tipos derivados se conoce como enlace de modelos polimórfico. El enlace de modelos personalizado polimórfico es necesario cuando el valor de la solicitud se debe enlazar al tipo de modelo derivado específico. Enlace de modelos polimórfico:

  • No es habitual para una API de REST diseñada para interoperar con todos los lenguajes.
  • Dificulta el razonamiento sobre los modelos enlazados.

Sin embargo, si una aplicación requiere el enlace de modelos polimórfico, una implementación podría ser similar al código siguiente:

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

Sugerencias y procedimientos recomendados

Los enlazadores de modelos personalizados deben caracterizarse por lo siguiente:

  • No deben tratar de establecer códigos de estado ni devolver resultados (por ejemplo, 404 No encontrado). Los errores que se produzcan en un enlace de modelos se deben controlar con un filtro de acciones o con la lógica del propio método de acción.
  • Son realmente útiles para eliminar el código repetitivo y las cuestiones transversales de los métodos de acción.
  • No se deben usar en general para convertir una cadena en un tipo personalizado. Para ello, TypeConverter suele ser una mejor opción.

Por Steve Smith

Con el enlace de modelos, las acciones de controlador pueden funcionar directamente con tipos de modelos (pasados como argumentos de método), en lugar de con solicitudes HTTP. La asignación entre los datos de solicitudes entrantes y los modelos de aplicaciones se controla por medio de enlazadores de modelos. Los desarrolladores pueden ampliar la funcionalidad integrada de enlace de modelos implementando enlazadores de modelos personalizados (si bien, por lo general, no es necesario escribir un proveedor propio).

Vea o descargue el código de ejemplo (cómo descargarlo)

Limitaciones de los enlazadores de modelos predeterminados

Los enlazadores de modelos predeterminados admiten la mayoría de los tipos de datos de .NET Core comunes y deberían cubrir las necesidades de casi cualquier desarrollador. Esperan enlazar entradas basadas en texto desde la solicitud directamente a tipos de modelos. Puede que sea necesario transformar la entrada antes de enlazarla. Es el caso, por ejemplo, si tiene una clave que se puede usar para buscar datos del modelo. Se puede usar un enlazador de modelos personalizado para capturar datos en función de la clave.

Revisión del enlace de modelos

El enlace de modelos usa definiciones específicas de los tipos con los que funciona. Un tipo simple se convierte a partir de una sola cadena de la entrada, mientras que un tipo complejo se convierte a partir de varios valores de entrada. El marco establece la diferencia dependiendo de si existe un TypeConverter. Conviene crear un convertidor de tipos si existe una asignación simple string ->SomeType que no necesita recursos externos.

Antes de crear su propio enlazador de modelos personalizado, no está de más que repase cómo se implementan los enlazadores de modelos existentes. Hay que considerar el uso de ByteArrayModelBinder, que sirve para convertir cadenas codificadas con base64 en matrices de bytes. Las matrices de bytes se suelen almacenar como archivos o como campos de tipo BLOB de base de datos.

Trabajar con ByteArrayModelBinder

Las cadenas codificadas con base64 se pueden usar para representar datos binarios. Por ejemplo, una imagen se puede codificar como una cadena. En el ejemplo se incluye una imagen como una cadena codificada con base64 en Base64String.txt.

ASP.NET Core MVC toma cadenas codificadas con base64 y usa un ByteArrayModelBinder para convertirlas en una matriz de bytes. ByteArrayModelBinderProvider asigna argumentos 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;
}

Cuando cree su propio enlazador de modelos personalizado, puede implementar su tipo IModelBinderProvider particular o usar ModelBinderAttribute.

En el siguiente ejemplo se indica cómo usar ByteArrayModelBinder para convertir una cadena codificada con base64 en un byte[] y guardar el resultado en un archivo:

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

Puede PUBLICAR una cadena codificada en base64 en el método de API anterior mediante una herramienta como curl.

Siempre y cuando el enlazador pueda enlazar datos de la solicitud a argumentos o propiedades con el nombre adecuado, el enlace de modelos se realizará correctamente. En el siguiente ejemplo se muestra cómo usar ByteArrayModelBinder con un modelo de vista:

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

Ejemplo de enlazador de modelos personalizado

En esta sección implementaremos un enlazador de modelos personalizado que haga lo siguiente:

  • Convertir los datos de solicitud entrantes en argumentos clave fuertemente tipados
  • Usar Entity Framework Core para capturar la entidad asociada
  • Pasar la entidad asociada como argumento al método de acción

En el siguiente ejemplo se usa el atributo ModelBinder en el modelo 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; }
    }
}

En el código anterior, el atributo ModelBinder especifica el tipo de IModelBinder que se debe emplear para enlazar parámetros de acción de Author.

La clase AuthorEntityBinder siguiente enlaza un parámetro Author capturando la entidad de un origen de datos por medio de Entity Framework Core y un elemento 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 clase AuthorEntityBinder anterior está diseñada para ilustrar un enlazador de modelos personalizado. La clase no está pensada para ilustrar los procedimientos recomendados para un escenario de búsqueda. Para la búsqueda, enlace el valor authorId y consulte la base de datos en un método de acción. Este enfoque permite diferenciar y separar los errores de enlace de modelo de los casos NotFound.

En el siguiente código se indica cómo usar AuthorEntityBinder en un método de acción:

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

El atributo ModelBinder se puede usar para aplicar AuthorEntityBinder a los parámetros que no usan convenciones predeterminadas:

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

    return Ok(author);
}

En este ejemplo, como el nombre del argumento no es el authorId predeterminado, se especifica en el parámetro por medio del atributo ModelBinder. Tanto el controlador como el método de acción se simplifican, en contraste con tener que buscar la entidad en el método de acción. La lógica para capturar el autor a través de Entity Framework Core se traslada al enlazador de modelos. Esto puede reducir enormemente la complejidad cuando existen varios métodos que se enlazan al modelo Author.

Puede aplicar el atributo ModelBinder a propiedades de modelo individuales (como en un modelo de vista) o a parámetros del método de acción para especificar un determinado nombre de modelo o enlazador de modelos que sea exclusivo de ese tipo o acción en particular.

Implementar un ModelBinderProvider

En lugar de aplicar un atributo, puede implementar IModelBinderProvider. Así es como se implementan los enlazadores de marco integrados. Cuando se especifica el tipo con el que el enlazador funciona, lo que se está indicando es el tipo de argumento que ese enlazador genera, no la entrada que acepta. El siguiente proveedor de enlazador funciona con AuthorEntityBinder. Cuando se agrega a la colección de proveedores de MVC, no es necesario usar el atributo ModelBinder en los parámetros con tipo 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: El código anterior devuelve un BinderTypeModelBinder. BinderTypeModelBinder actúa como una fábrica de enlazadores de modelos y proporciona la inserción de dependencias. AuthorEntityBinder requiere que la inserción de dependencias tenga acceso a EF Core. Use BinderTypeModelBinder si su enlazador de modelos necesita servicios de inserción de dependencias.

Para usar un proveedor de enlazadores de modelos personalizado, agréguelo a 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);
}

Al evaluar enlazadores de modelos, la colección de proveedores se examina en orden. Se usará el primer proveedor que devuelva un enlazador. Si su proveedor se agrega al final de la colección, puede ocurrir que se llame a un enlazador de modelos integrado antes que al suyo. En este ejemplo, el proveedor personalizado se agrega al principio de la colección para procurar que se use en los argumentos de acción de Author.

Enlace de modelos polimórfico

El enlace a diferentes modelos de tipos derivados se conoce como enlace de modelos polimórfico. El enlace de modelos personalizado polimórfico es necesario cuando el valor de la solicitud se debe enlazar al tipo de modelo derivado específico. Enlace de modelos polimórfico:

  • No es habitual para una API de REST diseñada para interoperar con todos los lenguajes.
  • Dificulta el razonamiento sobre los modelos enlazados.

Sin embargo, si una aplicación requiere el enlace de modelos polimórfico, una implementación podría ser similar al código siguiente:

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

Sugerencias y procedimientos recomendados

Los enlazadores de modelos personalizados deben caracterizarse por lo siguiente:

  • No deben tratar de establecer códigos de estado ni devolver resultados (por ejemplo, 404 No encontrado). Los errores que se produzcan en un enlace de modelos se deben controlar con un filtro de acciones o con la lógica del propio método de acción.
  • Son realmente útiles para eliminar el código repetitivo y las cuestiones transversales de los métodos de acción.
  • No se deben usar en general para convertir una cadena en un tipo personalizado. Para ello, TypeConverter suele ser una mejor opción.