Desarrollo de aplicaciones ASP.NET Core MVC
Sugerencia
Este contenido es un extracto del libro electrónico "Architect Modern Web Applications with ASP.NET Core and Azure" (Diseño de la arquitectura de aplicaciones web modernas con ASP.NET Core y Azure), disponible en Documentación de .NET o como un PDF descargable y gratuito para leerlo sin conexión.
"No es importante hacerlo bien la primera vez. Es importante conseguirlo la última vez." - Andrew Hunt y David Thomas
ASP.NET Core es un marco multiplataforma de código abierto para compilar aplicaciones web modernas optimizadas para la nube. Las aplicaciones ASP.NET Core son ligeras y modulares, con compatibilidad integrada para la inserción de dependencias, lo que permite una mayor capacidad de prueba y mantenimiento. Al combinarlo con MVC, que admite la creación de API web modernas además de aplicaciones basadas en vistas, ASP.NET Core es un marco eficaz con el que compilar aplicaciones web empresariales.
MVC y Razor Pages
ASP.NET Core MVC ofrece muchas características que son útiles para crear API y aplicaciones basadas en web. El término MVC significa "Modelo-Vista-Controlador", un patrón de interfaz de usuario que divide las responsabilidades de responder a las solicitudes de los usuarios en varias partes. Además de seguir este patrón, también puede implementar características en sus aplicaciones ASP.NET Core, como Razor Pages.
Razor Pages está integrado en ASP.NET Core MVC y usa las mismas características de enrutamiento, enlace de modelos, filtros, autorización, etcétera. Pero, en lugar de tener archivos y carpetas independientes para los controladores, los modelos, las vistas y otros elementos, y usar el enrutamiento basado en atributos, Razor Pages se coloca en una sola carpeta ("/Páginas"). La ruta se basa en su ubicación relativa en la carpeta, y las solicitudes se controlan mediante controladores en lugar de acciones de controlador. Como resultado, al trabajar con Razor Pages, todos los archivos y clases que necesita se colocan normalmente, no se distribuyen por todo el proyecto web.
Obtenga más información sobre cómo se aplican MVC, Razor Pages y los patrones relacionados en la aplicación de ejemplo eShopOnWeb.
Al crear una aplicación ASP.NET Core, debe tener un plan en cuenta para el tipo de aplicación que quiera crear. Al crear un proyecto, en el IDE o mediante el comando dotnet new
de la CLI, elegirá entre varias plantillas. Las plantillas de proyecto más comunes son Vacía, API web, Aplicación web y Aplicación web (Modelo-Vista-Controlador). Aunque solo puede tomar esta decisión al crear un proyecto, no es irrevocable. El proyecto API web usa controladores de Modelo-Vista-Controlador estándar; simplemente carece de las vistas de forma predeterminada. Del mismo modo, la plantilla predeterminada Aplicación web usa Razor Pages, por lo que tampoco incluye la carpeta de vistas. Puede agregar la carpeta de vistas para estos proyectos más adelante para admitir el comportamiento basado en vistas. Los proyectos de tipo API web y Modelo-Vista-Controlador no incluyen la carpeta de páginas de forma predeterminada, pero puede agregar una más tarde para admitir el comportamiento basado en Razor Pages. Estos tres tipos de plantilla están diseñados para tres tipos de interacción predeterminada del usuario distintos: datos (API web), basada en páginas y basada en vistas. Si quiere, puede combinar cualquiera de estas plantillas, o todas ellas, en un mismo proyecto.
¿Por qué Razor Pages?
Razor Pages es el método predeterminado para nuevas aplicaciones web en Visual Studio. Razor Pages ofrece una manera más fácil de crear características de aplicaciones basadas en páginas, como los formularios que no son de aplicaciones de página única. Al usar controladores y vistas, era habitual que las aplicaciones tuvieran controladores muy grandes que funcionaban con varias dependencias y varios modelos de vista distintos, así como que devolvieran muchas vistas diferentes. Esto generaba una mayor complejidad y, a menudo, daba lugar a controladores que no seguían el principio de responsabilidad única o los principios de apertura y cierre de forma eficaz. Razor Pages soluciona este problema, ya que encapsula la lógica del lado servidor para una determinada "página" lógica en una aplicación web con el marcado de Razor. Una página de Razor Pages que no tenga ninguna lógica del lado servidor solo puede constar de un archivo de Razor (por ejemplo, "Index.cshtml"). Sin embargo, la mayoría de Razor Pages no triviales tendrá una clase de modelo de página asociado, que, por convención, tendrá el mismo nombre que el archivo Razor con una extensión ".cs" (por ejemplo, "Index.cshtml.cs").
Un modelo de página de Razor Pages combina las responsabilidades de un controlador de MVC y un modelo de vista. En lugar de controlar las solicitudes con los métodos de acción de controlador, se ejecutan los controladores de modelo de página, como "OnGet()", con lo que se representa la página asociada de forma predeterminada. Razor Pages simplifica el proceso de compilar páginas individuales en la aplicación ASP.NET Core sin dejar de proporciona todas las características arquitectónicas de ASP.NET Core MVC. Es una buena opción predeterminada para la nueva funcionalidad basada en páginas.
Cuándo usar MVC
Si va a crear interfaces API web, el patrón MVC tiene más sentido que Razor Pages. Si el proyecto va a exponer solo los puntos de conexión de la API Web, lo ideal es empezar a partir de la plantilla de proyecto de API web. De lo contrario, es fácil agregar controladores y puntos de conexión de API asociados a cualquier aplicación ASP.NET Core. Use el método MVC basado en vista si quiere realizar la migración de una aplicación ASP.NET MVC 5 existente o versiones anteriores a ASP.NET Core MVC y quiere hacerlo de la forma más fácil posible. Una vez que haya realizado la migración inicial, podrá evaluar si tiene sentido adoptar Razor Pages para las nuevas características o incluso como migración general. Para más información sobre cómo migrar las aplicaciones de .NET 4.x a .NET 8, vea Migración de aplicaciones existentes de ASP.NET a ASP.NET Core eBook.
Si opta por crear su aplicación web mediante Razor Pages o las vistas de MVC, la aplicación tendrá un rendimiento similar e incluirá compatibilidad con la inserción de dependencias, los filtros, el enlace de modelos, la validación y otras características.
Asignación de solicitudes a respuestas
En su núcleo, las aplicaciones ASP.NET Core asignan las solicitudes entrantes a las respuestas salientes. En un nivel bajo, esta asignación se realiza mediante middleware, y las aplicaciones y microservicios sencillos de ASP.NET Core pueden constar únicamente de middleware personalizado. Cuando se usa ASP.NET Core MVC, se puede trabajar en un cierto nivel superior y pensar en términos de rutas, controladores y acciones. Cada solicitud entrante se compara con la tabla de enrutamiento de la aplicación y, si se encuentra una ruta coincidente, se llama al método de acción asociado (perteneciente a un controlador) para controlar la solicitud. Si no se encuentra ninguna ruta que coincida, se llama a un controlador de errores (en este caso, se devuelve un resultado de NotFound).
Las aplicaciones ASP.NET Core MVC pueden usar rutas convencionales, rutas de atributo o las dos. Las rutas convencionales se definen en el código, especificando convenciones de enrutamiento con una sintaxis similar a la del ejemplo siguiente:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});
En este ejemplo, se agregó una ruta con el nombre "default" a la tabla de enrutamiento. Define una plantilla de ruta con marcadores de posición para controller
, action
y id
. Los marcadores de posición controller
y action
tienen el valor predeterminado especificado (Home
y Index
, respectivamente), y el marcador de posición id
es opcional (en virtud de la aplicación de "?"). La convención que se define aquí indica que la primera parte de una solicitud debe corresponder al nombre del controlador, la segunda parte, a la acción y después, si es necesario, una tercera parte representará un parámetro del identificador. Normalmente, las rutas convencionales se definen en un solo lugar para la aplicación, como en Program.cs, donde se configura la canalización de middleware de solicitud.
Las rutas de atributo se aplican directamente a los controladores y acciones, en lugar de especificarse globalmente. Este enfoque tiene la ventaja de que son mucho más sencillas de detectar cuando se examina un método concreto, pero significa que la información de enrutamiento no se mantiene en un lugar de la aplicación. Con las rutas de atributo, se pueden especificar fácilmente varias rutas para una acción determinada, así como combinar rutas entre controladores y acciones. Por ejemplo:
[Route("Home")]
public class HomeController : Controller
{
[Route("")] // Combines to define the route template "Home"
[Route("Index")] // Combines to define route template "Home/Index"
[Route("/")] // Does not combine, defines the route template ""
public IActionResult Index() {}
}
Las rutas se pueden especificar en atributos [HttpGet] y similares, evitando la necesidad de agregar atributos [Route] independientes. Las rutas de atributo también pueden usar tokens para reducir la necesidad de repetir los nombres de acciones o controladores, como se muestra a continuación:
[Route("[controller]")]
public class ProductsController : Controller
{
[Route("")] // Matches 'Products'
[Route("Index")] // Matches 'Products/Index'
public IActionResult Index() {}
}
Razor Pages no usa el enrutamiento de atributos. Puede especificar información de la plantilla de ruta adicional para una Razor Page como parte de su directiva de @page
:
@page "{id:int}"
En el ejemplo anterior, la página en cuestión coincidiría con una ruta con un parámetro id
de número entero. Por ejemplo, la página Products.cshtml, que se encuentra en la raíz de /Pages
respondería a solicitudes como esta:
/Products/123
Una vez que una determinada solicitud se ha asociado a una ruta, pero antes de llamar al método de acción, ASP.NET Core MVC realizará el enlace del modelo y la validación del modelo en la solicitud. El enlace del modelo es responsable de convertir los datos HTTP entrantes en los tipos de .NET especificados como parámetros del método de acción que se va a llamar. Por ejemplo, si el método de acción espera un parámetro int id
, el enlace de modelos intentará proporcionar este parámetro a partir de un valor que se suministre como parte de la solicitud. Para ello, el enlace de modelos busca valores en un formulario enviado, valores en la propia misma ruta y valores de cadena de consulta. Suponiendo que se encuentre un valor id
, se convertirá en un entero antes de pasarlo al método de acción.
Después de enlazar el modelo, pero antes de llamar al método de acción, se produce la validación del modelo. La validación del modelo usa atributos opcionales en el tipo de modelo y puede ayudar a garantizar que el objeto de modelo proporcionado cumple determinados requisitos de datos. Se pueden especificar determinados valores como obligatorios, o limitarlos a una longitud o un intervalo numérico determinados, etc. Si se especifican atributos de validación, pero el modelo no cumple sus requisitos, la propiedad ModelState.IsValid será false y el conjunto de reglas de validación con errores estará disponible para enviarlo al cliente que realiza la solicitud.
Si se usa la validación del modelo, siempre se debe comprobar que el modelo es válido antes de ejecutar cualquier comando de modificación del estado, para asegurarse de que la aplicación no resulta dañada por datos no válidos. Se puede usar un filtro para evitar la necesidad de agregar código para esta validación en todas las acciones. Los filtros de ASP.NET Core MVC ofrecen una manera de interceptar grupos de solicitudes, para poder aplicar directivas comunes e intereses transversales por cada destino. Los filtros se pueden aplicar a acciones individuales, controladores completos o de forma global para una aplicación.
Para las API web, ASP.NET Core MVC admite la negociación de contenido, lo que permite a las solicitudes especificar cómo se debe aplicar formato a las respuestas. Según los encabezados proporcionados en la solicitud, las acciones que devuelven datos darán formato a la respuesta en XML, JSON o en otro formato compatible. Esta característica permite usar la misma API en varios clientes con requisitos de formato de datos diferentes.
Para los proyectos de API web, debería valorarse el uso del atributo [ApiController]
, que se puede aplicar a controladores individuales, a una clase de controlador base o a todo el ensamblado. Este atributo agrega la comprobación de validación de modelos automática; cualquier acción con una modelo no válido devolverá una solicitud BadRequest con los detalles de los errores de validación. El atributo también requiere que todas las acciones tengan una ruta de atributo, en lugar de utilizar una ruta convencional, y devuelve información más detallada de ProblemDetails en respuesta a los errores.
Mantenimiento de los controladores bajo control
En el caso de las aplicaciones basadas en páginas, Razor Pages hace un gran trabajo evitando que los controladores se vuelvan demasiado grandes. Cada página tiene sus propios archivos y clases dedicados únicamente para sus controladores. Antes de la introducción de Razor Pages, muchas aplicaciones centradas en las vistas tenían grandes clases de controladores responsables de muchas acciones y vistas diferentes. Estas clases crecían de forma natural hasta tener muchas responsabilidades y dependencias, lo que dificultaba el mantenimiento. Si sus controladores basados en vistas están creciendo demasiado, considere la posibilidad de refactorizarlos para usar Razor Pages o de incluir un patrón como mediador.
El patrón de diseño de mediador se usa para reducir el acoplamiento entre las clases y permitir la comunicación entre ellas. En las aplicaciones de ASP.NET Core MVC, este patrón suele emplearse para dividir los controladores en partes más pequeñas mediante el uso de rutinas de controladores para realizar el trabajo de los métodos de acción. El popular paquete NuGet MediatR se usa a menudo para lograr esto. Normalmente, los controladores incluyen muchos métodos de acción diferentes, y cada uno puede requerir ciertas dependencias. El conjunto de todas las dependencias requeridas por cualquier acción debe transferirse al constructor del controlador. Cuando se usa MediatR, la única dependencia que un controlador tendrá normalmente es una instancia del mediador. Cada acción usa la instancia del mediador para enviar un mensaje, el cual es procesado por un controlador. El controlador es específico para una única acción y, por tanto, solo necesita las dependencias que requiere dicha acción. A continuación se muestra un ejemplo de un controlador que usa MediatR:
public class OrderController : Controller
{
private readonly IMediator _mediator;
public OrderController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<IActionResult> MyOrders()
{
var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
return View(viewModel);
}
// other actions implemented similarly
}
En la acción MyOrders
, esta clase controla la llamada a Send
para enviar un mensaje de GetMyOrders
:
public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>>
{
private readonly IOrderRepository _orderRepository;
public GetMyOrdersHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request, CancellationToken cancellationToken)
{
var specification = new CustomerOrdersWithItemsSpecification(request.UserName);
var orders = await _orderRepository.ListAsync(specification);
return orders.Select(o => new OrderViewModel
{
OrderDate = o.OrderDate,
OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
{
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
Units = oi.Units
}).ToList(),
OrderNumber = o.Id,
ShippingAddress = o.ShipToAddress,
Total = o.Total()
});
}
}
El resultado final de este enfoque es que los controladores deben ser mucho más pequeños y estar centrados principalmente en el enrutamiento y el enlace de modelos; mientras que las rutinas de controladores individuales son responsables de las tareas específicas que necesita un punto de conexión determinado. Este enfoque también se puede lograr sin MediatR mediante el uso del paquete NuGet ApiEndpoints, que intenta proporcionar a los controladores de API las mismas ventajas que proporciona Razor Pages a los controladores basados en vistas.
Referencias: asignación de solicitudes a respuestas
- Routing to Controller Actions (Enrutamiento a acciones del controlador)
https://learn.microsoft.com/aspnet/core/mvc/controllers/routing- Enlace de modelos
https://learn.microsoft.com/aspnet/core/mvc/models/model-binding- Introduction to model validation in ASP.NET Core MVC (Introducción a la validación de modelos en ASP.NET Core MVC)
https://learn.microsoft.com/aspnet/core/mvc/models/validation- Filtros
https://learn.microsoft.com/aspnet/core/mvc/controllers/filters- Atributo ApiController
https://learn.microsoft.com/aspnet/core/web-api/
Trabajar con dependencias
ASP.NET Core tiene compatibilidad integrada con una técnica conocida como inserción de dependencias y la usa de manera interna. La inserción de dependencias es una técnica que permite el acoplamiento flexible entre los distintos elementos de una aplicación. El acoplamiento más flexible es deseable porque facilita aislar los elementos de la aplicación, lo que permite realizar pruebas o reemplazos. También hace que sea menos probable que un cambio en un elemento de la aplicación tenga un impacto inesperado en otro lugar de la aplicación. La inserción de dependencias se basa en el principio de inversión de dependencias y suele ser es clave para lograr el principio abierto o cerrado. Al evaluar el funcionamiento de la aplicación con sus dependencias, tenga cuidado del problema de la estática en el código y recuerde el aforismo "lo nuevo se pega".
La estática se produce cuando las clases realizan llamadas a métodos estáticos, o bien tienen acceso a propiedades estáticas, que tienen efectos secundarios o dependencias en la infraestructura. Por ejemplo, si tiene un método que llama a un método estático, que a su vez escribe en una base de datos, el método está estrechamente acoplado a la base de datos. Todo lo que interrumpa esa llamada a la base de datos interrumpirá el método. Es muy difícil probar este tipo de métodos, ya que esas pruebas requieren bibliotecas de simulación comerciales para simular las llamadas estáticas o solo se pueden probar con una base de datos de prueba. Las llamadas estáticas que no tienen ninguna dependencia de la infraestructura, sobre todo las que son completamente sin estado, se pueden llamar sin problemas y no tienen ningún impacto en el acoplamiento o la capacidad de prueba (más allá del acoplamiento de código a la propia llamada estática).
Muchos desarrolladores comprenden los riesgos de la estática y el estado global, pero siguen acoplando estrechamente el código a implementaciones específicas a través de la creación directa de instancias. "Lo nuevo se pega" está pensado para ser un aviso de este acoplamiento y no un rechazo general del uso de la palabra clave new
. Como sucede con las llamadas de métodos estáticos, las instancias nuevas de tipos que no tienen dependencias externas normalmente no acoplan estrechamente código a los detalles de implementación ni dificultan las pruebas. Pero cada vez que se crea una instancia de una clase, dedique un instante a considerar si tiene sentido integrar como parte del código esa instancia específica en esa ubicación concreta, o si un mejor diseño sería solicitar esa instancia como una dependencia.
Declarar las dependencias
ASP.NET Core se basa en hacer que los métodos y las clases declaren sus dependencias y las soliciten como argumentos. Las aplicaciones de ASP.NET normalmente se configuran en Program.cs o en una clase Startup
.
Nota
La configuración completa de aplicaciones en Program.cs es el enfoque predeterminado para las aplicaciones de .NET 6 (y posteriores) y Visual Studio 2022. Las plantillas de proyecto se han actualizado para ayudarle a empezar a trabajar con este nuevo enfoque. Los proyectos de ASP.NET Core aún pueden usar una clase Startup
, si se desea.
Configuración de servicios en Program.cs
Para aplicaciones muy sencillas, puede conectar dependencias directamente en el archivo Program.cs mediante WebApplicationBuilder
. Una vez que se han agregado todos los servicios necesarios, el generador se usa para crear la aplicación.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var app = builder.Build();
Configuración de servicios en Startup.cs
Startup.cs está configurado para admitir la inserción de dependencias en varios puntos. Si usa una clase Startup
, puede asignarle un constructor y es posible solicitar dependencias a través de él, de la siguiente manera:
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
}
}
La clase Startup
es interesante en el sentido de que no hay requisitos de tipos explícitos para ella. No hereda de una clase base Startup
especial, ni implementa ninguna interfaz determinada. Se le puede asignar un constructor, o no, y se pueden especificar tantos parámetros en el constructor como se quiera. Cuando se inicia el host web configurado para la aplicación, llamará a la clase Startup
(si le ha indicado que use una) y usará la inserción de dependencias para rellenar todas las dependencias que la clase Startup
requiera. Por supuesto, si se solicitan parámetros que no están configurados en el contenedor de servicios que usa ASP.NET Core, se obtendrá una excepción, pero siempre que se respeten las dependencias que el contenedor conoce, se puede solicitar lo que se quiera.
La inserción de dependencias se integra en las aplicaciones ASP.NET Core desde el principio, cuando se crea la instancia de la clase de inicio. No se detiene ahí para la clase de inicio. Las dependencias también se pueden solicitar en el método Configure
:
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
}
El método ConfigureServices es la excepción a este comportamiento; solo debe tomar un parámetro de tipo IServiceCollection
. Realmente no necesita admitir la inserción de dependencias, ya que por un lado es responsable de agregar objetos al contenedor de servicios y, por otro, tiene acceso a todos los servicios configurados actualmente a través del parámetro IServiceCollection
. Por tanto, se puede trabajar con las dependencias definidas en la colección de servicios de ASP.NET Core en todos los elementos de la clase Startup
, ya sea solicitando el servicio necesario como un parámetro o trabajando con IServiceCollection
en ConfigureServices
.
Nota
Si necesita asegurarse de que determinados servicios estén disponibles para la clase Startup
, puede configurarlos mediante IWebHostBuilder
y su método ConfigureServices
dentro de la llamada CreateDefaultBuilder
.
La clase de inicio es un modelo de cómo estructurar otros elementos de la aplicación ASP.NET Core, desde controladores a software intermedio o filtros para sus propios servicios. En cada caso, se debe seguir el principio de dependencias explícitas, y solicitar las dependencias en lugar de crearlas directamente, y aprovechar la inserción de dependencias en toda la aplicación. Tenga cuidado de dónde y cómo se crean instancias directas de las implementaciones, especialmente de servicios y objetos que funcionan con la infraestructura o tienen efectos secundarios. Es preferible trabajar con abstracciones definidas en el núcleo de la aplicación y pasadas como argumentos que integrar las referencias como parte del código en tipos de implementación específicos.
Estructuración de la aplicación
Las aplicaciones monolíticas suelen tener un único punto de entrada. En el caso de una aplicación web ASP.NET Core, el punto de entrada será el proyecto web ASP.NET Core. Pero eso no significa que la solución deba constar simplemente de un solo proyecto. Resulta útil dividir la aplicación en diferentes capas para seguir la separación de intereses. Una vez dividida en capas, resulta útil ir más allá de las carpetas para separar los proyectos, lo que puede ayudar a conseguir una mejor encapsulación. El mejor método para lograr estos objetivos con una aplicación ASP.NET Core consiste en una variación de la arquitectura limpia que se describe en el capítulo 5. A raíz de este enfoque, la solución de la aplicación estará compuesta por bibliotecas independientes para la interfaz de usuario, la infraestructura y ApplicationCore.
Además de estos proyectos, también se incluyen proyectos de prueba independientes (las pruebas se describen en el capítulo 9).
El modelo de objetos y las interfaces de la aplicación se deben colocar en el proyecto ApplicationCore. Este proyecto tendrá el menor número posible de dependencias, y ninguna en cuestiones específicas de la infraestructura, y los otros proyectos de la solución le harán referencia. Las entidades de negocio que deban conservarse se definen en el proyecto ApplicationCore, al igual que los servicios que no dependen directamente de la infraestructura.
Los detalles de implementación, por ejemplo, cómo se realiza la persistencia o cómo se pueden enviar notificaciones a un usuario, se mantienen en el proyecto de infraestructura. Este proyecto hará referencia a paquetes específicos de la implementación como Entity Framework Core, pero no debe exponer detalles sobre estas implementaciones fuera del proyecto. Los servicios de infraestructura y los repositorios deben implementar interfaces definidas en el proyecto ApplicationCore, y sus implementaciones de persistencia serán responsables de recuperar y almacenar las entidades definidas en ApplicationCore.
El propio proyecto de interfaz de usuario de ASP.NET Core es responsable de cualquier interés del nivel de la interfaz de usuario, pero no debe incluir lógica empresarial o detalles de infraestructura. De hecho, idealmente ni siquiera debería tener una dependencia en el proyecto de infraestructura, lo que ayudará a garantizar que no se introduce por accidente ninguna dependencia entre los dos proyectos. Esto se puede lograr mediante un contenedor de DI de terceros, como Autofac, que permite definir reglas de DI en clases del módulo en cada proyecto.
Otro enfoque para desacoplar la aplicación de los detalles de implementación consiste en hacer que la aplicación llame a microservicios, posiblemente implementados en contenedores de Docker individuales. Esto proporciona una separación de intereses y desacoplamiento incluso mayor que aprovechar la DI entre dos proyectos, pero tiene una complejidad adicional.
Organización de las características
De forma predeterminada, las aplicaciones ASP.NET Core organizan su estructura de carpetas para que incluir controladores y vistas, y con frecuencia ViewModels. El código del lado cliente para admitir estas estructuras del lado servidor normalmente se almacena por separado en la carpeta wwwroot. Pero es posible que esta organización genere problemas en aplicaciones grandes, puesto que trabajar con cualquier característica determinada a menudo requiere saltar entre estas carpetas. Esto se complica cada vez más a medida que aumenta el número de archivos y subcarpetas en cada carpeta, lo que da lugar a una gran cantidad de desplazamiento por el Explorador de soluciones. Una solución a este problema consiste en organizar el código de aplicación por característica en lugar de por tipo de archivo. Este estilo profesional se conoce normalmente como carpetas de características o sectores de características (vea también: Vertical Slice (Sectores verticales).
ASP.NET Core MVC admite las áreas para este propósito. Con las áreas, se pueden crear conjuntos independientes de carpetas de controladores y vistas (así como los modelos asociados) en cada carpeta de área. En la figura 7-1 se muestra una estructura de carpetas de ejemplo, en la que se usan áreas.
Figura 7-1. Organización de áreas de ejemplo
Cuando se usan las áreas, se deben usar atributos para decorar los controladores con el nombre del área a la que pertenecen:
[Area("Catalog")]
public class HomeController
{}
También se debe agregar compatibilidad con las áreas a las rutas:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "areaRoute", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});
Además de la compatibilidad integrada con las áreas, también se puede usar una estructura de carpetas propia y convenciones en lugar de atributos y rutas personalizadas. Esto permitiría tener carpetas de características que no incluyeran carpetas independientes para vistas, controladores, etc., manteniendo la jerarquía más plana y facilitando la tarea de ver todos los archivos relacionados en un único lugar para cada característica. En el caso de las API, las carpetas se pueden usar para reemplazar controladores, y cada carpeta puede contener todos los puntos de conexión de API y sus DTO asociados.
ASP.NET Core usa tipos de convención integrados para controlar su comportamiento. Estas convenciones se pueden modificar o reemplazar. Por ejemplo, se puede crear una convención que obtenga automáticamente el nombre de característica de un controlador determinado en función de su espacio de nombres (lo que normalmente se correlaciona con la carpeta en la que se encuentra el controlador):
public class FeatureConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
controller.Properties.Add("feature",
GetFeatureName(controller.ControllerType));
}
private string GetFeatureName(TypeInfo controllerType)
{
string[] tokens = controllerType.FullName.Split('.');
if (!tokens.Any(t => t == "Features")) return "";
string featureName = tokens
.SkipWhile(t => !t.Equals("features", StringComparison.CurrentCultureIgnoreCase))
.Skip(1)
.Take(1)
.FirstOrDefault();
return featureName;
}
}
Después, esta convención se especifica como una opción al agregar compatibilidad para MVC a la aplicación en ConfigureServices
, o bien en Program.cs:
// ConfigureServices
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));
// Program.cs
builder.Services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));
ASP.NET Core MVC también usa una convención para localizar vistas. Se puede reemplazar con una convención personalizada para que las vistas se ubiquen en las carpetas de características (mediante el nombre de la característica proporcionado por FeatureConvention, anteriormente). Puede obtener más información sobre este enfoque y descargar un ejemplo funcional en el artículo de MSDN Magazine Sectores de características para ASP.NET Core MVC.
API y aplicaciones Blazor
Si la aplicación incluye un conjunto de API web que se deba proteger, estas API idealmente se deben configurar como un proyecto independiente de la aplicación de vista o de Razor Pages. La separación de las API, sobre todo las API públicas, de la aplicación web del lado servidor tiene una serie de ventajas. Estas aplicaciones a menudo tendrán características de implementación y carga únicas. También es muy probable que adopten diferentes mecanismos de seguridad; las aplicaciones estándar basadas en formularios aprovechan la autenticación basada en cookies y las API probablemente usan la autenticación basada en tokens.
Además, las aplicaciones Blazor, tanto si usan Blazor Server como BlazorWebAssembly, deben compilarse como proyectos independientes. Estas aplicaciones tienen características de entorno de ejecución y modelos de seguridad diferentes. Es probable que compartan tipos comunes con la aplicación web del lado servidor (o el proyecto de API), y estos tipos se deben definir en un proyecto compartido común.
La adición de una interfaz de administrador de BlazorWebAssembly a eShopOnWeb hizo necesaria la adición de varios proyectos nuevos: El propio proyecto de BlazorWebAssembly, BlazorAdmin
; el proyecto PublicApi
, donde se define un nuevo conjunto de puntos de conexión de API pública usados por BlazorAdmin
y configurados para usar la autenticación basada en tokens; y un nuevo proyecto BlazorShared
que almacena ciertos tipos compartidos que se usan en los dos proyectos anteriores.
Podría preguntarse por qué es necesario agregar un proyecto BlazorShared
independiente cuando ya existe un proyecto ApplicationCore
común que podría usarse para compartir los tipos que requieren tanto PublicApi
como BlazorAdmin
. La respuesta es que este proyecto incluye toda la lógica empresarial de la aplicación y, por tanto, es mucho mayor de lo necesario y es mucho más probable que deba mantenerse protegido en el servidor. Recuerde que cualquier biblioteca a la que haga referencia BlazorAdmin
se descargará en los exploradores de los usuarios cuando carguen la aplicación Blazor.
Dependiendo de si se usa el patrón Backends For Frontends (BFF), es posible que las API que consume la aplicación WebAssemblyBlazor no compartan sus tipos con Blazor al 100 %. En concreto, una API pública pensada para ser usada por muchos clientes diferentes puede definir sus propios tipos de solicitud y de resultado, en lugar de compartirlos en un proyecto compartido específico del cliente. En el ejemplo de eShopOnWeb, se supone que el proyecto PublicApi
está hospedando una API pública, por lo que no todos sus tipos de solicitud y de respuesta proceden del proyecto BlazorShared
.
Intereses transversales
A medida que las aplicaciones aumentan de tamaño, resulta cada vez más importante separar los intereses transversales para eliminar la duplicación y mantener la coherencia. Algunos ejemplos de intereses transversales en aplicaciones ASP.NET Core son la autenticación, las reglas de validación del modelo, el almacenamiento en caché de salida y el control de errores, aunque hay muchos otros. Los filtros de ASP.NET Core MVC permiten ejecutar código antes o después de ciertas fases de la canalización de procesamiento de la solicitud. Por ejemplo, se puede ejecutar un filtro antes y después del enlace del modelo, antes y después de una acción, o antes y después del resultado de una acción. También se puede usar un filtro de autorización para controlar el acceso al resto de la canalización. En la figura 7-2 se muestra cómo solicitar flujos de ejecución a través de filtros, si se han configurado.
Figura 7-2. Ejecución de la solicitud a través de los filtros y la canalización de solicitud.
Normalmente, los filtros se implementan como atributos, para que se puedan aplicar a controladores o acciones, o incluso de forma global. Cuando se agregan de esta forma, los filtros especificados en el nivel de acción invalidan o se basan en los filtros especificados en el nivel de controlador, que a su vez invalidan los filtros globales. Por ejemplo, el atributo [Route]
se puede usar para crear rutas entre controladores y acciones. Del mismo modo, la autorización se puede configurar en el nivel de controlador y, después, reemplazarse por acciones individuales, como se muestra en el ejemplo siguiente:
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous] // overrides the Authorize attribute
public async Task<IActionResult> Login() {}
public async Task<IActionResult> ForgotPassword() {}
}
En el primer método, Login, se usa el filtro [AllowAnonymous]
(atributo) para invalidar el filtro Authorize establecido en el nivel de controlador. La acción ForgotPassword
(y cualquier otra acción de la clase que no tenga un atributo AllowAnonymous) requerirá una solicitud autenticada.
Los filtros se pueden usar para eliminar la duplicación en forma de directivas de control de errores comunes para las API. Por ejemplo, una directiva de API típica es devolver una respuesta NotFound a las solicitudes que hacen referencia a claves que no existen y una respuesta BadRequest
si se produce un error en la validación del modelo. En el ejemplo siguiente se muestran estas dos directivas en funcionamiento:
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
{
return NotFound(id);
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
author.Id = id;
await _authorRepository.UpdateAsync(author);
return Ok();
}
No permita que los métodos de acción se llenen con código condicional similar a este. En su lugar, extraiga las directivas en filtros que se puedan aplicar según sea necesario. En este ejemplo, la comprobación de validación del modelo, que debería producirse siempre que se envíe un comando a la API, se puede reemplazar por el atributo siguiente:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
Puede agregar el elemento ValidateModelAttribute
al proyecto como una dependencia de NuGet ; para ello, debe incluir el paquete Ardalis.ValidateModel. Para las API, puede usar el atributo ApiController
para aplicar este comportamiento sin tener que usar un filtro ValidateModel
independiente.
Del mismo modo, se puede usar un filtro para comprobar si existe un registro y devolver un error 404 antes de ejecutar la acción, lo que elimina la necesidad de realizar estas comprobaciones en la acción. Una vez que se han extraído las convenciones comunes y se ha organizado la solución para separar el código de infraestructura y la lógica de negocio de la interfaz de usuario, los métodos de acción de MVC deben ser sumamente ligeros:
[HttpPut("{id}")]
[ValidateAuthorExists]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
await _authorRepository.UpdateAsync(author);
return Ok();
}
Puede obtener más detalles sobre la implementación de filtros y descargar un ejemplo funcional en el artículo de MSDN Magazine ASP.NET Core: filtros reales de ASP.NET Core MVC.
Si descubre que tiene una serie de respuestas comunes de las API basadas en escenarios comunes, como errores de validación (solicitud no correcta), recursos no encontrados y errores de servidor, puede considerar la posibilidad de usar una abstracción de resultados. Los servicios consumidos por los puntos de conexión de API devolverían la abstracción de resultados, y la acción o el punto de conexión del controlador usarían un filtro para traducirlos en IActionResults
.
Referencias: estructuración de aplicaciones
- Áreas
https://learn.microsoft.com/aspnet/core/mvc/controllers/areas- MSDN Magazine: Sectores de características para ASP.NET Core MVC
https://learn.microsoft.com/archive/msdn-magazine/2016/september/asp-net-core-feature-slices-for-asp-net-core-mvc- Filtros
https://learn.microsoft.com/aspnet/core/mvc/controllers/filters- MSDN Magazine: filtros reales de ASP.NET Core MVC
https://learn.microsoft.com/archive/msdn-magazine/2016/august/asp-net-core-real-world-asp-net-core-mvc-filters- Resultado en eShopOnWeb
https://github.com/dotnet-architecture/eShopOnWeb/wiki/Patterns#result
Seguridad
La protección de las aplicaciones web es un tema amplio, con muchas consideraciones. En su nivel más básico, la seguridad implica asegurarse de saber de quién procede una solicitud determinada y, después, asegurarse de que la solicitud solo tiene acceso a los recursos que debe. La autenticación es el proceso de comparación de las credenciales proporcionadas con una solicitud con las de un almacén de datos de confianza, para ver si la solicitud se debe tratar como procedente de una entidad conocida. La autorización es el proceso de restringir el acceso a determinados recursos en función de la identidad del usuario. Un tercer interés de seguridad es proteger las solicitudes contra el espionaje por parte de terceros, para lo que al menos se debe asegurar de que la aplicación usa SSL.
Identidad
ASP.NET Core Identity es un sistema de pertenencia que se puede usar para admitir la funcionalidad de inicio de sesión para la aplicación. Tiene compatibilidad con cuentas de usuario locales y también con proveedores de inicio de sesión externo como Microsoft Account, Twitter, Facebook, Google y muchos más. Además de ASP.NET Core Identity, la aplicación puede usar la autenticación de Windows o un proveedor de identidades de terceros como Identity Server.
ASP.NET Core Identity se incluye en las nuevas plantillas de proyecto si se selecciona la opción Cuentas de usuario individuales. Esta plantilla incluye compatibilidad para el registro, inicio de sesión, inicios de sesión externos, contraseñas olvidadas y funcionalidad adicional.
Figura 7-3. Figura 7-3 Selección de Cuentas de usuario individuales para preconfigurar Identity.
La compatibilidad con identidades se configura en Program.cs o Startup
, e incluye la configuración de servicios, así como middleware.
Configuración de la identidad en Program.cs
En Program.cs, configure los servicios de la instancia WebHostBuilder
y, después, una vez creada la aplicación, configure su middleware. Los puntos clave que se deben tener en cuenta son la llamada a AddDefaultIdentity
para los servicios necesarios y las llamadas UseAuthentication
y UseAuthorization
que agregan el middleware necesario.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Configuración de la identidad al iniciar la aplicación
// Add framework services.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddMvc();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
Es importante que UseAuthentication
y UseAuthorization
aparezcan antes de MapRazorPages
. Al configurar los servicios de identidad, observará una llamada a AddDefaultTokenProviders
. Esto no tiene nada que ver con los tokens que se pueden usar para proteger las comunicaciones web, sino que, en su lugar, hace referencia a los proveedores que crean mensajes que se pueden enviar a los usuarios a través de SMS o correo electrónico para que confirmen su identidad.
Puede obtener más información sobre la configuración de la autenticación en dos fases y la habilitación de proveedores de inicio de sesión externos en la documentación oficial de ASP.NET Core.
Authentication
La autenticación es el proceso de determinar quién tiene acceso al sistema. Si usa ASP.NET Core Identity y los métodos de configuración que hemos visto en la sección anterior, se configurarán automáticamente algunos valores predeterminados de autenticación en la aplicación. Pero también puede configurar estos valores predeterminados de forma manual o reemplazar los valores definidos por AddIdentity. Si usa Identity, este sistema configura la autenticación basada en cookies como el esquema predeterminado.
En la autenticación basada en web, normalmente se pueden realizar hasta cinco acciones durante la autenticación de un cliente de un sistema. Dichos componentes son:
- Autenticar: use la información que proporciona el cliente a fin de crearle una identidad para que la use dentro de la aplicación.
- Desafiar: esta acción se usa para exigir que el cliente se identifique.
- Prohibir: informe al cliente de que tiene prohibido realizar una acción.
- Iniciar sesión: conserve el cliente existente de alguna manera.
- Cerrar sesión: quite el cliente de la persistencia.
Hay una serie de técnicas comunes para llevar a cabo la autenticación en aplicaciones web. Se conocen como "esquemas". Un esquema determinado definirá las acciones de algunas o de todas las opciones anteriores. Algunos esquemas solo admiten un subconjunto de acciones y pueden requerir un esquema independiente para realizar aquellas que no admiten. Por ejemplo, el esquema OpenId Connect (OIDC) no admite el inicio o el cierre de sesión, sino que se configura normalmente para usar la autenticación de cookies para esta persistencia.
En la aplicación de ASP.NET Core, puede configurar un esquema DefaultAuthenticateScheme
, así como esquemas específicos opcionales para cada una de las acciones descritas anteriormente. Por ejemplo, DefaultChallengeScheme
y DefaultForbidScheme
. Al llamar a AddIdentity, se configuran varios aspectos de la aplicación y se agregan muchos servicios necesarios. También se incluye esta llamada para configurar el esquema de autenticación:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
De forma predeterminada, estos esquemas usan cookies para la persistencia y el redireccionamiento a páginas de inicio de sesión para la autenticación. Estos esquemas son adecuados para las aplicaciones web que interactúan con los usuarios a través de exploradores web, pero no se recomiendan para las API. En su lugar, las API suelen usar otra forma de autenticación, como los tokens de portador JWT.
Las API web se consumen mediante código, como HttpClient
en aplicaciones .NET y tipos equivalentes en otros marcos de trabajo. Estos clientes esperan una respuesta que puedan usar de una llamada API, o bien un código de estado que indique qué problema se ha producido, si hubiera uno. Estos clientes no interactúan a través de un explorador y tampoco interactúan con ningún código HTML que pueda devolver una API, ni lo representan. Por lo tanto, no es adecuado que los puntos de conexión de API redirijan a sus clientes a páginas de inicio de sesión si estos no se han autenticado. Hay otro esquema más adecuado.
Para configurar la autenticación para las API, puede establecer la siguiente autenticación, la cual usa el proyecto PublicApi
en la aplicación eShopOnWeb de referencia:
builder.Services
.AddAuthentication(config =>
{
config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(config =>
{
config.RequireHttpsMetadata = false;
config.SaveToken = true;
config.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
Aunque se pueden configurar varios esquemas de autenticación diferentes en un único proyecto, es mucho más sencillo configurar un único esquema predeterminado. Por este motivo, entre otros, la aplicación de referencia eShopOnWeb separa sus API en su propio proyecto, PublicApi
, que es independiente del proyecto Web
principal que incluye las vistas y las instancias de Razor Pages de la aplicación.
Autenticación en aplicaciones Blazor
Las aplicaciones Blazor Server pueden aprovechar las mismas características de autenticación que cualquier otra aplicación ASP.NET Core. Sin embargo, las aplicaciones BlazorWebAssembly no pueden usar los proveedores de identidad de autenticación integrados, ya que se ejecutan en el explorador. Las aplicaciones BlazorWebAssembly pueden almacenar el estado de autenticación de los usuarios localmente y acceder a las notificaciones para determinar qué acciones deberían poder llevar a cabo los usuarios. Todas las comprobaciones de autenticación y autorización deben realizarse en el servidor, independientemente de la lógica implementada dentro de la aplicación BlazorWebAssembly, ya que los usuarios pueden omitir fácilmente la aplicación e interactuar directamente con las API.
Referencias: autenticación
- Acciones de autenticación y valores predeterminados
https://stackoverflow.com/a/52493428- Autenticación y autorización para aplicaciones de página única
https://learn.microsoft.com/aspnet/core/security/authentication/identity-api-authorization- Autenticación y autorización de Blazor de ASP.NET Core
https://learn.microsoft.com/aspnet/core/blazor/security/- Seguridad: Autenticación y autorización en ASP.NET Web Forms y Blazor
https://learn.microsoft.com/dotnet/architecture/blazor-for-web-forms-developers/security-authentication-authorization
Autorización
La forma más sencilla de autorización implica restringir el acceso a los usuarios anónimos. Esta función se puede lograr mediante la aplicación del atributo [Authorize]
a determinados controladores o acciones. Si se usan roles, el atributo se puede ampliar más para restringir el acceso a los usuarios que pertenecen a roles concretos, como se muestra a continuación:
[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{
}
En este caso, los usuarios que pertenecen a los roles HRManager
o Finance
(o a ambos) tendrían acceso a SalaryController. Para requerir que un usuario pertenezca a varios roles (no solo a uno de varios), se puede aplicar el atributo varias veces y especificar un rol necesario cada vez.
La especificación de determinados conjuntos de roles como cadenas en muchos controladores y acciones diferentes puede dar lugar a repeticiones no deseadas. Como mínimo, defina las constantes para estos literales de cadena y use las constantes donde necesite especificar la cadena. También se pueden configurar directivas de autorización, que encapsulan las reglas de autorización y, después, especificar la directiva en lugar de roles individuales al aplicar el atributo [Authorize]
:
[Authorize(Policy = "CanViewPrivateReport")]
public IActionResult ExecutiveSalaryReport()
{
return View();
}
Al usar las directivas de esta manera, se pueden separar los tipos de acciones que se restringen de las reglas o roles específicos a los que se aplican. Más adelante, si se crea un rol que necesita tener acceso a recursos concretos, se puede actualizar simplemente una directiva, en lugar de actualizar cada lista de roles en todos los atributos [Authorize]
.
Notificaciones
Las notificaciones son pares nombre-valor que representan las propiedades de un usuario autenticado. Por ejemplo, es posible almacenar el número de empleado de los usuarios como una notificación. Después, las notificaciones se pueden usar como parte de las directivas de autorización. Podría crear una directiva denominada "EmployeeOnly" que requiera la existencia de una notificación denominada "EmployeeNumber"
, como se muestra en este ejemplo:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
}
Después, esta directiva se podría usar con el atributo [Authorize]
para proteger cualquier controlador o acción, como se describió anteriormente.
Protección de las API web
La mayoría de las API web deben implementar un sistema de autenticación basado en tokens. La autenticación por tokens carece de estado y está diseñada para que sea escalable. En un sistema de autenticación basado en tokens, el cliente debe autenticarse primero con el proveedor de autenticación. Si se realiza correctamente, se emite un token para el cliente, que simplemente es una cadena de caracteres criptográficamente significativa. El formato más común para los tokens es JSON Web Token o JWT (que a menudo se pronuncia "jot"). Después, cuando el cliente tiene que emitir una solicitud a una API, agrega este token como un encabezado a la solicitud. Luego, el servidor valida el token que se encuentra en el encabezado de solicitud antes de completar la solicitud. En la figura 7-4 se muestra este proceso.
Figura 7-4. Autenticación basada en tokens para las API web.
Puede crear su propio servicio de autenticación, realizar la integración con Azure AD y OAuth o implementar un servicio mediante una herramienta de código abierto como IdentityServer.
Los tokens JWT pueden insertar notificaciones sobre el usuario que se pueden leer en el cliente o en el servidor. Puede usar una herramienta como jwt.io para ver el contenido de un token JWT. No almacene datos confidenciales (como contraseñas o claves) en tokens de JWT, ya que su contenido se puede leer fácilmente.
Al usar tokens JWT con aplicaciones de página única o BlazorWebAssembly, debe almacenar el token en algún lugar del cliente y, después, agregarlo a cada llamada API. Esta actividad se suele hacer como encabezado, tal y como se muestra en el código siguiente:
// AuthService.cs in BlazorAdmin project of eShopOnWeb
private async Task SetAuthorizationHeader()
{
var token = await GetToken();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
Después de llamar al método anterior, las solicitudes realizadas con _httpClient
tendrán el token insertado en los encabezados de solicitud, lo que permite que la API del lado servidor autentique y autorice la solicitud.
Seguridad personalizada
Precaución
Como regla general, evite realizar sus propias implementaciones de seguridad personalizadas.
Tenga especial cuidado al crear una implementación de criptografía, una pertenencia de usuarios o un sistema de generación de tokens propios. Existen muchas alternativas comerciales y de código abierto disponibles que seguramente ofrecerán una mayor seguridad que una implementación personalizada.
Referencias: seguridad
- Introducción a la seguridad de ASP.NET Core
https://learn.microsoft.com/aspnet/core/security/- Exigir SSL en una aplicación ASP.NET básica
https://learn.microsoft.com/aspnet/core/security/enforcing-ssl- Introducción a Identity
https://learn.microsoft.com/aspnet/core/security/authentication/identity- Introducción a la autorización
https://learn.microsoft.com/aspnet/core/security/authorization/introduction- Autenticación y autorización para aplicaciones de API en Azure App Service
https://learn.microsoft.com/azure/app-service-api/app-service-api-authentication- Servidor de identidades
https://github.com/IdentityServer
Comunicación de cliente
Además de servir páginas y responder a las solicitudes de datos a través de las API web, las aplicaciones ASP.NET Core se pueden comunicar directamente con los clientes conectados. En esta comunicación de salida se puede usar una amplia variedad de tecnologías de transporte, siendo WebSockets la más común. ASP.NET Core SignalR es una biblioteca que simplifica la funcionalidad de agregar comunicación de servidor a cliente en tiempo real en las aplicaciones. SignalR admite diversas tecnologías de transporte, incluyendo WebSockets, y abstrae muchos de los detalles de implementación del desarrollador.
La comunicación de cliente en tiempo real, con independencia de que se use WebSockets directamente u otras técnicas, es útil en una variedad de escenarios de aplicación. Estos son algunos ejemplos:
Aplicaciones de salón de chat en vivo
Aplicaciones de supervisión
Actualizaciones de progreso de trabajo
Notificaciones
Aplicaciones de formularios interactivos
Al compilar la comunicación de cliente en las aplicaciones, normalmente hay dos componentes:
El administrador de conexiones del lado servidor (SignalR Hub, WebSocketManager WebSocketHandler)
La biblioteca del lado cliente
Los clientes no están limitados a los exploradores: aplicaciones móviles, aplicaciones de consola y otras aplicaciones nativas también se pueden comunicar mediante SignalR/WebSockets. El siguiente programa sencillo devuelve a la consola todo el contenido enviado a una aplicación de chat, como parte de una aplicación de ejemplo WebSocketManager:
public class Program
{
private static Connection _connection;
public static void Main(string[] args)
{
StartConnectionAsync();
_connection.On("receiveMessage", (arguments) =>
{
Console.WriteLine($"{arguments[0]} said: {arguments[1]}");
});
Console.ReadLine();
StopConnectionAsync();
}
public static async Task StartConnectionAsync()
{
_connection = new Connection();
await _connection.StartConnectionAsync("ws://localhost:65110/chat");
}
public static async Task StopConnectionAsync()
{
await _connection.StopConnectionAsync();
}
}
Considere las formas en que las aplicaciones se comunican directamente con las aplicaciones cliente, y considere si la comunicación en tiempo real mejoraría la experiencia del usuario de la aplicación.
Referencias: comunicación de cliente
- SignalR de ASP.NET Core
https://github.com/dotnet/aspnetcore/tree/main/src/SignalR- Administrador de WebSocket
https://github.com/radu-matei/websocket-manager
¿Se debe aplicar el diseño controlado por dominios?
El Diseño controlado por dominios (DDD) es un enfoque ágil para la creación de software que resalta centrarse en el dominio de negocio. Coloca un gran énfasis en la comunicación e interacción con los expertos de dominio de negocio, que pueden identificarse con los desarrolladores sobre cómo funciona el sistema del mundo real. Por ejemplo, si está creando un sistema que controla cotizaciones de bolsa, es posible que el experto de dominio sea un broker de bolsa con experiencia. DDD está diseñado para resolver problemas empresariales grandes y complejos, y no suele ser adecuado para aplicaciones más pequeñas y sencillas, dado que la inversión necesaria para comprender y modelar el dominio no es rentable.
Al compilar software con un enfoque de DDD, el equipo (incluidas las partes interesadas sin experiencia técnica y los colaboradores) deberían desarrollar un lenguaje ubicuo para el espacio del problema. Es decir, se debe usar la misma terminología para el concepto del mundo real que se está modelando, el equivalente de software y las estructuras que podrían existir para conservar el concepto (por ejemplo, las tablas de base de datos). Por tanto, los conceptos descritos en el lenguaje ubicuo deberían ser la base del modelo de dominio.
El modelo de dominio se compone de objetos que interactúan entre sí para representar el comportamiento del sistema. Estos objetos se dividen en las categorías siguientes:
Entidades, que representan objetos con un subproceso de identidad. Normalmente las entidades se almacenan en la persistencia con una clave por la que después se pueden recuperar.
Agregados, que representan grupos de objetos que se deben conservar como una unidad.
Objetos de valor, que representan conceptos que se pueden comparar en función de la suma de sus valores de propiedad. Por ejemplo, DateRange consta de una fecha de inicio y una fecha de finalización.
Eventos de dominio, que representan lo que ocurre en el sistema que resulta de interés para otros elementos del sistema.
Un modelo de dominio de DDD debe encapsular un comportamiento complejo dentro del modelo. Las entidades, en concreto, no deben ser simplemente colecciones de propiedades. Cuando el modelo de dominio carece de comportamiento y simplemente representa el estado del sistema, se dice que es un modelo anémico, lo que no es deseable en DDD.
Además de estos tipos de modelo, DDD normalmente emplea diversos modelos:
Repositorio, para abstraer los detalles de persistencia.
Generador, para encapsular la creación de objetos complejos.
Servicios, para encapsular un comportamiento complejo o los detalles de implementación de la infraestructura.
Comando, para desacoplar la emisión de comandos y la ejecución del propio comando.
Especificación, para encapsular los detalles de la consulta.
DDD también recomienda el uso de la arquitectura limpia que se describió anteriormente, lo que permite acoplamiento débil, encapsulación y código que se pueda comprobar fácilmente mediante pruebas unitarias.
Casos en los que se debe aplicar DDD
DDD es ideal para aplicaciones de gran tamaño con complejidad de negocio significativa (no solo técnica). La aplicación debe requerir el conocimiento de expertos de dominio. Debe haber un comportamiento importante en el propio modelo de dominio, que represente las reglas de negocio e interacciones más allá de simplemente almacenar y recuperar el estado actual de diferentes registros desde almacenes de datos.
Casos en los que no se debe aplicar DDD
DDD implica invertir en modelado, arquitectura y comunicación, lo que puede que no esté garantizado para aplicaciones más pequeñas o aplicaciones que esencialmente son de tipo CRUD (crear/leer/actualizar/eliminar). Si se elige el enfoque de DDD para la aplicación, pero se descubre que el dominio tiene un modelo anémico sin comportamiento, es posible que haya que reconsiderar el enfoque. O la aplicación no necesita DDD o es posible que necesite asistencia para refactorizarla con el fin de encapsular la lógica de negocios en el modelo de dominio, en lugar de la interfaz de usuario o la base de datos.
Un enfoque híbrido consistiría en usar DDD solo para las áreas más complejas o transaccionales de la aplicación, pero no para partes CRUD más sencillas o de solo lectura. Por ejemplo, no se necesitan las restricciones de un agregado si se están consultando datos para mostrar un informe o visualizar datos para un panel. Es absolutamente aceptable tener un modelo de lectura más sencillo e independiente para estos requisitos.
Referencias: diseño controlado por dominios
- DDD in Plain English (StackOverflow Answer) (DDD en términos sencillos [respuesta en StackOverflow])
https://stackoverflow.com/questions/1222392/can-someone-explain-domain-driven-design-ddd-in-plain-english-please/1222488#1222488
Implementación
En el proceso de implementación de la aplicación ASP.NET Core hay algunos pasos implicados, independientemente de dónde se vaya a hospedar. El primer paso consiste en publicar la aplicación, lo que se puede hacer con el comando de CLI dotnet publish
. Este paso compilará la aplicación y colocará todos los archivos necesarios para ejecutarla en una carpeta designada. Cuando se implementa desde Visual Studio, este paso se realiza de forma automática. La carpeta publish contiene archivos .exe y .dll de la aplicación y sus dependencias. Una aplicación independiente también incluirá una versión del runtime de .NET. Las aplicaciones ASP.NET Core también incluyen archivos de configuración, activos de cliente estáticos y vistas MVC.
Las aplicaciones ASP.NET Core son aplicaciones de consola que se deben iniciar cuando se inicia el servidor y reiniciarse si la aplicación (o el servidor) se bloquea. Para automatizar este proceso se puede usar un administrador de procesos. Los administradores de procesos más comunes para ASP.NET Core son Nginx y Apache en Linux, e IIS y Servicio de Windows en Windows.
Además de un administrador de procesos, las aplicaciones de ASP.NET Core pueden usar un servidor proxy inverso. Un servidor proxy inverso recibe las solicitudes HTTP de Internet y las reenvía a Kestrel después de un control preliminar. Los servidores proxy inversos proporcionan una capa de seguridad para la aplicación. Kestrel tampoco admite el hospedaje de varias aplicaciones en el mismo puerto, por lo que no se pueden usar técnicas como los encabezados de host con él para habilitar el hospedaje de varias aplicaciones en el mismo puerto y dirección IP.
Figura 7-5. ASP.NET hospedado en Kestrel detrás de un servidor proxy inverso
Otro escenario en el que un proxy inverso puede ser útil es para proteger varias aplicaciones mediante SSL/HTTPS. En este caso, solo sería necesario configurar SSL en el proxy inverso. La comunicación entre el servidor proxy inverso y Kestrel se podría llevar a cabo a través de HTTP, como se muestra en la figura 7-6.
Figura 7-6. ASP.NET hospedado detrás de un servidor proxy inverso protegido mediante HTTPS
Un enfoque cada vez más popular consiste en hospedar la aplicación ASP.NET Core en un contenedor de Docker, que después se puede hospedar localmente o implementar en Azure para hospedaje basado en la nube. El contenedor de Docker podría contener el código de aplicación que se ejecuta en Kestrel y se implementaría detrás de un servidor proxy inverso, como se mostró anteriormente.
Si la aplicación se va a hospedar en Azure, se puede usar Microsoft Azure Application Gateway como un dispositivo virtual dedicado para proporcionar varios servicios. Además de actuar como un proxy inverso para aplicaciones individuales, Application Gateway también puede ofrecer las características siguientes:
Equilibrio de carga HTTP
Descarga SSL (SSL solo para Internet)
SSL de extremo a extremo
Enrutamiento de varios sitios (hasta 20 sitios consolidados en una sola instancia de Application Gateway)
Firewall de aplicación web
Compatibilidad de WebSocket
Diagnósticos avanzados
Más información sobre las opciones de implementación de Azure en el capítulo 10.
Referencias: implementación
- Información general sobre implementación y hospedaje
https://learn.microsoft.com/aspnet/core/publishing/- Casos en los que usar Kestrel con un proxy inverso
https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel#when-to-use-kestrel-with-a-reverse-proxy- Hospedar aplicaciones ASP.NET Core en contenedores de Docker
https://learn.microsoft.com/aspnet/core/publishing/docker- Introducción a Azure Application Gateway
https://learn.microsoft.com/azure/application-gateway/application-gateway-introduction