由于自己菜鸡,只能任由社会摆布,自学一段时间JAVA想去找JAVA工作,突然一份.NET Core的工作摆在面前,还好有点学历,虽然C#已经不太记得,领导仍然放我进去了。开始的一段时间,各种摸项目,也能做点东西,但是仍然感觉有些东西不理解,刚好开了个微信读书会员,就在上面找点知识看吧,就看到了这本书,这里就记录一下初步学习的总结,都是比较基础的ASP .NET Core知识,下一次再记录DDD。后面发现这本书还有视频,而且视频讲得东西更多,后续也会把视频的东西总结一下,也可能是对该篇文章有所修改。
重难点
新语法
顶级语句
可以直接在C#文件中编写入口代码,不再需要声明类和方法
全局using
建立
Usings.cs
全局同意管理引用,使用global using System.Text.Json;
引用
simply using embeding
using会在程序结束关闭,或者使用大括号,括号结束using资源释放
using (var conn = new SqlConnection(conStr))
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "select * from form";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
}
}
}
}
||
||
||
||
||
\/
using var con = new SqlConnection(connStr);
con.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "select * from form";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
}
simply namespace to avoid embed
直接写namespace关键字避免命名空间嵌套,代码结构嵌套严重较难看
namespace MyNamespace;
Record
使用Record可以创建类,会重写
ToString,Equals
;可以使用with快速构建Record这种特殊类
#region Record
#region demo1
var p1 = new Person("a", "b");
var p2 = new Person("a", "b");
Console.WriteLine(p1);
Console.WriteLine(p1 == p2);
public record Person(string FirstName, string LastName);
#endregion
#region demo2
var u1 = new User("li", 25);
var u2 = new User("li", "56464654@qq.com", 18);
var u3 = u1 with { Email = "test_fjiort@qq.com", Age = 99 };
public record User(string UserName, string? Email, int Age)
{
public User(string userName, int age) : this(userName, null, age)
{
}
}
#endregion
#endregion
异步编程
异步编程提高服务器接待请求的数量,但不会使得单个请求处理效率变高,甚至可能略又降低
await\async
使用异步方法注意点:
- 返回值
Task<T>
- 方法一般以Async结尾
- 调用一般在方法签名加上
await
原理:
使用反编译工具查看代码
async方法会被C#编译器编译成一个类,并根据await调用把方法切分成多个状态,对async方法的调用就会被拆分成若干次对MoveNext方法的调用
线程切换
在异步方法进行await调用的等待期间,框架会把当前的线程返回给线程池,等异步方法调用执行完毕,框架会从线程池再去除一个线程,以执行后续的代码。
注意
- 异步方法不等于多线程。
- 如何使得代码再新线程执行呢,使用
Task.Run
- 如何使得代码再新线程执行呢,使用
- 异步方法不一定必须有async。
- 如果一个异步方法只是对别的异步方法进行简单的调用,并没有复杂的逻辑,比如获取异步方法的返回值后再进一步的处理,就可以去掉
asqyn\await
关键字
- 如果一个异步方法只是对别的异步方法进行简单的调用,并没有复杂的逻辑,比如获取异步方法的返回值后再进一步的处理,就可以去掉
- 建议开发人员只使用异步方法,因为这样能提升系统并发处理能力
- 若由于框架限制,我们编写的方法不能标注
async
- 那么可以在
Task<T>
类型对象调用Result属性来等待异步执行结束获取返回值 - 返回值为
Task
,可以在Task类型对象调用Wait方法来调用异步方法并等待任务执行结束
- 那么可以在
- 异步暂停使用
Thread.Sleep
- 异步方法中的
CancellationToken
对象让异步方法提前终止 - 可以使用
Task.WhenAll
等待多个Task的执行结束 - 接口中的方法或者抽象方法是不能修饰为
async
的
LINQ
- where
- Count
- Any
- OrderBy
- Skip/Take
- Max,Min,Avg,Sum
- GroupBy
- Select
- ToArry/ToList
.NET Core核心基础组件
依赖注入
什么是控制反转、服务定位器和依赖注入
控制反转就是把“创建和组装对象”操作的控制权从业务逻辑的new转移到框架中,这样业务代码只要说明我要A对象,框架就会帮助我们创建这个对象
控制反转的两种方式:
- 服务定位器:调用GetService方法就可以获取想要的对象
- 依赖注入:框架中有个自动为类的属性赋值的功能,只要代码声明需要什么类型对象,框架就会帮助我们创建这个对象
依赖注入生命周期
- 瞬态
- 范围
- 单例
NuGet包
- Microsoft.Extensions.DependencyInjection
使用
- 创建容器:new ServiceCollection
- 注册服务:services.AddXXX()
- 调用IServiceCollection的BuildServiceProvider方法创建一个ServiceProvider对象
- 调用ServiceProvider类的GetRequiredService()方法
配置系统
json
依赖
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.Configuration
使用方法
- new ConfigurationBuilder
- configurationBuilder.AddJsonFile()
- configurationBuilder.Build()
- config[“name”]
选项方式读取
IOptions不监听配置的改变,因此它的资源占用比较少,适用于对服务器启动后就不会改变的值进行读取。如果我们需要在程序运行中读取修改后的值,建议使用IOptionsSnapshot
- 构建模型类
- 配置读取配置的Demo类
private readonly IOptionsSnapshot<DbSettings> optDbSettings;
-
services.AddOptions() .Configure<DbSettings>(e => config.GetSection("DB").Bind(e)) .Configure<SmtpSettings>(e => config.GetSection("Smtp").Bind(e));
命令行读取配置
依赖
- Microsoft.Extensions.Configuration.CommandLine
使用
- AddCommandLine()
环境变量
依赖
- Microsoft.Extensions.Configuration.EnvironmentVariables
使用
- AddEnvironmentVariables
多配置问题
按照“后添加的配置提供程序中的配置覆盖之前的配置”的原则
日志
依赖
- Microsoft.Extensions.Logging
- 控制台输出日志 Microsoft.Extensions.Logging.Console
文件日志提供程序NLog
在生产环境中我们需要把日志写入存储介质的方式,比如写入文件。
常用的第三方日志提供程序有Log4Net
、NLog
、Serilog
。这里推荐使用NLog或者Serilog,因为它们不仅使用简单,而且功能强大。
集中式日志
在集群环境中,如果每台服务器都把日志写入本地的文件中,那么在对日志进行分析的时候,我们就需要逐个打开各台服务器的磁盘中的日志文件,这非常麻烦。因此,在分布式环境下,我们最好采用集中式的日志服务器,各台服务器都把产生的日志写入日志服务器。
推荐使用
- Exceptionless(C#)
- Exceptionless的开发人员主推它们的日志云服务,也就是不用自己搭建服务器,而是直接购买他们的云服务,然后程序直接把日志发送给他们的服务器即可,因此在Exceptionless的官网是看不到自己部署服务器的页面的,需要到它的GitHub开源页面去找文档的“Self Hosting”这一节。
- ELK(JAVA)
EF Core基础
OnConfiguring
连接配置
OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
CRUD
实体类配置
EF Core采用了“约定大于配置”的设计原则,也就是说EF Core会默认按照约定根据实体类以及DbContext的定义来实现和数据库表的映射配置,除非用户显式地指定了配置规则。
配置类
使用:
- 配置类继承 IEntityTypeConfiguration
1 class BookEntityConfig : IEntityTypeConfiguration<Book>
2 {
3 public void Configure(EntityTypeBuilder<Book> builder)
4 {
5 builder.ToTable("T_Books");
6 builder.Property(e => e.Title).HasMaxLength(50).IsRequired();
7 builder.Property(e => e.AuthorName).HasMaxLength(20).IsRequired();
8 }
9 }
Data Annotation配置
Fluent API配置
注意
- 推荐Fluent API配置
- 两种配置都用,Fluent API配置优先级高
关系配置
一对多、一对一、多对多
EF Core中实体类之间关系的配置采用如下的模式:HasXXX(…).WithYYY(…);
关联数据的获取
Include
关系的外键属性的配置
在关系配置中通过HasForeignKey()指定这个属性为外键可以不适用Include
单项导航属性
有时候我们不方便声明双向导航。比如在大部分系统中,基础的“用户”实体类会被非常多的其他实体类引用,这种单向导航属性的配置其实很简单,只要在WithMany方法中不指定属性即可
主键类型
自增long类型
优点
- 使用简单
- 所有主流数据库系统都内置了对自增列的支持
- 新插入的数据会由数据库自动赋予一个新增的、不重复的主键值
- 自增long类型占用磁盘空间小,可读性强
缺点:
- 自增long类型的主键在数据库迁移以及分布式系统(如分库分表、数据库集群)中使用起来比较麻烦,而且在高并发插入的时候性能比较差。
Guid算法
Guid算法使用网卡的MAC地址、时间戳等信息生成一个全球唯一的ID。由于Guid的全球唯一性,它适用于分布式系统,在进行多数据库数据合并的时候很方便,因此我们也可以用Guid类型作为主键。
注:
- 如果我们使用Guid类型(也就是uniqueidentifier类型)作为主键,一定不能把主键设置为聚集索引
- 在MySQL中,如果使用InnoDB引擎,并且数据插入频繁,那么一定不要用Guid类型作为主键
- 使用其他数据库管理系统的时候,也请先查阅在对应的数据库管理系统中,是否可以把主键设置为非聚集索引
数据迁移
迁移文件
- XXX.cs:记录的是和具体数据库无关的抽象模型
- XXX.Designer.cs:记录的是和具体数据库相关的代码
查看EFCore生成的SQL语句
我们只要在上下文的OnConfiguring方法中调用optionsBuilder类的LogTo方法,传递一个参数为String的委托即可。当相关日志输出的时候,对应的委托就会被执行
原理
IQueryable与IEnumerable
Enumerable类中定义的供普通集合用的Where等方法都是“客户端评估”,Queryable中定义的Where方法都是“服务的评估”
总结:
在使用EF Core的时候,为了避免“客户端评估”,我们要尽量调用IQueryable版本的方法,而不是直接调用IEnumerable版本的方法。
IQueryable的延迟执行
对于IQueryable接口,调用“非立即执行”方法的时候不会执行查询,而调用“立即执行”方法的时候则会立即执行查询。
判断方法:
一个方法是否是立即执行方法的简单方式是:一个方法的返回值类型如果是IQueryable类型,这个方法一般就是非立即执行方法,否则这个方法就是立即执行方法。
注:IQueryable是一个待查询的逻辑,因此它是可以被重复使用的
IQueryable的底层运行
IQueryable是用类似DataReader的方式读取查询结果的。DataReader会分批从数据库服务器读取数据。
优点是客户端内存占用小,缺点是如果遍历读取数据并进行处理的过程缓慢的话,会导致程序占用数据库连接的时间较长,从而降低数据库服务器的并发连接能力。因此,在遍历IQueryable的过程中,它需要占用一个数据库连接。
EF优化
AsNoTracking
如果开发人员能够确认通过上下文查询出来的对象只是用来展示,不会发生状态改变,那么可以使用AsNoTracking方法告诉IQueryable在查询的时候“禁用跟踪”
1 Book[] books = ctx.Books.AsNoTracking().Take(3).ToArray();
2 Book b1 = books[0];
3 b1.Title = "abc";
4 EntityEntry entry1 = ctx.Entry(b1);
5 Console.WriteLine(entry1.State);
上面代码的执行结果是“Detached”,也就说使用AsNoTracking查询出来的实体类是不被上下文跟踪的。
实体类状态跟踪
ctx.Entry(b1).State
并发控制
EF Core内置了使用并发令牌列实现的乐观并发控制,并发令牌列通常就是被并发操作影响的列。
例子:
我们可以把Owner列用作并发令牌列。在更新Owner列的时候,我们把Owner列更新前的值也放入Update语句的条件中,SQL语句如下:Update T_Houses set Owner=新值where Id=1 and Owner=旧值。
使用:
EF Core中,我们只要把被并发修改的属性使用IsConcurrencyToken设置为并发令牌即可。
表达式树
表达式树(expression tree)是用树形数据结构来表示代码逻辑运算的技术,它让我们可以在运行时访问逻辑运算的结构。表达式树在.NET中对应Expression <> 类型。
通过代码动态构建表达式树
- 安装NuGet包ExpressionTreeToString
- 在代码中添加对ExpressionTreeToString命名空间的引用
- 我们就可以在Expression类型上调用ToString扩展方法来输出表达式树结构的字符串了
- ExpressionTreeToString提供的ToString(“Object notation”,“C#”)方法只是输出一个用C#语法描述表达式树的结构及每个节点的字符串,但是这个字符串并不是可以直接运行的C#代码。
- 我们可以用C#的using static方法来静态引入Expression类
- 这样上面的代码就几乎可以直接放到C#代码中编译通过了
注:一般只有在编写不特定于某个实体类的通用框架的时候,由于无法在编译期确定要操作的类名、属性等,才需要编写动态构建表达式树的代码,否则为了提高代码的可读性和可维护性,我们要尽量避免动态构建表达式树。
ASP.NET Core基础组件
依赖注入
使用
- 注入代码在
Program.cs
文件中的var app=builder.Build()
代码之前 - 使用
builder.Services.AddXXX<xxxService>()
- 直接可在构造方法中注入服务
模块化的服务注入框架
- 依赖:Zack.Commons
- 创建Zack.Commons中的IModuleInitializer接口的实现类ModuleInitializer
Program.cs
添加var services = new ServiceCollection(); // 获取所有的用户程序集 var assemblies = ReflectionHelper.GetAllReferencedAssemblies(); // 扫描指定程序集中所有实现了IModuleInitialier接口的类 services.RunModuleInitializers(assemblies); using var sp = services.BuildServiceProvider(); var items = sp.GetServices<IMyService>(); foreach (var item in items) { item.SayHello(); }
EF Core与ASP.NET Core的集成
使用
- 我们尽量把上下文的数据库配置的代码写到ASP.NET Core项目中,也就是
context
只编写OnModelCreating
方法 Program.cs
中添加builder.Services.AddDbContext<MyDbContext>(opt=>{ var conStr = builder.Configuration.GetConnectionString("dEFAULT"); opt.UseMysql(conStr) });
缓存
客户端响应缓存
使用:
controller添加[ResponseCache(Duration=60)]
服务端响应缓存
使用:
Program.cs
中app.MapControllers
之前加上app.UseResponseCaching
内存缓存
使用:
Program.cs
的builder.Build
之前添加builder.Services.AddMemoryCache
来把内存缓存相关服务注册到依赖注入容器中。
var items = await memCache.GetOrCreateAsync("AllBooks", async (e) => {
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
logger.LogInformation("从数据库中读取数据");
return await dbCtx.Books.ToArrayAsync();
});
过期策略有“绝对过期时间”和“滑动过期时间”两种
缓存穿透问题的规避
如果有恶意访问者使用不存在的图书ID来发送大量的请求,这样的请求就会一直执行第8行查询数据库的代码,因此数据库就会承受非常大的压力,甚至可能会导致数据库服务器崩溃,这种问题就叫作缓存穿透。
解决:在日常开发中只要使用GetOrCreateAsync方法即可,因为这个方法会把null也当成合法的缓存值,这样就可以轻松规避缓存穿透的问题了
缓存雪崩问题的规避
如果数据缓存设置的过期时间都相同,到了过期时间的时候,缓存项会集中过期,因此又会导致大量的数据库请求,这样数据库服务器就会出现周期性的压力,这种陡增的压力甚至会把数据库服务器“压垮”(崩溃),当数据库服务器从崩溃中恢复后,这些压力又压了过来,从而造成数据库服务器反复崩溃、恢复,这就是数据库服务器的“雪崩”。
解决:写缓存时,在基础过期时间之上,再加一个随机的过期时间
缓存数据混乱的规避
使用
UserInfo
当缓存键,就会存在数据混乱问题
解决: 使用UserInfo
+UserId
使得缓存键唯一
注意事项
IQueryable、IEnumerable等类型可能存在延迟加载的问题,如果把这两种类型的变量指向的对象保存到内存缓存中,在把它们取出来再去执行的时候,如果它们延迟加载时需要的对象已经被释放,就会执行失败。
因此,这两种类型的变量指向的对象在保存到内存缓存之前,最好将其转换为数组或者List类型,从而强制数据立即加载。
分布式缓存
Redis
.NET Core中提供了统一的分布式缓存服务器的操作接口IDistributedCache,无论用什么类型的分布式缓存服务器,我们都可以统一使用IDistributedCache接口进行操作。
使用:
- 依赖:Microsoft.Extensions.Caching.StackExchangeRedis
Program.cs
的build
之前builder.Service.AddStackExchangeRedisCache(options=>{ options.Configuration="localhost"; // 前缀,避免和其他数据混淆 options.InstanceName="lyy_"; });
筛选器
ASP.N ET Core中的筛选器有以下5种类型:授权筛选器、资源筛选器、操作筛选器、异常筛选器和结果筛选器。
异常筛选器
- 继承IAsyncExceptionFilter
- Program.cs添加
builder.Services.Configure<MvcOptions>(options=>{ option.Filters.Add<MyExceptionFilter>(); });
- 注意:只有ASP.NET Core线程中的未处理异常才会被异常筛选器处理,后台线程中的异常不会被异常筛选器处理
操作筛选器
- 继承IAsyncActionFilter
- 实现OnActionExecutionAsync方法,其中,context参数代表Action执行的上下文对象,从context中我们可以获取请求的路径、参数值等信息;next参数代表下一个要执行的操作筛选器。
- 一个项目中可以注册多个操作筛选器,这些操作筛选器组成一个链,上一个筛选器执行完了再执行下一个。
中间件
中间件(middleware)是ASP.NET Core中的核心组件,ASP.NET Core MVC框架、响应缓存、用户身份验证、CORS、Swagger等重要的框架功能都是由ASP.NET内置的中间件提供的,我们也可以开发自定义的中间件来提供额外的功能。
每个中间件由前逻辑、next、后逻辑3部分组成,前逻辑为第一段要执行的逻辑代码,next为指向下一个中间件的调用,后逻辑为从下一个中间件返回所执行的逻辑代码。
要进行中间件的开发,我们需要先了解3个重要的概念:Map、Use和Run。Map用来定义一个管道可以处理哪些请求,Use和Run用来定义管道,一个管道由若干个Use和一个Run组成,每个Use引入一个中间件,而Run用来执行最终的核心应用逻辑。如下图所示。
中间件使用
-
Program添加
app.Map("/test", async appbuiler => { appbuiler.Use(async (context, next) => { context.Response.ContentType = "text/html"; await context.Response.WriteAsync("1 Start <br/>"); await next.Invoke(); await context.Response.WriteAsync("1 End<br/>"); }); appbuiler.Use(async (context, next) => { await context.Response.WriteAsync("2 Start <br/>"); await next.Invoke(); await context.Response.WriteAsync("2 End<br/>"); }); appbuiler.Run(async ctx => { await ctx.Response.WriteAsync("hello middleware <br/>"); }); });
自定义中间件
-
中间件类是一个普通的.NET类,它不需要继承任何父类或者实现任何接口
-
但是这个类需要有一个构造方法,构造方法至少要有一个RequestDelegate类型的参数,这个参数用来指向下一个中间件。
-
这个类还需要定义一个名字为Invoke或InvokeAsync的方法,方法中至少有一个HttpContext类型的参数,方法的返回值必须是Task类型。中间件类的构造方法和Invoke(或InvokeAsync)方法还可以定义其他参数,其他参数会通过依赖注入自动赋值。
-
在
Program
中使用appbuilder.UseMiddleware<Class>()
调用中间件
注意
- 中间件的组装顺序非常重要,在使用它们的时候一定要注意仔细阅读文档中关于中间件组装顺序的说明
高级组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o968hdaS-1665807763193)(./高级组件.png)]
Authentication与Authorization
标识框架使用EF Core对数据库进行操作,由于EF Core屏蔽了底层数据库的差异,因此标识框架支持几乎所有数据库。
标识框架中提供了IdentityUser<TKey>、IdentityRole<TKey>两个实体类型,我们一般编写继承自IdentityUser、IdentityRole等的自定义类。
使用:
-
NuGet安装Microsoft.AspNetCore.Identity.EntityFrameworkCore
-
编写分别继承自IdentityUser<long>、IdentityRole<long>的User类和Role类
-
创建继承自IdentityDbContext的类,这是一个EF Core中的上下文类,我们可以通过这个类操作数据库。IdentityDbContext是一个泛型类,有3个泛型参数,分别代表用户类型、角色类型和主键类型。
-
向依赖注入容器中注册与标识框架相关的服务,并且对相关的选项进行配置。
services.AddDbContext<IdDbContext>(opt => { string connStr = builder.Configuration.GetConnectionString("Default"); opt.UseSqlServer(connStr); }); services.AddDataProtection(); services.AddIdentityCore<User>(options => { options.Password.RequireDigit = false; options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; options.Password.RequiredLength = 6; options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider; options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; }); var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services); idBuilder.AddEntityFrameworkStores<IdDbContext>() .AddDefaultTokenProviders() .AddRoleManager<RoleManager<Role>>() .AddUserManager<UserManager<User>>();
-
通过执行Add-Migration、Update-database等命令执行EF Core的数据库迁移
-
编写控制器的代码。我们在控制器中需要对角色、用户进行操作
private readonly RoleManager<Role> roleManager; private readonly UserManager<User> userManager;
除了这些基本的用法之外,标识框架中还提供了多因素验证(短信验证、指纹验证等)、外部登录、重置密码等功能,官方文档中关于这些内容的介绍非常清晰。
JWT
Session:
-
实现用户登录功能的经典做法是用Session
-
也就是在用户登录验证成功后,服务器端生成唯一标识SessionId
-
服务器端不仅会把SessionId返回给浏览器端,还会把SessionId和登录用户的信息的对应关系保存到服务器的内存中
-
当浏览器端再次向服务器端发送请求的时候,浏览器端就在HTTP请求中携带SessionId,服务器端就可以根据SessionId从服务器的内存中取到用户的信息,这样就实现了用户登录的功能。
但是在分布式环境下,特别是在“前后端分离、多客户端”时代,Session暴露出很多缺点。
- 当登录用户量很大的时候,Session数据就会占用非常多的内存
- 而且无法支持分布式集群环境
- 如果Session数据保存到Redis等状态服务器中,它可以支持分布式集群环境,但是每遇到一次客户端请求都要向状态服务器获取一次Session数据,这会导致请求的响应速度变慢
在现在的项目开发中,我们倾向于采用JWT代替Session实现登录。JWT全称是JSON web token,从名字中可以看出,JWT是使用JSON格式来保存令牌信息的。
- 为了防止客户端的数据造假,保存在客户端的令牌经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器端收到客户端提交的令牌的时候都要检查一下签名,如果发现数据被篡改,则拒绝接收客户端提交的令牌。
JWT的结构如图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ny6saB8z-1665807763194)(./JWT%E7%BB%93%E6%9E%84%E5%9B%BE.png)]
JWT的头部(header)中保存的是加密算法的说明,负载(payload)中保存的是用户的ID、用户名、角色等信息,签名(signature)是根据头部和负载一起算出来的值。
JWT登录流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WyW0Ccqb-1665807763194)(./JWT%E7%99%BB%E5%BD%95%E6%B5%81%E7%A8%8B.png)]
使用:
- 安装NuGet包System.IdentityModel.Tokens.Jwt
- 编写生产JWT的程序
1 var claims = new List<Claim>(); 2 claims.Add(new Claim(ClaimTypes.NameIdentifier, "6")); 3 claims.Add(new Claim(ClaimTypes.Name, "yzk")); 4 claims.Add(new Claim(ClaimTypes.Role, "User")); 5 claims.Add(new Claim(ClaimTypes.Role, "Admin")); 6 claims.Add(new Claim("PassPort", "E90000082")); 7 string key = "fasdfad&9045dafz222#fadpio@0232"; 8 DateTime expires = DateTime.Now.AddDays(1); 9 byte[] secBytes = Encoding.UTF8.GetBytes(key); 10 var secKey = new SymmetricSecurityKey(secBytes); 11 var credentials = new SigningCredentials(secKey,SecurityAlgorithms.HmacSha256Signature); 12 var tokenDescriptor = new JwtSecurityToken(claims: claims, 13 expires: expires, signingCredentials: credentials); 14 string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); 15 Console.WriteLine(jwt);
- 调用JwtSecurityTokenHandler类对JWT进行解码,因为它会在对JWT解码前对签名进行校验
1 string jwt = Console.ReadLine()!; 2 string secKey = "fasdfad&9045dafz222#fadpio@0232"; 3 JwtSecurityTokenHandler tokenHandler = new(); 4 TokenValidationParameters valParam = new (); 5 var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secKey)); 6 valParam.IssuerSigningKey = securityKey; 7 valParam.ValidateIssuer = false; 8 valParam.ValidateAudience = false; 9 ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt, 10 valParam,out SecurityToken secToken); 11 foreach (var claim in claimsPrincipal.Claims) 12 { 13 Console.WriteLine($"{claim.Type}={claim.Value}"); 14 }
注意:
- 一定不要把不能被客户端知道的信息放到负载中。
- 当我们使用这个被篡改的JWT去运行代码8-11,程序运行时就会抛出内容为“Signature validation failed”的异常。
NET Core对于JWT的封装
- 当我们使用这个被篡改的JWT去运行代码8-11,程序运行时就会抛出内容为“Signature validation failed”的异常。
- NuGet为项目安装Microsoft.AspNetCore.Authentication.JwtBearer包
- 编写代码对JWT进行配置
1 services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT")); 2 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 3 .AddJwtBearer(x => 4 { 5 var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>(); 6 byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey); 7 var secKey = new SymmetricSecurityKey(keyBytes); 8 x.TokenValidationParameters = new() 9 { 10 ValidateIssuer=false, ValidateAudience=false, ValidateLifetime=true, 11 ValidateIssuerSigningKey=true, IssuerSigningKey=secKey 12 }; 13 });
- app.UseAuthorization之前添加app.UseAuthentication
- 类似上面生产JWT
- 在需要登录才能访问的控制器类上添加[Authorize]这个ASP.NET Core内置的Attribute
注:
- 在前端项目中,我们可以把令牌保存到Cookie、LocalStorage等位置,从而在后续请求中重复使用
- 而对于移动App、PC客户端,我们可以把令牌保存到配置文件中或者本地文件数据库中。
让Swagger中调试带验证的请求更简单
修改AddSwaggerGen()
1 builder.Services.AddSwaggerGen(c =>
2 {
3 var scheme = new OpenApiSecurityScheme()
4 {
5 Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'",
6 Reference = new OpenApiReference{Type = ReferenceType.SecurityScheme,
7 Id = "Authorization"},
8 Scheme = "oauth2",Name = "Authorization",
9 In = ParameterLocation.Header,Type = SecuritySchemeType.ApiKey,
10 };
11 c.AddSecurityDefinition("Authorization", scheme);
12 var requirement = new OpenApiSecurityRequirement();
13 requirement[scheme] = new List<string>();
14 c.AddSecurityRequirement(requirement);
15 });
解决JWT无法提前撤回的难题
JWT的缺点是:一旦JWT被发放给客户端,在有效期内这个令牌就一直有效,令牌是无法被提前撤回的。
解决思路是:在用户表中增加一个整数类型的列JWTVersion,它代表最后一次发放出去的令牌的版本号;每次登录、发放令牌的时候,我们都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT的负载中;当执行禁用用户、撤回用户的令牌等操作的时候,我们让这个用户对应的JWTVersion的值自增;当服务器端收到客户端提交的JWT后,先把JWT中的JWTVersion值和数据库中的JWTVersion值做比较,如果JWT中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT过期了,这样我们就实现了JWT的撤回机制。
JWT和Session比较
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fNlSjU4p-1665807763194)(./JWT%E5%92%8CSession%E6%AF%94%E8%BE%83.png)]
托管服务
使用:
- 继承
BackgroudService
托管服务中使用依赖注入的陷阱
-
托管服务是以单例的生命周期注册到依赖注入容器中的。
-
长生命周期的服务不能依赖短生命周期的服务,因此我们可以在托管服务中通过构造方法注入其他生命周期为单例的服务,但是不能注入生命周期为范围或者瞬态的服务。
-
我们可以通过构造方法注入IServiceScopeFactory服务,它可以用来创建IServiceScope对象,这样我们就可以通过IServiceScope来创建短生命周期的服务了
this.serviceScope = scopeFactory.CreateScope(); var sp = serviceScope.ServiceProvider; this.ctx = sp.GetRequiredService<TestDbContext>();
- services.AddHostedService()
- 由于IServiceScope继承了IDisposable接口,因此我们需要在托管服务的Dispose方法中销毁serviceScope。
请求数据校验
FluentValidation的基本使用
- 在项目中安装NuGet包FluentValidation.AspNetCore。
- 在Program.cs中添加注册相关服务的代码
1 builder.Services.AddFluentValidation(fv => { 2 Assembly assembly = Assembly.GetExecutingAssembly(); 3 fv.RegisterValidatorsFromAssembly(assembly); 4 });
- 编写一个模型类Request
- 编写一个继承自AbstractValidator的数据校验类
1 public class Login2RequestValidator: AbstractValidator<Login2Request> 2 { 3 public Login2RequestValidator() 4 { 5 RuleFor(x=>x.Email).NotNull().EmailAddress() 6 .Must(v=>v.EndsWith("@qq.com")||v.EndsWith("@163.com")) 7 .WithMessage("只支持QQ和163邮箱"); 8 RuleFor(x => x.Password).NotNull().Length(3, 10) 9 .WithMessage("密码长度必须介于3到10之间") 10 .Equal(x => x.Password2).WithMessage("两次密码必须一致"); 11 } 12 }
SignalR
ASP.NET Core SignalR(以下简称SignalR)是.NET Core平台中对WebSocket的封装,从而让开发人员可以更简单地进行WebSocket开发。
基本使用:
- ASP.NET Core SignalR(以下简称SignalR)是.NET Core平台中对WebSocket的封装,从而让开发人员可以更简单地进行WebSocket开发。
- 编写方法
- 编辑Program.cs,在builder.Build之前调用builder.Services.AddSignalR注册所有SignalR的服务,在app.MapControllers之前调用app.MapHub(“/Hubs/ChatRoomHub”)启用SignalR中间件,并且设置当客户端通过SignalR请求“/Hubs/ChatRoomHub”这个路径的时候,由ChatRoomHub进行处理。
从中心调用客户端方法
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
从客户端调用中心方法
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Dispatcher.Invoke(() =>
{
var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});
部署
我们在开发环境中运行的项目所加载的程序集是为了方便开发工具调试而生成的调试版程序集,运行效率并不高,因此我们不能直接把项目文件夹下bin/Debug中的程序集部署到生产环境的服务器上。我们应该创建网站的发布版,创建网站发布版的过程简称为“发布”。
两种部署模式:“框架独立”和“独立”
新语法
顶级语句
可以直接在C#文件中编写入口代码,不再需要声明类和方法
全局using
建立
Usings.cs
全局同意管理引用,使用global using System.Text.Json;
引用
simply using embeding
using会在程序结束关闭,或者使用大括号,括号结束using资源释放
using (var conn = new SqlConnection(conStr))
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "select * from form";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
}
}
}
}
||
||
||
||
||
\/
using var con = new SqlConnection(connStr);
con.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "select * from form";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
}
simply namespace to avoid embed
直接写namespace关键字避免命名空间嵌套,代码结构嵌套严重较难看
namespace MyNamespace;
Record
使用Record可以创建类,会重写
ToString,Equals
;可以使用with快速构建Record这种特殊类
#region Record
#region demo1
var p1 = new Person("a", "b");
var p2 = new Person("a", "b");
Console.WriteLine(p1);
Console.WriteLine(p1 == p2);
public record Person(string FirstName, string LastName);
#endregion
#region demo2
var u1 = new User("li", 25);
var u2 = new User("li", "56464654@qq.com", 18);
var u3 = u1 with { Email = "test_fjiort@qq.com", Age = 99 };
public record User(string UserName, string? Email, int Age)
{
public User(string userName, int age) : this(userName, null, age)
{
}
}
#endregion
#endregion
异步编程
异步编程提高服务器接待请求的数量,但不会使得单个请求处理效率变高,甚至可能略又降低
await\async
使用异步方法注意点:
- 返回值
Task<T>
- 方法一般以Async结尾
- 调用一般在方法签名加上
await
原理:
使用反编译工具查看代码
async方法会被C#编译器编译成一个类,并根据await调用把方法切分成多个状态,对async方法的调用就会被拆分成若干次对MoveNext方法的调用
线程切换
在异步方法进行await调用的等待期间,框架会把当前的线程返回给线程池,等异步方法调用执行完毕,框架会从线程池再去除一个线程,以执行后续的代码。
注意
- 异步方法不等于多线程。
- 如何使得代码再新线程执行呢,使用
Task.Run
- 如何使得代码再新线程执行呢,使用
- 异步方法不一定必须有async。
- 如果一个异步方法只是对别的异步方法进行简单的调用,并没有复杂的逻辑,比如获取异步方法的返回值后再进一步的处理,就可以去掉
asqyn\await
关键字
- 如果一个异步方法只是对别的异步方法进行简单的调用,并没有复杂的逻辑,比如获取异步方法的返回值后再进一步的处理,就可以去掉
- 建议开发人员只使用异步方法,因为这样能提升系统并发处理能力
- 若由于框架限制,我们编写的方法不能标注
async
- 那么可以在
Task<T>
类型对象调用Result属性来等待异步执行结束获取返回值 - 返回值为
Task
,可以在Task类型对象调用Wait方法来调用异步方法并等待任务执行结束
- 那么可以在
- 异步暂停使用
Thread.Sleep
- 异步方法中的
CancellationToken
对象让异步方法提前终止 - 可以使用
Task.WhenAll
等待多个Task的执行结束 - 接口中的方法或者抽象方法是不能修饰为
async
的
LINQ
- where
- Count
- Any
- OrderBy
- Skip/Take
- Max,Min,Avg,Sum
- GroupBy
- Select
- ToArry/ToList
.NET Core核心基础组件
依赖注入
什么是控制反转、服务定位器和依赖注入
控制反转就是把“创建和组装对象”操作的控制权从业务逻辑的new转移到框架中,这样业务代码只要说明我要A对象,框架就会帮助我们创建这个对象
控制反转的两种方式:
- 服务定位器:调用GetService方法就可以获取想要的对象
- 依赖注入:框架中有个自动为类的属性赋值的功能,只要代码声明需要什么类型对象,框架就会帮助我们创建这个对象
依赖注入生命周期
- 瞬态
- 范围
- 单例
NuGet包
- Microsoft.Extensions.DependencyInjection
使用
- 创建容器:new ServiceCollection
- 注册服务:services.AddXXX()
- 调用IServiceCollection的BuildServiceProvider方法创建一个ServiceProvider对象
- 调用ServiceProvider类的GetRequiredService()方法
配置系统
json
依赖
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.Configuration
使用方法
- new ConfigurationBuilder
- configurationBuilder.AddJsonFile()
- configurationBuilder.Build()
- config[“name”]
选项方式读取
IOptions不监听配置的改变,因此它的资源占用比较少,适用于对服务器启动后就不会改变的值进行读取。如果我们需要在程序运行中读取修改后的值,建议使用IOptionsSnapshot
- 构建模型类
- 配置读取配置的Demo类
private readonly IOptionsSnapshot<DbSettings> optDbSettings;
-
services.AddOptions() .Configure<DbSettings>(e => config.GetSection("DB").Bind(e)) .Configure<SmtpSettings>(e => config.GetSection("Smtp").Bind(e));
命令行读取配置
依赖
- Microsoft.Extensions.Configuration.CommandLine
使用
- AddCommandLine()
环境变量
依赖
- Microsoft.Extensions.Configuration.EnvironmentVariables
使用
- AddEnvironmentVariables
多配置问题
按照“后添加的配置提供程序中的配置覆盖之前的配置”的原则
日志
依赖
- Microsoft.Extensions.Logging
- 控制台输出日志 Microsoft.Extensions.Logging.Console
文件日志提供程序NLog
在生产环境中我们需要把日志写入存储介质的方式,比如写入文件。
常用的第三方日志提供程序有Log4Net
、NLog
、Serilog
。这里推荐使用NLog或者Serilog,因为它们不仅使用简单,而且功能强大。
集中式日志
在集群环境中,如果每台服务器都把日志写入本地的文件中,那么在对日志进行分析的时候,我们就需要逐个打开各台服务器的磁盘中的日志文件,这非常麻烦。因此,在分布式环境下,我们最好采用集中式的日志服务器,各台服务器都把产生的日志写入日志服务器。
推荐使用
- Exceptionless(C#)
- Exceptionless的开发人员主推它们的日志云服务,也就是不用自己搭建服务器,而是直接购买他们的云服务,然后程序直接把日志发送给他们的服务器即可,因此在Exceptionless的官网是看不到自己部署服务器的页面的,需要到它的GitHub开源页面去找文档的“Self Hosting”这一节。
- ELK(JAVA)
EF Core基础
OnConfiguring
连接配置
OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
CRUD
实体类配置
EF Core采用了“约定大于配置”的设计原则,也就是说EF Core会默认按照约定根据实体类以及DbContext的定义来实现和数据库表的映射配置,除非用户显式地指定了配置规则。
配置类
使用:
- 配置类继承 IEntityTypeConfiguration
1 class BookEntityConfig : IEntityTypeConfiguration<Book>
2 {
3 public void Configure(EntityTypeBuilder<Book> builder)
4 {
5 builder.ToTable("T_Books");
6 builder.Property(e => e.Title).HasMaxLength(50).IsRequired();
7 builder.Property(e => e.AuthorName).HasMaxLength(20).IsRequired();
8 }
9 }
Data Annotation配置
Fluent API配置
注意
- 推荐Fluent API配置
- 两种配置都用,Fluent API配置优先级高
关系配置
一对多、一对一、多对多
EF Core中实体类之间关系的配置采用如下的模式:HasXXX(…).WithYYY(…);
关联数据的获取
Include
关系的外键属性的配置
在关系配置中通过HasForeignKey()指定这个属性为外键可以不适用Include
单项导航属性
有时候我们不方便声明双向导航。比如在大部分系统中,基础的“用户”实体类会被非常多的其他实体类引用,这种单向导航属性的配置其实很简单,只要在WithMany方法中不指定属性即可
主键类型
自增long类型
优点
- 使用简单
- 所有主流数据库系统都内置了对自增列的支持
- 新插入的数据会由数据库自动赋予一个新增的、不重复的主键值
- 自增long类型占用磁盘空间小,可读性强
缺点:
- 自增long类型的主键在数据库迁移以及分布式系统(如分库分表、数据库集群)中使用起来比较麻烦,而且在高并发插入的时候性能比较差。
Guid算法
Guid算法使用网卡的MAC地址、时间戳等信息生成一个全球唯一的ID。由于Guid的全球唯一性,它适用于分布式系统,在进行多数据库数据合并的时候很方便,因此我们也可以用Guid类型作为主键。
注:
- 如果我们使用Guid类型(也就是uniqueidentifier类型)作为主键,一定不能把主键设置为聚集索引
- 在MySQL中,如果使用InnoDB引擎,并且数据插入频繁,那么一定不要用Guid类型作为主键
- 使用其他数据库管理系统的时候,也请先查阅在对应的数据库管理系统中,是否可以把主键设置为非聚集索引
数据迁移
迁移文件
- XXX.cs:记录的是和具体数据库无关的抽象模型
- XXX.Designer.cs:记录的是和具体数据库相关的代码
查看EFCore生成的SQL语句
我们只要在上下文的OnConfiguring方法中调用optionsBuilder类的LogTo方法,传递一个参数为String的委托即可。当相关日志输出的时候,对应的委托就会被执行
原理
IQueryable与IEnumerable
Enumerable类中定义的供普通集合用的Where等方法都是“客户端评估”,Queryable中定义的Where方法都是“服务的评估”
总结:
在使用EF Core的时候,为了避免“客户端评估”,我们要尽量调用IQueryable版本的方法,而不是直接调用IEnumerable版本的方法。
IQueryable的延迟执行
对于IQueryable接口,调用“非立即执行”方法的时候不会执行查询,而调用“立即执行”方法的时候则会立即执行查询。
判断方法:
一个方法是否是立即执行方法的简单方式是:一个方法的返回值类型如果是IQueryable类型,这个方法一般就是非立即执行方法,否则这个方法就是立即执行方法。
注:IQueryable是一个待查询的逻辑,因此它是可以被重复使用的
IQueryable的底层运行
IQueryable是用类似DataReader的方式读取查询结果的。DataReader会分批从数据库服务器读取数据。
优点是客户端内存占用小,缺点是如果遍历读取数据并进行处理的过程缓慢的话,会导致程序占用数据库连接的时间较长,从而降低数据库服务器的并发连接能力。因此,在遍历IQueryable的过程中,它需要占用一个数据库连接。
EF优化
AsNoTracking
如果开发人员能够确认通过上下文查询出来的对象只是用来展示,不会发生状态改变,那么可以使用AsNoTracking方法告诉IQueryable在查询的时候“禁用跟踪”
1 Book[] books = ctx.Books.AsNoTracking().Take(3).ToArray();
2 Book b1 = books[0];
3 b1.Title = "abc";
4 EntityEntry entry1 = ctx.Entry(b1);
5 Console.WriteLine(entry1.State);
上面代码的执行结果是“Detached”,也就说使用AsNoTracking查询出来的实体类是不被上下文跟踪的。
实体类状态跟踪
ctx.Entry(b1).State
并发控制
EF Core内置了使用并发令牌列实现的乐观并发控制,并发令牌列通常就是被并发操作影响的列。
例子:
我们可以把Owner列用作并发令牌列。在更新Owner列的时候,我们把Owner列更新前的值也放入Update语句的条件中,SQL语句如下:Update T_Houses set Owner=新值where Id=1 and Owner=旧值。
使用:
EF Core中,我们只要把被并发修改的属性使用IsConcurrencyToken设置为并发令牌即可。
表达式树
表达式树(expression tree)是用树形数据结构来表示代码逻辑运算的技术,它让我们可以在运行时访问逻辑运算的结构。表达式树在.NET中对应Expression <> 类型。
通过代码动态构建表达式树
- 安装NuGet包ExpressionTreeToString
- 在代码中添加对ExpressionTreeToString命名空间的引用
- 我们就可以在Expression类型上调用ToString扩展方法来输出表达式树结构的字符串了
- ExpressionTreeToString提供的ToString(“Object notation”,“C#”)方法只是输出一个用C#语法描述表达式树的结构及每个节点的字符串,但是这个字符串并不是可以直接运行的C#代码。
- 我们可以用C#的using static方法来静态引入Expression类
- 这样上面的代码就几乎可以直接放到C#代码中编译通过了
注:一般只有在编写不特定于某个实体类的通用框架的时候,由于无法在编译期确定要操作的类名、属性等,才需要编写动态构建表达式树的代码,否则为了提高代码的可读性和可维护性,我们要尽量避免动态构建表达式树。
ASP.NET Core基础组件
依赖注入
使用
- 注入代码在
Program.cs
文件中的var app=builder.Build()
代码之前 - 使用
builder.Services.AddXXX<xxxService>()
- 直接可在构造方法中注入服务
模块化的服务注入框架
- 依赖:Zack.Commons
- 创建Zack.Commons中的IModuleInitializer接口的实现类ModuleInitializer
Program.cs
添加var services = new ServiceCollection(); // 获取所有的用户程序集 var assemblies = ReflectionHelper.GetAllReferencedAssemblies(); // 扫描指定程序集中所有实现了IModuleInitialier接口的类 services.RunModuleInitializers(assemblies); using var sp = services.BuildServiceProvider(); var items = sp.GetServices<IMyService>(); foreach (var item in items) { item.SayHello(); }
EF Core与ASP.NET Core的集成
使用
- 我们尽量把上下文的数据库配置的代码写到ASP.NET Core项目中,也就是
context
只编写OnModelCreating
方法 Program.cs
中添加builder.Services.AddDbContext<MyDbContext>(opt=>{ var conStr = builder.Configuration.GetConnectionString("dEFAULT"); opt.UseMysql(conStr) });
缓存
客户端响应缓存
使用:
controller添加[ResponseCache(Duration=60)]
服务端响应缓存
使用:
Program.cs
中app.MapControllers
之前加上app.UseResponseCaching
内存缓存
使用:
Program.cs
的builder.Build
之前添加builder.Services.AddMemoryCache
来把内存缓存相关服务注册到依赖注入容器中。
var items = await memCache.GetOrCreateAsync("AllBooks", async (e) => {
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
logger.LogInformation("从数据库中读取数据");
return await dbCtx.Books.ToArrayAsync();
});
过期策略有“绝对过期时间”和“滑动过期时间”两种
缓存穿透问题的规避
如果有恶意访问者使用不存在的图书ID来发送大量的请求,这样的请求就会一直执行第8行查询数据库的代码,因此数据库就会承受非常大的压力,甚至可能会导致数据库服务器崩溃,这种问题就叫作缓存穿透。
解决:在日常开发中只要使用GetOrCreateAsync方法即可,因为这个方法会把null也当成合法的缓存值,这样就可以轻松规避缓存穿透的问题了
缓存雪崩问题的规避
如果数据缓存设置的过期时间都相同,到了过期时间的时候,缓存项会集中过期,因此又会导致大量的数据库请求,这样数据库服务器就会出现周期性的压力,这种陡增的压力甚至会把数据库服务器“压垮”(崩溃),当数据库服务器从崩溃中恢复后,这些压力又压了过来,从而造成数据库服务器反复崩溃、恢复,这就是数据库服务器的“雪崩”。
解决:写缓存时,在基础过期时间之上,再加一个随机的过期时间
缓存数据混乱的规避
使用
UserInfo
当缓存键,就会存在数据混乱问题
解决: 使用UserInfo
+UserId
使得缓存键唯一
注意事项
IQueryable、IEnumerable等类型可能存在延迟加载的问题,如果把这两种类型的变量指向的对象保存到内存缓存中,在把它们取出来再去执行的时候,如果它们延迟加载时需要的对象已经被释放,就会执行失败。
因此,这两种类型的变量指向的对象在保存到内存缓存之前,最好将其转换为数组或者List类型,从而强制数据立即加载。
分布式缓存
Redis
.NET Core中提供了统一的分布式缓存服务器的操作接口IDistributedCache,无论用什么类型的分布式缓存服务器,我们都可以统一使用IDistributedCache接口进行操作。
使用:
- 依赖:Microsoft.Extensions.Caching.StackExchangeRedis
Program.cs
的build
之前builder.Service.AddStackExchangeRedisCache(options=>{ options.Configuration="localhost"; // 前缀,避免和其他数据混淆 options.InstanceName="lyy_"; });
筛选器
ASP.N ET Core中的筛选器有以下5种类型:授权筛选器、资源筛选器、操作筛选器、异常筛选器和结果筛选器。
异常筛选器
- 继承IAsyncExceptionFilter
- Program.cs添加
builder.Services.Configure<MvcOptions>(options=>{ option.Filters.Add<MyExceptionFilter>(); });
- 注意:只有ASP.NET Core线程中的未处理异常才会被异常筛选器处理,后台线程中的异常不会被异常筛选器处理
操作筛选器
- 继承IAsyncActionFilter
- 实现OnActionExecutionAsync方法,其中,context参数代表Action执行的上下文对象,从context中我们可以获取请求的路径、参数值等信息;next参数代表下一个要执行的操作筛选器。
- 一个项目中可以注册多个操作筛选器,这些操作筛选器组成一个链,上一个筛选器执行完了再执行下一个。
中间件
中间件(middleware)是ASP.NET Core中的核心组件,ASP.NET Core MVC框架、响应缓存、用户身份验证、CORS、Swagger等重要的框架功能都是由ASP.NET内置的中间件提供的,我们也可以开发自定义的中间件来提供额外的功能。
每个中间件由前逻辑、next、后逻辑3部分组成,前逻辑为第一段要执行的逻辑代码,next为指向下一个中间件的调用,后逻辑为从下一个中间件返回所执行的逻辑代码。
要进行中间件的开发,我们需要先了解3个重要的概念:Map、Use和Run。Map用来定义一个管道可以处理哪些请求,Use和Run用来定义管道,一个管道由若干个Use和一个Run组成,每个Use引入一个中间件,而Run用来执行最终的核心应用逻辑。
中间件使用
-
Program添加
app.Map("/test", async appbuiler => { appbuiler.Use(async (context, next) => { context.Response.ContentType = "text/html"; await context.Response.WriteAsync("1 Start <br/>"); await next.Invoke(); await context.Response.WriteAsync("1 End<br/>"); }); appbuiler.Use(async (context, next) => { await context.Response.WriteAsync("2 Start <br/>"); await next.Invoke(); await context.Response.WriteAsync("2 End<br/>"); }); appbuiler.Run(async ctx => { await ctx.Response.WriteAsync("hello middleware <br/>"); }); });
自定义中间件
-
中间件类是一个普通的.NET类,它不需要继承任何父类或者实现任何接口
-
但是这个类需要有一个构造方法,构造方法至少要有一个RequestDelegate类型的参数,这个参数用来指向下一个中间件。
-
这个类还需要定义一个名字为Invoke或InvokeAsync的方法,方法中至少有一个HttpContext类型的参数,方法的返回值必须是Task类型。中间件类的构造方法和Invoke(或InvokeAsync)方法还可以定义其他参数,其他参数会通过依赖注入自动赋值。
-
在
Program
中使用appbuilder.UseMiddleware<Class>()
调用中间件
注意
- 中间件的组装顺序非常重要,在使用它们的时候一定要注意仔细阅读文档中关于中间件组装顺序的说明
高级组件
Authentication与Authorization
标识框架使用EF Core对数据库进行操作,由于EF Core屏蔽了底层数据库的差异,因此标识框架支持几乎所有数据库。
标识框架中提供了IdentityUser<TKey>、IdentityRole<TKey>两个实体类型,我们一般编写继承自IdentityUser、IdentityRole等的自定义类。
使用:
-
NuGet安装Microsoft.AspNetCore.Identity.EntityFrameworkCore
-
编写分别继承自IdentityUser<long>、IdentityRole<long>的User类和Role类
-
创建继承自IdentityDbContext的类,这是一个EF Core中的上下文类,我们可以通过这个类操作数据库。IdentityDbContext是一个泛型类,有3个泛型参数,分别代表用户类型、角色类型和主键类型。
-
向依赖注入容器中注册与标识框架相关的服务,并且对相关的选项进行配置。
services.AddDbContext<IdDbContext>(opt => { string connStr = builder.Configuration.GetConnectionString("Default"); opt.UseSqlServer(connStr); }); services.AddDataProtection(); services.AddIdentityCore<User>(options => { options.Password.RequireDigit = false; options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; options.Password.RequiredLength = 6; options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider; options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; }); var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services); idBuilder.AddEntityFrameworkStores<IdDbContext>() .AddDefaultTokenProviders() .AddRoleManager<RoleManager<Role>>() .AddUserManager<UserManager<User>>();
-
通过执行Add-Migration、Update-database等命令执行EF Core的数据库迁移
-
编写控制器的代码。我们在控制器中需要对角色、用户进行操作
private readonly RoleManager<Role> roleManager; private readonly UserManager<User> userManager;
除了这些基本的用法之外,标识框架中还提供了多因素验证(短信验证、指纹验证等)、外部登录、重置密码等功能,官方文档中关于这些内容的介绍非常清晰。
JWT
Session:
-
实现用户登录功能的经典做法是用Session
-
也就是在用户登录验证成功后,服务器端生成唯一标识SessionId
-
服务器端不仅会把SessionId返回给浏览器端,还会把SessionId和登录用户的信息的对应关系保存到服务器的内存中
-
当浏览器端再次向服务器端发送请求的时候,浏览器端就在HTTP请求中携带SessionId,服务器端就可以根据SessionId从服务器的内存中取到用户的信息,这样就实现了用户登录的功能。
但是在分布式环境下,特别是在“前后端分离、多客户端”时代,Session暴露出很多缺点。
- 当登录用户量很大的时候,Session数据就会占用非常多的内存
- 而且无法支持分布式集群环境
- 如果Session数据保存到Redis等状态服务器中,它可以支持分布式集群环境,但是每遇到一次客户端请求都要向状态服务器获取一次Session数据,这会导致请求的响应速度变慢
在现在的项目开发中,我们倾向于采用JWT代替Session实现登录。JWT全称是JSON web token,从名字中可以看出,JWT是使用JSON格式来保存令牌信息的。
- 为了防止客户端的数据造假,保存在客户端的令牌经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器端收到客户端提交的令牌的时候都要检查一下签名,如果发现数据被篡改,则拒绝接收客户端提交的令牌。
JWT的结构如图
JWT的头部(header)中保存的是加密算法的说明,负载(payload)中保存的是用户的ID、用户名、角色等信息,签名(signature)是根据头部和负载一起算出来的值。
JWT登录流程
使用:
- 安装NuGet包System.IdentityModel.Tokens.Jwt
- 编写生产JWT的程序
1 var claims = new List<Claim>(); 2 claims.Add(new Claim(ClaimTypes.NameIdentifier, "6")); 3 claims.Add(new Claim(ClaimTypes.Name, "yzk")); 4 claims.Add(new Claim(ClaimTypes.Role, "User")); 5 claims.Add(new Claim(ClaimTypes.Role, "Admin")); 6 claims.Add(new Claim("PassPort", "E90000082")); 7 string key = "fasdfad&9045dafz222#fadpio@0232"; 8 DateTime expires = DateTime.Now.AddDays(1); 9 byte[] secBytes = Encoding.UTF8.GetBytes(key); 10 var secKey = new SymmetricSecurityKey(secBytes); 11 var credentials = new SigningCredentials(secKey,SecurityAlgorithms.HmacSha256Signature); 12 var tokenDescriptor = new JwtSecurityToken(claims: claims, 13 expires: expires, signingCredentials: credentials); 14 string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); 15 Console.WriteLine(jwt);
- 调用JwtSecurityTokenHandler类对JWT进行解码,因为它会在对JWT解码前对签名进行校验
1 string jwt = Console.ReadLine()!; 2 string secKey = "fasdfad&9045dafz222#fadpio@0232"; 3 JwtSecurityTokenHandler tokenHandler = new(); 4 TokenValidationParameters valParam = new (); 5 var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secKey)); 6 valParam.IssuerSigningKey = securityKey; 7 valParam.ValidateIssuer = false; 8 valParam.ValidateAudience = false; 9 ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt, 10 valParam,out SecurityToken secToken); 11 foreach (var claim in claimsPrincipal.Claims) 12 { 13 Console.WriteLine($"{claim.Type}={claim.Value}"); 14 }
注意:
- 一定不要把不能被客户端知道的信息放到负载中。
- 当我们使用这个被篡改的JWT去运行代码8-11,程序运行时就会抛出内容为“Signature validation failed”的异常。
NET Core对于JWT的封装
- 当我们使用这个被篡改的JWT去运行代码8-11,程序运行时就会抛出内容为“Signature validation failed”的异常。
- NuGet为项目安装Microsoft.AspNetCore.Authentication.JwtBearer包
- 编写代码对JWT进行配置
1 services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT")); 2 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 3 .AddJwtBearer(x => 4 { 5 var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>(); 6 byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey); 7 var secKey = new SymmetricSecurityKey(keyBytes); 8 x.TokenValidationParameters = new() 9 { 10 ValidateIssuer=false, ValidateAudience=false, ValidateLifetime=true, 11 ValidateIssuerSigningKey=true, IssuerSigningKey=secKey 12 }; 13 });
- app.UseAuthorization之前添加app.UseAuthentication
- 类似上面生产JWT
- 在需要登录才能访问的控制器类上添加[Authorize]这个ASP.NET Core内置的Attribute
注:
- 在前端项目中,我们可以把令牌保存到Cookie、LocalStorage等位置,从而在后续请求中重复使用
- 而对于移动App、PC客户端,我们可以把令牌保存到配置文件中或者本地文件数据库中。
让Swagger中调试带验证的请求更简单
修改AddSwaggerGen()
1 builder.Services.AddSwaggerGen(c =>
2 {
3 var scheme = new OpenApiSecurityScheme()
4 {
5 Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'",
6 Reference = new OpenApiReference{Type = ReferenceType.SecurityScheme,
7 Id = "Authorization"},
8 Scheme = "oauth2",Name = "Authorization",
9 In = ParameterLocation.Header,Type = SecuritySchemeType.ApiKey,
10 };
11 c.AddSecurityDefinition("Authorization", scheme);
12 var requirement = new OpenApiSecurityRequirement();
13 requirement[scheme] = new List<string>();
14 c.AddSecurityRequirement(requirement);
15 });
解决JWT无法提前撤回的难题
JWT的缺点是:一旦JWT被发放给客户端,在有效期内这个令牌就一直有效,令牌是无法被提前撤回的。
解决思路是:在用户表中增加一个整数类型的列JWTVersion,它代表最后一次发放出去的令牌的版本号;每次登录、发放令牌的时候,我们都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT的负载中;当执行禁用用户、撤回用户的令牌等操作的时候,我们让这个用户对应的JWTVersion的值自增;当服务器端收到客户端提交的JWT后,先把JWT中的JWTVersion值和数据库中的JWTVersion值做比较,如果JWT中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT过期了,这样我们就实现了JWT的撤回机制。
JWT和Session比较
托管服务
使用:
- 继承
BackgroudService
托管服务中使用依赖注入的陷阱
-
托管服务是以单例的生命周期注册到依赖注入容器中的。
-
长生命周期的服务不能依赖短生命周期的服务,因此我们可以在托管服务中通过构造方法注入其他生命周期为单例的服务,但是不能注入生命周期为范围或者瞬态的服务。
-
我们可以通过构造方法注入IServiceScopeFactory服务,它可以用来创建IServiceScope对象,这样我们就可以通过IServiceScope来创建短生命周期的服务了
this.serviceScope = scopeFactory.CreateScope(); var sp = serviceScope.ServiceProvider; this.ctx = sp.GetRequiredService<TestDbContext>();
- services.AddHostedService()
- 由于IServiceScope继承了IDisposable接口,因此我们需要在托管服务的Dispose方法中销毁serviceScope。
请求数据校验
FluentValidation的基本使用
- 在项目中安装NuGet包FluentValidation.AspNetCore。
- 在Program.cs中添加注册相关服务的代码
1 builder.Services.AddFluentValidation(fv => { 2 Assembly assembly = Assembly.GetExecutingAssembly(); 3 fv.RegisterValidatorsFromAssembly(assembly); 4 });
- 编写一个模型类Request
- 编写一个继承自AbstractValidator的数据校验类
1 public class Login2RequestValidator: AbstractValidator<Login2Request> 2 { 3 public Login2RequestValidator() 4 { 5 RuleFor(x=>x.Email).NotNull().EmailAddress() 6 .Must(v=>v.EndsWith("@qq.com")||v.EndsWith("@163.com")) 7 .WithMessage("只支持QQ和163邮箱"); 8 RuleFor(x => x.Password).NotNull().Length(3, 10) 9 .WithMessage("密码长度必须介于3到10之间") 10 .Equal(x => x.Password2).WithMessage("两次密码必须一致"); 11 } 12 }
SignalR
ASP.NET Core SignalR(以下简称SignalR)是.NET Core平台中对WebSocket的封装,从而让开发人员可以更简单地进行WebSocket开发。
基本使用:
- ASP.NET Core SignalR(以下简称SignalR)是.NET Core平台中对WebSocket的封装,从而让开发人员可以更简单地进行WebSocket开发。
- 编写方法
- 编辑Program.cs,在builder.Build之前调用builder.Services.AddSignalR注册所有SignalR的服务,在app.MapControllers之前调用app.MapHub(“/Hubs/ChatRoomHub”)启用SignalR中间件,并且设置当客户端通过SignalR请求“/Hubs/ChatRoomHub”这个路径的时候,由ChatRoomHub进行处理。
从中心调用客户端方法
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
从客户端调用中心方法
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Dispatcher.Invoke(() =>
{
var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});
部署
我们在开发环境中运行的项目所加载的程序集是为了方便开发工具调试而生成的调试版程序集,运行效率并不高,因此我们不能直接把项目文件夹下bin/Debug中的程序集部署到生产环境的服务器上。我们应该创建网站的发布版,创建网站发布版的过程简称为“发布”。
两种部署模式:“框架独立”和“独立”