视频管理系统项目实战
DbContext
基本增删改查
- DbContext可以使用DbSet更改实体,并在修改后使用SaveChange()方法保存到数据库。DbSet可以放到DbContext属性中
public DbSet<Entity> DbSetName {get;set;}
...
DbSetName.add(entity);
或者直接用DbSet修改数据
_dbcontext.set<Entity>().add(entity);
_dbcontext.SaveChange();
- 增加
增加支持异步
DbSet.Add(entity);
DbSet.AddRange(entities);
- 删除
DbSet.Remove(entity);
DbSet.RemoveRange(enities);
- 修改
DbSet.Update(entity);
DbSet.UpdateRange(entities);
- 查询
查询使用Linq查询,可以直接传入一个Expression<Func<IEntity,bool>>类型变量
DbSet.Where(x=>x.Id==1);
CancellationToken类
- 所有异步方法都能传入一个取消令牌参数,默认为None,因为异步操作无法控制,所以需要一个参数来控制异步线程,防止异步线程因为某个业务异常阻塞线程过久浪费资源。
ConfigureAwait(false)
-
当ConfigureAwait(true),代码由同步执行进入异步执行时,当前同步执行的线程上下文信息(比如HttpConext.Current,Thread.CurrentThread.CurrentCulture)就会被捕获并保存至SynchronizationContext中,供异步执行中使用,并且供异步执行完成之后(await之后的代码)的同步执行中使用(虽然await之后是同步执行的,但是发生了线程切换,会在另外一个线程中执行「ASP.NET场景」)。这个捕获当然是有代价的,当时我们误以为性能问题是这个地方的开销引起,但实际上这个开销很小,在我们的应用场景不至于会带来性能问题。
-
当Configurewait(flase),则不进行线程上下文信息的捕获,async方法中与await之后的代码执行时就无法获取await之前的线程的上下文信息,在ASP.NET中最直接的影响就是HttpConext.Current的值为null。
事务
-
和数据库存储事务一样,就是把对实体的修改存储起来,当成功提交时一起修改到数据库,当失败时回滚到整个事务修改前。
-
开启事务
_dbContext.Database.BeginTransactionAsync(cancellationToken);
- 提交事务
await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await _dbContext.Database.CommitTransactionAsync(cancellationToken).ConfigureAwait(false);
- 回滚事务
await _dbContext.Database.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false);
ORM是通过上下文Dbcontext修改数据库数据的,需要配置DbContext来对数据库进行操作
依赖注入容器IServiceCollection
- 可以通过依赖注入容器实现简单的依赖注入,可以用来封装DbContext的注入
public static IServiceCollection AddEntityFrameworkCore<IDbContext>(
this IServiceCollection service,
Action<DbContextOptionsBuilder>? options=null,
ServiceLifetime lifetime=ServiceLifetime.Singleton)
where IDbContext:MasterDbContext<IDbContext>
{
service.AddDbContext<IDbContext>(options);
return service;
}
跟踪与标识解析
如何跟踪实体
实体实例在以下情况下会被跟踪:
-
从针对数据库执行的查询返回
-
通过 Add、Attach、Update 或类似方法显示附加到 DbContext
-
检测为连接到现有跟踪实体的新实体
-
默认开启跟踪查询,被跟踪查询的实体被修改时可以通过调用DbContext.SaveChange保存到数据库,未被跟踪的实体将不能通过SaveChange保存实例到数据库。
//禁用跟踪查询重写DbContext的OnConfiguring方法
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTrackingWithIdentityResolution);
-
跟踪查询禁用后仍然可以使用更改跟踪跟踪实体,跟踪的实体仍可以用SaveChange保存。
-
标识解析:根据主键保证实体在上下文中的唯一性
ChangeTracker类
-
存储实体的更改信息。
-
ChangeTracker.DetectChanges()可以刷新更改信息,保证更改信息为最新的,并防止被自动调用。
上下文配置DbContextOptionsBuilder
-
用于配置数据库的类型,连接字符串,版本,生命周期等
-
获取程序配置
var configuration=services.BuildServiceProvider().GetRequiredService<IConfiguration>();
- this扩展方法:类型的扩展方法,在第一个参数前加上this关键字,成为第一个参数类型的扩展方法例如:
public static string StringExtension(this string str){
return str+=".";
}
此方法为string的扩展方法,调用时会为string加上.
- 注入配置DbcontextOptionsBuilder
services.AddEntityFrameworkCore<IDbContext>(x=>{x.UseMySql(configuration.GetConnectionString(connect),new MySqlServerVersion(new Version(5,5,28)));},ServiceLifetime.Singleton);
迁移
- 迁移命令
--startup-project:开始项目,运行命令的项目
--project:目标项目
迁移需要在开始项目上安装EntityFrameworkCore.Tool和EntityFrameworkCore.Design
dotnet ef migrations add Init --startup-project .\src\Video.HttpApi.Host --project .\src\Video.EntityFrameworkCore
- 同步数据到数据库命令(创建数据库)
踩坑:数据库连接字符串Server需要使用127.0.0.1,使用LocalHost会报错
踩坑:需要更新数据库种子数据需要先删除迁移后新建迁移再更新,如果迁移失败可以在命令最后加参数-v查看详细信息,且大概率是数据库包的兼容问题
dotnet ef database update --startup-project .\src\Video.HttpApi.Host --project .\src\Video.EntityFrameworkCore
dotnet ef migrations remove --startup-project .\src\Video.HttpApi.Host --project .\src\Video.EntityFrameworkCore
Serilog日志
- 安装
Serilog.AspNetCore
Serilog.Sinks.Async - 使用
在主程序最前面加入以下代码
using Serilog;
using Serilog.Events;
Log.Logger=new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft",LogEventLevel.Information)
.ReadFrom.Configuration(new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")??"Production"}.json",//根据环境变量加载指定配置
optional:true).Build())
.Enrich.FromLogContext()
.WriteTo.Async(c=>c.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory+"/log/","log"),
rollingInterval:RollingInterval.Day))//写入日志到文件
.WriteTo.Async(c=>c.Console())
.CreateLogger();
在appsettings.json加入以下配置
"Serilog": {
"MinimumLevel": {
"Default": "Debug", //最小日志记录级别
"Override": { //系统日志最小记录级别
"Default": "Warning",
"System": "Warning",
"Microsoft": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" }, //输出到控制台
{
"Name": "Async", //异步写入日志
"Args": {
"configure": [
{
"Name": "File", //输出文件
"Args": {
"path": "log/log.txt",
"outputTemplate": "{NewLine}Date:{Timestamp:yyyy-MM-dd HH:mm:ss.fff}{NewLine}LogLevel:{Level}{NewLine}Class:{SourceContext}{NewLine}Message:{Message}{NewLine}{Exception}",
"rollingInterval": "3" //日志文件生成精度:1:年 2:月 3:日 4:小时
}
}
]
}
}
]
}
最后替换掉原有日志并使用Serilog
builder.Host.UseSerilog();
配置SwaggerUI支持JWT
-
安装Swashbuckle.AspNetCore
-
注入swagger
builder.Services.AddSwaggerGen(delegate (SwaggerGenOptions option)
{
//配置显示文档
option.SwaggerDoc("v1.0",new OpenApiInfo{
Version="v1.0",//版本
Title="Video Api 文档",//标题
Description="Video Api 文档",//描述
Contact=new OpenApiContact{
Name="lrp",//作者
Email="2833784318@qq.com",//邮箱
Url=new Uri("https://github.com/lrplrplrp")//可以放Github地址
}
});
//加载xml文档,显示Swaffer的注释
string[] files=Directory.GetFiles(AppContext.BaseDirectory,"*.xml");//获取api文档
string[] array=files;
foreach (string filePath in array)
{
option.IncludeXmlComments(filePath,includeControllerXmlComments:true);
}
//添加安全要求
option.AddSecurityRequirement(new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme{
Reference=new OpenApiReference{
Id="Bearer",
Type=ReferenceType.SecurityScheme
}
},
Array.Empty<string>()
}
});
//添加Authorization的输入框
option.AddSecurityDefinition("Bearer",new OpenApiSecurityScheme
{
Description = "请输入token,格式为 Bearer xxxxxxxx(注意中间必须有空格)",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
});
});
- 将swagger加入app管道
app.UseSwagger();
app.UseSwaggerUI(c=>{
c.SwaggerEndpoint("/swagger/1/swagger.json","Web Api");
c.DocExpansion(DocExpansion.None);
c.DefaultModelExpandDepth(-1);
});
生成XML格式的Api文档
-
VS
项目右键->属性->生成选项卡->生成XML文件 -
VSCore
在项目的csproj文件中添加属性配置
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DocumentationFile>D:\C#dome\Video\src\Video.Application.Contract\Video.Application.Contract.xml</DocumentationFile>
</PropertyGroup>
Controller依赖注入
- 构造函数注入依赖:
创建一个私有只读的字段,通过构造函数传入服务然后赋值给字段保存到类中,供方法使用。
public class HomeController : Controller
{
private readonly IDateTime _dateTime;
public HomeController(IDateTime dateTime)
{
_dateTime = dateTime;
}
}
- FromServices注入依赖:
控制器方法的参数前添加FromServices特性可以直接注入服务参数。
public IActionResult About([FromServices] IDateTime dateTime)
{
return Content( $"Current server time: {dateTime.Now}");
}
- 控制器访问配置与Options选项模式:
一般不直接将配置注入到控制器中而是通过Option选项模式注入配置
先将配置读取出来,绑定到实体类
var configuration=builder.Services.BuildServiceProvider().GetRequiredService<IConfiguration>();
var jwtsection=configuration.GetSection(nameof(JWTOptions));//读取配置
builder.Services.Configure<JWTOptions>(jwtsection);//将配置绑定到类并注入服务
然后注入到方法中使用
public class SettingsController : Controller
{
private readonly JWTOptions _settings;
public SettingsController(IOptions<JWTOptions> settingsOptions)
{
_settings = settingsOptions.Value;
}
public IActionResult Index()
{
ViewData["Title"] = _settings.Title;
ViewData["Updates"] = _settings.Updates;
return View();
}
}
MapGet方法
-
MapGet(string pattern,Delegate handler)方法向管道添加一个迷你控制器,pattern是请求方法路由地址和描述,handler是控制器方法委托。可以很方便的用于测试。
-
注意:MapGet方法应用在管道,所以不会经过过滤器,而正常的Controller会被放在过滤器后面,所以如果需要过滤器请使用Controller方法添加控制器
Claim验证
-
Claim可以存储键值对形式的数据,用来进行登录验证。
-
如果使用了ClaimsIdentity枚举类型的键,还可以针对接口进行角色的权限认证如:
在token中加入Claim,token中使用的是Claim数组,也就是可以设置多个Claim值。
Claim[] claims=new Claim[]{
new Claim(ClaimsIdentity.DefaultRoleClaimType,"admin")
};
Controller中验证角色只需要给控制器或者方法加上特性[Authorize(Roles="admin")]即可验证角色,如果角色不为admin请求会报403错误,也就是没有权限访问。
JWT配置
-
安装Microsoft.AspNetCore.Authentication.JwtBearer
-
appsettings.json配置
"JWTOptions": {
"Issuer": "tokengo.top",
"Audience": "tokengo.top",
"SecretKey": "123456789abcdefghi",
"ExpireMinutes": 120000
}
- JWT组件注入
var configuration=builder.Services.BuildServiceProvider().GetRequiredService<IConfiguration>();//读取全部配置
var jwtsection=configuration.GetSection(nameof(JWTOptions));//读取JWTOptions配置
builder.Services.Configure<JWTOptions>(jwtsection);//将配置绑定到类
var jwt=jwtsection.Get<JWTOptions>();//获取JWTOptions类型的配置
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options=>{
options.TokenValidationParameters=new TokenValidationParameters{
ValidateIssuer =true,//是否在令牌期间验证签发者
ValidateAudience=true,//是否验证接收者
ValidateLifetime=true,//是否验证失效时间
ValidateIssuerSigningKey=true,//是否验证签名
ValidAudience=jwt.Audience,//接收者
ValidIssuer=jwt.Issuer,//签发者,签发token的人
IssuerSigningKey=new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt.SecretKey))//签发者密钥
};
}
);
- 将授权和认证应用于管道
app.UseAuthentication();//认证
app.UseAuthorization();//授权
- token授权
(IOptions<JWTOptions> options)=>{
Claim[] claims=new Claim[]{
new Claim(ClaimsIdentity.DefaultRoleClaimType,"admin")
};//Claim为键值对类型,可以判断值是否被修改,当作身份认证
var jwt=options.Value;//获取配置
var keyByte=Encoding.UTF8.GetBytes(jwt.SecretKey);//将密钥转为字节类型
var cred=new SigningCredentials(new SymmetricSecurityKey(keyByte),SecurityAlgorithms.HmacSha256);//通过密钥和算法生成令牌
var securityToken=new JwtSecurityToken(
jwt.Issuer,//签发者
jwt.Audience,//接收者
claims,//payload
expires:DateTime.Now.AddMinutes(jwt.ExpireMinutes),//过期时间,AddMinutes()方法可以添加以分钟为单位的时间到DateTime数据上并返回
signingCredentials:cred//令牌
);
var token=new JwtSecurityTokenHandler().WriteToken(securityToken);//生成Token
return token;
}
AutoMapper
-
AutoMapper可以看作是实体数据映射到外部前的一个过滤,可以防止敏感数据泄露,只让外部某些接口访问到安全信息。
-
安装AutoMapper.Extensions.Microsoft.DependencyInjection
-
创建Dto类
/// <summary>
/// UserInfo
/// </summary>
public class UserInfoDto:EntityDto
{
/// <summary>
/// 用户名
/// </summary>
/// <value></value>
public string? Name { get; set; }
/// <summary>
/// 账号
/// </summary>
/// <value></value>
public string? UserName { get; set; }
/// <summary>
/// 密码(加密)
/// </summary>
/// <value></value>
public string? Password { get; set; }
/// <summary>
/// 头像
/// </summary>
/// <value></value>
public string? Avatar { get; set; }
/// <summary>
/// 是否启用
/// </summary>
/// <value></value>
public bool Status { get; set; }=true;
}
/// <summary>
/// 实体基类
/// </summary>
public class EntityDto
{
/// <summary>
/// 主键
/// </summary>
/// <value></value>
public Guid id { get; set; }
/// <summary>
/// 创建时间
/// </summary>
/// <value></value>
public DateTime CreateTime { get; set; }
}
- 创建继承Profile的Profile类配置映射关系
public class UserInfoAutoMapperProfile:Profile
{
public UserInfoAutoMapperProfile(){
CreateMap<UserInfo,UserInfoDto>().ReverseMap();
}
}
- 创建服务接口并实现
/// <summary>
/// IUserInfoService接口
/// </summary>
public interface IUserInfoService
{
/// <summary>
/// text
/// </summary>
/// <returns></returns>
Task<Dtos.UserInfoDto> GetAsync();
}
//接口实现
public class UserInfoService:IUserInfoService
{
private readonly IMapper _mapper;
public UserInfoService(IMapper mapper)
{
_mapper = mapper;
}
public async Task<UserInfoDto> GetAsync(){
var userInfo=new UserInfo{
CreateTime=DateTime.Now,
id=Guid.NewGuid(),
Name="2833784318",
UserName="lrp",
Avatar="",
Status=false
};
var userInfoDto=_mapper.Map<UserInfoDto>(userInfo);
return await Task.FromResult(userInfoDto);
}
}
- 创建IServiceCollection扩展方法注入AutoMapper和自定义服务依赖
public static void AddVideoApplication(this IServiceCollection services){
services.AddAutoMapper(typeof(VideoApplicationExtension).Assembly);
services.AddTransient<IUserInfoService,UserInfoService>();
}
- 注入自定义扩展方法
builder.Services.AddVideoApplication();
Redis
-
安装FreeRedis
-
配置连接字符串
"RedisConnection":"127.0.0.1:6379"
- 注入服务
builder.Services.AddSingleton(new RedisClient(configuration["RedisConnection"]));
CRUD
登录接口实现仓储
- 创建登录接口数据
/// <summary>
/// 登录接口
/// </summary>
public class LoginInput
{
/// <summary>
/// 用户名
/// </summary>
/// <value></value>
public string? Username { get; set; }
/// <summary>
/// 密码
/// </summary>
/// <value></value>
public string? Password { get; set; }
}
-
视图模型
视图模型不对应数据库的表,单纯拼接成想要的模型,在无法改动原模型并对数据类型有需求时使用,在仓储层拼接数据。 -
创建视图模型(包含角色集合的UserInfo)
public class UserInfoRoleView:Entity
{
public UserInfoRoleView()
{
Role=new List<Role>();
}
/// <summary>
/// 用户名
/// </summary>
/// <value></value>
public string? Name { get; set; }
/// <summary>
/// 账号
/// </summary>
/// <value></value>
public string? UserName { get; set; }
/// <summary>
/// 密码(加密)
/// </summary>
/// <value></value>
public string? Password { get; set; }
/// <summary>
/// 头像
/// </summary>
/// <value></value>
public string? Avatar { get; set; }
/// <summary>
/// 是否启用
/// </summary>
/// <value></value>
public bool Status { get; set; }=true;
public List<Role> Role { get; set; }
}
- 创建Dto
/// <summary>
///
/// </summary>
public class UserInfoRoleDto:EntityDto
{
/// <summary>
///
/// </summary>
public UserInfoRoleDto(){
Role=new List<RoleDto>();
}
/// <summary>
/// 用户名
/// </summary>
/// <value></value>
public string? Name { get; set; }
/// <summary>
/// 账号
/// </summary>
/// <value></value>
public string? UserName { get; set; }
/// <summary>
/// 密码(加密)
/// </summary>
/// <value></value>
public string? Password { get; set; }
/// <summary>
/// 头像
/// </summary>
/// <value></value>
public string? Avatar { get; set; }
/// <summary>
/// 是否启用
/// </summary>
/// <value></value>
public bool Status { get; set; }=true;
/// <summary>
/// 角色
/// </summary>
/// <value></value>
public List<RoleDto> Role { get; set; }
}
创建相关Dto
/// <summary>
///
/// </summary>
public class RoleDto:EntityDto
{
/// <summary>
/// 角色名称
/// </summary>
/// <value></value>
public string? Name { get; set; }
/// <summary>
/// 角色编号
/// </summary>
/// <value></value>
public string? Code { get; set; }
}
- 添加Mapper映射关系
CreateMap<UserInfoRoleView,UserInfoRoleDto>().ReverseMap();
CreateMap<Role,RoleDto>().ReverseMap();
- 仓储层添加接口和方法,实现通过账号密码获得用户数据
Task<UserInfoRoleView?> GetUserInfoRoleView(string username,string password);
public async Task<UserInfoRoleView?> GetUserInfoRoleView(string username, string password)
{
var userInfo=await _dbContext.UserInfo
.Where(x=>x.UserName==username&&x.Password==password)
.Select(x=>new UserInfoRoleView{
Avatar=x.Avatar,
CreateTime=x.CreateTime,
id=x.id,
Name=x.Name,
Password=x.Password,
Status=x.Status,
UserName=x.UserName
})
.FirstOrDefaultAsync();
if(userInfo==null){
return null;
}
var query=
from role in _dbContext.Role
join userRole in _dbContext.UserRole on role.id equals userRole.RoleId
select role;
userInfo.Role=query.ToList();
return userInfo;
}
- 服务层添加接口和方法,将仓储层返回的数据进行Dto转换
/// <summary>
/// 登录账号获取用户信息
/// </summary>
/// <param name="loginInput"></param>
/// <returns></returns>
Task<UserInfoRoleDto> LoginAsync(LoginInput loginInput);
public async Task<UserInfoRoleDto> LoginAsync(LoginInput loginInput)
{
var data=await _userInfoRepository.GetUserInfoRoleView(loginInput.Username,loginInput.Password);
var dto=_mapper.Map<UserInfoRoleDto>(data);
return dto;
}
- Controller方法接收账号密码,调用服务层方法,并将用户信息封装进Token返回。
app.MapPost("/LoginInput",async (IOptions<JWTOptions> options,IUserInfoService userInfoService,LoginInput input)=>{
var userInfo=await userInfoService.LoginAsync(input);
//设置角色
var roles=userInfo.Role.Select(x=>new Claim(ClaimsIdentity.DefaultRoleClaimType,x.Code!)).ToList();
//设置用户信息
roles.Add(new Claim(ClaimsIdentity.DefaultIssuer,JsonSerializer.Serialize(userInfo)));
var jwt = options.Value;
var keyByte = Encoding.UTF8.GetBytes(jwt.SecretKey);
var cred = new SigningCredentials(new SymmetricSecurityKey(keyByte), SecurityAlgorithms.HmacSha256);
var securityToken = new JwtSecurityToken(
jwt.Issuer,//签发者
jwt.Audience,//接收者
roles,//payload
expires: DateTime.Now.AddMinutes(jwt.ExpireMinutes),//过期时间
signingCredentials: cred//令牌
);
var token = new JwtSecurityTokenHandler().WriteToken(securityToken);
return token;
});
过滤器
- 创建响应视图(响应数据格式实体类)
/// <summary>
/// 响应视图
/// </summary>
public class ResponseView
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
public ResponseView(int code, string? message=null, object? data=null)
{
Code = code;
Message = message;
Data = data;
}
/// <summary>
/// 状态码
/// </summary>
/// <value></value>
public int Code { get; set; }
/// <summary>
/// 提示消息
/// </summary>
/// <value></value>
public string? Message { get; set; }
/// <summary>
/// 数据
/// </summary>
/// <value></value>
public object? Data { get; set; }
}
- 创建业务异常类(业务异常数据格式)
/// <summary>
/// 业务异常
/// </summary>
public class BusinessException:Exception
{
public int Code { get; set; }
public BusinessException(string message,int Code=400):base(message){
}
}
- 创建响应过滤器
/// <summary>
/// 过滤器
/// </summary>
public class ResponseFilter:ActionFilterAttribute
{
/// <summary>
///
/// </summary>
/// <param name="context"></param>
public override void OnActionExecuted(ActionExecutedContext context)
{
if(context.Result!=null){
if(context.Result is ObjectResult){
ObjectResult? objectResult=context.Result as ObjectResult;
if(objectResult?.Value?.GetType().Name==nameof(ResponseView)){
var result=objectResult.Value as ResponseView;
context.Result=new ObjectResult(result);
}
else{
context.Result=new ObjectResult(new ResponseView(200,data:objectResult?.Value));
}
}
else if(context.Result is EmptyResult){
context.Result=new ObjectResult(new ResponseView(200));
}
else if(context.Result is ResponseView modelStateResult){
context.Result=new ObjectResult(modelStateResult);
}
}else{
context.Result=new ObjectResult(new ResponseView(200));
}
base.OnActionExecuted(context);
}
}
- 创建异常过滤器
/// <summary>
/// 异常过滤器
/// </summary>
public class ExceptionFilter:ExceptionFilterAttribute
{
private readonly ILogger<ExceptionFilter> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="logger"></param>
public ExceptionFilter(ILogger<ExceptionFilter> logger)
{
_logger = logger;
}
/// <summary>
/// 异常处理
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task OnExceptionAsync(ExceptionContext context)
{
var ex=context.Exception;
_logger.LogError("Path {Path} message {Exception}",context.HttpContext.Request.Path,context.Exception);
if(ex is BusinessException exception){
context.Result=new OkObjectResult(new ResponseView(exception.Code,exception.Message));
}
else{
context.Result=new OkObjectResult(new ResponseView(500,ex.Message));
}
context.ExceptionHandled=true;
return Task.CompletedTask;
}
}
- 注册服务
builder.Services.AddMvcCore(options=>{
options.Filters.Add<ResponseFilter>();
options.Filters.Add<ExceptionFilter>();
});
修改用户信息
- 创建用户输入编辑信息实体模型
/// <summary>
/// 修改用户输入
/// </summary>
public class UpdateUserInfoInput
{
/// <summary>
/// 用户名
/// </summary>
/// <value></value>
public string? Name { get; set; }
/// <summary>
/// 密码(加密)
/// </summary>
/// <value></value>
public string? Password { get; set; }
/// <summary>
/// 头像
/// </summary>
/// <value></value>
public string? Avatar { get; set; }
}
- 应用层创建可获取用户Cliam的类,并添加获取Cliam中id和rolecode的方法
public class CurrentManage
{
private readonly IHttpContextAccessor _httpContext;
public CurrentManage(IHttpContextAccessor httpContextAccessor)
{
_httpContext = httpContextAccessor;
}
public Guid GetId(){
var value=_httpContext.HttpContext.User.Claims.FirstOrDefault(x=>x.Type==Constant.Id)?.Value;
if(string.IsNullOrEmpty(value)){
throw new BusinessException("账号未登录",401);
}
return Guid.Parse(value);
}
/// <summary>
/// 获取角色编码
/// </summary>
/// <returns></returns>
public IEnumerable<string> GetRole(){
return _httpContext.HttpContext.User.Claims.Where(x=>x.Type==ClaimsIdentity.DefaultRoleClaimType).Select(x=>x.Value);
}
}
- 注入IHttpContextAccessor服务
builder.Services.AddTransient<IHttpContextAccessor,HttpContextAccessor>();
- 用户服务层接口创建编辑用户信息方法
/// <summary>
/// 编辑用户信息
/// </summary>
/// <param name="updateUserInfoInput"></param>
/// <returns></returns>
Task UpdateAsync(UpdateUserInfoInput updateUserInfoInput);
- 实现接口
public async Task UpdateAsync(UpdateUserInfoInput input)
{
var userInfo=await _userInfoRepository.FirstOfDefaultAsync(x=>x.id==_currentManage.GetId());
if(userInfo==null){
throw new BusinessException("用户信息不存在");
}
userInfo.Name=input.Name;
userInfo.Avatar=input.Avatar;
userInfo.Password=input.Password;
await _userInfoRepository.UpdateAsync(userInfo);
await _unitOfWork.SaveChangesAsync();
}
用户注册实现
- 创建用户注册输入实体模型
/// <summary>
/// 注册输入
/// </summary>
public class RegisterInput
{
/// <summary>
/// 验证码
/// </summary>
/// <value></value>
public string? Code { get; set; }
/// <summary>
/// 用户名
/// </summary>
/// <value></value>
public string? Name { get; set; }
/// <summary>
/// 账号
/// </summary>
/// <value></value>
public string? UserName { get; set; }
/// <summary>
/// 密码(加密)
/// </summary>
/// <value></value>
public string? Password { get; set; }
/// <summary>
/// 头像
/// </summary>
/// <value></value>
public string? Avatar { get; set; }
}
- 契约层创建验证码服务接口以及验证码输入实体模型
/// <summary>
/// 验证码
/// </summary>
public interface ICodeService
{
/// <summary>
/// 获取验证码
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<string> GetAsync(CodeInput input);
}
/// <summary>
/// 验证码输入
/// </summary>
public class CodeInput
{
/// <summary>
/// 内容
/// </summary>
/// <value></value>
public string? Value { get; set; }
/// <summary>
/// 验证码类型
/// </summary>
/// <value></value>
public CodeType Type { get; set; }
}
- 应用层实现验证码接口
public class CodeService : ICodeService
{
private readonly RedisClient _redisClient;
public CodeService(RedisClient redisClient)
{
_redisClient = redisClient;
}
public async Task<string> GetAsync(CodeInput input)
{
var value=new Random().Next(9999).ToString("0000");
await _redisClient.SetExAsync($"{input.Type}:{input.Value}",60,value);
return value;
}
}
- 服务层接口添加注册并在服务层实现
/// <summary>
/// 注册账号
/// </summary>
/// <returns></returns>
Task<UserInfoRoleDto> RegisterAsync(RegisterInput registerInput);
public async Task<UserInfoRoleDto> RegisterAsync(RegisterInput registerInput)
{
var code=await _redisClient.GetAsync<string>($"{CodeType.Register}:{registerInput.UserName}");
if(code!=registerInput.Code){
throw new BusinessException("验证码错误");
}
if(await _userInfoRepository.isExistAsync(x=>x.UserName==registerInput.UserName)){
throw new BusinessException("用户名已存在!");
}
var data=_mapper.Map<UserInfo>(registerInput);
data=await _userInfoRepository.InsertAsync(data);
await _unitOfWork.SaveChangesAsync();
return _mapper.Map<UserInfoRoleDto>(data);
}
- 添加控制器添加注册方法
/// <summary>
/// 注册
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("Register")]
public async Task<string> RegisterAsync(RegisterInput input){
var userInfo=await _userInfoService.RegisterAsync(input);
//设置角色
var roles = userInfo.Role.Select(x => new Claim(ClaimsIdentity.DefaultRoleClaimType, x.Code!)).ToList();
//设置用户信息
roles.Add(new Claim(ClaimsIdentity.DefaultIssuer, JsonSerializer.Serialize(userInfo)));
roles.Add(new Claim(Constant.Id, userInfo.id.ToString()));
var jwt = _options.Value;
var keyByte = Encoding.UTF8.GetBytes(jwt.SecretKey);
var cred = new SigningCredentials(new SymmetricSecurityKey(keyByte), SecurityAlgorithms.HmacSha256);
var securityToken = new JwtSecurityToken(
jwt.Issuer,//签发者
jwt.Audience,//接收者
roles,//payload
expires: DateTime.Now.AddMinutes(jwt.ExpireMinutes),//过期时间
signingCredentials: cred//令牌
);
var token = new JwtSecurityTokenHandler().WriteToken(securityToken);
return token;
}
获取用户列表实现
- 在基层efcore里添加IQueryable类型的扩展方法WhereIf、PageBy
public static class QueryableExtensions
{
public static IQueryable<T> PageBy<T>(
this IQueryable<T> query,
int skipCount,
int maxResultCount)
{
return Queryable.Take<T>(Queryable.Skip<T>(query, skipCount), maxResultCount);
}
public static TQueryable PageBy<T, TQueryable>(
this TQueryable query,
int skipCount,
int maxResultCount)
where TQueryable : IQueryable<T>
{
return (TQueryable)Queryable.Take<T>(Queryable.Skip<T>(query, skipCount), maxResultCount);
}
public static IQueryable<T> WhereIf<T>(
this IQueryable<T> query,
bool condition,
Expression<Func<T, bool>> predicate)
{
return !condition ? query : query.Where<T>(predicate);
}
public static TQueryable WhereIf<T, TQueryable>(
this TQueryable query,
bool condition,
Expression<Func<T, bool>> predicate)
where TQueryable : IQueryable<T>
{
return !condition ? query : (TQueryable)query.Where<T>(predicate);
}
public static IQueryable<T> WhereIf<T>(
this IQueryable<T> query,
bool condition,
Expression<Func<T, int, bool>> predicate)
{
return !condition ? query : query.Where<T>(predicate);
}
public static TQueryable WhereIf<T, TQueryable>(
this TQueryable query,
bool condition,
Expression<Func<T, int, bool>> predicate)
where TQueryable : IQueryable<T>
{
return !condition ? query : (TQueryable)query.Where<T>(predicate);
}
}
- 创建分页请求和分页结果实体模型
public class PageRequestDto
{
private int _page=1;
private int _pageSize=20;
/// <summary>
/// 页码:默认1
/// </summary>
/// <value></value>
public int Page{
get=>_page;
set=>_page=value<=0?1:value;
}
/// <summary>
/// 页大小:默认20
/// </summary>
/// <value></value>
public int PageSize{
get=>_pageSize;
set=>_pageSize=value<=0?20:value;
}
/// <summary>
/// 忽略,只传page和pagesize
/// </summary>
/// <returns></returns>
[OpenApiIgnore]
public new int SkipCount => (Page-1)*MaxResultCount;
/// <summary>
/// 忽略
/// </summary>
[OpenApiIgnore]
public new int MaxResultCount =>
PageSize>1000
?1000
:PageSize;
}
public class PageResultDto<T>
{
/// <summary>
/// 分页数据
/// </summary>
/// <value></value>
public IReadOnlyList<T> Items { get; set; }
/// <summary>
/// 总数
/// </summary>
/// <value></value>
public int Count { get; set; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="count"></param>
/// <param name="items"></param>
public PageResultDto(int count, IReadOnlyList<T> items)
{
Count = count;
Items = items;
}
}
- 仓储层添加接口和实现
/// <summary>
/// 获取用户列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<List<UserInfo>> GetListAsync(string? keywords,DateTime? startTime,DateTime? endTime,int skipCount,int maxResultCount);
/// <summary>
/// 获取用户总数
/// </summary>
/// <param name="keywords"></param>
/// <param name="startTime"></param>
/// <param name="endTime"></param>
/// <returns></returns>
Task<int> GetCountAsync(string? keywords,DateTime? startTime,DateTime? endTime);
public async Task<int> GetCountAsync(string? keywords, DateTime? startTime, DateTime? endTime)
{
var query =CreateQueryable(keywords,startTime,endTime);
return await query.CountAsync();
}
public async Task<List<UserInfo>> GetListAsync(string? keywords, DateTime? startTime, DateTime? endTime, int skipCount, int maxResultCount)
{
var query =CreateQueryable(keywords,startTime,endTime);
return await query.PageBy(skipCount,maxResultCount).ToListAsync();
}
public IQueryable<UserInfo> CreateQueryable(string? keywords, DateTime? startTime, DateTime? endTime){
var query=
_dbContext.UserInfo.WhereIf(!string.IsNullOrEmpty(keywords),x=>EF.Functions.Like(x.Name,keywords)&&
EF.Functions.Like(x.UserName,keywords))
.WhereIf(startTime.HasValue,x=>x.CreateTime>=startTime)
.WhereIf(endTime.HasValue,x=>x.CreateTime<=endTime);
return query;
}
- 服务层添加接口和实现
/// <summary>
/// 获取用户列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<PageResultDto<UserInfoDto>> GetListAsync(GetListInput input);
public async Task<PageResultDto<UserInfoDto>> GetListAsync(GetListInput input)
{
var data=await _userInfoRepository.GetListAsync(input.Keywords,input.StartTime,input.EndTime,input.SkipCount,
input.MaxResultCount);
var count=await _userInfoRepository.GetCountAsync(input.Keywords,input.StartTime,input.EndTime);
var dto =_mapper.Map<List<UserInfoDto>>(data);
return new PageResultDto<UserInfoDto>(count,dto);
}
- 控制器添加方法
/// <summary>
/// 获取用户列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpGet("list")]
[Authorize(Roles ="admin")]
public async Task<PageResultDto<UserInfoDto>> GetListAsync([FromQuery]GetListInput input){
return await _userInfoService.GetListAsync(input);
}
删除,禁用用户实现
- 仓储层添加接口和方法
/// <summary>
/// 删除用户
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
Task DeleteAsync(IEnumerable<Guid> ids);
/// <summary>
/// 禁用用户
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
Task StatusAsync(IEnumerable<Guid> ids,bool status=true);
public async Task DeleteAsync(IEnumerable<Guid> ids)
{
await _dbContext.Database.ExecuteSqlRawAsync("DELETE FROM UserInfo WHERE Id In ({0})",string.Join(",",ids));
}
public async Task StatusAsync(IEnumerable<Guid> ids,bool status=true)
{
await _dbContext.Database.ExecuteSqlRawAsync("UPDATE UserInfo SET Status={0} WHERE Id In ({1})",status,string.Join(",",ids));
}
- 服务层添加接口和方法
/// <summary>
/// 删除用户
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
Task DeletesAsync(IEnumerable<Guid> ids);
/// <summary>
/// 禁用用户
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
Task StatusAsync(IEnumerable<Guid> ids,bool status=true);
public async Task DeletesAsync(IEnumerable<Guid> ids)
{
await _userInfoRepository.DeleteAsync(ids);
}
public async Task StatusAsync(IEnumerable<Guid> ids,bool status=true)
{
await _userInfoRepository.StatusAsync(ids,status);
}
- 添加控制器方法
/// <summary>
/// 删除用户
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[HttpDelete("list")]
[Authorize(Roles ="admin")]
public async Task DeleteAsync(IEnumerable<Guid> ids){
await _userInfoService.DeletesAsync(ids);
}
/// <summary>
/// 禁用用户
/// </summary>
/// <param name="ids"></param>
/// <param name="status"></param>
/// <returns></returns>
[HttpPut("status")]
[Authorize(Roles ="admin")]
public async Task StatusAsync(IEnumerable<Guid> ids,bool status=true){
await _userInfoService.StatusAsync(ids,status);
}