dotnet开发

一 asp.net core模板的区别和用法

1 空

  最简单和基础的项目!默认创建后,在项目里不能创建 controllersviewsrazorpages,但可以处理简单的网络请求,如下:
在这里插入图片描述
  可以将它扩展为webapi、web应用(razorpages)或web应用(mvc)。扩展的方法是在ConfigureServices()方法里注入相关的服务,然后在app.UseEndpoints()里进行路由的映射就可以了。

2 “API”模板项目

  前后端分离的项目(如:vue+aspnetcore)一般选择此类型。
  这个项目相比“空”模板的项目,多出来如下东西:
  a). 注册了controllers服务:services.AddControllers();
  b). 终结点映射设置里直接将所有的请求映射到了controller上,路由规则由具体的Controller上的特性规定:app.UseEndpoints(endpoints => endpoints.MapControllers());
  c). 给了一个示例的Controller,它的大致内容如下:
在这里插入图片描述

3 “web 应用程序”之RazorPages

  这种类型的项目用的较少,类似于以前的aspx,但是简单的项目写起来也是很方便的
  这个项目相比“空”模板项目多出如下东西:
  a). 注册了RazorPages服务:services.AddRazorPages();
  b). 终结点映射设置里直接将所有的请求映射到了RazorPages上:app.UseEndpoints(endpoints => endpoints.MapRazorPages());
  c). 给了示例的RazorPage,它的大致内容如下:
在这里插入图片描述

4 “web 应用”之mvc

  在asp.net时代,mvc是做web应用的首选,如今SPA(单页面应用)盛行下有下滑趋势,但它作为web应用开发应该还是坚挺的!
  相比“空”模板项目,多了如下东西:
  a). 注册了controller和views服务:services.AddControllersWithViews();
  b). 终结点映射设置里将所有的请求映射到了指定了规则的路由上:app.UseEndpoints(endpoints => endpoints.MapControllerRoute(name: “default”, pattern: “{controller=Home}/{action=Index}/{id?}”));
  c). 给了示例的Controller、Model和View,它的大致内容如下:
在这里插入图片描述

5 应用层区别

  从上面新建的几个示例中可以看到,除“空”模板外他们都是有各自的应用场景:
  “空”项目:学习、研究;适用于特别简单的项目(几个api的应用);一般都是基于此项目引入其他(mvc/api等)的服务后再使用。
  “web api”:单页面应用;前后端分离项目,应用较广泛。
  “web mvc”:asp.net mvc的升级版,传统c#开发web网站的进阶版;如果不是太考虑前后端分离,应用此模式是最佳的,应用一般。
  “web razorpages”:aspx的升级版,对于小型项目,不考虑前后端分离,可以使用此模式,应用一般。
  注意:你可以将其中的任何一个项目修改成其他的项目类型而不用考虑引入nuget包或修改工程属性,因为他们的区别仅在于ConfigureServices()和Configure()里面的写法不同而已,至于其他的工程文件,你在书写时遵从约定即可。

6 底层区别

  我们知道这三个项目之间引用的服务分别是:web api:services.AddControllers()web mvc:services.AddControllersWithViews()web razor:services.AddRazorPages();那么它们之间有什么区别呢?区别在于添加进去的服务不同,它们添加到框架中的服务如下表所示:
在这里插入图片描述
  如上图所示,把服务拆分成controllers、views、pages三种,各个项目引入服务如上所示。为了更能清晰的看出它们引入服务的情况,阅读它们的源代码,发现如下关系:
在这里插入图片描述

二 WebApi简单创建及使用

1 简单创建

  打开Controllers文件夹,使用默认创建的ValuesController控制器。在这里插入图片描述
  把该控制器的内容重新写一下,将路由设置为api/控制器/方法(api/[controller]/[action])。 按照常用Get 和 Post两个请求,写了两个Get方法和一个Post方法,一个参数类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace FirstApi.Controllers
{
    //路由设置
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        /// <summary>
        /// 获取文本
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Hello World!";
        }
        /// <summary>
        /// 两数相加
        /// </summary>
        /// <param name="num1">第一个数</param>
        /// <param name="num2">第二个数</param>
        /// <returns></returns>
        [HttpGet]
        public ActionResult<int> Sum(int num1,int num2)
        {
            return num1 + num2;
        }
        /// <summary>
        /// 两数相减
        /// </summary>
        /// <param name="param">参数</param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult<int> Subtract(Param param)
        {
            int result = param.num1 - param.num2;
            return result;
        }
    }
    /// <summary>
    /// 参数
    /// </summary>
    public class Param
    {
        /// <summary>
        /// 第一个数
        /// </summary>
        public int num1 { get; set; }
        /// <summary>
        /// 第二个数
        /// </summary>
        public int num2 { get; set; }
    }
}

  然后右键项目→属性→调试,将启动浏览器默认指向为第一个Get方法。
在这里插入图片描述
  调试运行,访问第一个方法,返回结果。

  访问第二个方法加上参数sum?num1=5&num2=8,得到结果。

  第三个方法是Post请求,无法直接输入,可以用其他方式实现。

2 swagger

  Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的 Web 服务。
  Swagger Codegen:通过Codegen可将描述文件生成html和cwiki形式的接口文档,同时也能生成多种语言的服务端和客户端的代码。可以在后面的Swagger Editor中在线生成。
  Swagger UI:提供了一个可视化的UI页面 展示描述文件。接口的调用方、测试、项目经理等都可以在该页面中对相关接口进行查阅和做一些简单的接口请求。该项目支持在线导入文件和本地部署UI项目。
  Swagger Editor:类似于markendown编辑器的编辑Swagger描述文件的编辑器,改编辑器支持实时预览描述文件的更新效果。也提供了在线编辑器和本地部署编辑器两种方式。
  Swagger Inspector:感觉和postman差不多,是一个可以对接口进行测试的在线版的postman。比在Swagger UI里面做接口请求,会返回更多的信息,也会保存你请求的实际请求参数等数据。
  Swagger Hub:继承了上面所有项目的各个功能,你可以以项目和版本为单位,将你的描述文件上传到Swagger Hub中。在Swagger Hub中跨域完成上面项目的所有工作,需要注册账号,分免费版和收费版。
  在nuget管理界面,搜索“Swashbuckle.AspNetCore”,安装。引入命名空间:using Swashbuckle.AspNetCore.Swagger;;在 Startup 类中,导入以下命名空间来使用 OpenApiInfo 类:using Microsoft.OpenApi.Models;,将Swagger生成器添加到Startup.ConfigureServices 方法中的服务集合中:
  在.Net Core3.0之前:

//注册Swagger生成器,定义一个和多个Swagger 文档
services.AddSwaggerGen(c =>
{
     c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});

  但是在.Net Core 3.0中,要这样写:

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});

  在Configure方法中,启动中间件为生成的JSON文档和Swagger UI提供服务:

//启用中间件服务生成Swagger作为JSON终结点
app.UseSwagger();
//启用中间件服务对swagger-ui,指定Swagger JSON终结点
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});

  要在应用的根 (http://localhost:<port>/) 处提供 Swagger UI,请将RoutePrefix属性设置为空字符串:

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    //设置 RoutePrefix 为空字符串
    c.RoutePrefix = string.Empty;
});

3 设置

  然后,右键项目,点击属性。选择生成,选择我们的Debug路径。勾选XML文档文件,自动填充,然后点击调试,将启动浏览器后面url去掉。
在这里插入图片描述
  完成后,直接运行VS,就会进入文档UI页面了。点开方法,点击Try it out按钮,直接点击Execute执行。

三 ASP.NET Core中的配置

  官方文档
  ASP.NET Core中的配置是使用一个或多个配置提供程序执行的。配置提供程序使用各种配置源从键值对读取配置数据:

  • 设置文件,例如 appsettings.json
  • 环境变量
  • Azure Key Vault
  • Azure 应用程序配置
  • 命令行参数
  • 已安装或已创建的自定义提供程序
  • 目录文件
  • 内存中的 .NET 对象

1 默认配置

  通过 dotnet new 或 Visual Studio 创建的 ASP.NET Core Web 应用会生成以下代码:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args) //
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

  CreateDefaultBuilder按照以下顺序为应用提供默认配置:
  1、ChainedConfigurationProvider:添加现有 IConfiguration 作为源。 在默认配置示例中,添加主机配置,并将它设置为应用__ 配置的第一个源。
  2、使用 JSON 配置提供程序通过 appsettings.json 提供。
  3、使用 JSON 配置提供程序通过 appsettings.Environment.json 提供。 例如,appsettings.Production.json 和 appsettings.Development.json。
  4、应用在 Development 环境中运行时的应用机密。
  5、使用环境变量配置提供程序通过环境变量提供。
  6、使用命令行配置提供程序通过命令行参数提供。
  7、后来添加的配置提供程序会替代之前的密钥设置。 例如,如果在 appsettings.json 和环境中设置了 MyKey,则会使用环境值。 使用默认配置提供程序,命令行配置提供程序将替代所有其他的提供程序。
  8、若要详细了解 CreateDefaultBuilder,请参阅默认生成器设置。
  9、以下代码按添加顺序显示了已启用的配置提供程序:

public class Index2Model : PageModel
{
    private IConfigurationRoot ConfigRoot;

    public Index2Model(IConfiguration configRoot)
    {
        ConfigRoot = (IConfigurationRoot)configRoot;
    }

    public ContentResult OnGet()
    {
        string str = "";
        foreach (var provider in ConfigRoot.Providers.ToList())
        {
            str += provider.ToString() + "\n";
        }

        return Content(str);
    }
}

2 appsetting.json设置

  https://www.cnblogs.com/yuangang/p/5736892.html

3 使用选项模式绑定分层配置数据

4 自定义配置

  https://www.cnblogs.com/CoderAyu/p/10776845.html
  https://www.cnblogs.com/jhxk/articles/10059343.html
  https://www.cnblogs.com/jhxk/articles/10059343.html

四 常见接口

  app.UseHttpsRedirection();:它建议ASP.NET Core web应用都应该调用HTTPS重定向中间件, 这样就可以把所有的HTTP请求转换为HTTPS.

五 asp.net core 修改默认端口的方式

  本节内容来自这里

1 解决办法(UseUrls)

  骨架代码就那么几行,很容易在这个IWebHostBuilder中找到一个叫做UseUrls的方法,从注解中可以看得出来让WebHost监听指定的端口号,修改如下:

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }
    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
               .UseUrls("http://*:8080")
               .UseStartup<Startup>();
}

  对于core3的版本代码:

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                    .UseUrls("http://*:5001", "https://*:5002")
                    .UseStartup<Startup>();
                });

  或者添加 ConfigureKestrel 方法配置,好处就是可以通过 IConfiguration 对象 读取 appsettings 配置信息,而 UseUrls 不行,但是不支持 httphttps 等协议(默认都是 http)切换(这个不知道谁懂)。

public static IHostBuilder CreateHostBuilder(string[] args) =>
	Host.CreateDefaultBuilder(args)
	    .ConfigureWebHostDefaults(webBuilder =>
	    {
	        webBuilder
	        .ConfigureKestrel((context, options) =>
	        {
	            var config = context.Configuration;   //可以读取配置
	            options.Listen(new IPEndPoint(IPAddress.Parse("192.168.2.6"), 5001));
	            options.Listen(new IPEndPoint(IPAddress.Parse("192.168.2.6"), 5002));
	        })
	        .UseStartup<Startup>();
	    });

  但是在发布之后,你突然发现,卧槽,端口冲突了,我想换端口,tmd我还得为此再发一次程序,一个字麻烦,说一送一。差点被砍到的第一反应就是把硬编码送到配置文件中。

2 解决办法(host.json)

  你会突然发现要使用到的Configuration属性只能在Startup类中,毕竟在WebHost的Build之前ServiceCollection都没有初始化,哪里有统一化的配置系统呢,
那怎么办,还能怎么办,自己定义一个Configuration了,然后修改的步骤如下:

  1. 新增一个host.json,名字随便定义,自己看得懂就行啦。

六 ASP.NET Web API 中的路由

  本节内容来自这里
  本节内容来自这里
  本节内容来自这里
  本节内容来自这里

七 ASP .NET Core 的运行机制

1 开始

  这篇教程讲了ASP.NET Web API是如何将HTTP请求路由到controller的。
  注意:如果你熟悉ASP.NET MVC,那太好了因为Web API的路由规则与MVC的非常相似,两者最主要的区别是Web API在选择action时使用的是HTTP路径,而不是URI路径。当然你也可以为Web API使用MVC风格的路由规则。这篇文章不涉及任何ASP.NET MVC的知识。

2 路由表(routing table)

  在 ASP.NET Web API 中,controller是一个被用来处理HTTP请求的实体类(class),它的public的方法被称为action(action methods or simply actions)。当Web API framework 收到一个请求时,会将这个请求路由给一个action来处理。
  Web API framework 使用一个路由表来判断调用哪个action。Visual Studio 里的Web API项目模板中含有一个默认的路由规则:

routes.MapHttpRoute(
    name: "API Default",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

3 启动

在这里插入图片描述
  1、Web Server: ASP.NET Core 提供两种服务器可用, 分别是 Kestrel 和 HTTP.sys (Core 1.x 中被命名为 WebListener)。
  2、Kestrel是一个跨平台的Web服务器。
  3、HTTP.sys只能用在Windows系统中.
  4、Internet: 当需要部署在Internal Network 中并需要 Kestrel 中没有的功能(如 Windows 身份验证)时,可以选择HTTP.sys。
  5、IIS、Apache、Nginx: Kestrel 可以单独使用 ,也可以将其与反向代理服务器(如 IIS、Nginx 或 Apache)结合使用。 请求经这些服务器进行初步处理后转发给Kestrel(即图中虚线的可选流程).
  ASP .NET Core 的启动
在这里插入图片描述
  Main:程序的起点. ASP .NET Core 应用程序本质上是控制台应用程序。
  CreateDefaultBuilder:创建并配置WebHostBuilder, 首先调用Create­DefaultBuilder( 如图所示, 它是一系列配置的大综合,下文做详细介绍), 进行一系列配置。
  UseStartup: 指定Startup为启动配置文件. 在Startup中, 将进行两个比较重要的工作, 服务的依赖注入和配置管道。
  ConfigureServices:方法是注册服务
  Configure:方法是配置管道,用来具体指定如何处理每个http请求的, 例如我们可以让这个程序知道我使用mvc来处理http请求, 那就调用app.UseMvc这个方法就行.
  BuildWebHost:生成WebHostBuilder并进行了一系列配置之后, 通过这个WebHostBuilder来Build出一个IWebHost。
  Run:调用IWebHost的Run方法使之开始运行。

4 ASP.NET Core的管道和中间件

  请求管道: 那些处理http requests并返回responses的代码组成了request pipeline(请求管道)
  中间件: 我们可以使用一些程序来配置请求管道(request pipeline)以便处理requestsresponses。 比如处理验证(authentication)的程序, MVC本身就是个中间件(middleware)。
  当接收到一个请求时,请求会交给中间件构成的中间件管道进行处理,管道就是多个中间件构成,请求从一个中间件的一端进入,从中间件的另一端出来,每个中间件都可以对HttpContext请求开始和结束进行处理。
在这里插入图片描述

5 注册中间件(Use、UseMiddleWare、Map、Run)

  本节内容来自这里
  本节内容来自这里
  自己写一个中间件测试下:

public class Floor1Middleware{
    private readonly RequestDelegate _next;
    public Floor1Middleware(RequestDelegate next) { _next = next; }
    public async Task InvokeAsync(HttpContext context)
    {
        Console.WriteLine("Floor1Middleware In"); //Do Something
        //To FloorTwoMiddleware
        await _next(context); //Do Something
        Console.WriteLine("Floor1Middleware Out");
    }
}

public static class Floor1MiddlewareExtensions {
    public static IApplicationBuilder UseFloor1Middleware(this ApplicationBuilder builder)
    {
        return builder.UseMiddleware<Floor1Middleware>;
    }
}

  通过IMiddleware实现:

public class Floor3Middleware : IMiddleware {
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        Console.WriteLine("Floor3Middleware In"); //Do Something
        //To FloorTwoMiddleware
        await next(context);
        //Do Something
        Console.WriteLine("Floor3Middleware Out");
    }
}

public static class MiddlewareExtensions {
    public static IApplicationBuilder UseFloor3Middleware( this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<Floor3Middleware>;
    }
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<Floor3Middleware>();
    services.AddMvc();
}

  也可以用简要的写法,直接在Startup的Configure方法中这样写:

app.Use(async (context, next) =>
{
    Console.WriteLine("Floor2Middleware In");
    await next.Invoke; Console.WriteLine("Floor2Middleware Out");
});
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseFloor1Middleware;
    app.Use(async (context, next) =>
    {
        Console.WriteLine("Floor2Middleware In");
        await next.Invoke;
        Console.WriteLine("Floor2Middleware Out");
    });
    if (env.IsDevelopment)
    {
        app.UseDeveloperExceptionPage;
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }
    app.UseStaticFiles;
    app.UseCookiePolicy;
    app.UseMvc(routes =>
    {
        routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}");
    });
}
/*
DIDemo> Floor1Middleware In DIDemo> Floor2Middleware In DIDemo> Floor3Middleware In DIDemo> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] DIDemo> Route matched with {action = "Index", controller = "Home"}. Executing action DIDemo.Controllers.HomeController.Index (DIDemo) DIDemo> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] DIDemo> Executing action method DIDemo.Controllers.HomeController.Index (DIDemo) - Validation state: Valid DIDemo> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] DIDemo> Executed action method DIDemo.Controllers.HomeController.Index (DIDemo), returned result Microsoft.AspNetCore.Mvc.ViewResult in 0.1167ms.
DIDemo> info: Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[1] DIDemo> Executing ViewResult, running view Index. DIDemo> info: Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[4] DIDemo> Executed ViewResult - view Index executed in 3.3508ms.
DIDemo> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] DIDemo> Executed action DIDemo.Controllers.HomeController.Index (DIDemo) in 9.5638ms DIDemo> Floor3Middleware Out DIDemo> Floor2Middleware Out DIDemo> Floor1Middleware Out
*/

  使用IApplicationBuilder注册中间件,Use()

app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("hello world");
        await next.Invoke();
    });
app.Use((requestDelegate) =>
    {
        return async (context) =>
        {
            await context.Response.WriteAsync("hello world2");
            await requestDelegate(context);
        };
    });

  UseMiddleWare():将中间件封装,最终是使用Use注册;

//自定义中间件
public class TestMiddelware
{
    public RequestDelegate _next;

    public TestMiddelware(RequestDelegate next)
    {
        this._next = next;
    }
    public Task Invoke(HttpContext context)
    {
        if (context.Request.Path.Value.Contains("1.jpg"))
        {
            return context.Response.SendFileAsync("1.jpg");
        }

        if (this._next != null)
        {
            return this._next(context);
        }
        throw new Exception("TestMiddelware error");
    }
}
app.UseMiddleware<TestMiddelware>(app, Array.Empty<object>());

  Run(RequestDelegate handler):终结点,在管道尾端增加一个中间件,之后的中间件不再执行;

app.Run(async context => {
    await context.Response.WriteAsync("hello world3");
});

  Map()MapWhen()管道中增加分支,条件匹配就走分支,且不切换回主分支:

app.Map(new PathString("/test"), application =>
{
    application.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("test");
    await next();
    });
});

  MapWhen():按条件执行,MapWhenMap 处理范围更广:

app.MapWhen(context => context.Request.Path.Value.Contains("q"), application =>
{
    application.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("q");
        await next();
    });
});

  UseWhen():按条件执行,与 MapWhen 不同的是,UseWhen 执行完后切回主分支。

6 ASP.NET Core给中间件传参数的方法

  本节内容来自这里

八 依赖注入

  本节内容来自这里
  官方文档
  依赖注入(Dependency Injection),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。

1 setter注入

  Setter注入(Setter Injection)是指在客户类中,设置一个服务类接口类型的数据成员,并设置一个Set方法作为注入点,这个Set方法接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。

using System;

namespace SetterInjection
{
    internal interface IServiceClass
    {
        String ServiceInfo();
    }

    internal class ServiceClassA : IServiceClass
    {
        public String ServiceInfo()
        {
            return "我是ServceClassA";
        }
    }

    internal class ServiceClassB : IServiceClass
    {
        public String ServiceInfo()
        {
            return "我是ServceClassB";
        }
    }

    internal class ClientClass
    {
        //注入点
        private IServiceClass _serviceImpl;
        //客户类中的方法,初始化注入点
        public void Set_ServiceImpl(IServiceClass serviceImpl)
        {
            this._serviceImpl = serviceImpl;
        }

        public void ShowInfo()
        {
            Console.WriteLine(_serviceImpl.ServiceInfo());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IServiceClass serviceA = new ServiceClassA();
            IServiceClass serviceB = new ServiceClassB();
            ClientClass client = new ClientClass();

            client.Set_ServiceImpl(serviceA);
            client.ShowInfo();//结果:我是ServceClassA
            client.Set_ServiceImpl(serviceB);
            client.ShowInfo();//结果:我是ServceClassB
            Console.ReadKey();
        }
    }
}

2 构造函数注入

  构造注入(Constructor Injection)是指在客户类中,设置一个服务类接口类型的数据成员,并以构造函数为注入点,这个构造函数接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。
  与Setter注入很类似,只是注入点由Setter方法变成了构造方法。这里要注意,由于构造注入只能在实例化客户类时注入一次,所以一点注入,程序运行期间是没法改变一个客户类对象内的服务类实例的。
  由于构造注入和Setter注入的IServiceClassServiceClassAServiceClassB是一样的,所以这里给出另外ClientClass类的示例代码。

namespace ConstructorInjection
{
    internal class ClientClass
    {
        private IServiceClass _serviceImpl;

        public ClientClass(IServiceClass serviceImpl)
        {
            this._serviceImpl = serviceImpl;
        }

        public void ShowInfo()
        {
            Console.WriteLine(_serviceImpl.ServiceInfo());
        }
    }
}

3 ConfigureServices与Configure

  1、Configure配置请求管道;2、ConfigureServices配置服务。
1、ConfigureServices

This method gets called by the runtime. Use this method to add services to the container.
此方法由运行时调用。使用此方法将服务添加到容器。

2、Configure

This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
此方法由运行时调用。使用此方法配置HTTP请求管道。

  ConfigureServices是可选方法,Configure是必须要有的方法;执行顺序:先执行ConfigureServices, 再执行Configure
1、Configure在请求管道中配置中间件

并非每个中间件都需要按照这个确切顺序进行,但是很多中间件都需要遵循这个顺序。
例如UseCors,UseAuthentication和UseAuthorization必须按照显示的顺序。

在这里插入图片描述
  1、异常/错误处理;
  2、HTTPS重定向中间件(UseHttpsRedirection)将HTTP请求重定向到HTTPS;
  3、静态文件中间件(UseStaticFiles)返回静态文件,并使进一步的请求处理短路;
  4、Cookie政策中间件(UseCookiePolicy)使该应用符合EU通用数据保护法规(GDPR)法规;
  5、路由中间件(UseRouting)路由请求;
  6、身份验证中间件(UseAuthentication)尝试在允许用户访问安全资源之前对其进行身份验证;
  7、授权中间件(UseAuthorization)授权用户访问安全资源;
  8、会话中间件(UseSession)建立并维护会话状态。如果应用使用会话状态,请在Cookie策略中间件之后和MVC中间件之前调用会话中间件;
  9、端点路由中间件(UseEndpoints带有MapRazorPages)将Razor Pages端点添加到请求管道。

2、ConfigureServices配置服务

AddLocalization        添加本地化方法
AddLogging             添加记录方法
AddStackExchangeRedis   添加Redis缓存服务
...
...

4 DI简介

  官方DI相对于其它框架(例如 autofac)使用起来麻烦许多,既没有一次注入程序集中所有类的功能,也没有方便的属性注入,所以感觉起来官方的DI框架只是一个简单的标准。
  属性注入:一种被称为 service Locator 的模式,蒋老师在Core文章中也推荐了建议不要使用这种模式。
  首先从 ServiceDescriptorServiceCollection 来认识,这两个类也是注册时使用的类。
  这两个类是我们使用注册服务的两个类型,注册服务时,DI都会封装成一个 ServiceDescriptor 类型进行缓存到 ServiceCollection 类型中,其中 ServiceCollection 有三个扩展类型。

ServiceCollectionServiceExtensions:实现了各种我们所使用了注册方式。
ServiceCollectionDescriptorExtensions:实现了各种 TryAdd 和删除替换等操作。
ServiceCollectionContainerBuilderExtensions:实现了构造 ServiceProvider 实例。

5 ServiceCollection

  使用官方DI时注册我们都是将服务注册到一个 ServiceCollection 对象中, ServiceCollection 类型看名称感觉就是一个服务集合的类型,其实并没有错,IServiceCollection 集合就是一个继承 IList<ServiceDescriptor> 集合接口的一个类型,而 ServiceDescriptor 类型则是一个注册的服务描述类型,我们传入注册最后都会封装为一个 ServiceDescriptor 类型然后缓存到 ServiceCollection 集合之中。
  调用ServiceCollection实例对象的方法进行注册:

static void Main(string[] args)
{
     //使用ServiceCollaction对象的扩展方法进行注册服务
     IServiceCollection services = new ServiceCollection()
          //提供具体实例类
          .AddScoped<IFoo, Foo>()
          //提供实例化具体的工厂
          .AddScoped(typeof(IBar), _ => new Bar())
          //提供具体实例化对象,此方法只适用于Singleton生命周期
          .AddSingleton(typeof(IBaz),new Baz());
}

  IServiceCollection类型的继承关系:

/// <summary>
/// Specifies the contract for a collection of service descriptors.
/// </summary>
public interface IServiceCollection : IList<ServiceDescriptor>{}

   ServiceCollection 本身类型中只有一些 IList<T> 具体实现方法,而所有注册的方法都是以扩展方法提供在一个 ServiceCollectionServiceExtensionsServiceCollectionDescriptorExtensions 这两个扩展类中。
  ServiceCollectionDescriptorExtensions 扩展类中大多都是 TryAdd 添加(不存在则添加),添加时参数直接为 ServiceDescriptor 对象或者有删除或替换操作;
  ServiceCollectionServiceExtensions 扩展类则以上面例子那样进行传入基类与派生类类型(派生类对象或工厂)。
  ServiceCollection类型可用成员

/// <summary>
/// Default implementation of <see cref="IServiceCollection"/>.
/// </summary>
public class ServiceCollection : IServiceCollection
{
    // ServiceDescriptor缓存集合,ServiceDescriptor对象缓存到这个属性中
    private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();
    //注册到当前ServiceCollection对象中的ServiceDescriptor数量
    public int Count => _descriptors.Count;
    public bool IsReadOnly => false;
    //设置索引器
    public ServiceDescriptor this[int index]
    {
        get=> _descriptors[index];
        set=> _descriptors[index] = value;
    }

    //清空所有注册到此ServiceCollection上的ServiceDescriptor对象
    public void Clear() => _descriptors.Clear();

    //查询此ServiceCollection是否包含指定ServiceDescriptor对象
    public bool Contains(ServiceDescriptor item)=> _descriptors.Contains(item);
    //  拷贝ServiceDescriptor
    public void CopyTo(ServiceDescriptor[] array, int arrayIndex) =>_descriptors.CopyTo(array, arrayIndex);

    // 从此ServiceCollection移除指定ServiceDescriptor
    public bool Remove(ServiceDescriptor item)=>_descriptors.Remove(item);

    //获取此ServiceCollection的迭代器
    public IEnumerator<ServiceDescriptor> GetEnumerator()=> _descriptors.GetEnumerator();
    public int IndexOf(ServiceDescriptor item) => _descriptors.IndexOf(item);
    public void Insert(int index, ServiceDescriptor item) => _descriptors.Insert(index, item);
    public void RemoveAt(int index)=> _descriptors.RemoveAt(index);
}

  ServiceCollectionServiceExtensions
  在大部分我们都是调用 ServiceCollectionServiceExtensions 扩展类的方法进行注册到Collection之中的,在这个扩展中提供了大量的重载,以便允许我们采用不同的方式进行注册,泛型、类型参数等。

//列出Sinleton生命周期一部分,Scoped和Transient生命周期都一致

//基类型和派生类型
public static IServiceCollection AddSingleton(this IServiceCollection services,Type serviceType,Type implementationType);

//基类型和派生类型工厂
public static IServiceCollection AddSingleton(this IServiceCollection services,Type serviceType,Func<IServiceProvider, object> implementationFactory)//基类型和派生类型泛型
public static IServiceCollection AddSingleton<TService, TImplementation>(this IServiceCollection services)where TService : class where TImplementation : class, TService

//此方法注册 services必须是一个实例化的类型
public static IServiceCollection AddSingleton<TService>(this IServiceCollection services) where TService : class

//基类型与派生类型实例对象,此方式适用于Sinleton生命周期
public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, TService implementationInstance) where TService : class

  虽然在ServiceCollectionServiceExtensions扩展类中具有大量的重载,但是这是重载都是一些"虚"方法,其最终只是使用了3个方法进行注册;

//使用基类和派生类类型实例化ServiceDescriptor对象,然后进行缓存,
private static IServiceCollection Add(IServiceCollection collection,Type serviceType,Type implementationType,ServiceLifetime lifetime)
{
    var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
    collection.Add(descriptor);
    return collection;
}
//使用基类型和工厂实例化ServiceDescriptor对象,然后进行缓存
private static IServiceCollection Add(IServiceCollection collection,Type serviceType,Func<IServiceProvider, object> implementationFactory,ServiceLifetime lifetime)
{
    var descriptor = new ServiceDescriptor(serviceType, implementationFactory, lifetime);
    collection.Add(descriptor);
    return collection;
}
//使用基类型和具体实例对象实例化ServiceDescriptor对象,然后进行缓存
//此方法只适用于Singleton生命周期
public static IServiceCollection AddSingleton(this IServiceCollection services,Type serviceType,object implementationInstance)
{
    var serviceDescriptor = new ServiceDescriptor(serviceType, implementationInstance);
    services.Add(serviceDescriptor);
    return services;
}

  所调用的注册服务方式最后都是调用上面三个方法进行注册,微软只是只是提供了大量的壳子,从上面可以看出ServiceDescriptor类具有三个构造器起码,分别以三种方式进行实例化
  ServiceCollectionDescriptorExtensions
  ServiceCollectionDescriptorExtensions 扩展类中具有 Replace RemoveAll Add(参数为ServiceDescriptor)和很多重载的 TryAdd 方法;
  Replace(替换方法) 由新的 ServiceDescriptor 对象替换 ServiceType 的第一个 ServiceDescriptor 对象。

//使用一个新的ServiceDescriptor对象替换指定基类的第一个ServiceDescriptor
public static IServiceCollection Replace(this IServiceCollection collection,ServiceDescriptor descriptor)
{
    //		获取注册的第一个serviceType进行删除并添加进这个新的ServiceDescriptor
    var registeredServiceDescriptor = collection.FirstOrDefault(s => s.ServiceType == descriptor.ServiceType);
    if (registeredServiceDescriptor != null)
    {
        collection.Remove(registeredServiceDescriptor);
    }
    collection.Add(descriptor);
    return collection;
}

  RemoveAll(删除方法)Collection 删除指定 ServiceType 的所有 ServiceDescriptor 对象

//		移除指定ServiceType的所有ServiceDescriptor
public static IServiceCollection RemoveAll(this IServiceCollection collection, Type serviceType)
{
    for (var i = collection.Count - 1; i >= 0; i--)
    {
        var descriptor = collection[i];
        if (descriptor.ServiceType == serviceType)
            collection.RemoveAt(i);
    }
    return collection;
}
//		移除指定泛型类型的所有ServiceDescriptor
public static IServiceCollection RemoveAll<T>(this IServiceCollection collection) => 			RemoveAll(collection, typeof(T));

  Add(添加方法) 参数直接为 ServiceDescriptor 对象

public static IServiceCollection Add(this IServiceCollection collection,ServiceDescriptor descriptor)
{
   	collection.Add(descriptor);
   	return collection;
}
public static IServiceCollection Add(this IServiceCollection collection,IEnumerable<ServiceDescriptor> descriptors)
{
   	foreach (var descriptor in descriptors)
         collection.Add(descriptor);
     return collection;
}

  TryAdd和TryAddEnumerable方法
  TryAdd和TryAddEnumerable这两个方法是如果不存在则添加,其中TryAdd方法具有大量的包装方法,跟ServiceCollectionServiceExtensions中Add方法差不多,TryAdd方法如果当前ServiceType已被注册,那么再次注册就不会成功

public static void TryAdd(this IServiceCollection collection,ServiceDescriptor descriptor)
{
     if (!collection.Any(d => d.ServiceType == descriptor.ServiceType))
          collection.Add(descriptor);
}

  有许多类似TryAddTransient方法进行了包装TryAdd

public static void TryAddTransient(this IServiceCollection collection,Type service)
{
     //		使用ServiceDescriptor的静态方法创建实例化方法,
     //		此静态方法用于实例一个ServiceDescriptor对象,也是拥有大量重载
     var descriptor = ServiceDescriptor.Transient(service, service);
     TryAdd(collection, descriptor);
}

  TryAddEnumerable方法在添加时除了判断基类型之外也会判断其派生类型是否被注册过

public static void TryAddEnumerable(this IServiceCollection services,ServiceDescriptor descriptor)
{
     //      ServiceDescriptor.GetImplementationType()是获取派生类型
     //      使用TryAddEnumerable进行判断时也会判断其派生类型
     var implementationType = descriptor.GetImplementationType();
     if (implementationType == typeof(object) ||
         implementationType == descriptor.ServiceType)
     {
          throw new ArgumentException(
               Resources.FormatTryAddIndistinguishableTypeToEnumerable(
                    implementationType,
                    descriptor.ServiceType),
               nameof(descriptor));
     }
	//	如果当前基类型和当前派生类型没有注册过,便进行注册
     if (!services.Any(d =>
                       d.ServiceType == descriptor.ServiceType &&
                       d.GetImplementationType() == implementationType))
          services.Add(descriptor);
}

public static void TryAddEnumerable(this IServiceCollection services,IEnumerable<ServiceDescriptor> descriptors)
{
    foreach (var d in descriptors)
        services.TryAddEnumerable(d);
}

  ServiceCollectionContainerBuilderExtensions
  这个扩展类是创建IServiceProvider的,在这个扩展类中只具有BuildServiceProvider()方法,这个方法也就是我们用来获取ServiceProvider类型,ServiceProvider是获取服务对象的类型

public static ServiceProvider BuildServiceProvider(this IServiceCollection services)
    //		使用默认的ServiceProviderOptions实例
     =>BuildServiceProvider(services, ServiceProviderOptions.Default);

public static ServiceProvider BuildServiceProvider(this IServiceCollection services, bool validateScopes)
      =>services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = validateScopes });

 public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
      => new ServiceProvider(services, options);

  可以看到这个方法具有三个重载,在所有重载中都有一个ServiceProviderOptions类型,这是一个什么类型呢, 首先看一下这个类型定义
  ServiceProviderOptions

public class ServiceProviderOptions
{
     internal static readonly ServiceProviderOptions Default = new ServiceProviderOptions();
     /// <summary>
     /// 当此属性为true,不能从获取顶级容器中的scoped
     /// </summary>
     public bool ValidateScopes { get; set; }
     /// <summary>
     ///        实例化ServiceProvider模式,当前只能使用Dynamic模式
     /// </summary>
     internal ServiceProviderMode Mode { get; set; } = ServiceProviderMode.Dynamic;
}

  这个类中具有三个数据,一个是当前类的默认实例Default ,一个是实例化ServiceProvider的模式 ServiceProvderMode是一个枚举,默认为Dynamic,这个属性是internal修饰的,所以在外部使用时是不可以设置的,然而目前这三种都是使用了Dynamic

internal enum ServiceProviderMode
{
   Dynamic,
   Runtime,
   Expressions,
   ILEmit
}

  还有一个Bool类型属性ValidateScopes,如果这个类型为true,则不能从顶级容器中获取scoped生命周期的服务
  ServiceDescriptor
  此类型是服务注册的描述类型,此类型中拥有注册的ServiceType(基类型) ImplementationType(派生类型)/具体服务对象/实例化服务类型的工厂 和注册服务的生命周期Lifetime

//      注册的类型的生命周期
/// <inheritdoc />
public ServiceLifetime Lifetime { get; }
//      注册类型的基类型
/// <inheritdoc />
public Type ServiceType { get; }
//     注册类型的实例类型(派生类型)
/// <inheritdoc />
public Type ImplementationType { get; }
//      注册类型的实例对象
/// <inheritdoc />
public object ImplementationInstance { get; }
//      注册类型实例化对象的工厂
/// <inheritdoc />
public Func<IServiceProvider, object> ImplementationFactory { get; }

  ServiceDescriptor类型中具有三个构造函数,就是使用派生类型,工厂和具体实例对象三种实例化服务对象方式

public ServiceDescriptor(Type serviceType,object instance)
     : this(serviceType, ServiceLifetime.Singleton)
{
      Lifetime = lifetime;
      ServiceType = serviceType;
      //      对内部维护的注册类型对象进行赋值
      ImplementationInstance = instance;
}
public ServiceDescriptor(Type serviceType,Func<IServiceProvider, object> factory,ServiceLifetime lifetime)
     : this(serviceType, lifetime)
{
      Lifetime = lifetime;
      ServiceType = serviceType;
      //         对内部维护的实例化注册对象的工厂进行赋值
      ImplementationFactory = factory;
}
public ServiceDescriptor(Type serviceType,Type implementationType,ServiceLifetime lifetime)
     : this(serviceType, lifetime)
{
      Lifetime = lifetime;
      ServiceType = serviceType;
      //     对象内部维护的实现类型进行赋值
      ImplementationType = implementationType;
}

  此类中方法具有一个获取实际注册类型GetImplementationType()和一批实例化ServiceDescriptor对象的方法 GetImplementationType()方法根据其实例化ServiceDescriptor的方法进行判断获取实例化的实际类型,
  ?访问修饰符是internal,所以此方法并没有对外开放,只允许内部使用

/// <summary>
///     获取当前注册类型的实例类型
/// </summary>
/// <returns></returns>
internal Type GetImplementationType()
{
     if (ImplementationType != null)
          return ImplementationType;
     else if (ImplementationInstance != null)
          return ImplementationInstance.GetType();
     else if (ImplementationFactory != null)
     {
          var typeArguments = ImplementationFactory.GetType().GenericTypeArguments;
          return typeArguments[1];
     }
     return null;
}

  实例化本类对象的方法具有很多重载,跟ServiceCollectionDescriptorExtensions``ServiceCollectionServiceExtensions扩展类一样,其中ServiceCollectionDescriptorExtensions扩展类中便利用了这些方法进行实例化此对象

//		真正实例化对象的方法,重载都是调用此类方法
public static ServiceDescriptor Describe(Type serviceType, Func<IServiceProvider, object> implementationFactory, ServiceLifetime lifetime)
     => new ServiceDescriptor(serviceType, implementationFactory, lifetime);

 public static ServiceDescriptor Describe(Type serviceType, Type implementationType, ServiceLifetime lifetime)
    => new ServiceDescriptor(serviceType, implementationType, lifetime);
//		此方法只有Sinleton生命周期才能调用
public static ServiceDescriptor Singleton(Type serviceType,object implementationInstance)
    =>new ServiceDescriptor(serviceType, implementationInstance);

  测试
  TryAdd

static void Main(string[] args)
{
     IServiceCollection services = new ServiceCollection()
          .AddScoped(typeof(IBaz),typeof(Baz2));
     //		尝试注册使用TryAdd再次注册IBaz类型
     services.TryAdd(new ServiceDescriptor(typeof(IBaz), typeof(Baz1), ServiceLifetime.Scoped));
     var provider=  services.BuildServiceProvider();
     //		获取所有IBaz的注册对象
     IList<IBaz> baz=  provider.GetServices<IBaz>().ToList();
     Console.WriteLine("获取到的数量:"+baz.Count);
     //		循环输出所有实际对象类型
     foreach (var item in baz)
          Console.WriteLine("实际类型:" + item.GetType());
}

在这里插入图片描述
  从结果看出TryAdd方法并没有将IBaz再次注册到ServiceCollection对象
  TryAddEnumerable

static void Main(string[] args)
{
     IServiceCollection services = new ServiceCollection()
          .AddScoped(typeof(IBaz),typeof(Baz2));
     //		使用TryAddEnumerable尝试注册
     services.TryAddEnumerable(new ServiceDescriptor(typeof(IBaz), typeof(Baz2), ServiceLifetime.Scoped));
     services.TryAddEnumerable(new ServiceDescriptor(typeof(IBaz), typeof(Baz1), ServiceLifetime.Scoped));
     var provider=  services.BuildServiceProvider();
     IList<IBaz> baz=  provider.GetServices<IBaz>().ToList();
     Console.WriteLine("获取到的数量:"+baz.Count);
     foreach (var item in baz)
          Console.WriteLine("实际类型:" + item.GetType());
}

在这里插入图片描述
  ?注意:使用TryAddEnumerable进行注册时不能使用工厂方法实例对象那种方式

static void Main(string[] args)
{
     IServiceCollection services = new ServiceCollection()
          .AddScoped(typeof(IBaz),typeof(Baz2));
     //      使用工厂方法实例化对象方式
     var service = ServiceDescriptor
          .Scoped<IBaz>(_ => new Baz1());
     //      使用TryAddEnumerable进行注册,会抛出一个System.ArgumentException异常
     services.TryAddEnumerable(service);
}

在这里插入图片描述

6 测试ServiceProviderOptions的ValidateScopes

static void Main(string[] args)
{
     //      顶级容器
     IServiceProvider provider = new ServiceCollection()
          .AddScoped(typeof(IFoo), typeof(Foo))
          //        设置不能从顶级容器中获取scoped生命周期服务
          .BuildServiceProvider(true);
     //  顶级容器构造Foo对象
     var foo1= provider.GetService<IFoo>();
}

  如果运行上面程序,则会抛出一个 InvalidOperationException 异常
在这里插入图片描述
  可以看到并不允许让我们创建顶级容器的 scoped 服务对象,但是如果我们使用子容器就不会抛出异常

static void Main(string[] args)
{
     //      顶级容器
     IServiceProvider provider = new ServiceCollection()
          .AddScoped(typeof(IFoo), typeof(Foo))
          //        设置不能从顶级容器中获取scoped生命周期服务
          .BuildServiceProvider(true);
     //      子容器
     IServiceProvider childProvider= provider.CreateScope().ServiceProvider;
     var foo2= childProvider.GetService<IFoo>();
}

在这里插入图片描述
  在 .NET Core 里面,有一个 IServiceCollection,这个是 .NET Core 框架自带的一个容器。下面代码,是本次实例中需要注入的类型。

namespace Bingle.Core.Interface
{
    public interface ITestServiceA
    {
        void Show();
    }
}
namespace Bingle.Core.Service
{
    public class TestServiceA : ITestServiceA
    {
        public void Show()
        {
            Console.WriteLine("A123456");
        }
    }
}

namespace Bingle.Core.Interface
{
    public interface ITestServiceB
    {
        void Show();
    }
}

namespace Bingle.Core.Service
{
    public class TestServiceB : ITestServiceB
    {
        public TestServiceB(ITestServiceA iTestService)
        {
        }
        public void Show()
        {
            Console.WriteLine($"This is TestServiceB B123456");
        }
    }
}

namespace Bingle.Core.Interface
{
    public interface ITestServiceC
    {
        void Show();
    }
}

namespace Bingle.Core.Service
{
    public class TestServiceC : ITestServiceC
    {
        public TestServiceC(ITestServiceB iTestServiceB)
        {
        }
        public void Show()
        {
            Console.WriteLine("C123456");
        }
    }
}

namespace Bingle.Core.Interface
{
    public interface ITestServiceD
    {
        void Show();
    }
}

namespace Bingle.Core.Service
{
    public class TestServiceD : ITestServiceD
    {
        public void Show()
        {
            Console.WriteLine("D123456");
        }
    }
}

  需要通过Nuget包,把 IServiceCollection 依赖的dll文件进入进来Microsoft.Extensions.DependencyInjection
  使用容器的三部曲:实例化一个容器、注册、获取服务:

IServiceCollection container = new ServiceCollection();
// IServiceCollection
container.AddTransient<ITestServiceA, TestServiceA>();  // 瞬时生命周期  每一次获取的对象都是新的对象
container.AddSingleton<ITestServiceB, TestServiceB>(); // 单例生命周期  在容器中永远只有当前这一个
container.AddScoped<ITestServiceC, TestServiceC>();    //当前请求作用域内  只有当前这个实例
container.AddSingleton<ITestServiceD>(new TestServiceD());  // 也是单例生命周期,加入服务后直接创建

ServiceProvider provider = container.BuildServiceProvider();

ITestServiceA testA = provider.GetService<ITestServiceA>();
ITestServiceA testA1 = provider.GetService<ITestServiceA>();
Console.WriteLine(object.ReferenceEquals(testA, testA1));

ITestServiceB testB = provider.GetService<ITestServiceB>();
ITestServiceB testB1 = provider.GetService<ITestServiceB>();
Console.WriteLine(object.ReferenceEquals(testB, testB1));

ITestServiceC testC = provider.GetService<ITestServiceC>();
ITestServiceC testC1 = provider.GetService<ITestServiceC>();
Console.WriteLine(object.ReferenceEquals(testC, testC1));

IServiceScope scope = provider.CreateScope();
ITestServiceC testc3 = provider.GetService<ITestServiceC>();
var testc4 = scope.ServiceProvider.GetService<ITestServiceC>();
Console.WriteLine(object.ReferenceEquals(testc3, testc4));

ITestServiceD testD = provider.GetService<ITestServiceD>();
ITestServiceD testD1 = provider.GetService<ITestServiceD>();
Console.WriteLine(object.ReferenceEquals(testD, testD1));

  权重:AddSingleton→AddTransient→AddScoped;
  AddSingleton(单例)的生命周期:项目启动-项目关闭,相当于静态类,只会有一个;
  AddScoped(作用域)的生命周期:请求开始-请求结束,在这次请求中获取的对象都是同一个;
  AddTransient(瞬时)的生命周期:>请求获取-(GC回收-主动释放)每一次获取的对象都不是同一个。
  注意:由于AddScoped对象是在请求的时候创建的;所以不能在AddSingleton对象中使用;甚至也不能在AddTransient对象中使用。

7 依赖获取

  上面提到的注入方式,都是客户类被动接受所依赖的服务类,这也符合“注入”这个词。不过还有一种方法,可以和依赖注入达到相同的目的,就是依赖获取。
  依赖获取(Dependency Locate)是指在系统中提供一个获取点,客户类仍然依赖服务类的接口。当客户类需要服务类时,从获取点主动取得指定的服务类,具体的服务类类型由获取点的配置决定。
  可以看到,这种方法变被动为主动,使得客户类在需要时主动获取服务类,而将多态性的实现封装到获取点里面。获取点可以有很多种实现,也许最容易想到的就是建立一个Simple Factory作为获取点,客户类传入一个指定字符串,以获取相应服务类实例。如果所依赖的服务类是一系列类,那么依赖获取一般利用Abstract Factory模式构建获取点,然后,将服务类多态性转移到工厂的多态性上,而工厂的类型依赖一个外部配置,如XML文件。
  不过,不论使用Simple Factory还是Abstract Factory,都避免不了判断服务类类型或工厂类型,这样系统中总要有一个地方存在不符合OCP的if…else或switch…case结构,这种缺陷是Simple Factory和Abstract Factory以及依赖获取本身无法消除的,而在某些支持反射的语言中(如C#),通过将反射机制的引入彻底解决了这个问题(后面讨论)。
  下面给一个具体的例子,现在我们假设有个程序,既可以使用Windows风格外观,又可以使用Mac风格外观,而内部业务是一样的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZmYmNcPn-1658306188682)(https://www.icode9.com/i/l/?n=18&i=blog/740100/202002/740100-20200225175347351-649285439.png#pic_center)]
  上图乍看有点复杂,不过如果读者熟悉Abstract Factory模式,应该能很容易看懂,这就是Abstract Factory在实际中的一个应用。这里的Factory Container作为获取点,是一个静态类,它的“Type构造函数”依据外部的XML配置文件,决定实例化哪个工厂。下面还是来看示例代码。由于不同组件的代码是相似的,这里只给出Button组件的示例代码,完整代码请参考文末附上的完整源程序。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DependencyLocate
{
    internal interface IButton
    {
        String ShowInfo();
    }

    internal sealed class WindowsButton : IButton
    {
        public String Description { get; private set; }

        public WindowsButton()
        {
            this.Description = "Windows风格按钮";
        }

        public String ShowInfo()
        {
            return this.Description;
        }
    }

    internal sealed class MacButton : IButton
    {
        public String Description { get; private set; }

        public MacButton()
        {
            this.Description = " Mac风格按钮";
        }

        public String ShowInfo()
        {
            return this.Description;
        }
    }

    internal interface IFactory
    {
        IWindow MakeWindow();
        IButton MakeButton();
        ITextBox MakeTextBox();
    }

    internal sealed class WindowsFactory : IFactory
    {
        public IWindow MakeWindow()
        {
            return new WindowsWindow();
        }

        public IButton MakeButton()
        {
            return new WindowsButton();
        }

        public ITextBox MakeTextBox()
        {
            return new WindowsTextBox();
        }
    }

    internal sealed class MacFactory : IFactory
    {
        public IWindow MakeWindow()
        {
            return new MacWindow();
        }

        public IButton MakeButton()
        {
            return new MacButton();
        }

        public ITextBox MakeTextBox()
        {
            return new MacTextBox();
        }
    }

    internal static class FactoryContainer
    {
        public static IFactory factory { get; private set; }

        static FactoryContainer()
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load("http://www.cnblogs.com/Config.xml");
            XmlNode xmlNode = xmlDoc.ChildNodes[1].ChildNodes[0].ChildNodes[0];

            if ("Windows" == xmlNode.Value)
            {
                factory = new WindowsFactory();
            }
            else if ("Mac" == xmlNode.Value)
            {
                factory = new MacFactory();
            }
            else
            {
                throw new Exception("Factory Init Error");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IFactory factory = FactoryContainer.factory;
            IWindow window = factory.MakeWindow();
            Console.WriteLine("创建 " + window.ShowInfo());
            IButton button = factory.MakeButton();
            Console.WriteLine("创建 " + button.ShowInfo());
            ITextBox textBox = factory.MakeTextBox();
            Console.WriteLine("创建 " + textBox.ShowInfo());

            Console.ReadLine();
        }
    }
}

  这里我们用XML作为配置文件。配置文件Config.xml如下:

<?xml version="1.0" encoding="utf-8" ?>
<config>
    <factory>Mac</factory>
</config>

8 .Net Core学习依赖注入自定义Service

  1. 定义一个服务,包含一个方法:
public class TextService
{
    public string Print(string m)
    {
        return m;
    }
}
  1. 写一个扩展方法用来注入服务:
using Haos.Develop.CoreTest.Service;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Haos.Develop.CoreTest
{
    public static class Extension
    {
        public static IServiceCollection AddTestService(this IServiceCollection service)
        {
            return service.AddScoped(factory => new TextService());
        }
    }
}
  1. 回到Startup类中找到ConfigureServices方法添加如下代码:
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddTestService();//手动高亮
}
  1. 我们可以采用构造函数方式来使用或者方法用参数的形式注入和直接获取:
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Haos.Develop.CoreTest.Service;

namespace Haos.Develop.CoreTest.Controllers
{
    public class HomeController : Controller
    {
        public TextService T;

        public HomeController(TextService t)
        {
            T = t;
        }

        public ActionResult Index()
        {
            return Content("ok");
        }
        /// <summary>
        /// 使用构造函数注入
        /// </summary>
        /// <returns></returns>
        public JsonResult Test()
        {
            T.Print("哈哈哈哈哈哈哈哈哈哈哈哈");
            return Json("");
        }
        /// <summary>
        /// 参数注入
        /// </summary>
        /// <param name="t2"></param>
        /// <returns></returns>
        public JsonResult Test2(TextService t2)
        {
            t2.Print("哈哈哈哈哈哈哈哈哈哈哈哈");
            return Json("");
        }
        /// <summary>
        /// 直接获取
        /// </summary>
        /// <returns></returns>
        public JsonResult Test3()
        {
            var t3 = HttpContext.RequestServices.GetService(typeof(TextService)) as TextService;
            t3.Print("哈哈哈哈哈哈哈哈哈哈哈哈");
            return Json("");
        }
    }
}
  1. 如果存在参数可以在构造函数中赋值,示例:
//修改第一点的代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Haos.Develop.CoreTest.Service
{
    public class TextService
    {
        public string StrString { get; set; }

        public TextService(string m)
        {
            StrString = m;
        }

        public string Print(string m)
        {
            return StrString + m;
        }
    }
}
//修改第二点的代码
using Haos.Develop.CoreTest.Service;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Haos.Develop.CoreTest
{
    public static class Extension
    {
        public static IServiceCollection AddTestService(this IServiceCollection service, string str)
        {
            return service.AddScoped(factory => new TextService(str));
        }
    }
}
//最后注入
public void ConfigureServices(IServiceCollection services)
{
    services.AddTestService("this test param");
}

九 ASP.NET Core MVC/WebAPi模型绑定探索

  本节内容来自这里
  在ASP.NET Core之前MVC和Web APi被分开,也就说其请求管道是独立的,而在ASP.NET Core中,WebAPi和MVC的请求管道被合并在一起,当我们建立控制器时此时只有一个Controller的基类而不再是Controller和APiController。所以才有本节的话题在模型绑定上呈现出有何不同呢?下面我们一起来看看。
  ASP.NET MVC模型绑定:

public class Person
{
    public string Name { get; set; }
    public string Address { get; set; }
    public int Age { get; set; }
}

  接着 POST 请求通过 Action 方法进行模型绑定。

[HttpPost]
public JsonResult PostPerson(Person p)
{
    return Json(p);
}

  到这里,后台就大概over了,是不是就这么完了呢,我们一直在强调模型绑定这个词语,那么到底什么是模型绑定呢,有必要解释下。我们 PostPerson 这个方法中有一个 Person 的变量参数,那么问题来了,前台发出请求到这个方法从而该参数接收到传递过来的数据从而进行响应,这个p到底是怎么接收到的呢,恩,通过模型绑定呗,为了将数据聚合到对象或者其他简单的参数可以通过模型绑定来查找数据,常见的绑定方式有如下四种。
  路由值(Route Values):通过导航到路由如 {controller}/{action}/{id} 此时将绑定到id参数。
  查询字符串(QueryStrings):通过查询字符串中的参数来绑定,如 name=Jeffcky&id=1,此时name和id将进行绑定。
  请求Body(Body):通过在POST请求中将数据传入到Body中此时将绑定如上述Person对象中。
  请求Header(Header):绑定数据到Http中的请求头中,这种相对来说比较少见。

十 ASP.NET Core IHostedService手动开始/停止/暂停

  我想在ASPNET内核中实现一个递归(定时) iHoSTDDService 实例,它可以按需停止和启动。我的理解是 ihostedService 是在应用程序启动时由框架启动的。
  但是,我希望能够“手动”启动/停止服务,也许可以通过ui使用on/off开关。理想情况下,“关闭”状态将丢弃当前正在运行的服务,然后“打开”状态将创建一个新实例。
  我在这里读过MS文档:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1
  我最初的想法是获取一个运行服务的实例,然后调用公共<cc>方法。然而,当涉及到应该传入哪个令牌时,我有点卡住了,对于 StopAsync(CancellationToken token) 方法也可以这样说。
  有什么办法可以做到这一点,或者这是否明智?我的方法是否与aspnet core中托管服务的预期设计背道而驰?
  因此,在进行了更多的研究(也就是实际阅读文档:d)之后,似乎托管服务 startasync/stopasync 方法实际上是为了与应用程序的生命周期相一致。注册的 ihostedServices 似乎没有添加到DI容器中以注入其他类。
  因此,我认为我最初的想法行不通。现在,我用配置依赖项 (StartAsync(CancellationToken cancellationToken)) 注册了我的服务,这些配置依赖关系可以在运行时更新。当托管服务正在处理时,它将检查配置是否应该继续,否则它将只是等待(而不是停止或丢弃托管服务)。
  我可能很快就会把这个作为我的答案,除非我听到其他的想法。
  最佳答案:
  对于 StopAsync(CancellationToken token),可以通过 new System.Threading.CancellationToken()。在 public CancellationToken(bool canceled) 的定义中,canceled 表示令牌的状态。对于您的场景,不需要指定 canceled,因为您要停止服务。
  您可以一步一步地执行以下操作:
  创建 IHostedService

public class RecureHostedService : IHostedService, IDisposable{
    private readonly ILogger _log;
    private Timer _timer;
    public RecureHostedService(ILogger<RecureHostedService> log)
    {
        _log = log;
    }

    public void Dispose()
    {
        _timer.Dispose();
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _log.LogInformation("RecureHostedService is Starting");
        _timer = new Timer(DoWork,null,TimeSpan.Zero, TimeSpan.FromSeconds(5));
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _log.LogInformation("RecureHostedService is Stopping");
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }
    private void DoWork(object state)
    {
        _log.LogInformation("Timed Background Service is working.");
    }
}

  注册 IHostedService

services.AddSingleton<IHostedService, RecureHostedService>();

  启动和停止服务:

public class HomeController : Controller {
    private readonly RecureHostedService _recureHostedService;
    public HomeController(IHostedService hostedService)
    {
        _recureHostedService = hostedService as RecureHostedService;
    }
    public IActionResult About()
    {
        ViewData["Message"] = "Your application description page.";
        _recureHostedService.StopAsync(new System.Threading.CancellationToken());
        return View();
    }

    public IActionResult Contact()
    {
        ViewData["Message"] = "Your contact page.";
        _recureHostedService.StartAsync(new System.Threading.CancellationToken());
        return View();
    }
}

  https://www.cnblogs.com/linxingxunyan/p/10571247.html
  https://www.cnblogs.com/8090sns/p/8086828.html
  https://www.cnblogs.com/artech/p/application-life-time.html

十一 ASP.NET Core Web API使用静态页面

  官方文档
  1、配置应用提供静态文件并启用默认文件映射。在 Startup.cs 的 Configure 方法中需要以下的代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    //添加下面两句
    app.UseDefaultFiles(); //1
    app.UseStaticFiles(); //2
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

  2、在项目根中创建 wwwroot 文件夹 。
  3、将一个名为 index.html 的 HTML 文件添加到 wwwroot 文件夹 。
  可能需要更改 ASP.NET Core 项目的启动设置,以便对 HTML 页面进行本地测试:
  1、打开 Properties\launchSettings.json 。
  2、删除 launchUrl 以便在项目的默认文件 index.html 强制打开应用 。

十二 ASP.NET Core控件

1 WebSocket

  在 Startup 类的 Configure 方法中添加 WebSocket 中间件:

using System.Net.WebSockets;
using Microsoft.AspNetCore.Http;

app.UseWebSockets();

  可配置以下设置:
  KeepAliveInterval - 向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态。 默认值为 2 分钟。
  ReceiveBufferSize - 用于接收数据的缓冲区的大小。 高级用户可能需要对其进行更改,以便根据数据大小调整性能。 默认值为 4 KB。
  AllowedOrigins - 用于 WebSocket 请求的允许的 Origin 标头值列表。 默认情况下,允许使用所有源。 有关详细信息,请参阅以下“WebSocket 源限制”。

var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
    ReceiveBufferSize = 4 * 1024
};

app.UseWebSockets(webSocketOptions);

2 log4net

1、nuget安装
Microsoft.Extensions.Logging.Log4Net.AspNetCore
2、配置文件

<?xml version="1.0" encoding="utf-8" ?>
<log4net>
    <!--根配置-->
    <root>
        <!--日志级别:可选值: ERROR > WARN > INFO > DEBUG -->
        <level value="ERROR"/>
        <level value="WARN"/>
        <level value="INFO"/>
        <level value="DEBUG"/>
        <appender-ref ref="ErrorLog" />
        <appender-ref ref="WarnLog" />
        <appender-ref ref="InfoLog" />
        <appender-ref ref="DebugLog" />
    </root>
    <!-- 错误 Error.log-->
    <appender name="ErrorLog" type="log4net.Appender.RollingFileAppender">
        <!--目录路径,可以是相对路径或绝对路径-->
        <param name="File" value="C:\logs\"/>
        <!--文件名,按日期生成文件夹-->
        <param name="DatePattern" value="/yyyy-MM-dd/"Error.log""/>
        <!--追加到文件-->
        <appendToFile value="true"/>
        <!--创建日志文件的方式,可选值:Date[日期],文件大小[Size],混合[Composite]-->
        <rollingStyle value="Composite"/>
        <!--写到一个文件-->
        <staticLogFileName value="false"/>
        <!--单个文件大小。单位:KB|MB|GB-->
        <maximumFileSize value="200MB"/>
        <!--最多保留的文件数,设为"-1"则不限-->
        <maxSizeRollBackups value="-1"/>
        <!--日志格式-->
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="[%d{HH:mm:ss}]%m%n"/>
        </layout>
        <filter type="log4net.Filter.LevelRangeFilter">
            <param name="LevelMin" value="ERROR" />
            <param name="LevelMax" value="ERROR" />
        </filter>
    </appender>
 
    <!-- 警告 Warn.log-->
    <appender name="WarnLog" type="log4net.Appender.RollingFileAppender">
        <!--目录路径,可以是相对路径或绝对路径-->
        <param name="File" value="C:\logs\"/>
        <!--文件名,按日期生成文件夹-->
        <param name="DatePattern" value="/yyyy-MM-dd/"Warn.log""/>
        <!--追加到文件-->
        <appendToFile value="true"/>
        <!--创建日志文件的方式,可选值:Date[日期],文件大小[Size],混合[Composite]-->
        <rollingStyle value="Composite"/>
        <!--写到一个文件-->
        <staticLogFileName value="false"/>
        <!--单个文件大小。单位:KB|MB|GB-->
        <maximumFileSize value="200MB"/>
        <!--最多保留的文件数,设为"-1"则不限-->
        <maxSizeRollBackups value="-1"/>
        <!--日志格式-->
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="[%d{HH:mm:ss}]%m%n"/>
        </layout>
        <filter type="log4net.Filter.LevelRangeFilter">
            <param name="LevelMin" value="WARN" />
            <param name="LevelMax" value="WARN" />
        </filter>
    </appender>
 
    <!-- 信息 Info.log-->
    <appender name="InfoLog" type="log4net.Appender.RollingFileAppender">
        <!--目录路径,可以是相对路径或绝对路径-->
        <param name="File" value="C:\logs\"/>
        <!--文件名,按日期生成文件夹-->
        <param name="DatePattern" value="/yyyy-MM-dd/"Info.log""/>
        <!--追加到文件-->
        <appendToFile value="true"/>
        <!--创建日志文件的方式,可选值:Date[日期],文件大小[Size],混合[Composite]-->
        <rollingStyle value="Composite"/>
        <!--写到一个文件-->
        <staticLogFileName value="false"/>
        <!--单个文件大小。单位:KB|MB|GB-->
        <maximumFileSize value="200MB"/>
        <!--最多保留的文件数,设为"-1"则不限-->
        <maxSizeRollBackups value="-1"/>
        <!--日志格式-->
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="[%d{HH:mm:ss}]%m%n"/>
        </layout>
        <filter type="log4net.Filter.LevelRangeFilter">
            <param name="LevelMin" value="INFO" />
            <param name="LevelMax" value="INFO" />
        </filter>
    </appender>
 
    <!-- 调试 Debug.log-->
    <appender name="DebugLog" type="log4net.Appender.RollingFileAppender">
        <!--目录路径,可以是相对路径或绝对路径-->
        <param name="File" value="C:\logs\"/>
        <!--文件名,按日期生成文件夹-->
        <param name="DatePattern" value="/yyyy-MM-dd/"Debug.log""/>
        <!--追加到文件-->
        <appendToFile value="true"/>
        <!--创建日志文件的方式,可选值:Date[日期],文件大小[Size],混合[Composite]-->
        <rollingStyle value="Composite"/>
        <!--写到一个文件-->
        <staticLogFileName value="false"/>
        <!--单个文件大小。单位:KB|MB|GB-->
        <maximumFileSize value="200MB"/>
        <!--最多保留的文件数,设为"-1"则不限-->
        <maxSizeRollBackups value="-1"/>
        <!--日志格式-->
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="[%d{HH:mm:ss}]%m%n"/>
        </layout>
        <filter type="log4net.Filter.LevelRangeFilter">
            <param name="LevelMin" value="DEBUG" />
            <param name="LevelMax" value="DEBUG" />
        </filter>
    </appender>
 
</log4net>

在Startup.cs类里面配置使用log4net

public static ILoggerRepository repository { get; set; }
public Startup(IConfiguration configuration)
{
     Configuration = configuration;
     repository = LogManager.CreateRepository("NETCoreRepository");
     // 指定配置文件
     XmlConfigurator.Configure(repository, new FileInfo("log4net.config"));
}

4、在控制器里面注入log4net

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using log4net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using NetCoreLogDemo.Models;
 
namespace NetCoreLogDemo.Controllers
{
    public class HomeController : Controller
    {
        private ILog log; 
        public HomeController(IHostingEnvironment hostingEnv)
        {
            this.log = LogManager.GetLogger(Startup.repository.Name, typeof(HomeController));
        }
 
        public IActionResult Index()
        {
            log.Error("测试日志");
            return View();
        }
    }
}

十三 谈谈.NET Core中基于Generic Host来实现后台任务

  本节内容来自这里

1 前言

  很多时候,后台任务对我们来说是一个利器,帮我们在后面处理了成千上万的事情。
  在.NET Framework时代,我们可能比较多的就是一个项目,会有一到多个对应的Windows服务,这些Windows服务就可以当作是我们所说的后台任务了。
  我喜欢将后台任务分为两大类,一类是不停的跑,好比MQ的消费者,RPC的服务端。另一类是定时的跑,好比定时任务。
  那么在.NET Core时代是不是有一些不同的解决方案呢?答案是肯定的。
  Generic Host就是其中一种方案,也是本文的主角。

2 什么是Generic Host

  Generic Host是ASP.NET Core 2.1中的新增功能,它的目的是将HTTP管道从Web Host的API中分离出来,从而启用更多的Host方案。
  这样可以让基于Generic Host的一些特性延用一些基础的功能。如:如配置、依赖关系注入和日志等。
  Generic Host更倾向于通用性,换句话就是说,我们即可以在Web项目中使用,也可以在非Web项目中使用!
  虽然有时候后台任务混杂在Web项目中并不是一个太好的选择,但也并不失是一个解决方案。尤其是在资源并不充足的时候。
  比较好的做法还是让其独立出来,让它的职责更加单一。
  下面就先来看看如何创建后台任务吧。

3 后台任务示例

  我们先来写两个后台任务(一个一直跑,一个定时跑),体验一下这些后台任务要怎么上手,同样也是我们后面要使用到的。

这两个任务统一继承BackgroundService这个抽象类,而不是IHostedService这个接口。后面会说到两者的区别。

一直跑的后台任务
先上代码

public class PrinterHostedService2 : BackgroundService
{
    private readonly ILogger _logger;
    private readonly AppSettings _settings;

    public PrinterHostedService2(ILoggerFactory loggerFactory, IOptionsSnapshot<AppSettings> options)
    {
        this._logger = loggerFactory.CreateLogger<PrinterHostedService2>();
        this._settings = options.Value;
    }

    public override Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Printer2 is stopped");
        return Task.CompletedTask;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation($"Printer2 is working. {_settings.PrinterDelaySecond}");
            await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond), stoppingToken);
        }
    }
}

  来看看里面的细节。
  我们的这个服务继承了BackgroundService,就一定要实现里面的ExecuteAsync,至于StartAsync和StopAsync等方法可以选择性的override。
  我们ExecuteAsync在里面就是输出了一下日志,然后休眠在配置文件中指定的秒数。
  这个任务可以说是最简单的例子了,其中还用到了依赖注入,如果想在任务中注入数据仓储之类的,应该就不需要再多说了。
  同样的方式再写一个定时的。
  定时跑的后台任务
  这里借助了Timer来完成定时跑的功能,同样的还可以结合Quartz来完成。

public class TimerHostedService : BackgroundService
{
    //other ...

    private Timer _timer;

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_settings.TimerPeriod));
        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timer is working");
    }

    public override Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timer is stopping");
        _timer?.Change(Timeout.Infinite, 0);
        return base.StopAsync(cancellationToken);
    }

    public override void Dispose()
    {
        _timer?.Dispose();
        base.Dispose();
    }
}

  和第一个后台任务相比,没有太大的差异。
  下面我们先来看看如何用控制台的形式来启动这两个任务。
  控制台形式
  这里会同时引入NLog来记录任务跑的日志,方便我们观察。
  Main函数的代码如下:

class Program
{
    static async Task Main(string[] args)
    {
        var builder = new HostBuilder()
            //logging
            .ConfigureLogging(factory =>
            {
                //use nlog
                factory.AddNLog(new NLogProviderOptions { CaptureMessageTemplates = true, CaptureMessageProperties = true });
                NLog.LogManager.LoadConfiguration("nlog.config");
            })
            //host config
            .ConfigureHostConfiguration(config =>
            {
                //command line
                if (args != null)
                {
                    config.AddCommandLine(args);
                }
            })
            //app config
            .ConfigureAppConfiguration((hostContext, config) =>
            {
                var env = hostContext.HostingEnvironment;
                config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                config.AddEnvironmentVariables();

                if (args != null)
                {
                    config.AddCommandLine(args);
                }
            })
            //service
            .ConfigureServices((hostContext, services) =>
            {
                services.AddOptions();
                services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));

                //basic usage
                services.AddHostedService<PrinterHostedService2>();
                services.AddHostedService<TimerHostedService>();
            }) ;

        //console
        await builder.RunConsoleAsync();

        start and wait for shutdown
        //var host = builder.Build();
        //using (host)
        //{
        //    await host.StartAsync();

        //    await host.WaitForShutdownAsync();
        //}
    }
}

  对于控制台的方式,需要我们对HostBuilder有一定的了解,虽说它和WebHostBuild有相似的地方。可能大部分时候,我们是直接使用了WebHost.CreateDefaultBuilder(args)来构造的,如果对CreateDefaultBuilder里面的内容没有了解,那么对上面的代码可能就不会太清晰。
  上述代码的大致流程如下:
  new一个HostBuilder对象
配置日志,主要是接入了NLog
Host的配置,这里主要是引入了CommandLine,因为需要传递参数给程序
应用的配置,指定了配置文件,和引入CommandLine
Service的配置,这个就和我们在Startup里面写的差不多了,最主要的是我们的后台服务要在这里注入
启动
其中,

2-5的顺序可以按个人习惯来写,里面的内容也和我们写Startup大同小异。

第6步,启动的时候,有多种方式,这里列出了两种行为等价的方式。

a. 通过RunConsoleAsync的方式来启动

b. 先StartAsync然后再WaitForShutdownAsync

RunConsoleAsync的奥秘,我觉得还是直接看下面的代码比较容易懂。

/// <summary>
/// Listens for Ctrl+C or SIGTERM and calls <see cref="IApplicationLifetime.StopApplication"/> to start the shutdown process.
/// This will unblock extensions like RunAsync and WaitForShutdownAsync.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
{
    return hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());
}

/// <summary>
/// Enables console support, builds and starts the host, and waits for Ctrl+C or SIGTERM to shut down.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
{
    return hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken);
}

  这里涉及到了一个比较重要的IHostLifetime,Host的生命周期,ConsoleLifeTime是默认的一个,可以理解成当接收到ctrl+c这样的指令时,它就会触发停止。
  接下来,写一下nlog的配置文件

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Info" >

  <targets>
    <target xsi:type="File"
            name="ghost"
            fileName="logs/ghost.log"
            layout="${date}|${level:uppercase=true}|${message}" />
  </targets>

  <rules>
    <logger name="GHost.*" minlevel="Info" writeTo="ghost" />
    <logger name="Microsoft.*" minlevel="Info" writeTo="ghost" />
  </rules>
</nlog>

  这个时候已经可以通过命令启动我们的应用了。
  dotnet run – --environment Staging
这里指定了运行环境为Staging,而不是默认的Production。
  在构造HostBuilder的时候,可以通过UseEnvironment或ConfigureHostConfiguration直接指定运行环境,但是个人更加倾向于在启动命令中去指定,避免一些不可控因素。
  这个时候大致效果如下:
  虽然效果已经出来了,不过大家可能会觉得这个有点小打小闹,下面来个略微复杂一点的后台任务,用来监听并消费RabbitMQ的消息。

4 消费MQ消息的后台任务

public class ComsumeRabbitMQHostedService : BackgroundService
{
    private readonly ILogger _logger;
    private readonly AppSettings _settings;
    private IConnection _connection;
    private IModel _channel;

    public ComsumeRabbitMQHostedService(ILoggerFactory loggerFactory, IOptionsSnapshot<AppSettings> options)
    {
        this._logger = loggerFactory.CreateLogger<ComsumeRabbitMQHostedService>();
        this._settings = options.Value;
        InitRabbitMQ(this._settings);
    }

    private void InitRabbitMQ(AppSettings settings)
    {
        var factory = new ConnectionFactory { HostName = settings.HostName, };
        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();

        _channel.ExchangeDeclare(_settings.ExchangeName, ExchangeType.Topic);
        _channel.QueueDeclare(_settings.QueueName, false, false, false, null);
        _channel.QueueBind(_settings.QueueName, _settings.ExchangeName, _settings.RoutingKey, null);
        _channel.BasicQos(0, 1, false);

        _connection.ConnectionShutdown += RabbitMQ_ConnectionShutdown;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        stoppingToken.ThrowIfCancellationRequested();

        var consumer = new EventingBasicConsumer(_channel);
        consumer.Received += (ch, ea) =>
        {
            var content = System.Text.Encoding.UTF8.GetString(ea.Body);
            HandleMessage(content);
            _channel.BasicAck(ea.DeliveryTag, false);
        };

        consumer.Shutdown += OnConsumerShutdown;
        consumer.Registered += OnConsumerRegistered;
        consumer.Unregistered += OnConsumerUnregistered;
        consumer.ConsumerCancelled += OnConsumerConsumerCancelled;

        _channel.BasicConsume(_settings.QueueName, false, consumer);
        return Task.CompletedTask;
    }

    private void HandleMessage(string content)
    {
        _logger.LogInformation($"consumer received {content}");
    }

    private void OnConsumerConsumerCancelled(object sender, ConsumerEventArgs e)  { ... }
    private void OnConsumerUnregistered(object sender, ConsumerEventArgs e) { ... }
    private void OnConsumerRegistered(object sender, ConsumerEventArgs e) { ... }
    private void OnConsumerShutdown(object sender, ShutdownEventArgs e) { ... }
    private void RabbitMQ_ConnectionShutdown(object sender, ShutdownEventArgs e)  { ... }

    public override void Dispose()
    {
        _channel.Close();
        _connection.Close();
        base.Dispose();
    }
}

  代码细节就不需要多说了,下面就启动MQ发送程序来模拟消息的发送
  同时看我们任务的日志输出
  由启动到停止,效果都是符合我们预期的。
  下面再来看看Web形式的后台任务是怎么处理的。
  Web形式
  这种模式下的后台任务,其实就是十分简单的了。
  我们只要在Startup的ConfigureServices方法里面注册我们的几个后台任务就可以了。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddHostedService<PrinterHostedService2>();
    services.AddHostedService<TimerHostedService>();
    services.AddHostedService<ComsumeRabbitMQHostedService>();
}

  启动Web站点后,我们发了20条MQ消息,再访问了一下Web站点的首页,最后是停止站点。
  下面是日志结果,都是符合我们的预期。
  可能大家会比较好奇,这三个后台任务是怎么混合在Web项目里面启动的。
  答案就在下面的两个链接里。
https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs#L153
https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/HostedServiceExecutor.cs
  上面说了那么多,都是在本地直接运行的,可能大家会比较关注这个要怎样部署,下面我们就不看看怎么部署。
  部署
  部署的话,针对不同的情形(web和非web)都有不同的选择。
  正常来说,如果本身就是web程序,那么平时我们怎么部署的,就和平时那样部署即可。
  花点时间讲讲部署非web的情形。
  其实这里的部署等价于让程序在后台运行。
  在Linux下面让程序在后台运行方式有好多好多,Supervisor、Screen、pm2、systemctl等。
  这里主要介绍一下systemctl,同时用上面的例子来进行部署,由于个人服务器没有MQ环境,所以没有启用消费MQ的后台任务。
  先创建一个 service 文件

vim /etc/systemd/system/ghostdemo.service
内容如下:

[Unit]
Description=Generic Host Demo

[Service]
WorkingDirectory=/var/www/ghost
ExecStart=/usr/bin/dotnet /var/www/ghost/ConsoleGHost.dll --environment Staging
KillSignal=SIGINT
SyslogIdentifier=ghost-example

[Install]
WantedBy=multi-user.target

  其中,各项配置的含义可以自行查找,这里不作说明。
  然后可以通过下面的命令来启动和停止这个服务
  service ghostdemo start
service ghostdemo stop
  测试无误之后,就可以设为自启动了。
  
systemctl enable ghostdemo.service
下面来看看运行的效果

我们先启动服务,然后去查看实时日志,可以看到应用的日志不停的输出。

当我们停了服务,再看实时日志,就会发现我们的两个后台任务已经停止了,也没有日志再进来了。

再去看看服务系统日志

sudo journalctl -fu ghostdemo.service

发现它确实也是停了。

在这里,我们还可以看到服务的当前环境和根路径。

IHostedService和BackgroundService的区别
前面的所有示例中,我们用的都是BackgroundService,而不是IHostedService。

这两者有什么区别呢?

可以这样简单的理解,IHostedService是原料,BackgroundService是一个用原料加工过一部分的半成品。

这两个都是不能直接当成成品来用的,都需要进行加工才能做成一个可用的成品。

同时也意味着,如果使用IHostedService可能会需要做比较多的控制。

基于前面的打印后台任务,在这里使用IHostedService来实现。

如果我们只是纯綷的把实现代码放到StartAsync方法中,那么可能就会有惊喜了。

public class PrinterHostedService : IHostedService, IDisposable
{
    //other ....

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            Console.WriteLine("Printer is working.");
            await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond), cancellationToken);
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Printer is stopped");
        return Task.CompletedTask;
    }
}

  运行之后,想用ctrl+c来停止,发现还是一直在跑。
  ps一看,这个进程还在,kill掉之后才不会继续输出。。
  问题出在那里呢?原因其实还是比较明显的,因为这个任务还没有启动成功,一直处于启动中的状态!
  换句话说,StartAsync方法还没有执行完。这个问题一定要小心再小心。
要怎么处理这个问题呢?解决方法也比较简单,可以通过引用一个变量来记录要运行的任务,将其从StartAsync方法中解放出来。

public class PrinterHostedService3 : IHostedService, IDisposable
{
    //others .....
    private bool _stopping;
    private Task _backgroundTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Printer3 is starting.");
        _backgroundTask = BackgroundTask(cancellationToken);
        return Task.CompletedTask;
    }

    private async Task BackgroundTask(CancellationToken cancellationToken)
    {
        while (!_stopping)
        {
            await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond),cancellationToken);
            Console.WriteLine("Printer3 is doing background work.");
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Printer3 is stopping.");
        _stopping = true;
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        Console.WriteLine("Printer3 is disposing.");
    }
}

  这样就能让这个任务真正的启动成功了!效果就不放图了。
  相对来说,BackgroundService用起来会比较简单,实现核心的ExecuteAsync这个抽象方法就差不多了,出错的概率也会比较低。

5 IHostBuilder的扩展写法

在注册服务的时候,我们还可以通过编写IHostBuilder的扩展方法来完成。

public static class Extensions
{
    public static IHostBuilder UseHostedService<T>(this IHostBuilder hostBuilder)
        where T : class, IHostedService, IDisposable
    {
        return hostBuilder.ConfigureServices(services =>
            services.AddHostedService<T>());
    }

    public static IHostBuilder UseComsumeRabbitMQ(this IHostBuilder hostBuilder)
    {
        return hostBuilder.ConfigureServices(services =>
                 services.AddHostedService<ComsumeRabbitMQHostedService>());
    }
}

使用的时候就可以像下面一样。

var builder = new HostBuilder()
        //others ...
        .ConfigureServices((hostContext, services) =>
        {
            services.AddOptions();
            services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));

            //basic usage
            //services.AddHostedService<PrinterHostedService2>();
            //services.AddHostedService<TimerHostedService>();
            //services.AddHostedService<ComsumeRabbitMQHostedService>();
        })
        //extensions usage
        .UseComsumeRabbitMQ()
        .UseHostedService<TimerHostedService>()
        .UseHostedService<PrinterHostedService2>()
        //.UseHostedService<ComsumeRabbitMQHostedService>()
        ;

6 总结

  Generic Host让我们可以用熟悉的方式来处理后台任务,不得不说这是一个很?的特性。无论是将后台任务独立一个项目,还是将其混搭在Web项目中,都已经符合不少应用的情景了。
  最后放上本文用到的示例代码:GenericHostDemo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值