8 ASP.NET Core
缓存
客户端响应缓存
使用 cache-control 响应报文头, 实现客户端缓存这一请求的结果一段时间, cache-control: max-age=20 表示在浏览器中缓存 20 秒
// 设置缓存 20 秒
[ResponseCache(Duration = 20)]
[HttpGet}
public int GetNum()
{
return 10;
}
服务器端响应缓存
app.UseCors();
// 启用服务器端响应缓存
app.UseResponseCaching();
app.MapControllers();
当使用"禁用缓存"时, 浏览器端请求时带有请求头
cache-control: no-cache, 即使启用了服务器端响应缓存, 服务器也不会从缓存中获取数据
服务器端响应缓存的问题
- 无法解决恶意请求给服务器带来的压力
- 响应状态码为 200 的 GET 或 HEAD 响应才可能被缓存, 报文头中不能含有 Authorization, Set-Cookie 等
内存缓存
// Program.cs
builder.Services.AddMemoryCache();
// 服务类
private readonly IMemoryCache _memoryCache;
public WeatherForecastController(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public async Task<ActionResult<Book>> GetBook(int id)
{
// 从缓存取数据, 如果没有缓存, 则调用方法, 并返回数据(同时保存到缓存)
var book = await _memoryCache.GetOrCreateAsync("Book_" + id, async entry =>
{
// 没有缓存, 查询数据
Console.WriteLine($"没有缓存, 查询数据");
var bookData = id switch
{
1 => new Book(1, "1"),
2 => new Book(2, "2"),
3 => new Book(3, "3"),
_ => new Book(4, "4")
};
return bookData;
});
if (book == null)
{
return NotFound("查询不到数据");
}
return book;
}
缓存过期时间策略
绝对过期策略
var book = await _memoryCache.GetOrCreateAsync("Book_" + id, async entry =>
{
var bookData = id switch
{
1 => new Book(1, "1"),
2 => new Book(2, "2"),
3 => new Book(3, "3"),
_ => null
};
// 缓存时间为 1 分钟
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
return bookData;
});
滑动过期策略
// 从缓存取数据, 如果没有缓存, 则调用方法, 并返回数据(同时保存到缓存)
var book = await _memoryCache.GetOrCreateAsync("Book_" + id, async entry =>
{
var bookData = id switch
{
1 => new Book(1, "1"),
2 => new Book(2, "2"),
3 => new Book(3, "3"),
_ => null
};
// 在缓存没过期的时间, 请求一次的时候, 缓存有效期会自动延长
entry.SlidingExpiration = TimeSpan.FromSeconds(10);
return bookData;
});
同时设置两种策略
使用滑动过期时间策略, 如果一个缓存项一直被频繁访问, 那么这个缓存项就会一直被续期而不过期. 可以对一个缓存项同时设定滑动过期时间和绝对过期时间, 并且把绝对过期时间设定的比滑动过期时间长, 这样缓存项的内容会在绝对过期时间内随着访问被滑动续期, 但是一旦超过了绝对过期时间, 缓存项就会被删除
缓存问题
缓存穿透
缓存穿透是由于"查询不到的数据用 null 表示" 导致的, 因此解决方法是, 将"查不到"也当成数据放入缓存, 使用 GetOrCreateAsync() 方法可以解决这种问题, 因为这个方法会把 null 也当成合法的缓存值
缓存雪崩
缓存项集中过期引起的缓存雪崩, 解决方法是在基础过期时间之上, 加入一个随机的过期时间
// 不使用 new Random(), 高频访问时可能造成随机数是一样的
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.Next(10, 20));
配置系统
默认添加的配置提供者
ASP.NET Core 默认添加了常用的提供者, 按以下顺序:
- 加载现有的 IConfiguration
- 加载项目根目录下的 appsettings.json
- 加载项目根目录下的 appsettings.{Environment}.json
- 环境变量 ASPNETCORE_ENVIRONMENT 的值
- Development(开发环境)
- Staging(测试环境)
- Production(生产环境)
- 代码中读取当前环境
- 在 Program.cs 中
- app.Environment.EnvironmentName
- app.Environment.IsDevelopment()
- 在其它代码文件中
- 注入 IWebHostEnvironment 即可
- 在 Program.cs 中
- 环境变量 ASPNETCORE_ENVIRONMENT 的值
- 当程序运行在开发环境下, 程序会加载"用户机密"配置
- 加载环境变量中的配置
- 加载命令行中的配置
EF Core
分层项目中 EF Core 迁移的问题
可能会出现"No DbContext was found in assembly", "Unable to create an object of type 'MyDbContext'"等错误提示, 可以采用 IDesignTimeDbContextFactory 接口来解决问题
当项目中存在一个 IDesignTimeDbContextFactory 接口的实现类时, 数据库迁移工具会调用这个实现类的 CreateDbContext() 方法来获取上下文对象, 使用这个上下文对象来连接数据库
// 数据库项目安装 Nuget 包: Microsoft.EntityFrameworkCore.Tools, Microsoft.EntityFrameworkCore.SqlServer(或其它数据库提供者)
public class MyDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
// 只在做数据库迁移时会运行, 在正式运行程序时不会使用到这个类
public MyDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<MyDbContext> builder = new();
// 连接字符串可以从环境变更中读取, 也可以直接写死
string connStr = Environment.GetEnvironmentVariable("ConnectionStrings:BooksEFCore");
builder.UseSqlServer(connStr);
return new MyDbContext(builder.Options);
}
}
上下文池(不推荐使用)
初始化时的 AddDbContext() 是 Scope, 也可以使用 AddDbContextPool() 实现池化, 但也会有问题(可以看视频或书第 197 页)
Filter
ExceptionFilter(异常筛选器)
接口: IAsyncExceptionFilter
程序出现未处理异常时, 会执行 Filter
public class MyExceptionFilter : IAsyncExceptionFilter
{
private readonly IWebHostEnvironment _webHostEnvironment;
public MyExceptionFilter(IWebHostEnvironment webHostEnvironment)
{
_webHostEnvironment = webHostEnvironment;
}
public Task OnExceptionAsync(ExceptionContext context)
{
/*
* context.Exception: 异常信息对象
* context.ExceptionHandled: 赋值为 true, 则之后的 ExceptionFilter 不会再执行
* context.Result: 返回给客户端的值
*/
string msg;
if (!_webHostEnvironment.IsDevelopment())
{
msg = context.Exception.Message;
}
else
{
msg = "服务器出现未知异常";
}
context.ExceptionHandled = true;
context.Result = new ObjectResult(new
{
code = 500,
message = msg,
});
return Task.CompletedTask;
}
}
public class LogExceptionFilter : IAsyncExceptionFilter
{
public LogExceptionFilter(IWebHostEnvironment webHostEnvironment)
{
}
public Task OnExceptionAsync(ExceptionContext context)
{
Console.WriteLine(context.Exception.ToString());
return Task.CompletedTask;
}
}
// 需要注意添加顺序, 后添加的先执行
builder.Services.Configure<MvcOptions>(p =>
{
p.Filters.Add<MyExceptionFilter>();
p.Filters.Add<LogExceptionFilter>();
});
ActionFilter
接口: IAsyncActionFilter
在每个 Action 执行之前之后会执行 Filter
public class MyActionFilter1 : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Console.WriteLine("MyActionFilter1: 开始执行");
ActionExecutedContext result = await next();
if (result.Exception != null)
{
Console.WriteLine("MyActionFilter1: 出现异常");
}
else
{
Console.WriteLine("MyActionFilter1: 执行成功");
}
}
}
public class MyActionFilter2 : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Console.WriteLine("MyActionFilter2: 开始执行");
ActionExecutedContext result = await next();
if (result.Exception != null)
{
Console.WriteLine("MyActionFilter2: 出现异常");
}
else
{
Console.WriteLine("MyActionFilter2: 执行成功");
}
}
}
// 输出
MyActionFilter1: 开始执行
MyActionFilter2: 开始执行
MyActionFilter2: 执行成功
MyActionFilter1: 执行成功
// 执行 Filter 的顺序是按添加的顺序来执行的
builder.Services.Configure<MvcOptions>(p =>
{
p.Filters.Add<MyActionFilter1>();
p.Filters.Add<MyActionFilter2>();
});
自动启用事务的 ActionFilter
TransactionScope
可以看视频或书第 226 页
当需要进行分布式事务处理时, 需要使用最终一致性事务
- 可以使用 TransactionScope 来简化事务代码的编写(否则要去手动启用/提交/回滚事务, 会比较麻烦)
- 当一段使用 EF Core 进行数据库操作的代码放到 TransactionScope 声明的范围中时, 这段代码会自动被标记为"支持事务"
- TransactionScope 实现了 IDisposable 接口, 如果一个 TransactionScope 的对象没有调用
Complete()方法就执行了Dispose()方法, 则事务会被回滚, 否则事务就会被提交 - TransactionScope 还支持嵌套, 只有最外层的 TransactionScope 提交了事务, 所有的操作才生效. 如果最外层 TransactionScope 回滚了事务, 即使内层的 TransactionScope 提交了事务, 最终所有的操作仍然会被回滚
// 使用异步方法时, 需要指定 TransactionScopeAsyncFlowOption.Enabled 参数
using (var ts = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
_dbContext.Books.Add(new Part4Study.Book { Title = "书1", Price = "1" });
await _dbContext.SaveChangesAsync();
_dbContext.Persons.Add(new Person { Name = "名1", Age = 1 });
await _dbContext.SaveChangesAsync();
ts.Complete();
}
中间件
基本使用
app.Map("/test", async (appbuilder) =>
{
appbuilder.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/>");
});
appbuilder.Use(async (context, next) =>
{
await context.Response.WriteAsync("2 start<br/>");
await next.Invoke();
await context.Response.WriteAsync("2 end<br/>");
});
appbuilder.Run(async context =>
{
await context.Response.WriteAsync("Run<br/>");
});
// 在 Run() 后边再调用 Use() 方法的话, 这些方法也不会执行
});
// 输出
1 start
2 start
Run
2 end
1 end
中间件类
public class Test1Middleware
{
private readonly RequestDelegate _next;
public Test1Middleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
context.Response.WriteAsync("Test1Middleware Start<br/>");
await _next(context);
context.Response.WriteAsync("Test1Middleware End<br/>");
}
}
// Program.cs 使用中间件
appbuilder.UseMiddleware<Test1Middleware>();
中间件类的构造函数和 Invoke() 方法的参数还可以添加其它参数, 这些参数都会通过依赖注入自动赋值
中间件和 Filter 的区别
中间件是 ASP.NET Core 这个基础提供的功能, 而 Filter 是 ASP.NET Core MVC 中提供的功能
标识框架
看视频
JWT
基本使用
安装Nuget包: System.IdentityModel.Tokens.Jwt
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, "John Doe"));
claims.Add(new Claim(ClaimTypes.Email, "johndoe@gmail.com"));
claims.Add(new Claim(ClaimTypes.Role, "Administrator"));
claims.Add(new Claim(ClaimTypes.Role, "User"));
claims.Add(new Claim("PassPort", "123456"));
var key = "123qaweasdzxc12@21312312eqweqwewqewqeqwe3";
var expires = DateTime.Now.AddDays(1);
var secKeyBytes = Encoding.UTF8.GetBytes(key);
var secKey = new SymmetricSecurityKey(secKeyBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new JwtSecurityToken(claims: claims, expires: expires, signingCredentials: credentials);
// 生成JWT
var jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
Console.WriteLine(jwt);
Console.WriteLine("-------------------------------------");
// 解析JWT
var tokenHandler = new JwtSecurityTokenHandler();
var valParam = new TokenValidationParameters();
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
valParam.IssuerSigningKey = securityKey;
valParam.ValidateIssuer = false;
valParam.ValidateAudience = false;
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt, valParam, out _);
// 输出
foreach (var claim in claimsPrincipal.Claims)
{
Console.WriteLine(claim.Type + ": " + claim.Value);
}
在 ASP.NET 中使用 JWT
安装包: Microsoft.AspNetCore.Authentication.JwtBearer
// 从配置文件中取得Key和过期时间
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(p =>
{
var jwtOptions = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>();
byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOptions.SecKey);
var secKey = new SymmetricSecurityKey(keyBytes);
p.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = secKey,
};
});
// 在 UseAuthorization() 前边
app.UseAuthentication();
app.UseAuthorization();
Swagger 中添加 JWT 报文头
builder.Services.AddSwaggerGen(p =>
{
var scheme = new OpenApiSecurityScheme()
{
Description = "Authorization header.",
Reference = new OpenApiReference()
{
Type = ReferenceType.SecurityScheme,
Id = "Authorization"
},
Scheme = "oauth2",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
};
p.AddSecurityDefinition("Authorization", scheme);
var requirement = new OpenApiSecurityRequirement();
requirement[scheme] = new List<string> { };
p.AddSecurityRequirement(requirement);
});
托管服务
public class BackService : BackgroundService
{
private readonly IServiceScope _serviceScope;
public BackService(IServiceScopeFactory serviceScopeFactory)
{
// 创建 Scope 实现注入
_serviceScope = serviceScopeFactory.CreateScope();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await Task.Delay(5000, stoppingToken);
var serviceProvider = _serviceScope.ServiceProvider;
// serviceProvider.GetService<...>();
// ...
}
catch (Exception ex)
{
// 后台服务异常时如果不 try catch, 默认会导致程序停止
Console.WriteLine(ex);
}
}
public override void Dispose()
{
// 及时释放
_serviceScope.Dispose();
base.Dispose();
}
}
// 托管服务是以单例的生命周期注册到 DI 中的
builder.Services.AddHostedService<BackService>();
数据校验
安装包: FluentValidation.AspNetCore
public record AddNewUserRequest(string Email, string UserName, string Password);
public class AddNewUserValidator : AbstractValidator<AddNewUserRequest>
{
public AddNewUserValidator()
{
RuleFor(p => p.Email).NotNull()
.EmailAddress() .WithMessage("邮箱需要合法")
.Must(p => p.EndsWith("@163.com") || p.EndsWith("@qq.com"))
.WithMessage("邮箱格式不正确");
}
}
builder.Services.AddControllers();
//...
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssembly(Assembly.GetEntryAssembly());
SignalR

No Comments