在实际项目开发中,我们可能会碰到各种各样的项目环境,有些项目需要一个大而全的整体框架来支撑开发,有些中小项目这需要一些简单便捷的系统框架灵活开发。目前大型一点的框架,可以采用ABP或者ABP VNext的框架,两者整体思路和基础设计类似,不过ABP侧重于一个独立完整的项目框架,开发的时候统一整合处理;而ABP VNext则是以微服务架构为基础,各个模块独立开发,既可以整合在一个项目中,也可以以微服务进行单独发布,并统一通过网关处理进行交流。不管ABP或者ABP VNext框架,都集合了.NET CORE领域众多技术为一体,并且基础类设计上,错综复杂,关系较多,因此开发学习有一定的门槛,中小型项目应用起来有一定的费劲之处。本系列随笔介绍底层利用SqlSugar来做ORM数据访问模块,设计一个简单便捷一点的框架,本篇从基础开始介绍一些框架内容,参照一些ABP/ABP VNext中的一些类库处理,来承载类似条件分页信息,查询条件处理等处理细节。

1、基于SqlSugar开发框架的架构设计

主要的设计模块场景如下所示。

为了避免像ABP VNext框架那样分散几十个项目,我们尽可能聚合内容放在一个项目里面。

1)其中一些常用的类库,以及SqlSugar框架的基类放在框架公用模块里面。

2)Winform开发相关的基础界面以及通用组件内容,放在基础Winform界面库BaseUIDx项目中。

3)基础核心数据模块SugarProjectCore,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。其中Service里面一些框架基类和接口定义,统一也放在公用类库里面。

4)Winform应用模块,主要就是针对业务开发的WInform界面应用,而WInform开发为了方便,也会将一些基础组件和基类放在了BaseUIDx的Winform专用的界面库里面。

5)WebAPI项目采用基于.net Core6的项目开发,通过调用SugarProjectCore实现相关控制器API的发布,并整合Swagger发布接口,供其他前端界面应用进行调用。

6)纯前端通过API进行调用Web API的接口,纯前端模块可以包含Vue3&Element项目,以及基于EelectronJS应用,发布跨平台的基于浏览器的应用界面,以及其他App或者小程序整合Web API进行业务数据的处理或者展示需要。

如后端开发,我们可以在VS2022中进行管理,管理开发Winform项目、Web API项目等。

Winform界面,我们可以采用基于.net Framework开发或者.net core6进行开发均可,因为我们的SugarProjectCore项目是采用.net Standard模式开发,兼容两者。这里以权限模块来进行演示整合使用。

而纯前端的项目,我们可以基于VSCode或者 HBuilderX等工具进行项目的管理开发工作。

2、框架基础类的定义和处理

在开发一个易于使用的框架的时候,主要目的就是减少代码开发,并尽可能通过基类和泛型约束的方式,提高接口的通用性,并通过结合代码生成工具的方式,来提高标准项目的开发效率。

那么我们这里基于SqlSugar的ORM处理,来实现常规数据的增删改查等常规操作的时候,我们是如何进行这些接口的封装处理的呢。

例如,我们对于一个简单的客户信息表,如下所示。

那么它生成的SqlSugar实体类如下所示。

    /// <summary>
/// 客户信息
/// 继承自Entity,拥有Id主键属性
/// </summary>
[SugarTable("T_Customer")]
public class CustomerInfo : Entity<string>
{
/// <summary>
/// 默认构造函数(需要初始化属性的在此处理)
/// </summary>
public CustomerInfo()
{
this.CreateTime = System.DateTime.Now;
} #region Property Members /// <summary>
/// 姓名
/// </summary>
public virtual string Name { get; set; } /// <summary>
/// 年龄
/// </summary>
public virtual int Age { get; set; } /// <summary>
/// 创建人
/// </summary>
public virtual string Creator { get; set; } /// <summary>
/// 创建时间
/// </summary>
public virtual DateTime CreateTime { get; set; } #endregion
}

其中 Entity<string> 是我们根据需要定义一个基类实体对象,主要就是定义一个Id的属性来处理,毕竟对于一般表对象的处理,SqlSugar需要Id的主键定义(非中间表处理)。

    [Serializable]
public abstract class Entity<TPrimaryKey> : IEntity<TPrimaryKey>
{
/// <summary>
/// 实体类唯一主键
/// </summary>
[SqlSugar.SugarColumn(IsPrimaryKey = true, ColumnDescription = "主键")]
public virtual TPrimaryKey Id { get; set; }
}

而IEntity<T>定义了一个接口

    public interface IEntity<TPrimaryKey>
{
/// <summary>
/// 实体类唯一主键
/// </summary>
TPrimaryKey Id { get; set; }
}

以上就是实体类的处理,我们一般为了查询信息,往往通过一些条件传入进行处理,那么我们就需要定义一个通用的分页查询对象,供我们精准进行条件的处理。

生成一个以***PageDto的对象类,如下所示。

    /// <summary>
/// 用于根据条件分页查询,DTO对象
/// </summary>
public class CustomerPagedDto : PagedAndSortedInputDto, IPagedAndSortedResultRequest
{
/// <summary>
/// 默认构造函数
/// </summary>
public CustomerPagedDto() : base() { } /// <summary>
/// 参数化构造函数
/// </summary>
/// <param name="skipCount">跳过的数量</param>
/// <param name="resultCount">最大结果集数量</param>
public CustomerPagedDto(int skipCount, int resultCount) : base(skipCount, resultCount)
{
} /// <summary>
/// 使用分页信息进行初始化SkipCount 和 MaxResultCount
/// </summary>
/// <param name="pagerInfo">分页信息</param>
public CustomerPagedDto(PagerInfo pagerInfo) : base(pagerInfo)
{
} #region Property Members /// <summary>
/// 不包含的对象的ID,用于在查询的时候排除对应记录
/// </summary>
public virtual string ExcludeId { get; set; } /// <summary>
/// 姓名
/// </summary>
public virtual string Name { get; set; } /// <summary>
/// 年龄-开始
/// </summary>
public virtual int? AgeStart { get; set; }
/// <summary>
/// 年龄-结束
/// </summary>
public virtual int? AgeEnd { get; set; } /// <summary>
/// 创建时间-开始
/// </summary>
public DateTime? CreateTimeStart { get; set; }
/// <summary>
/// 创建时间-结束
/// </summary>
public DateTime? CreateTimeEnd { get; set; } #endregion
}

其中PagedAndSortedInputDto, IPagedAndSortedResultRequest都是参考来自于ABP/ABP VNext的处理方式,这样我们可以便于数据访问基类的查询处理操作。

接着我们定义一个基类MyCrudService,并传递如相关的泛型约束,如下所示

    /// <summary>
/// 基于SqlSugar的数据库访问操作的基类对象
/// </summary>
/// <typeparam name="TEntity">定义映射的实体类</typeparam>
/// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
/// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
public abstract class MyCrudService<TEntity, TKey, TGetListInput> :
IMyCrudService<TEntity, TKey, TGetListInput>
where TEntity : class, IEntity<TKey>, new()
where TGetListInput : IPagedAndSortedResultRequest

我们先忽略基类接口的相关实现细节,我们看看对于这个MyCrudService和 IMyCrudService 我们应该如何使用的。

首先我们定义一个应用层的接口ICustomerService如下所示。

    /// <summary>
/// 客户信息服务接口
/// </summary>
public interface ICustomerService : IMyCrudService<CustomerInfo, string, CustomerPagedDto>, ITransientDependency
{ }

然后实现在CustomerService中实现它的接口。

    /// <summary>
/// 应用层服务接口实现
/// </summary>
public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService

这样我们对于特定Customer的接口在ICustomer中定义,标准接口直接调用基类即可。

基类MyCrudService提供重要的两个接口,让子类进行重写,以便于进行准确的条件处理和排序处理,如下代码所示。

    /// <summary>
/// 基于SqlSugar的数据库访问操作的基类对象
/// </summary>
/// <typeparam name="TEntity">定义映射的实体类</typeparam>
/// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
/// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
public abstract class MyCrudService<TEntity, TKey, TGetListInput> :
IMyCrudService<TEntity, TKey, TGetListInput>
where TEntity : class, IEntity<TKey>, new()
where TGetListInput : IPagedAndSortedResultRequest
{
/// <summary>
/// 留给子类实现过滤条件的处理
/// </summary>
/// <returns></returns>
protected virtual ISugarQueryable<TEntity> CreateFilteredQueryAsync(TGetListInput input)
{
return EntityDb.AsQueryable();
}
/// <summary>
/// 默认排序,通过ID进行排序
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
protected virtual ISugarQueryable<TEntity> ApplyDefaultSorting(ISugarQueryable<TEntity> query)
{
if (typeof(TEntity).IsAssignableTo<IEntity<TKey>>())
{
return query.OrderBy(e => e.Id);
}
else
{
return query.OrderBy("Id");
}
}
}

对于Customer特定的业务对象来说,我们需要实现具体的条件查询细节和排序条件,毕竟我们父类没有约束确定实体类有哪些属性的情况下,这些就交给子类做最合适了。

    /// <summary>
/// 应用层服务接口实现
/// </summary>
public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
/// <summary>
/// 自定义条件处理
/// </summary>
/// <param name="input">查询条件Dto</param>
/// <returns></returns>
protected override ISugarQueryable<CustomerInfo> CreateFilteredQueryAsync(CustomerPagedDto input)
{
var query = base.CreateFilteredQueryAsync(input); query = query
.WhereIF(!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID
.WhereIF(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals
//年龄区间查询
.WhereIF(input.AgeStart.HasValue, s => s.Age >= input.AgeStart.Value)
.WhereIF(input.AgeEnd.HasValue, s => s.Age <= input.AgeEnd.Value) //创建日期区间查询
.WhereIF(input.CreateTimeStart.HasValue, s => s.CreateTime >= input.CreateTimeStart.Value)
.WhereIF(input.CreateTimeEnd.HasValue, s => s.CreateTime <= input.CreateTimeEnd.Value)
; return query;
} /// <summary>
/// 自定义排序处理
/// </summary>
/// <param name="query">可查询LINQ</param>
/// <returns></returns>
protected override ISugarQueryable<CustomerInfo> ApplyDefaultSorting(ISugarQueryable<CustomerInfo> query)
{
return query.OrderBy(t => t.CreateTime, OrderByType.Desc); //先按第一个字段排序,然后再按第二字段排序
//return base.ApplySorting(query, input).OrderBy(s=>s.Customer_ID).OrderBy(s => s.Seq);
}
}

通过 CreateFilteredQueryAsync 的精确条件处理,我们就可以明确实体类的查询条件处理,因此对于CustomerPagedDto来说,就是可以有客户端传入,服务后端的基类进行处理了。

如基类的分页条件查询函数GetListAsync就是根据这个来处理的,它的实现代码如下所示。

        /// <summary>
/// 根据条件获取列表
/// </summary>
/// <param name="input">分页查询条件</param>
/// <returns></returns>
public virtual async Task<PagedResultDto<TEntity>> GetListAsync(TGetListInput input)
{
var query = CreateFilteredQueryAsync(input);
var totalCount = await query.CountAsync(); query = ApplySorting(query, input);
query = ApplyPaging(query, input); var list = await query.ToListAsync(); return new PagedResultDto<TEntity>(
totalCount,
list
);
}

而其中 ApplySorting 就是根据条件决定是否选择子类实现的默认排序进行处理的。

        /// <summary>
/// 记录排序处理
/// </summary>
/// <returns></returns>
protected virtual ISugarQueryable<TEntity> ApplySorting(ISugarQueryable<TEntity> query, TGetListInput input)
{
//Try to sort query if available
if (input is ISortedResultRequest sortInput)
{
if (!sortInput.Sorting.IsNullOrWhiteSpace())
{
return query.OrderBy(sortInput.Sorting);
}
} //IQueryable.Task requires sorting, so we should sort if Take will be used.
if (input is ILimitedResultRequest)
{
return ApplyDefaultSorting(query);
} //No sorting
return query;
}

对于获取单一对象,我们一般提供一个ID主键获取即可。

        /// <summary>
/// 根据ID获取单一对象
/// </summary>
/// <param name="id">主键ID</param>
/// <returns></returns>
public virtual async Task<TEntity> GetAsync(TKey id)
{
return await EntityDb.GetByIdAsync(id);
}

也可以根据用户的Express条件进行处理,在基类我们定义很多这样的Express条件处理,便于子类进行条件处理的调用。如对于删除,可以指定ID,也可以指定条件删除。

        /// <summary>
/// 删除指定ID的对象
/// </summary>
/// <param name="id">记录ID</param>
/// <returns></returns>
public virtual async Task<bool> DeleteAsync(TKey id)
{
return await EntityDb.DeleteByIdAsync(id);
}
/// <summary>
/// 根据指定条件,删除集合
/// </summary>
/// <param name="input">表达式条件</param>
/// <returns></returns>
public virtual async Task<bool> DeleteAsync(Expression<Func<TEntity, bool>> input)
{
var result = await EntityDb.DeleteAsync(input);
return result;
}

如判断是否存在也是一样处理

        /// <summary>
/// 判断是否存在指定条件的记录
/// </summary>
/// <param name="id">ID 主键</param>
/// <returns></returns>
public virtual async Task<bool> IsExistAsync(TKey id)
{
var info = await EntityDb.GetByIdAsync(id);
var result = (info != null);
return result;
} /// <summary>
/// 判断是否存在指定条件的记录
/// </summary>
/// <param name="input">表达式条件</param>
/// <returns></returns>
public virtual async Task<bool> IsExistAsync(Expression<Func<TEntity, bool>> input)
{
var result = await EntityDb.IsAnyAsync(input);
return result;
}

关于Web API的处理,我在随笔《基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用》中也有介绍,主要就是先弄好.net6的开发环境,然后在进行相关的项目开发即可。

根据项目的需要,我们定义了一些控制器的基类,用于实现不同的功能。

其中ControllerBase是.net core Web API中的标准控制器基类,我们由此派生一个LoginController用于登录授权,而BaseApiController则处理常规接口用户身份信息,而BusinessController则是对标准的增删改查等基础接口进行的封装,我们实际开发的时候,只需要开发编写类似CustomerController基类即可。

BaseApiController没有什么好介绍的,就是封装一下获取用户的身份信息。

可以通过下面代码获取接口用户的Id

        /// <summary>
/// 当前用户身份ID
/// </summary>
protected virtual string? CurrentUserId => HttpContext.User.FindFirst(JwtClaimTypes.Id)?.Value;

而BusinessController控制器则是继承这个BaseApiController即可。通过泛型约束传入相关的对象信息。

    /// <summary>
/// 本控制器基类专门为访问数据业务对象而设的基类
/// </summary>
/// <typeparam name="TEntity">定义映射的实体类</typeparam>
/// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
/// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
[Route("[controller]")]
[Authorize] //需要授权登录访问
public class BusinessController<TEntity, TKey, TGetListInput> : BaseApiController
where TEntity : class, IEntity<TKey>, new()
where TGetListInput : IPagedAndSortedResultRequest
{
/// <summary>
/// 通用基础操作接口
/// </summary>
protected IMyCrudService<TEntity, TKey, TGetListInput> _service { get; set; } /// <summary>
/// 构造函数,初始化基础接口
/// </summary>
/// <param name="service">通用基础操作接口</param>
public BusinessController(IMyCrudService<TEntity, TKey, TGetListInput> service)
{
this._service = service;
} ....

这个基类接收一个符合基类接口定义的对象作为基类增删删改查等处理方法的接口对象。在具体的CustomerController中的定义处理如下所示。

    /// <summary>
/// 客户信息的控制器对象
/// </summary>
public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto>
{
private ICustomerService _customerService; /// <summary>
/// 构造函数,并注入基础接口对象
/// </summary>
/// <param name="customerService"></param>
public CustomerController(ICustomerService customerService) :base(customerService)
{
this._customerService = customerService;
}
}

这样就可以实现基础的相关操作了。如果需要特殊的接口实现,那么定义方法实现即可。

类似字典项目中的控制器处理代码如下所示。定义好HTTP方法,路由信息等即可。

        /// <summary>
/// 根据字典类型ID获取所有该类型的字典列表集合(Key为名称,Value为值)
/// </summary>
/// <param name="dictTypeId">字典类型ID</param>
/// <returns></returns>
[HttpGet]
[Route("by-typeid/{dictTypeId}")]
public async Task<Dictionary<string, string>> GetDictByTypeID(string dictTypeId)
{
return await _dictDataService.GetDictByTypeID(dictTypeId);
} /// <summary>
/// 根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)
/// </summary>
/// <param name="dictTypeName">字典类型名称</param>
/// <returns></returns>
[HttpGet]
[Route("by-typename/{dictTypeName}")]
public async Task<Dictionary<string, string>> GetDictByDictType(string dictTypeName)
{
return await _dictDataService.GetDictByDictType(dictTypeName);
}

基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用的更多相关文章

  1. 基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理

    在前面介绍的SqlSugar的相关查询处理操作中,我们主要以单表的方式生成相关的实体类,并在查询的时候,对单表的字段进行条件的对比处理,从而返回对应的数据记录.本篇随笔介绍在一些外键或者中间表的处理中 ...

  2. 基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发

    我喜欢在一个项目开发模式成熟的时候,使用代码生成工具Database2Sharp来配套相关的代码生成,对于我介绍的基于SqlSugar的开发框架,从整体架构确定下来后,我就着手为它们量身定做相关的代码 ...

  3. 基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理

    我们在设计数据库表的时候,往往为了方便,主键ID一般采用字符串类型或者GUID类型,这样对于数据库表记录的迁移非常方便,而且有时候可以在处理关联记录的时候,提前对应的ID值.但有时候进行数据记录插入的 ...

  4. 基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转

    在前面随笔,我们介绍过这个基于SqlSugar的开发框架,我们区分Interface.Modal.Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface ...

  5. 基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口

    在基于SqlSugar的开发框架中,我们设计了一些系统服务层的基类,在基类中会有很多涉及到相关的数据处理操作的,如果需要跟踪具体是那个用户进行操作的,那么就需要获得当前用户的身份信息,包括在Web A ...

  6. 基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录

    在我们对数据进行重要修改调整的时候,往往需要跟踪记录好用户操作日志.一般来说,如对重要表记录的插入.修改.删除都需要记录下来,由于用户操作日志会带来一定的额外消耗,因此我们通过配置的方式来决定记录那些 ...

  7. 基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理

    在早期的随笔就介绍过,把常规页面的内容拆分为几个不同的组件,如普通的页面,包括列表查询.详细资料查看.新增资料.编辑资料.导入资料等页面场景,这些内容相对比较独立,而有一定的代码量,本篇随笔介绍基于V ...

  8. 基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用

    在我们实际项目开发过程中,往往需要根据实际情况,对组件进行封装,以更简便的在界面代码中使用,在实际的前端应用中,适当的组件封装,可以减少很多重复的界面代码,并且能够非常简便的使用,本篇随笔介绍基于El ...

  9. 基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用

    刚完成一些前端项目的开发,腾出精力来总结一些前端开发的技术点,以及继续完善基于SqlSugar的开发框架循序渐进介绍的系列文章,本篇随笔主要介绍一下基于Vue3+TypeScript的全局对象的注入和 ...

随机推荐

  1. STM32试题及答案

    一.选择题 1.Cortex-M处理器采用的架构是(  D  ) (A)v4T               (B)v5TE              (C)v6                (D)v ...

  2. Mybatis入门程序(二)

    1.实现需求 添加用户 更新用户 删除用户 2.添加用户 (1)映射文件User.xml(Mapper)中,配置添加用户的Statement <!-- 添加用户: parameterType:指 ...

  3. Linux用户无法使用sudo命令

    新建的用无法使用sudo命令,出现这样的提示: xiaojing is not in the sudoers file. This incident will be reported 原来是新建的用户 ...

  4. 学习Apache(六)

    Apache 是一款使用量排名第一的 web 服务器,LAMP 中的 A 指的就是它.由于其开源.稳定.安全等特性而被广泛使用.下边记录了使用 Apache 以来经常用到的功能,做此梳理,作为日常运维 ...

  5. springboot使用redis实现发布与订阅

    配置redis连接地址 # Redis服务器地址 spring.redis.host=youxiu326.xin # Redis服务器连接端口 spring.redis.port=6379 # Red ...

  6. BUG战斗史 —— 日期格式与字符串之间的转换

    说在前面 最近在公司实习,接触了一个中小型的后台管理系统,不得不说,项目的目录结构比我平时做的"课程设计"要来得复杂,于是我先去看了Github上一些后台管理系统的模板项目 在gu ...

  7. Python模块导入方式

    import导入方式 from...import导入方式  from...import... 导入模块相当于在此文件中写了所导入函数名(对比c/c++中的.h文件来理解),所以在之后使用导入的函数直接 ...

  8. Unknown host mirrors.opencas.cn You may need to adjust the proxy settings in Gradle 报错及解决办法

    亲测Unknown host mirrors.opencas.cn You may need to adjust the proxy settings in Gradle 解决办法 - 程序员大本营 ...

  9. Jackson 和 fastJSON 导包异常

    内容 一.异常信息 HTTP Status 400 - type Status report message org.springframework.http.converter.HttpMessag ...

  10. Python入门-迭代器和生成器

    迭代演示 # 传统数据生成缺陷演示,编号操作未全部使用,会占用内存 #合适的做法,是需要的时候再生产,而不是全部生成好了再用 def generator(maxnum): print("[代 ...