注:此博客僅適合於剛入門的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>:不需要額外定義視圖實體
寫到后面,發現有點亂了,不知怎么表達我想表達的東西了,就這樣吧,也不是多么有技術含量的設計,有興趣地看我代碼吧。