基于EF的一个简单实战型分层架构
注:此博客仅适合于刚入门的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>:不需要额外定义视图实体
写到后面,发现有点乱了,不知怎么表达我想表达的东西了,就这样吧,也不是多么有技术含量的设计,有兴趣地看我代码吧。
基于EF的一个简单实战型分层架构的更多相关文章
- 基于PHP实现一个简单的在线聊天功能(轮询ajax )
基于PHP实现一个简单的在线聊天功能(轮询ajax ) 一.总结 1.用的轮询ajax 二.基于PHP实现一个简单的在线聊天功能 一直很想试着做一做这个有意思的功能,感觉复杂的不是数据交互和表结构,麻 ...
- 基于 Roslyn 实现一个简单的条件解析引擎
基于 Roslyn 实现一个简单的条件解析引擎 Intro 最近在做一个勋章的服务,我们想定义一些勋章的获取条件,满足条件之后就给用户颁发一个勋章,定义条件的时候会定义需要哪些参数,参数的类型,获取勋 ...
- 使用CEF(二)— 基于VS2019编写一个简单CEF样例
使用CEF(二)- 基于VS2019编写一个简单CEF样例 在这一节中,本人将会在Windows下使用VS2019创建一个空白的C++Windows Desktop Application项目,逐步进 ...
- 基于SOUI开发一个简单的小工具
基于DriectUI有很多库,比如 Duilib (免费) soui (免费) DuiVision (免费) 炫彩 (界面库免费,UI设计器付费,不提供源码) skinui (免费使用,但不开放源码, ...
- 基于node实现一个简单的脚手架工具(node控制台交互项目)
实现控制台输入输出 实现文件读写操作 全原生实现一个简单的脚手架工具 实现vue-cli2源码 一.实现控制台输入输出 关于控制台的输入输出依然是基于node进程管理对象process,在proces ...
- 【玩转开源】BananaPi R2 —— 第三篇 基于Openwrt开发一个简单的路由器
上一篇讲解了R2的网口配置,这一篇我们以BananaPi R2为例子来实现一个简单的路由器:那么一个简单的路由器应该具备什么样的功能呢?最简单的说就是wan+lan+ap这三个功能. 首先wan+la ...
- 基于MFC的一个简单计算器
写一个简单的计算器并不是什么很难的事,主要目的是要通过这个程序来学习和分析其中的核心算法.这个简易计算器的核心部分就是对输入的表达式的正确性判断与求值,其中包括对表达式的解析.中缀表达式转后缀表达式. ...
- 基于ABP做一个简单的系统——实战篇:1.项目准备
现阶段需要做一个小项目,体量很小,业务功能比较简单,就想到用最熟悉的.net来做,更何况现在.net core已经跨平台,也可以在linux服务器上部署.所以决定用.net core 3.1+mysq ...
- 基于ABP做一个简单的系统——实战篇:2.代码生成器
上一篇正说着呢,代码生成器就来了. 1.适用于ABP官网的Startup Template V3.x的包含了登录.用户等页面的MPA应用模板2.当前view仅支持文本框生成,远期规划根据字段类型生成不 ...
随机推荐
- 基于TCP协议之SSH
#SSH客户端 import socket # 1. 创建符合TCp协议的手机 client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # ...
- java基础- Java编程规范与注释
一 前言 java编程规约是指在java代码编写过程中通俗的约定:通常我们是要遵守这些规范:这好比我们在生活中要遵守的道德准则,如果你做的事情超出了道德的底线,那就有可能会受到社会抨击:在java编程 ...
- i++和++i的区别(主要为返回的值的区别)
初学者经常会搞不清i++,和++i 的关系 i++ 是把i的值拿过来,然后再+1++i 是吧i的值直接+1,之后再用
- 一些实用的 Laravel 小技巧
Laravel 中一些常用的小技巧,说不定你就用上了. 1.侧栏 网站一般都有侧栏,用来显示分类,标签,热门文章,热门评论啥的,但是这些侧栏都是相对独立的模块,如果在每一个引入侧栏的视图中都单独导入与 ...
- Blazor(WebAssembly) + .NETCore 实现斗地主
之前群里大神发了一个 html5+ .NETCore的斗地主,刚好在看Blazor WebAssembly 就尝试重写试试. 还有就是有些标题党了,因为文章里几乎没有斗地主的相关实现:),这里主要介绍 ...
- 消费者驱动的契约Consumer drivern Contract
消费者驱动的契约Consumer Driven Contracts (CDC) A contract between a consuming service and a providing servi ...
- CODING 代码多仓库实践
关于代码的管理问题已经讨论多年,随着企业业务的复杂度提高.软件行业技术栈的选择度变宽泛,现代软件的代码仓库也变得越来越庞大和复杂.一个中型项目,将测试代码.核心业务代码.编译构建.部署打包等基础设施的 ...
- 微信小程序—支付宝身份验证(支付宝小程序)
查看应用:https://open.alipay.com/platform/keyManage.htm 这里找到您调用接口的应用 支付宝身份验证快速接入:https://docs.open.alip ...
- 《Hands-On System Programming with Go》之目录操作
开一个新书<Hands-On System Programming with Go>,系统的了解一下, 这方面的东东,以前用C语言实现过, 现在用GO,重新来!! package main ...
- SpringCloud之Feign 负载均衡请求超时时间
版本声明: SpringCloud:Greenwich.SR4 SpringBoot:2.1.9.RELEASE Feign调用服务的默认时长是1秒钟,也就是如果超过1秒没连接上或者超过1秒没响应,那 ...