zoukankan      html  css  js  c++  java
  • 【asp.net core 系列】8 实战之 利用 EF Core 完成数据操作层的实现

    0. 前言

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

    1. 添加EF Core

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

    cd Domain.Implements
    dotnet 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方法。

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

    更多内容烦请关注我的博客《高先生小屋》

    file

  • 相关阅读:
    Security and Cryptography in Python
    Security and Cryptography in Python
    Security and Cryptography in Python
    Security and Cryptography in Python
    Security and Cryptography in Python
    Security and Cryptography in Python
    Security and Cryptography in Python
    微信小程序TodoList
    C语言88案例-找出数列中的最大值和最小值
    C语言88案例-使用指针的指针输出字符串
  • 原文地址:https://www.cnblogs.com/c7jie/p/13081316.html
Copyright © 2011-2022 走看看