Trabajar con el modelo de aplicación en ASP.NET Core

Por Steve Smith

ASP.NET Core MVC define un modelo de aplicación que representa los componentes de una aplicación MVC. Lea y manipule este modelo para modificar la manera en que se comportan los elementos de MVC. De forma predeterminada, MVC sigue ciertas convenciones para determinar qué clases se consideran controladores, qué métodos de esas clases son acciones y cómo se comportan los parámetros y el enrutamiento. Personalice este comportamiento para adaptarlo a las necesidades de la aplicación. Para ello, basta con que cree sus convenciones personalizadas y las aplique globalmente o como atributos.

Modelos y proveedores (IApplicationModelProvider)

El modelo de aplicación de ASP.NET Core MVC incluye interfaces abstractas y clases de implementación concretas que describen una aplicación MVC. Este modelo es el resultado de la detección por parte de MVC de los controladores, las acciones, los parámetros de acción, las rutas y los filtros de la aplicación de acuerdo con las convenciones predeterminadas. Cuando trabaje con el modelo de aplicación, modifique la aplicación para que siga convenciones diferentes del comportamiento predeterminado de MVC. Los parámetros, nombres, rutas y filtros se usan como datos de configuración para las acciones y los controladores.

El modelo de aplicación de ASP.NET Core MVC tiene la estructura siguiente:

  • ApplicationModel
    • Controladores (ControllerModel)
      • Acciones (ActionModel)
        • Parámetros (ParameterModel)

Cada nivel del modelo tiene acceso a una colección Properties común, y los niveles inferiores pueden tener acceso a los valores de propiedad establecidos por los niveles superiores de la jerarquía y sobrescribirlos. Las propiedades se conservan en ActionDescriptor.Properties cuando se crean las acciones. Después, cuando se controla una solicitud, se puede obtener acceso a través de ActionContext.ActionDescriptor a todas las propiedades que agregue o modifique una convención. El uso de propiedades es una manera excelente de configurar por acción los filtros, los enlazadores de modelos y otros aspectos en el modelo de aplicación.

Nota:

La colección ActionDescriptor.Properties no es segura para subprocesos (para escrituras) una vez que se ha iniciado la aplicación. Las convenciones son la mejor manera de agregar datos de forma segura a esta colección.

ASP.NET Core MVC carga el modelo de aplicación mediante un patrón de proveedor definido por la interfaz IApplicationModelProvider. En esta sección se describen algunos detalles de implementación interna relacionados con el funcionamiento de este proveedor. El uso del patrón de proveedor es un tema avanzado, principalmente para el uso del marco. La mayoría de las aplicaciones deben usar convenciones, no el patrón de proveedor.

Las implementaciones de la interfaz IApplicationModelProvider "se encapsulan" entre sí, y cada implementación llama a OnProvidersExecuting en orden ascendente en función de su propiedad Order. Después, se llama al método OnProvidersExecuted en orden inverso. El marco de trabajo define varios proveedores:

Primero, (Order=-1000):

  • DefaultApplicationModelProvider

Después, (Order=-990):

  • AuthorizationApplicationModelProvider
  • CorsApplicationModelProvider

Nota:

El orden en que se llama a dos proveedores con el mismo valor para Order no está definido y no se debe confiar en él.

Nota

IApplicationModelProvider es un concepto avanzado pensado para que los autores del marco de trabajo lo extiendan. En general, las aplicaciones deben usar convenciones y los marcos de trabajo deben usar proveedores. La diferencia clave es que los proveedores siempre se ejecutan antes que las convenciones.

DefaultApplicationModelProvider establece muchos de los comportamientos predeterminados que usa ASP.NET Core MVC. Entre sus responsabilidades se incluyen las siguientes:

  • Agregar filtros globales al contexto
  • Agregar controladores al contexto
  • Agregar métodos de controlador públicos como acciones
  • Agregar parámetros de métodos de acción al contexto
  • Aplicar la ruta y otros atributos

Algunos comportamientos integrados se implementan mediante DefaultApplicationModelProvider. Este proveedor es responsable de la construcción de ControllerModel, que a su vez hace referencia a instancias de ActionModel, PropertyModel y ParameterModel. La clase DefaultApplicationModelProvider es un detalle de implementación del marco de trabajo interno que cambiará en el futuro.

AuthorizationApplicationModelProvider se encarga de aplicar el comportamiento asociado a los atributos AuthorizeFilter y AllowAnonymousFilter. Para obtener más información, vea Autorización simple en ASP.NET Core.

CorsApplicationModelProvider implementa el comportamiento asociado a IEnableCorsAttribute y IDisableCorsAttribute. Para obtener más información, vea Habilitar solicitudes entre orígenes (CORS) en ASP.NET Core.

La información sobre los proveedores internos del marco descritos en esta sección no está disponible a través del explorador de la API de .NET. Sin embargo, los proveedores se pueden inspeccionar en el origen de referencia de ASP.NET Core (repositorio de GitHub dotnet/aspnetcore). Use la búsqueda de GitHub para buscar los proveedores por nombre y seleccione la versión del origen con la lista desplegable Cambiar ramas o etiquetas.

Convenciones

El modelo de aplicación define abstracciones de convenciones que proporcionan una forma más sencilla de personalizar el comportamiento de los modelos que invalidan el modelo completo o el proveedor. Se recomienda el uso de estas abstracciones para modificar el comportamiento de la aplicación. Las convenciones le ofrecen una manera de escribir código que aplica dinámicamente las personalizaciones. Mientras los filtros proporcionan una forma de modificar el comportamiento del marco de trabajo, las personalizaciones hacen posible controlar cómo funciona la aplicación en su conjunto.

Están disponibles las convenciones siguientes:

Las convenciones se aplican agregándolas a las opciones de MVC o implementando atributos y aplicándolos a controladores, acciones o parámetros de acción (similares a los filtros). A diferencia de los filtros, las convenciones solo se ejecutan cuando se inicia la aplicación, no como parte de cada solicitud.

Nota:

Para obtener información sobre las convenciones Razor de proveedor de modelos de aplicación y ruta de Pages, consulte Convenciones de aplicaciones y rutas de Razor Pages en ASP.NET Core.

Modificar el ApplicationModel

La siguiente convención se usa para agregar una propiedad al modelo de aplicación:

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ApplicationDescription : IApplicationModelConvention
    {
        private readonly string _description;

        public ApplicationDescription(string description)
        {
            _description = description;
        }

        public void Apply(ApplicationModel application)
        {
            application.Properties["description"] = _description;
        }
    }
}

Las convenciones del modelo de aplicación se aplican como opciones cuando se agrega MVC en Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Conventions.Add(new ApplicationDescription("My Application Description"));
        options.Conventions.Add(new NamespaceRoutingConvention());
    });
}

Las propiedades son accesibles desde la colección ActionDescriptor.Properties dentro de las acciones de controlador:

public class AppModelController : Controller
{
    public string Description()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }
}

Modificar la descripción de ControllerModel

El modelo de controlador también puede incluir propiedades personalizadas. La propiedades personalizadas invalidarán las propiedades existentes con el mismo nombre especificado en el modelo de aplicación. El atributo de convención siguiente agrega una descripción en el nivel de controlador:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention
    {
        private readonly string _description;

        public ControllerDescriptionAttribute(string description)
        {
            _description = description;
        }

        public void Apply(ControllerModel controllerModel)
        {
            controllerModel.Properties["description"] = _description;
        }
    }
}

Esta convención se aplica como un atributo en un controlador:

[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
    public string Index()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }

Modificar la descripción de ActionModel

Se puede aplicar una convención de atributo independiente a acciones individuales, con lo que se invalida el comportamiento que ya se haya aplicado en el nivel de aplicación o controlador:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ActionDescriptionAttribute : Attribute, IActionModelConvention
    {
        private readonly string _description;

        public ActionDescriptionAttribute(string description)
        {
            _description = description;
        }

        public void Apply(ActionModel actionModel)
        {
            actionModel.Properties["description"] = _description;
        }
    }
}

Si se aplica a una acción dentro del controlador se puede ver cómo invalida la convención en el nivel de controlador:

[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
    public string Index()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }

    [ActionDescription("Action Description")]
    public string UseActionDescriptionAttribute()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }
}

Modificar el ParameterModel

La convención siguiente se puede aplicar a parámetros de acción para modificar su BindingInfo. La convención siguiente requiere que el parámetro sea un parámetro de ruta. Otros posibles orígenes de enlace, como los valores de cadena de consulta, se omiten:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace AppModelSample.Conventions
{
    public class MustBeInRouteParameterModelConvention : Attribute, IParameterModelConvention
    {
        public void Apply(ParameterModel model)
        {
            if (model.BindingInfo == null)
            {
                model.BindingInfo = new BindingInfo();
            }
            model.BindingInfo.BindingSource = BindingSource.Path;
        }
    }
}

El atributo se puede aplicar a cualquier parámetro de acción:

public class ParameterModelController : Controller
{
    // Will bind:  /ParameterModel/GetById/123
    // WON'T bind: /ParameterModel/GetById?id=123
    public string GetById([MustBeInRouteParameterModelConvention]int id)
    {
        return $"Bound to id: {id}";
    }
}

Para aplicar la convención a todos los parámetros de acción, agregue MustBeInRouteParameterModelConvention a MvcOptions en Startup.ConfigureServices:

options.Conventions.Add(new MustBeInRouteParameterModelConvention());

Modificar el nombre ActionModel

La convención siguiente modifica el valor de ActionModel para actualizar el nombre de la acción a la que se aplica. El nuevo nombre se proporciona como un parámetro al atributo. El enrutamiento usará este nuevo nombre, lo que afecta a la ruta usada para llegar a este método de acción:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class CustomActionNameAttribute : Attribute, IActionModelConvention
    {
        private readonly string _actionName;

        public CustomActionNameAttribute(string actionName)
        {
            _actionName = actionName;
        }

        public void Apply(ActionModel actionModel)
        {
            // this name will be used by routing
            actionModel.ActionName = _actionName;
        }
    }
}

Este atributo se aplica a un método de acción en HomeController:

// Route: /Home/MyCoolAction
[CustomActionName("MyCoolAction")]
public string SomeName()
{
    return ControllerContext.ActionDescriptor.ActionName;
}

Aunque el nombre del método es SomeName, el atributo invalida la convención de MVC de usar el nombre del método y reemplaza el nombre de la acción por MyCoolAction. De este modo, la ruta usada para llegar a esta acción es /Home/MyCoolAction.

Nota

Este ejemplo en esta sección básicamente equivale a usar el atributo ActionNameAttribute integrado.

Convención de enrutamiento personalizado

Use IApplicationModelConvention para personalizar cómo funciona el enrutamiento. Por ejemplo, la convención siguiente incorpora espacios de nombres de controladores en sus rutas, al reemplazar . en el espacio de nombres por / en la ruta:

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;

namespace AppModelSample.Conventions
{
    public class NamespaceRoutingConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            foreach (var controller in application.Controllers)
            {
                var hasAttributeRouteModels = controller.Selectors
                    .Any(selector => selector.AttributeRouteModel != null);

                if (!hasAttributeRouteModels
                    && controller.ControllerName.Contains("Namespace")) // affect one controller in this sample
                {
                    // Replace the . in the namespace with a / to create the attribute route
                    // Ex: MySite.Admin namespace will correspond to MySite/Admin attribute route
                    // Then attach [controller], [action] and optional {id?} token.
                    // [Controller] and [action] is replaced with the controller and action
                    // name to generate the final template
                    controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
                    {
                        Template = controller.ControllerType.Namespace.Replace('.', '/') + "/[controller]/[action]/{id?}"
                    };
                }
            }

            // You can continue to put attribute route templates for the controller actions depending on the way you want them to behave
        }
    }
}

La convención se agrega como una opción en Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Conventions.Add(new ApplicationDescription("My Application Description"));
        options.Conventions.Add(new NamespaceRoutingConvention());
    });
}

Sugerencia

Agregue convenciones al middleware mediante MvcOptions el siguiente enfoque. El marcador de posición {CONVENTION} es la convención que se va a agregar:

services.Configure<MvcOptions>(c => c.Conventions.Add({CONVENTION}));

Este ejemplo se aplica la convención a las rutas que no usan el enrutamiento de atributos donde el controlador contiene Namespace en su nombre:

using Microsoft.AspNetCore.Mvc;

namespace AppModelSample.Controllers
{
    public class NamespaceRoutingController : Controller
    {
        // using NamespaceRoutingConvention
        // route: /AppModelSample/Controllers/NamespaceRouting/Index
        public string Index()
        {
            return "This demonstrates namespace routing.";
        }
    }
}

Uso del modelo de aplicación en WebApiCompatShim

ASP.NET Core MVC usa un conjunto diferente de convenciones de ASP.NET Web API 2. Mediante el uso de convenciones personalizadas, puede modificar el comportamiento de una aplicación ASP.NET Core MVC para que sea coherente con el de una aplicación web API. Microsoft suministra el paquete WebApiCompatShim de NuGet específicamente para este propósito.

Nota:

Para obtener más información sobre la migración desde ASP.NET Web API, consulte Migración de ASP.NET Web API a ASP.NET Core.

Para usar las Correcciones de compatibilidad (Shim) con la API web:

  • Agregue el paquete Microsoft.AspNetCore.Mvc.WebApiCompatShim al proyecto.
  • Agregue las convenciones a MVC mediante una llamada a AddWebApiConventions en Startup.ConfigureServices:
services.AddMvc().AddWebApiConventions();

Las convenciones proporcionadas por las correcciones de compatibilidad solo se aplican a las partes de la aplicación a las que se han aplicado ciertos atributos. Los cuatro atributos siguientes se usan para controlar en qué controladores se deben modificar las convenciones mediante las convenciones de las correcciones de compatibilidad:

Convenciones de acción

UseWebApiActionConventionsAttribute se usa para asignar el método HTTP a las acciones según su nombre (por ejemplo, Get se asignaría a HttpGet). Solo se aplica a las acciones que no usan el enrutamiento de atributos.

Sobrecarga

UseWebApiOverloadingAttribute se usa para aplicar la convención WebApiOverloadingApplicationModelConvention. Esta convención agrega OverloadActionConstraint al proceso de selección de acciones, que limita las acciones candidatas a aquellas en las que la solicitud cumple todos los parámetros no opcionales.

Convenciones de parámetro

UseWebApiParameterConventionsAttribute se usa para aplicar la convención de acción WebApiParameterConventionsApplicationModelConvention. Esta convención especifica que los tipos simples usados como parámetros de acción se enlazan desde el URI de forma predeterminada, mientras que los tipos complejos se enlazan desde el cuerpo de la solicitud.

Rutas

UseWebApiRoutesAttribute controla si se ha aplicado la convención de controlador WebApiApplicationModelConvention. Cuando está habilitada, esta convención se usa para agregar compatibilidad con áreas a la ruta e indica que el controlador está en el área api.

Además de un conjunto de convenciones, el paquete de compatibilidad incluye una clase base System.Web.Http.ApiController que reemplaza la que proporciona web API. Esto permite que los controladores de web API escritos para web API y que heredan de ApiController funcionen mientras se ejecutan en ASP.NET Core MVC. Todos los atributos UseWebApi* enumerados anteriormente se aplican a la clase de controlador base. ApiController expone propiedades, métodos y tipos de resultados compatibles con los que se encuentran en web API.

Uso de ApiExplorer para documentar una aplicación

El modelo de aplicación expone una propiedad ApiExplorerModel en cada nivel que se puede usar para recorrer la estructura de la aplicación. Esto se puede usar para generar páginas de ayuda para las web API mediante el uso de herramientas como Swagger. La propiedad ApiExplorer expone una propiedad IsVisible que se puede establecer para especificar qué partes del modelo de la aplicación deben exponerse. Configurar esta opción mediante una convención:

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class EnableApiExplorerApplicationConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            application.ApiExplorer.IsVisible = true;
        }
    }
}

Mediante el uso de este enfoque (y de convenciones adicionales si es necesario), la visibilidad de API puede habilitarse o deshabilitarse en cualquier nivel dentro de la aplicación.