Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Sugerencia
Este contenido es un extracto del libro electrónico, Arquitecto de aplicaciones web modernas con ASP.NET Core y Azure, disponible en .NET Docs o como un PDF descargable gratuito que se puede leer sin conexión.
"No es importante hacerlo bien la primera vez. Es vital hacerlo bien por última vez". - Andrew Hunt y David Thomas
ASP.NET Core es un marco de código abierto multiplataforma para crear aplicaciones web modernas optimizadas para la nube. ASP.NET Las aplicaciones Core son ligeras y modulares, con compatibilidad integrada para la inserción de dependencias, lo que permite una mayor capacidad de prueba y mantenimiento. Combinado 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 crear aplicaciones web empresariales.
MVC y Razor Pages
ASP.NET Core MVC ofrece muchas características que son útiles para crear aplicaciones y API basadas en web. El término MVC significa "Model-View-Controller", un patrón de interfaz de usuario que divide las responsabilidades de responder a las solicitudes del usuario en varias partes. Además de seguir este patrón, también puede implementar características en las aplicaciones de ASP.NET Core como Razor Pages.
Las páginas de Razor están integradas en ASP.NET Core MVC y usan las mismas características para el enrutamiento, el enlace de modelos, los filtros, la autorización, etc. Sin embargo, en lugar de tener carpetas y archivos independientes para Controladores, Modelos, Vistas, etc. y mediante el enrutamiento basado en atributos, Razor Pages se coloca en una sola carpeta ("/Pages"), ruta basada en su ubicación relativa en esta carpeta y controla las solicitudes con 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 patrones relacionados en la aplicación de ejemplo eShopOnWeb.
Al crear una nueva aplicación ASP.NET Core, debe tener un plan en mente para el tipo de aplicación que desea desarrollar. Al crear un nuevo proyecto, en el IDE o mediante el comando de la dotnet new
CLI, elegirá entre varias plantillas. Las plantillas de proyecto más comunes son Empty, Web API, Web App y Web App (Model-View-Controller). Aunque solo puede tomar esta decisión cuando cree por primera vez un proyecto, no es una decisión irreversible. El proyecto de API web usa controladores estándar de modeloView-Controller: solo carece de vistas de forma predeterminada. Del mismo modo, la plantilla de aplicación web predeterminada usa Razor Pages, por lo que también carece de una carpeta Views. Puede agregar una carpeta Views a estos proyectos más adelante para admitir el comportamiento basado en vistas. Los proyectos de API web y ModeloView-Controller no incluyen una carpeta Páginas de forma predeterminada, pero puede agregar una más adelante para admitir el comportamiento basado en Razor Pages. Puede considerar estas tres plantillas como compatibles con tres tipos diferentes de interacción de usuario predeterminada: datos (API web), basados en páginas y basados en vistas. Sin embargo, puede mezclar y hacer coincidir cualquiera o todas estas plantillas dentro de un solo proyecto si lo desea.
¿Por qué Razor Pages?
Razor Pages es el enfoque predeterminado para las nuevas aplicaciones web en Visual Studio. Razor Pages ofrece una manera más sencilla de crear características de aplicación basadas en páginas, como formularios que no son SPA. Con controladores y vistas, era habitual que las aplicaciones tuvieran controladores muy grandes que funcionaban con muchas dependencias y modelos de vista diferentes y devolvieron muchas vistas diferentes. Esto dio lugar a una mayor complejidad y, a menudo, a los controladores que no seguían el principio de responsabilidad única o los principios abiertos y cerrados de forma eficaz. Razor Pages soluciona este problema mediante la encapsulación de la lógica del lado servidor para una "página lógica" específica en una aplicación web mediante su marcado de Razor. Una página de Razor que no tiene lógica del lado servidor solo puede constar de un archivo de Razor (por ejemplo, "Index.cshtml"). Sin embargo, la mayoría de las páginas de Razor no triviales tendrán una clase de modelo de página asociada, que por convención se denomina igual que el archivo de Razor con una extensión ".cs" (por ejemplo, "Index.cshtml.cs").
Un modelo de página de Razor Page combina las responsabilidades de un controlador MVC y un modelo de vista. En lugar de controlar las solicitudes con métodos de acción del controlador, se ejecutan controladores de modelo de página como "OnGet()", que representan su página asociada de forma predeterminada. Razor Pages simplifica el proceso de creación de páginas individuales en una aplicación ASP.NET Core, a la vez que proporciona todas las características arquitectónicas de ASP.NET Core MVC. Son una buena opción predeterminada para la nueva funcionalidad basada en páginas.
Cuándo usar MVC
Si estás desarrollando APIs web, el patrón MVC tiene más sentido que intentar usar Razor Pages. Si el proyecto solo expondrá puntos de conexión de API web, lo ideal es empezar desde 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 de ASP.NET Core. Use el enfoque de MVC basado en vistas si va a migrar una aplicación existente de ASP.NET MVC 5 o anterior a ASP.NET Core MVC y desea hacerlo con la menor cantidad de esfuerzo. Una vez que haya realizado la migración inicial, puede evaluar si tiene sentido adoptar Razor Pages para nuevas características o incluso como una migración mayorista. Para obtener más información sobre cómo migrar aplicaciones de .NET 4.x a .NET 8, consulte Migración de aplicaciones de ASP.NET existentes a ASP.NET Core eBook.
Tanto si decide compilar la aplicación web mediante páginas de Razor o vistas de MVC, la aplicación tendrá un rendimiento similar e incluirá compatibilidad con la inserción de dependencias, filtros, enlace de modelos, validación, etc.
Asignación de solicitudes a respuestas
En su corazón, las aplicaciones ASP.NET Core asignan las solicitudes entrantes a las respuestas salientes. A nivel básico, este mapeo se realiza con middleware, y las aplicaciones simples de ASP.NET Core y los microservicios pueden estar compuestos únicamente de middleware específico. Al usar ASP.NET Core MVC, puede trabajar en un nivel algo más alto, pensando en las 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 (que pertenece 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).
ASP.NET aplicaciones de Core MVC pueden usar rutas convencionales, rutas de atributo o ambas. Las rutas convencionales se definen en el código, especificando convenciones de enrutamiento mediante sintaxis como en el ejemplo siguiente:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});
En este ejemplo, se ha agregado una ruta denominada "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 por la aplicación de un "?" a este. La convención definida aquí indica que la primera parte de una solicitud debe corresponder al nombre del controlador, la segunda parte a la acción y, si es necesario, una tercera parte representará un parámetro ID. Las rutas convencionales normalmente 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 controladores y acciones, en lugar de especificarse globalmente. Este enfoque tiene la ventaja de hacerlas mucho más reconocibles cuando se examina un método determinado, pero significa que la información de enrutamiento no se mantiene en un solo lugar de la aplicación. Con las rutas de atributo, puede 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 [HttpGet] y atributos similares, lo que evita la necesidad de agregar atributos [Route] independientes. Las rutas de atributo también pueden usar tokens para reducir la necesidad de repetir nombres de controlador o acción, 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 entero id
. Por ejemplo, la página Products.cshtml ubicada en la raíz de /Pages
respondería a solicitudes como esta:
/Products/123
Una vez que una solicitud determinada se ha combinado con una ruta, pero antes de llamar al método de acción, ASP.NET Core MVC realizará el enlace de modelos y la validación de modelos en la solicitud. El enlace de modelos es responsable de convertir los datos HTTP entrantes en los tipos de .NET especificados como parámetros del método de acción al que se va a llamar. Por ejemplo, si el método de acción espera un int id
parámetro, el enlace de modelos intentará proporcionar este parámetro a partir de un valor proporcionado como parte de la solicitud. Para ello, la vinculación de modelos busca valores en un formulario publicado, valores en la propia ruta y valores en la cadena de consulta. Suponiendo que se encuentra un id
valor, se convertirá en un entero antes de pasarse 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 de modelos usa atributos opcionales en el tipo de modelo y puede ayudar a garantizar que el objeto de modelo proporcionado se ajusta a determinados requisitos de datos. Algunos valores pueden especificarse según sea necesario o limitarse a una longitud determinada o un intervalo numérico, 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 enviar al cliente que realiza la solicitud.
Si usa la validación del modelo, debe asegurarse de comprobar siempre que el modelo es válido antes de realizar cualquier comando de modificación de estado, para asegurarse de que la aplicación no está dañada por datos no válidos. Puede usar un filtro para evitar la necesidad de agregar código para esta validación en cada acción. Filtros de ASP.NET Core MVC ofrecen una manera de interceptar grupos de solicitudes, de modo que las directivas comunes y las preocupaciones transversales se puedan aplicar en un modo específico. Los filtros se pueden aplicar a acciones individuales, controladores completos o globalmente para una aplicación.
En el caso de las API web, ASP.NET Core MVC admite la negociación de contenido, lo que permite a las solicitudes especificar cómo se deben dar formato a las respuestas. En función de los encabezados proporcionados en la solicitud, las acciones que devuelven datos darán formato a la respuesta en XML, JSON u otro formato compatible. Esta característica permite que varios clientes usen la misma API con distintos requisitos de formato de datos.
Los proyectos de API web deben considerar el uso del [ApiController]
atributo , que se puede aplicar a controladores individuales, a una clase de controlador base o a todo el ensamblado. Este atributo agrega la comprobación automática de validación de modelos y cualquier acción con un modelo no válido devolverá un 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 usar una ruta convencional, y devuelva información de ProblemDetails más detallada en respuesta a errores.
Mantener los controladores bajo control
En el caso de las aplicaciones basadas en páginas, Razor Pages realiza un gran trabajo para evitar que los controladores sean demasiado grandes. Cada página individual recibe sus propios archivos y clases dedicados solo a sus controladores. Antes de la introducción de Razor Pages, muchas aplicaciones centradas en vistas tendrían clases de controlador grandes responsables de muchas acciones y vistas diferentes. Estas clases crecerían naturalmente para tener muchas responsabilidades y dependencias, lo que hace que sean más difíciles de mantener. 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 clases, al tiempo que permite la comunicación entre ellas. En las aplicaciones ASP.NET Core MVC, este patrón se emplea con frecuencia para dividir los controladores en partes más pequeñas mediante el uso de manejadores 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, cada uno de los cuales puede requerir determinadas dependencias. El conjunto de todas las dependencias requeridas por cualquier acción debe pasarse al constructor del controlador. Al usar MediatR, la única dependencia que tendrá un controlador suele ser una instancia del mediador. A continuación, cada acción usa la instancia de mediador para enviar un mensaje, que un controlador procesa. El controlador es específico de una sola acción y, por tanto, solo necesita las dependencias requeridas por esa acción. Aquí se muestra un ejemplo de un controlador mediante 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 MyOrders
acción, esta clase controla la llamada a Send
un GetMyOrders
mensaje:
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 sean mucho más pequeños y se centren principalmente en el enrutamiento y el enlace del modelo, mientras que los 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 paquete NuGet ApiEndpoints, que intenta traer a los controladores de API las mismas ventajas que Razor Pages aporta a los controladores basados en vistas.
Referencias: asignación de solicitudes a respuestas
- Enrutamiento a acciones del controlador
https://learn.microsoft.com/aspnet/core/mvc/controllers/routing- Vinculación de Modelos
https://learn.microsoft.com/aspnet/core/mvc/models/model-binding- Validación de modelos
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 e internamente hace uso de una técnica conocida como inserción de dependencias. La inserción de dependencias es una técnica que permite el acoplamiento flexible entre los distintos elementos de una aplicación. El acoplamiento flexible es deseable porque facilita el aislamiento de partes de la aplicación, lo que permite realizar pruebas o reemplazos. También hace menos probable que un cambio en una parte 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 clave para lograr el principio abierto y cerrado. Al evaluar cómo funciona la aplicación con sus dependencias, tenga cuidado con el olor a código rígido y recuerde el aforismo "el nuevo es pegamento".
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 de base de datos interrumpirá el método. Probar estos métodos es notoriamente difícil, ya que estas pruebas requieren bibliotecas de simulación comercial para simular las llamadas estáticas, o solo se pueden probar con una base de datos de prueba en su lugar. Las llamadas estáticas que no tienen ninguna dependencia de la infraestructura, especialmente aquellas que son completamente sin estado, son adecuadas para llamarse y no tienen ningún impacto en el acoplamiento o la capacidad de prueba (más allá del acoplamiento del código a la propia llamada estática).
Muchos desarrolladores entienden los riesgos del acoplamiento estático y el estado global. A pesar de ello, seguirán acoplando estrechamente su código a implementaciones específicas mediante la instanciación directa. "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
. Al igual que con las llamadas a métodos estáticos, las nuevas instancias de tipos que no tienen dependencias externas normalmente no acoplan estrechamente el código a los detalles de implementación o hacen que las pruebas sean más difíciles. Pero cada vez que se crea una instancia de una clase, dedique un breve momento a tener en cuenta si tiene sentido codificar esa instancia específica en esa ubicación concreta o si sería un mejor diseño solicitar esa instancia como dependencia.
Declaración de las dependencias
ASP.NET Core se basa en la creación de métodos y clases que declaran sus dependencias, solicitándolas como argumentos. Las aplicaciones de ASP.NET generalmente se configuran en Program.cs o en una clase Startup
.
Nota:
La configuración de aplicaciones completamente 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 las dependencias directamente en el archivo Program.cs usando un WebApplicationBuilder
. Una vez agregados 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
La Startup.cs está configurada para admitir la inserción de dependencias en varios puntos. Si usa una clase Startup
, puede darle un constructor y puede pedir dependencias a través de ella, 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 Startup
clase es interesante en que no hay requisitos de tipo explícitos para ella. No hereda de una clase base especial Startup
ni implementa ninguna interfaz determinada. Puede darle un constructor, o no, y puede especificar tantos parámetros en el constructor como desee. Cuando se inicie el host web que ha configurado para la aplicación, llamará a la Startup
clase (si le ha dicho que use una) y usará la inserción de dependencias para rellenar las dependencias que requiere la Startup
clase. Por supuesto, si solicita parámetros que no están configurados en el contenedor de servicios usado por ASP.NET Core, obtendrá una excepción, pero siempre que se ajuste a las dependencias que conoce el contenedor, puede solicitar todo lo que desee.
La inserción de dependencias se integra en las aplicaciones de ASP.NET Core directamente desde el principio, al crear la instancia de inicio. La clase Startup no termina aquí. También puede solicitar dependencias en el Configure
método :
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
}
El método ConfigureServices es la excepción a este comportamiento; debe tomar solo un parámetro de tipo IServiceCollection
. Realmente no es necesario 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 IServiceCollection
parámetro . Por lo tanto, puede trabajar con dependencias definidas en la colección de servicios principales de ASP.NET en cada parte de la Startup
clase, ya sea solicitando el servicio necesario como parámetro o trabajando con IServiceCollection
en ConfigureServices
.
Nota:
Si necesita asegurarse de que determinados servicios están disponibles para su Startup
clase, puede configurarlos mediante un IWebHostBuilder
y su ConfigureServices
método dentro de la CreateDefaultBuilder
llamada.
La clase Startup es un modelo para cómo debe estructurar otras partes de la aplicación ASP.NET Core, desde Controladores a Middleware a Filtros a sus propios servicios. En cada caso, debe seguir el principio de dependencias explícitas, 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 crea instancias directas de implementaciones, especialmente servicios y objetos que funcionan con infraestructura o tienen efectos secundarios. Prefiere trabajar con abstracciones definidas en el núcleo de tu aplicación y pásalas como argumentos, en lugar de codificar rígidamente referencias a tipos de implementación específicos.
Estructuración de la aplicación
Normalmente, las aplicaciones monolíticas tienen 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. Sin embargo, eso no significa que la solución debe constar de un solo proyecto. Resulta útil dividir la aplicación en diferentes capas con el fin de seguir la separación de problemas. Una vez divididas en capas, resulta útil ir más allá de carpetas para separar proyectos, lo que puede ayudar a lograr una mejor encapsulación. El mejor enfoque para lograr estos objetivos con una aplicación ASP.NET Core es una variación de la arquitectura limpia que se describe en el capítulo 5. Después de este enfoque, la solución de la aplicación incluirá 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 tratan en el capítulo 9).
El modelo de objetos y las interfaces de la aplicación deben colocarse en el proyecto ApplicationCore. Este proyecto tendrá tan pocas dependencias como sea posible (y ninguna en problemas de infraestructura específicos), y los demás proyectos de la solución harán referencia a él. Las entidades empresariales que deben conservarse se definen en el proyecto ApplicationCore, ya que son servicios que no dependen directamente de la infraestructura.
Detalles de implementación, como cómo se realiza la persistencia o cómo se pueden enviar notificaciones a un usuario, se conservan en el proyecto de infraestructura. Este proyecto hará referencia a paquetes específicos de implementación, como Entity Framework Core, pero no debe exponer detalles sobre estas implementaciones fuera del proyecto. Los servicios y repositorios de infraestructura deben implementar interfaces definidas en el proyecto ApplicationCore y sus implementaciones de persistencia son responsables de recuperar y almacenar entidades definidas en ApplicationCore.
El proyecto de interfaz de usuario principal de ASP.NET es responsable de cualquier problema de nivel de interfaz de usuario, pero no debe incluir la lógica de negocios ni los detalles de la infraestructura. De hecho, lo ideal es que ni siquiera tenga una dependencia en el proyecto de infraestructura, lo que ayudará a garantizar que no se introduzca ninguna dependencia entre los dos proyectos accidentalmente. 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 es tener los microservicios de llamada a la aplicación, quizás implementados en contenedores individuales de Docker. 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 funcionalidades
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 se almacena normalmente por separado en la carpeta wwwroot. Sin embargo, las aplicaciones grandes pueden encontrar problemas con esta organización, ya que trabajar en cualquier característica determinada a menudo requiere saltar entre estas carpetas. Esto resulta más y más difícil a medida que crece el número de archivos y subcarpetas de cada carpeta, lo que da lugar a una gran cantidad de desplazamiento por el Explorador de soluciones. Una solución a este problema es organizar el código de aplicación por característica en lugar de por tipo de archivo. Este estilo organizativo se conoce normalmente como carpetas de características o segmentos de características (consulte también: Segmentos verticales).
ASP.NET Core MVC admite áreas para este propósito. Usando áreas, puede crear conjuntos independientes de carpetas de Controladores y Vistas (así como cualquier modelo asociado) en cada carpeta Área. En la figura 7-1 se muestra una estructura de carpetas de ejemplo, mediante Áreas.
Figura 7-1. Organización de áreas de ejemplo
Al usar Áreas, debe 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 áreas, también puede usar su propia estructura de carpetas y convenciones en lugar de atributos y rutas personalizadas. Esto le permitiría tener carpetas de características que no incluyeban carpetas independientes para vistas, controladores, etc., mantener la jerarquía más plana y facilitar la visualización de todos los archivos relacionados en un solo 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. Puede modificar o reemplazar estas convenciones. Por ejemplo, puede crear una convención que obtendrá automáticamente el nombre de la característica de un controlador determinado en función de su espacio de nombres (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;
}
}
A continuación, especifique esta convención como opción al agregar compatibilidad con MVC a la aplicación en ConfigureServices
(o 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 buscar 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 de trabajo del artículo de MSDN Magazine , Segmentos de características para ASP.NET Core MVC.
API y aplicaciones Blazor
Si la aplicación incluye un conjunto de API web, que deben protegerse, estas API deben configurarse idealmente como un proyecto independiente de la aplicación View o Razor Pages. Separar las API, especialmente 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 únicas de implementación y carga. También es muy probable que adopten diferentes mecanismos para la seguridad, con aplicaciones estándar basadas en formularios que aprovechan la autenticación basada en cookies y las API más probables mediante la autenticación basada en tokens.
Además, las aplicaciones Blazor, ya sea que usen Blazor Server o BlazorWebAssembly, deben desarrollarse como proyectos independientes. Las aplicaciones tienen diferentes características en tiempo de ejecución, así como modelos de seguridad. Es probable que compartan tipos comunes con la aplicación web del lado servidor (o proyecto de API) y estos tipos deben definirse en un proyecto compartido común.
La adición de una BlazorWebAssembly interfaz de administración a eShopOnWeb requiere agregar varios proyectos nuevos. El propio proyecto de BlazorWebAssembly, BlazorAdmin
; En el proyecto se define un nuevo conjunto de puntos de conexión de API públicos, usados por BlazorAdmin
y configurados para usar la PublicApi
autenticación basada en tokens. Ciertos tipos compartidos utilizados por estos proyectos se conservan en un nuevo proyecto BlazorShared
.
Podría preguntarse, ¿por qué agregar un proyecto independiente BlazorShared
cuando ya hay un proyecto común ApplicationCore
que podría usarse para compartir cualquier tipo requerido por ambos PublicApi
y BlazorAdmin
? La respuesta es que este proyecto incluye toda la lógica de negocios de la aplicación y, por tanto, es mucho mayor que lo necesario y, además, es mucho más probable que tenga que mantenerse protegido en el servidor. Recuerde que cualquier biblioteca a la que hace referencia BlazorAdmin
se descargará en los exploradores de los usuarios cuando carguen la aplicación Blazor.
En función de si se usa el patrón backends-For-Frontends (BFF), es posible que las API consumidas por la BlazorWebAssembly aplicación no compartan sus tipos 100% con Blazor. En concreto, una API pública que está pensada para ser consumida por muchos clientes diferentes puede definir sus propios tipos de solicitud y resultados, en lugar de compartirlos en un proyecto compartido específico del cliente. En el ejemplo de eShopOnWeb, se supone que el proyecto PublicApi
está, de hecho, hospedando una API pública, por lo que no todos sus tipos de solicitud y respuesta proceden del proyecto BlazorShared
.
Intereses transversales
A medida que crecen las aplicaciones, es cada vez más importante tener en cuenta los problemas transversales para eliminar la duplicación y mantener la coherencia. Algunos ejemplos de problemas transversales en ASP.NET Aplicaciones principales son la autenticación, las reglas de validación de modelos, el almacenamiento en caché de salida y el control de errores, aunque hay muchos otros. ASP.NET filtros de Core MVC permiten ejecutar código antes o después de determinados pasos en la canalización de procesamiento de solicitudes. Por ejemplo, un filtro puede ejecutarse 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 puede usar un filtro de autorización para controlar el acceso al resto de la canalización. Las figuras 7-2 muestran cómo fluye la ejecución de solicitudes a través de filtros, si están configurados.
Figura 7-2. Ejecución de solicitudes a través de filtros y canalización de solicitudes.
Normalmente, los filtros se implementan como atributos, por lo que puede aplicarlos a controladores o acciones (o incluso globalmente). Cuando se agregan de esta manera, los filtros especificados en el nivel de acción anulan o se construyen sobre los filtros especificados en el nivel de controlador, que a su vez reemplazan los filtros globales. Por ejemplo, el [Route]
atributo 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, a continuación, invalidarla mediante 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() {}
}
El primer método, Login, usa el [AllowAnonymous]
filtro (atributo) para invalidar el conjunto de filtros Authorize en el nivel de controlador. La ForgotPassword
acción (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 comunes de control de errores para las API. Por ejemplo, una directiva de API típica consiste en devolver una respuesta NotFound a las solicitudes que hacen referencia a claves que no existen y una respuesta si se produce un BadRequest
error en la validación del modelo. En el ejemplo siguiente se muestran estas dos directivas en acción:
[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 vuelvan desordenados con código condicional como este. En su lugar, organice las directivas en filtros que se pueden aplicar según las necesidades. En este ejemplo, la comprobación de validación del modelo, que debe producirse cada vez que se envía 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 ValidateModelAttribute
a su proyecto como una dependencia de NuGet mediante la inclusión del paquete Ardalis.ValidateModel. En el caso de las API, puede usar el ApiController
atributo para aplicar este comportamiento sin necesidad de un filtro independiente ValidateModel
.
Del mismo modo, se puede usar un filtro para comprobar si existe un registro y devolver un 404 antes de ejecutar la acción, lo que elimina la necesidad de realizar estas comprobaciones en la acción. Una vez que haya extraído las convenciones comunes y organizado la solución para separar el código de infraestructura y la lógica empresarial de la interfaz de usuario, los métodos de acción de MVC deben ser extremadamente finos:
[HttpPut("{id}")]
[ValidateAuthorExists]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
await _authorRepository.UpdateAsync(author);
return Ok();
}
Puede obtener más información sobre la implementación de filtros y descargar un ejemplo de trabajo en el artículo de MSDN Magazine, Real-World ASP.NET Filtros de MVC Core.
Si encuentra que tiene una serie de respuestas comunes de las API basadas en escenarios comunes, como errores de validación (solicitud incorrecta), errores de recurso 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: Funcionalidades 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 Real World 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 aplicaciones web es un tema grande, con muchas consideraciones. En su nivel más básico, la seguridad implica asegurarse de saber a quién procede una solicitud determinada y, a continuación, asegurarse de que la solicitud solo tiene acceso a los recursos que debe. La autenticación es el proceso de comparar las credenciales proporcionadas con una solicitud a las de un almacén de datos de confianza para ver si la solicitud debe tratarse 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 problema de seguridad es proteger las solicitudes de interceptación por parte de terceros, para las que debe asegurarse al menos de que la aplicación use SSL.
identidad
ASP.NET Core Identity es un sistema de pertenencia que puede usar para admitir la funcionalidad de inicio de sesión de la aplicación. Tiene compatibilidad con cuentas de usuario locales, así como compatibilidad con proveedores de inicio de sesión externos, como cuentas de Microsoft, Twitter, Facebook, Google, etc. 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 está incluido en las nuevas plantillas de proyecto si se selecciona la opción de Cuentas de usuario individuales. Esta plantilla incluye compatibilidad con el registro, el inicio de sesión, los inicios de sesión externos, las contraseñas olvidadas y la funcionalidad adicional.
Figura 7-3. Seleccione Cuentas de usuario individuales para que la identidad esté preconfigurada.
La configuración del soporte de identidades se realiza en Program.cs o Startup
, e incluye la configuración de servicios y middleware.
Configuración de la identidad en Program.cs
En Program.cs, configurará los servicios desde la WebHostBuilder
instancia y, después, una vez creada la aplicación, configurará su middleware. Los puntos clave que se deben tener en cuenta son la llamada a AddDefaultIdentity
para los servicios necesarios y las llamadas a 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 identidad en el inicio de 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 servicios de identidad, notarás una llamada a AddDefaultTokenProviders
. Esto no tiene nada que ver con tokens que se pueden usar para proteger las comunicaciones web, pero en su lugar hace referencia a proveedores que crean avisos 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 cómo configurar la autenticación en dos fases y habilitar proveedores de inicio de sesión externos desde los documentos oficiales de ASP.NET Core.
Autenticación
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 se muestran en la sección anterior, configurará automáticamente algunos valores predeterminados de autenticación en la aplicación. Sin embargo, también puede configurar estos valores predeterminados manualmente o invalidar los establecidos por AddIdentity. Si usa Identity, configura la autenticación basada en cookies como esquema predeterminado.
En la autenticación basada en web, normalmente hay hasta cinco acciones que se pueden realizar durante la autenticación de un cliente de un sistema. Estos son:
- Autenticar: Use la información proporcionada por el cliente para crear una identidad para que la usen en la aplicación.
- Desafío Esta acción se usa para requerir que el cliente se identifique a sí mismo.
- Prohibir. Informe al cliente de que está 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 varias técnicas comunes para realizar la autenticación en aplicaciones web. Estos se conocen como esquemas. Un esquema determinado definirá acciones para algunas o todas las opciones anteriores. Algunos esquemas solo admiten un subconjunto de acciones y pueden necesitar un esquema separado para realizar aquellas que no admiten. Por ejemplo, el esquema de OpenId-Connect (OIDC) no admite el inicio de sesión ni el cierre de sesión, pero normalmente está configurado para usar la autenticación de cookies para esta persistencia.
En su aplicación ASP.NET Core, puede configurar un DefaultAuthenticateScheme
así como esquemas específicos opcionales para cada una de las acciones descritas anteriormente. Por ejemplo, DefaultChallengeScheme
y DefaultForbidScheme
. La llamada a AddIdentity configura varios aspectos de la aplicación y añade muchos servicios necesarios. También 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;
});
Estos esquemas usan cookies para la persistencia y el redireccionamiento a las páginas de inicio de sesión para la autenticación de forma predeterminada. 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 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 utilizable de una llamada API o un código de estado que indica qué, si existe, problema. Estos clientes no interactúan a través de un explorador y no representan ni interactúan con ningún CÓDIGO HTML que pueda devolver una API. 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 no se autentican. Otro esquema es más adecuado.
Para configurar la autenticación para las API, puede configurar la autenticación como la siguiente, que usa el PublicApi
proyecto en la aplicación de referencia eShopOnWeb:
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 es posible configurar varios esquemas de autenticación diferentes dentro de un solo 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
, independiente del proyecto principal Web
, que incluye las vistas de la aplicación y las páginas Razor.
Autenticación en Blazor aplicaciones
Blazor Las aplicaciones de servidor pueden aprovechar las mismas características de autenticación que cualquier otra aplicación de ASP.NET Core. Blazor WebAssembly Sin embargo, las aplicaciones no pueden usar los proveedores de identidades y autenticación integrados, ya que se ejecutan en el explorador. Blazor WebAssembly Las aplicaciones pueden almacenar el estado de autenticación de usuario localmente y pueden acceder a las notificaciones para determinar qué acciones deben poder realizar los usuarios. Sin embargo, todas las comprobaciones de autenticación y autorización deben realizarse en el servidor, independientemente de cualquier lógica implementada dentro de la BlazorWebAssembly aplicación, 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 SPA
https://learn.microsoft.com/aspnet/core/security/authentication/identity-api-authorization- autenticación y autorización principales Blazor de ASP.NET
https://learn.microsoft.com/aspnet/core/blazor/security/- Seguridad: autenticación y autorización en formularios web de ASP.NET 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 funcionalidad se puede lograr aplicando el [Authorize]
atributo a determinados controladores o acciones. Si se usan roles, el atributo se puede ampliar aún más para restringir el acceso a los usuarios que pertenecen a determinados roles, como se muestra a continuación:
[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{
}
En este caso, los usuarios que pertenecen a los HRManager
roles o Finance
(o ambos) tendrán acceso a SalaryController. Para requerir que un usuario pertenezca a varios roles (no solo uno de varios), puede aplicar el atributo varias veces, especificando un rol necesario cada vez.
Especificar determinados conjuntos de roles como cadenas en muchos controladores y acciones diferentes puede provocar una repetición no deseada. Como mínimo, defina constantes para estos literales de cadena y use las constantes en cualquier lugar donde necesite especificar la cadena. También puede configurar directivas de autorización, que encapsulan las reglas de autorización y, a continuación, especificar la directiva en lugar de roles individuales al aplicar el [Authorize]
atributo :
[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 crea un nuevo rol que necesita tener acceso a determinados recursos, solo puede actualizar una directiva, en lugar de actualizar cada lista de roles en cada [Authorize]
atributo.
Reclamaciones
Las declaraciones 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 podría usarse con el [Authorize]
atributo para proteger cualquier controlador o acción, como se ha descrito 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 de tokens no tiene estado y está diseñada para ser escalable. En un sistema de autenticación basado en tokens, el cliente primero debe autenticarse con el proveedor de autenticación. Si se ejecuta correctamente, el cliente emite un token, que es simplemente una cadena criptográficamente significativa de caracteres. El formato más común para los tokens es JSON Web Token o JWT (a menudo se pronuncia "jot"). Cuando el cliente necesita emitir una solicitud a una API, agrega este token como encabezado en la solicitud. A continuación, 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, integrarlo con Azure AD y OAuth, o implementar un servicio mediante una herramienta de código abierto como IdentityServer.
Los tokens JWT pueden incluir declaraciones sobre el usuario, que se pueden leer en el cliente o 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 JTW, ya que su contenido se lee fácilmente.
Al usar tokens JWT con SPA o BlazorWebAssembly aplicaciones, debe almacenar el token en algún lugar del cliente y, a continuación, agregarlo a cada llamada API. Normalmente, esta actividad se realiza como encabezado, 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 incrustado en los encabezados de la solicitud, lo que permite que la API del lado servidor se autentique y autorice la solicitud.
Seguridad personalizada
Precaución
Como regla general, evite implementar 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. Hay muchas alternativas comerciales y de código abierto disponibles, que seguramente tendrán una mejor seguridad que una implementación personalizada.
Referencias: seguridad
- Información general sobre la documentación de seguridad
https://learn.microsoft.com/aspnet/core/security/- Aplicación de SSL en una aplicación de ASP.NET Core
https://learn.microsoft.com/aspnet/core/security/enforcing-ssl- Introducción a la identidad
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- Identity Server
https://github.com/IdentityServer
Comunicación de cliente
Además de atender páginas y responder a solicitudes de datos a través de API web, las aplicaciones de ASP.NET Core pueden comunicarse directamente con los clientes conectados. Esta comunicación saliente puede usar una variedad de tecnologías de transporte, siendo WebSockets la más común. ASP.NET Core SignalR es una biblioteca que facilita la adición de funcionalidades de comunicación de servidor a cliente en tiempo real a las aplicaciones. SignalR admite una variedad de tecnologías de transporte, como WebSockets, y abstrae muchos de los detalles de implementación del desarrollador.
La comunicación de cliente en tiempo real, ya sea mediante WebSockets directamente u otras técnicas, es útil en una variedad de escenarios de aplicación. Algunos ejemplos son:
Aplicaciones de sala de chat en directo
Supervisión de aplicaciones
Actualizaciones del progreso del trabajo
Notificaciones
Aplicaciones de formularios interactivos
Al integrar la comunicación con los clientes en las aplicaciones, normalmente hay dos componentes:
Administrador de conexiones del lado servidor (SignalR Hub, WebSocketManager WebSocketHandler)
Biblioteca del lado cliente
Los clientes no están limitados a exploradores: las aplicaciones móviles, las aplicaciones de consola y otras aplicaciones nativas también pueden comunicarse mediante SignalR/WebSockets. El siguiente programa simple devuelve todo el contenido enviado a una aplicación de chat a la consola, como parte de una aplicación de ejemplo de 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.
Comunicación con clientes – Referencias
- SignalR de ASP.NET Core
https://github.com/dotnet/aspnetcore/tree/main/src/SignalR- Administrador de WebSocket
https://github.com/radu-matei/websocket-manager
Diseño controlado por dominio: ¿Debe aplicarlo?
Domain-Driven Diseño (DDD) es un enfoque ágil para crear software que enfatiza centrarse en el dominio empresarial. Hace hincapié en la comunicación y la interacción con expertos en dominios empresariales, quienes pueden explicar a los desarrolladores cómo funciona el sistema en el mundo real. Por ejemplo, si va a crear un sistema que controle las operaciones de acciones, es posible que su experto en dominio sea un agente de acciones experimentado. DDD está diseñado para abordar problemas empresariales grandes y complejos, y a menudo no es adecuado para aplicaciones más pequeñas y sencillas, ya que la inversión en comprender y modelar el dominio no vale la pena.
Al desarrollar software siguiendo un enfoque de DDD, el equipo (incluidas las partes interesadas y colaboradores no técnicos) debe desarrollar un lenguaje omnipresente para el área problemática. Es decir, se debe usar la misma terminología para el concepto real que se modela, el equivalente de software y cualquier estructura que pueda existir para conservar el concepto (por ejemplo, tablas de base de datos). Por lo tanto, los conceptos descritos en el lenguaje omnipresente deben formar la base del modelo de dominio.
El modelo de dominio consta de objetos que interactúan entre sí para representar el comportamiento del sistema. Estos objetos pueden estar incluidos en las siguientes categorías:
Entidades, que representan objetos con un hilo de identidad. Normalmente, las entidades se almacenan en almacenamiento persistente con una clave por la que se pueden recuperar más adelante.
Agregados, que representan grupos de objetos que se deben conservar como una unidad.
Objetos value, que representan conceptos que se pueden comparar basándose en la suma de sus valores de propiedad. Por ejemplo, DateRange consta de una fecha de inicio y finalización.
Eventos de dominio, que representan cosas que ocurren dentro del sistema que son de interés para otras partes del sistema.
Un modelo de dominio DDD debe encapsular el comportamiento complejo dentro del modelo. Las entidades, en particular, 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, que no es deseable en DDD.
Además de estos tipos de modelo, DDD normalmente emplea una variedad de patrones:
Repositorio, para abstraer detalles relacionados con la persistencia.
Fábrica, para encapsular la creación de objetos complejos.
Servicios, para encapsular detalles de implementación de infraestructura o comportamiento complejo.
Comando, para desacoplar la emisión de comandos y ejecutar el comando en sí.
Especificación, para encapsular los detalles de la consulta.
DDD también recomienda el uso de la arquitectura limpia descrita anteriormente, lo que permite acoplamiento flexible, encapsulación y código que se puede comprobar fácilmente mediante pruebas unitarias.
¿Cuándo debe aplicar el DDD?
DDD es adecuado para aplicaciones grandes con una complejidad empresarial significativa (no solo técnica). La aplicación debe requerir el conocimiento de los expertos en dominio. Debe haber un comportamiento significativo en el propio modelo de dominio, que representa las reglas de negocio y las interacciones más allá de simplemente almacenar y recuperar el estado actual de varios registros de almacenes de datos.
¿Cuándo no debes aplicar DDD?
DDD implica inversiones en modelado, arquitectura y comunicación que pueden no estar justificadas para aplicaciones más pequeñas que esencialmente solo realizan operaciones CRUD (crear/leer/actualizar/borrar). Si decide abordar la aplicación después de DDD, pero encuentra que el dominio tiene un modelo anémico sin comportamiento, es posible que tenga que replantearse el enfoque. Es posible que la aplicación no necesite DDD o que necesite ayuda para refactorizar la aplicación para encapsular la lógica de negocios en el modelo de dominio, en lugar de en la base de datos o la interfaz de usuario.
Un enfoque híbrido sería utilizar DDD solo para las áreas transaccionales o más complejas de la aplicación, pero no para las partes más sencillas como las de creación, lectura, actualización y eliminación (CRUD) o de solo lectura de la aplicación. Por ejemplo, no necesita las restricciones de un agregado si está consultando datos para mostrar un informe o para visualizar los datos de un panel. Es perfectamente aceptable tener un modelo de lectura independiente y más sencillo para estos requisitos.
Referencias – Diseño Domain-Driven
- DDD en lenguaje sencillo (respuesta de StackOverflow)
https://stackoverflow.com/questions/1222392/can-someone-explain-domain-driven-design-ddd-in-plain-english-please/1222488#1222488
Despliegue
Hay algunos pasos implicados en el proceso de implementación de la aplicación ASP.NET Core, independientemente de dónde se hospede. El primer paso es publicar la aplicación, que se puede realizar mediante el comando de la dotnet publish
CLI. Este paso compilará la aplicación y colocará todos los archivos necesarios para ejecutar la aplicación en una carpeta designada. Al implementar desde Visual Studio, este paso se realiza automáticamente. La carpeta publish contiene archivos .exe y .dll para la aplicación y sus dependencias. Una aplicación independiente también incluirá una versión del entorno de ejecución de .NET. ASP.NET aplicaciones principales también incluirán archivos de configuración, recursos de cliente estáticos y vistas de MVC.
Las aplicaciones ASP.NET Core son aplicaciones de consola que deben iniciarse cuando el servidor arranca y deben reiniciarse si la aplicación (o el servidor) se bloquea. Un administrador de procesos se puede usar para automatizar este proceso. Los administradores de procesos más comunes para ASP.NET Core son Nginx y Apache en Linux e IIS o servicio de Windows en Windows.
Además de un administrador de procesos, las aplicaciones ASP.NET Core pueden usar un servidor proxy inverso. Un servidor proxy inverso recibe 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 encabezados de host 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 resultar útil es proteger varias aplicaciones mediante SSL/HTTPS. En este caso, solo el proxy inverso tendría que tener SSL configurado. La comunicación entre el servidor proxy inverso y Kestrel podría tener lugar 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 por HTTPS
Un enfoque cada vez más popular es hospedar la aplicación ASP.NET Core en un contenedor de Docker, que luego se puede hospedar localmente o implementar en Azure para el hospedaje basado en la nube. El contenedor de Docker podría contener el código de la aplicación, que se ejecuta en Kestrel y se implementaría detrás de un servidor proxy inverso, como se muestra anteriormente.
Si hospeda la aplicación en Azure, puede usar Microsoft Azure Application Gateway como una aplicación virtual dedicada para proporcionar varios servicios. Además de actuar como proxy inverso para aplicaciones individuales, Application Gateway también puede ofrecer las siguientes características:
Equilibrio de carga HTTP
Descarga SSL (SSL solo para Internet)
SSL de extremo a extremo
Enrutamiento de varios sitios (consolidar hasta 20 sitios en una sola instancia de Application Gateway)
Firewall de aplicaciones web
Compatibilidad con Websocket
Diagnósticos avanzados
Obtenga más información sobre las opciones de implementación de Azure en el capítulo 10.
Referencias: implementación
- Información general sobre hospedaje e implementación
https://learn.microsoft.com/aspnet/core/publishing/- Cuándo usar Kestrel con un proxy inverso
https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel#when-to-use-kestrel-with-a-reverse-proxy- Hospedaje de aplicaciones ASP.NET Core en 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