基於EF的一個簡單實戰型分層架構

注:此博客僅適合於剛入門的Asp.net Core初學者,僅作參考。git

 

學了3個月的Asp.net Core,將以前一個系統(http://caijt.com/it)的PHP後端用Asp.net Core重寫了一遍,http://it.caijt.com:1001 (注:是日本服務器,比較慢),剛入門時,我是想用DDD或ABP這種高大上的框架來重寫我以前的系統,後面我發現這些概念對我這個剛入門的初學者來講,理解起來仍是有點困難,也可能我經歷系統仍是比較簡單,用這些框架反而會比較麻煩。github

代碼: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等方法了。

 

用框架的目標都是一致的,不寫重複的代碼!對於框架,個人理解就是把通用的重複的代碼提取出來,寫成一個基類,而後在那麼須要個性化的地方挖坑,派生類中再對這些坑進行補充,這樣就實現了每一個派生類有基類的通用代碼,也能有派生類獨特的代碼。每一個派生類只寫跟別人不同的代碼,不寫重複性代碼。服務器

可能說得還不太能準確表達我想說的意思,下面以代碼展現。app

 

例如查詢列表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都不太同樣。那我就在這些地方挖坑。this

 

以下代碼,我定義了一個基類,裏面有selectExpression、 onInclude、onWhere、orderProp屬性,這些都是我挖的坑,哈哈。而後定義了一個通用的GetList方法,那麼派生類繼承於這個基類後,不用寫任何方法,就有了通用的GetList方法,若是須要具備字段查詢功能或字段排序功能的話,就在派生類的構造方法裏對這些坑進行賦值。spa

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對象接口的定義代碼.net

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>:不須要額外定義視圖實體

 

寫到後面,發現有點亂了,不知怎麼表達我想表達的東西了,就這樣吧,也不是多麼有技術含量的設計,有興趣地看我代碼吧。

相關文章
相關標籤/搜索