.net 温故知新【17】:Asp.Net Core WebAPI 中间件 - XSpringSun - 博客园

mikel阅读(284)

来源: .net 温故知新【17】:Asp.Net Core WebAPI 中间件 – XSpringSun – 博客园

一、前言

到这篇文章为止,关于.NET “温故知新”系列的基础知识就完结了,从这一系列的系统回顾和再学习,对于.NET core、ASP.NET CORE又有了一个新的认识。

不光是从使用,还包括这些知识点的原理,虽然深入原理谈不上,但对于日常使用也够了,我想的是知其然,知其所以然。

在实际开发过程中可能是知道怎么使用就行,但系统学习了这些基本的框架、组件、或者说原理后,对于我们软件设计、开发、扩展和解决问题还是有帮助的。

刚好到2023新年前赶着写完,也算对自己这个系列的一个交代,实际上我平时基本不使用ASP.NET CORE,目前我主要开发桌面程序,还是用的winform。

写这个系列的初衷是想紧跟.NET的发展进程,同时储备基础知识,平时还搞一些微服务(Java)、NLP、OCR、知识图谱、前端(Vue3),只要需要反正啥都搞,没必要固执,技术只是手段,不是目的。

那么接下来就继续简单的梳理一下中间件,欢迎对这个系列拍砖!

二、中间件

中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件:

  • 选择是否将请求传递到管道中的下一个组件。
  • 可在管道中的下一个组件前后执行工作。

这个是关于中间件概念的概括,官方的概括是相当精准,那么我们就围绕管道、传递、组件来看看中间件。

请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。使用 Run、Map 和 Use 扩展方法来配置请求委托。

我们照例新建一个ASP.NET CORE Web API 项目:WebAPI_Middleware

namespace WebAPI_Middleware
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();


            app.MapControllers();

            app.Run();
        }
    }
}

在Program.cs 中我们看到前面部分builder是配置依赖注入的东西,这部分可以参看.net 温故知新【13】:Asp.Net Core WebAPI 使用依赖注入DI 。

app 使用Use扩展用于中间件添加到管道中

Map 基于给定请求路径的匹配项来创建请求管道分支

Run 委托始终为终端,用于终止管道。

中间件的执行顺序过程如下:

image

三、Map

我们将上面自动创建的东西全都删除,用Map来匹配路由,然后通过不同的代理处理请求。

    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            var app = builder.Build();
           
            //匹配map1 请求
            app.Map("/map1", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map1 run");
                });
            }));
            //匹配map2 请求
            app.Map("/map2", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map2 run");
                });
            }));

            app.Run();
        }
    }
  • 请求map1 我们输出:map1 run

image

  • 请求map2 我们输出:map2 run

image

Asp.Net Core MapControllers 的扩展方法也是类似道理,用来匹配路由调用处理程序。

四、Run

在上面的 Map 后面我们使用的处理方法中 Run 用于终止管道。也就是说在该管道中如果调用了 Run 那么就直接返回了,即使你后面还添加了 Use 也不会执行。

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

Map 相当于是迎客进门,Map 上了就用指定的管道进行处理,如果没有 Map 上就调用主管道,也就是主管道上的其他中间件也会执行处理。比如我们再加一个 Run 用于没匹配上路由也输出点信息。

image

加了context.Response.ContentType = "text/plain; charset=utf-8"; 不然中文会乱码。

image

因为 Run 是终结点,那这个管道中我还想加其他处理怎么办呢,这个时候就该轮到 Use 出场了。

五、Use

用 Use 将多个请求委托链接在一起。 next 参数表示管道中的下一个委托。 可通过不调用 next 参数使管道短路。

首先我们在外面添加两个 Use,不放到 Map 中,这样的话就只有未匹配到的路由会调用

    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            var app = builder.Build();

            

            app.Map("/map1", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map1 run");
                });
            }));

            app.Map("/map2", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map2 run");
                });
            }));
            //Use1
            app.Use(async (context, next) =>
            {
                context.Response.ContentType = "text/plain; charset=utf-8";

                await context.Response.WriteAsync("第 1 个Use   开始!\r\n", Encoding.UTF8);

                await next();

                await context.Response.WriteAsync("第 1 个Use   结束!\r\n", Encoding.UTF8);

            });
            
            //Use2
            app.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("第 2 个Use   开始!\r\n", Encoding.UTF8);

                await next();

                await context.Response.WriteAsync("第 2 个Use   结束!\r\n", Encoding.UTF8);

            });
            //结束管道处理
            app.Run(async context =>
            {
                await context.Response.WriteAsync("未匹配处理!\r\n", Encoding.UTF8);
            });

            app.Run();
        }
    }

最后执行的路径和最开始的图是一致的。

image

为什么将context.Response.ContentType = "text/plain; charset=utf-8"; 放到第一个 Use 呢,因为如果放到 Run 里面会报错,改变了 Header 标头。所以理论上也不要在 Use 里面发送响应WriteAsync,此处为了演示所以这么写。

image

六、中间件类

上面的代理方法可以移动到类中,这个类就是中间件类。中间件类需要如下要求:

  • 具有类型为 RequestDelegate 的参数的公共构造函数。
  • 名为 Invoke 或 InvokeAsync 的公共方法。 此方法必须:
    返回 Task。
    接受类型 HttpContext 的第一个参数。

构造函数和 Invoke/InvokeAsync 的其他参数由依赖关系注入 (DI) 填充。

将上面的未匹配路由处理逻辑移动到中间件类中:

  • TestMiddleware1:
    public class TestMiddleware1
    {
        private readonly RequestDelegate _next;

        public TestMiddleware1(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {

            context.Response.ContentType = "text/plain; charset=utf-8";

            await context.Response.WriteAsync("第 1 个Use   开始!\r\n", Encoding.UTF8);

            await _next(context);

            await context.Response.WriteAsync("第 1 个Use   结束!\r\n", Encoding.UTF8);
        }
    }
  • TestMiddleware2
    public class TestMiddleware2
    {
        private readonly RequestDelegate _next;

        public TestMiddleware2(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {


            await context.Response.WriteAsync("第 2 个Use   开始!\r\n", Encoding.UTF8);

            await _next(context);

            await context.Response.WriteAsync("第 2 个Use   结束!\r\n", Encoding.UTF8);
        }
    }
  • Program
    image
  • 运行
    image

此处的中间件使用有顺序问题,如果我先app.UseMiddleware<TestMiddleware2>() 因为 TestMiddleware1 修改了标头,根据约定是不允许的,所以程序是有报错。

image

因此中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。

七、中间件顺序

image

以上是内置中间件的默认顺序规则,具体如何使用内置中间件,可参阅官方资料。

八、写在最后

以上就是关于中间件的部分知识,结合我自己的理解做了前后衔接的梳理逻辑。

官方网站更多的是讲解每个知识点的细节,前后需要结合起来理解,当然我还是强烈建议跟着官方文档学习,而且是最权威最可信的:ASP.NET Core 中间件

这个系列历时2年,工作生活都比较忙,也有放纵啥事不相干的时候,中间断断续续的,总算是坚持完了。很多东西就是这样,累了就休息一下贵在坚持,即使再慢,积累的成果也有收获。

.net 温故知新【16】:Asp.Net Core WebAPI 筛选器 - XSpringSun - 博客园

mikel阅读(256)

来源: .net 温故知新【16】:Asp.Net Core WebAPI 筛选器 – XSpringSun – 博客园

一、筛选器

通过使用筛选器可在请求处理管道中的特定阶段之前或之后运行代码。

这即是我们经常听到的面向切面编程AOP(Aspect Oriented Programming)技术,AOP通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。

筛选器在 ASP.NET Core 操作调用管道(有时称为筛选器管道)内运行。 筛选器管道在 ASP.NET Core 选择了要执行的操作之后运行:
image

Asp.Net Core 关注的切面点 包括错误处理、缓存、配置、授权和日志记录筛选器,这个是说通过筛选器可以实现对以上关注点的一些操作。

在Asp.Net Core中有如下几种类型的筛选器:

image

其中部分是内置筛选器,比如授权,响应缓存已经帮我们内置进了框架,我们只需要配置即可使用;其他筛选器是可以自定义处理逻辑的。

下图展示了筛选器类型在筛选器管道中的交互方式和执行顺序:

image

二、操作型筛选器

第一部分主要是对筛选器的一个梳理,有些重点的提炼,详情查看文档,因为文档部分理解起来比较晦涩,比如关注点是关注点,知识说筛选器可以对这些关注点启到作用,筛选器是固定的几种,不要被文档中的这种描述搞晕了,一会儿有这几种,怎么到下面又是另外几种,要注意区分重点。

操作筛选器可以实现接口IActionFilter,在接口中有两个方法,OnActionExecuting 在调用操作方法之前执行。 OnActionExecuted 在操作方法返回之后执行。

  • 先建WebAPI项目 WebAPI_Filter
  • 建一个 FilterController,并创建Get请求Test
namespace WebAPI_Filter.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class FilterController : ControllerBase
    {
        [HttpGet]
        public string Test()
        {
            return "测试Filter!";
        }
    }
}
  • 创建ActionFilter 筛选器
namespace WebAPI_Filter.Filter
{
    public class MyActionFilter : IActionFilter
    {
        public void OnActionExecuted(ActionExecutedContext context)
        {
            Console.WriteLine(context.HttpContext.Request.GetDisplayUrl()+ "  执行之后!");
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            Console.WriteLine(context.HttpContext.Request.GetDisplayUrl() + "  执行之前!");
        }
    }
}
  • 在Program.cs里面添加筛选器
    image

执行测试接口
image

三、筛选器作用域和执行顺序

上面直接在Program.cs里面添加筛选器的方式称为全局筛选器,所有控制器、操作都会受全局筛选器影响。还有一种筛选器实现方式是属性筛选器,通过继承属性类然后将属性标签放置在控制器或者操作上。

新建两个属性类MyAttributeFilter 用于Controller控制器类,MyOPAttributeFilter用于操作方法上。

    public class MyAttributeFilter: ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext context)
        {
            Console.WriteLine(context.HttpContext.Request.GetDisplayUrl() + "  控制器之后-筛选器属性!");
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            Console.WriteLine(context.HttpContext.Request.GetDisplayUrl() + "  控制器之前-筛选器属性!");
        }
    }


    public class MyOPAttributeFilter : ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext context)
        {
            Console.WriteLine(context.HttpContext.Request.GetDisplayUrl() + "  操作之后-筛选器属性!");
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            Console.WriteLine(context.HttpContext.Request.GetDisplayUrl() + "  操作之前-筛选器属性!");
        }
    }

image

加上之前的全局筛选器,我们一共有三个作用域的筛选器,现在我们测试看看筛选器的执行顺序。
image

则可总结出不同作用域筛选器的执行顺序:

全局筛选器的 before 代码。
	控制器筛选器的 before 代码。
		操作方法筛选器的 before 代码。
		操作方法筛选器的 after 代码。
	控制器筛选器的 after 代码。
全局筛选器的 after 代码。

当然可以通过 Order 属性来确定执行顺序,在全局或者属性筛选器里面设置 Order 值,值越小执行优先级越高。

image

四、筛选器依赖注入

可按类型或实例添加筛选器。 如果添加实例,该实例将用于每个请求。

其中builder.Services.AddControllers(options => options.Filters.Add<MyActionFilter>())即为按实例添加,该MyActionFilter用于每个请求。

如果添加类型,则将激活该类型。 激活类型的筛选器意味着:第一种是为每个请求创建一个实例,第二种依赖关系注入 (DI) 将填充所有构造函数依赖项。

上面位置我们是为每个请求创建一个实例,这样的话无法使用依赖注入体系为我们自动注入,因为因为属性在应用时必须提供自己的构造函数参数,该参数需要手动指定。

比如我们想在操作方法的MyOPAttributeFilter筛选属性 注入IHostEnvironment:

    public class MyOPAttributeFilter : ActionFilterAttribute
    {
        IHostEnvironment hostEnvironment;

        public MyOPAttributeFilter(IHostEnvironment _hostEnvironment)
        {
            hostEnvironment = _hostEnvironment;
        }
        public override void OnActionExecuted(ActionExecutedContext context)
        {
            Console.WriteLine(context.HttpContext.Request.GetDisplayUrl() + "  操作之后-筛选器属性!");
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            Console.WriteLine(context.HttpContext.Request.GetDisplayUrl() + "  操作之前-筛选器属性!");
            //打印环境变量
            Console.WriteLine(hostEnvironment.EnvironmentName);
        }
    }

这个时候直接就报错提示需要参数,而我们想的是通过依赖注入配置。

image

框架提供以下筛选器支持从 DI 提供的构造函数依赖项:

  • ServiceFilterAttribute
  • TypeFilterAttribute
  • 在属性上实现 IFilterFactory。

TypeFilterAttribute:不会直接从 DI 容器解析其类型。Microsoft.Extensions.DependencyInjection.ObjectFactory 对类型进行实例化,所以不需要先将MyOPAttributeFilter加入容器,直接使用:

[TypeFilter(typeof(MyOPAttributeFilter))]

image

ServiceFilterAttribute 使用需要先将MyOPAttributeFilter注入到容器,然后再使用。

image

以上就是关于AOP切面编程和筛选器的梳理,其他类型的筛选器和细节可查询官方文档:ASP.NET Core 中的筛选器

.net 温故知新【15】:Asp.Net Core WebAPI 配置 - XSpringSun - 博客园

mikel阅读(251)

来源: .net 温故知新【15】:Asp.Net Core WebAPI 配置 – XSpringSun – 博客园

关于ASP.NET Core中的配置实际之前我已经整理过.net 中以json方式进行配置的介绍(.net 温故知新:【8】.NET 中的配置从xml转向json),当时我们说ASP.NET Core也是按照基础方法,只是组织形式的问题,有个封装过程。所以我这里就着重介绍一下ASP.NET Core中配置的重点。

1、主机配置和应用程序配置

ASP.NET Core 应用配置和启动“主机”。 主机负责应用程序启动和生存期管理。 ASP.NET Core 模板创建的 WebApplicationBuilder 包含主机。 虽然可以在主机和应用程序配置提供程序中完成一些配置,但通常,只有主机必需的配置才应在主机配置中完成。

主机配置和应用程序配置是什么意思呢,可以粗略的理解为我们ASP.NET Core 项目启动的时候要用到的一些配置为主机配置,而程序运行阶段使用的配置为应用程序配置。

而ASP.NET Core 包含一些默认的主机和应用程序配置,我们先看看这些默认配置在哪里以及配置的规范。

2、默认主机配置

image

从文档描述可以看到,默认主机配置是的优先级为:

命令行-> DOTNET_ 为前缀的环境变量->ASPNETCORE_ 为前缀的环境变量

并且部分变量是锁定在启动阶段,不受其他配置的影响:

image

命令行可以通过启动程序设置,比如:dotnet run --environment Production

而其他的则可以直接设置系统环境变量,并且按照前缀优先。由于去系统设置比较麻烦,且设置后可能还需要重启VS,所以提供一些能便捷设置的能力。

launchSettings.json 仅在本地开发计算机上使用,包含配置文件设置。可以设置启动方式和相应的环境变量。

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:21115",
      "sslPort": 44325
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5007",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:7211;http://localhost:5007",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

如上是创建项目默认的launchSettings.json文件,在文件中对https、http、IIS Express等配置的ASPNETCORE_ENVIRONMENT都为Development

ENVIRONMENT 可以配置任意值,框架提供了Development、Staging、Production三种,当没有设置 DOTNET_ENVIRONMENT 和 ASPNETCORE_ENVIRONMENT 时的默认值为Production。

这就是为什么我们不在开发工具中调试,而是直接运行项目的时候会是Production。

同时我们如果要自己设置其他值,那么项目中对于使用的地方也要注意,比如appsettings.{Environment}.json文件名,这个我们稍后讲。

这个值在Program.cs中是最好的体现,当是Development时启用swagger:

image

3、默认应用程序配置

image

默认应用程序配置是的优先级为:

命令行-> 非前缀的环境变量->Development环境中用户机密配置->appsettings.{Environment}.json-> appsettings.json->主机配置(非锁定)

命令行配置和非前缀环境变量不用说了,接着就是用户机密文件,稍后我们单独说下机密文件。

然后再加载appsettings.{Environment}.json文件,这个文件是根据上面主机配置的环境变量ENVIRONMENT加载的,比如你配置ENVIRONMENT=AA,那么json文件应为appsettings.AA.json。

image

当读取了环境变量的json后就读取appsettings.json文件,该文件一般是正式部署中使用。

最后就是主机配置相关的变量,这部分优先级比较高。

(注意:这些配置的读取由配置提供程序实现,也就决定了他们不同的功能或者实现,提供程序这个概念和设计是.net 中大面积使用的,可以从这个部分去理解)

4、配置机密文件

机密管理器工具可用于存储开发环境中的机密,比如开发过程中用到的一些数据库配置,这个机密文件不在项目目录下,所以不会被上传到代码管理工具,例如git或者svn。

右键项目->管理用户机密
image

我们看到这个文件是在C盘下,而且文件夹名是一串编码,该编码在项目文件中已经自动配置好了。
image

image

5、使用配置

使用依赖注入的方式将IConfiguration注入,之后我们写一个测试获取上面我们配置的用户机密文件里面SecretKey的值。

namespace WebAPI_Config.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        private IConfiguration _configuration;
        public TestController(IConfiguration configuration)
        {
            _configuration = configuration;
        }


        [HttpGet]
        public string Get()
        {
            return _configuration.GetValue(typeof(string), "SecretKey").ToString();
        }
    }
}

如果对依赖注入不了解的可以看看我的另外一篇:.net 温故知新:【7】IOC控制反转,DI依赖注入

测试接口可以看到已经获取到了对应的值。
image

.net 温故知新【14】:Asp.Net Core WebAPI 缓存 - XSpringSun - 博客园

mikel阅读(243)

来源: .net 温故知新【14】:Asp.Net Core WebAPI 缓存 – XSpringSun – 博客园

一、缓存

缓存指在中间层中存储数据的行为,该行为可使后续数据检索更快。 从概念上讲,缓存是一种性能优化策略和设计考虑因素。 缓存可以显著提高应用性能,方法是提高不常更改(或检索成本高)的数据的就绪性。

二、RFC9111

在最新的缓存控制规范文件RFC9111中,详细描述了浏览器缓存和服务器缓存控制的规范,其中有一个最重要的响应报文头Cache-Control

该报文头的设置会影响我们的缓存,包括浏览器端和服务端。

RFC911:https://www.rfc-editor.org/rfc/rfc9111#name-cache-control

image

三、网页端缓存

Cache-Control中,如果设置max-age=10,则表示告诉浏览器缓存10s,而为什么浏览器要认这个表示呢,就是上面我们说的前后端都要根据RFC标准规范去实现,就是硬件的统一插口,不然其他生成出来的就用不了。

那么在ASP.NET Core 中只需要在接口上打上ResponseCacheAttribute并设置max-age的时间即可。

首先建一个ASP.NET Core WebAPI 项目,写一个获取学生的Get接口。

image

namespace WebAPI_Cache.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CacheController : ControllerBase
    {

        public CacheController()
        {

        }

        [HttpGet]
        public ActionResult<Student> GetStudent()
        {

            return new Student()
            {
                Id = 1,
                Name = "Test",
                Age = Random.Shared.Next(0, 100),
            };
        }
    }
}
namespace WebAPI_Cache.Model
{
    public class Student
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public int Age { get; set; }
    }
}

在接口中我返回Studentage为1-100的随机数。启动项目测试,短时间内两次调用返回的age不一样

第一次age:

image

第二次age:

image

当我在接口方法打上[ResponseCache(Duration = 10)],再次调用接口返回的信息可以看到已经有了cache-control: public,max-age=10的Header。

image

并且我在10秒内的请求,只有第一次请求过服务器,其他都是从缓存中取的,查看edge浏览器网络访问如下:

image

四、服务器缓存

网页端缓存是放在浏览器端的,对于单点请求会有用,但是如果是多个不同前端请求呢。这个时候我们可以将缓存放置在后端服务中,在ASP.NET Core 中配置响应缓存中间件。

在 Program.cs中,将响应缓存中间件服务 AddResponseCaching 添加到服务集合,并配置应用,如果使用 CORS 中间件时,必须在 UseResponseCaching 之前调用 UseCors。

如果header包含 Authorization,Set-Cookie 标头,也不会缓存,因为这些用户信息缓存会引起数据混乱。

image

然后对于我们需要服务器缓存的接口打上ResponseCache属性,和设置浏览器缓存一样,还有其他参数可设置。我们通过两个进程来测试,一个用浏览器swagger,一个用postman,可以看到两个请求的age都是等于18的。所以可以确定服务器端确实存在缓存。

image

但是在用postman测试的时候记得在settings里面把Send no-cache header勾掉,如果不去掉,发送的时候就会在请求头里面包含Cache-Control:no-cache,这样服务端即便有缓存也不会使用缓存。

image

对于浏览器端相当于禁用缓存,如果禁用了缓存,发送的请求头也会带上Cache-Control:no-cache,服务端看到no-cache 后便不会再使用缓存进行响应。

而这个约定就是RFC9111的规范,所以这个后端缓存策略比较鸡肋,如果用户禁用缓存就没用了,因此我们还可以使用内存缓存。

五、内存缓存

内存缓存基于 IMemoryCache。 IMemoryCache 表示存储在 Web 服务器内存中的缓存。

  • 首先Nuget安装包
Install-Package Microsoft.Extensions.Caching.Memory
  • 在Program.cs中添加依赖
builder.Services.AddMemoryCache();
  • 缓存数据
    我添加一个Post方法模拟id查询Student

image

这样我就将数据缓存到了内存,可以设置缓存的绝对过期时间,也可以设置滑动过期,稍后我们会看到过期策略的使用。

六、缓存击穿

缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,或者是查询了不存在的数据,缓存里面没有,从而大量的请求打到数据库上形成数据库压力。

上面内存缓存中的写法我们可以看到,如果查询缓存等于null就会再去查询数据(我这里只是模拟,没有去写真的数据库查询),如果这样暴力请求攻击就会有问题。

对于这个问题我们可以使用ImemoryCacheGetOrCreate方法,当然它还有异步方式。通过该方法传入缓存的key和func 委托方法返回值来进行查询并缓存,如果没查询到返回的null也会存储在缓存中,防止恶意查询不存在的数据。

        [HttpPost]
        public ActionResult<Student> GetStudent2(int id)
        {
            //查询并创建缓存
            var student = _memoryCache.GetOrCreate("student_" + id, t =>
            {
                t.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(20);
                //模拟只有id=1有数据
                if (id == 1)
                {
                    return new Student()
                    {
                        Id = 1,
                        Name = "Test",
                        Age = Random.Shared.Next(0, 100),
                    };
                }
                else
                {
                    //其他的返回空,但是空值也会缓存,比如查询 id=2,id=3 都会缓存
                    return null;
                }

            });
            if (student == null)
            {
                return NotFound("未找到");
            }
            else
            {
                return student;
            }
        }

七、缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,导致所有请求都会去查数据库,而查询数据量巨大,引起数据库压力过大甚至down机。

对于雪崩情况我们对缓存的策略主要是设置过期时间,部分不重要的站点,比如新闻网站我们将绝对过期时间AbsoluteExpiration设置的久一点。

对于要一定灵活性,能在请求不频繁的时候进行失效以更新数据的,我们可以用滑动过期时间,就是如果频繁请求就一值滑动过期时间。

当然为了避免滑动时间一直不过期,还可以两种方式混合使用。上面的例子,我们设置绝对过期时间是20秒,我们将滑动过期设置5秒,在5秒内有持续访问就一直续命,直到20秒绝对过期。

那么如果没人访问,在5秒后就过期了,这样数据下次访问也能及时查询最新数据。

image

八、分布式缓存

有了上面的缓存方案,对付一些小的简单业务系统完全够用了,但是如果你是分布式部署服务,那么像内存缓存访问的数据就是单个服务器的缓存。

你可能需要多个服务器的请求之间保持一致、在进行服务器重启和应用部署后仍然有效、不使用本地内存等情况。

这个时候我们可以使用第三方缓存,比如memecache,Redis等。Asp.Net Core 使用 IDistributedCache 接口与缓存进行交互。

  • NuGet安装包
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
  • 在 Program.cs 中注册 IDistributedCache 实现
    image

Configuration: 为连接配置。
InstanceName: 为存储键前缀。

编写测试方法GetStuden3

image

IDistributedCache 接受字符串键并以 byte[] 数组的形式添加或检索缓存项,所以数据是以byte[]形式访问,但是扩展了一个string类型的方法可以进行使用,我这里用字符串进行操作。

以上这些就是关于asp.net core 当中使用缓存的重要点和基础使用方法,详细参数和文档可参看官方文档:ASP.NET Core 中的缓存概述

.net 温故知新【13】:Asp.Net Core WebAPI 使用依赖注入DI - XSpringSun - 博客园

mikel阅读(237)

来源: .net 温故知新【13】:Asp.Net Core WebAPI 使用依赖注入DI – XSpringSun – 博客园

一、使用DI注入

在之前的文章中已经讲过DI的概念(.net 温故知新:【7】IOC控制反转,DI依赖注入),基于控制台程序演示了DI依赖注入的使用,基于Microsoft.Extensions.DependencyInjection完成。那在WebAPI中如何使用依赖注入呢?

首先新建一个WebAPI项目WebAPI_DI,框架.net 7,其实 webapi 项目也是控制台应用程序,只是在ASP.NET Core webapi框架中很多基础工作已经帮我们封装配置好了。

项目新建完成后在Program.cs 中自动生成如下代码:

namespace WebAPI_DI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();


            app.MapControllers();

            app.Run();
        }
    }
}

builder.Services 则是帮我已经创建好的IServiceCollection 对象。

我们再新建一个测试类 DITestClass:

    public class DITestClass
    {
        public int Add(int i, int n)
        {
            return i + n;
        }
    }

然后我们在builder.Services中进行注册

image

最后我们在默认的WeatherForecastController控制器里面加一个post方法,并用构造函数注入的方式将DITestClass注入进去。

image

swagger中调用测试:

image

二、[FromService] 注入

FromServicesAttribute 允许将服务直接注入到操作方法,而无需使用构造函数注入。
改属性的作用主要针对我们在依赖注入初始化对象(该对象初始化很耗时),这个时候不管请求的api方法有没有用到该对象都会等待很长时间。
所以使用FromService让接口在请求的时候再注入,从而不影响其他接口

image

三、多层架构注入

在多层架构中我们如果引用了其他项目,要使用其他项目中的类,那么要在主项目中进行DI注入,这样相当于所有其他模块或者其他人写的项目都需要主项目来维护注入,比如:

新建一个ClassLibrary1项目,Class1类,方法Sub:

image

我们在web项目里面引用ClassLibrary1项目,如果要使用Class1类就需要在 Programe.cs里面注册

builder.Services.AddScoped(typeof(Class1));

image

那么有没有方法让他们自己的项目自己管理注册呢,我们可以简单改造一下

  • 新建一个类库,定义一个公共接口,安装Microsoft.Extensions.DependencyInjection 包

image

  • 在ClassLibarary1里面定义实现接口类
    public class ModulInit : IModuleInit
    {
        public void Init(IServiceCollection service)
        {
            //所有需要DI的在此处注入
            service.AddScoped<Class1>();
        }
    }
  • 在Programe.cs里面调用引用项目的注册类

image

这样其他项目也按照这种方式,在主项目中只要调用一次注册管理。不过这是最简单的方式,你也可以使用反射来查找引用的项目继承了IModuleInit的类,然后进行Init方法调用,这样会更优雅一些。
当然你还可以使用其他依赖注入框架来取代Microsoft.Extensions.DependencyInjection,也许这个问题就不再是问题!

.net 温故知新【12】:Asp.Net Core WebAPI 中的Rest风格 - XSpringSun - 博客园

mikel阅读(236)

来源: .net 温故知新【12】:Asp.Net Core WebAPI 中的Rest风格 – XSpringSun – 博客园

RPC

RPC(Remote Procedure Call),远程过程调用),这种RPC形式的API组织形态是类和方法的形式。所以API的请求往往是一个动词用来标识接口的意思,比如 https://xxxx/GetStudent?id=1 和 https://xxxx/AddStudent 这种风格,并且往往没有规范需要我们去查看接口定义文档。HTTP方法基本只用GET和POST,没有使用HTTP的其它谓词设计比较简单。

Rest

Rest:按照Http的语义来使用HTTP协议的一种风格,Rest全称Representational State Transfer(表现层状态转换)。他是一种规范或者设计风格而不是特别的技术。REST形式的API组织形态是资源和实体,请求的路由可以看出对资源的访问,规范统一接口自解释。
比如 https://xxxx/Student/1 用Get方法调用就是获取编号为1的学生。 https://xxxx/Student/1 用Delete调用就是删除编号为1的学生,用delete调用就是删除该学生。
在HTTP中这些调用方法GET、POST、PUT、PATCH、DELETE 即HTTP谓词。GET用来获取资源,POST用来新建资源,PUT用来更新指定资源,PATCH用来批量更新资源,DELETE用来删除资源,通过谓词来表示请求动作或者意图,通过url定位资源。
在请求中GET、PUT、DELETE 请求是幂等的,也就是说可以重试请求。而POST不是幂等,因为POST意思是添加数据。
在Rest风格中使用状态码来标识返回结果,其中常用200、201、400、401、404、410、500等。

ASP.NET Core WebAPI接口

ASP.NET WebAPI中我们也能看到Rest的风格,理想很丰满,显示很骨干。如果我们严格的按照Rest风格设计接口的话,需要对技术人员有很高的要求,需要去划分不同业务不同的资源定位,而且有些业务也找不到准确的谓词去定义,响应状态码有限无法表达准确的意思,或者是时间上来不及等等原因。
而且这种方式更符合国外语言表达的方式,不太适合我国宝宝体质。
所以我们在设计接口的时候不用非要用Rest风格,我们可以靠近或者在特定的更适合使用Rest接口系统中使用。
本篇我们结合Rest看下接口如何设计和交互。

  • 在http接口请求中有三种方式传递参数或者数据。
  1. URL:资源定位,也就是Rest风格,在请求的url中包含信息,比如https://xxxx/Student/1 1就是学生编号。
  2. QueryString: URL之外的额外信息,比如RPC中https://xxxx/GetStudent?id=1 id=1就是QueryString
  3. 请求报文体:供PUT、POST提交提供数据,请求体有多种格式application/x-www-form-urlencoded、multipart/form-data、application/json、text/plain、application/xml。
  • 返回状态码
    在RPC中Post请求我们习惯如果请求已经在服务器处理,不管处理结果是否正确,我们都返回200状态码。然后在返回数据中用其它信息来标识业务结果。比如{code:1,msg:"成功"}或者{code:0,msg:"失败"}
    而在Rest 中Post通常用201返回新增成功,delete 删除的数据不存在返回404,但是404大家知道可能也许是url错误,所以表诉不清。
    因此我们在实际设计中可能会进行Rest裁剪,我们既使用RPC的返回结果,同时多用准确的状态码,不用什么都返回200。
    使用RPC风格,尽量使用合理谓词,不知道使用什么谓词的时候就用POST,Get Delete参数尽量用资源定位URL,业务错误服务端返回合适的状体吗,不知道返回什么就返回400,如果请求处理成功就用200同时返回结果数据。

在上一篇中遗留的这个问题 .net 温故知新【11】:Asp.Net Core WebAPI 入门使用及介绍

image

所以我们在Controller中Route配置为[Controller]则不管方法接口名称是什么,仍然以Rest的方式访问。

    [Route("[controller]")]
    [ApiController]
    public class RestCutController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<string> GetStudents()
        {
            //获取所有学生
            return new string[] { "student1", "student2" };
        }

        [HttpGet("{id}")]
        public string GetStudent(int id)
        {
            //获取id的学生
            return "student"+id;
        }

        [HttpPost]
        public void PostStudent([FromBody] string value)
        {
            //新增
        }

        [HttpPut("{id}")]
        public void PutStudent(int id, [FromBody] string value)
        {
            //修改
        }

        [HttpDelete("{id}")]
        public void DeleteStudent(int id)
        {
            //删除id学生
        }
    }

image

当我们修改Rout按照RPC方式,[Route("[controller]/[action]")] 运行后发现swagger展示的接口方式就改变了。并且保留了参数URL的方式。

image

关于返回状态码的问题可以有两种方式,一种是直接在ControllerBase.Response 响应中指定返回状态码。

        [HttpDelete("{id}")]
        public string DeleteStudent(int id)
        {
            //删除id学生
            if (id == 1)
            {
                return "删除成功";
            }
            else {

                Response.StatusCode = 404;
                return "未找到!";
            }
        }

另外一种方式就是返回泛型ActionResult<string>,其中OKNotFound是继承自ActionResult然后隐式转换到泛型,也可以直接返回IActionResult或者ActionResult但是类型不确定这样swagger文档就不会解析出返回值,所以我们用ActionResult泛型。

        [HttpDelete("{id}")]
        public ActionResult<string> DeleteStudent(int id)
        {
            //删除id学生
            if (id == 1)
            {
                return Ok("删除成功");
            }
            else
            {
                return NotFound("未找到!");
            }
        }

image

        [HttpDelete("{id}")]
        public ActionResult DeleteStudent(int id) //返回ActionResult
        {
            //删除id学生
            if (id == 1)
            {
                return Ok("删除成功");
            }
            else
            {
                return NotFound("未找到!");
            }
        }

image

最后我们在总结下关于API参数获取的方式,在[HttpGet("{id}")]中我们看到有{id},这个就是占位符,从RUL中获取,不光可以配置占位符还可以配置路径的其它值,甚至可以随意组织,只要我们的参数明和占位符相同就行。

        [HttpDelete("number/{id}/Name/{name}")] //自己组织的URL
        public ActionResult<string> DeleteStudent(int id,string name)
        {
            //删除id学生
            if (id == 1)
            {
                return Ok("删除成功");
            }
            else
            {
                return NotFound("未找到!");
            }
        }

image

当然也可以使用[FromRoute]从route获取,另外我们还有一些Attribute用于从不同的地方获取参数,比如从QueryString获取。那么我的请求URL就应该是/RestCut/DeleteStudent?id=1

        [HttpDelete]
        public ActionResult<string> DeleteStudent([FromQuery] int id)
        {
            //删除id学生
            if (id == 1)
            {
                return Ok("删除成功");
            }
            else
            {
                return NotFound("未找到!");
            }
        }

image

最后还有[FromHeader][FromForm][FromBody]这些获取参数的方式,不清楚的使用的时候查询就行了。

.net 温故知新【11】:Asp.Net Core WebAPI 入门使用及介绍 - XSpringSun - 博客园

mikel阅读(260)

来源: .net 温故知新【11】:Asp.Net Core WebAPI 入门使用及介绍 – XSpringSun – 博客园

ASP.NET Core 上面由于现在前后端分离已经是趋势,所以ASP.NET core MVC用的没有那么多,主要以WebApi作为学习目标。

一、创建一个WebApi项目

我使用的是VS2022, .Net 7版本。
在创建界面有几项配置:

  • 配置Https
  • 启用Docker
  • 使用控制器
  • 启用OpenAPI支持
  • 不使用顶级语句

image

其中配置Https 是WebApi是否使用https协议,启用docker是配置服务是否docker部署支持。我们这边作为学习就先不管docker了。
然后下面还有三个配置,第一个是说是否使用控制器,如果使用接口服务放在Controllers文件夹下统一管理并且相关路由规则不一样。
第二个启用OpenAPI支持,如果启用OpenAPI说的是swagger支持,也就是说.net 自动集成了swagger。
第三个不使用顶级语句,如果勾选后则程序的Program类和Main方法完整。

那么我们看下上面配置是什么意思,第二个swagger支持我们就不管了默认开启。我建两个项目AspNetCoreWebAPI_1、AspNetCoreWebAPI_2,AspNetCoreWebAPI_1我们勾选上【使用控制器】、【不使用顶级语句】。AspNetCoreWebAPI_2项目这两项都不选。

先看下项目目录结构

不同在于AspNetCoreWebAPI_1项目多了Controllers文件夹和一个WeatherForecast类,WeatherForecast类是示例接口中有使用。

image

我们再对比一下 Program

可以看到在AspNetCoreWebAPI_1项目中Program类和Main方法完整,因为要使用Controller的原因,所以依赖注入了Controller服务。并且使用了MapControllers注册路由。

在AspNetCoreWebAPI_2项目中没有只有Main方法内的代码,这就是顶级语句。然后由于我们还使用了最小API,就是不使用Controller方式注册和配置路由,直接在代码中自己注册接口和实现接口处理的代理方法。

image

按照以前asp.net习惯和项目清晰度维护性我们一般是使用Controller的方式,并且不使用顶级语句。

而最小 API,是创建具有最小依赖项的 HTTP API。 它非常适合于需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。

另外还有一个appsetting.json配置文件,这部分内容也在前面已经介绍过,欢迎了解:.net 温故知新:【8】.NET 中的配置从xml转向json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

二、WebApi运行

我们调试项目AspNetCoreWebAPI_1,此时会启动一个服务在后端,同时启动浏览器访问该站点的swagger,该swagger用于调我们调试webapi接口。
image

image

我们点击示例接口WeatherForecast,访问接口会返回json格式数据。响应的headers里面可以看到后端运行的服务器是Kestrel,和我们以前.net framework不一样的事需要借助IIS作为服务器。现在的Kestrel是包含在程序中的,这个Kestrel 以后再讨论。
image

三、WeatherForecastController

WeatherForecastController是在创建项目后默认生成的一个示例Controller。在该Controller中我们可以看到几个重点项。

using Microsoft.AspNetCore.Mvc;

namespace AspNetCoreWebAPI_1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}
  1. ControllerBase 基类
    web API 控制器通常应派生自 ControllerBase 而不是 Controller。 Controller 派生自 ControllerBase,并添加对视图的支持,因此它用于处理 Web 页面,而不是 Web API 请求。 如果同一控制器必须支持视图和 Web API,则派生自 Controller。
  2. [ApiController]
    [ApiController] 属性可应用于控制器类,以启用下述 API 特定的固定行为:
    1)属性路由要求:不能通过由 UseEndpoints、UseMvc 或 UseMvcWithDefaultRoute 定义的传统路由访问操作,传统路由就是以前老的路由规则,型如”{controller=Home}/{action=Index}/{id?}”。需要使用[Route(“XX”)]指定路由。
    自动 HTTP 400 响应:[ApiController] 属性使模型验证错误自动触发 HTTP 400 响应。
    2)绑定源参数推理:绑定源特性定义可找到操作参数值的位置,接口的参数通过推理规则应用于操作参数的默认数据源。
    3)Multipart/form-data 请求推理:[ApiController] 属性对 IFormFile 和 IFormFileCollection 类型的操作参数应用推理规则。 为这些类型推断 multipart/form-data 请求内容类型。
    4)、错误状态代码的问题详细信息: 将错误结果(状态代码为 400 或更高的状态码)转换为为 ProblemDetails 的结果。也就是说状态码会转换如下json格式返回信息。
{
  type: "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  title: "Not Found",
  status: 404,
  traceId: "0HLHLV31KRN83:00000001"
}
  1. [Route(“[controller]”)]
    指定控制器上的属性路由,属性路由将应用的功能建模为一组资源,其中操作由 HTTP 谓词表示。也就是说路由该属性配置了路由,如上图请求时的路由https://localhost:7122/WeatherForecast,配置中“[controller]”为标记替换,为方便起见,属性路由支持标记替换,方法是将标记用方括号([、])括起来[controller]用于替换WeatherForecastController中WeatherForecast部分。
  2. [HttpGet(Name = “GetWeatherForecast”)]
    HttpGet指示Get方法为Route路由的操作,即使我们将Get方法改为其他名字仍然不影响请求路由https://localhost:7122/WeatherForecast,并且以Get方式。这种api风格即为Rest风格。Rest风格我们后面再学习。
    ASP.NET Core 具有以下 HTTP 谓词模板:

    • [HttpGet]
    • [HttpPost]
    • [HttpPut]
    • [HttpDelete]
    • [HttpHead]
    • [HttpPatch]
  3. logger日志记录
    日志记录是基础知识点,这部分内容在我们之前温故知新中已经详细介绍过,可移步了解:.net 温故知新:【9】.NET日志记录 ILogger使用和原理

以上为我们入门WebApi创建的一个默认项目,并对创建选项、项目结构、服务要点进行了分析,后面将更进一步学习分享其他asp.net core webapi重要知识。

.net 温故知新【17】:Asp.Net Core WebAPI 中间件 - XSpringSun - 博客园

mikel阅读(269)

来源: .net 温故知新【17】:Asp.Net Core WebAPI 中间件 – XSpringSun – 博客园

一、前言

到这篇文章为止,关于.NET “温故知新”系列的基础知识就完结了,从这一系列的系统回顾和再学习,对于.NET core、ASP.NET CORE又有了一个新的认识。

不光是从使用,还包括这些知识点的原理,虽然深入原理谈不上,但对于日常使用也够了,我想的是知其然,知其所以然。

在实际开发过程中可能是知道怎么使用就行,但系统学习了这些基本的框架、组件、或者说原理后,对于我们软件设计、开发、扩展和解决问题还是有帮助的。

刚好到2023新年前赶着写完,也算对自己这个系列的一个交代,实际上我平时基本不使用ASP.NET CORE,目前我主要开发桌面程序,还是用的winform。

写这个系列的初衷是想紧跟.NET的发展进程,同时储备基础知识,平时还搞一些微服务(Java)、NLP、OCR、知识图谱、前端(Vue3),只要需要反正啥都搞,没必要固执,技术只是手段,不是目的。

那么接下来就继续简单的梳理一下中间件,欢迎对这个系列拍砖!

二、中间件

中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件:

  • 选择是否将请求传递到管道中的下一个组件。
  • 可在管道中的下一个组件前后执行工作。

这个是关于中间件概念的概括,官方的概括是相当精准,那么我们就围绕管道、传递、组件来看看中间件。

请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。使用 Run、Map 和 Use 扩展方法来配置请求委托。

我们照例新建一个ASP.NET CORE Web API 项目:WebAPI_Middleware

namespace WebAPI_Middleware
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();


            app.MapControllers();

            app.Run();
        }
    }
}

在Program.cs 中我们看到前面部分builder是配置依赖注入的东西,这部分可以参看.net 温故知新【13】:Asp.Net Core WebAPI 使用依赖注入DI 。

app 使用Use扩展用于中间件添加到管道中

Map 基于给定请求路径的匹配项来创建请求管道分支

Run 委托始终为终端,用于终止管道。

中间件的执行顺序过程如下:

image

三、Map

我们将上面自动创建的东西全都删除,用Map来匹配路由,然后通过不同的代理处理请求。

    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            var app = builder.Build();
           
            //匹配map1 请求
            app.Map("/map1", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map1 run");
                });
            }));
            //匹配map2 请求
            app.Map("/map2", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map2 run");
                });
            }));

            app.Run();
        }
    }
  • 请求map1 我们输出:map1 run

image

  • 请求map2 我们输出:map2 run

image

Asp.Net Core MapControllers 的扩展方法也是类似道理,用来匹配路由调用处理程序。

四、Run

在上面的 Map 后面我们使用的处理方法中 Run 用于终止管道。也就是说在该管道中如果调用了 Run 那么就直接返回了,即使你后面还添加了 Use 也不会执行。

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

Map 相当于是迎客进门,Map 上了就用指定的管道进行处理,如果没有 Map 上就调用主管道,也就是主管道上的其他中间件也会执行处理。比如我们再加一个 Run 用于没匹配上路由也输出点信息。

image

加了context.Response.ContentType = "text/plain; charset=utf-8"; 不然中文会乱码。

image

因为 Run 是终结点,那这个管道中我还想加其他处理怎么办呢,这个时候就该轮到 Use 出场了。

五、Use

用 Use 将多个请求委托链接在一起。 next 参数表示管道中的下一个委托。 可通过不调用 next 参数使管道短路。

首先我们在外面添加两个 Use,不放到 Map 中,这样的话就只有未匹配到的路由会调用

    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            var app = builder.Build();

            

            app.Map("/map1", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map1 run");
                });
            }));

            app.Map("/map2", new Action<IApplicationBuilder>((app) =>
            {
                app.Run(async context =>
                {
                    await context.Response.WriteAsync("map2 run");
                });
            }));
            //Use1
            app.Use(async (context, next) =>
            {
                context.Response.ContentType = "text/plain; charset=utf-8";

                await context.Response.WriteAsync("第 1 个Use   开始!\r\n", Encoding.UTF8);

                await next();

                await context.Response.WriteAsync("第 1 个Use   结束!\r\n", Encoding.UTF8);

            });
            
            //Use2
            app.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("第 2 个Use   开始!\r\n", Encoding.UTF8);

                await next();

                await context.Response.WriteAsync("第 2 个Use   结束!\r\n", Encoding.UTF8);

            });
            //结束管道处理
            app.Run(async context =>
            {
                await context.Response.WriteAsync("未匹配处理!\r\n", Encoding.UTF8);
            });

            app.Run();
        }
    }

最后执行的路径和最开始的图是一致的。

image

为什么将context.Response.ContentType = "text/plain; charset=utf-8"; 放到第一个 Use 呢,因为如果放到 Run 里面会报错,改变了 Header 标头。所以理论上也不要在 Use 里面发送响应WriteAsync,此处为了演示所以这么写。

image

六、中间件类

上面的代理方法可以移动到类中,这个类就是中间件类。中间件类需要如下要求:

  • 具有类型为 RequestDelegate 的参数的公共构造函数。
  • 名为 Invoke 或 InvokeAsync 的公共方法。 此方法必须:
    返回 Task。
    接受类型 HttpContext 的第一个参数。

构造函数和 Invoke/InvokeAsync 的其他参数由依赖关系注入 (DI) 填充。

将上面的未匹配路由处理逻辑移动到中间件类中:

  • TestMiddleware1:
    public class TestMiddleware1
    {
        private readonly RequestDelegate _next;

        public TestMiddleware1(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {

            context.Response.ContentType = "text/plain; charset=utf-8";

            await context.Response.WriteAsync("第 1 个Use   开始!\r\n", Encoding.UTF8);

            await _next(context);

            await context.Response.WriteAsync("第 1 个Use   结束!\r\n", Encoding.UTF8);
        }
    }
  • TestMiddleware2
    public class TestMiddleware2
    {
        private readonly RequestDelegate _next;

        public TestMiddleware2(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {


            await context.Response.WriteAsync("第 2 个Use   开始!\r\n", Encoding.UTF8);

            await _next(context);

            await context.Response.WriteAsync("第 2 个Use   结束!\r\n", Encoding.UTF8);
        }
    }
  • Program
    image
  • 运行
    image

此处的中间件使用有顺序问题,如果我先app.UseMiddleware<TestMiddleware2>() 因为 TestMiddleware1 修改了标头,根据约定是不允许的,所以程序是有报错。

image

因此中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。

七、中间件顺序

image

以上是内置中间件的默认顺序规则,具体如何使用内置中间件,可参阅官方资料。

八、写在最后

以上就是关于中间件的部分知识,结合我自己的理解做了前后衔接的梳理逻辑。

官方网站更多的是讲解每个知识点的细节,前后需要结合起来理解,当然我还是强烈建议跟着官方文档学习,而且是最权威最可信的:ASP.NET Core 中间件

这个系列历时2年,工作生活都比较忙,也有放纵啥事不相干的时候,中间断断续续的,总算是坚持完了。很多东西就是这样,累了就休息一下贵在坚持,即使再慢,积累的成果也有收获。

Redis系列:使用 Redis Module 扩展功能 - Hello-Brand - 博客园

mikel阅读(322)

来源: Redis系列:使用 Redis Module 扩展功能 – Hello-Brand – 博客园

1 啥是Redis Module

Redis Module是Redis的一种扩展模块,从 4.0版本开始,允许用户自定义扩展模块,在Redis内部实现新的数据类型和功能,使用统一的调用方式和传输协议格式扩展Redis的能力。它本身的设计目的就是在不同版本的Redis中运行,因此无需重新编译模块即可与特定版本(Redis > 4.0)一起运行。

通过使用Redis Module,可以在Redis中添加新的命令和数据类型,以实现更高级的功能。例如,一些第三方模块支持全文搜索、JSON数据的存储和查询、自定义分布式锁、时间序列数据的存储和查询等。我们也可以基于 Redis 去定制开发属于自己的 Module,来支撑自己的业务发展。

2 有哪些常见的Redis Module

目前,被 Redis 官方推荐的 Module 有:

2.1 RediSearch

一个功能齐全,可实现 快速检索、二次索引和全文搜索的搜索引擎模块
image
地址:https://github.com/RediSearch/RediSearch

2.2 RedisJSON

RedisJSON是一个用于处理 JSON 数据的模块,它实现了JSON数据交换标准,允许从Redis 文档中存储、更新和获取JSON值。
地址:https://github.com/RedisJSON/RedisJSON

2.3 RedisTimeSeries

RedisTimeSeries是Redis的一个时间序列数据库(TSDB)管理模块。RedisTimeSeries可以保存多个时间序列,每个时间序列都可以通过一个Redis键访问(类似于任何其他Redis数据结构)。
地址:https://github.com/RedisTimeSeries/RedisTimeSeries

2.4 RedisGraph

用于实现图形数据库的模块
https://github.com/RedisGraph/RedisGraph?tab=readme-ov-file#trying-redisgraph

2.5 RedisBloom

RedisBloom为Redis增加了一组概率数据结构,包括Bloom filter(布隆过滤器), Cuckoo filter, Count-min sketch, Top-K和t-digest。使用此功能,您可以查询流数据,而无需存储流的所有元素。每种概率数据结构都应对不同的业务场景。
地址:https://github.com/RedisBloom/RedisBloom

2.6 RedisCell

实现分布式限流能力的模块,使用了相对精妙的算法 generic cell rate algorithm (GCRA) 。
地址:https://github.com/brandur/redis-cell

2.7 RedisAI

RedisAI是一个Redis模块,用于执行深度学习/机器学习模型并管理其数据。它的目的是成为模型服务的“主力军”,为流行的DL/ML框架提供开箱即用的支持和无与伦比的性能。RedisAI坚持数据本地化原则,最大限度地提高了计算吞吐量,减少了延迟。
地址:https://github.com/RedisAI/RedisAI

上面给出的几个都是GitHub小星星比较高的模块,如果你想要了解很多的内容,可以参考官方文档:https://redis.io/modules。

3 Redis module 实践指南

3.1 安装Redis

因为这些扩展模块是依赖于Redis服务存在的,所以前提是Redis安装完成。
Redis官方安装指南:https://redis.io/docs/install/install-redis/

3.2 以RedisJson 为例子

3.2.1 下载RedisJSON

github上下载ReJSON的地址:https://github.com/RedisJSON/RedisJSON/releases
官方使用文档:https://redis.io/docs/data-types/json/
可以看到,最新的版本是 v2.6.8
image

3.2.2 安装

1、获取文件

在 redis 安装目录下新建 module 文件夹,把获取到的rejson.so文件 放到 module 文件夹中

# 先进入redis的安装目录
cd /usr/local/soft/redis-6.2.6/

# 然后新建一个module文件夹
mkdir module

# 最后rejson.so文件放到module文件夹即可

2、修改配置

# 进入到module目录下
cd ./module

# 为文件加X,即修改rejson.so为可执行文件
chmod +x rejson.so

# 修改 redis.conf,初始时自动 loadmodule
loadmodule /usr/local/soft/redis-6.2.6/module/rejson.so

# 重启 redis
redis-cli -a 123456 shutdown
redis-server conf/redis.conf
redis-cli -p 6379

3.2.3 RedisJSON操作实践

1、写入JSON.SET

语法解析

JSON.SET <key> <path> <json>  [NX | XX]
  • key:要操作的键。
  • path:JSON路径,用于指定要设置值的对象或数组的路径。
  • json:要设置的JSON值。
  • [NX | XX]:可选参数,用于指定操作的条件。
    • NX:仅当指定的键不存在时,才设置值。
    • XX:仅当指定的键存在时,才设置值。

比如下面,保存两条数据

# 保存两条记录
127.0.0.1:6379> JSON.SET user1 $ '{"name":"Brand", "age":18, "sex":"1"}'
127.0.0.1:6379> JSON.SET user2 $ '{"name":"Candy", "age":17, "sex":"0"}'

2、读取操作JSON.GET

语法解析

JSON.GET <key>
         [INDENT indentation-string]
         [NEWLINE line-break-string]
         [SPACE space-string]
         [path ...]
  • Key:要获取值的键
  • INDENT:查询结果替换掉默认缩进字符
  • NEWLINE:查询结果替换掉默认换行符
  • SPACE:查询结果替换掉默认空格
  • path:允许使用多个path进行查询
  • 获取JSON对象中的属性时需要以.开头

比如下面,获取相关信息

# 得到myjson的所有数据
127.0.0.1:6379> JSON.GET user1
1) '{"name":"Brand", "age":18, "sex":"1"}'

# 得到json数据中的键name的值
127.0.0.1:6379> JSON.GET user1 name
1) "Brand"

3、批量读取操作JSON.MGET

这是一个批量操作的语法,语法解析:

JSON.MGET <key> [key ...] <path>
  • key:是一个列表,指的是可以有多个key
  • path:指所有key的path,即遍历每一个key的path,如果不存在,则返回null

下面的语句演示MGET获取批量数据:

127.0.0.1:6379> JSON.MGET user1 user2 $.name
1) "Brand"
2) "Candy"

4、除操作JSON.DEL
这是删除操作的语法,语法解析:

JSON.DEL <key> [path]
  • path是可选参数,如果没有提供,则默认整个Key删除掉

下面语句中

# 删除整个Json
127.0.0.1:6379> JSON.DEL user1
(integer) 1

# 删除某个字段
127.0.0.1:6379> JSON.DEL user2  $.age
1) "17"

5、其他操作类行
除了上面的几种常见操作,官方还支持如下命令,官方命令地址:https://redis.io/docs/data-types/json/
常用命令(数字可以递增、相乘):

# JSON.NUMINCRBY,JSON.NUMMULTBY,JSON.STRAPPEND,JSON.STRLEN

> JSON.SET num $ 0
OK
> JSON.NUMINCRBY num $ 1
"[1]"
> JSON.NUMINCRBY num $ 1.5
"[2.5]"
> JSON.NUMINCRBY num $ -0.75
"[1.75]"
> JSON.NUMMULTBY num $ 24
"[42]"

数组命令:

#  JSON.ARRAPPEND,JSON.ARRINDEX,JSON.ARRINSERT,JSON.ARRLEN,JSON.ARRPOP,JSON.ARRTRIM

> JSON.SET arr $ []
OK
> JSON.ARRAPPEND arr $ 0
1) (integer) 1
> JSON.GET arr $
"[[0]]"
> JSON.ARRINSERT arr $ 0 -2 -1
1) (integer) 3
> JSON.GET arr $
"[[-2,-1,0]]"
> JSON.ARRTRIM arr $ 1 1
1) (integer) 1
> JSON.GET arr $
"[[-1]]"
> JSON.ARRPOP arr $
1) "-1"
> JSON.ARRPOP arr $
1) (nil)

对象命令(对象长度和key值输出):

#  JSON.OBJKEYS,JSON.OBJLEN

> JSON.SET obj $ '{"name":"Leonard Cohen","lastSeen":1478476800,"loggedOut": true}'
OK
> JSON.OBJLEN obj $
1) (integer) 3
> JSON.OBJKEYS obj $
1) 1) "name"
   2) "lastSeen"
   3) "loggedOut"

组件命令:
JSON.TYPE,JSON.Debug,JSON.FORGET,JSON.RESP

4 总结

本文介绍了Redis的扩展模块Redis Module,设计目的就是使用加载Redis Module,在Redis中添加新的命令和数据类型,以实现更高级的功能。
文章介绍了目前官方推荐的几个Module,并演示了RedisJSON的效果。

工作中使用Redis的10种场景 - 苏三说技术 - 博客园

mikel阅读(332)

来源: 工作中使用Redis的10种场景 – 苏三说技术 – 博客园

前言

Redis作为一种优秀的基于key/value的缓存,有非常不错的性能和稳定性,无论是在工作中,还是面试中,都经常会出现。

今天这篇文章就跟大家一起聊聊,我在实际工作中使用Redis的10种场景,希望对你会有所帮助。

1. 统计访问次数

对于很多官方网站的首页,经常会有一些统计首页访问次数的需求。

访问次数只有一个字段,如果保存到数据库中,再最后做汇总显然有些麻烦。

该业务场景可以使用Redis,定义一个key,比如:OFFICIAL_INDEX_VISIT_COUNT。

在Redis中有incr命令,可以实现给value值加1操作:

incr OFFICIAL_INDEX_VISIT_COUNT

当然如果你想一次加的值大于1,可以用incrby命令,例如:

incrby OFFICIAL_INDEX_VISIT_COUNT 5

这样可以一次性加5。

2. 获取分类树

在很多网站都有分类树的功能,如果没有生成静态的html页面,想通过调用接口的方式获取分类树的数据。

我们一般为了性能考虑,会将分类树的json数据缓存到Redis当中,为了后面在网站当中能够快速获取数据。

不然在接口中需要使用递归查询数据库,然后拼接成分类树的数据结构。

这个过程非常麻烦,而且需要多次查询数据库,性能很差。

因此,可以考虑用一个定时任务,异步将分类树的数据,直接缓存到Redis当中,定义一个key,比如:MALL_CATEGORY_TREE。

然后接口中直接使用MALL_CATEGORY_TREE这个key从缓存中获取数据即可。

可以直接用key/value字符串保存数据。

不过需要注意的是,如果分类树的数据非常多可能会出现大key的问题,优化方案可以参考我的另外一篇文章《分类树,我从2s优化到0.1s》。

3. 做分布式锁

分布式锁可能是使用Redis最常见的场景之一,相对于其他的分布式锁,比如:数据库分布式锁或者Zookeeper分布式锁,基于Redis的分布式锁,有更好的性能,被广泛使用于实际工作中。

我们使用下面这段代码可以加锁:

try{
  String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
      return true;
  }
  return false;
} finally {
    unlock(lockKey);
}  

但上面这段代码在有些场景下,会有一些问题,释放锁可能会释放了别人的锁。

说实话Redis分布式锁虽说很常用,但坑也挺多的,如果用不好的话,很容易踩坑。

如果大家对Redis分布式锁的一些坑比较感兴趣,可以看看我的另一篇文章《聊聊redis分布式锁的8大坑》,文章中有非常详细的介绍。

4. 做排行榜

很多网站有排行榜的功能,比如:商城中有商品销量的排行榜,游戏网站有玩家获得积分的排行榜。

通常情况下,我们可以使用Sorted Set保存排行榜的数据。

使用ZADD可以添加排行榜的数据,使用ZRANGE可以获取排行榜的数据。

例如:

ZADD rank:score 100 "周星驰"
ZADD rank:score 90 "周杰伦"
ZADD rank:score 80 "周润发"
ZRANGE rank:score 0 -1 WITHSCORES

返回数据:

1) "周星驰"
2) "100"
3) "周杰伦"
4) "90"
5) "周润发"
6) "80"

5. 记录用户登录状态

通常下,用户登录成功之后,用户登录之后的状态信息,会保存到Redis中。

这样后面该用户访问其他接口的时候,会直接从Redis中查询用户登录状态,如果可以查到数据,说明用户已登录,则允许做后续的操作。

如果从Redis中没有查到用户登录状态,说明该用户没有登录,或者登录状态失效了,则直接跳转到用户登录页面。

使用Redis保存用户登录状态,有个好处是它可以设置一个过期时间,比如:该时间可以设置成30分钟。

jedis.set(userId, userInfo, 1800);

在Redis内部有专门的job,会将过期的数据删除,也有获取数据时实时删除的逻辑。

6. 限流

使用Redis还有一个非常常用的的业务场景是做限流

当然还有其他的限流方式,比如:使用nginx,但使用Redis控制可以更精细。

比如:限制同一个ip,1分钟之内只能访问10次接口,10分钟之内只能访问50次接口,1天之内只能访问100次接口。

如果超过次数,则接口直接返回:请求太频繁了,请稍后重试。

跟上面保存用户登录状态类似,需要在Redis中保存用户的请求记录。

比如:key是用户ip,value是访问的次数从1开始,后面每访问一次则加1。

如果value超过一定的次数,则直接拦截这种异常的ip。

当然也需要设置一个过期时间,异常ip如果超过这个过期时间,比如:1天,则恢复正常了,该ip可以再发起请求了。

或者限制同一个用户id。

7. 位统计

比如现在有个需求:有个网站需要统计一周内连续登陆的用户,以及一个月内登陆过的用户。

这个需求使用传统的数据库,实现起来比较麻烦,但使用Redis的bitmap让我们可以实时的进行类似的统计。

bitmap 是二进制的byte数组,也可以简单理解成是一个普通字符串。它将二进制数据存储在byte数组中以达到存储数据的目的。

保存数据命令使用setbit,语法:

setbit key offset value

具体示例:

setbit user:view:2024-01-17 123456 1

往bitmap数组中设置了用户id=123456的登录状态为1,标记2024-01-17已登录。

然后通过命令getbit获取数据,语法:

getbit key offset

具体示例:

getbit user:view:2024-01-17 123456

如果获取的值是1,说明这一天登录了。

如果我们想统计一周内连续登录的用户,只需要遍历用户id,根据日期中数组中去查询状态即可。

8. 缓存加速

我们在工作中使用Redis作为缓存加速,这种用法也是非常常见的。

如果查询订单数据,先从Redis缓存中查询,如果缓存中存在,则直接将数据返回给用户。

如果缓存中不存在,则再从数据库中查询数据,如果数据存在,则将数据保存到缓存中,然后再返回给用户。

如果缓存和数据库都不存在,则直接给用户返回数据不存在。

流程图如下:

但使用缓存加速的业务场景,需要注意一下,可能会出现:缓存击穿、穿透和雪崩等问题,感兴趣的小伙伴,可以看看我的另一篇文章《烂大街的缓存穿透、缓存击穿和缓存雪崩,你真的懂了?》,里面有非常详细的介绍。

9. 做消息队列

我们说起队列经常想到是:kafka、rabbitMQ、RocketMQ等这些分布式消息队列。

其实Redis也有消息队列的功能,我们之前有个支付系统,就是用的Redis队列功能。

PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。

顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。对应channel发送消息后,所有订阅者都能收到相关消息。

在java代码中可以实现MessageListener接口,来消费队列中的消息。

@Slf4j
@Component
public class RedisMessageListenerListener implements MessageListener {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(pattern);
        RedisSerializer<?> valueSerializer = redisTemplate.getValueSerializer();
        Object deserialize = valueSerializer.deserialize(message.getBody());
        if (deserialize == null) return;
        String md5DigestAsHex = DigestUtils.md5DigestAsHex(deserialize.toString().getBytes(StandardCharsets.UTF_8));
        Boolean result = redisTemplate.opsForValue().setIfAbsent(md5DigestAsHex, "1", 20, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(result)) {
            log.info("接收的结果:{}", deserialize.toString());
        } else {
            log.info("其他服务处理中");
        }
    }
}

10. 生成全局ID

在有些需要生成全局ID的业务场景,其实也可以使用Redis。

可以使用incrby命令,利用原子性操作,可以执行下面这个命令:

incrby userid 10000

在分库分表的场景,对于有些批量操作,我们可以从Redis中,一次性拿一批id出来,然后给业务系统使用。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。