WebAPI(二)

内存缓存

启用:

builder.Services.AddMemoryCache()

注入:IMemoryCache接口,查看接口的方法:TryGetValue;Remove;Set;GetOrCreatAsync

public async Task<Book[]> GetBooks()
{
    logger.LogInformation("开始执行GetBooks");
    var item=await memCache.GetOrCreateAsync("AllBooks",async(e)=>
                                             {
                                                 logger.LogInformation("从数据库中读取数据");
                                                 return await dbCtx.Books.ToArrayAsync();
                                             });
    logger.LogInformation("把数据返回给调用者");
    return items;
}

缓存的过期时间策略:

在数据改变的时候调用Remove或者Set来删除或者修改缓存

设置过期时间 绝对过期策略 滑动国企策略

//绝对过期策略
var item=await memCache.GetOrCreateAsync("AllBooks",async(e)=>
 {
     //这里设置了10秒的缓存时间
     e.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(10);
 }
//滑动过期策略 只要在缓存过期之前请求一次,缓存自动续时间,重置时间
var item=await memCache.GetOrCreateAsync("AllBooks",async(e)=>
 {
     e.SlidingExpiration=TimeSpan.FromSeconds(10);
 }
//两种过期时间混合使用
var item=await memCache.GetOrCreateAsync("AllBooks",async(e)=>
 {
    e.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(30);
     e.SlidingExpiration=TimeSpan.FromSeconds(10);
 }

缓存穿透

string cacheKey="Book"+id;//缓存的键
Book? b=memCache.Get<Book?>(cacheKey);
if(b==null)
{
    //查询数据库,然后写入缓存
	b=await dbCtx.Books.FindAsync(id);
	memCache.Set(cacheKey,b);
}

以上代码,如果你查询的id不存在,那么查询到的是null,缓存中的也是null,程序会认为没有缓存数据,导致重复存入,对数据库造成压力

GetOrCreateAsync()方法可以避免这个问题

缓存雪崩和数据混乱问题

缓存项集中过期会引起缓存雪崩

解决方案:在基础过期时间是,再加上一个随机过期时间。

var item=await memCache.GetOrCreateAsync("AllBooks",async(e)=>
 {
     //随机事件更新缓存
     e.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(Random.Shared.Next(10,15));
 }

封装内存缓存操作的帮助类

分布式缓存

统一缓存服务器

redis是其中一个可以被用作分布式缓存服务器的,但是Redis并不是专门用来做缓存服务器的

.net Core提供了统一的分布式缓存服务器的操作接口IDistbutedCache,用法和内存缓存相似

分布式缓存和内存缓存的区别:缓存值的类型为byte[],需要我们进行类型转换,也提供了一些按照string类型存取缓存值的扩展方法。

方法 说明
Task<byte[]> GetAsync(string key) 查询缓存键key对应的缓存值,返回值是byte[]
类型,如果对应的缓存不存在,则返回null。
Task RefreshAsync(string key) 刷新缓存键key对应的缓存项,会对设置了滑动
过期时间的缓存项续期。
Task RemoveAsync(string key) 删除缓存键key对应的缓存项。
Task SetAsync(string key, bytel
value, DistributedCacheEntryOptions
options)
设置缓存键key对应的缓存项,value属性为
byte[]类型的缓存值,注意value不能是 null
值。
Task GetStringAsync(string
key)
按照string类型查询缓存键key对应的缓存值,
返回值是string类型,如果对应的缓存不存在,
则返回 null。
Task SetStringAsync(string key, string
value, DistributedCacheEntryOptions
options)
设置缓存键key对应的缓存项,value属性为
string类型的缓存值,注意 value不能是 null
值。

NuGet

Microsoft.Extensions.Caching.StackExchangeRedis
builder.Service.AddSrackExchangeRedisCache(options=>
{
    options.Configuration="localhost";
    options.InstanceName="cache_"//前缀
});
public async Task<ActionResult<Book?>> Test(long id)
{
    Book? book;
    string? s=await distCache.GetStringAsync("Book"+id);
    if(s==null)
    {
        //从数据库拿去数据
        book=await MyDbContext.GetByIdAsync(id);
        //反序列化,转换成为Json字符串,写入redis
        distChche.SetStringAsync("Book"+id,JsonSerializer.Serialize(book));
    }
    else
    {
        //将从redis拿出的数据序列化
        book=JsonSerializer.Deserialize<Book?>(s);
    }
}

ASP.NETCore于配置系统的集成

配置的环境问题

ASP.NET Core会从环境变量中读取名字为ASPNETCORE_ENVIROONMENT的值。

推荐值:Development(开发环境);Staging(测试环境);Production(生产环境)

每一个环境都需要单独的数据库配置

在Program

app.Environment.EnvironmentName
app.Environment.IsProduction()    

.NET Core放置机密配置外泄

右键项目,点击管理用户机密。

app.Configuration.GetSection("conn")

ASP.NET CORE在progarm中如何读取配置

分层ASP.NET的用法

EFCore配置

  1. 建立类库项目,放置实体类,DbContext, 配置类等DbContext中不配置数据库连接,而是为DbContext增加一个DbContextOptons类型的构造函数。
  2. EFCore项目安装对应数据库的EFCore Provider
  3. asp.net core项目引用EFCore项目,并且通过AddDbContext来注入DbContext及DbContext进行配置。
  4. Controller中就可以注入 DbContext类使用了。
  5. 让开发环境的Add-Migration知道连接哪个数据库
  6. 在efcore项目中,创建了一个实现了IDesignTimeDbContextTactory接口的类。并且在CreatDbContext返回一个连接开发数据库的DbContext。
  7. 在这里必须把连接字符串配置到Windows的正式环境变量中,然后再Environment.GetEnvironmentVariable读取。
  8. 正常执行Add-Migration、Update-Database迁移就行了。需要把EFCore项目设置为启动项目,并且在【程序包管理器控制台】中也要选中EFCore项目,并且安装Microsoft.EntityFrameworkCore.SqlServer;Microsoft.EntityFrameworkCore.Tools

DBContext池

builder.Services.AddDbContextPool<MyDbContext>(opt =>
{
    string connStr=builder.Configuration.GetSection("Connect").Value;
    opt.UseSqlServer(connStr);
});

连接池池有一些限制,一些声明周期短的服务无法引用。

采用批量注册上下文的方案

中间件

中间件是ASP.NET Core的核心组件,MVC框架,响应缓存,身份验证,CORS,Swagger等都是内置中间件。

什么是中间件

  1. 广义上来讲:Tomcat、WebLogic、Redis、IS;狭义上来讲,ASP.NET Core中的中间件指ASP.NET Core中的一个组件。
  2. 中间件由前逻辑、next、后逻辑3部分组成,前逻辑为第一段要执行的逻辑代码、next为指向下一个中间件的调用、后逻辑为从下一个中间件执行返回所执行的逻辑代码。每个HTTP请求都要经历一系列中间件的处理,每个中间件对于请求进行特定的处理后,再转到下一个中间件,最终的业务逻辑代码执行完成后,响应的内容也会按照处理的相反顺序进行处理,然后形成HTTP响应报文返回给客户端。
  3. 中间件组成一个管道,整个ASP.NET Core的执行过程就是HTTP请求和响应按照中间件组装的顺序在中间件之间流转的过程。开发人员可以对组成管道的中间件按照需要进行自由组合。

中间件的三个概念

Map;Use;Run。

Map用来定义一个管道可以处理哪些请求,Use和Run用来定义管道,一个管道又若干个Use和一个Run组成,每个Use引入一个中间件,而Run是用来执行最终核心应用逻辑。

Map就是路由

基本的中间件

ASP.NET Core高级

Identity框架(标识框架)

标识框架

采用基于校色的访问控制RBAC策略,内置了对用户,角色等表的管理以及相关的接口,支持外部登录,2FA

Authentication和Authorization的区别

  • Authentication对访问者的用户身份进行验证,"用户是否登录成功"
  • Authorization验证访问者的用户身份是否有对资源访问的额访问权限,"用户是否有权限访问这个地址"
  • 一个管理身份,一个管理权限,认证和鉴权授权

Identity框架使用

  • ldentityUser、IdentityRole,TKey代表主键的类型。我们一般编写继承自ldentityUser
    IdentityRole等的自定义类,可以增加自定义属性。
  • NuGet安装
    Microsoft.AspNetCore.Identity.EntityFrameworkCore.
  • 创建继承自ldentityDbContext的类
  • 可以通过IdDbContext类来操作数据库,不过框架中提供
    了RoleManager、UserManager等类来简化对数据库的操作。
  • 部分方法的返回值为Task类型,查看、
    讲解ldentityResult类型定义。
  1. User(用户):
    • 用户是系统中的一个实体,代表使用应用程序的个人或系统账户。
    • 用户通常具有一些属性,如用户名、电子邮件、密码、电话号码等。
    • 用户可以被分配一个或多个角色,这些角色决定了用户在系统中的权限。
  2. Role(角色):
    • 角色是一组权限的集合,用于定义用户可以执行的操作。
    • 角色可以简化权限管理,因为你可以为一组具有相似权限的用户分配相同的角色。
    • 角色通常用于实现基于角色的访问控制(RBAC,Role-Based Access Control)。

在Identity框架中,用户和角色之间的关系通常是多对多的。这意味着一个用户可以拥有多个角色,而一个角色也可以被多个用户所拥有。这种设计允许灵活地管理用户的权限。

代替Session的JWT

1、配置JWT节点,节点下创建SigningKey、ExpireSeconds两个配置项,分别代表JWT的密钥和过
期时间(单位:秒)。再创建配置类JWTOptions,包含SigningKey、ExpireSeconds两个属性。

Nuget: Microsoft.AspNetCore.Authentication.JwtBearer

对JWT进行配置

services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));
 
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(x =>{
    
    var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>();
    byte[] keyBytes =Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
    var seckey = new SymmetricSecurityKey(keyBytes);
    
    x.TokenValidationParameters = new(){
        
        Validatelssuer=false, ValidateAudience=false, ValidateLifetime=true,
		ValidatelssuerSigningKey=true, IssuerSigningKey=secKey
    }
});

jwt的应用写在项目上了。

ASP.NET的托管服务

  1. 场景,代码运行在后台。比如服务器启动的时候在后台预先加载数据到缓存,每天凌晨3点把数据导出到
    备份数据库,每隔5秒钟在两张表之间同步一次数据。

  2. 托管服务实现lHostedService接口,一般编写从BackgroundService继承的类。

  3. 3services.AddHostedService();

代码异常情况

  1. 从.NET 6开始,当托管服务中发生未处理异常的时候,程序就会自动停止并退出。可以把
    HostOptions.BackgroundServiceExceptionBehavior设置为lgnore,程序会忽略异常,而不是停止程序。不过推荐
    采用默认的设置,因为“异常应该被妥善的处理,而不是被忽略”。
  2. 要在ExecuteAsync方法中把代码用try …….. catch包裹起来,当发生异常的时候,记录日志中或发警报等。

托管代码的生命周期问题

  1. 托管服务是以单例的生命周期注册到依赖注入容器中的。因此不能注入生命周期为范围或者瞬态的服务。
    比如注入EF Core的上下文的话,程序就会抛出异常
  2. 可以通过构造方法注入一个IServiceScopeFactory服务,它可以用来创建一个IServiceScope对象,这样我们
    就可以通过IServiceScope来创建短生命周期的服务了。记得在Dispose中释放IServiceScope。

后台托管程序并不是长期运行了,需要有代码在其中长期支持

Hangfire一个定时任务的一个框架

对请求数据的校验

  • .NET Core中内置了对数据校验的支持,在System.ComponentModel.DataAnnotations这个命名空间下,比如[Required]、[EmailAddress]、[RegularExpression].
  • 使用此书写自定义的校验规则 CustomValidationAttribute,IValidatableObject.
  • 内置的校验机制的问题:校验规则都是和模型类耦合在一起,违反“单一职责原则”;很多常用的校验都需要编写自定义校验规则,而且写起来麻烦。

数据校验的分离机制

使用第三方的开源库FluentValidation

  • 用类似于EF Core中Fluent API的方式进行校验规则的配置,也就是我们可以把对模型类的校验放到单独的校验类中。

  • FluentValidation在ASP.NET Core项目中的用法

  • NuGet: FluentValidation.AspNetCore

    builder.Services.AddFluentValidation(fv => {
    	Assembly assembly = Assembly.GetExecutingAssembly();
    	fv.RegisterValidatorsFromAssembly(assembly);//
    	RegisterValidatorsFromAssemblies
    });
    

编写模型类Login2Request

public record Login2Request(string Email, string Password, string Password2);

编写继承自AbstractValidator的数据校验类

public class Login2RequestValidator: AbstractValidator<Login2Request>
{
    public Login2RequestValidator()
    {
        RuleFor(x=>x.Email).NotNull().EmailAddress()
			.Must(v=>v.EndsWith("@qq.com")| |v.EndsWith("@163.com"))
			.WithMessage("只支持QQ和163邮箱”);
		RuleFor(x=> x.Password).NotNull().Length(3, 10)
			.WithMessage("密码长度必须介于310之间”)
			.Equal(x=>x.Password2).WithMessage("两次密码必须一致”);		
    }

}

可以将FluentValidation进行注入执行

builder.Services.AddFluentValidation(opt=>{
    opt.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
});

WebSocket和SignalR

websocket:长连接,实时数据。基于TCP协议。

WebSocket独立于HTTP协议,不过我们一般仍然WebSocket服务器端部署到Web服务器上,因为可以借助HTTP协议完成初始的握手(可选),并且共享HTTP服务器的端口(主要)。

ASP.NET Core SignalR,是.net Core平台下对WebSocket的封装。

Hub(集线器),数据交换中心。

SingalR的基本使用

public class ChatRoomHub:Hub
{
    public Task SendPublicMessage(string message)
    {
          string connld = this.Context.Connectionld;
    	  string msg = $"{connld} {DateTime.Now}:{message}";
          return Clients.All.SendAsync("ReceivePublicMessage", msg);
    }
}
builder.Services.AddSignalR()

app.MapControllers()之前调

app.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub")

默认还要启用CORS。跨域

CORS跨域

  • 跨域通讯的问题。解决方案:JSONP、前端代理后端请求、CORS等。
  • CORS原理:在服务器的响应报文头中通过access-control-allow-origin告诉浏
    览器允许跨域访问的域名。
  • 在Program.cs的“var app=builder.Build()”这句代码之前注册
string[] urls = new[] { "http://localhost:3000" };
builder.Services.AddCors(options =>
    options.AddDefaultPolicy(builder => builder.WithOrigins(urls)
    .AllowAnyMethod().AllowAnyHeader().AllowCredentials()));
  • 在Program.cs的app.UseHttpsRedirection()这句代码之前增加一行app.UseCors()

前端代码

<script>
    import { reactive, onMounted } from 'vue';
    import * as signalR from '@microsoft/signalr';
    let connection;
    export default {name: 'Login',
    	setup() { const state = reactive({ userMessage: "", messages: [] });
    		const txtMsgOnkeypress = asyrk function (e) {
    			if (e.keyCode != 13) return;
    			await connection.invoke("SendPublicMessage", state.userMessage]; state.userMessage=""; };
    		onMounted(asyncfunction () {
    			connection= new signalR.HubConnectionBuilder()
    				.withUrl('https://localhost:7112/Hubs/ChatRoomHub")//一定要写全路径
                    .withAutomaticReconnect().build();
    			await connection.start();
   			    connection.on('ReceivePublicMessage', msg =>{
    				state.messages.push(msg);
        		});
     		});
    		return { state, txtMsgOnkeypress };
		},
    }
</script>

SingalR的协议协商

SignalR支持多种服务器推送方式:Websocket、Server-Sent Events、长轮询。默认按顺序尝试。

websocket和HTTP是不同的协议,为什么能用同一个端口。

协议协商的问题
1、集群中协议协商的问题:“协商”请求被服务器A处理,而接下来的WebSocket请求却被服务器B处理。
2、解决方法:粘性会话和禁用协商。
3、“粘性会话”(Sticky Session)会话保持:把来自同一个客户端的请求都转发给同一台服务器上。缺点:因为共享公网IP等造成请求无法被平均的分配到服务器集群;扩容的自适应性不强。
4、“禁用协商”:直接向服务器发出WebSocket请求。WebSocket连接一旦建立后,在客户端和服务器端直接就建立了持续的网络连接通道,在这个WebSocket连接中的后续往返WebSocket通信都是由同一台服务器来处理。缺点:无法降级到“服务器发送事件”或“长轮询”,不过不是大问题。

禁用协议协商的方式,在前端
const options = {skipNegotiation: true, transport:
signalR.HttpTransportType.WebSockets};
connection = new signalR.HubConnectionBuilder()
	.withUrl('https://localhost:7047/Hubs/ChatRoomHub',
options)
	.withAutomaticReconnect().build();

SingalR的分布式部署

2, 解决万案,所有服务参连接到同一个消息中间件。使用
粘性会话或者跳过协商(用websocket)
3、官方方案:Redis backplane。

NuGet:

Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR().AddStackExchangeRedis("127.0.0
.1", options => {
	options.Configuration.ChannelPrefix ="Test1_";
});

singalR的身份认证

验证过身份后,才能进行连接

第一种方式,运用JWT进行验证

  • 配置SigningKey.ExpireSeconds。

  • 创建配置类JWTOptions。

    收藏了博客,学习博客文章

SignalR向部分客户端发消息

筛选客户端

  1. 客户端筛选的3个参数:Connectionld、组和用户Id,三个维度(它对应ClaimTypes.Nameldentifier的Claim)。
  2. Hub的Groups属性为IGroupManager属性,可以对组成员进行管理。查看类型的成员。
  3. Hub的Clients属性为IHubCallerClients类型,可以对连接到当前集线器的客户端进行筛选。查看类型的成员。
  4. IClientProxy类型。无法知道具体有哪些客户端调用SendAsync()方法向筛选的客户端发送消息。
  5. 实现聊天室私聊。

注入IHubContext<>,在外部调用通讯时

IHubContext接口和Hub类有区别,因此在
HubContext中不能调用Caller、Others等成员,所以不
能向“当前连接的客户端”、“除了当前连接之外的其他
客户端”推送消息。
3、为什么?在控制器等集线器的外部调用的
HubContext服务,这些请求并不在一个SignalR连接中,
因此也就没有了“当前SignalR连接”的概念。

posted @   想要来杯咖啡吗  阅读(45)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示