tdfblog

两个星期前,微软发布了EF Core 2.1 Preview 1,同时还发布了.NET Core 2.1 Preview 1ASP.NET Core 2.1 Preview 1;EF Core 2.1 Preview 1 除了许多小改进和超过100种产品错误修复之外,还包括几个常用的新功能,今天我为您详细介绍这些新功能的部分内容。

实体构造函数参数

EF.Core 2.1开始支持在实体的构造函数的实体中转入参数,目前支持的类型如下:

  • 实体属性
  • IOC容器中注册的服务
  • 当前的DbContext
  • 当前实体的元数据

实体属性

在某些情况下为了保证数据的安全性,将属性改为只读,在构造函数中传递属性的值,框架通过参数与属性匹配关系,将数据行中属性的值作为参数传递给构造函数。

例如下面的实体:

    public class Order
    {
        public Order(int orderID, string customerID, DateTime? orderDate)
        {
            OrderID = orderID;
            CustomerID = customerID;
            OrderDate = orderDate;
        }

        public int OrderID { get; }
        
        public string CustomerID { get; }

        public DateTime? OrderDate { get; }

    }

其中参数与属性的配置规则如下:

  • 参数的类型与属性的类型一致;
  • 属性名与参数名除首字母不区分大小写之外,其它字符一致,并且可以使用 _m_做为前缀,使用OrderID属性来举例,存在如下匹配规则:

    属性名 参数名
    OrderID OrderID
    OrderID orderID
    _OrderID orderID
    _OrderID OrderID
    m_OrderID OrderID
    m_OrderID OrderID

具体的匹配规则可以见Github上面的源代码:https://github.com/aspnet/EntityFrameworkCore/blob/8965f0b91cf89e36abca8636d58420cbd26c22fd/src/EFCore/Metadata/Internal/PropertyParameterBindingFactory.cs#L37-L45
不过我认识后面四种模式有待斟酌的,在.Net开发规范,应该没有人将公有的属性名使用 _m_作为前缀。

IOC容器中注册的服务

在实体的构造函数的中,可以将注册的服务作为参数。

示例代码:

    public class Order
    {
        private ILazyLoader _lazyLoader;

        public Order(ILazyLoader lazyLoader)
        {
            this._lazyLoader = lazyLoader;
        }

        public int OrderID { get; set; }
        
        public string CustomerID { get; set; }

        private ICollection<OrderDetail> _orderDetails;

        public ICollection<OrderDetail> OrderDetails
        {
            get => _lazyLoader.Load(this, ref _orderDetails);
            set => _orderDetails = value;
        }
    }

}

其中ILazyLoader是EF Core框架在容器中注册的一个服务,通过实体的构造函数中传入,实现导航属性的赖加载(关于ILazyLoader的具体使用方式在本章的下一节中讲解)。

当前的DbContext

在实体的构造函数的参数中,将当前的DbContext作为参数。

示例代码:

    public class Order
    {
        private NorthwindContext _northwindContext;

        public Order(NorthwindContext northwindContext)
        {
            this._northwindContext = northwindContext;
        }

        public int OrderID { get; set; }
        
        public string CustomerID { get; set; }

        private ICollection<OrderDetail> _orderDetails;

        [NotMapped]
        public ICollection<OrderDetail> OrderDetails
        {
            get
            {
                if (this._orderDetails == null)
                    this._orderDetails = this._northwindContext.Set<OrderDetail>()
                        .Where(item => item.OrderID == this.OrderID).ToList();
                return this._orderDetails;
            }
            set => _orderDetails = value;
        }
    }

当前实体的元数据

在实体的构造函数的参数中,将当前实体的的IEntityType作为参数。

示例代码:

    public class Order
    {

        private IEntityType _entityType;

        public Order(IEntityType entityType)
        {
            this._entityType = entityType;
        }

        public int OrderID { get; set; }
        
        public string CustomerID { get; set; }

        [NotMapped]
        public IEntityType EntityType
        {
            get { return this._entityType; }
        }

    }

如果实体存在多个构造函数,框架会选择参数个数最多的那个;如果按参数个数优先选择后,依然存在多个构造函数,则会抛异常。在当前体验版本中,暂时无法直接支持自定义参数,不过在下一个发布版本中,会提供解决方案。

懒加载

懒加载是一个非常有争论的功能激烈争论的功能。虽然有些人认为它会导致性能下降或出现意想不到的Bug,但是不影响有些开发人员依旧喜欢它。EF Core 2.1 Preview 1增加了懒加载,提供了两种实现方式。

使用ILazyLoader接口实现懒加载

在实体的构造函数中传入ILazyLoader,在导航属性中,使用接口的Load方法,实现导航属性的数据加载。

示例代码:

    public class Order
    {
        private ILazyLoader _lazyLoader;


        public Order(ILazyLoader lazyLoader)
        {
            this._lazyLoader = lazyLoader;
        }

        public int OrderID { get; set; }
        
        public string CustomerID { get; set; }

        public DateTime? OrderDate { get; set; }
   
        private ICollection<OrderDetail> _orderDetails;

        public ICollection<OrderDetail> OrderDetails
        {
            get => this._lazyLoader.Load(this, ref _orderDetails);
            set => _orderDetails = value;
        }
    }

通过代理类实现懒加载

这种方式,需要单独安装 Microsoft.EntityFrameworkCore.Proxies Nuget 包,它通过 Castle.Core 框架来生成代理类来实现对导航属性的延迟加载。

启用懒加载需要注意以下两点:

  • 在配置中启用懒加载;
  • 实体类不能是封闭(sealed)类,导航属性必须是虚(virtual)属性。

这种方式,在以前的博客我已经分享过,只不过当时还没有发布,原文地址:Entity Framework Core 懒加载

值转换

EF Core 2.1 允许您将插入数据库的值自定义转换逻辑。例如:将属性的值进行加密与解密。

示例,将插入的值进行Base64编码,在查询的时候进行Base64解码。

定义的UserInfo实体,用于保存用户信息,属性PhoneNumber表示用户的手机号码;为了用户信息安全,需要将手机号码进行加密后再保存到数据库,只是为了达到演示的目的,我们采用Base64进行编码。

     public class UserInfo
     {
         public int Id { get; set; }

         public string PhoneNumber { get; set; }
     }

Base64ValueConverter表示进行值转换的具体逻辑,继承自泛型ValueConverter<string, string>,具体的逻辑非常简单,不再叙述。

    public class Base64ValueConverter : ValueConverter<string, string>
    {
        public Base64ValueConverter() : base((v) => ToBase64(v), (v) => FromBase64(v))
        {
        }
        private static string ToBase64(string input)
        {
            if (string.IsNullOrEmpty(input))
                return input;

            var bytes = Encoding.UTF8.GetBytes(input);
            return Convert.ToBase64String(bytes);
        }

        private static string FromBase64(string input)
        {
            if (string.IsNullOrEmpty(input))
                return input;

            var bytes = Convert.FromBase64String(input);
            return Encoding.UTF8.GetString(bytes);
        }
    }

SampleDbContext表示数据上下文,在OnModelCreating方法中,定义UserInfo实体的PhoneNumber属性需要使用Base64进行值转换。

    public class SampleDbContext : DbContext
    {

        public DbSet<UserInfo> Users { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var sqlConnectionStringBuilder = new SqlConnectionStringBuilder
            {
                DataSource = "*******",
                InitialCatalog = "ValueConverterTest",
                UserID = "sa",
                Password = "sa"
            };
            optionsBuilder.UseSqlServer(sqlConnectionStringBuilder.ConnectionString);

            base.OnConfiguring(optionsBuilder);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<UserInfo>().Property(e => e.PhoneNumber).HasConversion(new Base64ValueConverter());
        }
    }

下面的代码是对预期的结果进行单测。

    [Fact]
    public async void ValueConverter_Test()
    {
        string phoneNumber = "13658556925";

        using (SampleDbContext dbContext = new SampleDbContext())
        {
            await dbContext.Database.EnsureDeletedAsync();

            await dbContext.Database.EnsureCreatedAsync();

            dbContext.Users.Add(new UserInfo()
            {
                PhoneNumber = phoneNumber
            });

            await dbContext.SaveChangesAsync();
        }

        UserInfo user;

        using (SampleDbContext dbContext = new SampleDbContext())
        {
            user = dbContext.Users.Single();
        }

        Assert.NotNull(user);
        Assert.Equal(phoneNumber, user.PhoneNumber);
    }

运行后,查询数据库中保存的结果:

手机号码 13658556925 在数据库保存的值是 MTM2NTg1NTY5MjU=

使用值转换的另一个常用场景是将枚举的值存储为字符串类型,默认情况下,枚举的值保存到数据库中是通过整数表示的,如果需要在值存储为字符串类型。

   public enum CategoryName
   {
       Clothing,
       Footwear,
       Accessories
   }
   public class Category
   {
       public int Id { get; set; }
 
       public CategoryName Name { get; set; }
   }

实体CategoryName属性是用枚举表示的,如果在存储时用字符串类型表示,我们可以在DbContextOnModelCreating方法中使用如下代码,

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>().Property(e => e.Name).HasConversion<string>();
    }

EF Core 默认提供常用类型的转换,我们只需指定存储的类型即可,框架默认支持的类型转换映射表如下:

源类型 目标类型
enum intshortlongsbyteuintushortulongbytedecimaldoublefloat
bool intshortlongsbyteuintushortulongbytedecimaldoublefloat
bool string
bool byte[]
char string
char intshortlongsbyteuintushortulongbytedecimaldoublefloat
char byte[]
Guid byte[]
Guid string
byte[] string
string byte[]
DateTimeDateTimeOffsetTimeSpan stringlongbyte[]
intshortlongsbyteuintushortulongbytedecimaldoublefloat stringbyte[]

LINQ GroupBy 解析

在版本2.1之前,在EF Core中,GroupBy 表达式运算符总是在内存中进行计算的。现在支持在大多数情况下将其转换为SQL GROUP BY子句。

var query = context.Orders
    .GroupBy(o => new { o.CustomerId, o.EmployeeId })
    .Select(g => new
        {
          g.Key.CustomerId,
          g.Key.EmployeeId,
          Sum = g.Sum(o => o.Amount),
          Min = g.Min(o => o.Amount),
          Max = g.Max(o => o.Amount),
          Avg = g.Average(o => Amount)
        });

相应的SQL解析如下所示:

SELECT [o].[CustomerId], [o].[EmployeeId],
    SUM([o].[Amount]), MIN([o].[Amount]), MAX([o].[Amount]), AVG([o].[Amount])
FROM [Orders] AS [o]
GROUP BY [o].[CustomerId], [o].[EmployeeId];

查询类型

EF Core 模型现在可以包含查询类型。与实体类型不同,查询类型没有定义主键,也不能插入、删除或更新操作(即它们是只读的),但它们可以直接由查询返回。查询类型的一些使用场景:

  • 映射到没有主键的视图
  • 映射到没有主键的表
  • 映射到模型中定义的查询
  • 作为FromSql()查询的返回类型

示例,定义一个简单的BlogPost模型:

    public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public ICollection<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public int BlogId { get; set; }
    }

定义一个简单的数据库视图,能够查询每博客与文章数:

    db.Database.ExecuteSqlCommand(
        @"CREATE VIEW View_BlogPostCounts AS 
            SELECT Name, Count(p.PostId) as PostCount from Blogs b
            JOIN Posts p on p.BlogId = b.BlogId
            GROUP BY b.Name");

定义一个类映射的数据库视图的结果:

    public class BlogPostsCount
    {
        public string BlogName { get; set; }
        public int PostCount { get; set; }
    }

DbContext类的OnModelCreating使用modelBuilder.Query<T>API。 我们可以使用标准 fluent 配置 Api 来配置查询类型的映射:

    public class SampleDbContext : DbContext
    {
        public DbQuery<BlogPostsCount> BlogPostCounts { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder
                .Query<BlogPostsCount>().ToTable("View_BlogPostCounts")
                .Property(v => v.BlogName).HasColumnName("Name");
        }
    }

查询数据库视图中的标准方式:

    var postCounts = db.BlogPostCounts.ToList();
    
    foreach (var postCount in postCounts)
    {
        Console.WriteLine($"{postCount.BlogName} has {postCount.PostCount} posts.");
        Console.WriteLine();
    }

最后

EF Core 2.1 Preview1 新增功能的部分内容已经介绍完了,希望对您有帮助。如果文章中描述的功能存在遗漏或错误,请在评论中留言,谢谢!

相关文章: