注:此博客仅适合于刚入门的Asp.net Core初学者,仅做参考。
学了3个月的Asp.net Core,将之前一个系统(http://caijt.com/it)的PHP后端用Asp.net Core重写了一遍,http://it.caijt.com:1001 (注:是日本服务器,比较慢),刚入门时,我是想用DDD或ABP这种高大上的框架来重写我之前的系统,后面我发现这些概念对我这个刚入门的初学者来说,理解起来还是有点困难,也可能我经历系统还是比较简单,用这些框架反而会比较麻烦。
代码:https://github.com/Caijt/ItSysAspNetCore
以下是我的分层图,非常简单的分层,连标准三层都不是,用了EF我觉得Respository仓储层没必要,如果是用Sql或Dapper的话,就会加个Respository层
- ItSys:UI层,Asp.net Core 项目类型为WebApi接口
- ItSys.Dto:数据传输对象
- ItSys.Entity:实体层,一般是一个实体对应数据库一个表,也有一个实体对应视图
- ItSys.EntityFramework:EF Core层
- ItSys.Service:服务层,封装了几个主要Service基层,里面主要封装了GetList(获取列表)、GetPageList(获取分页列表)、Create(创建实体)、Update(更新实体)、Delete(删除实体)通用方法,实体的Service类只要继承了某个Service类,就具备了GetList、GetPageList等方法了。
用框架的目标都是一致的,不写重复的代码!对于框架,我的理解就是把通用的重复的代码提取出来,写成一个基类,然后在那么需要个性化的地方挖坑,派生类中再对这些坑进行补充,这样就实现了每个派生类有基类的通用代码,也能有派生类独特的代码。每个派生类只写跟别人不一样的代码,不写重复性代码。
可能说得还不太能准确表达我想说的意思,下面以代码展示。
例如查询列表GetList功能,我用EF的话,那我IT资产及IT合同的Service类,需要这样写
//IT资产查询列表方法 public List<ItAssetDto> GetList(ItAssetQueryDto queryParams) { var query = dbContext.Set<ItAsset>().AsQueryable(); query = query.Include(e => e.CreateUser); #region 资产编号 if (!string.IsNullOrWhiteSpace(queryParams.no)) { query = query.Where(e => e.no.Contains(queryParams.no)); } #endregion #region 资产型号 if (!string.IsNullOrWhiteSpace(queryParams.model)) { query = query.Where(e => e.no.Contains(queryParams.model)); } #endregion #region 标识号 if (!string.IsNullOrWhiteSpace(queryParams.diy_no)) { query = query.Where(e => e.diy_no.Contains(queryParams.diy_no)); } #endregion if (queryParams.sortOrder == "no") { query = query.OrderBy(e => e.no); } if (queryParams.sortOrder == "inbound_date") { query = query.OrderBy(e => e.inbound_date); } return query.Select(e => new ItAssetDto { no = e.no, inbound_date = e.inbound_date, id = e.Id }) .ToList(); }
//IT合同查询列表方法 public List<ItContractDto> GetList(ItContractQueryDto queryParams) { var query = dbContext.Set<ItContract>().AsQueryable(); query = query.Include(e => e.CreateUser).Include(e=>e.Supplier); #region 合同编号 if (!string.IsNullOrWhiteSpace(queryParams.no)) { query = query.Where(e => e.no.Contains(queryParams.no)); } #endregion #region 合同名称 if (!string.IsNullOrWhiteSpace(queryParams.name)) { query = query.Where(e => e.name.Contains(queryParams.name)); } #endregion return query.Select(e => new ItContractDto { no = e.no, name = e.name, id = e.Id }) .ToList(); }
有没有从其中发现一些重复,但又不重复的地方,重复的是 dbContext.Set<实体>().Include().Where().OrderBy().Select().ToList();不同的是每个实体表它的Where、Include、OrderBy、Select都不太一样。那我就在这些地方挖坑。
如下代码,我定义了一个基类,里面有selectExpression、 onInclude、onWhere、orderProp属性,这些都是我挖的坑,哈哈。然后定义了一个通用的GetList方法,那么派生类继承于这个基类后,不用写任何方法,就有了通用的GetList方法,如果需要具有字段查询功能或字段排序功能的话,就在派生类的构造方法里对这些坑进行赋值。
using AutoMapper; using ItSys.Dto; using ItSys.EntityFramework; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Text; namespace ItSys.Service.Base { public class BaseService<TEntity, TDto, TQueryDto> where TEntity : class where TQueryDto : IQueryDto { protected ItSysDbContext dbContext; protected IMapper mapper; /// <summary> /// 实体转化为Dto对象的表达式 /// </summary> protected Expression<Func<TEntity, TDto>> selectExpression { get; set; } /// <summary> /// 构建Include关联属性数据 /// </summary> /// <param name="query"></param> /// <returns></returns> protected Func<IQueryable<TEntity>, IQueryable<TEntity>> onInclude { get; set; } /// <summary> /// 构建Where查询 /// </summary> /// <param name="query"></param> /// <returns></returns> protected Func<IQueryable<TEntity>, TQueryDto, IQueryable<TEntity>> onWhere { get; set; } /// <summary> /// 根据排序字段的字符串返回一个排序表达式 /// </summary> protected Func<string, Expression<Func<TEntity, dynamic>>> orderProp { get; set; } public BaseService(ItSysDbContext dbContext, IMapper mapper) { this.dbContext = dbContext; this.mapper = mapper; selectExpression = e => mapper.Map<TDto>(e); } protected List<TDto> GetList(TQueryDto queryParams) { var query = dbContext.Set<TEntity>().AsNoTracking(); #region 加载导航属性 if (onInclude != null) { query = onInclude(query); } #endregion #region 查询条件 if (onWhere != null) { query = onWhere(query, queryParams); } #endregion #region 排序 var exp = orderProp != null ? orderProp(queryParams.orderProp) : null; if (exp != null) { query = queryParams.orderDesc.GetValueOrDefault(true) ? query.OrderByDescending(exp) : query.OrderBy(exp); } #endregion return query.Select(selectExpression).ToList(); } } }
下面是上面代码IQueryDto对象接口的定义代码
namespace ItSys.Dto { public interface IQueryDto { /// <summary> /// 每页数量 /// </summary> int pageSize { get; set; } /// <summary> /// 当前页 /// </summary> int currentPage { get; set; }/// <summary> /// 排序字段 /// </summary> string orderProp { get; set; } /// <summary> /// 是否倒序排序 /// </summary> bool? orderDesc { get; set; } } }
现在的IT资产的Service类就可以很简单了,继承BaseService,泛型类型第一个是实体类型ItAsset,第二个是对应的Dto对象ItAssetDto,第三个是实现了IQueryDto接口的查询参数对象ItAssetQueryDto,然后不用写一个方法,只要在构造方法里对onWhere、OrderProp及SelectExpression属性配置就好了。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using AutoMapper; using ItSys.Dto; using ItSys.Entity; using ItSys.EntityFramework; using ItSys.Service.Base; namespace ItSys.Service.It { public class ItAssetService : BaseService<ItAsset, ItAssetDto, ItAssetQueryDto> { public ItAssetService(ItSysDbContext dbContext, IMapper mapper) : base(dbContext, mapper) { //定义Where的坑 onWhere = (query, queryParams) => { #region 资产编号 if (!string.IsNullOrWhiteSpace(queryParams.no)) { query = query.Where(e => e.no.Contains(queryParams.no)); } #endregion #region 资产型号 if (!string.IsNullOrWhiteSpace(queryParams.model)) { query = query.Where(e => e.no.Contains(queryParams.model)); } #endregion return query; }; //定义Order的坑 orderProp = prop => { switch (prop) { case "create_time": return e => e.CreateTime; case "update_time": return e => e.UpdateTime; case "no": return e => e.no; } return null; }; //定义Select的坑 selectExpression = e => new ItAssetDto { id = e.Id, no = e.no, model = e.model }; } } }
按照这个思路,给Create,Update,Delete方法也挖坑,我是在三个方法的之前跟之后分别挖了两个坑,因为有些实体创建时我需要给某些字段定义初始值,例如create_time字段,我可以在onBeforeCreate给实体初始化create_time值,有些实体我需要在更新时定义字段值,例如update_time,我在onBeforeUpdate初始化update_time的值,有些实体的删除,我需要在删除之前查询此实体跟其它表还有没有关联,我可以在onBeforeDelete中查询。
下面以Create代码为例
protected Action<TEntity, TCreateDto> onBeforeCreate { get; set; } protected Action<TEntity, TCreateDto> onAfterCreate { get; set; } /// <summary> /// 创建实体 /// </summary> /// <returns></returns> public virtual TDto Create(TCreateDto createDto) { var entity = mapper.Map<TEntity>(createDto); if (onBeforeCreate != null) { onBeforeCreate(entity, createDto); } dbSet.Add(entity); dbContext.SaveChanges(); if (onAfterCreate != null) { onAfterCreate(entity, createDto); } return mapper.Map<TDto>(entity); }
如果从github下载了我的代码看后会发现里面的代码跟上面的代码还有很大差别,因为我把方法拆得更细,主要考虑一些特殊情况,方法拆细点,可以实现更多特殊操作,不过思路是一样的,都是按上面的方式,在适合的地点挖坑。
介绍我这几个主要的Service基类,我是实体的一些通用特点(例如说某些实体都有Id主键,某些实体都有create_time、update_time字段)进行定义的
- ViewService<TViewEntity, TDto, TQueryDto>:这个主要用于视图查询,没有增删改操作;
- EntityViewService<TEntity,TViewEntity,TDto,TCreateDto,TUpdateDto,TQueryDto> :需定义实体与视图实体,因为我有一些实体的查询列表会比较麻烦,比如查询时还要统计某些关联记录的值,在EF中查询起来很不方便,所以在数据库再创建对应的视图查询,同时在系统中定义对应的视图实体,实体就用来增删改,视图实体就用来查;
- EntityService<TEntity, TDto, TCreateDto, TUpdateDto, TQueryDto> :当实体的查询列表没那么复杂时,可只定义一个实体,也就是实体跟视图实体是一致的
- IdEntityViewService<TEntity, TViewEntity,TDto,TCreateDto,TUpdateDto,TQueryDto> :实体都具有Id主键,这个基类里默认会对Id主键字段进行统一的配置,例如默认对Id排序
- IdEntityService<TEntity, TDto, TCreateDto, TUpdateDto, TQueryDto> :不需要额外定义视图实体
- AuditViewService<TEntity, TViewEntity, TDto, TCreateDto, TUpdateDto, TQueryDto> :实体都具有主键Id字段、create_time字段、create_user_id字段、update_time字段、update_user_id字段,这个基层默认会在创建时更新时对create_time、update_time字段进行赋值以及排序字段的配置;
- AuditService<TEntity, TDto, TCreateDto, TUpdateDto, TQueryDto>:不需要额外定义视图实体
- AuditCompanyViewService<TEntity, TViewEntity, TDto, TCreateDto, TUpdateDto, TQueryDto>:在AuditViewService的基础上,实体还具有Company_id字段,因为我的系统里,很多数据都是需要根据当前登录用户的所具有公司管理权限过滤相应的数据,这个Service默认会在查询时进行Company_id字段的过滤
- AuditCompanyService<TEntity, TDto, TCreateDto, TUpdateDto, TQueryDto>:不需要额外定义视图实体
写到后面,发现有点乱了,不知怎么表达我想表达的东西了,就这样吧,也不是多么有技术含量的设计,有兴趣地看我代码吧。