Enlace de modelos en ASP.NET Core

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión ASP.NET Core 8.0 de este artículo.

En este artículo se explica qué es el enlace de modelos, cómo funciona y cómo personalizar su comportamiento.

Qué es el enlace de modelos

Los controladores y lasRazor pages trabajan con datos que provienen de solicitudes HTTP. Por ejemplo, los datos de ruta pueden proporcionar una clave de registro y los campos de formulario publicados pueden proporcionar valores para las propiedades del modelo. La escritura de código para recuperar cada uno de estos valores y convertirlos de cadenas a tipos de .NET sería tediosa y propensa a errores. El enlace de modelos automatiza este proceso. El sistema de enlace de modelos:

  • Recupera datos de diversos orígenes, como datos de ruta, campos de formulario y cadenas de consulta.
  • Proporciona los datos a los controladores y Razor pages en parámetros de método y propiedades públicas.
  • Convierte datos de cadena en tipos de .NET.
  • Actualiza las propiedades de tipos complejos.

Ejemplo

Imagine que tiene el siguiente método de acción:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

Y que la aplicación recibe una solicitud con esta dirección URL:

https://contoso.com/api/pets/2?DogsOnly=true

El enlace de modelos realiza los pasos siguientes después de que el sistema de enrutamiento selecciona el método de acción:

  • Busca el primer parámetro de GetById, un entero denominado id.
  • Examina los orígenes disponibles en la solicitud HTTP y busca id = "2" en los datos de ruta.
  • Convierte la cadena "2" en el entero 2.
  • Busca el siguiente parámetro de GetById, un valor booleano denominado dogsOnly.
  • Examina los orígenes y busca "DogsOnly=true" en la cadena de consulta. La coincidencia de nombres no distingue mayúsculas de minúsculas.
  • Convierte la cadena "true" en un valor booleano true.

Después, el marco llama al método GetById y pasa 2 para el parámetro id y true para el parámetro dogsOnly.

En el ejemplo anterior, los destinos de enlace de modelos son parámetros de método que son tipos simples. Los destinos también pueden ser las propiedades de un tipo complejo. Después de que cada propiedad se haya enlazado correctamente, se produce la validación de modelos para esa propiedad. El registro de qué datos se han enlazado al modelo y los errores de enlace o validación se almacenan en ControllerBase.ModelState o PageModel.ModelState. Para averiguar si este proceso se ha realizado correctamente, la aplicación comprueba la marca ModelState.IsValid.

Targets

El enlace de modelos intenta encontrar valores para los tipos de destinos siguientes:

  • Parámetros del método de acción de controlador al que se enruta una solicitud.
  • Parámetros del método de administración de Razor páginas al que se enruta una solicitud.
  • Propiedades públicas de un controlador o una clase PageModel, si se especifican mediante atributos.

Atributo [BindProperty]

Se puede aplicar a una propiedad pública de un controlador o una clase PageModel para hacer que el enlace de modelos tenga esa propiedad como destino:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Atributo [BindProperties]

Se pueden aplicar a un controlador o una clase PageModel para indicar al enlace de modelos que seleccione como destino todas las propiedades públicas de la clase:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Enlace de modelos para solicitudes HTTP GET

De forma predeterminada, las propiedades no se enlazan para las solicitudes HTTP GET. Normalmente, todo lo que necesita para una solicitud GET es un parámetro de id. de registro. El id. de registro se usa para buscar el elemento en la base de datos. Por tanto, no es necesario enlazar una propiedad que contiene una instancia del modelo. En escenarios donde quiera propiedades enlazadas a datos de las solicitudes GET, establezca la propiedad SupportsGet en true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

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 conversión de string a SomeType que no requiera recursos externos ni varias entradas.

Orígenes

De forma predeterminada, el enlace de modelos obtiene datos en forma de pares clave-valor de los siguientes orígenes de una solicitud HTTP:

  1. Campos de formulario
  2. El cuerpo de la solicitud (para controladores que tienen el atributo [ApiController]).
  3. Datos de ruta
  4. Parámetros de cadena de consulta
  5. Archivos cargados

Para cada parámetro o propiedad de destino, se examinan los orígenes en el orden indicado en la lista anterior. Hay algunas excepciones:

  • Los datos de ruta y los valores de cadena de consulta solo se usan para tipos simples.
  • Los archivos cargados solo se enlazan a tipos de destino que implementan IFormFile o IEnumerable<IFormFile>.

Si el origen predeterminado no es correcto, use uno de los atributos siguientes para especificar el origen:

  • [FromQuery]: obtiene valores de la cadena de consulta.
  • [FromRoute]: obtiene valores de los datos de ruta.
  • [FromForm]: obtiene valores de los campos de formulario publicados.
  • [FromBody]: obtiene valores del cuerpo de la solicitud.
  • [FromHeader]: obtiene valores de encabezados HTTP.

Estos atributos:

  • Se agregan de forma individual a las propiedades del modelo y no a la clase de modelo, como en el ejemplo siguiente:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Opcionalmente, acepte un valor de nombre de modelo en el constructor. Esta opción se proporciona en caso de que el nombre de la propiedad no coincida con el valor de la solicitud. Por ejemplo, es posible que el valor de la solicitud sea un encabezado con un guion en el nombre, como en el ejemplo siguiente:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Atributo [FromBody]

Aplique el atributo [FromBody] a un parámetro para rellenar sus propiedades desde el cuerpo de una solicitud HTTP. El runtime de ASP.NET Core delega la responsabilidad de leer el cuerpo al formateador de entrada. Los formateadores de entrada se explican más adelante en este artículo.

Cuando se aplica [FromBody] a un parámetro de tipo complejo, se omiten los atributos de origen de enlace aplicados a sus propiedades. Por ejemplo, la acción Create siguiente especifica que su parámetro pet se rellena a partir del cuerpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

La clase Pet especifica que su propiedad Breed se rellena a partir de un parámetro de cadena de consulta:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

En el ejemplo anterior:

  • El atributo [FromQuery] se ignora.
  • La propiedad Breed no se rellena desde un parámetro de cadena de consulta.

Los formateadores de entrada solo leen el cuerpo y no entienden los atributos de origen de enlace. Si se encuentra un valor adecuado en el cuerpo, ese valor se usa para rellenar la propiedad Breed.

No aplique [FromBody] a más de un parámetro por método de acción. Una vez que un formateador de entrada ha leído la secuencia de solicitudes, deja de estar disponible para una nueva lectura con el fin de enlazar otros parámetros [FromBody].

Orígenes adicionales

Los proveedores de valores proporcionan datos de origen al sistema de enlace de modelos. Puede escribir y registrar proveedores de valores personalizados que obtienen datos de otros orígenes para el enlace de modelos. Por ejemplo, es posible que le interesen datos de cookies o del estado de sesión. Para obtener datos desde un origen nuevo:

  • Cree una clase que implemente IValueProvider.
  • Cree una clase que implemente IValueProviderFactory.
  • Registre la clase de generador en Program.cs.

El ejemplo incluye un proveedor de valores y un generador que obtiene valores de cookies. Registre generadores de proveedores de valores personalizados en Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

En el código siguiente, el proveedor de valor personalizado se coloca después de todos los proveedores de valor integrados. Para que sea el primero en la lista, llame a Insert(0, new CookieValueProviderFactory()) en lugar de a Add.

No hay origen para una propiedad de modelo

De forma predeterminada, si no se encuentra ningún valor para una propiedad de modelo no se crea un error de estado del modelo. La propiedad se establece en NULL o en un valor predeterminado:

  • Los tipos simples que aceptan valores NULL se establecen en null.
  • Los tipos de valor que no aceptan valores NULL se establecen en default(T). Por ejemplo, un parámetro int id se establece en 0.
  • Para los tipos complejos, el enlace de modelos crea una instancia mediante el constructor predeterminado, sin establecer propiedades.
  • Las matrices se establecen en Array.Empty<T>(), salvo las matrices byte[], que se establecen en null.

Si el estado del modelo se debe invalidar cuando no se encuentra nada en los campos de formulario para una propiedad de modelo, use el atributo [BindRequired].

Tenga en cuenta que este comportamiento de [BindRequired] se aplica al enlace de modelos desde datos de formulario publicados, no a los datos JSON o XML del cuerpo de una solicitud. Los datos del cuerpo de la solicitud se controlan mediante formateadores de entrada.

Errores de la conversión de tipos

Si se encuentra un origen pero no se puede convertir al tipo de destino, el estado del modelo se marca como no válido. El parámetro o la propiedad de destino se establece en NULL o en un valor predeterminado, como se ha indicado en la sección anterior.

En un controlador de API que tenga el atributo [ApiController], el estado de modelo no válido genera una respuesta HTTP 400 automática.

En una página de Razor, se vuelve a mostrar la página con un mensaje de error:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Cuando el código anterior vuelve a mostrar la página, no se muestra la entrada no válida en el campo de formulario. El motivo es que la propiedad de modelo se ha establecido en NULL o en un valor predeterminado. La entrada no válida sí aparece en un mensaje de error. Si quiere volver a mostrar los datos incorrectos en el campo de formulario, considere la posibilidad de convertir la propiedad de modelo en una cadena y realizar la conversión de datos de forma manual.

Se recomienda la misma estrategia si no quiere que los errores de conversión de tipo generen errores de estado de modelo. En ese caso, convierta la propiedad de modelo en una cadena.

Tipos simples

Consulte Tipos simples y complejos de enlace de modelos para obtener una explicación de tipos simples y complejos.

Los tipos simples a los que el enlazador de modelos puede convertir las cadenas de origen incluyen los siguientes:

Enlazar con IParsable<T>.TryParse

La API IParsable<TSelf>.TryParse admite el enlace de valores de parámetros de acción del controlador:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

La siguiente clase DateRange implementa IParsable<TSelf> para admitir el enlace de un intervalo de fechas:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

El código anterior:

  • Convierte una cadena que representa dos fechas en un objeto DateRange
  • El enlazador de modelos usa el método IParsable<TSelf>.TryParse para enlazar DateRange.

La siguiente acción del controlador usa la clase DateRange para enlazar un intervalo de fechas:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

La siguiente clase Locale implementa IParsable<TSelf> para admitir el enlace de CultureInfo:

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

La siguiente acción del controlador usa la clase Locale para enlazar una cadena CultureInfo:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

La siguiente acción del controlador usa la clase DateRange y Locale para enlazar un intervalo de fechas con CultureInfo:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

La aplicación de ejemplo de API en GitHub muestra el ejemplo anterior para un controlador de API.

Enlazar con TryParse

La API TryParse admite el enlace de valores de parámetros de acción del controlador:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse es el enfoque recomendado para el enlace de parámetros porque, a diferencia de TryParse, no depende de la reflexión.

La siguiente clase DateRangeTP implementa TryParse:

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

La siguiente acción del controlador usa la clase DateRangeTP para enlazar un intervalo de fechas:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Tipos complejos

Un tipo complejo debe tener un constructor público predeterminado y propiedades grabables públicas para enlazar. Cuando se produce el enlace de modelos, se crea una instancia de la clase con el constructor predeterminado público.

Para cada propiedad del tipo complejo, el enlace de modelos busca entre los orígenes el patrón de nombre prefijo.nombre_de_propiedad. Si no se encuentra nada, solo busca nombre_de_propiedad sin el prefijo. La decisión de usar el prefijo no se realiza por propiedad. Por ejemplo, con una consulta que contiene ?Instructor.Id=100&Name=foo, enlazada al método OnGet(Instructor instructor), el objeto resultante de tipo Instructor contiene:

  • Id se establece en 100.
  • Name se establece en null. El enlace de modelos espera Instructor.Name porque Instructor.Id se usó en el parámetro de consulta anterior.

Para el enlace a un parámetro, el prefijo es el nombre del parámetro. Para el enlace a una propiedad pública PageModel, el prefijo es el nombre de la propiedad pública. Algunos atributos tienen una propiedad Prefix que permite invalidar el uso predeterminado del nombre de parámetro o propiedad.

Por ejemplo, imagine que el tipo complejo es la clase Instructor siguiente:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefijo = nombre del parámetro

Si el modelo que se va a enlazar es un parámetro denominado instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

El enlace de modelos se inicia mediante el examen de los orígenes para la clave instructorToUpdate.ID. Si no se encuentra, busca ID sin un prefijo.

Prefijo = nombre de propiedad

Si el modelo que se va a enlazar es una propiedad denominada Instructor del controlador o la clase PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

El enlace de modelos se inicia mediante el examen de los orígenes para la clave Instructor.ID. Si no se encuentra, busca ID sin un prefijo.

Prefijo personalizado

Si el modelo que se va a enlazar es un parámetro denominado instructorToUpdate y un atributo Bind, especifica Instructor como el prefijo:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

El enlace de modelos se inicia mediante el examen de los orígenes para la clave Instructor.ID. Si no se encuentra, busca ID sin un prefijo.

Atributos para destinos de tipo complejo

Existen varios atributos integrados para controlar el enlace de modelos de tipos complejos:

Advertencia

Estos atributos afectan al enlace de modelos cuando el origen de los valores son datos de formulario publicados. No afectan a los formateadores de entrada, que procesan los cuerpos de la solicitud JSON y XML publicados. Los formateadores de entrada se explican más adelante en este artículo.

Atributo [Bind]

Se puede aplicar a una clase o un parámetro de método. Especifica qué propiedades de un modelo se deben incluir en el enlace de modelos. [Bind]no afecta a los formateadores de entrada.

En el ejemplo siguiente, solo se enlazan las propiedades especificadas del modelo Instructor cuando se llama a cualquier método de acción o controlador:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

En el ejemplo siguiente, solo se enlazan las propiedades especificadas del modelo Instructor cuando se llama al método OnPost:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

El atributo [Bind] se puede usar para protegerse de la publicación excesiva en escenarios de creación. No funciona bien en escenarios de edición porque las propiedades excluidas se establecen en NULL o en un valor predeterminado en lugar de mantenerse sin cambios. Para defenderse de la publicación excesiva, se recomiendan modelos de vista en lugar del atributo [Bind]. Para más información, vea Nota de seguridad sobre la publicación excesiva.

Atributo [ModelBinder]

ModelBinderAttribute se puede aplicar a tipos, propiedades o parámetros. Permite especificar el tipo de enlazador de modelos utilizado para enlazar la instancia o el tipo específicos. Por ejemplo:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder<MyInstructorModelBinder>] Instructor instructor)

El atributo [ModelBinder] también se puede usar para cambiar el nombre de una propiedad o parámetro cuando se enlaza al modelo:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Atributo [BindRequired]

Hace que el enlace de modelos agregue un error de estado de modelo si no se puede realizar el enlace para la propiedad de un modelo. Este es un ejemplo:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Vea también la explicación del atributo [Required] en Validación de modelos.

Atributo [BindNever]

Se puede aplicar a una propiedad o a un tipo. Impide que el enlace de modelos establezca la propiedad de un modelo. Cuando se aplica a un tipo, el sistema de enlace de modelos excluye todas las propiedades que define el tipo. Este es un ejemplo:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Colecciones

Para los destinos que son colecciones de tipos simples, el enlace de modelos busca coincidencias con nombre_de_parámetro o nombre_de_propiedad. Si no se encuentra ninguna coincidencia, busca uno de los formatos admitidos sin el prefijo. Por ejemplo:

  • Imagine que el parámetro que se va a enlazar es una matriz llamada selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Los datos de cadena de consulta o formulario pueden estar en uno de los formatos siguientes:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evite enlazar un parámetro o una propiedad denominada index o Index si está adyacente a un valor de colección. El enlace de modelos intenta usar index como índice para la colección, lo que podría dar lugar a un enlace incorrecto. Considere, por ejemplo, las siguientes acciones:

    public IActionResult Post(string index, List<Product> products)
    

    En el código anterior, el parámetro de cadena de consulta index se enlaza al parámetro de método index y también se usa para enlazar la colección de productos. Cambiar el nombre del parámetro index o usar un atributo de enlace de modelos para configurar el enlace evita este problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • El formato siguiente solo se admite en datos de formulario:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Para todos los formatos de ejemplo anteriores, el enlace de modelos pasa una matriz de dos elementos al parámetro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Los formatos de datos que usan números de subíndice (... [0] ... [1] ...) deben asegurarse de que se numeran de forma secuencial a partir de cero. Si hay algún hueco en la numeración de los subíndices, se omiten todos los elementos que aparecen después del hueco. Por ejemplo, si los subíndices son 0 y 2 en lugar de 0 y 1, se omite el segundo elemento.

Diccionarios

Para los destinos Dictionary, el enlace de modelos busca coincidencias con nombre_de_parámetro o nombre_de_propiedad. Si no se encuentra ninguna coincidencia, busca uno de los formatos admitidos sin el prefijo. Por ejemplo:

  • Imagine que el parámetro de destino es un elemento Dictionary<int, string> denominado selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Los datos de cadena de consulta o de formulario publicados pueden ser similares a uno de los ejemplos siguientes:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Para todos los formatos de ejemplo anteriores, el enlace de modelos pasa un diccionario de dos elementos al parámetro selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Tipos de registro y enlace de constructores

El enlace de modelos requiere que los tipos complejos tengan un constructor sin parámetros. Tanto en System.Text.Json como en Newtonsoft.Json los formateadores de entrada basados admiten la deserialización de clases que no tienen un constructor sin parámetros.

Los tipos de registro son un modo fantástico de representar más concisamente datos en la red. ASP.NET Core admite el enlace de modelos y la validación de tipos de registro con un único constructor:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

Al validar los tipos de registro, el tiempo de ejecución busca metadatos de enlace y validación específicamente en parámetros en lugar de en propiedades.

El marco permite enlazar y validar tipos de registro:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Para que funcione el anterior, el tipo debe:

  • Ser un tipo de registro.
  • Tener exactamente un constructor público.
  • Contener parámetros que tienen una propiedad con el mismo nombre y tipo. Los nombres no deben diferir entre mayúsculas y minúsculas.

POCO sin constructores carentes de parámetros

Los POCO que no tienen constructores sin parámetros no se pueden enlazar.

El código siguiente da como resultado una excepción que indica que el tipo debe tener un constructor sin parámetros:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Tipos de registro con constructores creados manualmente

Los tipos de registro con constructores creados manualmente que parecen los constructores principales funcionan

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipos de registro, validación y metadatos de enlace

En el caso de los tipos de registro, se usan metadatos de validación y enlace en parámetros. Se omiten los metadatos de las propiedades

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validación y metadatos

La validación usa metadatos en el parámetro, pero usa la propiedad para leer el valor. En el caso normal con constructores principales, los dos serían idénticos. Sin embargo, hay maneras de derrotarlo:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel no actualiza parámetros en un tipo de registro

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

En este caso, MVC no intentará enlazarse Name de nuevo. Sin embargo, Age se puede actualizar

Comportamiento de globalización del enlace de modelos datos de ruta y cadenas de consulta

El proveedor de valores de ruta de ASP.NET Core y el proveedor de valores de cadena de consulta:

  • Tratan los valores como referencia cultural de todos los idiomas.
  • Esperan que las direcciones URL sean independientes de la referencia cultural.

Por el contrario, los valores procedentes de datos de formulario se someten a una conversión que tiene en cuenta la referencia cultural. Esto es así por diseño, para que las direcciones URL se puedan compartir entre configuraciones regionales.

Para que el proveedor de valores de ruta de ASP.NET Core y el proveedor de valores de cadena de consulta se sometan a una conversión dependiente de la referencia cultural:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipos de datos especiales

Hay algunos tipos de datos especiales que el enlace de modelos puede controlar.

IFormFile e IFormFileCollection

Un archivo cargado incluido en la solicitud HTTP. También se admite IEnumerable<IFormFile> para varios archivos.

CancellationToken

Las acciones pueden enlazar opcionalmente CancellationToken como un parámetro. Esto enlaza RequestAborted que indica cuándo se anula la conexión subyacente a la solicitud HTTP. Las acciones pueden usar este parámetro para cancelar las operaciones asincrónicas de larga duración que se ejecutan como parte de las acciones del controlador.

FormCollection

Se usa para recuperar todos los valores de los datos de formulario publicados.

Formateadores de entrada

Los datos del cuerpo de la solicitud pueden estar en XML, JSON u otro formato. Para analizar estos datos, el enlace de modelos usa un formateador de entrada configurado para controlar un tipo de contenido determinado. De forma predeterminada, en ASP.NET Core se incluyen formateadores de entrada basados en JSON para el control de los datos JSON. Puede agregar otros formateadores para otros tipos de contenido.

ASP.NET Core selecciona los formateadores de entrada en función del atributo Consumes. Si no hay ningún atributo, usa el encabezado Content-Type.

Para usar los formateadores de entrada XML integrados:

Personalización del enlace de modelos con formateadores de entrada

Un formateador de entrada asume toda la responsabilidad de leer datos del cuerpo de la solicitud. Para personalizar este proceso, configure las API que usa el formateador de entrada. En esta sección se describe cómo personalizar el formateador de entrada basado en System.Text.Json para entender un tipo personalizado denominado ObjectId.

Considere el modelo siguiente, que contiene una propiedad ObjectId:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Para personalizar el proceso de enlace de modelos al usar System.Text.Json, cree una clase derivada de JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Para usar un convertidor personalizado, aplique el atributo JsonConverterAttribute al tipo. En el ejemplo siguiente, el tipo ObjectId se configura con ObjectIdConverter como su convertidor personalizado:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Para más información, consulte Procedimientos para escribir convertidores personalizados.

Exclusión de tipos especificados del enlace de modelos

El comportamiento del enlace de modelos y los sistemas de validación se controla mediante ModelMetadata. Puede personalizar ModelMetadata mediante la adición de un proveedor de detalles a MvcOptions.ModelMetadataDetailsProviders. Los proveedores de detalles integrados están disponibles para deshabilitar el enlace de modelos o la validación para tipos especificados.

Para deshabilitar el enlace de modelos en todos los modelos de un tipo especificado, agregue una instancia de ExcludeBindingMetadataProvider en Program.cs. Por ejemplo, para deshabilitar el enlace de modelos en todos los modelos del tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Para deshabilitar la validación en propiedades de un tipo especificado, agregue una instancia de SuppressChildValidationMetadataProvider en Program.cs. Por ejemplo, para deshabilitar la validación en las propiedades de tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Enlazadores de modelos personalizados

Puede ampliar el enlace de modelos si escribe un enlazador de modelos personalizado y usa el atributo [ModelBinder] para seleccionarlo para un destino concreto. Más información sobre el enlace de modelos personalizados.

Enlace de modelos manual

El enlace de modelos se puede invocar de forma manual mediante el método TryUpdateModelAsync. El método se define en las clases ControllerBase y PageModel. Las sobrecargas de método permiten especificar el prefijo y el proveedor de valores que se van a usar. El método devuelve false si se produce un error en el enlace de modelos. Este es un ejemplo:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa proveedores de valor para obtener datos del cuerpo del formulario, la cadena de consulta y los datos de ruta. TryUpdateModelAsync suele ser:

  • Se usa con aplicaciones de Razor Pages y MVC con controladores y vistas para evitar el exceso de publicación.
  • No se usa con una API web a menos que se consuma a partir de datos de formulario, cadenas de consulta y datos de ruta. Los puntos de conexión de la API web que consumen JSON usan formateadores de entrada para deserializar el cuerpo de la solicitud en un objeto.

Para más información, consulte TryUpdateModelAsync.

Atributo [FromServices]

El nombre de este atributo sigue el patrón de los atributos de enlace de modelos que especifican un origen de datos. Pero no se trata de enlazar datos desde un proveedor de valores. Obtiene una instancia de un tipo desde el contenedor de inserción de dependencias. Su objetivo es proporcionar una alternativa a la inserción de constructores cuando se necesita un servicio solo si se llama a un método concreto.

Si una instancia del tipo no está registrada en el contenedor de inserción de dependencias, la aplicación produce una excepción al intentar enlazar el parámetro. Para que el parámetro sea opcional, use uno de los métodos siguientes:

  • Hacer que el parámetro acepte valores NULL.
  • Establezca un valor predeterminado para el parámetro.

Para los parámetros que aceptan valores NULL, asegúrese de que el parámetro no sea null antes de acceder a él.

Recursos adicionales

En este artículo se explica qué es el enlace de modelos, cómo funciona y cómo personalizar su comportamiento.

Qué es el enlace de modelos

Los controladores y lasRazor pages trabajan con datos que provienen de solicitudes HTTP. Por ejemplo, los datos de ruta pueden proporcionar una clave de registro y los campos de formulario publicados pueden proporcionar valores para las propiedades del modelo. La escritura de código para recuperar cada uno de estos valores y convertirlos de cadenas a tipos de .NET sería tediosa y propensa a errores. El enlace de modelos automatiza este proceso. El sistema de enlace de modelos:

  • Recupera datos de diversos orígenes, como datos de ruta, campos de formulario y cadenas de consulta.
  • Proporciona los datos a los controladores y Razor pages en parámetros de método y propiedades públicas.
  • Convierte datos de cadena en tipos de .NET.
  • Actualiza las propiedades de tipos complejos.

Ejemplo

Imagine que tiene el siguiente método de acción:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

Y que la aplicación recibe una solicitud con esta dirección URL:

https://contoso.com/api/pets/2?DogsOnly=true

El enlace de modelos realiza los pasos siguientes después de que el sistema de enrutamiento selecciona el método de acción:

  • Busca el primer parámetro de GetById, un entero denominado id.
  • Examina los orígenes disponibles en la solicitud HTTP y busca id = "2" en los datos de ruta.
  • Convierte la cadena "2" en el entero 2.
  • Busca el siguiente parámetro de GetById, un valor booleano denominado dogsOnly.
  • Examina los orígenes y busca "DogsOnly=true" en la cadena de consulta. La coincidencia de nombres no distingue mayúsculas de minúsculas.
  • Convierte la cadena "true" en un valor booleano true.

Después, el marco llama al método GetById y pasa 2 para el parámetro id y true para el parámetro dogsOnly.

En el ejemplo anterior, los destinos de enlace de modelos son parámetros de método que son tipos simples. Los destinos también pueden ser las propiedades de un tipo complejo. Después de que cada propiedad se haya enlazado correctamente, se produce la validación de modelos para esa propiedad. El registro de qué datos se han enlazado al modelo y los errores de enlace o validación se almacenan en ControllerBase.ModelState o PageModel.ModelState. Para averiguar si este proceso se ha realizado correctamente, la aplicación comprueba la marca ModelState.IsValid.

Targets

El enlace de modelos intenta encontrar valores para los tipos de destinos siguientes:

  • Parámetros del método de acción de controlador al que se enruta una solicitud.
  • Parámetros del método de administración de Razor páginas al que se enruta una solicitud.
  • Propiedades públicas de un controlador o una clase PageModel, si se especifican mediante atributos.

Atributo [BindProperty]

Se puede aplicar a una propiedad pública de un controlador o una clase PageModel para hacer que el enlace de modelos tenga esa propiedad como destino:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Atributo [BindProperties]

Se pueden aplicar a un controlador o una clase PageModel para indicar al enlace de modelos que seleccione como destino todas las propiedades públicas de la clase:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Enlace de modelos para solicitudes HTTP GET

De forma predeterminada, las propiedades no se enlazan para las solicitudes HTTP GET. Normalmente, todo lo que necesita para una solicitud GET es un parámetro de id. de registro. El id. de registro se usa para buscar el elemento en la base de datos. Por tanto, no es necesario enlazar una propiedad que contiene una instancia del modelo. En escenarios donde quiera propiedades enlazadas a datos de las solicitudes GET, establezca la propiedad SupportsGet en true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

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 conversión de string a SomeType que no requiera recursos externos ni varias entradas.

Orígenes

De forma predeterminada, el enlace de modelos obtiene datos en forma de pares clave-valor de los siguientes orígenes de una solicitud HTTP:

  1. Campos de formulario
  2. El cuerpo de la solicitud (para controladores que tienen el atributo [ApiController]).
  3. Datos de ruta
  4. Parámetros de cadena de consulta
  5. Archivos cargados

Para cada parámetro o propiedad de destino, se examinan los orígenes en el orden indicado en la lista anterior. Hay algunas excepciones:

  • Los datos de ruta y los valores de cadena de consulta solo se usan para tipos simples.
  • Los archivos cargados solo se enlazan a tipos de destino que implementan IFormFile o IEnumerable<IFormFile>.

Si el origen predeterminado no es correcto, use uno de los atributos siguientes para especificar el origen:

  • [FromQuery]: obtiene valores de la cadena de consulta.
  • [FromRoute]: obtiene valores de los datos de ruta.
  • [FromForm]: obtiene valores de los campos de formulario publicados.
  • [FromBody]: obtiene valores del cuerpo de la solicitud.
  • [FromHeader]: obtiene valores de encabezados HTTP.

Estos atributos:

  • Se agregan de forma individual a las propiedades del modelo y no a la clase de modelo, como en el ejemplo siguiente:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Opcionalmente, acepte un valor de nombre de modelo en el constructor. Esta opción se proporciona en caso de que el nombre de la propiedad no coincida con el valor de la solicitud. Por ejemplo, es posible que el valor de la solicitud sea un encabezado con un guion en el nombre, como en el ejemplo siguiente:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Atributo [FromBody]

Aplique el atributo [FromBody] a un parámetro para rellenar sus propiedades desde el cuerpo de una solicitud HTTP. El runtime de ASP.NET Core delega la responsabilidad de leer el cuerpo al formateador de entrada. Los formateadores de entrada se explican más adelante en este artículo.

Cuando se aplica [FromBody] a un parámetro de tipo complejo, se omiten los atributos de origen de enlace aplicados a sus propiedades. Por ejemplo, la acción Create siguiente especifica que su parámetro pet se rellena a partir del cuerpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

La clase Pet especifica que su propiedad Breed se rellena a partir de un parámetro de cadena de consulta:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

En el ejemplo anterior:

  • El atributo [FromQuery] se ignora.
  • La propiedad Breed no se rellena desde un parámetro de cadena de consulta.

Los formateadores de entrada solo leen el cuerpo y no entienden los atributos de origen de enlace. Si se encuentra un valor adecuado en el cuerpo, ese valor se usa para rellenar la propiedad Breed.

No aplique [FromBody] a más de un parámetro por método de acción. Una vez que un formateador de entrada ha leído la secuencia de solicitudes, deja de estar disponible para una nueva lectura con el fin de enlazar otros parámetros [FromBody].

Orígenes adicionales

Los proveedores de valores proporcionan datos de origen al sistema de enlace de modelos. Puede escribir y registrar proveedores de valores personalizados que obtienen datos de otros orígenes para el enlace de modelos. Por ejemplo, es posible que le interesen datos de cookies o del estado de sesión. Para obtener datos desde un origen nuevo:

  • Cree una clase que implemente IValueProvider.
  • Cree una clase que implemente IValueProviderFactory.
  • Registre la clase de generador en Program.cs.

El ejemplo incluye un proveedor de valores y un generador que obtiene valores de cookies. Registre generadores de proveedores de valores personalizados en Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

En el código siguiente, el proveedor de valor personalizado se coloca después de todos los proveedores de valor integrados. Para que sea el primero en la lista, llame a Insert(0, new CookieValueProviderFactory()) en lugar de a Add.

No hay origen para una propiedad de modelo

De forma predeterminada, si no se encuentra ningún valor para una propiedad de modelo no se crea un error de estado del modelo. La propiedad se establece en NULL o en un valor predeterminado:

  • Los tipos simples que aceptan valores NULL se establecen en null.
  • Los tipos de valor que no aceptan valores NULL se establecen en default(T). Por ejemplo, un parámetro int id se establece en 0.
  • Para los tipos complejos, el enlace de modelos crea una instancia mediante el constructor predeterminado, sin establecer propiedades.
  • Las matrices se establecen en Array.Empty<T>(), salvo las matrices byte[], que se establecen en null.

Si el estado del modelo se debe invalidar cuando no se encuentra nada en los campos de formulario para una propiedad de modelo, use el atributo [BindRequired].

Tenga en cuenta que este comportamiento de [BindRequired] se aplica al enlace de modelos desde datos de formulario publicados, no a los datos JSON o XML del cuerpo de una solicitud. Los datos del cuerpo de la solicitud se controlan mediante formateadores de entrada.

Errores de la conversión de tipos

Si se encuentra un origen pero no se puede convertir al tipo de destino, el estado del modelo se marca como no válido. El parámetro o la propiedad de destino se establece en NULL o en un valor predeterminado, como se ha indicado en la sección anterior.

En un controlador de API que tenga el atributo [ApiController], el estado de modelo no válido genera una respuesta HTTP 400 automática.

En una página de Razor, se vuelve a mostrar la página con un mensaje de error:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Cuando el código anterior vuelve a mostrar la página, no se muestra la entrada no válida en el campo de formulario. El motivo es que la propiedad de modelo se ha establecido en NULL o en un valor predeterminado. La entrada no válida sí aparece en un mensaje de error. Si quiere volver a mostrar los datos incorrectos en el campo de formulario, considere la posibilidad de convertir la propiedad de modelo en una cadena y realizar la conversión de datos de forma manual.

Se recomienda la misma estrategia si no quiere que los errores de conversión de tipo generen errores de estado de modelo. En ese caso, convierta la propiedad de modelo en una cadena.

Tipos simples

Consulte Tipos simples y complejos de enlace de modelos para obtener una explicación de tipos simples y complejos.

Los tipos simples a los que el enlazador de modelos puede convertir las cadenas de origen incluyen los siguientes:

Enlazar con IParsable<T>.TryParse

La API IParsable<TSelf>.TryParse admite el enlace de valores de parámetros de acción del controlador:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

La siguiente clase DateRange implementa IParsable<TSelf> para admitir el enlace de un intervalo de fechas:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

El código anterior:

  • Convierte una cadena que representa dos fechas en un objeto DateRange
  • El enlazador de modelos usa el método IParsable<TSelf>.TryParse para enlazar DateRange.

La siguiente acción del controlador usa la clase DateRange para enlazar un intervalo de fechas:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

La siguiente clase Locale implementa IParsable<TSelf> para admitir el enlace de CultureInfo:

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

La siguiente acción del controlador usa la clase Locale para enlazar una cadena CultureInfo:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

La siguiente acción del controlador usa la clase DateRange y Locale para enlazar un intervalo de fechas con CultureInfo:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

La aplicación de ejemplo de API en GitHub muestra el ejemplo anterior para un controlador de API.

Enlazar con TryParse

La API TryParse admite el enlace de valores de parámetros de acción del controlador:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse es el enfoque recomendado para el enlace de parámetros porque, a diferencia de TryParse, no depende de la reflexión.

La siguiente clase DateRangeTP implementa TryParse:

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

La siguiente acción del controlador usa la clase DateRangeTP para enlazar un intervalo de fechas:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Tipos complejos

Un tipo complejo debe tener un constructor público predeterminado y propiedades grabables públicas para enlazar. Cuando se produce el enlace de modelos, se crea una instancia de la clase con el constructor predeterminado público.

Para cada propiedad del tipo complejo, el enlace de modelos busca entre los orígenes el patrón de nombre prefijo.nombre_de_propiedad. Si no se encuentra nada, solo busca nombre_de_propiedad sin el prefijo. La decisión de usar el prefijo no se realiza por propiedad. Por ejemplo, con una consulta que contiene ?Instructor.Id=100&Name=foo, enlazada al método OnGet(Instructor instructor), el objeto resultante de tipo Instructor contiene:

  • Id se establece en 100.
  • Name se establece en null. El enlace de modelos espera Instructor.Name porque Instructor.Id se usó en el parámetro de consulta anterior.

Para el enlace a un parámetro, el prefijo es el nombre del parámetro. Para el enlace a una propiedad pública PageModel, el prefijo es el nombre de la propiedad pública. Algunos atributos tienen una propiedad Prefix que permite invalidar el uso predeterminado del nombre de parámetro o propiedad.

Por ejemplo, imagine que el tipo complejo es la clase Instructor siguiente:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefijo = nombre del parámetro

Si el modelo que se va a enlazar es un parámetro denominado instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

El enlace de modelos se inicia mediante el examen de los orígenes para la clave instructorToUpdate.ID. Si no se encuentra, busca ID sin un prefijo.

Prefijo = nombre de propiedad

Si el modelo que se va a enlazar es una propiedad denominada Instructor del controlador o la clase PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

El enlace de modelos se inicia mediante el examen de los orígenes para la clave Instructor.ID. Si no se encuentra, busca ID sin un prefijo.

Prefijo personalizado

Si el modelo que se va a enlazar es un parámetro denominado instructorToUpdate y un atributo Bind, especifica Instructor como el prefijo:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

El enlace de modelos se inicia mediante el examen de los orígenes para la clave Instructor.ID. Si no se encuentra, busca ID sin un prefijo.

Atributos para destinos de tipo complejo

Existen varios atributos integrados para controlar el enlace de modelos de tipos complejos:

Advertencia

Estos atributos afectan al enlace de modelos cuando el origen de los valores son datos de formulario publicados. No afectan a los formateadores de entrada, que procesan los cuerpos de la solicitud JSON y XML publicados. Los formateadores de entrada se explican más adelante en este artículo.

Atributo [Bind]

Se puede aplicar a una clase o un parámetro de método. Especifica qué propiedades de un modelo se deben incluir en el enlace de modelos. [Bind]no afecta a los formateadores de entrada.

En el ejemplo siguiente, solo se enlazan las propiedades especificadas del modelo Instructor cuando se llama a cualquier método de acción o controlador:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

En el ejemplo siguiente, solo se enlazan las propiedades especificadas del modelo Instructor cuando se llama al método OnPost:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

El atributo [Bind] se puede usar para protegerse de la publicación excesiva en escenarios de creación. No funciona bien en escenarios de edición porque las propiedades excluidas se establecen en NULL o en un valor predeterminado en lugar de mantenerse sin cambios. Para defenderse de la publicación excesiva, se recomiendan modelos de vista en lugar del atributo [Bind]. Para más información, vea Nota de seguridad sobre la publicación excesiva.

Atributo [ModelBinder]

ModelBinderAttribute se puede aplicar a tipos, propiedades o parámetros. Permite especificar el tipo de enlazador de modelos utilizado para enlazar la instancia o el tipo específicos. Por ejemplo:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

El atributo [ModelBinder] también se puede usar para cambiar el nombre de una propiedad o parámetro cuando se enlaza al modelo:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Atributo [BindRequired]

Hace que el enlace de modelos agregue un error de estado de modelo si no se puede realizar el enlace para la propiedad de un modelo. Este es un ejemplo:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Vea también la explicación del atributo [Required] en Validación de modelos.

Atributo [BindNever]

Se puede aplicar a una propiedad o a un tipo. Impide que el enlace de modelos establezca la propiedad de un modelo. Cuando se aplica a un tipo, el sistema de enlace de modelos excluye todas las propiedades que define el tipo. Este es un ejemplo:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Colecciones

Para los destinos que son colecciones de tipos simples, el enlace de modelos busca coincidencias con nombre_de_parámetro o nombre_de_propiedad. Si no se encuentra ninguna coincidencia, busca uno de los formatos admitidos sin el prefijo. Por ejemplo:

  • Imagine que el parámetro que se va a enlazar es una matriz llamada selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Los datos de cadena de consulta o formulario pueden estar en uno de los formatos siguientes:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evite enlazar un parámetro o una propiedad denominada index o Index si está adyacente a un valor de colección. El enlace de modelos intenta usar index como índice para la colección, lo que podría dar lugar a un enlace incorrecto. Considere, por ejemplo, las siguientes acciones:

    public IActionResult Post(string index, List<Product> products)
    

    En el código anterior, el parámetro de cadena de consulta index se enlaza al parámetro de método index y también se usa para enlazar la colección de productos. Cambiar el nombre del parámetro index o usar un atributo de enlace de modelos para configurar el enlace evita este problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • El formato siguiente solo se admite en datos de formulario:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Para todos los formatos de ejemplo anteriores, el enlace de modelos pasa una matriz de dos elementos al parámetro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Los formatos de datos que usan números de subíndice (... [0] ... [1] ...) deben asegurarse de que se numeran de forma secuencial a partir de cero. Si hay algún hueco en la numeración de los subíndices, se omiten todos los elementos que aparecen después del hueco. Por ejemplo, si los subíndices son 0 y 2 en lugar de 0 y 1, se omite el segundo elemento.

Diccionarios

Para los destinos Dictionary, el enlace de modelos busca coincidencias con nombre_de_parámetro o nombre_de_propiedad. Si no se encuentra ninguna coincidencia, busca uno de los formatos admitidos sin el prefijo. Por ejemplo:

  • Imagine que el parámetro de destino es un elemento Dictionary<int, string> denominado selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Los datos de cadena de consulta o de formulario publicados pueden ser similares a uno de los ejemplos siguientes:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Para todos los formatos de ejemplo anteriores, el enlace de modelos pasa un diccionario de dos elementos al parámetro selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Tipos de registro y enlace de constructores

El enlace de modelos requiere que los tipos complejos tengan un constructor sin parámetros. Tanto en System.Text.Json como en Newtonsoft.Json los formateadores de entrada basados admiten la deserialización de clases que no tienen un constructor sin parámetros.

Los tipos de registro son un modo fantástico de representar más concisamente datos en la red. ASP.NET Core admite el enlace de modelos y la validación de tipos de registro con un único constructor:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

Al validar los tipos de registro, el tiempo de ejecución busca metadatos de enlace y validación específicamente en parámetros en lugar de en propiedades.

El marco permite enlazar y validar tipos de registro:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Para que funcione el anterior, el tipo debe:

  • Ser un tipo de registro.
  • Tener exactamente un constructor público.
  • Contener parámetros que tienen una propiedad con el mismo nombre y tipo. Los nombres no deben diferir entre mayúsculas y minúsculas.

POCO sin constructores carentes de parámetros

Los POCO que no tienen constructores sin parámetros no se pueden enlazar.

El código siguiente da como resultado una excepción que indica que el tipo debe tener un constructor sin parámetros:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Tipos de registro con constructores creados manualmente

Los tipos de registro con constructores creados manualmente que parecen los constructores principales funcionan

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipos de registro, validación y metadatos de enlace

En el caso de los tipos de registro, se usan metadatos de validación y enlace en parámetros. Se omiten los metadatos de las propiedades

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validación y metadatos

La validación usa metadatos en el parámetro, pero usa la propiedad para leer el valor. En el caso normal con constructores principales, los dos serían idénticos. Sin embargo, hay maneras de derrotarlo:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel no actualiza parámetros en un tipo de registro

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

En este caso, MVC no intentará enlazarse Name de nuevo. Sin embargo, Age se puede actualizar

Comportamiento de globalización del enlace de modelos datos de ruta y cadenas de consulta

El proveedor de valores de ruta de ASP.NET Core y el proveedor de valores de cadena de consulta:

  • Tratan los valores como referencia cultural de todos los idiomas.
  • Esperan que las direcciones URL sean independientes de la referencia cultural.

Por el contrario, los valores procedentes de datos de formulario se someten a una conversión que tiene en cuenta la referencia cultural. Esto es así por diseño, para que las direcciones URL se puedan compartir entre configuraciones regionales.

Para que el proveedor de valores de ruta de ASP.NET Core y el proveedor de valores de cadena de consulta se sometan a una conversión dependiente de la referencia cultural:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipos de datos especiales

Hay algunos tipos de datos especiales que el enlace de modelos puede controlar.

IFormFile e IFormFileCollection

Un archivo cargado incluido en la solicitud HTTP. También se admite IEnumerable<IFormFile> para varios archivos.

CancellationToken

Las acciones pueden enlazar opcionalmente CancellationToken como un parámetro. Esto enlaza RequestAborted que indica cuándo se anula la conexión subyacente a la solicitud HTTP. Las acciones pueden usar este parámetro para cancelar las operaciones asincrónicas de larga duración que se ejecutan como parte de las acciones del controlador.

FormCollection

Se usa para recuperar todos los valores de los datos de formulario publicados.

Formateadores de entrada

Los datos del cuerpo de la solicitud pueden estar en XML, JSON u otro formato. Para analizar estos datos, el enlace de modelos usa un formateador de entrada configurado para controlar un tipo de contenido determinado. De forma predeterminada, en ASP.NET Core se incluyen formateadores de entrada basados en JSON para el control de los datos JSON. Puede agregar otros formateadores para otros tipos de contenido.

ASP.NET Core selecciona los formateadores de entrada en función del atributo Consumes. Si no hay ningún atributo, usa el encabezado Content-Type.

Para usar los formateadores de entrada XML integrados:

Personalización del enlace de modelos con formateadores de entrada

Un formateador de entrada asume toda la responsabilidad de leer datos del cuerpo de la solicitud. Para personalizar este proceso, configure las API que usa el formateador de entrada. En esta sección se describe cómo personalizar el formateador de entrada basado en System.Text.Json para entender un tipo personalizado denominado ObjectId.

Considere el modelo siguiente, que contiene una propiedad ObjectId:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Para personalizar el proceso de enlace de modelos al usar System.Text.Json, cree una clase derivada de JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Para usar un convertidor personalizado, aplique el atributo JsonConverterAttribute al tipo. En el ejemplo siguiente, el tipo ObjectId se configura con ObjectIdConverter como su convertidor personalizado:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Para más información, consulte Procedimientos para escribir convertidores personalizados.

Exclusión de tipos especificados del enlace de modelos

El comportamiento del enlace de modelos y los sistemas de validación se controla mediante ModelMetadata. Puede personalizar ModelMetadata mediante la adición de un proveedor de detalles a MvcOptions.ModelMetadataDetailsProviders. Los proveedores de detalles integrados están disponibles para deshabilitar el enlace de modelos o la validación para tipos especificados.

Para deshabilitar el enlace de modelos en todos los modelos de un tipo especificado, agregue una instancia de ExcludeBindingMetadataProvider en Program.cs. Por ejemplo, para deshabilitar el enlace de modelos en todos los modelos del tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Para deshabilitar la validación en propiedades de un tipo especificado, agregue una instancia de SuppressChildValidationMetadataProvider en Program.cs. Por ejemplo, para deshabilitar la validación en las propiedades de tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Enlazadores de modelos personalizados

Puede ampliar el enlace de modelos si escribe un enlazador de modelos personalizado y usa el atributo [ModelBinder] para seleccionarlo para un destino concreto. Más información sobre el enlace de modelos personalizados.

Enlace de modelos manual

El enlace de modelos se puede invocar de forma manual mediante el método TryUpdateModelAsync. El método se define en las clases ControllerBase y PageModel. Las sobrecargas de método permiten especificar el prefijo y el proveedor de valores que se van a usar. El método devuelve false si se produce un error en el enlace de modelos. Este es un ejemplo:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa proveedores de valor para obtener datos del cuerpo del formulario, la cadena de consulta y los datos de ruta. TryUpdateModelAsync suele ser:

  • Se usa con aplicaciones de Razor Pages y MVC con controladores y vistas para evitar el exceso de publicación.
  • No se usa con una API web a menos que se consuma a partir de datos de formulario, cadenas de consulta y datos de ruta. Los puntos de conexión de la API web que consumen JSON usan formateadores de entrada para deserializar el cuerpo de la solicitud en un objeto.

Para más información, consulte TryUpdateModelAsync.

Atributo [FromServices]

El nombre de este atributo sigue el patrón de los atributos de enlace de modelos que especifican un origen de datos. Pero no se trata de enlazar datos desde un proveedor de valores. Obtiene una instancia de un tipo desde el contenedor de inserción de dependencias. Su objetivo es proporcionar una alternativa a la inserción de constructores cuando se necesita un servicio solo si se llama a un método concreto.

Si una instancia del tipo no está registrada en el contenedor de inserción de dependencias, la aplicación produce una excepción al intentar enlazar el parámetro. Para que el parámetro sea opcional, use uno de los métodos siguientes:

  • Hacer que el parámetro acepte valores NULL.
  • Establezca un valor predeterminado para el parámetro.

Para los parámetros que aceptan valores NULL, asegúrese de que el parámetro no sea null antes de acceder a él.

Recursos adicionales

En este artículo se explica qué es el enlace de modelos, cómo funciona y cómo personalizar su comportamiento.

Qué es el enlace de modelos

Los controladores y lasRazor pages trabajan con datos que provienen de solicitudes HTTP. Por ejemplo, los datos de ruta pueden proporcionar una clave de registro y los campos de formulario publicados pueden proporcionar valores para las propiedades del modelo. La escritura de código para recuperar cada uno de estos valores y convertirlos de cadenas a tipos de .NET sería tediosa y propensa a errores. El enlace de modelos automatiza este proceso. El sistema de enlace de modelos:

  • Recupera datos de diversos orígenes, como datos de ruta, campos de formulario y cadenas de consulta.
  • Proporciona los datos a los controladores y Razor pages en parámetros de método y propiedades públicas.
  • Convierte datos de cadena en tipos de .NET.
  • Actualiza las propiedades de tipos complejos.

Ejemplo

Imagine que tiene el siguiente método de acción:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

Y que la aplicación recibe una solicitud con esta dirección URL:

https://contoso.com/api/pets/2?DogsOnly=true

El enlace de modelos realiza los pasos siguientes después de que el sistema de enrutamiento selecciona el método de acción:

  • Busca el primer parámetro de GetById, un entero denominado id.
  • Examina los orígenes disponibles en la solicitud HTTP y busca id = "2" en los datos de ruta.
  • Convierte la cadena "2" en el entero 2.
  • Busca el siguiente parámetro de GetById, un valor booleano denominado dogsOnly.
  • Examina los orígenes y busca "DogsOnly=true" en la cadena de consulta. La coincidencia de nombres no distingue mayúsculas de minúsculas.
  • Convierte la cadena "true" en un valor booleano true.

Después, el marco llama al método GetById y pasa 2 para el parámetro id y true para el parámetro dogsOnly.

En el ejemplo anterior, los destinos de enlace de modelos son parámetros de método que son tipos simples. Los destinos también pueden ser las propiedades de un tipo complejo. Después de que cada propiedad se haya enlazado correctamente, se produce la validación de modelos para esa propiedad. El registro de qué datos se han enlazado al modelo y los errores de enlace o validación se almacenan en ControllerBase.ModelState o PageModel.ModelState. Para averiguar si este proceso se ha realizado correctamente, la aplicación comprueba la marca ModelState.IsValid.

Targets

El enlace de modelos intenta encontrar valores para los tipos de destinos siguientes:

  • Parámetros del método de acción de controlador al que se enruta una solicitud.
  • Parámetros del método de administración de Razor páginas al que se enruta una solicitud.
  • Propiedades públicas de un controlador o una clase PageModel, si se especifican mediante atributos.

Atributo [BindProperty]

Se puede aplicar a una propiedad pública de un controlador o una clase PageModel para hacer que el enlace de modelos tenga esa propiedad como destino:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Atributo [BindProperties]

Se pueden aplicar a un controlador o una clase PageModel para indicar al enlace de modelos que seleccione como destino todas las propiedades públicas de la clase:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Enlace de modelos para solicitudes HTTP GET

De forma predeterminada, las propiedades no se enlazan para las solicitudes HTTP GET. Normalmente, todo lo que necesita para una solicitud GET es un parámetro de id. de registro. El id. de registro se usa para buscar el elemento en la base de datos. Por tanto, no es necesario enlazar una propiedad que contiene una instancia del modelo. En escenarios donde quiera propiedades enlazadas a datos de las solicitudes GET, establezca la propiedad SupportsGet en true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

Orígenes

De forma predeterminada, el enlace de modelos obtiene datos en forma de pares clave-valor de los siguientes orígenes de una solicitud HTTP:

  1. Campos de formulario
  2. El cuerpo de la solicitud (para controladores que tienen el atributo [ApiController]).
  3. Datos de ruta
  4. Parámetros de cadena de consulta
  5. Archivos cargados

Para cada parámetro o propiedad de destino, se examinan los orígenes en el orden indicado en la lista anterior. Hay algunas excepciones:

  • Los datos de ruta y los valores de cadena de consulta solo se usan para tipos simples.
  • Los archivos cargados solo se enlazan a tipos de destino que implementan IFormFile o IEnumerable<IFormFile>.

Si el origen predeterminado no es correcto, use uno de los atributos siguientes para especificar el origen:

  • [FromQuery]: obtiene valores de la cadena de consulta.
  • [FromRoute]: obtiene valores de los datos de ruta.
  • [FromForm]: obtiene valores de los campos de formulario publicados.
  • [FromBody]: obtiene valores del cuerpo de la solicitud.
  • [FromHeader]: obtiene valores de encabezados HTTP.

Estos atributos:

  • Se agregan de forma individual a las propiedades del modelo y no a la clase de modelo, como en el ejemplo siguiente:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Opcionalmente, acepte un valor de nombre de modelo en el constructor. Esta opción se proporciona en caso de que el nombre de la propiedad no coincida con el valor de la solicitud. Por ejemplo, es posible que el valor de la solicitud sea un encabezado con un guion en el nombre, como en el ejemplo siguiente:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Atributo [FromBody]

Aplique el atributo [FromBody] a un parámetro para rellenar sus propiedades desde el cuerpo de una solicitud HTTP. El runtime de ASP.NET Core delega la responsabilidad de leer el cuerpo al formateador de entrada. Los formateadores de entrada se explican más adelante en este artículo.

Cuando se aplica [FromBody] a un parámetro de tipo complejo, se omiten los atributos de origen de enlace aplicados a sus propiedades. Por ejemplo, la acción Create siguiente especifica que su parámetro pet se rellena a partir del cuerpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

La clase Pet especifica que su propiedad Breed se rellena a partir de un parámetro de cadena de consulta:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

En el ejemplo anterior:

  • El atributo [FromQuery] se ignora.
  • La propiedad Breed no se rellena desde un parámetro de cadena de consulta.

Los formateadores de entrada solo leen el cuerpo y no entienden los atributos de origen de enlace. Si se encuentra un valor adecuado en el cuerpo, ese valor se usa para rellenar la propiedad Breed.

No aplique [FromBody] a más de un parámetro por método de acción. Una vez que un formateador de entrada ha leído la secuencia de solicitudes, deja de estar disponible para una nueva lectura con el fin de enlazar otros parámetros [FromBody].

Orígenes adicionales

Los proveedores de valores proporcionan datos de origen al sistema de enlace de modelos. Puede escribir y registrar proveedores de valores personalizados que obtienen datos de otros orígenes para el enlace de modelos. Por ejemplo, es posible que le interesen datos de cookies o del estado de sesión. Para obtener datos desde un origen nuevo:

  • Cree una clase que implemente IValueProvider.
  • Cree una clase que implemente IValueProviderFactory.
  • Registre la clase de generador en Program.cs.

El ejemplo incluye un proveedor de valores y un generador que obtiene valores de cookies. Registre generadores de proveedores de valores personalizados en Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

En el código siguiente, el proveedor de valor personalizado se coloca después de todos los proveedores de valor integrados. Para que sea el primero en la lista, llame a Insert(0, new CookieValueProviderFactory()) en lugar de a Add.

No hay origen para una propiedad de modelo

De forma predeterminada, si no se encuentra ningún valor para una propiedad de modelo no se crea un error de estado del modelo. La propiedad se establece en NULL o en un valor predeterminado:

  • Los tipos simples que aceptan valores NULL se establecen en null.
  • Los tipos de valor que no aceptan valores NULL se establecen en default(T). Por ejemplo, un parámetro int id se establece en 0.
  • Para los tipos complejos, el enlace de modelos crea una instancia mediante el constructor predeterminado, sin establecer propiedades.
  • Las matrices se establecen en Array.Empty<T>(), salvo las matrices byte[], que se establecen en null.

Si el estado del modelo se debe invalidar cuando no se encuentra nada en los campos de formulario para una propiedad de modelo, use el atributo [BindRequired].

Tenga en cuenta que este comportamiento de [BindRequired] se aplica al enlace de modelos desde datos de formulario publicados, no a los datos JSON o XML del cuerpo de una solicitud. Los datos del cuerpo de la solicitud se controlan mediante formateadores de entrada.

Errores de la conversión de tipos

Si se encuentra un origen pero no se puede convertir al tipo de destino, el estado del modelo se marca como no válido. El parámetro o la propiedad de destino se establece en NULL o en un valor predeterminado, como se ha indicado en la sección anterior.

En un controlador de API que tenga el atributo [ApiController], el estado de modelo no válido genera una respuesta HTTP 400 automática.

En una página de Razor, se vuelve a mostrar la página con un mensaje de error:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Cuando el código anterior vuelve a mostrar la página, no se muestra la entrada no válida en el campo de formulario. El motivo es que la propiedad de modelo se ha establecido en NULL o en un valor predeterminado. La entrada no válida sí aparece en un mensaje de error. Si quiere volver a mostrar los datos incorrectos en el campo de formulario, considere la posibilidad de convertir la propiedad de modelo en una cadena y realizar la conversión de datos de forma manual.

Se recomienda la misma estrategia si no quiere que los errores de conversión de tipo generen errores de estado de modelo. En ese caso, convierta la propiedad de modelo en una cadena.

Tipos simples

Los tipos simples a los que el enlazador de modelos puede convertir las cadenas de origen incluyen los siguientes:

Tipos complejos

Un tipo complejo debe tener un constructor público predeterminado y propiedades grabables públicas para enlazar. Cuando se produce el enlace de modelos, se crea una instancia de la clase con el constructor predeterminado público.

Para cada propiedad del tipo complejo, el enlace de modelos busca entre los orígenes el patrón de nombre prefijo.nombre_de_propiedad. Si no se encuentra nada, solo busca nombre_de_propiedad sin el prefijo. La decisión de usar el prefijo no se realiza por propiedad. Por ejemplo, con una consulta que contiene ?Instructor.Id=100&Name=foo, enlazada al método OnGet(Instructor instructor), el objeto resultante de tipo Instructor contiene:

  • Id se establece en 100.
  • Name se establece en null. El enlace de modelos espera Instructor.Name porque Instructor.Id se usó en el parámetro de consulta anterior.

Para el enlace a un parámetro, el prefijo es el nombre del parámetro. Para el enlace a una propiedad pública PageModel, el prefijo es el nombre de la propiedad pública. Algunos atributos tienen una propiedad Prefix que permite invalidar el uso predeterminado del nombre de parámetro o propiedad.

Por ejemplo, imagine que el tipo complejo es la clase Instructor siguiente:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefijo = nombre del parámetro

Si el modelo que se va a enlazar es un parámetro denominado instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

El enlace de modelos se inicia mediante el examen de los orígenes para la clave instructorToUpdate.ID. Si no se encuentra, busca ID sin un prefijo.

Prefijo = nombre de propiedad

Si el modelo que se va a enlazar es una propiedad denominada Instructor del controlador o la clase PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

El enlace de modelos se inicia mediante el examen de los orígenes para la clave Instructor.ID. Si no se encuentra, busca ID sin un prefijo.

Prefijo personalizado

Si el modelo que se va a enlazar es un parámetro denominado instructorToUpdate y un atributo Bind, especifica Instructor como el prefijo:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

El enlace de modelos se inicia mediante el examen de los orígenes para la clave Instructor.ID. Si no se encuentra, busca ID sin un prefijo.

Atributos para destinos de tipo complejo

Existen varios atributos integrados para controlar el enlace de modelos de tipos complejos:

Advertencia

Estos atributos afectan al enlace de modelos cuando el origen de los valores son datos de formulario publicados. No afectan a los formateadores de entrada, que procesan los cuerpos de la solicitud JSON y XML publicados. Los formateadores de entrada se explican más adelante en este artículo.

Atributo [Bind]

Se puede aplicar a una clase o un parámetro de método. Especifica qué propiedades de un modelo se deben incluir en el enlace de modelos. [Bind]no afecta a los formateadores de entrada.

En el ejemplo siguiente, solo se enlazan las propiedades especificadas del modelo Instructor cuando se llama a cualquier método de acción o controlador:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

En el ejemplo siguiente, solo se enlazan las propiedades especificadas del modelo Instructor cuando se llama al método OnPost:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

El atributo [Bind] se puede usar para protegerse de la publicación excesiva en escenarios de creación. No funciona bien en escenarios de edición porque las propiedades excluidas se establecen en NULL o en un valor predeterminado en lugar de mantenerse sin cambios. Para defenderse de la publicación excesiva, se recomiendan modelos de vista en lugar del atributo [Bind]. Para más información, vea Nota de seguridad sobre la publicación excesiva.

Atributo [ModelBinder]

ModelBinderAttribute se puede aplicar a tipos, propiedades o parámetros. Permite especificar el tipo de enlazador de modelos utilizado para enlazar la instancia o el tipo específicos. Por ejemplo:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

El atributo [ModelBinder] también se puede usar para cambiar el nombre de una propiedad o parámetro cuando se enlaza al modelo:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Atributo [BindRequired]

Hace que el enlace de modelos agregue un error de estado de modelo si no se puede realizar el enlace para la propiedad de un modelo. Este es un ejemplo:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Vea también la explicación del atributo [Required] en Validación de modelos.

Atributo [BindNever]

Se puede aplicar a una propiedad o a un tipo. Impide que el enlace de modelos establezca la propiedad de un modelo. Cuando se aplica a un tipo, el sistema de enlace de modelos excluye todas las propiedades que define el tipo. Este es un ejemplo:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Colecciones

Para los destinos que son colecciones de tipos simples, el enlace de modelos busca coincidencias con nombre_de_parámetro o nombre_de_propiedad. Si no se encuentra ninguna coincidencia, busca uno de los formatos admitidos sin el prefijo. Por ejemplo:

  • Imagine que el parámetro que se va a enlazar es una matriz llamada selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Los datos de cadena de consulta o formulario pueden estar en uno de los formatos siguientes:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evite enlazar un parámetro o una propiedad denominada index o Index si está adyacente a un valor de colección. El enlace de modelos intenta usar index como índice para la colección, lo que podría dar lugar a un enlace incorrecto. Considere, por ejemplo, las siguientes acciones:

    public IActionResult Post(string index, List<Product> products)
    

    En el código anterior, el parámetro de cadena de consulta index se enlaza al parámetro de método index y también se usa para enlazar la colección de productos. Cambiar el nombre del parámetro index o usar un atributo de enlace de modelos para configurar el enlace evita este problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • El formato siguiente solo se admite en datos de formulario:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Para todos los formatos de ejemplo anteriores, el enlace de modelos pasa una matriz de dos elementos al parámetro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Los formatos de datos que usan números de subíndice (... [0] ... [1] ...) deben asegurarse de que se numeran de forma secuencial a partir de cero. Si hay algún hueco en la numeración de los subíndices, se omiten todos los elementos que aparecen después del hueco. Por ejemplo, si los subíndices son 0 y 2 en lugar de 0 y 1, se omite el segundo elemento.

Diccionarios

Para los destinos Dictionary, el enlace de modelos busca coincidencias con nombre_de_parámetro o nombre_de_propiedad. Si no se encuentra ninguna coincidencia, busca uno de los formatos admitidos sin el prefijo. Por ejemplo:

  • Imagine que el parámetro de destino es un elemento Dictionary<int, string> denominado selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Los datos de cadena de consulta o de formulario publicados pueden ser similares a uno de los ejemplos siguientes:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Para todos los formatos de ejemplo anteriores, el enlace de modelos pasa un diccionario de dos elementos al parámetro selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Tipos de registro y enlace de constructores

El enlace de modelos requiere que los tipos complejos tengan un constructor sin parámetros. Tanto en System.Text.Json como en Newtonsoft.Json los formateadores de entrada basados admiten la deserialización de clases que no tienen un constructor sin parámetros.

Los tipos de registro son un modo fantástico de representar más concisamente datos en la red. ASP.NET Core admite el enlace de modelos y la validación de tipos de registro con un único constructor:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

Al validar los tipos de registro, el tiempo de ejecución busca metadatos de enlace y validación específicamente en parámetros en lugar de en propiedades.

El marco permite enlazar y validar tipos de registro:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Para que funcione el anterior, el tipo debe:

  • Ser un tipo de registro.
  • Tener exactamente un constructor público.
  • Contener parámetros que tienen una propiedad con el mismo nombre y tipo. Los nombres no deben diferir entre mayúsculas y minúsculas.

POCO sin constructores carentes de parámetros

Los POCO que no tienen constructores sin parámetros no se pueden enlazar.

El código siguiente da como resultado una excepción que indica que el tipo debe tener un constructor sin parámetros:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Tipos de registro con constructores creados manualmente

Los tipos de registro con constructores creados manualmente que parecen los constructores principales funcionan

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipos de registro, validación y metadatos de enlace

En el caso de los tipos de registro, se usan metadatos de validación y enlace en parámetros. Se omiten los metadatos de las propiedades

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validación y metadatos

La validación usa metadatos en el parámetro, pero usa la propiedad para leer el valor. En el caso normal con constructores principales, los dos serían idénticos. Sin embargo, hay maneras de derrotarlo:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel no actualiza parámetros en un tipo de registro

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

En este caso, MVC no intentará enlazarse Name de nuevo. Sin embargo, Age se puede actualizar

Comportamiento de globalización del enlace de modelos datos de ruta y cadenas de consulta

El proveedor de valores de ruta de ASP.NET Core y el proveedor de valores de cadena de consulta:

  • Tratan los valores como referencia cultural de todos los idiomas.
  • Esperan que las direcciones URL sean independientes de la referencia cultural.

Por el contrario, los valores procedentes de datos de formulario se someten a una conversión que tiene en cuenta la referencia cultural. Esto es así por diseño, para que las direcciones URL se puedan compartir entre configuraciones regionales.

Para que el proveedor de valores de ruta de ASP.NET Core y el proveedor de valores de cadena de consulta se sometan a una conversión dependiente de la referencia cultural:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipos de datos especiales

Hay algunos tipos de datos especiales que el enlace de modelos puede controlar.

IFormFile e IFormFileCollection

Un archivo cargado incluido en la solicitud HTTP. También se admite IEnumerable<IFormFile> para varios archivos.

CancellationToken

Las acciones pueden enlazar opcionalmente CancellationToken como un parámetro. Esto enlaza RequestAborted que indica cuándo se anula la conexión subyacente a la solicitud HTTP. Las acciones pueden usar este parámetro para cancelar las operaciones asincrónicas de larga duración que se ejecutan como parte de las acciones del controlador.

FormCollection

Se usa para recuperar todos los valores de los datos de formulario publicados.

Formateadores de entrada

Los datos del cuerpo de la solicitud pueden estar en XML, JSON u otro formato. Para analizar estos datos, el enlace de modelos usa un formateador de entrada configurado para controlar un tipo de contenido determinado. De forma predeterminada, en ASP.NET Core se incluyen formateadores de entrada basados en JSON para el control de los datos JSON. Puede agregar otros formateadores para otros tipos de contenido.

ASP.NET Core selecciona los formateadores de entrada en función del atributo Consumes. Si no hay ningún atributo, usa el encabezado Content-Type.

Para usar los formateadores de entrada XML integrados:

Personalización del enlace de modelos con formateadores de entrada

Un formateador de entrada asume toda la responsabilidad de leer datos del cuerpo de la solicitud. Para personalizar este proceso, configure las API que usa el formateador de entrada. En esta sección se describe cómo personalizar el formateador de entrada basado en System.Text.Json para entender un tipo personalizado denominado ObjectId.

Considere el modelo siguiente, que contiene una propiedad ObjectId:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Para personalizar el proceso de enlace de modelos al usar System.Text.Json, cree una clase derivada de JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Para usar un convertidor personalizado, aplique el atributo JsonConverterAttribute al tipo. En el ejemplo siguiente, el tipo ObjectId se configura con ObjectIdConverter como su convertidor personalizado:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Para más información, consulte Procedimientos para escribir convertidores personalizados.

Exclusión de tipos especificados del enlace de modelos

El comportamiento del enlace de modelos y los sistemas de validación se controla mediante ModelMetadata. Puede personalizar ModelMetadata mediante la adición de un proveedor de detalles a MvcOptions.ModelMetadataDetailsProviders. Los proveedores de detalles integrados están disponibles para deshabilitar el enlace de modelos o la validación para tipos especificados.

Para deshabilitar el enlace de modelos en todos los modelos de un tipo especificado, agregue una instancia de ExcludeBindingMetadataProvider en Program.cs. Por ejemplo, para deshabilitar el enlace de modelos en todos los modelos del tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Para deshabilitar la validación en propiedades de un tipo especificado, agregue una instancia de SuppressChildValidationMetadataProvider en Program.cs. Por ejemplo, para deshabilitar la validación en las propiedades de tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Enlazadores de modelos personalizados

Puede ampliar el enlace de modelos si escribe un enlazador de modelos personalizado y usa el atributo [ModelBinder] para seleccionarlo para un destino concreto. Más información sobre el enlace de modelos personalizados.

Enlace de modelos manual

El enlace de modelos se puede invocar de forma manual mediante el método TryUpdateModelAsync. El método se define en las clases ControllerBase y PageModel. Las sobrecargas de método permiten especificar el prefijo y el proveedor de valores que se van a usar. El método devuelve false si se produce un error en el enlace de modelos. Este es un ejemplo:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa proveedores de valor para obtener datos del cuerpo del formulario, la cadena de consulta y los datos de ruta. TryUpdateModelAsync suele ser:

  • Se usa con aplicaciones de Razor Pages y MVC con controladores y vistas para evitar el exceso de publicación.
  • No se usa con una API web a menos que se consuma a partir de datos de formulario, cadenas de consulta y datos de ruta. Los puntos de conexión de la API web que consumen JSON usan formateadores de entrada para deserializar el cuerpo de la solicitud en un objeto.

Para más información, consulte TryUpdateModelAsync.

Atributo [FromServices]

El nombre de este atributo sigue el patrón de los atributos de enlace de modelos que especifican un origen de datos. Pero no se trata de enlazar datos desde un proveedor de valores. Obtiene una instancia de un tipo desde el contenedor de inserción de dependencias. Su objetivo es proporcionar una alternativa a la inserción de constructores cuando se necesita un servicio solo si se llama a un método concreto.

Si una instancia del tipo no está registrada en el contenedor de inserción de dependencias, la aplicación produce una excepción al intentar enlazar el parámetro. Para que el parámetro sea opcional, use uno de los métodos siguientes:

  • Hacer que el parámetro acepte valores NULL.
  • Establezca un valor predeterminado para el parámetro.

Para los parámetros que aceptan valores NULL, asegúrese de que el parámetro no sea null antes de acceder a él.

Recursos adicionales

En este artículo se explica qué es el enlace de modelos, cómo funciona y cómo personalizar su comportamiento.

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

Qué es el enlace de modelos

Los controladores y lasRazor pages trabajan con datos que provienen de solicitudes HTTP. Por ejemplo, los datos de ruta pueden proporcionar una clave de registro y los campos de formulario publicados pueden proporcionar valores para las propiedades del modelo. La escritura de código para recuperar cada uno de estos valores y convertirlos de cadenas a tipos de .NET sería tediosa y propensa a errores. El enlace de modelos automatiza este proceso. El sistema de enlace de modelos:

  • Recupera datos de diversos orígenes, como datos de ruta, campos de formulario y cadenas de consulta.
  • Proporciona los datos a los controladores y Razor pages en parámetros de método y propiedades públicas.
  • Convierte datos de cadena en tipos de .NET.
  • Actualiza las propiedades de tipos complejos.

Ejemplo

Imagine que tiene el siguiente método de acción:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

Y que la aplicación recibe una solicitud con esta dirección URL:

http://contoso.com/api/pets/2?DogsOnly=true

El enlace de modelos realiza los pasos siguientes después de que el sistema de enrutamiento selecciona el método de acción:

  • Busca el primer parámetro de GetById, un entero denominado id.
  • Examina los orígenes disponibles en la solicitud HTTP y busca id = "2" en los datos de ruta.
  • Convierte la cadena "2" en el entero 2.
  • Busca el siguiente parámetro de GetById, un valor booleano denominado dogsOnly.
  • Examina los orígenes y busca "DogsOnly=true" en la cadena de consulta. La coincidencia de nombres no distingue mayúsculas de minúsculas.
  • Convierte la cadena "true" en un valor booleano true.

Después, el marco llama al método GetById y pasa 2 para el parámetro id y true para el parámetro dogsOnly.

En el ejemplo anterior, los destinos de enlace de modelos son parámetros de método que son tipos simples. Los destinos también pueden ser las propiedades de un tipo complejo. Después de que cada propiedad se haya enlazado correctamente, se produce la validación de modelos para esa propiedad. El registro de qué datos se han enlazado al modelo y los errores de enlace o validación se almacenan en ControllerBase.ModelState o PageModel.ModelState. Para averiguar si este proceso se ha realizado correctamente, la aplicación comprueba la marca ModelState.IsValid.

Targets

El enlace de modelos intenta encontrar valores para los tipos de destinos siguientes:

  • Parámetros del método de acción de controlador al que se enruta una solicitud.
  • Parámetros del método de administración de Razor páginas al que se enruta una solicitud.
  • Propiedades públicas de un controlador o una clase PageModel, si se especifican mediante atributos.

Atributo [BindProperty]

Se puede aplicar a una propiedad pública de un controlador o una clase PageModel para hacer que el enlace de modelos tenga esa propiedad como destino:

public class EditModel : InstructorsPageModel
{
    [BindProperty]
    public Instructor Instructor { get; set; }

Atributo [BindProperties]

Disponible en ASP.NET 2.1 Core y versiones posteriores. Se pueden aplicar a un controlador o una clase PageModel para indicar al enlace de modelos que seleccione como destino todas las propiedades públicas de la clase:

[BindProperties(SupportsGet = true)]
public class CreateModel : InstructorsPageModel
{
    public Instructor Instructor { get; set; }

Enlace de modelos para solicitudes HTTP GET

De forma predeterminada, las propiedades no se enlazan para las solicitudes HTTP GET. Normalmente, todo lo que necesita para una solicitud GET es un parámetro de id. de registro. El id. de registro se usa para buscar el elemento en la base de datos. Por tanto, no es necesario enlazar una propiedad que contiene una instancia del modelo. En escenarios donde quiera propiedades enlazadas a datos de las solicitudes GET, establezca la propiedad SupportsGet en true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string ApplicationInsightsCookie { get; set; }

Orígenes

De forma predeterminada, el enlace de modelos obtiene datos en forma de pares clave-valor de los siguientes orígenes de una solicitud HTTP:

  1. Campos de formulario
  2. El cuerpo de la solicitud (para controladores que tienen el atributo [ApiController]).
  3. Datos de ruta
  4. Parámetros de cadena de consulta
  5. Archivos cargados

Para cada parámetro o propiedad de destino, se examinan los orígenes en el orden indicado en la lista anterior. Hay algunas excepciones:

  • Los datos de ruta y los valores de cadena de consulta solo se usan para tipos simples.
  • Los archivos cargados solo se enlazan a tipos de destino que implementan IFormFile o IEnumerable<IFormFile>.

Si el origen predeterminado no es correcto, use uno de los atributos siguientes para especificar el origen:

  • [FromQuery]: obtiene valores de la cadena de consulta.
  • [FromRoute]: obtiene valores de los datos de ruta.
  • [FromForm]: obtiene valores de los campos de formulario publicados.
  • [FromBody]: obtiene valores del cuerpo de la solicitud.
  • [FromHeader]: obtiene valores de encabezados HTTP.

Estos atributos:

  • Se agregan de forma individual a las propiedades del modelo (no a la clase de modelo), como en el ejemplo siguiente:

    public class Instructor
    {
        public int ID { get; set; }
    
        [FromQuery(Name = "Note")]
        public string NoteFromQueryString { get; set; }
    
  • Opcionalmente, acepte un valor de nombre de modelo en el constructor. Esta opción se proporciona en caso de que el nombre de la propiedad no coincida con el valor de la solicitud. Por ejemplo, es posible que el valor de la solicitud sea un encabezado con un guion en el nombre, como en el ejemplo siguiente:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Atributo [FromBody]

Aplique el atributo [FromBody] a un parámetro para rellenar sus propiedades desde el cuerpo de una solicitud HTTP. El runtime de ASP.NET Core delega la responsabilidad de leer el cuerpo al formateador de entrada. Los formateadores de entrada se explican más adelante en este artículo.

Cuando se aplica [FromBody] a un parámetro de tipo complejo, se omiten los atributos de origen de enlace aplicados a sus propiedades. Por ejemplo, la acción Create siguiente especifica que su parámetro pet se rellena a partir del cuerpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

La clase Pet especifica que su propiedad Breed se rellena a partir de un parámetro de cadena de consulta:

public class Pet
{
    public string Name { get; set; }

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; }
}

En el ejemplo anterior:

  • El atributo [FromQuery] se ignora.
  • La propiedad Breed no se rellena desde un parámetro de cadena de consulta.

Los formateadores de entrada solo leen el cuerpo y no entienden los atributos de origen de enlace. Si se encuentra un valor adecuado en el cuerpo, ese valor se usa para rellenar la propiedad Breed.

No aplique [FromBody] a más de un parámetro por método de acción. Una vez que un formateador de entrada ha leído la secuencia de solicitudes, deja de estar disponible para una nueva lectura con el fin de enlazar otros parámetros [FromBody].

Orígenes adicionales

Los proveedores de valores proporcionan datos de origen al sistema de enlace de modelos. Puede escribir y registrar proveedores de valores personalizados que obtienen datos de otros orígenes para el enlace de modelos. Por ejemplo, es posible que le interesen datos de cookies o del estado de sesión. Para obtener datos desde un origen nuevo:

  • Cree una clase que implemente IValueProvider.
  • Cree una clase que implemente IValueProviderFactory.
  • Registre la clase de generador en Startup.ConfigureServices.

En la aplicación de ejemplo se incluye un proveedor de valores y un generador que obtiene valores de cookies. Este es el código de registro de Startup.ConfigureServices:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

En el código mostrado, el proveedor de valor personalizado se coloca después de todos los proveedores de valor integrados. Para que sea el primero en la lista, llame a Insert(0, new CookieValueProviderFactory()) en lugar de a Add.

No hay origen para una propiedad de modelo

De forma predeterminada, si no se encuentra ningún valor para una propiedad de modelo no se crea un error de estado del modelo. La propiedad se establece en NULL o en un valor predeterminado:

  • Los tipos simples que aceptan valores NULL se establecen en null.
  • Los tipos de valor que no aceptan valores NULL se establecen en default(T). Por ejemplo, un parámetro int id se establece en 0.
  • Para los tipos complejos, el enlace de modelos crea una instancia mediante el constructor predeterminado, sin establecer propiedades.
  • Las matrices se establecen en Array.Empty<T>(), salvo las matrices byte[], que se establecen en null.

Si el estado del modelo se debe invalidar cuando no se encuentra nada en los campos de formulario para una propiedad de modelo, use el atributo [BindRequired].

Tenga en cuenta que este comportamiento de [BindRequired] se aplica al enlace de modelos desde datos de formulario publicados, no a los datos JSON o XML del cuerpo de una solicitud. Los datos del cuerpo de la solicitud se controlan mediante formateadores de entrada.

Errores de la conversión de tipos

Si se encuentra un origen pero no se puede convertir al tipo de destino, el estado del modelo se marca como no válido. El parámetro o la propiedad de destino se establece en NULL o en un valor predeterminado, como se ha indicado en la sección anterior.

En un controlador de API que tenga el atributo [ApiController], el estado de modelo no válido genera una respuesta HTTP 400 automática.

En una página de Razor, se vuelve a mostrar la página con un mensaje de error:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _instructorsInMemoryStore.Add(Instructor);
    return RedirectToPage("./Index");
}

La validación del lado cliente detecta la mayoría de los datos incorrectos que en otros casos se enviarían a un formulario de Razor Pages. Esta validación dificulta que se pueda desencadenar el código resaltado anterior. En la aplicación de ejemplo se incluye un botón Submit with Invalid Date (Enviar con fecha no válida) que agrega datos incorrectos al campo Hire Date (Fecha de contratación) y envía el formulario. Este botón muestra cómo funciona el código para volver a mostrar la página cuando se producen errores de conversión de datos.

Cuando el código anterior vuelve a mostrar la página, no se muestra la entrada no válida en el campo de formulario. El motivo es que la propiedad de modelo se ha establecido en NULL o en un valor predeterminado. La entrada no válida sí aparece en un mensaje de error. Pero si quiere volver a mostrar los datos incorrectos en el campo de formulario, considere la posibilidad de convertir la propiedad de modelo en una cadena y realizar la conversión de datos de forma manual.

Se recomienda la misma estrategia si no quiere que los errores de conversión de tipo generen errores de estado de modelo. En ese caso, convierta la propiedad de modelo en una cadena.

Tipos simples

Los tipos simples a los que el enlazador de modelos puede convertir las cadenas de origen incluyen los siguientes:

Tipos complejos

Un tipo complejo debe tener un constructor público predeterminado y propiedades grabables públicas para enlazar. Cuando se produce el enlace de modelos, se crea una instancia de la clase con el constructor predeterminado público.

Para cada propiedad del tipo complejo, el enlace de modelos busca entre los orígenes el patrón de nombre prefijo.nombre_de_propiedad. Si no se encuentra nada, solo busca nombre_de_propiedad sin el prefijo.

Para el enlace a un parámetro, el prefijo es el nombre del parámetro. Para el enlace a una propiedad pública PageModel, el prefijo es el nombre de la propiedad pública. Algunos atributos tienen una propiedad Prefix que permite invalidar el uso predeterminado del nombre de parámetro o propiedad.

Por ejemplo, imagine que el tipo complejo es la clase Instructor siguiente:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefijo = nombre del parámetro

Si el modelo que se va a enlazar es un parámetro denominado instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

El enlace de modelos se inicia mediante el examen de los orígenes para la clave instructorToUpdate.ID. Si no se encuentra, busca ID sin un prefijo.

Prefijo = nombre de propiedad

Si el modelo que se va a enlazar es una propiedad denominada Instructor del controlador o la clase PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

El enlace de modelos se inicia mediante el examen de los orígenes para la clave Instructor.ID. Si no se encuentra, busca ID sin un prefijo.

Prefijo personalizado

Si el modelo que se va a enlazar es un parámetro denominado instructorToUpdate y un atributo Bind, especifica Instructor como el prefijo:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

El enlace de modelos se inicia mediante el examen de los orígenes para la clave Instructor.ID. Si no se encuentra, busca ID sin un prefijo.

Atributos para destinos de tipo complejo

Existen varios atributos integrados para controlar el enlace de modelos de tipos complejos:

  • [Bind]
  • [BindRequired]
  • [BindNever]

Advertencia

Estos atributos afectan al enlace de modelos cuando el origen de los valores son datos de formulario publicados. No afectan a los formateadores de entrada, que procesan los cuerpos de la solicitud JSON y XML publicados. Los formateadores de entrada se explican más adelante en este artículo.

Atributo [Bind]

Se puede aplicar a una clase o un parámetro de método. Especifica qué propiedades de un modelo se deben incluir en el enlace de modelos. [Bind]no afecta a los formateadores de entrada.

En el ejemplo siguiente, solo se enlazan las propiedades especificadas del modelo Instructor cuando se llama a cualquier método de acción o controlador:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

En el ejemplo siguiente, solo se enlazan las propiedades especificadas del modelo Instructor cuando se llama al método OnPost:

[HttpPost]
public IActionResult OnPost([Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

El atributo [Bind] se puede usar para protegerse de la publicación excesiva en escenarios de creación. No funciona bien en escenarios de edición porque las propiedades excluidas se establecen en NULL o en un valor predeterminado en lugar de mantenerse sin cambios. Para defenderse de la publicación excesiva, se recomiendan modelos de vista en lugar del atributo [Bind]. Para más información, vea Nota de seguridad sobre la publicación excesiva.

Atributo [ModelBinder]

ModelBinderAttribute se puede aplicar a tipos, propiedades o parámetros. Permite especificar el tipo de enlazador de modelos utilizado para enlazar la instancia o el tipo específicos. Por ejemplo:

[HttpPost]
public IActionResult OnPost([ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

El atributo [ModelBinder] también se puede usar para cambiar el nombre de una propiedad o parámetro cuando se enlaza al modelo:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    public string Name { get; set; }
}

Atributo [BindRequired]

Solo se puede aplicar a propiedades del modelo, no a parámetros de método. Hace que el enlace de modelos agregue un error de estado de modelo si no se puede realizar el enlace para la propiedad de un modelo. Este es un ejemplo:

public class InstructorWithCollection
{
    public int ID { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Hire Date")]
    [BindRequired]
    public DateTime HireDate { get; set; }

Vea también la explicación del atributo [Required] en Validación de modelos.

Atributo [BindNever]

Solo se puede aplicar a propiedades del modelo, no a parámetros de método. Impide que el enlace de modelos establezca la propiedad de un modelo. Este es un ejemplo:

public class InstructorWithDictionary
{
    [BindNever]
    public int ID { get; set; }

Colecciones

Para los destinos que son colecciones de tipos simples, el enlace de modelos busca coincidencias con nombre_de_parámetro o nombre_de_propiedad. Si no se encuentra ninguna coincidencia, busca uno de los formatos admitidos sin el prefijo. Por ejemplo:

  • Imagine que el parámetro que se va a enlazar es una matriz llamada selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Los datos de cadena de consulta o formulario pueden estar en uno de los formatos siguientes:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evite enlazar un parámetro o una propiedad denominada index o Index si está adyacente a un valor de colección. El enlace de modelos intenta usar index como índice para la colección, lo que podría dar lugar a un enlace incorrecto. Considere, por ejemplo, las siguientes acciones:

    public IActionResult Post(string index, List<Product> products)
    

    En el código anterior, el parámetro de cadena de consulta index se enlaza al parámetro de método index y también se usa para enlazar la colección de productos. Cambiar el nombre del parámetro index o usar un atributo de enlace de modelos para configurar el enlace evita este problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • El formato siguiente solo se admite en datos de formulario:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Para todos los formatos de ejemplo anteriores, el enlace de modelos pasa una matriz de dos elementos al parámetro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Los formatos de datos que usan números de subíndice (... [0] ... [1] ...) deben asegurarse de que se numeran de forma secuencial a partir de cero. Si hay algún hueco en la numeración de los subíndices, se omiten todos los elementos que aparecen después del hueco. Por ejemplo, si los subíndices son 0 y 2 en lugar de 0 y 1, se omite el segundo elemento.

Diccionarios

Para los destinos Dictionary, el enlace de modelos busca coincidencias con nombre_de_parámetro o nombre_de_propiedad. Si no se encuentra ninguna coincidencia, busca uno de los formatos admitidos sin el prefijo. Por ejemplo:

  • Imagine que el parámetro de destino es un elemento Dictionary<int, string> denominado selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Los datos de cadena de consulta o de formulario publicados pueden ser similares a uno de los ejemplos siguientes:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Para todos los formatos de ejemplo anteriores, el enlace de modelos pasa un diccionario de dos elementos al parámetro selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Tipos de registro y enlace de constructores

El enlace de modelos requiere que los tipos complejos tengan un constructor sin parámetros. Tanto en System.Text.Json como en Newtonsoft.Json los formateadores de entrada basados admiten la deserialización de clases que no tienen un constructor sin parámetros.

C# 9 presenta tipos de registro, que son una excelente manera de representar datos a través de la red. ASP.NET Core agrega compatibilidad con el enlace de modelos y la validación de tipos de registro con un único constructor:

public record Person([Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
   public IActionResult Index() => View();

   [HttpPost]
   public IActionResult Index(Person person)
   {
       ...
   }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
...
Age: <input asp-for="Age" />

Al validar los tipos de registro, el tiempo de ejecución busca metadatos de enlace y validación específicamente en parámetros en lugar de en propiedades.

El marco permite enlazar y validar tipos de registro:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Para que funcione el anterior, el tipo debe:

  • Ser un tipo de registro.
  • Tener exactamente un constructor público.
  • Contener parámetros que tienen una propiedad con el mismo nombre y tipo. Los nombres no deben diferir entre mayúsculas y minúsculas.

POCO sin constructores carentes de parámetros

Los POCO que no tienen constructores sin parámetros no se pueden enlazar.

El código siguiente da como resultado una excepción que indica que el tipo debe tener un constructor sin parámetros:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
   public Person(string Name) : this (Name, 0);
}

Tipos de registro con constructores creados manualmente

Los tipos de registro con constructores creados manualmente que parecen los constructores principales funcionan

public record Person
{
   public Person([Required] string Name, [Range(0, 100)] int Age) => (this.Name, this.Age) = (Name, Age);

   public string Name { get; set; }
   public int Age { get; set; }
}

Tipos de registro, validación y metadatos de enlace

En el caso de los tipos de registro, se usan metadatos de validación y enlace en parámetros. Se omiten los metadatos de las propiedades

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validación y metadatos

La validación usa metadatos en el parámetro, pero usa la propiedad para leer el valor. En el caso normal con constructores principales, los dos serían idénticos. Sin embargo, hay maneras de derrotarlo:

public record Person([Required] string Name)
{
   private readonly string _name;
   public Name { get; init => _name = value ?? string.Empty; } // Now this property is never null. However this object could have been constructed as `new Person(null);`
}

TryUpdateModel no actualiza parámetros en un tipo de registro

public record Person(string Name)
{
   public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

En este caso, MVC no intentará enlazarse Name de nuevo. Sin embargo, Age se puede actualizar

Comportamiento de globalización del enlace de modelos datos de ruta y cadenas de consulta

El proveedor de valores de ruta de ASP.NET Core y el proveedor de valores de cadena de consulta:

  • Tratan los valores como referencia cultural de todos los idiomas.
  • Esperan que las direcciones URL sean independientes de la referencia cultural.

Por el contrario, los valores procedentes de datos de formulario se someten a una conversión que tiene en cuenta la referencia cultural. Esto es así por diseño, para que las direcciones URL se puedan compartir entre configuraciones regionales.

Para que el proveedor de valores de ruta de ASP.NET Core y el proveedor de valores de cadena de consulta se sometan a una conversión dependiente de la referencia cultural:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        var index = options.ValueProviderFactories.IndexOf(
            options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().Single());
        options.ValueProviderFactories[index] = new CulturedQueryStringValueProviderFactory();
    });
}
public class CulturedQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query != null && query.Count > 0)
        {
            var valueProvider = new QueryStringValueProvider(
                BindingSource.Query,
                query,
                CultureInfo.CurrentCulture);

            context.ValueProviders.Add(valueProvider);
        }

        return Task.CompletedTask;
    }
}

Tipos de datos especiales

Hay algunos tipos de datos especiales que el enlace de modelos puede controlar.

IFormFile e IFormFileCollection

Un archivo cargado incluido en la solicitud HTTP. También se admite IEnumerable<IFormFile> para varios archivos.

CancellationToken

Las acciones pueden enlazar opcionalmente CancellationToken como un parámetro. Esto enlaza RequestAborted que indica cuándo se anula la conexión subyacente a la solicitud HTTP. Las acciones pueden usar este parámetro para cancelar las operaciones asincrónicas de larga duración que se ejecutan como parte de las acciones del controlador.

FormCollection

Se usa para recuperar todos los valores de los datos de formulario publicados.

Formateadores de entrada

Los datos del cuerpo de la solicitud pueden estar en XML, JSON u otro formato. Para analizar estos datos, el enlace de modelos usa un formateador de entrada configurado para controlar un tipo de contenido determinado. De forma predeterminada, en ASP.NET Core se incluyen formateadores de entrada basados en JSON para el control de los datos JSON. Puede agregar otros formateadores para otros tipos de contenido.

ASP.NET Core selecciona los formateadores de entrada en función del atributo Consumes. Si no hay ningún atributo, usa el encabezado Content-Type.

Para usar los formateadores de entrada XML integrados:

  • Instale el paquete NuGet Microsoft.AspNetCore.Mvc.Formatters.Xml.

  • En Startup.ConfigureServices, llame a AddXmlSerializerFormatters o a AddXmlDataContractSerializerFormatters.

    services.AddRazorPages()
        .AddMvcOptions(options =>
    {
        options.ValueProviderFactories.Add(new CookieValueProviderFactory());
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(System.Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
    })
    .AddXmlSerializerFormatters();
    
  • Aplique el atributo Consumes a las clases de controlador o los métodos de acción que deben esperar XML en el cuerpo de la solicitud.

    [HttpPost]
    [Consumes("application/xml")]
    public ActionResult<Pet> Create(Pet pet)
    

    Para más información, vea Introducción de la serialización XML.

Personalización del enlace de modelos con formateadores de entrada

Un formateador de entrada asume toda la responsabilidad de leer datos del cuerpo de la solicitud. Para personalizar este proceso, configure las API que usa el formateador de entrada. En esta sección se describe cómo personalizar el formateador de entrada basado en System.Text.Json para entender un tipo personalizado denominado ObjectId.

Considere el modelo siguiente, que contiene una propiedad ObjectId denominada Id:

public class ModelWithObjectId
{
    public ObjectId Id { get; set; }
}

Para personalizar el proceso de enlace de modelos al usar System.Text.Json, cree una clase derivada de JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return new ObjectId(JsonSerializer.Deserialize<int>(ref reader, options));
    }

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
    {
        writer.WriteNumberValue(value.Id);
    }
}

Para usar un convertidor personalizado, aplique el atributo JsonConverterAttribute al tipo. En el ejemplo siguiente, el tipo ObjectId se configura con ObjectIdConverter como su convertidor personalizado:

[JsonConverter(typeof(ObjectIdConverter))]
public struct ObjectId
{
    public ObjectId(int id) =>
        Id = id;

    public int Id { get; }
}

Para más información, consulte Procedimientos para escribir convertidores personalizados.

Exclusión de tipos especificados del enlace de modelos

El comportamiento del enlace de modelos y los sistemas de validación se controla mediante ModelMetadata. Puede personalizar ModelMetadata mediante la adición de un proveedor de detalles a MvcOptions.ModelMetadataDetailsProviders. Los proveedores de detalles integrados están disponibles para deshabilitar el enlace de modelos o la validación para tipos especificados.

Para deshabilitar el enlace de modelos en todos los modelos de un tipo especificado, agregue una instancia de ExcludeBindingMetadataProvider en Startup.ConfigureServices. Por ejemplo, para deshabilitar el enlace de modelos en todos los modelos del tipo System.Version:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

Para deshabilitar la validación en propiedades de un tipo especificado, agregue una instancia de SuppressChildValidationMetadataProvider en Startup.ConfigureServices. Por ejemplo, para deshabilitar la validación en las propiedades de tipo System.Guid:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

Enlazadores de modelos personalizados

Puede ampliar el enlace de modelos si escribe un enlazador de modelos personalizado y usa el atributo [ModelBinder] para seleccionarlo para un destino concreto. Más información sobre el enlace de modelos personalizados.

Enlace de modelos manual

El enlace de modelos se puede invocar de forma manual mediante el método TryUpdateModelAsync. El método se define en las clases ControllerBase y PageModel. Las sobrecargas de método permiten especificar el prefijo y el proveedor de valores que se van a usar. El método devuelve false si se produce un error en el enlace de modelos. Este es un ejemplo:

if (await TryUpdateModelAsync<InstructorWithCollection>(
    newInstructor,
    "Instructor",
    i => i.FirstMidName, i => i.LastName, i => i.HireDate))
{
    _instructorsInMemoryStore.Add(newInstructor);
    return RedirectToPage("./Index");
}
PopulateAssignedCourseData(newInstructor);
return Page();

TryUpdateModelAsync usa proveedores de valor para obtener datos del cuerpo del formulario, la cadena de consulta y los datos de ruta. TryUpdateModelAsync suele ser:

  • Se usa con aplicaciones de Razor Pages y MVC con controladores y vistas para evitar el exceso de publicación.
  • No se usa con una API web a menos que se consuma a partir de datos de formulario, cadenas de consulta y datos de ruta. Los puntos de conexión de la API web que consumen JSON usan formateadores de entrada para deserializar el cuerpo de la solicitud en un objeto.

Para más información, consulte TryUpdateModelAsync.

Atributo [FromServices]

El nombre de este atributo sigue el patrón de los atributos de enlace de modelos que especifican un origen de datos. Pero no se trata de enlazar datos desde un proveedor de valores. Obtiene una instancia de un tipo desde el contenedor de inserción de dependencias. Su objetivo es proporcionar una alternativa a la inserción de constructores cuando se necesita un servicio solo si se llama a un método concreto.

Recursos adicionales