5

万字长文,带你彻底理解EF Core5的运行机制,让你成为团队中的EF Core专家

 3 years ago
source link: https://www.cnblogs.com/webmote31/p/14671250.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在EF Core 5中,有很多方式可以窥察工作流程中发生的事情,并与该信息进行交互。这些功能点包括日志记录,拦截,事件处理程序和一些超酷的最新出现的调试功能。EF团队甚至从Entity Framework的第一个版本中恢复了一些有用的旧的功能。本博文带你更深入地研究访问EF Core 5的一些元数据和其有趣的使用方式。

1、将EF的ToTraceString移植为EF Core的ToQueryString

这是回忆杀。
在这里插入图片描述

在Entity Framework的第一个迭代版本中,没有内置的日志记录。但是有ObjectQuery.ToTraceString(),这是一种运行时方法,可以动态计算LINQ或Entity SQL查询的SQL,尽管这不是一个很好的日志记录方法,但它毕竟可以输出SQL,即使在今天,也有一些有用的场景。

直到最新版本EF Core 5,该功能才成为EF Core的一部分,并且已重命名为ToQueryString()

如果要查看实体类People的简单查询所生成的SQL,只需将ToQueryString附加到查询中即可。不涉及LINQ执行方法。

换句话说,将查询本身与执行方法分开,仅仅针对查询。

var sqlFromQuery=context.People.ToQueryString();

ToQueryString的一个有趣用例是在调试时查看其结果,不必等到运行方法即可检查日志中的SQL。例如我可以构建查询,捕获字符串,然后执行查询。

private static void GetAllPeople()
{
    using var context = new PeopleContext();
    var query = context.People.Where(p=>p.FirstName=="Julie");
    var sqlFromQuery = query.ToQueryString();
    var people = query.ToList();
}

然后在调试时,可以看到sqlFromQuery变量的预期SQL。当然您不需要将此代码嵌入生产代码中。实际上,也非常不建议这样做,因为当EF Core进行SQL编制过程时,它很容易影响性能

您应该可以在调试器中调用ToQueryString,如图所示。
在这里插入图片描述

在调用ToQueryString之前,查询变量已经作为DbQuery进行了评估,因此可以正常工作。

调试上下文并在调试器中直接显示DbSet,例如在调试器中成功运行context.People.ToQueryString(),但是您不能直接评估LINQ表达式。换句话说,如果要调试上下文变量,然后在调试器中使用Where方法,它将失败。这并不是什么新鲜事物,也不是ToQueryString的限制。

关于ToQueryString的最后一个要点是对它的评估基于最简单的执行:ToList。使用诸如FirstOrDefault之类的LINQ执行查询会影响SQL的呈现方式,因此,在使用FirstOrDefault执行查询时,ToQueryString呈现的SQL与发送给数据库的SQL不同。这种情况下需要 EF Core日志记录来打印准确的Sql,而不是还执拗于ToQueryString。

我发现在集成测试场景下,ToQueryString特别有用。如果您需要编写测试,测试的成功取决于生成的SQL表达式的某些部分,那么ToQueryString是比日志记录更简单的路径。使用日志记录时,您必须将日志捕获到文本编写器中,然后读取该文本。尽管使用InMemory提供程序可能很诱人,但请记住,InMemory提供程序不会生成SQL。您需要为真实数据库使用提供程序,数据库不需要存在即可使用ToQueryString。EF Core在内存中才能确定SQL。

这是一个演示测试示例,旨在证明EF Core编写的智能SQL比我编写的更为智能。请注意,我在测试项目中引用了Microsoft.EntityFrameworkCore.Sqlite提供程序。如您所知,EF和EF Core总是投影与实体属性相关的列。它不写SELECT *。

[TestMethod]
public void SQLDoesNotContainSelectStar()
{
   var builder = new DbContextOptionsBuilder();
   builder.UseSqlite("Data Source=testdb.db");
   using var context = new PeopleContext(builder.Options);
   var sql=context.People.ToQueryString();
   Assert.IsFalse(sql.ToUpper().Contains("SELECT *"));
}

如果您使用拦截器来执行软删除,并且使用全局查询过滤器来始终过滤出这些行。例如,这是我DbContext OnModelBuildling方法中的一个查询过滤器,它告诉EF Core过滤掉IsDeleted属性为true的Person行。

modelBuilder.Entity<Person>().HasQueryFilter(p => !p.IsDeleted);

有了这个,我可以编写与上面类似的测试,但是将断言更改为以下内容,以确保我不会破坏全局查询过滤器逻辑。

Assert.IsTrue(sql.ToUpper().Contains("WHERE NOT (\"p\".\"IsDeleted\")"));

2、从EF Core记录详细信息

共有三种方法可以利用EF Core的日志管道。

2.1、 简单的日志记录

可以与.NET的日志记录API结合使用,所有的繁重辛苦的工作都是在后台进行的。您可以使用LogTo方法轻松配置DbContext,将.NET日志记录输出。

嗯,我就想看着你,就这样子,简简单单。
在这里插入图片描述

EF Core将输出很多事件。分为以下类,这些类从DbCloggerCategory派生。

  • 变更追踪,ChangeTracking
  • 数据库命令,Database.Command
  • 数据库连接,Database.Connection
  • 数据库事务,Database.Transaction
  • 数据库,Database
  • 基础设施,Infrastructure
  • 移居,Migrations
  • 模型验证,Model.Validation
  • 模型,Model
  • 询问,Query
  • 脚手架,Scaffolding
  • 更新,Update
    您可以使用这些类别将输出筛选为要记录的信息类型。

LogTo的一个参数指定目标为控制台窗口、文件或调试窗口。
然后,第二个参数允许您通过.NET LogLevel以及您感兴趣的任何DLoggerCategoy进行筛选。

此示例配置DbContext将日志输出到控制台,并过滤掉所有DbLoggerCategory类型LogLevel.Information组。

optionsBuilder.UseSqlServer(myConnectionString)
.LogTo(Console.WriteLine,LogLevel.Information);

下面一个LogTo方法添加了第三个参数-DbLoggerCatetory数组(仅包含一个数组),以便仅对EF Core的数据库命令进行进一步过滤。

与LogTo方法一起,我添加了EnableSensitiveDataLogging方法以在SQL中显示传入参数。这将捕获所有发送到数据库的SQL:查询,更新,原始SQL甚至通过迁移发送的更改。

.LogTo(Console.WriteLine, LogLevel.Information,
    new[]{DbLoggerCategory.Database.Command.Name},
)
.EnableSensitiveDataLogging();

上面包含IsDeleted属性的“Person”类型也具有FirstName和LastName属性。这是添加新的Person对象后调用SaveChanges的日志。

info: 1/4/2021 17:56:09.935
    RelationalEventId.CommandExecuted[20101]
    (Microsoft.EntityFrameworkCore.Database.Command)

Executed DbCommand (22ms) [Parameters=[ 
    @p0='Julie' (Size = 4000), @p1='False', 
    @p2='Lerman' (Size = 4000)],CommandType='Text',
    CommandTimeout='30']

    SET NOCOUNT ON;
    INSERT INTO [People] ([FirstName],[IsDeleted], [LastName])
    VALUES (@p0, @p1, @p2);
    SELECT [Id]
    FROM [People]
    WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

日志记录显示信息类型、EventId、以及请求的记录器类别的详细信息。接下来,日志名称,执行时间和参数列表。由于启用了敏感数据记录,因此将显示参数。最后,它列出了发送到数据库的SQL。

LogTo使EF Core输出基本日志记录变得容易。您可以在DbContext,ASP.NET Core应用程序的启动文件或ASP.NET Core应用程序的应用程序配置文件中对其进行配置。

注意顶部的EventId。您甚至可以定义日志记录以使用这些ID过滤特定事件。您还可以过滤出特定的日志类别,并且可以控制格式。在https://docs.microsoft.com/zh-cn/ef/core/logging-events-diagnostics/simple-logging上查看有关这些各种功能的更多详细信息的文档。

简单日志记录是记录EF Core的高级方法,是的,高级的就是简单的,这就是计算机世界的定义

您也可以通过直接与Microsoft.Extensions.Logging一起,以对EF Core的日志方式进行更多控制。检查EF Core文档,以获取更多有关使用此更高级用法的详细信息:https://docs.microsoft.com/zh-cn/ef/core/logging-events-diagnostics/extensions-logging

2.2、响应EF Core 事件

EF Core 2.1在EF Core管道中引入了.NET事件。开始只有两个:ChangeTracker.Tracked(在DbContext开始跟踪实体时引发)和ChangeTracker.StateChanged(在已跟踪的实体的状态改变时引发)。

后来,看久了生情,事件的家族又迎来了几个小家伙......

在这里插入图片描述

有了基本逻辑,团队向EF Core 5添加三个事件:SaveChangesFailedSaveChangesSaveChangesAsync

当上下文将要保存更改时,将引发DbContext.SavingChanges
在两个保存更改方法中的任何一个成功完成之后,将引发DbContext.SavedChanges
DbContext.SaveChangesFailed用于捕获和检查故障。

能够分离此逻辑,而不是全部填充到SaveChanges方法的中,这是一个很好的选择。

可以使用这些事件来记录未跟踪的备用信息,您甚至可以使用事件来发出记录器无法跟踪的备用信息。

如果要使用影子属性跟踪审核数据,则可以在构造SQL并将其发送到数据库之前,使用SavingChanges事件更新这些属性。

例如,我将应用程序设置为向每个实体添加UserId阴影属性(不包括那些属性包和拥有的实体)。当用户登录时,我的应用程序有一个名为Globals.CurrentUserId的静态变量。此外,在我的DbContext类中,我创建了一个名为SetUserId的私有方法,该方法将我的shadow属性(存在的地方)的值设置为CurrentUserId

private void SetUserId(object sender, SavingChangesEventArgs e)
{
   foreach (var entry in ChangeTracker.Entries()
   .Where(entry => entry.Metadata
   .GetProperty("UserId") != null))
   {
       entry.Property("UserId").CurrentValue = Globals.CurrentUserId;
   }
}

最后,我可以将SetUserId方法连接到DbContext的构造函数中的SavingChanges事件:

public PeopleContext()
{
   SavingChanges += SetUserId;
}

现在,每当我调用SaveChanges时,UserId就会与其他数据一起持久保存到表中。

这是一些日志数据:

Executed DbCommand (29ms) [Parameters=[
   @p0='Julie' (Size = 4000), @p1='False',  
   @p2='Lerman' (Size = 4000), @p3='101'], 
   CommandType='Text', CommandTimeout='30']

SET NOCOUNT ON;
INSERT INTO [People] ([FirstName],[IsDeleted], [LastName], [UserId])
VALUES (@p0, @p1, @p2, @p3);
SELECT [Id]
FROM [People]
WHERE @@ROWCOUNT = 1
   AND [Id] = scope_identity();

这只是利用这些事件的一种简单方法。

2.3、使用事件计数器访问指标

EF Core 5利用了.NET Core 3.0中.NET引入的一项很酷的功能-dotnet-counters(https://docs.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-counters)。计数器是一个全局命令行工具。您可以使用dotnet CLI安装此工具。

dotnet tool install --global dotnet-counters

安装完成后,您可以告诉它监视在dotnet环境中运行的进程。您需要提供正在运行的.NET应用程序的进程ID

System.Diagnostics.Process.GetCurrentProcess().Id

在Visual Studio中,我无法简单地在调试器中调试此值。调试器只会告诉您“此表达式会产生副作用,因此不会被评估。” 因此,我将其嵌入到我的代码中并获得了值313131

在拥有ID且应用仍在运行的情况下,然后可以触发计数器,开始监视来自Microsoft.EntityFramework命名空间的事件。如下:

dotnet counters monitor    Microsoft.EntityFrameworkCore -p 313131

然后,当您遍历应用程序时,计数器将显示EF Core统计信息的特定列表,如图所示,然后在应用程序执行其功能时更新计数。

我仅监视了一个小型演示应用程序,因此计数并不是很好看,但是您可以看到我正在运行一个DbContext实例(Active DbContexts),我已经运行了三个查询,并利用了查询缓存(因为我运行了其中一些查询不止一次),并两次调用SaveChanges。

这看起来像您的代码分析工具,但是当针对更密集的解决方案运行时,它肯定会更有用。EF团队建议您在文档中仔细阅读dotnet-counters功能,以便正确使用EF Core。

3、拦截EF Core的数据——拦截器

EF Core的拦截器是一项功能,该功能始于EF6,并在EF Core 3中重新引入。EF Core 5中引入了SaveChanges的新拦截器。

由于此功能已经存在很长时间了(尽管它对于EF Core是相当新的),因此应该有很多文章介绍。即使这样,我还是觉得很神奇。

共有三种不同的拦截器类来拦截命令,连接和事务,以及用于SaveChanges的新拦截器类。每个类都有自己相关的虚拟方法(和相关对象)。例如,DbCommandInterceptor公开了ReaderExecutingReaderExecutingAsync,它们在命令即将发送到数据库时被触发。

public override InterceptionResult<DbDataReader>
   ReaderExecuting(
       DbCommand command,
       CommandEventData eventData,
       InterceptionResult<DbDataReader> result)
   {
       //例如,webmote支持你干点啥?
       return result;
   }

它的参数之一是DbCommand,其CommandText属性保存SQL。

如果要修改SQL,添加查询提示或其他任务,则可以更改命令,然后使用新CommandText值的命令将继续进行。

从数据库返回任何结果数据时,将触发ReaderExecuted / Async方法。

public override DbDataReader ReaderExecuted(
    DbCommand command,
    CommandExecutedEventData eventData,
    DbDataReader result)
    {
        return base.ReaderExecuted
        (command, eventData, result);
    }

例如,在这里您可以捕获DbDataReader,并对该数据进行某些处理,然后再继续执行EF Core实现。一个示例是记录一些记录器无法捕获的内容,例如:

4、查询拦截

EF Core 公开的 DbCommandInterceptor拦截器提供查询拦截功能,查询拦截是在数据库上执行查询之前插入逻辑,或者在查询执行之后(以及控制返回到调用代码之前)立即插入逻辑的能力。

此功能在现实世界中有多种使用案例:

  • 延长具有某些特征的命令的超时
  • 查询失败并记录异常时诊断信息
  • 当读取到内存的行数超过特定阈值时,记录警告

一个小例子:

public class TestQueryInterceptor : DbCommandInterceptor
{
	 // runs before a query is executed
	public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
	{
	    command.CommandText += " OPTION (OPTIMIZE FOR UNKNOWN)";   
   		command.CommandTimeout = 12345;   
  		 return result;
	}
	
	// runs after a query is excuted
	public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
	{
	    if (this.ShouldChangeResult(command, out var changedResult))
	    {
	        return changedResult;
	    }
	    
	    return result;
	}
}

注意: 大多数方法都有同步和异步版本。令人讨厌的是,异步查询仅触发异步方法(反之亦然),因此在编写拦截器时必须覆盖两者。

安装拦截器是很简单的。

public class SampleDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlite(@"Data Source=Sample.db;")
            .AddInterceptors(new TestQueryInterceptor (), new SampleInterceptor2());
    }
}

通过返回InterceptionResult<T>.SuppressWithResult()禁止执行。重要的是要注意,DbCommandInterceptor安装的其他所有组件仍将执行,并且可以通过上的HasResult属性检查其他拦截器是否已禁止执行result。

public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
{
    if (this.ShouldSuppressExecution(command))
    {
        return InterceptionResult.SuppressWithResult<object>(null);
    }
    
    return result;
}

方法中引发的异常从技术上将阻止执行,不要利用这个事实,将异常用于控制流几乎总是很糟糕的设计。

你可以拦截如下清单的操作:

方法 操作 CommandCreating 在创建命令之前(注意:一切都是命令,因此它将拦截所有查询) CommandCreated 创建命令之后但执行之前 CommandFailed[Async] 在执行过程中命令失败并出现异常后 ReaderExecuting[Async] 在执行“查询”命令之前 ReaderExecuted[Async] 执行“查询”命令后 NonQueryExecuting[Async] 在执行“非查询”命令之前(注意:非查询的一个示例是 ExecuteSqlRaw NonQueryExecuted[Async] 执行“非查询”命令后 ScalarExecuting [Async] 在执行“标量”命令之前(注意:“标量”是存储过程的同义词) ScalarExecuted [Async] 执行“标量”命令后 DataReaderDispose 执行命令后

这是一个耗时命令拦截

public class MyDBCommandInterceptor: DbCommandInterceptor
{
	public static ConcurrentDictionary CommandStartTimes = new ConcurrentDictionary();
	public static ConcurrentDictionary CommandDurations = new ConcurrentDictionary();

	public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) {
		CommandStartTimes.TryAdd(command, DateTime.Now);
		base.NonQueryExecuting(command, interceptionContext);
	}

	public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) {
		CommandStartTimes.TryAdd(command, DateTime.Now);
		base.ReaderExecuting(command, interceptionContext);
	}

	public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) {
		CommandStartTimes.TryAdd(command, DateTime.Now);
		base.ScalarExecuting(command, interceptionContext);
	}

	public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) {
		base.NonQueryExecuted(command, interceptionContext);
		AccumulateTime(command);
	}

	public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) {
		base.ReaderExecuted(command, interceptionContext);
		AccumulateTime(command);
	}

	public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) {
		base.ScalarExecuted(command, interceptionContext);
		AccumulateTime(command);
	}

	private void AccumulateTime(DbCommand command) {
		if (CommandStartTimes.TryRemove(command, out
		var commandStartTime)) {
			var commandDuration = DateTime.Now - commandStartTime;
			CommandDurations.AddOrUpdate(command.CommandText, commandDuration, (_, accumulated) => commandDuration + accumulated);
		}
	}
}

有关使用EF Core文档中的拦截器的大量指导,请访问https://docs.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors

5、EF Core 5中的Sleeper功能:调试视图

就是 ChangeTracker.DebugViewModel.DebugView

DebugViews输出格式正确的字符串,其中有ChangeTracker的状态或模型中的元数据的信息。DebugView提供了一个漂亮的文档,您可以捕获和打印该文档,并真正了解其幕后情况。

我在调试器上花费了大量时间,以探索有关变更跟踪器了解的内容或EF Core如何解释我所描述的模型的各种详细信息。能够以这种文本格式读取此信息,甚至将其保存在文件中,因此您无需反复调试即可收集详细信息,这是EF Core 5的一项神奇功能

确保您了解DebugViews是撰写本文的目的。

在DbContext.ChangeTracker.DebugView中,您将找到ShortView和LongView属性。例如,这里是我刚查询一个Person对象时的视图,而我的上下文仅包含一个人。

Person {Id: 1} Unchanged

这是最常用的信息-在我的上下文中,只有一个未更改的Person的ID为1。

LongView提供了有关被跟踪实体的更多详细信息。

Person {Id: 1} Unchanged
    Id: 1 PK
    FirstName: 'Julie'
    IsDeleted: 'False'
    LastName: 'Lerman'
    UserId: 101
    Addresses: []

如果要在跟踪的Person上对其进行编辑并强制上下文检测更改,则LongView除了将状态显示为Modified之外,还对LastName属性所做的更改进行记录。

Person {Id: 1} Modified
   Id: 1 PK
   FirstName: 'Julie'
   IsDeleted: 'False'
   LastName: 'Lermantov' Modified
   Originally 'Lerman'
   UserId: 101
   Addresses: []

您可以在此视图中看到一个Addresses属性。实际上,使用导航,“人”和“地址”之间存在多对多关系。EF Core在运行时推断内存中的PersonAddress实体,以便将关系数据持久化到联接表中。

当我在其“地址”集合中创建一个具有一个地址的人的图形时,您可以在ShortView中看到一个“人”,一个地址和一个推断的PersonAddress对象。长视图显示了这些对象的属性。

AddressPerson (Dictionary<string, object>)
    {AddressesId: 1, ResidentsId: 1} Unchanged FK
    {AddressesId: 1} FK {ResidentsId: 1}
Address {Id: 1} Unchanged
Person {Id: 1} Modified

我喜欢这些调试视图,这些视图可以在调试时帮助我发现被跟踪对象的状态和关系,无论我是在解决问题还是在学习它的工作方式。

让我们转到Model.DebugViews看看您可以从中学到什么。

首先,我应该阐明我的模型。使用Visual Studio中的EF Core Power Tools扩展来可视化模型。
在这里插入图片描述
DbContext.Model.DebugView也具有ShortView和LongView。它们都包含很多信息。

您可以看到属性,主键和外键,索引以及级联删除规则,多对多关系,甚至指定了它使用跳过导航。还描述了继承。您可以从这份文件中学到很多东西。

  • 清单1:数据模型的Model.DebugView.ShortView
Model:
    EntityType: AddressPerson (Dictionary<string, object>)    
        CLR Type: Dictionary<string, object>
            Properties:
                AddressesId (no field, int) Indexer Required PK FK AfterSave:Throw
                ResidentsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
           Keys:
               AddressesId, ResidentsId PK
           Foreign keys:
               AddressPerson (Dictionary<string, object>) {'AddressesId'} -> Address {'Id'} Cascade
               AddressPerson (Dictionary<string, object>) {'ResidentsId'} -> Person {'Id'} Cascade
           Indexes:
               ResidentsId
    EntityType: Address
        Properties:
            Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
            PostalCode (string)
            Street (string)
            UserId (no field, int) Shadow Required
        Navigations:
            WildlifeSightings (List<WildlifeSighting>) Collection ToDependent WildlifeSighting
        Skip navigations:
            Residents (List<Person>) CollectionPerson        
                Inverse: Addresses
        Keys:
            Id PK
    EntityType: Person
        Properties:
            Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
            FirstName (string)
            IsDeleted (bool) Required
            LastName (string)
            UserId (no field, int) Shadow Required
        Skip navigations:
            Addresses (List<Address>) CollectionAddress        
            Inverse: Residents
        Keys:
            Id PK
    EntityType: WildlifeSighting
        Properties:
            Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
            AddressId (int) Required FK Index
            DateTime (DateTime) Required
            Description (string)
            UserId (no field, int) Shadow Required
        Keys:
            Id PK
        Foreign keys:
            WildlifeSighting {'AddressId'} -> Address {'Id'}        
                ToDependent: WildlifeSightings Cascade
        Indexes:
            AddressId

Model.DebugView.LongView包含更多详细信息,它们描述了注释,数据库映射等。您可以从LongView中学到更多,但并不是每个人都希望看到这种细节,如果您需要,它就在那里。

  • 清单2:在Model.DebugView的LongView中描述的Person实体
EntityType: Person
    Properties:
        Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
            Annotations:
                Relational:DefaultColumnMappings: System.Collections.Generic.SortedSet`1   
                [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMappingBase]
                Relational:TableColumnMappings: System.Collections.Generic.SortedSet`1   
                [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMapping]
                    SqlServer:ValueGenerationStrategy: IdentityColumn
        FirstName (string)
            Annotations:
                Relational:DefaultColumnMappings: System.Collections.Generic.SortedSet`1
                [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMappingBase]
                Relational:TableColumnMappings:System.Collections.Generic.SortedSet`1
                [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMapping]
         IsDeleted (bool) Required
              Annotations:
                  Relational:DefaultColumnMappings: System.Collections.Generic.SortedSet`1
                  [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMappingBase]
                  Relational:TableColumnMappings: System.Collections.Generic.SortedSet`1
                  [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMapping]
         LastName (string)
              Annotations:
                  Relational:DefaultColumnMappings: System.Collections.Generic.SortedSet`1
                  [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMappingBase]
                  Relational:TableColumnMappings: System.Collections.Generic.SortedSet`1
                  [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMapping]
          UserId (no field, int) Shadow Required
              Annotations:
                  Relational:DefaultColumnMappings: System.Collections.Generic.SortedSet`1
                  [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMappingBase]
                  Relational:TableColumnMappings: System.Collections.Generic.SortedSet`1
                  [Microsoft.EntityFrameworkCore.Metadata.Internal.ColumnMapping]
          Skip navigations:
              Addresses (List<Address>) CollectionAddress        
                  Inverse: Residents
          Keys:
              Id PK
                 Annotations:
                     Relational:UniqueConstraintMappings: System.Collections.Generic.SortedSet`1
                     [Microsoft.EntityFrameworkCore.Metadata.Internal.UniqueConstraint]
                 Annotations:
                     ConstructorBinding: Microsoft.EntityFrameworkCore.Metadata.ConstructorBinding
                         QueryFilter: p => Not(p.IsDeleted)
                     Relational:DefaultMappings: System.Collections.Generic.List`1
                     [Microsoft.EntityFrameworkCore.Metadata.Internal.TableMappingBase]
                     Relational:TableMappings: System.Collections.Generic.List`1
                     [Microsoft.EntityFrameworkCore.Metadata.Internal.TableMapping]
                     Relational:TableName: People
                     ServiceOnlyConstructorBinding: 
                     Microsoft.EntityFrameworkCore.Metadata.ConstructorBinding

有关EF Core,您对幕后情况了解得越多,对该工具的掌控力就越大。

了解EF Core如何解释您的类和映射,你可以控制这些模型并按照您希望的方式持久保存数据,也可以根据需要修改SQL甚至结果。学习如何利用本文中介绍的各种调试,日志记录,侦听和事件处理方法,希望能帮助您成为团队中的EF Core专家。

当然,不用多久,你就会升职加薪、当上总经理、出任CEO、迎娶白富美、走上人生巅峰,想想是不是还有点小激动?

在这里插入图片描述


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK