由于自己菜鸡,只能任由社会摆布,自学一段时间JAVA想去找JAVA工作,突然一份.NET Core的工作摆在面前,还好有点学历,虽然C#已经不太记得,领导仍然放我进去了。开始的一段时间,各种摸项目,也能做点东西,但是仍然感觉有些东西不理解,刚好开了个微信读书会员,就在上面找点知识看吧,就看到了这本书,这里就记录一下初步学习的总结,都是比较基础的ASP .NET Core知识,下一次再记录DDD。后面发现这本书还有视频,而且视频讲得东西更多,后续也会把视频的东西总结一下,也可能是对该篇文章有所修改。
可以直接在C#文件中编写入口代码,不再需要声明类和方法
建立
Usings.cs
全局同意管理引用,使用global using System.Text.Json;
引用
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())
{
}
直接写namespace关键字避免命名空间嵌套,代码结构嵌套严重较难看
namespace MyNamespace;
使用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", "[email protected]", 18);
var u3 = u1 with { Email = "[email protected]", Age = 99 };
public record User(string UserName, string? Email, int Age)
{
public User(string userName, int age) : this(userName, null, age)
{
}
}
#endregion
#endregion
异步编程提高服务器接待请求的数量,但不会使得单个请求处理效率变高,甚至可能略又降低
使用异步方法注意点:
Task
await
原理:
使用反编译工具查看代码
async方法会被C#编译器编译成一个类,并根据await调用把方法切分成多个状态,对async方法的调用就会被拆分成若干次对MoveNext方法的调用
在异步方法进行await调用的等待期间,框架会把当前的线程返回给线程池,等异步方法调用执行完毕,框架会从线程池再去除一个线程,以执行后续的代码。
Task.Run
asqyn\await
关键字async
Task
类型对象调用Result属性来等待异步执行结束获取返回值Task
,可以在Task类型对象调用Wait方法来调用异步方法并等待任务执行结束Thread.Sleep
CancellationToken
对象让异步方法提前终止Task.WhenAll
等待多个Task的执行结束async
的控制反转就是把“创建和组装对象”操作的控制权从业务逻辑的new转移到框架中,这样业务代码只要说明我要A对象,框架就会帮助我们创建这个对象
控制反转的两种方式:
IOptions不监听配置的改变,因此它的资源占用比较少,适用于对服务器启动后就不会改变的值进行读取。如果我们需要在程序运行中读取修改后的值,建议使用IOptionsSnapshot
private readonly IOptionsSnapshot optDbSettings;
services.AddOptions()
.Configure(e => config.GetSection("DB").Bind(e))
.Configure(e => config.GetSection("Smtp").Bind(e));
按照“后添加的配置提供程序中的配置覆盖之前的配置”的原则
在生产环境中我们需要把日志写入存储介质的方式,比如写入文件。
常用的第三方日志提供程序有Log4Net
、NLog
、Serilog
。这里推荐使用NLog或者Serilog,因为它们不仅使用简单,而且功能强大。
在集群环境中,如果每台服务器都把日志写入本地的文件中,那么在对日志进行分析的时候,我们就需要逐个打开各台服务器的磁盘中的日志文件,这非常麻烦。因此,在分布式环境下,我们最好采用集中式的日志服务器,各台服务器都把产生的日志写入日志服务器。
连接配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
EF Core采用了“约定大于配置”的设计原则,也就是说EF Core会默认按照约定根据实体类以及DbContext的定义来实现和数据库表的映射配置,除非用户显式地指定了配置规则。
使用:
1 class BookEntityConfig : IEntityTypeConfiguration
2 {
3 public void Configure(EntityTypeBuilder 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 }
注意
EF Core中实体类之间关系的配置采用如下的模式:HasXXX(…).WithYYY(…);
Include
在关系配置中通过HasForeignKey()指定这个属性为外键可以不适用Include
有时候我们不方便声明双向导航。比如在大部分系统中,基础的“用户”实体类会被非常多的其他实体类引用,这种单向导航属性的配置其实很简单,只要在WithMany方法中不指定属性即可
优点
缺点:
Guid算法使用网卡的MAC地址、时间戳等信息生成一个全球唯一的ID。由于Guid的全球唯一性,它适用于分布式系统,在进行多数据库数据合并的时候很方便,因此我们也可以用Guid类型作为主键。
注:
我们只要在上下文的OnConfiguring方法中调用optionsBuilder类的LogTo方法,传递一个参数为String的委托即可。当相关日志输出的时候,对应的委托就会被执行
Enumerable类中定义的供普通集合用的Where等方法都是“客户端评估”,Queryable中定义的Where方法都是“服务的评估”
总结:
在使用EF Core的时候,为了避免“客户端评估”,我们要尽量调用IQueryable版本的方法,而不是直接调用IEnumerable版本的方法。
对于IQueryable接口,调用“非立即执行”方法的时候不会执行查询,而调用“立即执行”方法的时候则会立即执行查询。
判断方法:
一个方法是否是立即执行方法的简单方式是:一个方法的返回值类型如果是IQueryable类型,这个方法一般就是非立即执行方法,否则这个方法就是立即执行方法。
注:IQueryable是一个待查询的逻辑,因此它是可以被重复使用的
IQueryable是用类似DataReader的方式读取查询结果的。DataReader会分批从数据库服务器读取数据。
优点是客户端内存占用小,缺点是如果遍历读取数据并进行处理的过程缓慢的话,会导致程序占用数据库连接的时间较长,从而降低数据库服务器的并发连接能力。因此,在遍历IQueryable的过程中,它需要占用一个数据库连接。
如果开发人员能够确认通过上下文查询出来的对象只是用来展示,不会发生状态改变,那么可以使用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 <> 类型。
注:一般只有在编写不特定于某个实体类的通用框架的时候,由于无法在编译期确定要操作的类名、属性等,才需要编写动态构建表达式树的代码,否则为了提高代码的可读性和可维护性,我们要尽量避免动态构建表达式树。
Program.cs
文件中的var app=builder.Build()
代码之前builder.Services.AddXXX()
Program.cs
添加var services = new ServiceCollection();
// 获取所有的用户程序集
var assemblies = ReflectionHelper.GetAllReferencedAssemblies();
// 扫描指定程序集中所有实现了IModuleInitialier接口的类
services.RunModuleInitializers(assemblies);
using var sp = services.BuildServiceProvider();
var items = sp.GetServices();
foreach (var item in items)
{
item.SayHello();
}
context
只编写OnModelCreating
方法Program.cs
中添加builder.Services.AddDbContext(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接口进行操作。
使用:
Program.cs
的build
之前builder.Service.AddStackExchangeRedisCache(options=>{
options.Configuration="localhost";
// 前缀,避免和其他数据混淆
options.InstanceName="lyy_";
});
ASP.N ET Core中的筛选器有以下5种类型:授权筛选器、资源筛选器、操作筛选器、异常筛选器和结果筛选器。
builder.Services.Configure(options=>{
option.Filters.Add();
});
中间件(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
");
await next.Invoke();
await context.Response.WriteAsync("1 End
");
});
appbuiler.Use(async (context, next) =>
{
await context.Response.WriteAsync("2 Start
");
await next.Invoke();
await context.Response.WriteAsync("2 End
");
});
appbuiler.Run(async ctx =>
{
await ctx.Response.WriteAsync("hello middleware
");
});
});
中间件类是一个普通的.NET类,它不需要继承任何父类或者实现任何接口
但是这个类需要有一个构造方法,构造方法至少要有一个RequestDelegate类型的参数,这个参数用来指向下一个中间件。
这个类还需要定义一个名字为Invoke或InvokeAsync的方法,方法中至少有一个HttpContext类型的参数,方法的返回值必须是Task类型。中间件类的构造方法和Invoke(或InvokeAsync)方法还可以定义其他参数,其他参数会通过依赖注入自动赋值。
在Program
中使用appbuilder.UseMiddleware
调用中间件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o968hdaS-1665807763193)(./高级组件.png)]
标识框架使用EF Core对数据库进行操作,由于EF Core屏蔽了底层数据库的差异,因此标识框架支持几乎所有数据库。
标识框架中提供了IdentityUser
使用:
NuGet安装Microsoft.AspNetCore.Identity.EntityFrameworkCore
编写分别继承自IdentityUser
创建继承自IdentityDbContext的类,这是一个EF Core中的上下文类,我们可以通过这个类操作数据库。IdentityDbContext是一个泛型类,有3个泛型参数,分别代表用户类型、角色类型和主键类型。
向依赖注入容器中注册与标识框架相关的服务,并且对相关的选项进行配置。
services.AddDbContext(opt => {
string connStr = builder.Configuration.GetConnectionString("Default");
opt.UseSqlServer(connStr);
});
services.AddDataProtection();
services.AddIdentityCore(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()
.AddDefaultTokenProviders()
.AddRoleManager>()
.AddUserManager>();
通过执行Add-Migration、Update-database等命令执行EF Core的数据库迁移
编写控制器的代码。我们在控制器中需要对角色、用户进行操作
private readonly RoleManager roleManager;
private readonly UserManager userManager;
除了这些基本的用法之外,标识框架中还提供了多因素验证(短信验证、指纹验证等)、外部登录、重置密码等功能,官方文档中关于这些内容的介绍非常清晰。
Session:
实现用户登录功能的经典做法是用Session
也就是在用户登录验证成功后,服务器端生成唯一标识SessionId
服务器端不仅会把SessionId返回给浏览器端,还会把SessionId和登录用户的信息的对应关系保存到服务器的内存中
当浏览器端再次向服务器端发送请求的时候,浏览器端就在HTTP请求中携带SessionId,服务器端就可以根据SessionId从服务器的内存中取到用户的信息,这样就实现了用户登录的功能。
但是在分布式环境下,特别是在“前后端分离、多客户端”时代,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)]
使用:
1 var claims = new List();
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);
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 }
注意:
NET Core对于JWT的封装
1 services.Configure(builder.Configuration.GetSection("JWT"));
2 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
3 .AddJwtBearer(x =>
4 {
5 var jwtOpt = builder.Configuration.GetSection("JWT").Get();
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 });
注:
修改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();
14 c.AddSecurityRequirement(requirement);
15 });
JWT的缺点是:一旦JWT被发放给客户端,在有效期内这个令牌就一直有效,令牌是无法被提前撤回的。
解决思路是:在用户表中增加一个整数类型的列JWTVersion,它代表最后一次发放出去的令牌的版本号;每次登录、发放令牌的时候,我们都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT的负载中;当执行禁用用户、撤回用户的令牌等操作的时候,我们让这个用户对应的JWTVersion的值自增;当服务器端收到客户端提交的JWT后,先把JWT中的JWTVersion值和数据库中的JWTVersion值做比较,如果JWT中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT过期了,这样我们就实现了JWT的撤回机制。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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();
1 builder.Services.AddFluentValidation(fv => {
2 Assembly assembly = Assembly.GetExecutingAssembly();
3 fv.RegisterValidatorsFromAssembly(assembly);
4 });
1 public class Login2RequestValidator: AbstractValidator
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 }
ASP.NET Core SignalR(以下简称SignalR)是.NET Core平台中对WebSocket的封装,从而让开发人员可以更简单地进行WebSocket开发。
基本使用:
从中心调用客户端方法
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
从客户端调用中心方法
connection.On("ReceiveMessage", (user, message) =>
{
this.Dispatcher.Invoke(() =>
{
var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});
我们在开发环境中运行的项目所加载的程序集是为了方便开发工具调试而生成的调试版程序集,运行效率并不高,因此我们不能直接把项目文件夹下bin/Debug中的程序集部署到生产环境的服务器上。我们应该创建网站的发布版,创建网站发布版的过程简称为“发布”。
两种部署模式:“框架独立”和“独立”
可以直接在C#文件中编写入口代码,不再需要声明类和方法
建立
Usings.cs
全局同意管理引用,使用global using System.Text.Json;
引用
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())
{
}
直接写namespace关键字避免命名空间嵌套,代码结构嵌套严重较难看
namespace MyNamespace;
使用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", "[email protected]", 18);
var u3 = u1 with { Email = "[email protected]", Age = 99 };
public record User(string UserName, string? Email, int Age)
{
public User(string userName, int age) : this(userName, null, age)
{
}
}
#endregion
#endregion
异步编程提高服务器接待请求的数量,但不会使得单个请求处理效率变高,甚至可能略又降低
使用异步方法注意点:
Task
await
原理:
使用反编译工具查看代码
async方法会被C#编译器编译成一个类,并根据await调用把方法切分成多个状态,对async方法的调用就会被拆分成若干次对MoveNext方法的调用
在异步方法进行await调用的等待期间,框架会把当前的线程返回给线程池,等异步方法调用执行完毕,框架会从线程池再去除一个线程,以执行后续的代码。
Task.Run
asqyn\await
关键字async
Task
类型对象调用Result属性来等待异步执行结束获取返回值Task
,可以在Task类型对象调用Wait方法来调用异步方法并等待任务执行结束Thread.Sleep
CancellationToken
对象让异步方法提前终止Task.WhenAll
等待多个Task的执行结束async
的控制反转就是把“创建和组装对象”操作的控制权从业务逻辑的new转移到框架中,这样业务代码只要说明我要A对象,框架就会帮助我们创建这个对象
控制反转的两种方式:
IOptions不监听配置的改变,因此它的资源占用比较少,适用于对服务器启动后就不会改变的值进行读取。如果我们需要在程序运行中读取修改后的值,建议使用IOptionsSnapshot
private readonly IOptionsSnapshot optDbSettings;
services.AddOptions()
.Configure(e => config.GetSection("DB").Bind(e))
.Configure(e => config.GetSection("Smtp").Bind(e));
按照“后添加的配置提供程序中的配置覆盖之前的配置”的原则
在生产环境中我们需要把日志写入存储介质的方式,比如写入文件。
常用的第三方日志提供程序有Log4Net
、NLog
、Serilog
。这里推荐使用NLog或者Serilog,因为它们不仅使用简单,而且功能强大。
在集群环境中,如果每台服务器都把日志写入本地的文件中,那么在对日志进行分析的时候,我们就需要逐个打开各台服务器的磁盘中的日志文件,这非常麻烦。因此,在分布式环境下,我们最好采用集中式的日志服务器,各台服务器都把产生的日志写入日志服务器。
连接配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
EF Core采用了“约定大于配置”的设计原则,也就是说EF Core会默认按照约定根据实体类以及DbContext的定义来实现和数据库表的映射配置,除非用户显式地指定了配置规则。
使用:
1 class BookEntityConfig : IEntityTypeConfiguration
2 {
3 public void Configure(EntityTypeBuilder 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 }
注意
EF Core中实体类之间关系的配置采用如下的模式:HasXXX(…).WithYYY(…);
Include
在关系配置中通过HasForeignKey()指定这个属性为外键可以不适用Include
有时候我们不方便声明双向导航。比如在大部分系统中,基础的“用户”实体类会被非常多的其他实体类引用,这种单向导航属性的配置其实很简单,只要在WithMany方法中不指定属性即可
优点
缺点:
Guid算法使用网卡的MAC地址、时间戳等信息生成一个全球唯一的ID。由于Guid的全球唯一性,它适用于分布式系统,在进行多数据库数据合并的时候很方便,因此我们也可以用Guid类型作为主键。
注:
我们只要在上下文的OnConfiguring方法中调用optionsBuilder类的LogTo方法,传递一个参数为String的委托即可。当相关日志输出的时候,对应的委托就会被执行
Enumerable类中定义的供普通集合用的Where等方法都是“客户端评估”,Queryable中定义的Where方法都是“服务的评估”
总结:
在使用EF Core的时候,为了避免“客户端评估”,我们要尽量调用IQueryable版本的方法,而不是直接调用IEnumerable版本的方法。
对于IQueryable接口,调用“非立即执行”方法的时候不会执行查询,而调用“立即执行”方法的时候则会立即执行查询。
判断方法:
一个方法是否是立即执行方法的简单方式是:一个方法的返回值类型如果是IQueryable类型,这个方法一般就是非立即执行方法,否则这个方法就是立即执行方法。
注:IQueryable是一个待查询的逻辑,因此它是可以被重复使用的
IQueryable是用类似DataReader的方式读取查询结果的。DataReader会分批从数据库服务器读取数据。
优点是客户端内存占用小,缺点是如果遍历读取数据并进行处理的过程缓慢的话,会导致程序占用数据库连接的时间较长,从而降低数据库服务器的并发连接能力。因此,在遍历IQueryable的过程中,它需要占用一个数据库连接。
如果开发人员能够确认通过上下文查询出来的对象只是用来展示,不会发生状态改变,那么可以使用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 <> 类型。
注:一般只有在编写不特定于某个实体类的通用框架的时候,由于无法在编译期确定要操作的类名、属性等,才需要编写动态构建表达式树的代码,否则为了提高代码的可读性和可维护性,我们要尽量避免动态构建表达式树。
Program.cs
文件中的var app=builder.Build()
代码之前builder.Services.AddXXX()
Program.cs
添加var services = new ServiceCollection();
// 获取所有的用户程序集
var assemblies = ReflectionHelper.GetAllReferencedAssemblies();
// 扫描指定程序集中所有实现了IModuleInitialier接口的类
services.RunModuleInitializers(assemblies);
using var sp = services.BuildServiceProvider();
var items = sp.GetServices();
foreach (var item in items)
{
item.SayHello();
}
context
只编写OnModelCreating
方法Program.cs
中添加builder.Services.AddDbContext(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接口进行操作。
使用:
Program.cs
的build
之前builder.Service.AddStackExchangeRedisCache(options=>{
options.Configuration="localhost";
// 前缀,避免和其他数据混淆
options.InstanceName="lyy_";
});
ASP.N ET Core中的筛选器有以下5种类型:授权筛选器、资源筛选器、操作筛选器、异常筛选器和结果筛选器。
builder.Services.Configure(options=>{
option.Filters.Add();
});
中间件(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
");
await next.Invoke();
await context.Response.WriteAsync("1 End
");
});
appbuiler.Use(async (context, next) =>
{
await context.Response.WriteAsync("2 Start
");
await next.Invoke();
await context.Response.WriteAsync("2 End
");
});
appbuiler.Run(async ctx =>
{
await ctx.Response.WriteAsync("hello middleware
");
});
});
中间件类是一个普通的.NET类,它不需要继承任何父类或者实现任何接口
但是这个类需要有一个构造方法,构造方法至少要有一个RequestDelegate类型的参数,这个参数用来指向下一个中间件。
这个类还需要定义一个名字为Invoke或InvokeAsync的方法,方法中至少有一个HttpContext类型的参数,方法的返回值必须是Task类型。中间件类的构造方法和Invoke(或InvokeAsync)方法还可以定义其他参数,其他参数会通过依赖注入自动赋值。
在Program
中使用appbuilder.UseMiddleware
调用中间件
标识框架使用EF Core对数据库进行操作,由于EF Core屏蔽了底层数据库的差异,因此标识框架支持几乎所有数据库。
标识框架中提供了IdentityUser
使用:
NuGet安装Microsoft.AspNetCore.Identity.EntityFrameworkCore
编写分别继承自IdentityUser
创建继承自IdentityDbContext的类,这是一个EF Core中的上下文类,我们可以通过这个类操作数据库。IdentityDbContext是一个泛型类,有3个泛型参数,分别代表用户类型、角色类型和主键类型。
向依赖注入容器中注册与标识框架相关的服务,并且对相关的选项进行配置。
services.AddDbContext(opt => {
string connStr = builder.Configuration.GetConnectionString("Default");
opt.UseSqlServer(connStr);
});
services.AddDataProtection();
services.AddIdentityCore(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()
.AddDefaultTokenProviders()
.AddRoleManager>()
.AddUserManager>();
通过执行Add-Migration、Update-database等命令执行EF Core的数据库迁移
编写控制器的代码。我们在控制器中需要对角色、用户进行操作
private readonly RoleManager roleManager;
private readonly UserManager userManager;
除了这些基本的用法之外,标识框架中还提供了多因素验证(短信验证、指纹验证等)、外部登录、重置密码等功能,官方文档中关于这些内容的介绍非常清晰。
Session:
实现用户登录功能的经典做法是用Session
也就是在用户登录验证成功后,服务器端生成唯一标识SessionId
服务器端不仅会把SessionId返回给浏览器端,还会把SessionId和登录用户的信息的对应关系保存到服务器的内存中
当浏览器端再次向服务器端发送请求的时候,浏览器端就在HTTP请求中携带SessionId,服务器端就可以根据SessionId从服务器的内存中取到用户的信息,这样就实现了用户登录的功能。
但是在分布式环境下,特别是在“前后端分离、多客户端”时代,Session暴露出很多缺点。
在现在的项目开发中,我们倾向于采用JWT代替Session实现登录。JWT全称是JSON web token,从名字中可以看出,JWT是使用JSON格式来保存令牌信息的。
JWT的结构如图
JWT的头部(header)中保存的是加密算法的说明,负载(payload)中保存的是用户的ID、用户名、角色等信息,签名(signature)是根据头部和负载一起算出来的值。
使用:
1 var claims = new List();
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);
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 }
注意:
NET Core对于JWT的封装
1 services.Configure(builder.Configuration.GetSection("JWT"));
2 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
3 .AddJwtBearer(x =>
4 {
5 var jwtOpt = builder.Configuration.GetSection("JWT").Get();
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 });
注:
修改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();
14 c.AddSecurityRequirement(requirement);
15 });
JWT的缺点是:一旦JWT被发放给客户端,在有效期内这个令牌就一直有效,令牌是无法被提前撤回的。
解决思路是:在用户表中增加一个整数类型的列JWTVersion,它代表最后一次发放出去的令牌的版本号;每次登录、发放令牌的时候,我们都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT的负载中;当执行禁用用户、撤回用户的令牌等操作的时候,我们让这个用户对应的JWTVersion的值自增;当服务器端收到客户端提交的JWT后,先把JWT中的JWTVersion值和数据库中的JWTVersion值做比较,如果JWT中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT过期了,这样我们就实现了JWT的撤回机制。
使用:
BackgroudService
托管服务是以单例的生命周期注册到依赖注入容器中的。
长生命周期的服务不能依赖短生命周期的服务,因此我们可以在托管服务中通过构造方法注入其他生命周期为单例的服务,但是不能注入生命周期为范围或者瞬态的服务。
我们可以通过构造方法注入IServiceScopeFactory服务,它可以用来创建IServiceScope对象,这样我们就可以通过IServiceScope来创建短生命周期的服务了
this.serviceScope = scopeFactory.CreateScope();
var sp = serviceScope.ServiceProvider;
this.ctx = sp.GetRequiredService();
1 builder.Services.AddFluentValidation(fv => {
2 Assembly assembly = Assembly.GetExecutingAssembly();
3 fv.RegisterValidatorsFromAssembly(assembly);
4 });
1 public class Login2RequestValidator: AbstractValidator
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 }
ASP.NET Core SignalR(以下简称SignalR)是.NET Core平台中对WebSocket的封装,从而让开发人员可以更简单地进行WebSocket开发。
基本使用:
从中心调用客户端方法
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
从客户端调用中心方法
connection.On("ReceiveMessage", (user, message) =>
{
this.Dispatcher.Invoke(() =>
{
var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});
我们在开发环境中运行的项目所加载的程序集是为了方便开发工具调试而生成的调试版程序集,运行效率并不高,因此我们不能直接把项目文件夹下bin/Debug中的程序集部署到生产环境的服务器上。我们应该创建网站的发布版,创建网站发布版的过程简称为“发布”。
两种部署模式:“框架独立”和“独立”