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 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配置
- 建立类库项目,放置实体类,DbContext, 配置类等DbContext中不配置数据库连接,而是为DbContext增加一个DbContextOptons类型的构造函数。
- EFCore项目安装对应数据库的EFCore Provider
- asp.net core项目引用EFCore项目,并且通过AddDbContext来注入DbContext及DbContext进行配置。
- Controller中就可以注入 DbContext类使用了。
- 让开发环境的Add-Migration知道连接哪个数据库
- 在efcore项目中,创建了一个实现了IDesignTimeDbContextTactory接口的类。并且在CreatDbContext返回一个连接开发数据库的DbContext。
- 在这里必须把连接字符串配置到Windows的正式环境变量中,然后再Environment.GetEnvironmentVariable读取。
- 正常执行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等都是内置中间件。
什么是中间件
- 广义上来讲:Tomcat、WebLogic、Redis、IS;狭义上来讲,ASP.NET Core中的中间件指ASP.NET Core中的一个组件。
- 中间件由前逻辑、next、后逻辑3部分组成,前逻辑为第一段要执行的逻辑代码、next为指向下一个中间件的调用、后逻辑为从下一个中间件执行返回所执行的逻辑代码。每个HTTP请求都要经历一系列中间件的处理,每个中间件对于请求进行特定的处理后,再转到下一个中间件,最终的业务逻辑代码执行完成后,响应的内容也会按照处理的相反顺序进行处理,然后形成HTTP响应报文返回给客户端。
- 中间件组成一个管道,整个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类型定义。
- User(用户):
- 用户是系统中的一个实体,代表使用应用程序的个人或系统账户。
- 用户通常具有一些属性,如用户名、电子邮件、密码、电话号码等。
- 用户可以被分配一个或多个角色,这些角色决定了用户在系统中的权限。
- 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的托管服务
-
场景,代码运行在后台。比如服务器启动的时候在后台预先加载数据到缓存,每天凌晨3点把数据导出到
备份数据库,每隔5秒钟在两张表之间同步一次数据。 -
托管服务实现lHostedService接口,一般编写从BackgroundService继承的类。
-
3services.AddHostedService
();
代码异常情况
- 从.NET 6开始,当托管服务中发生未处理异常的时候,程序就会自动停止并退出。可以把
HostOptions.BackgroundServiceExceptionBehavior设置为lgnore,程序会忽略异常,而不是停止程序。不过推荐
采用默认的设置,因为“异常应该被妥善的处理,而不是被忽略”。 - 要在ExecuteAsync方法中把代码用try …….. catch包裹起来,当发生异常的时候,记录日志中或发警报等。
托管代码的生命周期问题
- 托管服务是以单例的生命周期注册到依赖注入容器中的。因此不能注入生命周期为范围或者瞬态的服务。
比如注入EF Core的上下文的话,程序就会抛出异常 - 可以通过构造方法注入一个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("密码长度必须介于3到10之间”)
.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向部分客户端发消息
筛选客户端
- 客户端筛选的3个参数:Connectionld、组和用户Id,三个维度(它对应ClaimTypes.Nameldentifier的Claim)。
- Hub的Groups属性为IGroupManager属性,可以对组成员进行管理。查看类型的成员。
- Hub的Clients属性为IHubCallerClients类型,可以对连接到当前集线器的客户端进行筛选。查看类型的成员。
- IClientProxy类型。无法知道具体有哪些客户端调用SendAsync()方法向筛选的客户端发送消息。
- 实现聊天室私聊。
注入IHubContext<>,在外部调用通讯时
IHubContext接口和Hub类有区别,因此在
HubContext中不能调用Caller、Others等成员,所以不
能向“当前连接的客户端”、“除了当前连接之外的其他
客户端”推送消息。
3、为什么?在控制器等集线器的外部调用的
HubContext服务,这些请求并不在一个SignalR连接中,
因此也就没有了“当前SignalR连接”的概念。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?