有效的ASP.NET Core

有效的ASP.NET Core


特别是对于“ 24小时内的C ++”系列的书迷,我决定写一篇有关ASP.NET Core的文章。


如果您以前从未在.NET或任何类似平台下进行过开发,那么为您着急是没有意义的。 但是,如果您有兴趣了解IoC,DI,DIP,Interseptors,中间件,过滤器(即,区分Core与经典.NET的所有内容),那么在开发过程中,您肯定需要单击“阅读更多”。如果不了解所有这些,显然是不正确的。


IoC,DI,DIP


如果剧院以衣架开始,则ASP.NET Core以依赖注入开始。 为了处理DI,您需要了解IoC是什么。


在谈到IoC时,人们常常想起好莱坞的原则:“不要打电话给我们,我们会打电话给您。” 这意味着“无需致电我们,我们会自己致电给您。”


不同的来源给出了可以应用IoC的不同模式。 而且很可能它们是对的,只是相辅相成。 以下是其中的一些模式:工厂,服务定位器,模板方法,观察者,策略。


让我们以一个简单的控制台应用程序为例来看看IoC。


假设我们有两个简单的类,它们使用一种方法实现接口:


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); } 

它们都依赖于抽象(在这种情况下,接口充当抽象)。


假设我们使用这些类有一个更高级别的对象:


  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); } } 

取决于构造函数参数,_instance变量由特定的类初始化。 而且,当调用Write时,将完成到控制台或Debug的输出。 一切似乎都很好,甚至看起来都与依赖倒置原则的第一部分相对应。


较高级别的对象独立于较低级别的对象。 那些和那些都依赖抽象。

在我们的案例中,ILayer充当了抽象。


但是我们还必须有一个更高层次的对象。 一种使用Logging类


  static void Main(string[] args) { var log = new Logging(1); log.Write("Hello!"); Console.Read(); } 

通过用1初始化Logging,我们在Logging类中获得了一个类的实例,该实例将数据输出到控制台。 如果我们使用其他任何编号初始化Logging,则log.Write会将数据输出到Debug。 看起来一切都正常,但是效果很差。 我们的上层对象Main取决于下层对象代码的详细信息-Logging类。 如果我们在此类中进行了某些更改,则需要更改Main类的代码。 为了防止这种情况的发生,我们将进行控制反转-控制反转。 让我们让Main类控制Logging类中发生的情况。 Logging类将接收实现ILayer接口的类的实例作为构造函数参数


  class Logging { private ILayer _instance; public Logging(ILayer instance) { _instance = instance; } public void Write(string text) { _instance.Write(text); } } 

现在,我们的Main类将如下所示:


  static void Main(string[] args) { var log = new Logging(new DebugLayer()); log.Write("Hello!"); Console.Read(); } 

实际上,我们用必要的对象来装饰Logging对象。


现在我们的应用程序符合依赖倒置原则的第二部分:


抽象与细节无关。 细节取决于抽象。 即 我们不知道Logging类中正在发生的事情的详细信息,我们只是将实现必要抽象的类传递给该类。

有这样的术语紧密耦合-紧密连接。 应用程序中组件之间的连接越弱越好。 我想指出,这个简单应用程序的示例并没有达到理想的水平。 怎么了 是的,因为在Main的最高级别的类中,我们两次使用了使用new的类实例的创建。 并且有这样的助记词“ New is a clue”-这意味着您使用的新代码越少,应用程序中组件的紧密连接就越好。 理想情况下,我们不应使用新的DebugLayer,而应以其他方式获取DebugLayer。 哪一个 例如,从IoC容器或使用传递给Main的参数的反射。


现在,我们确定了什么是控制反转(IoC)和什么是依赖反转(DIP)。 仍然需要了解什么是依赖注入(DI)。 IoC是一种设计范式。 依赖注入是一种模式。 这就是我们现在在Logging类的构造函数中所拥有的。 我们得到一个特定依赖的实例。 Logging类取决于实现ILayer的类的实例。 这个实例是通过构造函数注入的。


IoC容器


IoC容器就是这样的对象,它包含许多特定的依赖关系(dependency)。 依赖关系也可以称为服务-通常,它是具有某些功能的类。 如有必要,可以从容器中获得所需类型的依存关系。 将依赖项注入到容器中就是注入。 提取-解决。 这是最简单的自写IoC容器的示例:


  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); } } 

只需十几行代码,但是您已经可以使用它了(当然,不是用于生产,而是用于教育目的)。


您可以像下面这样注册依赖项(例如,在上一个示例中使用的ConsoleLayer或DebugLayer):


  IoCContainer.Register<ILayer, ConsoleLayer>(); 

然后从容器中将其提取到程序的必要位置,如下所示:


  ILayer layer = IoCContainer.Resolve<ILayer>(); layer.Write("Hello from IoC!"); 

在实际的容器中,还实现了Dispose(),它使您可以销毁不必要的资源。


顺便说一下,名称IoC容器并不能完全传达其含义,因为术语IoC的应用范围更广。 因此,近来越来越多地使用术语“ DI容器”(因为仍然应用依赖注入)。


组合根中的服务寿命和各种扩展方法


ASP.NET Core应用程序包含Startup.cs文件,该文件是配置DI的应用程序的起点。 在ConfigureServices方法中配置DI。


  public void ConfigureServices(IServiceCollection services) { services.AddScoped<ISomeRepository, SomeRepository>(); } 

此代码会将SomeRepository类添加到实现ISomeRepository接口的DI容器中。 使用AddScoped将服务添加到容器的事实意味着,每次请求页面时都会创建该类的实例。
您可以在不指定接口的情况下将服务添加到容器。


  services.AddScoped<SomeRepository>(); 

但是不建议使用此方法,因为您的应用程序失去了灵活性,并出现了紧密的连接。 建议始终指定一个接口,因为在这种情况下,您可以随时用另一种实现替换该接口。 并且,如果实现支持Liskov替换原理,则通过“轻拂”更改实现类的名称,即可更改整个应用程序的功能。


还有两个添加服务的选项-AddSingleton和AddTransient。
使用AddSingleton时,将创建一次服务,而使用应用程序时,调用将转到同一实例。 请特别小心使用此方法,因为可能会发生内存泄漏和多线程问题。


AddSingleton有一个小功能。 可以在第一次访问它时进行初始化


  services.AddSingleton<IYourService, YourService>(); 

要么立即添加到构造函数中


  services.AddSingleton<IYourService>(new YourService(param)); 

用第二种方法,您甚至可以向构造函数添加参数。
如果要将参数添加到不仅使用AddSingleton添加的服务的构造函数中,还可以使用AddTransient / AddScoped添加的服务的构造函数中,则可以使用lambda表达式:


  services.AddTransient<IYourService>(o => new YourService(param)); 

最后,当使用AddTransient时,每次访问该服务时都会创建一个服务。 非常适合不占用内存和资源的轻型服务。


如果使用AddSingleton和AddScoped应当使所有内容或多或少清晰,则需要对AddTransient进行说明。 官方文档给出了一个示例,其中某个服务既作为另一个服务的构造函数的参数又独立地添加到DI容器中。 如果使用AddTransient分别添加了该实例,则它将创建其实例2次。 我将举一个非常非常简化的示例。 在现实生活中,不建议使用它,因为 为简单起见,类不继承接口。 假设我们有一个简单的类:


  public class Operation { public Guid OperationId { get; private set; } public Operation() { OperationId = Guid.NewGuid(); } } 

还有第二个类,其中包含第一个作为依赖服务,并接收此依赖作为构造函数参数:


  public class OperationService { public Operation Operation { get; } public OperationService (Operation operation) { Operation = operation; } } 

现在我们注入两项服务:


  services.AddTransient<Operation>(); services.AddScoped<OperationService>(); 

并在Action中的某些控制器中,添加我们的依存关系的收据,并在Debug窗口中显示值。


  public IActionResult Index([FromServices] Operation operation, [FromServices] OperationService operationService) { Debug.WriteLine(operation.OperationId); Debug.WriteLine(operationService.Operation.OperationId); return View(); } 

因此,结果,我们得到2个不同的Guid值。 但是,如果将AddTransient替换为AddScoped,则结果将获得2个相同的值。


默认情况下,ASP.NET Core应用程序IoC容器包含一些服务。 例如,IConfiguration是一项服务,您可以通过该服务从文件appsettings.json和appsettings.Development.json获取应用程序设置。 IHostingEnvironment和ILoggerFactory,可用来获取当前配置以及允许记录的帮助程序类。


使用以下典型构造(最常见的示例)从容器中检索类:


  private readonly IConfiguration _configuration; public SomePageController(IConfiguration configuration) { _configuration = configuration; } public async Task<IActionResult> Index() { string connectionString = _configuration["connectionString"]; } 

在控制器的范围内创建带有私有只读访问修饰符的变量。 从类的构造函数中的容器获取依赖关系,并将其分配给私有变量。 此外,该变量可以在任何方法或Action控制器中使用。
有时,您不想创建一个变量以仅在一个Action中使用它。 然后,您可以使用[FromServices]属性。 一个例子:


  public IActionResult About([FromServices] IDateTime dateTime) { ViewData["Message"] = «  " + dateTime.Now; return View(); } 

看起来很奇怪,但是为了不在代码中调用静态类DateTime.Now()的方法,有时会这样做,以便从服务中获取时间值作为参数。 因此,可以随时通过作为参数,这意味着编写测试变得更加容易,并且通常,对应用程序进行更改也变得更加容易。
这并不是说静态就是邪恶。 静态方法更快。 而且最有可能在IoC容器本身的某个位置使用静态。 但是,如果我们将应用程序从所有静态和新内容中保存下来,那么我们将获得更大的灵活性。


第三方DI容器


默认情况下,我们所观察的和ASP.NET Core DI容器实际实现的是构造函数注入。 仍然有机会使用所谓的属性注入将依赖项注入属性,但是此功能在ASP.NET Core内置的容器中不可用。 例如,我们可能有一些您实现为依赖项的类,并且该类具有某种公共属性。 现在想象一下,在注入依赖项期间或之后,我们需要设置属性的值。 让我们回到与我们最近研究的示例相似的示例。
如果我们有这样的课程:


  public class Operation { public Guid OperationId { get; set; } public Operation() {} } 

我们可以将其介绍为成瘾,


  services.AddTransient<Operation>(); 

那么我们就无法使用标准容器设置属性值。
如果您想利用这个机会为OperationId属性设置一个值,则可以使用某种支持属性注入的第三方DI容器。 顺便说一句,不特别推荐注入属性。 但是,仍然存在方法注入和设置方法注入,它们可能很方便您使用,并且标准容器也不支持它们。


第三方容器可能还具有其他非常有用的功能。 例如,使用第三方容器,您只能将依赖项添加到名称中具有特定单词的控制器。 并且是经常使用的案例-针对性能进行了优化的DI容器。
这是ASP.NET Core支持的一些第三方DI容器的列表:Autofac,Castle Windsor,LightInject,DryIoC,StructureMap,Unity


尽管使用标准的DI容器,但是不能使用属性/方法注入,但是可以通过以下方式实现Factory模式,从而将依赖服务实现为构造函数参数:


  services.AddTransient<IDataService, DataService>((dsvc) => { IOtherService svc = dsvc.GetService<IOtherService>(); return new DataService(svc); }); 

在这种情况下,如果未找到依赖服务,则GetService将返回null。 GetRequiredService有一个变体,如果未找到依赖服务,它将抛出异常。
使用GetService获得依赖服务的过程实际上应用了Service locator模式。


Autofac


让我们看一个带有实际示例的Autofac。 可以方便地以默认方式和使用Autofac来注册和接收容器中的服务。


安装NuGet软件包Autofac.Extensions.DependencyInjection。
将ConfigureServices方法返回的值从void更改为IServiceProvider。 并添加属性


  public IContainer ApplicationContainer { get; private set; } 

之后,可以将以下代码添加到Startup类的ConfigureServices方法的末尾(这只是注册服务的选项之一):


  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); 

这里builder.Populate(服务); 将服务从IServiceCollection添加到容器。 不仅如此,已经可以使用builder.RegisterType注册服务。 哦是的 我差点忘了 您必须将ConfigureServices方法的返回值从void更改为IServiceProvider。


带有ASP.NET Core的AOP-Autofac Interseptors


在谈到面向方面的编程时,他们提到了另一个术语-跨领域关注点。 问题是一些影响代码的信息。 在俄语版本中,他们使用责任一词。 好吧,跨部门关注是影响其他责任的责任。 但是理想情况下,它们不应该相互影响,对吗? 当它们相互影响时,更改程序变得更加困难。 当我们分开进行所有操作时,这将更加方便。 使用AOP可以完成日志记录,事务处理,缓存等操作,而无需更改类和方法本身的代码。


在.NET世界中,使用后处理器将AOP代码嵌入到已编译的应用程序代码( PostSharp )中时,通常会使用一种方法;或者,也可以使用拦截器-这些是可以添加到应用程序代码中的事件挂钩。 这些拦截器通常使用我们已经对其工作进行检查的装饰器。


让我们创建自己的拦截器。 最容易复制的最简单,最典型的示例是日志记录。
除了Autofac.Extensions.DependencyInjection程序包,我们还将安装Autofac.Extras.DynamicProxy程序包
安装好了吗? 添加一个简单的日志类,该类将在访问某些服务时被调用。


  public class Logger : IInterceptor { public void Intercept(IInvocation invocation) { Debug.WriteLine($"Calling {invocation.Method.Name} from Proxy"); invocation.Proceed(); } } 

将拦截器的Autofac注册添加到我们的注册中:


  builder.Register(i => new Logger()); builder.RegisterType<SomeRepository >() .As<ISomeRepository >() .EnableInterfaceInterceptors() .InterceptedBy(typeof(Logger)); 

现在,每次调用该类时,都会调用Logger类的Intercept方法。
因此,我们可以简化我们的工作,而不必在每种方法的开头都写入日志条目。 我们将自动拥有它。 而且,如果需要的话,我们很容易在整个应用程序中更改或禁用它。


我们还可以删除.InterceptedBy(typeof(Logger)); 并使用[Intercept(typeof(Logger))]属性仅针对特定应用程序服务添加呼叫拦截-您必须在类头之前指定它。


中间件


ASP.NET具有在每个请求上发生的特定代码调用链。 即使在加载UI / MVC之前,也会执行某些操作。


也就是说,例如,如果我们在Startup.cs类的Configure方法的开头添加代码,


  app.Use(async (context, next) => { Debug.WriteLine(context.Request.Path); await next.Invoke(); }); 

然后我们可以在调试控制台中看到应用程序请求的文件。 实际上,我们“开箱即用”地获得了AOP的功能
我将向您展示一个使用中间件的无用但清晰而有用的示例:


  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."); }); } 

对于每个请求,一连串的呼叫开始。 在每个应用程序中,在调用next.invoke()之后使用进行到下一个调用的转换。 在app.Run运行后,一切都结束了。
您只能在访问特定路由时执行一些代码。
您可以使用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!"); }); } 

现在,如果您只是转到站点页面,您将看到文本“ Hello!”,并且,如果在地址栏中添加/再见,您将看到再见。


除了使用和映射之外,您还可以使用UseWhen或MapWhen仅在某些特定条件下将代码添加到中间件链中。


到目前为止,仍然有无用的例子,对吗? 这是一个正常的示例:


  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(); }); 

在这里,我们向每个请求添加标头,以帮助保护网页免受黑客攻击。


或者这是本地化的示例:


  var supportedCultures = new[] { new CultureInfo("ru"), new CultureInfo("fr") }; app.UseRequestLocalization(new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("ru"), SupportedCultures = supportedCultures, SupportedUICultures = supportedCultures }); 

现在,如果在页面地址中添加参数“ Culture = fr”,则可以将应用程序语言切换为法语(如果将本地化添加到您的应用程序中,那么一切都会正常进行)


筛选器


如果中间件链在MVC之前引用了进程,则过滤器与MVC一起使用。
以下示意图显示了过滤器的工作方式。


筛选器


首先,制定授权过滤器。 即 您可以创建某种过滤器,也可以创建多个过滤器,然后在其中输入某种授权码,这些授权码将根据请求进行计算。


然后它们完成资源过滤器。 使用这些过滤器,例如,您可以从缓存中返回一些信息。


然后发生数据绑定并执行动作过滤器。 在他们的帮助下,您可以操纵传递给Action的参数和返回的结果。


作为名称提示的异常过滤器使您可以为应用程序添加某种常规的错误处理。 处理所有相同地方的错误应该非常方便。 一种AOP-shny plus。


结果过滤器使您可以在执行动作控制器之前或之后执行某些动作。 它们与动作过滤器非常相似,但是仅在没有错误的情况下执行。 适用于与View关联的逻辑。


. :


  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))] 

Source: https://habr.com/ru/post/zh-CN437002/


All Articles