视频管理系统项目实战

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);
}
posted @ 2022-11-14 15:40  lrplrplrp  阅读(289)  评论(0编辑  收藏  举报