【asp.net core 系列】8 实战之 利用 EF Core 完成数据操作层的实现

0. 前言

通过前两篇,我们创建了一个项目,并规定了一个基本的数据层访问接口。这一篇,我们将以EF Core为例演示一下数据层访问接口如何实现,以及实现中需要注意的地方。

1. 添加EF Core

先在数据层实现层引入 EF Core:

cd Domain.Implementsdotnet add package Microsoft.EntityFrameworkCore

当前项目以SqlLite为例,所以再添加一个SqlLite数据库驱动:

dotnet add package Microsoft.EntityFrameworkCore.SQLite

删除 Domain.Implements 里默认的Class1.cs 文件,然后添加Insfrastructure目录,创建一个 DefaultContext:

using Microsoft.EntityFrameworkCore;namespace Domain.Implements.Insfrastructure{    public class DefaultContext : DbContext    {        private string ConnectStr { get; }        public DefaultContext(string connectStr)        {            ConnectStr = connectStr;        }        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)        {            optionsBuilder.UseSqlite(ConnectStr);//如果需要别的数据库,在这里进行修改        }    }}

2. EF Core 批量加载模型

通常情况下,在使用ORM的时候,我们不希望过度的使用特性来标注实体类。因为如果后期需要变更ORM或者出现其他变动的时候,使用特性来标注实体类的话,会导致迁移变得复杂。而且大部分ORM框架的特性都依赖于框架本身,并非是统一的特性结构,这样就会造成一个后果:本来应该是对调用方隐藏的实现就会被公开,而且在项目引用关系中容易出现循环引用。

所以,我在开发中会寻找是否支持配置类,如果使用配置类或者在ORM框架中设置映射关系,那么就可以保证数据层的纯净,也能实现对调用方隐藏实现。

EF Core的配置类我们在《C# 数据访问系列》中关于EF的文章中介绍过,这里就不做过多介绍了(没来得及看的小伙伴们不着急,后续会有一个简单版的介绍)。

通常情况下,配置类我也会放在Domain.Implements项目中。现在我给大家介绍一下如何快速批量加载配置类:

protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetAssembly(this.GetType()),t => t.GetInterfaces().Any(i => t.Name.Contains("IEntityTypeConfiguration")));}

现在版本的EF Core支持通过Assembly加载配置类,可以指定加载当前上下文类所在的Assembly,然后筛选实现接口中包含IEntityTypeConfiguration的类即可。

3. 使用EF Core实现数据操作

我们已经创建好了一个EF Context,那么现在就带领大家一起看一下,如何使用EF来实现 上一篇《「asp.net core」7 实战之 数据访问层定义》中介绍的数据访问接口:

新建一个BaseRepository类,在Domain.Implements项目的Insfrastructure 目录下:

using Domain.Infrastructure;using Microsoft.EntityFrameworkCore;namespace Domain.Implements.Insfrastructure{    public abstract class BaseRepository<T> : ISearchRepository<T>, IModifyRepository<T> where T : class    {        public DbContext Context { get; }        protected BaseRepository(DbContext context)        {            Context = context;        }    }}

先创建以上内容,这里给Repository传参的时候,使用的是EFCore的默认Context类不是我们自己定义的。这是我个人习惯,实际上并没有其他影响。主要是为了对实现类隐藏具体的EF 上下文实现类。

在实现各接口方法之前,创建如下属性:

public DbSet<T> Set { get => Context.Set<T>(); }

这是EF操作数据的核心所在。

3.1 实现IModifyRepository接口

先实现修改接口:

public T Insert(T entity){       return Set.Add(entity).Entity;}public void Insert(params T[] entities){    Set.AddRange(entities);}public void Insert(IEnumerable<T> entities){    Set.AddRange(entities);}public void Update(T entity){    Set.Update(entity);}public void Update(params T[] entities){    Set.UpdateRange(entities);}public void Delete(T entity){    Set.Remove(entity);}public void Delete(params T[] entities){    Set.RemoveRange(entities);}

在修改接口里,我预留了几个方法没有实现,因为这几个方法使用EF Core自身可以实现,但实现会比较麻烦,所以这里借助一个EF Core的插件:

dotnet add package Z.EntityFramework.Plus.EFCore

这是一个免费开源的插件,可以直接使用。在Domain.Implements 中添加后,在BaseRepository 中添加如下引用:

using System.Linq;using System.Linq.Expressions;

实现方法:

public void Update(Expression<Func<T, bool>> predicate, Expression<Func<T, T>> updator){    Set.Where(predicate).UpdateFromQuery(updator);}public void Delete(Expression<Func<T, bool>> predicate){    Set.Where(predicate).DeleteFromQuery();}public void DeleteByKey(object key){    Delete(Set.Find(key));}public void DeleteByKeys(params object[] keys){    foreach (var k in keys)    {        DeleteByKey(k);    }}

这里根据主键删除的方法有个问题,我们无法根据条件进行删除,实际上如果约定泛型T是BaseEntity的子类,我们可以获取到主键,但是这样又会引入另一个泛型,为了避免引入多个泛型根据主键的删除就采用了这种方式。

3.2 实现ISearchRepository 接口

获取数据以及基础统计接口:

public T Get(object key){    return Set.Find(key);}public T Get(Expression<Func<T, bool>> predicate){    return Set.SingleOrDefault(predicate);}public int Count(){    return Set.Count();}public long LongCount(){    return Set.LongCount();}public int Count(Expression<Func<T, bool>> predicate){    return Set.Count(predicate);}public long LongCount(Expression<Func<T, bool>> predicate){    return Set.LongCount(predicate);}public bool IsExists(Expression<Func<T, bool>> predicate){    return Set.Any(predicate);}

这里有一个需要关注的地方,在使用条件查询单个数据的时候,我使用了SingleOrDefault而不是FirstOrDefault。这是因为我在这里做了规定,如果使用条件查询,调用方应该能预期所使用条件是能查询出最多一条数据的。不过,这里可以根据实际业务需要修改方法:

  • Single 返回单个数据,如果数据大于1或者等于0,则抛出异常

  • SingleOrDefault 返回单个数据,如果结果集没有数据,则返回null,如果多于1,则抛出异常

  • First 返回结果集的第一个元素,如果结果集没有数据,则抛出异常

  • FirstOrDefault 返回结果集的第一个元素,如果没有元素则返回null

实现查询方法:

public List<T> Search(){    return Query().ToList();}public List<T> Search(Expression<Func<T, bool>> predicate){    return Query(predicate).ToList();}public IEnumerable<T> Query(){    return Set;}public IEnumerable<T> Query(Expression<Func<T, bool>> predicate){    return Set.Where(predicate);}public List<T> Search<P>(Expression<Func<T, bool>> predicate, Expression<Func<T, P>> order){    return Search(predicate, order, false);}public List<T> Search<P>(Expression<Func<T, bool>> predicate, Expression<Func<T, P>> order, bool isDesc){    var source = Set.Where(predicate);    if (isDesc)    {        source = source.OrderByDescending(order);    }    else    {        source = source.OrderBy(order);    }    return source.ToList();}

这里我尽量通过调用了参数最多的方法来实现查询功能,这样有一个好处,小伙伴们可以想一下哈。当然了,这是我自己觉得这样会好一点。

实现分页:

在实现分页之前,我们知道当时我们定义的分页参数类的排序字段用的是字符串,而不是lambda表达式,而Linq To EF需要一个Lambda表示才可以进行排序。这里就有两种方案,可以自己写一个方法,实现字符串到Lambda表达式的转换;第二种就是借用三方库来实现,正好我们之前引用的EF Core增强插件里有这个功能:

var list = context.Customers.OrderByDescendingDynamic(x => "x.Name").ToList();

这是它给出的示例。

我们可以先依此来写一份实现方法:

public PageModel<T> Search(PageCondition<T> condition){    var result = new PageModel<T>    {        TotalCount = LongCount(condition.Predicate),        CurrentPage = condition.CurrentPage,        PerpageSize = condition.PerpageSize,    };    var source = Query(condition.Predicate);    if (condition.Sort.ToUpper().StartsWith("a")) // asc    {        source = source.OrderByDynamic(t => $"t.{condition.OrderProperty}");    }    else // desc    {        source = source.OrderByDescendingDynamic(t => $"t.{condition.OrderProperty}");    }    var items = source.Skip((condition.CurrentPage -1)* condition.PerpageSize).Take(condition.PerpageSize);    result.Items = items.ToList();    return result;}

回到第一种方案:

我们需要手动写一个字符串的处理方法,先在Utils项目创建以下目录:Extend>Lambda,并在目录中添加一个ExtLinq类,代码如下:

using System.Linq;using System.Linq.Expressions;using System.Text.RegularExpressions;namespace Utils.Extend.Lambda{    public static class ExtLinq    {        public static IQueryable<T> CreateOrderExpression<T>(this IQueryable<T> source, string orderBy, string orderAsc)        {            if (string.IsNullOrEmpty(orderBy)|| string.IsNullOrEmpty(orderAsc)) return source;            var isAsc = orderAsc.ToLower() == "asc";            var _order = orderBy.Split(',');            MethodCallExpression resultExp = null;            foreach (var item in _order)            {                var orderPart = item;                orderPart = Regex.Replace(orderPart, @"\s+", " ");                var orderArry = orderPart.Split(' ');                var orderField = orderArry[0];                if (orderArry.Length == 2)                {                    isAsc = orderArry[1].ToUpper() == "ASC";                }                var parameter = Expression.Parameter(typeof(T), "t");                var property = typeof(T).GetProperty(orderField);                var propertyAccess = Expression.MakeMemberAccess(parameter, property);                var orderByExp = Expression.Lambda(propertyAccess, parameter);                resultExp = Expression.Call(typeof(Queryable), isAsc ? "OrderBy" : "OrderByDescending",                    new[] {typeof(T), property.PropertyType},                    source.Expression, Expression.Quote(orderByExp));            }            return resultExp == null                ? source                : source.Provider.CreateQuery<T>(resultExp);        }    }}

暂时不用关心为什么这样写,后续会为大家分析的。

然后回过头来再实现我们的分页,先添加Utils 到Domain.Implements项目中

cd ../Domain.Implements # 进入Domain.Implements 项目目录dotnet add reference ../Utils
public PageModel<T> Search(PageCondition<T> condition){    var result = new PageModel<T>    {        TotalCount = LongCount(condition.Predicate),        CurrentPage = condition.CurrentPage,        PerpageSize = condition.PerpageSize,    };    var source = Set.Where(condition.Predicate).CreateOrderExpression(condition.OrderProperty, condition.Sort);    var items = source.Skip((condition.CurrentPage -1)* condition.PerpageSize).Take(condition.PerpageSize);    result.Items = items.ToList();    return result;}

记得添加引用:

using Utils.Extend.Lambda;

在做分页的时候,因为前台传入的参数大多都是字符串的排序字段,所以到后端需要进程字符串到字段的处理。这里的处理利用了C# Expression的一个技术,这里就不做过多介绍了。后续在.net core高级篇中会有介绍。

4. 总结

到目前为止,看起来我们已经成功实现了利用EF Core为我们达成 数据操作和查询的目的。但是,别忘了EF Core需要手动调用一个SaveChanges方法。下一篇,我们将为大家介绍如何优雅的执行SaveChanges方法。

这一篇介绍到这里,虽然说明不是很多,但是这也是我在开发中总结的经验。

(0)

相关推荐

  • 【asp.net core 系列】9 实战之 UnitOfWork以及自定义代码生成

    0. 前言 在前一篇中我们创建了一个基于EF的数据查询接口实现基类,这一篇我将带领大家讲一下为这EF补充一些功能,并且提供一个解决避免写大量配置类的方案. 1. SaveChanges的外移 在之前介 ...

  • EF Core 基础知识

    EF Core 基础概念 概念 EF Core 全称是Entity Framework Core,可使用 EF Core 开发面向 .NET Core 的应用,EF Core 同时支持在 Visual ...

  • 通过反编译得到System.Core程序集中System.Linq命名空间下的Enumerable类

    通过反编译得到System.Core程序集中System.Linq命名空间下的Enumerable类如下所示: using System.Collections; using System.Colle ...

  • C# 基础知识系列-7 Linq详解

    前言 在上一篇中简单介绍了Linq的入门级用法,这一篇尝试讲解一些更加深入的使用方法,与前一篇的结构不一样的地方是,这一篇我会先介绍Linq里的支持方法,然后以实际需求为引导,分别以方法链的形式和类S ...

  • 【asp.net core 系列】10 实战之ActionFilter

    0.前言 在上一篇中,我们提到了如何创建一个UnitOfWork并通过ActionFilter设置启用.这一篇我们将简单介绍一下ActionFilter以及如何利用ActionFilter,顺便补齐一 ...

  • 【asp.net core 系列】2 控制器与路由的恩怨情仇

    0. 前言 在上一篇文章中,我们初步介绍了asp.net core,以及如何创建一个mvc项目.从这一篇开始,我将为大家展示asp.net core 的各种内容,并且尝试带领大家来挖掘其中的内在逻辑. ...

  • 【asp.net core 系列】5 布局页和静态资源

    0. 前言 在之前的4篇的内容里,我们较为详细的介绍了路由以及控制器还有视图之间的关系.也就是说,系统如何从用户的HTTP请求解析到控制器里,然后在控制器里处理数据,并返回给视图,在视图中显示出来.这 ...

  • 【asp.net core 系列】12 数据加密算法

    0. 前言 这一篇我们将介绍一下.net core 的加密和解密.在Web应用程序中,用户的密码会使用MD5值作为密码数据存储起来.而在其他的情况下,也会使用加密和解密的功能. 常见的加密算法分为对称 ...

  • 《ASP.NET Core项目开发实战入门》带你走进ASP.NET Core开发

    <ASP.NET Core项目开发实战入门>从基础到实际项目开发部署带你走进ASP.NET Core开发. ASP.NET Core项目开发实战入门是基于ASP.NET Core 3.1 ...

  • 发现君教你读研报系列之实战篇

    上周<发现君教你读研报系列之理论篇>讲解了应该读什么样的报告,如何读报告?本周我们将讲解如何找研报,如何找研报的重点? 1. 目的明确 即自己为什么有这个需求,需要做哪方面的研究或收集哪方 ...

  • C# 数据操作系列 - 6 EF Core 配置映射关系

    0. 前言 在<C# 数据操作系列 - 5. EF Core 入门>篇中,我们简单的通过两个类演示了一下EF增删改查等功能.细心的小伙伴可能看了生成的DDL SQL 语句,在里面发现了些端 ...

  • PoweBI实战:利用PBI打造私人理财账本

    废话不多说,直接进入正文. PoweBI实战:利用PBI打造私人理财账本 tiger_law 一.     写在前面 随着理财渠道的增多,理财产品的多样化,以及理财越来越便捷,很多人都有了理财的经历. ...