EntityFramework Core 1.1+ Backing Fields(返回字段)
前言
通过我发表的博文可知最近一段时间会将持续讲解EntityFramework Core特性,在此之前我提到过Backing Fields,回头翻了翻感觉写的还不够好,于是乎再来讲解一番,也是自己再一次巩固,废话少说,开门见山。
EntityFramework Core Backing Fields基础
Backing Fields特性出现于EF Core 1.1,我们姑且将其翻译为返回字段,这样翻译和实际作用对应,Backing Fields允许EF Core读或者写到一个字段而非属性,说的通俗易懂一点则是允许对字段进行映射。当属性只有一个GET访问器利用此特性将非常有用,在之前版本我们必须同时需要设置GET和SET访问器,接下来我们详细来讲解Backing Fields(对字段进行映射)。
Backing Fields特性允许EF Core读或者写数据到字段中而不是属性中。默认情况下满足以下四种规则都会配置成Backing Fields。
- _<camel-cased property name>
- _<property name>
- m_<camel-cased property name>
- m_<property name>
我们首先给出本节需要用到的两个类Blog和Post,如下:
public class Blog { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public string Name { get; set; } public int CommentCount { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public Blog Blog { get; set; } }
根据如上对Backing Fields的约定来我们将Blog中的Url配置成Backing Fields,如下:
public class Blog { public int Id { get; set; } public string Name { get; set; } private string _url { get; set; } public string Url { get { return _url; } set { _url = value; } } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public ICollection<Post> Posts { get; set; } }
官网对于如上配置Backing Fields即(_url)如此解释,在配置Backing Fields后,EF Core会将数据库表中数据直接写入该字段即(_url)。如果我们需要EF Core读取或写入值,那么将尽可能使用该属性。 例如,如果需要EF Core更新某个属性的值,那么它将使用属性设置器(若已定义), 如果该属性是只读的,则将它写入到字段中。想必如上解释已经很明了,无需过多再阐述。我们来演示此种情况通过字段来设置属性值,现在我们假设这样一种情况,在创建我们自己博客时,此时我们的博客Url就需要确定下来,所以在添加Blog时我们将Ur以构造函数参数传入给Backing Fields即_url,所以我们在上述基础上添加构造函数如下:
public Blog(string url) { _url = url; }
接下来我们在控制台中创建Blog并添加到数据库中,您可以先想象一下将会发生什么,如下:
using (var context = new EFCoreDbContext()) { context.Blogs.Add(new Blog("http://www.cnblogs/CreateMyself") { Name = "Jeffcky" }); context.SaveChanges(); foreach (var blog in context.Blogs) { Console.WriteLine($"{blog.Id} {blog.Name} {blog.Url}"); } }
因为EF Core默认是用无参构造函数实例化对象,既然我们自定义调用有参构造函数,所以必须显式声明无参构造函数。否则在遍历数据时将抛出异常:System.InvalidOperationException:“A parameterless constructor was not found on entity type ‘Blog’. In order to create an instance of ‘Blog’ EF requires that a parameterless constructor be declared.”。
除了上述我们根据给出的约定EF Core将其看作为返回字段外,我们仍然可以手动利用HasField进行配置,如下:
builder.Property(p => p.Url).HasField("_url");
除此之外我们还可通过UsePropertyAccessMode方法中的参数枚举来配置对属性的访问模式,该参数枚举存在如下三种:
比如我们需要对字段访问模式为在构造函数中,那么我们可以进行如下配置:
builder.Property(p => p.Url).HasColumnType("VARCHAR(100)").UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
这里需要注意的是所有访问模式依然是通过GET或者SET访问器,比如属性设置为只读即使进行了如上配置,依然是字段。上述参数枚举说明详情请见其具体定义而定。
上述我们是将属性的字段进行映射,同时EF Core 1.1也支持不需要属性而直接映射字段,比如我们在Blog中再定义如下字段:
public string _NonPropertyField;
接着我们进行如下映射配置,迁移后将在数据库表中生成NonPropertyBackingField列并对应字段指向_NonPropertyField。
builder.Property<string>("NonPropertyBackingField").HasField("_NonPropertyField");
EntityFramework Core Backing Fields思考
到此是不是就这么简单结束了呢?显然不是,当学习任何一门技术时,所出现技术特性是为了解决问题而不是凭空产出,什么意思呢?当我们在自学过程中看官网例子时,官网将基础知识一股脑全部灌输给我们,那我们是不是应该不假思索下,它有什么用呢?比如上述Backing Fields特性的出现,因为我给您讲解了,您就知道有这么个特性,但是不知道怎么用那和知道、了解有和区别呢?还不明白,接下来我们利用EF 6来看一个例子,通过此例子您就会顿悟了。请继续往下看。
EntityFramework 6.x 没有Backing Fields所带来问题
我们创建EF 6.x控制台程序,给出如下测试类:
public class UseCase { public int Id { get; set; } private string _url { get; set; } public string Url { get { return _url; } } public string GetUrl() { return _url = "http://www.cnblogs.com/CreateMyself"; } }
接下来我们来添加数据看看,看看数据库表是否能正常添加:
using (var ctx = new EfDbContext()) { var useCases = ctx.UseCases; var useCase = new UseCase(); useCase.GetUrl(); useCases.Add(useCase); ctx.SaveChanges(); };
在客户端我们通过C#代码设置了Url值,但是并未同步到数据库表中,这也是EF 6.x中没有解决的问题而在EF Core利用Backing Fields轻而易举。我们能够看到当访问器GET或者SET中包含业务逻辑时这个时候就很能凸显Backing Fields的实际作用。下面我们来看看在EF Core中的实际用途。
EntityFramework Core Backing Fields用途
我们知道在EntityFramework中导航属性必须是ICollection<T>集合类型,如文章开头我们定义Blog中的Posts导航属性,我们也知道在ICollection<T>集合类型中存在Add、Remove、Clear等方法,这也就意味着有该集合类型的导航属性我们都可以对其进行添加或删除对象甚至于清除对象。正常情况下我们需要将实际业务行为代码封装在实体模型中,从这个角度出发,很显然我们不能这么做。我们希望公开一个接口,通过该接口控制业务行为以及何时进行控制,以及何时应该发生怎样的行为,这不仅仅是良好的领域驱动设计行为,也是很好的面向对象的设计行为。幸运的是在EntityFramework Core中对集合导航属性不仅仅支持ICollection<T>,同时也支持IEnumerable<T>,此时我们将Blog中的Posts集合导航属性修改成IEnemerable<Posts>,如下:
public IEnumerable<Post> Posts { get; set; }
这个时候我们定义集合类型为IEnumerable<T>,紧接着我们修改成如下形式。
public class Blog { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } private readonly List<Post> _posts = new List<Post>(); public IEnumerable<Post> Posts => _posts.ToList(); }
那么问题就随之而来,我们为何要修改成如上形式呢?如上定义私有的_posts返回字段并通过Posts来公开暴露,从安全角度看非常必须而且很有必要,当定义集合类型为ICollection<T>,此时我们能完全控制Post对象,也就是说能够任意进行添加、删除、清除操作。因为这完全属于内部行为,无需对外暴露。当添加Post对象时,我们在Blog对象内部定义添加方法即可。
public void AddPost(Post post) { _posts.Add(post); }
那么问题又来了,我们定义了返回字段_posts后为何传递给对外暴露的Posts时要创建副本呢?因为对外暴露的Posts最终返回的是实际List<T>集合,所以最终还是会转换成ICollection<T>集合类型,所以我们需要通过创建副本来进行修正所以要ToList。还需要说明一点的是在EF Core 1.1版本中并不会映射上述私有的返回字段到数据存储中,我们需要在OnModelCreating方法中进行如下配置:
var navigation = modelBuilder.Entity<Blog>().Metadata.FindNavigation(nameof(Blog.Posts)); navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
如上代码告诉EF Core通过命名约定发现它的字段并访问Post属性。直到EF Core 2.0仍然无法对导航属性进行返回字段配置,只能对标量属性进行返回字段配置。通过如下配置抛出异常可得知:
builder.Property(p => p.Posts).HasField("_posts");
通过github上提交的Issue得知对导航属性进行返回字段的配置会在EF Core 2.1中实现,其实现方式为推荐为如下形式:
modelBuilder.Entity<Blog>() .HasMany( e => e.Posts, nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) .WithOne( e => e.Blog, nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field))
到此关于Backing Fields详细说明就已结束,这里我们来一个完整性总结。使用Backing Fields的时机是:大部分情况下当属性中访问器存在业务逻辑时可能会用到Backing Fields,同时对于集合导航属性 推荐使用如下组合方式。
- 定义私有只读的返回字段(Backing Fields)。
- 定义公共的IEnumerable<T>接口属性。
- 对返回字段创建副本传递给对外暴露的公共接口属性
总结
侃侃而谈如上诸多理论,在实际项目中或许直接定义集合导航属性为ICollection<T>更加简单粗暴,又或者赶项目进度谁会顾及那么多呢,能实现就行。精简的内容,深入的讲解,我们下节再会。