找回密码
 立即注册
首页 业界区 业界 【EF Core】通过 DbContext 选项扩展框架

【EF Core】通过 DbContext 选项扩展框架

拍棹 前天 11:18
本来老周计划在 10 月 1 日或 2 日写这篇水文的,没打算出去玩(确实没啥好玩)。不过因为买的运动相机到手,急着想试试效果,于是就备了些干粮,骑着山地车在外面鬼混了一天。10 月 2 日,家里来了三位热爱学习的小妹妹,必须传道授业解惑。10 月 3 日去表弟家里挑一只战斗力强的狸花猫,负责家里的治安。4、5 日清洗电风扇和一台有霉味的圆柱空调,顺便把家里的门窗都清洗一下。只好等到中秋节才来写文章。
EF Core 内部使用了 IoC 容器,使其支持依赖注入,理论上也很容易扩展。不过,框架有缓存自己的服务列表,咱们无法直接访问服务容器。目前阶段,EF Core 还不能传递咱们自己的 App Services——初始化时它会直接改为 null。
  1. var cacheKey = options;
  2. var extension = options.FindExtension<CoreOptionsExtension>();
  3. if (extension?.ApplicationServiceProvider != null)
  4. {
  5.     <strong>cacheKey </strong><strong>= ((DbContextOptions)options).WithExtension(extension.WithApplicationServiceProvider(null</strong><strong>))</strong>;
  6. }
复制代码
所以,就算你有本事往 Options 里面塞 App Services 也不起作用,人家直接给干成 null 了。微软社区团队表示将来会支持的。
先不要灰心,并不是不能扩展的,还有一个扩展点可以利用—— DbContext 的选项类。
其实,DbContext 选项类是由一组 IDbContextOptionsExtension 服务构成的。所以,咱们如果实现这个服务接口,然后放进选项类的扩展列表中,也能实现扩展 EF Core 的功能。先来认识一下,IDbContextOptionsExtension 接口规定了哪些成员。
1、Info 属性:返回类型为 DbContextOptionsExtensionInfo。注意各位,这是个抽象类,所以你必须实现自己的 Info,主要用于返回你正在编写的扩展的相关信息。这个类咱们后面再讨论。
2、ApplyDefaults 方法:这个方法有个默认实现,就是 return this。其作用是根据参数传入的 DbContextOptions(另一个选项类实例),给当前扩展设置一些默认值。这个一般用于:需要根据选项来设置某些参数值,比如,SqlServerOptionsExtension 类。
  1. public virtual IDbContextOptionsExtension ApplyDefaults(IDbContextOptions options)
  2. {
  3.      if (ExecutionStrategyFactory == null
  4.          && (EngineType == SqlServerEngineType.AzureSql
  5.              || EngineType == SqlServerEngineType.AzureSynapse
  6.              || UseRetryingStrategyByDefault))
  7.      {
  8.          return WithExecutionStrategyFactory(c => new SqlServerRetryingExecutionStrategy(c));
  9.      }
  10.      return this;
  11. }
复制代码
3、ApplyServices 方法:重点来了。对就是它,实现它,你就能向服务容器添加自定义的服务了。
4、Validate 方法:验证一下当前 DbContextOptions 的值是否符合你的要求,如果验证不通过,直接抛异常就行了。如果不需要验证,留空即可。
 
现在咱们再来认识一下 DbContextOptionsExtensionInfo 抽象类。
1、LogFragment  属性:返回一个字符串,在记录日志时,这个字符串会出现在日志里。至于说是什么字符串,你可自己决定。
2、Extension 属性:返回与当前信息类相关的 IDbContextOptionsExtension 对象。
3、ShouldUseSameServiceProvider 方法:这个方法其实是在 DbContextOptions 类的 Equals 方法中作为判断两个 DbContextOptions 实例的配置是否相同的条件之一。意思就是如果结果是 true,表明所有配置相同的 DbContextOptions 不需要初始化新的服务容器。
4、GetServiceProviderHashCode 与 PopulateDebugInfo 方法:.NET CLR 对象的相等判断除了 Equals 方法,还有 GetHashCode 方法,看看是否返回相同的哈希。GetServiceProviderHashCode 方法你可以自定义返回的哈希值,DbContextOptions 类的 GetHashCode 方法中也调用了此方法。
  1. public override int GetHashCode()
  2. {
  3.     var hashCode = new HashCode();
  4.     foreach (var (type, value) in _extensionsMap)
  5.     {
  6.         hashCode.Add(type);
  7.         hashCode.Add(<strong>value.Extension.Info.GetServiceProviderHashCode()</strong>);
  8.     }
  9.     return hashCode.ToHashCode();
  10. }
复制代码
可见,如果 GetServiceProviderHashCode 方法返回的值改变,就会影响到 DbContextOptions 对象的相等判断。同样,GetServiceProviderHashCode 方法会和 PopulateDebugInfo 方法搭配用。PopulateDebugInfo 方法的参数是一个字典,开发者可以往里面设置一些自定义的 Key-Value 元素,这些元素通常表示当前扩展中被更改的值。
PopulateDebugInfo 方法中向字典添加的值也会被传递给 ServiceProviderDebugInfo 事件,事件相关的数据被封装到 ServiceProviderDebugInfoEventData  类中。
不过,但是,当你开启日志功能后,你会发现,ServiceProviderDebugInfo 事件根本不会输出到日志中。Github 上有人提过这事,但没人回答。在本文后面,老周会告诉你如何解决此问题。
----------------------------------------------------------------------------------------------------------------------------------------------------
上面是对知识点的简单理论介绍。说简单一点,就是你想扩展 EF Core,就是实现 IDbContextOptionsExtension 接口,然后在 ApplyServices 方法中把你的服务放进容器
理论总是抽象的,咱们动手练一练就好了。
第一步,老规矩,随便写个实体,然后从 DbContext 继承一个你的上下文。
  1. public class Pet
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; } = "未知生物";
  5.     public int? Age { get; set; }
  6. }
  7. public class DemoDbContext : DbContext
  8. {
  9.     // 公共属性:数据集
  10.     public DbSet<Pet> Pets { get; set; }
  11.     protected override void OnConfiguring(DbContextOptionsBuilder opBuilder)
  12.     {
  13.         opBuilder.UseSqlite("data source=:memory:")
  14.                         // 开启日志
  15.                         .LogTo(log => Console.WriteLine(log));
  16.     }
  17. }
复制代码
第二步,既然咱们要扩展,当然要写服务类型了。
  1. public interface IHelloWorld
  2. {
  3.     void SayHello(string? who);
  4. }
  5. public class DemoHelloWorld : IHelloWorld
  6. {
  7.     public void SayHello(string? who)
  8.     {
  9.         Console.WriteLine("你好,{0}", who ?? "宇宙人");
  10.     }
  11. }
复制代码
当然了,通过构造函数的依赖注入,你是可以访问 EF Core 内部的服务的。这里为了演示的简单,就没有注入任何东西。
第三步,重点来了,实现那个,那个很辣眼睛的接口。
  1. public class DemoDbContextOptionsExtension : IDbContextOptionsExtension
  2. {
  3.     // 扩展信息
  4.     private MyExtInfo? _info;
  5.     // 这个属性用于返回扩展信息。
  6.     // 返回的类型是 DbContextOptionsExtensionInfo,不需要访问 MyExtInfo 类
  7.     public <em><strong>DbContextOptionsExtensionInfo Info => _info ??= new MyExtInfo(this</strong></em><em><strong>)</strong></em>;
  8.     public void ApplyServices(IServiceCollection services)
  9.     {
  10.         // 这里,添加你的服务
  11.         <em><strong>services.AddScoped<IHelloWorld, DemoHelloWorld></strong></em><em><strong>();</strong></em>
  12.     }
  13.     public void Validate(IDbContextOptions options)
  14.     {
  15.         // 不需要验证,空方法
  16.     }
  17.     // 一个随机整数,模拟选项改变
  18.     public int RandValue { get; set; }
  19.     // 这个类私有化就可以了,因为对外公开的是 DbContextOptionsExtensionInfo 类型
  20.     private class MyExtInfo : DbContextOptionsExtensionInfo
  21.     {
  22.         // 构造函数
  23.         public MyExtInfo(IDbContextOptionsExtension extension) : base(extension)
  24.         {
  25.         }
  26.         // 替换一下基类成员,方便获取
  27.         new public DemoDbContextOptionsExtension Extension => (DemoDbContextOptionsExtension)base.Extension;
  28.         // 此处要返回 false,因为咱们这个不是数据库提供者
  29.         public override bool IsDatabaseProvider => false;
  30.         // 自定义日志输出
  31.         public override string LogFragment => $"这是个大扩展 - { Extension.RandValue.ToString()}";
  32.         // 使用 _myRandValue 的哈希,
  33.         public override int GetServiceProviderHashCode()
  34.         {
  35.             return Extension.RandValue.GetHashCode();
  36.         }
  37.         public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
  38.         {
  39.             // 设置调试信息
  40.             debugInfo["MyExtension:RandomValue"] = Extension.RandValue.ToString();
  41.         }
  42.         public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
  43.         {
  44.             // 可以直接 retrun true
  45.             return other is MyExtInfo;
  46.         }
  47.     }
  48. }
复制代码
第四步,将自定义扩展添加到 DbContextOptions 的扩展集合中有点麻烦,所以一般要封装一个扩展方法,方便调用。
  1. public static class DemoDbContextOptionsBuilderExtensions
  2. {
  3.     public static DbContextOptionsBuilder UseDemoExt(
  4.         this DbContextOptionsBuilder builder)
  5.     {
  6.         // 在添加前应当查找一下,避免重复添加
  7.         var myext = builder.Options.FindExtension<DemoDbContextOptionsExtension>();
  8.         // 如果没有,就new一个
  9.         myext ??= new DemoDbContextOptionsExtension();
  10.         // 设置一下随机数据,模拟配置改变
  11.         myext.RandValue = Random.Shared.Next(100, 9999999);
  12.         // 添加到扩展集合中(注意类型转换)
  13.         <strong>((IDbContextOptionsBuilderInfrastructure)builder)</strong>.AddOrUpdateExtension(myext);
  14.         return builder;
  15.     }
  16. }
复制代码
a、要养成先找后加的习惯,即先 Find 一下扩展是不是已在集合中,然后再调用 AddOrUpdateExtension 方法添加到扩展集合中。
b、由于此方法是显式实现了 IDbContextOptionsBuilderInfrastructure 接口,所以要先把 builder 转换为 IDbContextOptionsBuilderInfrastructure 接口类型再调用 AddOrUpdateExtension 方法。
第五步,回过头去修改 DemoDbContext 类。
  1. public class DemoDbContext : DbContext
  2. {
  3.     ……
  4.     protected override void OnConfiguring(DbContextOptionsBuilder opBuilder)
  5.     {
  6.         opBuilder.UseSqlite("data source=:memory:")
  7.                         // 开启日志
  8.                         .LogTo(log => Console.WriteLine(log))
  9.                         // 使用自定义的扩展
  10.                         <strong>.UseDemoExt()</strong>;
  11.     }
  12.     // 测试服务
  13.     public void Greeting(string who)
  14.     {
  15.         IHelloWorld sv = this.GetService<IHelloWorld>();
  16.         sv.SayHello(who);
  17.     }
  18. }
复制代码
第六步,实例化上下文对象,运行,实验一下。
  1. static void Main(string[] args)
  2. {
  3.     using var ctx = new DemoDbContext();
  4.     ctx.Greeting("小王");
  5. }
复制代码
 
运行结果如下图所示。
1.png

 很显然,ServiceProviderDebugInfo 事件没有日志输出的。
 
现在,老周就说一下如何让它输出这个事件。
方法:使用 .NET Logging API。比如,咱们要使日志输出到控制台,需要添加 Microsoft.Extensions.Logging.Console 包的引用。
在上下文类中,定义 ILoggerFactory 类型的字段,并用 LoggerFactory.Create 方法创建实例。
  1. public class DemoDbContext : DbContext
  2. {
  3.     // 静态成员
  4.     <strong>static  ILoggerFactory logFac = LoggerFactory.Create(lb =></strong><strong> {
  5. </strong>        <strong>lb.AddConsole();
  6. </strong>        <strong>lb.SetMinimumLevel(LogLevel.Trace);
  7. </strong>     <strong>});</strong>
  8.     ……
  9.     protected override void OnConfiguring(DbContextOptionsBuilder opBuilder)
  10.     {
  11.         opBuilder.UseSqlite("data source=:memory:")
  12.                         // 开启日志
  13.                         //.LogTo(log => Console.WriteLine(log))
  14.                         <strong>.UseLoggerFactory(logFac)
  15. </strong>                        // 使用自定义的扩展
  16.                         .UseDemoExt();
  17.     }
  18.     ……
  19. }
复制代码
 SetMinimumLevel 方法将日志级别设置为 Debug 或 Trace。
在 OnConfiguring 方法中,使用 UseLoggerFactory 方法应用 LoggerFactory 对象。
修改之后,重新运行程序。结果如下。
2.png

 
好了,今天就水到这里吧。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册