
Especialmente para los amantes de los libros de la serie "C ++ en 24 horas", decidí escribir un artículo sobre ASP.NET Core.
Si no se ha desarrollado en .NET o en una plataforma similar antes, entonces no tiene sentido ir por debajo del corte por usted. Pero si está interesado en aprender qué IoC, DI, DIP, Interseptors, Middleware, Filters (es decir, todo lo que difiere de Core de .NET clásico), entonces definitivamente debe hacer clic en "Leer más", a medida que desarrolla Sin entender todo esto, claramente no es correcto.
IoC, DI, DIP
Si un teatro comienza con una percha, entonces ASP.NET Core comienza con una inyección de dependencia. Para tratar con DI, debe comprender qué es IoC.
Hablando de IoC, a menudo se recuerda el principio de Hollywood de "No nos llames, te llamaremos". Lo que significa "No es necesario que nos llame, lo llamaremos nosotros mismos".
Diferentes fuentes dan diferentes patrones a los que se puede aplicar IoC. Y lo más probable es que estén bien y se complementen entre sí. Estos son algunos de estos patrones: fábrica, localizador de servicios, método de plantilla, observador, estrategia.
Veamos IoC usando una aplicación de consola simple como ejemplo.
Supongamos que tenemos dos clases simples que implementan una interfaz con un método:
class ConsoleLayer : ILayer { public void Write(string text) { Console.WriteLine(text); } } class DebugLayer : ILayer { public void Write(string text) { Debug.WriteLine(text); } } interface ILayer { void Write(string text); }
Ambos dependen de la abstracción (en este caso, la interfaz actúa como una abstracción).
Y digamos que tenemos un objeto de nivel superior usando estas clases:
class Logging : ILayer { private ILayer _instance; public Logging(int i) { if (i == 1) { _instance = new ConsoleLayer(); } else { _instance = new DebugLayer(); } } public void Write(string text) { _instance.Write(text); } }
Dependiendo del parámetro constructor, la variable _instance es inicializada por una clase específica. Bueno y más, al llamar a Write, se completará la salida a la consola o a Debug. Todo parece ser bastante bueno e incluso, parece, corresponde a la primera parte del principio de Inversión de dependencia
Los objetos de nivel superior son independientes de los objetos de nivel inferior. Tanto esos como los que dependen de abstracciones.
En nuestro caso, ILayer actúa como una abstracción.
Pero también debemos tener un objeto de un nivel aún más alto. Uno que usa la clase Logging
static void Main(string[] args) { var log = new Logging(1); log.Write("Hello!"); Console.Read(); }
Al inicializar Logging con 1, obtenemos en la clase Logging una instancia de la clase que genera datos en la consola. Si inicializamos el registro con cualquier otro número, entonces log.Write enviará datos a Debug. Parece que todo funciona, pero funciona mal. Nuestro objeto de nivel superior Main depende de los detalles del código del objeto de nivel inferior: la clase Logging. Si cambiamos algo en esta clase, tendremos que cambiar el código de la clase Main. Para evitar que esto suceda, haremos una inversión de control: Inversión de control. Hagamos que la clase Main controle lo que sucede en la clase Logging. La clase Logging recibirá, como parámetro de constructor, una instancia de una clase que implementa la interfaz ILayer
class Logging { private ILayer _instance; public Logging(ILayer instance) { _instance = instance; } public void Write(string text) { _instance.Write(text); } }
Y ahora, nuestra clase principal se verá así:
static void Main(string[] args) { var log = new Logging(new DebugLayer()); log.Write("Hello!"); Console.Read(); }
De hecho, decoramos nuestro objeto Logging con el objeto necesario para nosotros.
Ahora nuestra aplicación cumple con la segunda parte del principio de Inversión de dependencia:
Las abstracciones son independientes de los detalles. Los detalles dependen de las abstracciones. Es decir no conocemos los detalles de lo que está sucediendo en la clase Logging, simplemente pasamos la clase allí que implementa la abstracción necesaria.
Existe el término acoplamiento hermético - conexión hermética. Cuanto más débil sea la conexión entre los componentes de la aplicación, mejor. Me gustaría señalar que este ejemplo de una aplicación simple no alcanza el ideal un poco. Por qué Sí, porque en la clase de nivel más alto en Main, usamos dos veces la creación de instancias de clase usando new. Y existe una frase tan mnemotécnica "Lo nuevo es una pista", lo que significa que cuanto menos use nuevos, menos conexiones de componentes en la aplicación y mejor. Idealmente, no deberíamos usar el nuevo DebugLayer, sino que deberíamos obtener DebugLayer de alguna otra manera. Cual? Por ejemplo, desde un contenedor de IoC o usando la reflexión de un parámetro pasado a Main.
Ahora hemos descubierto qué es la Inversión de control (IoC) y qué es la Inversión de dependencia (DIP). Queda por entender qué es la inyección de dependencia (DI). IoC es un paradigma de diseño. La inyección de dependencia es un patrón. Esto es lo que tenemos ahora en el constructor de la clase Logging. Obtenemos una instancia de una dependencia específica. La clase Logging depende de una instancia de una clase que implementa ILayer. Y esta instancia se inyecta a través del constructor.
Contenedor de IoC
Un contenedor de IoC es un objeto que contiene muchas dependencias específicas (dependencia). De lo contrario, la dependencia se puede llamar un servicio; por lo general, es una clase con una cierta funcionalidad. Si es necesario, la dependencia del tipo requerido se puede obtener del contenedor. Inyectar dependencia en un contenedor es Inject. Extracto - Resolver. Aquí hay un ejemplo del contenedor de IoC auto-escrito más simple:
public static class IoCContainer { private static readonly Dictionary<Type, Type> _registeredObjects = new Dictionary<Type, Type>(); public static dynamic Resolve<TKey>() { return Activator.CreateInstance(_registeredObjects[typeof(TKey)]); } public static void Register<TKey, TConcrete>() where TConcrete : TKey { _registeredObjects[typeof(TKey)] = typeof(TConcrete); } }
Solo una docena de líneas de código, pero ya puede usarlo (no para producción, por supuesto, sino con fines educativos).
Puede registrar la dependencia (por ejemplo, ConsoleLayer o DebugLayer que utilizamos en el ejemplo anterior) de esta manera:
IoCContainer.Register<ILayer, ConsoleLayer>();
Y extráigalo del contenedor en el lugar necesario del programa así:
ILayer layer = IoCContainer.Resolve<ILayer>(); layer.Write("Hello from IoC!");
En contenedores reales, también se implementa Dispose (), que le permite destruir recursos que se han vuelto innecesarios.
Por cierto, el nombre de contenedor IoC no transmite exactamente el significado, ya que el término IoC es mucho más amplio en su aplicación. Por lo tanto, recientemente el término contenedor DI se ha utilizado cada vez con más frecuencia (ya que la inyección de dependencia todavía se aplica).
Duración de servicio + varios métodos de extensión en Root de composición
Las aplicaciones ASP.NET Core contienen el archivo Startup.cs, que es el punto de partida de la aplicación para configurar DI. Configura DI en el método ConfigureServices.
public void ConfigureServices(IServiceCollection services) { services.AddScoped<ISomeRepository, SomeRepository>(); }
Este código agregará la clase SomeRepository al contenedor DI, que implementa la interfaz ISomeRepository. El hecho de que el servicio se agregue al contenedor mediante AddScoped significa que se creará una instancia de la clase cada vez que se solicite una página.
Puede agregar un servicio a un contenedor sin especificar una interfaz.
services.AddScoped<SomeRepository>();
Pero este método no se recomienda, ya que su aplicación pierde su flexibilidad y aparecen conexiones cercanas. Se recomienda que siempre especifique una interfaz, porque en este caso, en cualquier momento, puede reemplazar una implementación de la interfaz por otra. Y si las implementaciones son compatibles con el principio de sustitución de Liskov, al cambiar el nombre de la clase de implementación con un "movimiento de muñeca", cambiará la funcionalidad de toda la aplicación.
Hay 2 opciones más para agregar un servicio: AddSingleton y AddTransient.
Cuando se usa AddSingleton, el servicio se crea una vez, y cuando se usa la aplicación, la llamada va a la misma instancia. Utilice este método con especial cuidado, ya que son posibles pérdidas de memoria y problemas de subprocesos múltiples.
AddSingleton tiene una pequeña característica. Se puede inicializar ya sea en el primer acceso a él
services.AddSingleton<IYourService, YourService>();
ya sea inmediatamente cuando se agrega al constructor
services.AddSingleton<IYourService>(new YourService(param));
De la segunda manera, incluso puede agregar un parámetro al constructor.
Si desea agregar un parámetro al constructor de un servicio agregado no solo usando AddSingleton, sino también usando AddTransient / AddScoped, entonces puede usar la expresión lambda:
services.AddTransient<IYourService>(o => new YourService(param));
Y finalmente, cuando se usa AddTransient, se crea un servicio cada vez que accede a él. Ideal para servicios livianos que no consumen memoria y recursos.
Si con AddSingleton y AddScoped todo debería ser más o menos claro, entonces AddTransient necesita aclaración. La documentación oficial da un ejemplo en el que un determinado servicio se agrega al contenedor DI tanto como un parámetro del constructor de otro servicio como de forma independiente. Y en el caso de que se agregue por separado usando AddTransient, crea su instancia 2 veces. Daré un ejemplo muy, muy simplificado. En la vida real, no se recomienda su uso, porque Las clases para simplificar no heredan las interfaces. Digamos que tenemos una clase simple:
public class Operation { public Guid OperationId { get; private set; } public Operation() { OperationId = Guid.NewGuid(); } }
Y hay una segunda clase que contiene la primera como un servicio dependiente y recibe esta dependencia como un parámetro constructor:
public class OperationService { public Operation Operation { get; } public OperationService (Operation operation) { Operation = operation; } }
Ahora inyectamos dos servicios:
services.AddTransient<Operation>(); services.AddScoped<OperationService>();
Y en algún controlador en Acción, agregue el recibo de nuestras dependencias y muestre los valores en la ventana Depuración.
public IActionResult Index([FromServices] Operation operation, [FromServices] OperationService operationService) { Debug.WriteLine(operation.OperationId); Debug.WriteLine(operationService.Operation.OperationId); return View(); }
Entonces, como resultado, obtenemos 2 valores Guid diferentes. Pero si reemplazamos AddTransient con AddScoped, entonces como resultado obtenemos 2 valores idénticos.
El contenedor IoC de la aplicación ASP.NET Core contiene algunos servicios de manera predeterminada. Por ejemplo, IConfiguration es un servicio con el que puede obtener la configuración de la aplicación de los archivos appsettings.json y appsettings.Development.json. IHostingEnvironment e ILoggerFactory con el que puede obtener la configuración actual y una clase auxiliar que permite el registro.
Las clases se recuperan del contenedor utilizando la siguiente construcción típica (el ejemplo más común):
private readonly IConfiguration _configuration; public SomePageController(IConfiguration configuration) { _configuration = configuration; } public async Task<IActionResult> Index() { string connectionString = _configuration["connectionString"]; }
Se crea una variable con modificadores privados de acceso de solo lectura en el ámbito del controlador. La dependencia se obtiene del contenedor en el constructor de la clase y se asigna a una variable privada. Además, esta variable se puede usar en cualquier método o controlador de acción.
A veces no desea crear una variable para usarla en una sola Acción. Entonces puede usar el atributo [FromServices]. Un ejemplo:
public IActionResult About([FromServices] IDateTime dateTime) { ViewData["Message"] = « " + dateTime.Now; return View(); }
Parece extraño, pero para no llamar al método de la clase estática DateTime.Now () en el código, a veces se hace para que el valor de tiempo se obtenga del servicio como parámetro. Por lo tanto, es posible pasar cualquier momento como parámetro, lo que significa que es más fácil escribir pruebas y, por regla general, se hace más fácil realizar cambios en la aplicación.
Esto no quiere decir que la estática sea malvada. Los métodos estáticos son más rápidos. Y lo más probable es que la estática se pueda usar en algún lugar del contenedor IoC. Pero si guardamos nuestra aplicación de todo lo estático y nuevo, obtendremos más flexibilidad.
Contenedores DI de terceros
Lo que observamos y lo que el contenedor ASP.NET Core DI realmente implementa por defecto es la inyección del constructor. Todavía existe la oportunidad de inyectar dependencia en la propiedad utilizando la llamada inyección de propiedad, pero esta característica no está disponible en el contenedor integrado en ASP.NET Core. Por ejemplo, podemos tener alguna clase que implemente como una dependencia, y esta clase tiene algún tipo de propiedad pública. Ahora imagine que durante o después de inyectar la dependencia, necesitamos establecer el valor de la propiedad. Volvamos a un ejemplo similar al ejemplo que examinamos recientemente.
Si tenemos tal clase:
public class Operation { public Guid OperationId { get; set; } public Operation() {} }
que podemos introducir como adicción
services.AddTransient<Operation>();
luego, utilizando el contenedor estándar, no podemos establecer el valor de la propiedad.
Si desea aprovechar esta oportunidad para establecer un valor para la propiedad OperationId, puede usar algún tipo de contenedor DI de terceros que admita la inyección de propiedades. Por cierto, la inyección de propiedades no se recomienda particularmente. Sin embargo, todavía hay Inyección de método e Inyección de método Setter, que pueden ser útiles para usted y que tampoco son compatibles con el contenedor estándar.
Los contenedores de terceros pueden tener otras características muy útiles. Por ejemplo, al usar un contenedor de terceros, solo puede agregar dependencia a los controladores que tienen una palabra específica en el nombre. Y muy a menudo se utilizan cajas - contenedores DI, optimizados para el rendimiento.
Aquí hay una lista de algunos contenedores DI de terceros compatibles con ASP.NET Core: Autofac, Castle Windsor, LightInject, DryIoC, StructureMap, Unity
Si bien utiliza un contenedor DI estándar, no puede usar la inyección de propiedades / métodos, pero puede implementar un servicio dependiente como parámetro de constructor implementando el patrón Factory de la siguiente manera:
services.AddTransient<IDataService, DataService>((dsvc) => { IOtherService svc = dsvc.GetService<IOtherService>(); return new DataService(svc); });
En este caso, GetService devolverá nulo si no se encuentra el servicio dependiente. Existe una variación de GetRequiredService que generará una excepción si no se encuentra el servicio dependiente.
El proceso de obtener un servicio dependiente utilizando GetService en realidad aplica el patrón de localización del Servicio.
Autofac
Echemos un vistazo a Autofac con un ejemplo práctico. Convenientemente, los servicios del contenedor se pueden registrar y recibir, tanto de forma predeterminada como con Autofac.
Instale el paquete NuGet Autofac.Extensions.DependencyInjection.
Cambie el valor devuelto por el método ConfigureServices de nulo a IServiceProvider. Y agregar propiedad
public IContainer ApplicationContainer { get; private set; }
Después de eso, será posible agregar código como el siguiente al final del método ConfigureServices de la clase Startup (esta es solo una de las opciones para registrar servicios):
services.AddTransient<ISomeRepository, SomeRepository>(); var builder = new ContainerBuilder(); builder.Populate(services); builder.RegisterType<AnotherRepository>().As<IAnotherRepository>(); this.ApplicationContainer = builder.Build(); return new AutofacServiceProvider(this.ApplicationContainer);
Aquí constructor. Población (servicios); Agrega servicios de IServiceCollection al contenedor. Bueno y más, ya es posible registrar servicios con builder.RegisterType. Oh si Casi lo olvido. Debe cambiar de nulo a IServiceProvider el valor de retorno del método ConfigureServices.
AOP con ASP.NET Core: intersectores de Autofac
Hablando de programación orientada a aspectos, mencionan otro término: preocupaciones transversales. La preocupación es alguna información que afecta el código. En la versión rusa usan la palabra responsabilidad. Bueno, las preocupaciones transversales son responsabilidades que afectan otras responsabilidades. Pero idealmente, no deberían influenciarse entre sí, ¿verdad? Cuando se influyen entre sí, se hace más difícil cambiar el programa. Es más conveniente cuando tenemos todas las operaciones por separado. El registro, las transacciones, el almacenamiento en caché y mucho más se pueden hacer usando AOP sin cambiar el código de las clases y los métodos mismos.
En el mundo .NET, a menudo se usa un método cuando el código AOP se incrusta usando un postprocesador en un código de aplicación ya compilado ( PostSharp ) o, alternativamente, puede usar interceptores, estos son ganchos de eventos que se pueden agregar al código de la aplicación. Estos interceptores, como regla, usan el decorador que ya hemos examinado para su trabajo.
Creemos su propio interceptor. El ejemplo más simple y típico que es más fácil de reproducir es el registro.
Además del paquete Autofac.Extensions.DependencyInjection, también instalaremos el paquete Autofac.Extras.DynamicProxy
Instalado? Agregue una clase de registro simple que se llamará al acceder a ciertos servicios.
public class Logger : IInterceptor { public void Intercept(IInvocation invocation) { Debug.WriteLine($"Calling {invocation.Method.Name} from Proxy"); invocation.Proceed(); } }
Agregue a nuestro registro Autofac registro del interceptor:
builder.Register(i => new Logger()); builder.RegisterType<SomeRepository >() .As<ISomeRepository >() .EnableInterfaceInterceptors() .InterceptedBy(typeof(Logger));
Y ahora, con cada llamada a la clase, se llamará al método Intercept de la clase Logger.
Por lo tanto, podemos simplificar nuestra vida y no escribir una entrada de registro al comienzo de cada método. Lo tendremos automáticamente. Y si lo desea, nos será fácil cambiarlo o deshabilitarlo para toda la aplicación.
También podemos eliminar .InterceptedBy (typeof (Logger)); y agregue la interceptación de llamadas solo para servicios de aplicaciones específicos que utilizan el atributo [Intercepción (typeof (Logger))]; debe especificarlo antes del encabezado de la clase.
Middleware
ASP.NET tiene una cadena específica de llamadas de código que se produce en cada solicitud. Incluso antes de que se cargue la UI / MVC, se realizan ciertas acciones.
Es decir, por ejemplo, si agregamos al principio del método Configure de la clase Startup.cs el código
app.Use(async (context, next) => { Debug.WriteLine(context.Request.Path); await next.Invoke(); });
entonces podemos ver en la consola de depuración qué archivos solicita nuestra aplicación. De hecho, obtenemos las capacidades de AOP "fuera de la caja"
Un poco inútil, pero claro e informativo ejemplo del uso de middleware, te mostraré ahora:
public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("Hello!" + Environment.NewLine); await next.Invoke(); }); app.Run(async context => { await context.Response.WriteAsync("Hello again."); }); }
Con cada solicitud, comienza una cadena de llamadas. Desde cada aplicación, use, después de llamar a next.invoke (), se realiza la transición a la siguiente llamada. Y todo termina después de que la aplicación funciona.
Puede ejecutar algún código solo cuando acceda a una ruta específica.
Puedes hacer esto usando app.Map:
private static void Goodbye(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Goodbye!"); }); } public void Configure(IApplicationBuilder app) { app.Map("/goodbye", Goodbye); app.Run(async context => { await context.Response.WriteAsync("Hello!"); }); }
Ahora, si solo va a la página del sitio, puede ver el texto "¡Hola!", Y si agrega / Adiós a la barra de direcciones, verá Adiós.
Además de Use and Map, puede usar UseWhen o MapWhen para agregar código a la cadena de middleware solo bajo ciertas condiciones específicas.
Hasta ahora ha habido ejemplos inútiles, ¿verdad? Aquí hay un ejemplo normal:
app.Use(async (context, next) => { context.Response.Headers.Add("X-Frame-Options", "DENY"); context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("X-Xss-Protection", "1"); await next(); });
Aquí agregamos encabezados a cada solicitud para ayudar a proteger la página de los ataques de hackers.
O aquí hay un ejemplo de localización:
var supportedCultures = new[] { new CultureInfo("ru"), new CultureInfo("fr") }; app.UseRequestLocalization(new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("ru"), SupportedCultures = supportedCultures, SupportedUICultures = supportedCultures });
Ahora, si agrega el parámetro? Culture = fr a la dirección de la página, puede cambiar el idioma de la aplicación al francés (si se agrega la localización a su aplicación, entonces todo funcionará)
Filtros
Si la cadena de middleware se refiere a procesos anteriores a MVC, los filtros funcionan junto con MVC.
El siguiente diagrama esquemático muestra cómo funcionan los filtros.

Primero, se resuelven los filtros de autorización. Es decir puede crear algún tipo de filtro o varios filtros e ingresar en ellos algún tipo de código de autorización que funcionará con las solicitudes.
Luego cumplen con los filtros de recursos. Con estos filtros, puede, por ejemplo, devolver cierta información del caché.
Luego se produce el enlace de datos y se ejecutan los filtros de acción. Con su ayuda, puede manipular los parámetros pasados a Acción y el resultado devuelto.
Los filtros de excepción como las sugerencias de nombre le permiten agregar algún tipo de manejo general de errores para la aplicación. Debería ser bastante conveniente manejar los errores en todas partes de la misma manera. Una especie de AOP-shny plus.
Los filtros de resultados le permiten realizar alguna acción antes de ejecutar el controlador de Acción o después. Son bastante similares a los filtros de acción, pero se ejecutan solo si no hay errores. Adecuado para la lógica vinculada a la vista.
. :
public class YourCustomFilter : Attribute, IAuthorizationFilter { public async void OnAuthorization(AuthorizationFilterContext context) { // - , , context.Result = new ContentResult() { Content = " " }; } }
DI ( Startup.cs)
services.AddScoped<YourCustomFilter>();
- Action
[ServiceFilter(typeof(YourCustomFilter))]
– middleware - action . Configure
public class MyMiddlewareFilter { public void Configure(IApplicationBuilder applicationBuilder) { applicationBuilder.Use(async (context, next) => { Debug.WriteLine(" middleware!"); await next.Invoke(); }); } }
Action-
[MiddlewareFilter(typeof(MyMiddlewareFilter))]