惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
IT之家
IT之家
Cyberwarzone
Cyberwarzone
T
Troy Hunt's Blog
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
T
Threat Research - Cisco Blogs
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
G
GRAHAM CLULEY
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 叶小钗
Last Week in AI
Last Week in AI
C
CERT Recently Published Vulnerability Notes
The Hacker News
The Hacker News
Jina AI
Jina AI
T
Tor Project blog
V
Vulnerabilities – Threatpost
酷 壳 – CoolShell
酷 壳 – CoolShell
Spread Privacy
Spread Privacy
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Simon Willison's Weblog
Simon Willison's Weblog
Security Latest
Security Latest
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 司徒正美
V2EX - 技术
V2EX - 技术
I
Intezer
The Cloudflare Blog
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
博客园 - 【当耐特】
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
Lohrmann on Cybersecurity
Scott Helme
Scott Helme
Google Online Security Blog
Google Online Security Blog
量子位
The Last Watchdog
The Last Watchdog
AI
AI
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Security Affairs
P
Palo Alto Networks Blog
S
Secure Thoughts
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Attack and Defense Labs
Attack and Defense Labs

博客园 - 东邪独孤

【ASP.NET Core】在 Windows Forms 中运行 Web 服务器 【EF Core】级联删除行为 【EF Core】继承策略——TPC 【EF Core】继承策略——TPT 【EF Core】继承策略——TPH 【EF Core】使用自定义的值比较器 【EF Core】值转换器 【EF Core】直接更新数据 【EF Core】实体追踪——Entry中记录的数据 【EF Core】实体状态与变更追踪 【EF Core】将一个实体映射到多个表的正确方法 【EF Core】“Code First”方案下以编程方式生成迁移 【EF Core】“DB First”方案下用编程方式生成数据库模型代码 【EF Core】三种方法记录生成的 SQL 语句 【EF Core】未定义实体类的数据库模型 【EF Core】“多对多”关系与跳跃导航 【EF Core】FromExpression 方法有什么用? 【EF Core】框架底层的数据库连接管理 【EF Core】再谈普通实体关系与 Owned 关系的区别 【EF Core】实体类的依赖注入 【EF Core】优化后的模型 【EF Core】使用外部 Model 【EF Core】聊聊“复合”属性 【EF Core】为 DatabaseFacade 扩展“创建”与“删除”数据表功能 【EF Core】带主键实体与无主键实体 【EF Core】框架是如何识别实体类的属性和主键的
【EF Core】通过 DbContext 选项扩展框架
东邪独孤 · 2025-10-06 · via 博客园 - 东邪独孤

本来老周计划在 10 月 1 日或 2 日写这篇水文的,没打算出去玩(确实没啥好玩)。不过因为买的运动相机到手,急着想试试效果,于是就备了些干粮,骑着山地车在外面鬼混了一天。10 月 2 日,家里来了三位热爱学习的小妹妹,必须传道授业解惑。10 月 3 日去表弟家里挑一只战斗力强的狸花猫,负责家里的治安。4、5 日清洗电风扇和一台有霉味的圆柱空调,顺便把家里的门窗都清洗一下。只好等到中秋节才来写文章。

EF Core 内部使用了 IoC 容器,使其支持依赖注入,理论上也很容易扩展。不过,框架有缓存自己的服务列表,咱们无法直接访问服务容器。目前阶段,EF Core 还不能传递咱们自己的 App Services——初始化时它会直接改为 null。

var cacheKey = options;
var extension = options.FindExtension<CoreOptionsExtension>();
if (extension?.ApplicationServiceProvider != null)
{
    cacheKey = ((DbContextOptions)options).WithExtension(extension.WithApplicationServiceProvider(null));
}

所以,就算你有本事往 Options 里面塞 App Services 也不起作用,人家直接给干成 null 了。微软社区团队表示将来会支持的。

先不要灰心,并不是不能扩展的,还有一个扩展点可以利用—— DbContext 的选项类。

其实,DbContext 选项类是由一组 IDbContextOptionsExtension 服务构成的。所以,咱们如果实现这个服务接口,然后放进选项类的扩展列表中,也能实现扩展 EF Core 的功能。先来认识一下,IDbContextOptionsExtension 接口规定了哪些成员。

1、Info 属性:返回类型为 DbContextOptionsExtensionInfo。注意各位,这是个抽象类,所以你必须实现自己的 Info,主要用于返回你正在编写的扩展的相关信息。这个类咱们后面再讨论。

2、ApplyDefaults 方法:这个方法有个默认实现,就是 return this。其作用是根据参数传入的 DbContextOptions(另一个选项类实例),给当前扩展设置一些默认值。这个一般用于:需要根据选项来设置某些参数值,比如,SqlServerOptionsExtension 类。

 public virtual IDbContextOptionsExtension ApplyDefaults(IDbContextOptions options)
 {
     if (ExecutionStrategyFactory == null
         && (EngineType == SqlServerEngineType.AzureSql
             || EngineType == SqlServerEngineType.AzureSynapse
             || UseRetryingStrategyByDefault))
     {
         return WithExecutionStrategyFactory(c => new SqlServerRetryingExecutionStrategy(c));
     }

     return this;
 }

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 方法中也调用了此方法。

public override int GetHashCode()
{
    var hashCode = new HashCode();

    foreach (var (type, value) in _extensionsMap)
    {
        hashCode.Add(type);
        hashCode.Add(value.Extension.Info.GetServiceProviderHashCode());
    }

    return hashCode.ToHashCode();
}

可见,如果 GetServiceProviderHashCode 方法返回的值改变,就会影响到 DbContextOptions 对象的相等判断。同样,GetServiceProviderHashCode 方法会和 PopulateDebugInfo 方法搭配用。PopulateDebugInfo 方法的参数是一个字典,开发者可以往里面设置一些自定义的 Key-Value 元素,这些元素通常表示当前扩展中被更改的值。

PopulateDebugInfo 方法中向字典添加的值也会被传递给 ServiceProviderDebugInfo 事件,事件相关的数据被封装到 ServiceProviderDebugInfoEventData  类中。

不过,但是,当你开启日志功能后,你会发现,ServiceProviderDebugInfo 事件根本不会输出到日志中。Github 上有人提过这事,但没人回答。在本文后面,老周会告诉你如何解决此问题。

----------------------------------------------------------------------------------------------------------------------------------------------------

上面是对知识点的简单理论介绍。说简单一点,就是你想扩展 EF Core,就是实现 IDbContextOptionsExtension 接口,然后在 ApplyServices 方法中把你的服务放进容器

理论总是抽象的,咱们动手练一练就好了。

第一步,老规矩,随便写个实体,然后从 DbContext 继承一个你的上下文。

public class Pet
{
    public int Id { get; set; }
    public string Name { get; set; } = "未知生物";
    public int? Age { get; set; }
}


public class DemoDbContext : DbContext
{
    // 公共属性:数据集
    public DbSet<Pet> Pets { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder opBuilder)
    {
        opBuilder.UseSqlite("data source=:memory:")
                        // 开启日志
                        .LogTo(log => Console.WriteLine(log));
    }
}

第二步,既然咱们要扩展,当然要写服务类型了。

public interface IHelloWorld
{
    void SayHello(string? who);
}

public class DemoHelloWorld : IHelloWorld
{
    public void SayHello(string? who)
    {
        Console.WriteLine("你好,{0}", who ?? "宇宙人");
    }
}

当然了,通过构造函数的依赖注入,你是可以访问 EF Core 内部的服务的。这里为了演示的简单,就没有注入任何东西。

第三步,重点来了,实现那个,那个很辣眼睛的接口。

public class DemoDbContextOptionsExtension : IDbContextOptionsExtension
{
    // 扩展信息
    private MyExtInfo? _info;

    // 这个属性用于返回扩展信息。
    // 返回的类型是 DbContextOptionsExtensionInfo,不需要访问 MyExtInfo 类
    public DbContextOptionsExtensionInfo Info => _info ??= new MyExtInfo(this);

    public void ApplyServices(IServiceCollection services)
    {
        // 这里,添加你的服务
        services.AddScoped<IHelloWorld, DemoHelloWorld>();
    }

    public void Validate(IDbContextOptions options)
    {
        // 不需要验证,空方法
    }

    // 一个随机整数,模拟选项改变
    public int RandValue { get; set; }

    // 这个类私有化就可以了,因为对外公开的是 DbContextOptionsExtensionInfo 类型
    private class MyExtInfo : DbContextOptionsExtensionInfo
    {
        // 构造函数
        public MyExtInfo(IDbContextOptionsExtension extension) : base(extension)
        {
        }

        // 替换一下基类成员,方便获取
        new public DemoDbContextOptionsExtension Extension => (DemoDbContextOptionsExtension)base.Extension;

        // 此处要返回 false,因为咱们这个不是数据库提供者
        public override bool IsDatabaseProvider => false;

        // 自定义日志输出
        public override string LogFragment => $"这是个大扩展 - { Extension.RandValue.ToString()}";

        // 使用 _myRandValue 的哈希,
        public override int GetServiceProviderHashCode()
        {
            return Extension.RandValue.GetHashCode();
        }

        public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
        {
            // 设置调试信息
            debugInfo["MyExtension:RandomValue"] = Extension.RandValue.ToString();
        }

        public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
        {
            // 可以直接 retrun true
            return other is MyExtInfo;
        }
    }
}

第四步,将自定义扩展添加到 DbContextOptions 的扩展集合中有点麻烦,所以一般要封装一个扩展方法,方便调用。

public static class DemoDbContextOptionsBuilderExtensions
{
    public static DbContextOptionsBuilder UseDemoExt(
        this DbContextOptionsBuilder builder)
    {
        // 在添加前应当查找一下,避免重复添加
        var myext = builder.Options.FindExtension<DemoDbContextOptionsExtension>();
        // 如果没有,就new一个
        myext ??= new DemoDbContextOptionsExtension();
        // 设置一下随机数据,模拟配置改变
        myext.RandValue = Random.Shared.Next(100, 9999999);
        // 添加到扩展集合中(注意类型转换)
        ((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(myext);

        return builder;
    }
}

a、要养成先找后加的习惯,即先 Find 一下扩展是不是已在集合中,然后再调用 AddOrUpdateExtension 方法添加到扩展集合中。

b、由于此方法是显式实现了 IDbContextOptionsBuilderInfrastructure 接口,所以要先把 builder 转换为 IDbContextOptionsBuilderInfrastructure 接口类型再调用 AddOrUpdateExtension 方法。

第五步,回过头去修改 DemoDbContext 类。

public class DemoDbContext : DbContext
{
    ……

    protected override void OnConfiguring(DbContextOptionsBuilder opBuilder)
    {
        opBuilder.UseSqlite("data source=:memory:")
                        // 开启日志
                        .LogTo(log => Console.WriteLine(log))
                        // 使用自定义的扩展
                        .UseDemoExt();
    }

    // 测试服务
    public void Greeting(string who)
    {
        IHelloWorld sv = this.GetService<IHelloWorld>();
        sv.SayHello(who);
    }
}

第六步,实例化上下文对象,运行,实验一下。

static void Main(string[] args)
{
    using var ctx = new DemoDbContext();
    ctx.Greeting("小王");
}

运行结果如下图所示。

image

 很显然,ServiceProviderDebugInfo 事件没有日志输出的。

现在,老周就说一下如何让它输出这个事件。

方法:使用 .NET Logging API。比如,咱们要使日志输出到控制台,需要添加 Microsoft.Extensions.Logging.Console 包的引用。

在上下文类中,定义 ILoggerFactory 类型的字段,并用 LoggerFactory.Create 方法创建实例。

public class DemoDbContext : DbContext
{
    // 静态成员
    static  ILoggerFactory logFac = LoggerFactory.Create(lb => {
        lb.AddConsole();
        lb.SetMinimumLevel(LogLevel.Trace);
     });

    ……

    protected override void OnConfiguring(DbContextOptionsBuilder opBuilder)
    {
        opBuilder.UseSqlite("data source=:memory:")
                        // 开启日志
                        //.LogTo(log => Console.WriteLine(log))
                        .UseLoggerFactory(logFac)
                        // 使用自定义的扩展
                        .UseDemoExt();
    }

    ……
}

 SetMinimumLevel 方法将日志级别设置为 Debug 或 Trace。

在 OnConfiguring 方法中,使用 UseLoggerFactory 方法应用 LoggerFactory 对象。

修改之后,重新运行程序。结果如下。

image

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