.net core精彩实例分享 -- 依赖注入和中间件

介绍

随着.net core越来越流行,对.net core 基础知识的了解,实际应用等相关的知识也应该有所了解。所以就有了这篇文章,案例都是来自阅读的书籍,或者实际工作中感觉比较有用的应用。分享亦总结。

本文主要介绍 .net core 相关的依赖注入和中间件案例。

具体案例

临时访问服务

【导语】

有些服务类型可能要在应用程序初始化的过程中临时使用,一般在 Main 方法中,比较经典的用途就是数据库初始化。在启动 WebHost 之前,程序代码可能要检查数据库是否存在,如果不存在就创建新的数据库,然后写入一些初始数据。

当应用程序在初始化过程中需要临时访问服务类型时,可调用 CreateScope 扩展方法(被扩展类型为 IServiceProvider)创建一个基于临时作用域的 IServiceScope 对象,然后在通过临时的 IServiceScope 对象获取服务类型的实例。由于服务类型访问完成后要释放掉 IServiceScope 对象,因此创建将 CreateScope 扩展方法的调用写在 using 代码块中,以便自动清理。

本实例将演示在应用程序初始化过程中,临时获取 IHostingEnvironment 访问实例,然后修改 ApplicationName 属性。由于 IHostingEnvironment 访问在容器注册的是单例实例,所以修改 ApplicationName 属性后,在应用程序的其他地方进行依赖注入也能获取 ApplicationName 属性的最新值。

【操作流程】

步骤1:新建一个空白的 ASP.NET Core Web 应用程序项目。

步骤2:在 Main 方法中,使用 WebHost.CreateDefaultBuilder 方法快速创建 IWebHostBuilder,然后创建 WebHost 实例。

var builder = WebHost.CreateDefaultBuilder(args);
var host = builder
    .UseStartup<Startup>()
    .Build();

步骤3:临时提取 IHostingEnvironment 实例,修改 ApplicationName 属性。

using(IServiceScope scope = host.Services.CreateScope())
{
    IHostingEnvironment env = scope.ServiceProvider.GetRequiredService<IHostingEnvironment>();
    env.ApplicationName = "【示例应用程序】";
}

步骤4:启动 WebHost 实例。

host.Run();

步骤5:在 Startup.Configure 方法中,可以通过依赖注入从参数中获取 IHostingEnvironment 实例,然后由 Response 对象向客户端回发响应消息,响应消息中包含 ApplicationName 属性的值。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Run(async (context) =>
    {
        context.Response.ContentType = "text/html;charset=UTF-8";
        await context.Response.WriteAsync($"正在运行{env.ApplicationName}");
    });
}

步骤6:运行应用程序,在浏览器中得到的返回结果如下。

在这里插入图片描述

以委托形式定义中间件

【导语】

应用程序对 HTTP 请求的处理过程进行划分,每个环境成为中间件,将各个中间件串联起来,就形成了 HTTP 管道,大致的流出可参考下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zWPAGjAP-1620917686209)(./middle.png)]

执行中间件的顺序与它们添加到 HTTP 管道的顺序相同,即先添加的中间件会先执行,在 HTTP 管道中添加中间件有三种方法:

(1)委托。中间件专用的委托类型是 RequestDelegate,对应的方法结构就是带 HttpContext 类型的输入参数,并返回 Task 对象。一般来说,委托方式适用于代码较少、处理逻辑比较简单的中间件。

(2)基于约定的中间件。主要的约定在于类的方法,基于约定的中间件类型必须包含 InvokeInvokeAsync 方法,输入参数为一个 HttpContext 对象,并返回 Task 对象。中间件类可以通过构造函数的依赖注入来获取下一个中间件的 InvokeInvokeAsync 方法引用。

(3)实现 IMiddleware 接口。该接口同样包含 InvokeAsync 方法,需要HttpContext对象作为输入参数,并返回 Task 对象。用这种方式定义得到中间件需要在代码中显示将其添加到服务容器中,因此此种中间件的生命周期可以被改变(前面两种方式所定义的中间件都是单例模式,在应用程序生命周期内仅创建一次实例,而实现了 IMiddleware 接口的中间件在添加到服务容器时可以手动设置它的生命周期)。

在每个中间件的实现代码中都会通过输入参数获得下一个中间件的引用,这样开发人员可以灵活控制:是先执行当前中间件的代码,还是先执行下一个中间件的代码,或者不执行下一个中间件而直接向客户端返回响应消息。

本实例将演示通过委托的方式来定义中间件。实例创建了三个中间件,并在每个中间件的代码中产生一个字符串(临时存储在 HttpContext 对象的 Items 属性中),在最后一个中间件中,将三个字符串拼接并发回给客户端。

【操作流程】

步骤1:新建一个空白的 ASP.NET Core Web 应用程序项目。

步骤2:在 Startup.Configure 方法中,调用Use扩展方法定义三个中间件。

app.Use(async (context, next) =>
{
    context.Items["line1"] = "第一环节,完成";
    await next();
})
.Use(async (context, next) =>
{
    context.Items["line2"] = "第二环节,完成";
    await next();
})
.Use(async (context, next) =>
{
    context.Items["line3"] = "第三环节,完成";
    // 拼接回应消息
    var parts = (from o in context.Items select o.Value.ToString()).AsEnumerable();
    string responseMessage = string.Join("<br/>", parts);
    // 设置编码
    context.Response.ContentType = "text/html;charset=UTF-8";
    await context.Response.WriteAsync(responseMessage);
    await Task.CompletedTask;
});

第三个中间件的代码中,可以不调用 next 委托引用,因为该中间件已经是 HTTP 管道的最后一个环节了。

注意:Response.Write 方法一般是在最后一个中间件里面调用,即在所有的 HTTP 通信环节都处理完毕后才调用该方法。一旦调用 Write 方法,就开始向客户端回写消息了,这个会导致 HTTP 消息中某些内容无法被修改(例如 HTTP 头),进而引发异常,所以最佳做法是待所有环节都处理完成了再将消息回写到客户端。当然,任何中间件的代码中都可以同访问 HasStarted 属性来确定 HTTP 响应是否已经开始发回给客户端,如果值为 true,就不应该再修改 HTTP 头了。

步骤3:运行应用程序,浏览器输出的结果如下。

在这里插入图片描述

带参数中间件

【导语】

一般来说,中间件类的命名可以带上“Middleware”后缀,这虽然不是语法要求的,但是有规律的命名方式可以方便其他人识别该类是中间件。

中间件类的约定中必须包含 InvokeInvokeAsync 方法,此方法的声明如下:

public Task InvokeAsync(HttpContext context);
public Task Invoke(HttpContext context);

除了 HttpContext 类型的参数外,还可以在该方法的后面定义其他参数,而且参数支持依赖注入。

中间件允许使用参数,但并不是调用参数,而且仅能在中间件注册时使用,即在中间件生命周期内,参数只能传递一次。中间件的参数是通过构造函数传递的,即在定义中间件类的构造函数时,第一个参数是 HTTP 管道中下一个中间件的引用(RequestDelegate 委托),从第二个参数开始可以定义中间件的参数。

当在 Startup.Configure 方法中通过 UseMiddleware 方法注册中间件时,可以传递参数。如果中间件是基于约定所定义的类,那么传递的参数值尽量不要使用服务容器中非单实例模式的服务类型,因为这种中间件在应用程序运行期间只实例化一次,中间件实例始终会保留对参数的引用,这会使暂时服务或作用域服务的实例强制变成单实例服务。

【操作流程】

步骤1:新建一个空白的 ASP.NET Core Web 应用程序项目。

步骤2:定义一个中间件类。

public class CalcuMiddleware
{
    readonly RequestDelegate _next;
    readonly int _a, _b;
    public CalcuMiddleware(RequestDelegate next, int a, int b)
    {
        _next = next;
        _a = a;
        _b = b;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        int result = _a * _b;
        context.Response.ContentType = "text/html;charset=UTF-8";
        await context.Response.WriteAsync($"{_a}×{_b}={result}");
        await _next(context);
    }
}

该中间件通过构造函数来接收两个 int 类型的参数,并在 InvokeAsync 方法中计算两个参数的乘积,并将结果回写到客户端。

步骤3:在 Startup.Configure 方法中注册中间件,并传递参数值。

app.UseMiddleware<CalcuMiddleware>(15, 7);

步骤4:运行应用程序,浏览器输出的结果如下。

在这里插入图片描述

IMiddleware中间件的用途

【导语】

中间件类一般的实现方法是使用约定形式(包含公共的 InvokeInvokeAsync 方法),有时也考虑实现 IMiddleware 接口,该接口的原型如下:

public interface IMiddleware
{
    Task InvokeAsync(HttpContext context, RequestDelegate next);
}

IMiddleware 接口也包含 InvokeAsync 方法,但它有两个参数,context 参数表示请求的上下文数据,next 表示下一个中间件的引用。

基于约定的中间件类在应用程序运行期间只创建单个实例,而实现了 IMiddleware 接口的中间件类的生命周期就可以灵活控制。IMiddleware 接口方式实现的中间件,在 Startup.Configure 方法中调用 UseMiddleware 方法之前,必须在 Startup.ConfigureServices 方法中进行注册,服务的注册可以选择三种生命周期:暂时服务、作用域服务和单一实例服务,可以通过不同的服务注册方式来控制中间件类的生命周期,例如,对于不太常用的中间件,可以注册为暂时服务,减少内存占用。

【操作流程】

步骤1:新建一个空白的 ASP.NET Core Web 应用程序项目。

步骤2:定义中间件类 TestMiddleware,并让它实现 IMiddleware 接口。

public class TestMiddleware : IMiddleware
{
    public TestMiddleware()
    {
        Console.WriteLine($"类 {GetType().Name} 的构造函数被调用");
    }
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // 添加两个响应头
        context.Response.Headers["item 1"] = "hello";
        context.Response.Headers["item 2"] = "hi";
        // 写入响应消息
        context.Response.ContentType = "text/html;charset=UTF-8";
        await context.Response.WriteAsync("欢迎来到主页");
    }
}

TestMiddleware 类的构造函数中会输出一行文本,因此如果中间件在应用程序运行期间被多次创建实例,那么每次实例化的时候都会在控制台中输出一行文本,通过查看控制台的输出内容就可以验证中间件是否被多次实例化。

步骤3:在 Startup.ConfigureServices 方法中,对 TestMiddleware 中间件进行注册。

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

注意:通过实现 IMiddleware 接口来定义中间,必须先在服务容器中注册才能在 HTTP 管道中使用。

步骤4:在 Startup.Configure 方法中,使用自定义中间件。

app.UseMiddleware<TestMiddleware>();

步骤5:运行应用程序,在浏览器中打开 URL 后,进行多次刷新。可以发现,每次刷新后控制台中都会输出一行文本,表明 TestMiddleware 中间件创建了新实例。

在这里插入图片描述

让 HTTP 管道“短路”

【导语】

直接调用 IApplicationBuilderRun 扩展方法会使整个 HTTP 请求管道发生“短路”————直接把响应消息发回给客户端,终止此次 HTTP 通信。例如以下代码调用了三次 Run 方法:

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

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

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

应用程序在运行的时候,只有第一个 Run 方法的调用会被执行,后面两次调用都不会被执行,只是因为遇到了 Run 方法,意味着整个 HTTP 请求管道将被终止,并且将处理结果直接发回给客户端,不管 Run 后面还有没有新的插入的中间件,都不会被执行。读者可以参考以下 Run 扩展方法的源码。

public static void Run(this IApplicationBuilder app, RequestDelegate handler)
{
    if(app == null)
    {
        throw new ArgumentNullException(nameof(app));
    }

    if(handler == null)
    {
        throw new ArgumentNullException(nameof(handler));
    }

    app.Use(_ => handler);
}

可以看到,使 HTTP 管道“短路”的实现原理非常简单,就是在管道中插入一个中间件(即传递给Run方法的委托实例),一旦整个中间件执行之后,就不会去调用下一个中间件,从而使 HTTP 管道终结,即Run方法中添加的中间件成为 HTTP 请求处理流出中的最后环节。

【操作流程】

步骤1:新建一个空白的 ASP.NET Core Web 应用程序项目。

步骤2:项目模板在 Startup.Configure 方法中已经生产了一条调用 Run 方法的代码语句。为了演示,可以对其做以下修改。

app.Run(async context =>
{
    context.Response.ContentType = "text/html;charset=UTF-8";
    await context.Response.WriteAsync("你好,世界");
});

步骤3:以下代码也能实现与 Run 方法类似的效果。

app.Use(async (context, next) =>
{
    context.Response.ContentType = "text/html;charset=UTF-8";
    await context.Response.WriteAsync("你好,世界");
});

中间件的分支映射

【导语】

添加到 HTTP 管道中的中间件是默认响应根 URL 请求的,但在实际开发中,有时候需要在根 URL 下面通过子路径来区分不同的功能,即根据不同的子 URL 来调用不同的中间件。

分支映射有两种比较常见的使用场景:一种用法是错误处理,例如根URL是 http://abc.org,可以将 http://abc.org/errors 专用于错误处理,调用向客户端返回错误信息的中间件;另一种用法是 Web Socket,例如 http://abc.org/ws 分支可专用于 Web Socket 通信。

本实例将在根 URL 的基础上划分三个分支,当访问 /home 路径时返回文本“主页”,当访问 /about 路径时返回文件“关于本站”,访问 /news 路径时返回文本“新闻列表”。

【操作流程】

步骤1:新建一个空白的 ASP.NET Core Web 应用程序项目。

步骤2:首先在 Startup.Configure 方法中定义一个 HTTP 管道主路上的中间件,作用是将回写文本的字符编码设置为 UTF-8

app.Use(async (context, next) =>
{
    context.Response.ContentType = "text/html;charset=UTF-8";
    await next();
});

注意:在设置完字符编码后,必须调用 next 委托,这样接下来的各个分支中的中间件才能被调用。因为如果不调用 next 委托,就会直接终结 HTTP 管道(与在主路上调用 Run 方法结果相同)。

步骤3:接下来是三个分支,分别调用 Run 方法向客户端返回文本内容, HTTP 管道中的处理过程结束。

app.Map("/home", _app =>
{
    _app.Run(async context =>
    {
        await context.Response.WriteAsync("主页");
    });
})
.Map("/about", _app =>
{
    _app.Run(async context =>
    {
        await context.Response.WriteAsync("关于本站");
    });
})
.Map("/news", _app =>
{
    _app.Run(async context =>
    {
        await context.Response.WriteAsync("新闻列表");
    });
});

步骤4:运行应用程序,可以分别输入以下 URL 来进行测试。

http://localhost:3125/home
http://localhost:3125/about
http://localhost:3125/news

文件服务

【导语】

当应用项目既需要浏览目录结构,又需要访问静态文件时,可以考虑使用文件服务功能。

UseFileServer 方法综合了 UseStaticFiles 方法和 UseDirectoryBrowser 方法的功能,参数可以通过 FileServerOptions 类来设置。

【操作流程】

步骤1:新建一个空白的 ASP.NET Core Web 应用程序项目。

步骤2:在项目模板生成的 wwwroot 目录下新建六个文本文件,并向每个文件中随意输入一些内容,这些文件仅用于稍后测试。

步骤3:在 Startup.ConfigureServices 方法中调用 AddDirectoryBrowser 方法。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDirectoryBrowser();
}

步骤4:在 Startup.Configure 方法中调用 UseFileServer 方法,并配置好相关选项。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseFileServer(new FileServerOptions
    {
        FileProvider = new PhysicalFileProvider(env.WebRootPath),
        RequestPath = "/files",
        EnableDirectoryBrowsing = true
    });
    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

注意:将 EnableDirectoryBrowsing 属性设置为 true 才会提供浏览目录的服务,如果不设置该属性,就相当于提供静态文件服务,不能浏览目录结构。

步骤5:运行应用程序,结果如下。

在这里插入图片描述

总结

本文到这里就结束了,下一篇将介绍应用配置和数据库访问的知识案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值