CJ.Blog


.Net Core 后端开发说明

环境说明 #

数据库:SqlServer 2012 FastNet_Core

ORM: SqlSugar

IOC/DI (依赖注入): AutoFac

环境:.NET Core 8

ORM SqlSugar 说明 #

文档参考:https://www.donet5.com/home/doc

配置项 #

"Datasource": [
  {	
    "Key": "Main",//数据库别名
    "Type": "MSSQL",//数据库类型
    "Connection"://连接字符串 "Server=43.137.40.151,16436;Database=Fast_Core;Uid=Sa;Pwd=151@#ddaqws2a3;",
    "ShowSql": true//是否打印Sql
  },
  {
    "Key": "Local",
    "Type": "MSSQL",
    "Connection": "Server=192.168.0.128,1433;Database=Fast_Core;Uid=Sa;Pwd=P@ssw0rd;",
    "ShowSql": true
  }
]

在appsettings中可以配置多个数据源,其中Key作为数据库标识。

如果需要获取其他数据库连接:

var childA = db.GetConnection("Local");

指定Key即可。

全局SqlSugar注入实现 Core.Persistence/SqlSugarExtensions.cs #

builder.Register(r =>
{
    return new SqlSugarScope(connectionConfigs,
        db =>
        {
            //(A)全局生效配置点,一般AOP和程序启动的配置扔这里面 ,所有上下文生效
            //调试SQL事件,可以删掉
            // db.Aop.OnLogExecuting = (sql, pars) =>
            // {
            //     //获取原生SQL推荐 5.1.4.63  性能OK
            //
            //     SqlLog.Info(UtilMethods.GetNativeSql(sql, pars));
            //     //获取无参数化SQL 对性能有影响,特别大的SQL参数多的,调试使用
            //     //Console.WriteLine(UtilMethods.GetSqlString(DbType.SqlServer,sql,pars))
            // };

            foreach (var o in connectionConfigs.Select(x => x.ConfigId))
            {
                db.GetConnection(o).Aop.OnLogExecuting = (sql, p) =>
                {
                    SqlLog.Info($"数据库 {o} Exec Sql:{sql} ===>> Params:{p}");
                };
            }
            //多个配置就写下面
            db.Ado.IsDisableMasterSlaveSeparation = true;

            //注意多租户 有几个设置几个
            //db.GetConnection(i).Aop
        });
}).SingleInstance().PropertiesAutowired();

通过AutoFac将SqlSugar注入全局单例上下文,SingleInstance 表示单例创建对象,PropertiesAutowired表示支持属性注入,如:

namespace Sys.Service
{
    public class PermissionMgrImpl : BaseMgr, IPermissionMgr
    {
        public IMapper mapper { get; set; }

        public SqlSugarScope sqlSugarScope { get; set; } //定义属性即可直接使用 必须是public访问权限

自动获取通用字段并更新 #

管理系统中常见的通用基本实体:

        string? CreateUser { get; set; } 
        DateTime? CreateDate { get; set; }
        string LastModifyUser { get; set; }
        DateTime LastModifyDate { get; set; }

创建人,创建时间,修改人,修改时间。

通过 SqlSugar 事件注册实现字段自动填充。

public static IApplicationBuilder UseSqlSugarAop(this IApplicationBuilder app)
{
    return  app.Use(async (context, next) =>
    {
        var sqlSugarScope = (SqlSugarScope)context.RequestServices.GetService(typeof(SqlSugarScope));
        if (sqlSugarScope != null)
        {
            sqlSugarScope.Aop.DataExecuting = (oldValue, entityInfo) =>
            {
                if (entityInfo.EntityValue is ITracable ||  entityInfo.EntityValue is IAuditable && entityInfo.OperationType == DataFilterType.InsertByObject)
                {
                    if (context.GetToken() != null)
                    {
                        var redisClient = (IRedisClient)context.RequestServices.GetService(typeof(IRedisClient));
                        User user = redisClient.Db0.GetAsync<User>($"user:{context.GetToken()}").Result;
                        if (user != null)
                        {
                            entityInfo.EntityValue.GetType().GetProperty("CreateUser").SetValue(entityInfo.EntityValue, user.UserName);
                            entityInfo.EntityValue.GetType().GetProperty("CreateDate").SetValue(entityInfo.EntityValue, DateTime.Now);
                        }
                    }
                }

需要Entity实现Core.Entity中的ITracable或ILogable,如果全部需要那么实现IAuditable。

Autofac和AutoMapper #

Autofac 是一个 IOC/DI框架,用于管理对象的生命周期,创建和销毁。

举个例子,后端开发中的典型架构:Entity ,Service , Controller。

其中Controller需要引入Service中的方法进行调用。

不需要在构造函数中注入,直接声明即可使用。

比如我有一个IUserMgr接口需要在UserController里面使用:

public class UserController : BaseController
{
    public IUserMgr userMgr { get; set; }

    public IMapper mapper { get; set; }

    public SqlSugarScope sqlSugarScope { get; set; }

    [HttpPost("[action]")]
    public async Task<IActionResult> Create(UserReqModel user)
    {
        await userMgr.CreateUserAsync(user);
        return Ok("创建成功");
    }

直接使用即可。

属性注入实现原理 #

   public static void AddSysServices(
        this ContainerBuilder builder)
    {
        // register service
        builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
            .Where(x => x.IsSubclassOf(typeof(BaseMgr))
                        && !x.GetInterfaces().Where(x => x.Name.Equals("IManualConstructMgr")).Any())
            .AsImplementedInterfaces().SingleInstance()
            .PropertiesAutowired().EnableInterfaceInterceptors();
    }
-----------------------------StartUp.cs-----------------------------------

    public void ConfigureContainer(ContainerBuilder builder)
    {
        builder.AddCoreServices();
        builder.AddSysServices();

    }

StartUp.cs中的ConfigureContainer方法用于管理注册所有依赖注入项。

 // register service
    builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
        .Where(x => x.IsSubclassOf(typeof(BaseMgr))
                    && !x.GetInterfaces().Where(x => x.Name.Equals("IManualConstructMgr")).Any())
        .AsImplementedInterfaces().SingleInstance() .PropertiesAutowired().EnableInterfaceInterceptors();

这段代码会查找所有实现BaseMgr接口的类进行依赖注入,并使用PropertiesAutowired属性注入,并对实现了IManualConstructMgr接口的类进行拦截。

AutoMapper #

AutoMapper是以.NET(C#)语言开发的一个轻量的处理一个实体对象到另一个实体对象之间映射关系的组件库。开发人员需要做的是通过AutoMapper配置两个实体对象之间的一些映射关系。就可以直接实现映射关系的复用,提高开发效率,减少重复代码。

一个经典的使用场景:

class User{
    public string username;//用户名
    public string password;//密码
}

现在前端需要查询User对象,那么User里面的Password属性不能让用户看到,于是会重新定义一个DTO/VO类。

class UserDTO{
    public string username;//用户名
    ...
}

于是代码中会出现:

userDto.username = user.username
...

使用AutoMapping来管理对象映射可以让项目中减少很多这种垃圾代码。

定义映射关系 #

在WebApi项目目录下会定义AutoMapperProfile:

public AutoMapperProfile()
{
    CreateMap<User, RespUserModel>();
    CreateMap<UserReqModel, User>();
    // 基本属性的映射
    CreateMap<Role, RespRoleModel>()
        .ForMember(dest => dest.Enable, opt => opt.MapFrom(src => src.Enable == RoleType.启用))
        .ForMember(dest => dest.PermissionIds, opt => opt.MapFrom(src => src.PermissionIds.Select(p => p.Id).ToList()));
    CreateMap<Permission, int>()
        .ConvertUsing(p => p.Id);
    CreateMap<RoleReq, Role>()
        .ForMember(dest => dest.Enable,
            opt => opt.MapFrom(src =>
                src.Enable ? RoleType.启用 : RoleType.禁用))
        .ForMember(dest => dest.PermissionIds, opt => opt.Ignore());
    CreateMap<PermissionReq, Permission>()
        .ForMember(dest => dest.Children, opt => opt.Ignore())
        .ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null));

    CreateMap<RespUserModel, User>();
    CreateMap<RespRoleModel, Role>();
    CreateMap<ReqLogin, User>();
    CreateMap<RespDetailModel, UserStorage>();
    CreateMap<Permission, PermissionView>()
        .ForMember(dest => dest.Layout, opt => opt.MapFrom(src => src.Layout ?? ""));
    CreateMap<PermissionView, Permission>()
        .ForMember(dest => dest.Layout, opt => opt.MapFrom(src => src.Layout ?? ""));

    CreateMap<UserStorage, RespDetailModel>();
    
}
使用场景(接收POST请求对象转换为Entity): #

POST实体对象

public class RoleReq
{
    public string RoleName;
    public string RoleCode;
    public bool Enable;
    public List<int> PermissionIds;
}
using System;
using System.Linq;
using System.Text;
using Core.Entity.Enum;
using SqlSugar;

namespace Sys.Entity
{
    ///<summary>
    ///
    ///</summary>
    [SugarTable("Sys_Role")]
    public partial class Role
    {

        public static string FINAL_ROLE = "SUPER_ADMIN";
           public Role(){


           }
           /// <summary>
           /// Desc:
           /// Default:
           /// Nullable:False
           /// </summary>           
           [SugarColumn(IsPrimaryKey=true,IsIdentity=true)]
           public int Id {get;set;}

           /// <summary>
           /// Desc:角色代码
           /// Default:
           /// Nullable:False
           /// </summary>           
           public string RoleCode {get;set;} = null!;

           /// <summary>
           /// Desc:角色名称
           /// Default:
           /// Nullable:False
           /// </summary>           
           public string RoleName {get;set;} = null!;

           /// <summary>
           /// Desc:1启动 0禁用
           /// Default:
           /// Nullable:False
           /// </summary>           
           public RoleType Enable {get;set;}

           [Navigate(typeof(RolePermission),  nameof(RolePermission.RoleId),nameof(RolePermission.PermissionId))]//注意顺序

           public List<Permission> PermissionIds{ get; set; }
    }
}

首先定义AutoMapper映射关系:

CreateMap<RoleReq, Role>();

其中Role实体类中的RoleType是枚举,而POST参数是bool,如何设置映射关系?

   CreateMap<RoleReq, Role>()
                .ForMember(dest => dest.Enable,
                    opt => opt.MapFrom(src =>
                        src.Enable ? RoleType.启用 : RoleType.禁用))

使用以下语句实现转换过程:

var pageData = mapper.Map<List<RespRoleModel>>(pageList); //将List<Role>转换为List<RespRoleModel>
Role role = mapper.Map<RoleReq, Role>(addRoleReq);//将请求的RoleReq转换为Role

权限相关/认证鉴权 #

登录逻辑 #

用户登录成功后签发GUID作为Token,同时将用户信息(角色信息,权限信息)保存在Redis数据库中:

Token:UserStorage

获取用户 #

UserStorage user = HttpContext.GetUser();

鉴权逻辑 #

if (ValidateToken && context.Request.Headers.TryGetValue(AppGlobalSettings.AuthConfig.HeadField, out Microsoft.Extensions.Primitives.StringValues authValue))
{
    var authstr = authValue.ToString();
    if (AppGlobalSettings.AuthConfig.Prefix.Length > 0)
    {
        if (!authstr.Contains(AppGlobalSettings.AuthConfig.Prefix))
        {
            context.Response.StatusCode = 401;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(JsonConvert.ToString(ApiResponse<object>.Fail("无权限认证", 401)));
            _logger.Warn($"token [ {authstr} ] 认证失败");
        }

        authstr = authValue.ToString().Substring(AppGlobalSettings.AuthConfig.Prefix.Length, authValue.ToString().Length - (AppGlobalSettings.AuthConfig.Prefix.Length));
    }
   
    bool isAuth = await _redisClient.Db0.ExistsAsync($"user:{authstr}");

配置文件:

"Auth": {
  "Expire": 60,
  "HeadField": "Authorization",
  //头字段
  "Prefix": "Bearer ",
  //前缀
  "RenewalTime": 1,
  //单位分钟,Token续期的时间间隔,10表示超过10分钟再次请求就续期
  "IgnoreUrls": [
    "/swagger/",
    "/Auth/Login",
    "/Auth/Captcha",
    "/Permission/GetPermissionTree2"
  ]
},

通过请求拦截中间件实现,所有请求都会校验是否拥有请求头HeadField,也就是Authorization。

比如:Authorization: Bearer XXX

image-20240828145828619

而Bearer 后面的Guid就是用户token,后端就可以根据token直接去Redis查询到用户信息。

根据Expire对存储的Token设置过期时间,Redis内置此功能,当时间过期Redis会删除键值对,后端查不到用户信息也就表示登录过期和未认证。

接口鉴权 #

目前定义了两个注解:

CheckPermissionAttribute 和 CheckRoleAttribute。

用法:

[HttpPost("[action]")]
[CheckRole("SUPER_ADMIN")] //表示校验当前用户是否拥有SuperAdmin权限,如校验多个权限,用逗号分隔。[CheckRole("SUPER_ADMIN,X,X,")] 
public async Task<IActionResult> ResetPassword(ResetPasswordReq resetPassword)
{
    
    User user = await sqlSugar.Queryable<User>().Where(x => x.UserName.Equals(resetPassword.UserName)).FirstAsync();
    
    if (user.Equals(resetPassword.NewPass.EncryptDES(AppGlobalSettings.Secret.User)))
    {
        throw new BusinessException("新旧密码不能一致");
    }

    user.Password = resetPassword.NewPass.EncryptDES(AppGlobalSettings.Secret.User);
    await sqlSugar.Updateable<User>(user).ExecuteCommandAsync();
    return Ok("修改成功");
}

CheckPermission 使用同CheckRole

实体生成 #

项目中包含GeneratePOJO这个项目:

using SqlSugar;

namespace GeneratePOJO
{
    internal class Program
    {
        static void Main(string[] args)
        {
            SqlSugarClient db = new SqlSugarClient(new ConnectionConfig()
            {
                ConfigId = "Main",
                ConnectionString = "Server=43.137.40.151,16436;Database=Fast_Core;Uid=Sa;Pwd=151@#ddaqws2a3;",
                DbType = DbType.SqlServer,
                IsAutoCloseConnection = true
            },
                 db => {

                     db.Aop.OnLogExecuting = (sql, pars) =>
                     {

                         //获取原生SQL推荐 5.1.4.63  性能OK
                         Console.WriteLine(UtilMethods.GetNativeSql(sql, pars));

                         //获取无参数化SQL 对性能有影响,特别大的SQL参数多的,调试使用
                         //Console.WriteLine(UtilMethods.GetSqlString(DbType.SqlServer,sql,pars))


                     };

                     //注意多租户 有几个设置几个
                     //db.GetConnection(i).Aop

                 }); ;

            db.DbFirst
                .Where(x=>x.Contains("Dict")).IsCreateDefaultValue().FormatFileName(x => x.Split("_")[1]).FormatClassName(x => x.Split("_")[1]).IsCreateAttribute().StringNullable()

               .CreateClassFile("../../../Model/", "Sys.Entity");

        }
    }
}

定义数据库连接后,重点就一句话:

db.DbFirst
    .Where(x=>x.Contains("Dict")).IsCreateDefaultValue().FormatFileName(x => x.Split("_")[1]).FormatClassName(x => x.Split("_")[1]).IsCreateAttribute().StringNullable()

   .CreateClassFile("../../../Model/", "Sys.Entity");

这段代码表示获取所有表名包含Dict的表,创建带问好的类型,将表的下划线去掉生成文件名,将表的下划线去掉生成类名。

CreateClassFile表示生成到当前项目目录下的Model,Sys.Emity表示命名空间。

可以根据自己的情况修改生成实体。

这个代码不进入代码管理,无需提交。

结构说明 #

image-20240828151246096

开发流程 #

如果是开发业务相关功能,那么可以重新建立模块,比如做一个充电桩项目,在项目中创建Charge文件夹,创建:

Charge.Entity 实体

Charge.Service 服务

Charge.WebApi Web接口

其他根据需要自行创建Charge.XXX

这种好处是减少与Core和Sys模块耦合,结构清晰易于管理和查看,可插拔式的模块化。