经过前两篇,咱们建立了一个项目,并规定了一个基本的数据层访问接口。这一篇,咱们将以EF Core为例演示一下数据层访问接口如何实现,以及实现中须要注意的地方。数据库
先在数据层实现层引入 EF Core:c#
cd Domain.Implements dotnet add package Microsoft.EntityFrameworkCore
当前项目以SqlLite为例,因此再添加一个SqlLite数据库驱动:后端
dotnet add package Microsoft.EntityFrameworkCore.SQLite
删除 Domain.Implements 里默认的Class1.cs 文件,而后添加Insfrastructure目录,建立一个 DefaultContext:bash
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);//若是须要别的数据库,在这里进行修改 } } }
一般状况下,在使用ORM的时候,咱们不但愿过分的使用特性来标注实体类。由于若是后期须要变动ORM或者出现其余变更的时候,使用特性来标注实体类的话,会致使迁移变得复杂。并且大部分ORM框架的特性都依赖于框架自己,并不是是统一的特性结构,这样就会形成一个后果:原本应该是对调用方隐藏的实现就会被公开,并且在项目引用关系中容易出现循环引用。框架
因此,我在开发中会寻找是否支持配置类,若是使用配置类或者在ORM框架中设置映射关系,那么就能够保证数据层的纯净,也能实现对调用方隐藏实现。asp.net
EF Core的配置类咱们在《C# 数据访问系列》中关于EF的文章中介绍过,这里就不作过多介绍了(没来得及看的小伙伴们不着急,后续会有一个简单版的介绍)。ide
一般状况下,配置类我也会放在Domain.Implements项目中。如今我给你们介绍一下如何快速批量加载配置类:ui
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的类便可。this
咱们已经建立好了一个EF Context,那么如今就带领你们一块儿看一下,如何使用EF来实现 上一篇《「asp.net core」7 实战之 数据访问层定义》中介绍的数据访问接口:spa
新建一个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操做数据的核心所在。
先实现修改接口:
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的子类,咱们能够获取到主键,可是这样又会引入另外一个泛型,为了不引入多个泛型根据主键的删除就采用了这种方式。
获取数据以及基础统计接口:
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。这是由于我在这里作了规定,若是使用条件查询,调用方应该能预期所使用条件是能查询出最多一条数据的。不过,这里能够根据实际业务须要修改方法:
实现查询方法:
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高级篇中会有介绍。
到目前为止,看起来咱们已经成功实现了利用EF Core为咱们达成 数据操做和查询的目的。可是,别忘了EF Core须要手动调用一个SaveChanges方法。下一篇,咱们将为你们介绍如何优雅的执行SaveChanges方法。
这一篇介绍到这里,虽说明不是不少,可是这也是我在开发中总结的经验。
更多内容烦请关注个人博客《高先生小屋》