EF Core对实体类属性的读写操作有一个非常不容易被发现的秘密,了解这个秘密之后,我们能更好地在EF Core中实现充血模型。
按照常规的面向对象的要求,对于属性的读写操作都要通过get、set代码块进行。
因此当我们通过EF Core吧实体类对象写入数据库或者把数据从数据库中加载到实体类对象的时候,EF Core也应该通过实体类对象的属性的get、set进行属性的读写。
但是基于性能和对特殊功能支持的考虑,EF Core在读写属性的时候,如果可能,它会直接跳过get、set,而直接操作真正存储属性值的成员变量。
下面通过一个例子来进行演示:
首先,我们定义一个实体类Dog:
class Dog
{
public long Id { get; set; }
private string name;
public string Name
{
get
{
Console.WriteLine("get被调用");
return name;
}
set
{
Console.WriteLine("set被调用");
this.name = value;
}
}
}
Dog类只有Id、Name两个属性;
为了方便我们观察代码对Name属性的get、set代码块的调用情况,我们编写Name属性的代码时没有使用{get;set;}这样简化的语法,而是使用完全的get、set代码块,把属性的值显式地保存到名字为name的成员变量中,并且在get和set代码块中都加入调试输出信息。
下面我们创建Dog类的对象,并且把它插入数据库,如以下代码所示:
Dog d1 = new Dog { Name = "goofy" };
Console.WriteLine("Dog初始化完毕");
ctx.Dogs.Add(d1);
ctx.SaveChanges();
Console.WriteLine("SaveChanges完毕");
运行上面的程序,然后查看程序的运行结果,如下图所示:

我们发现,Name属性的get代码块竟然没有被调用。
按照常理来讲,EF Core在从数据库中读取数据的时候,应该会调用set代码块为对象的Name属性赋值,但是实验的结果表明set代码块没有被调用,那EF Core是怎么设置Name属性的值的呢?
我们再来尝试从数据库读取数据,如以下代码所示:
Console.WriteLine("准备读取数据");
Dog d2 = ctx.Dogs.First(d => d.Name == "goofy");
Console.WriteLine("读取数据完毕");
运行上面的程序,然后查看程序的运行结果,如下图所示:

我们发现,Name属性的set代码块竟然也没有被调用。
按照常理来讲,EF Core在从数据库中读取数据的时候,应该会调用set代码块为对象的Name属性赋值,但是实验的结果表明set代码块没有被调用,那么EF Core是怎么设置Name属性的值的呢?
答案其实很简单,EF Core在读写实体类对象的属性时,会查找类中是否有与属性的名字一样的成员变量,如果有这样的成员变量的化,EF Core会直接读写这个成员变量的值,而不是通过set和get代码块来读写。
如果我们采用string Name{get;set}这种简化的语法来声明属性,编译器会为我们生成名字为<Name>k_BackingField的成员变量来保存属性的值。
因此EF Core除了查找与属性同名的成员变量之外,也会查找符合<Name>k_BackingField规则的成员变量,还会查找”_name””m_name”等常见写法的成员变量。
由于EF Core直接读写属性背后的成员变量,而不是通过执行get、set代码块来读写属性的值,因此我们编写的get、set代码块就不会被EF Core执行了。
接下来,我们对Dog类中保存Name属性的成员变量名进行修改,让EF Core无法识别出这个成员变量和Name属性的关系。
比如,我们把成员变量name改名为xiaoming,如以下代码所示:
class Dog
{
public long Id { get; set; }
private string xiaoming;
public string Name
{
get
{
Console.WriteLine("get被调用");
return xiaoming;
}
set
{
Console.WriteLine("set被调用");
this.xiaoming = value;
}
}
}
完成上述修改之后,重新运行插入和查询数据库代码,程序运行结果如下图所示:


从程序运行结果可以看出,属性的get、set代码块被调用了。
因为我们把和Name属性关联的成员变量改名为xiaoming,EF Core无法在运行时通过反射得知Name属性和xiaoming的关系,因此它只能通过Name属性的get、set代码块进行属性读写。
综上所述,EF Core会尝试按照命名规则去直接读写属性对应的成员变量,只有无法根据命名规则找到对应成员变量的时候,EF Core才会通过属性的get、set代码块来读写属性值。
当然,我们可以在Fluent API中通过UsePropertyAccessMode方法来修改这个默认的行为,不过这很少用。