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

推荐订阅源

S
Schneier on Security
GbyAI
GbyAI
H
Help Net Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
The Blog of Author Tim Ferriss
Vercel News
Vercel News
Microsoft Azure Blog
Microsoft Azure Blog
Google DeepMind News
Google DeepMind News
Stack Overflow Blog
Stack Overflow Blog
AWS News Blog
AWS News Blog
Spread Privacy
Spread Privacy
Apple Machine Learning Research
Apple Machine Learning Research
L
LINUX DO - 最新话题
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Privacy International News Feed
C
CERT Recently Published Vulnerability Notes
C
Cybersecurity and Infrastructure Security Agency CISA
I
Intezer
Hugging Face - Blog
Hugging Face - Blog
H
Heimdal Security Blog
N
News and Events Feed by Topic
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Troy Hunt's Blog
大猫的无限游戏
大猫的无限游戏
SecWiki News
SecWiki News
Martin Fowler
Martin Fowler
人人都是产品经理
人人都是产品经理
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Application and Cybersecurity Blog
Application and Cybersecurity Blog
F
Fortinet All Blogs
Hacker News: Ask HN
Hacker News: Ask HN
Know Your Adversary
Know Your Adversary
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 聂微东
S
Secure Thoughts
云风的 BLOG
云风的 BLOG
O
OpenAI News
The Cloudflare Blog
Jina AI
Jina AI
U
Unit 42
L
Lohrmann on Cybersecurity
Attack and Defense Labs
Attack and Defense Labs
The Register - Security
The Register - Security
G
Google Developers Blog
量子位
Simon Willison's Weblog
Simon Willison's Weblog
V
Visual Studio Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
GRAHAM CLULEY
TaoSecurity Blog
TaoSecurity Blog

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
.NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
飞鸿踏雪 · 2020-06-10 · via 元视角

接触 .NET Core 有一段时间了,最大的感受无外乎无所不在的依赖注入,以及抽象化程度更高的全新框架设计。想起三年前 Peter 大神手写 IoC 容器时的惊艳,此时此刻,也许会有不一样的体会。的确,那个基于字典实现的 IoC 容器相当“简陋”,就像 .NET Core 里的依赖注入,默认(原生)都是采用构造函数注入的方式,可其实从整个依赖注入的理论上而言,属性注入和方法注入的方式,同样是依赖注入的实现方式啊。最近一位朋友找我讨论,.NET Core 里该如何实现 Autowried,这位朋友本身是 Java 出身,一番攀谈了解到原来是指属性注入啊。所以,我打算用两篇博客来聊聊 .NET Core 中的原生 DI 的扩展,而今天这篇,则单讲基于名称的注入的实现。

Autofac是一个非常不错的 IoC 容器,通常我们会使用它来替换微软内置的 IoC 容器。为什么要这样做呢?其实,微软在其官方文档中早已给出了说明,即微软内置的 IoC 容器实际上是不支持以下特性的: 属性注入、基于名称的注入、子容器、自定义生存期管理、对迟缓初始化的 Func 支持、基于约定的注册。这是我们为什么要替换微软内置的 IoC 容器的原因,除了 Autofac 以外,我们还可以考虑 UnityCastle 等容器,对我个人而言,其实最需要的一个功能是“扫描”,即它可以针对程序集中的组件或者服务进行自动注册。这个功能可以让人写起代码更省心一点,果然,人类的本质就是让自己变得更加懒惰呢。好了,话题拉回到本文主题,我们为什么需要基于名称的注入呢?它其实针对的是“同一个接口对应多种不同的实现”这种场景。

OK ,假设我们现在有一个接口 ISayHello,它对外提供一个方法 SayHello:

public interface ISayHello
{
  string SayHello(string receiver);
}

相对应地,我们有两个实现类,ChineseSayHello 和 EnglishSayHello:

//ChineseSayHello
public class ChineseSayHello : ISayHello
{
  public string SayHello(string receiver)
  {
      return $"你好,{receiver}";
  }
}

//EnglishSayHello
public class EnglishSayHello : ISayHello
{
  public string SayHello(string receiver)
  {
      return $"Hello,{receiver}";
  }
}

接下来,一顿操作猛如虎:

var services = new ServiceCollection();
services.AddTransient<ISayHello, ChineseSayHello>();
services.AddTransient<ISayHello, EnglishSayHello>();
var serviceProvider = services.BuildServiceProvider();
var sayHello = serviceProvider.GetRequiredService<ISayHello>();

没想到,尴尬的事情就发生了,大家来猜猜看,这个时候我们获取到的ISayHello到底是哪一个呢?事实上,它会获取到EnglishSayHello这个实现类,为什么呢?因为它后注册的呀!当然,微软的工程师们不可能想不到这个问题,所以,官方推荐的做法是使用IEnumerable<ISayHello>,这样我们就能拿到所有注册的ISayHello,然后自己决定到底要使用一种实现,类似下面这样:

var sayHellos = _serviceProvider.GetRequiredService<IEnumerable<ISayHello>>();
var chineseSayHello = sayHellos.FirstOrDefault(x => x.GetType() == (typeof(ChineseSayHello)));
var englishSayHello = sayHellos.FirstOrDefault(x => x.GetType() == (typeof(EnglishSayHello)));

可这样还是有一点不方便啊,继续改造:

services.AddTransient<ChineseSayHello>();
services.AddTransient<EnglishSayHello>();
services.AddTransient(implementationFactory =>
{
  Func<string, ISayHello> sayHelloFactory = lang =>
  {
    switch (lang)
    {
      case "Chinese":
        return implementationFactory.GetService<ChineseSayHello>();
      case "English":
        return implementationFactory.GetService<EnglishSayHello>();
      default:
        throw new NotImplementedException();
    }
  };

  return sayHelloFactory;
});

这样子,这个工厂类看起来就消失了对吧,其实并没有(逃

var sayHelloFactory = _serviceProvider.GetRequiredService<Func<string, ISayHello>>();
var chineseSayHello = sayHelloFactory("Chinese");
var englishSayHello = sayHelloFactory("English");

这距离我们的目标有一点接近了哈,唯一的遗憾是这个工厂类对调用方是透明的,可谓是隐藏细节上的失败。有没有更好的方案呢?好了,我不卖关子啦,一起来看下面的实现。

首先,我们定义一个接口INamedServiceProvider, 顾名思义,就不需要再解释什么了:

public interface INamedServiceProvider
{
  TService GetService<TService>(string serviceName);
}

接下来,编写实现类NamedServiceProvider:

public class NamedServiceProvider : INamedServiceProvider
{
  private readonly IServiceProvider _serviceProvider;
  private readonly IDictionary<string, Type> _registrations;
  public NamedServiceProvider(IServiceProvider serviceProvider, IDictionary<string, Type> registrations)
  {
    _serviceProvider = serviceProvider;
    _registrations = registrations;
  }

  public TService GetService<TService>(string serviceName)
  {
    if(!_registrations.TryGetValue(serviceName, out var implementationType))
      throw new ArgumentException($"Service \"{serviceName}\" is not registered in container");
    return (TService)_serviceProvider.GetService(implementationType);
  }
}

可以注意到,我们这里用一个字典来维护名称和类型间的关系,一切仿佛又回到三年前 Peter 大神手写 IoC 的那个下午。接下来,我们定义一个INamedServiceProviderBuilder, 它可以让我们使用链式语法注册服务:

public interface INamedServiceProviderBuilder
{
  INamedServiceProviderBuilder AddNamedService<TService>(string serviceName, ServiceLifetime lifetime) where TService : class;

  INamedServiceProviderBuilder TryAddNamedService<TService>(string serviceName, ServiceLifetime lifetime) where TService : class;

  void Build();
}

这里,Add 和 TryAdd 的区别就是后者会对已有的键进行检查,如果键存在则不会继续注册,和微软自带的 DI 中的 Add/TryAdd 对应,我们一起来看它的实现:

public class NamedServiceProviderBuilder : INamedServiceProviderBuilder
{
  private readonly IServiceCollection _services;
  private readonly IDictionary<string, Type> _registrations = new Dictionary<string, Type>();
  public NamedServiceProviderBuilder(IServiceCollection services)
  {
    _services = services;
  }

  public void Build()
  {
    _services.AddTransient<INamedServiceProvider>(sp => new NamedServiceProvider(sp, _registrations));
  }

  public INamedServiceProviderBuilder AddNamedService<TImplementation>(string serviceName, ServiceLifetime lifetime) where TImplementation : class
  {
    switch (lifetime)
    {
      case ServiceLifetime.Transient:
        _services.AddTransient<TImplementation>();
      break;
      case ServiceLifetime.Scoped:
        _services.AddScoped<TImplementation>();
      break;
      case ServiceLifetime.Singleton:
        _services.AddSingleton<TImplementation>();
      break;
    }

    _registrations.Add(serviceName, typeof(TImplementation));
    return this;
  }

  public INamedServiceProviderBuilder TryAddNamedService<TImplementation>(string serviceName, ServiceLifetime lifetime) where TImplementation : class
  {
    switch (lifetime)
    {
      case ServiceLifetime.Transient:
        _services.TryAddTransient<TImplementation>();
      break;
      case ServiceLifetime.Scoped:
        _services.TryAddScoped<TImplementation>();
      break;
      case ServiceLifetime.Singleton:
        _services.TryAddSingleton<TImplementation>();
      break;
    }

    _registrations.TryAdd(serviceName, typeof(TImplementation));
    return this;
  }
}

相信到这里,大家都明白博主的意图了吧,核心其实是在Build()方法中,因为我们最终需要的是其实是NamedServiceProvider,而在此之前的种种,都属于收集依赖、构建 ServiceProvider 的过程,所以,它被定义为NamedServiceProviderBuilder,我们在这里维护的这个字典,最终会被传入到NamedServiceProvider的构造函数中,这样我们就知道根据名称应该返回哪一个服务了。

接下来,为了让它和微软自带的 DI 无缝粘合,我们需要编写一点扩展方法:

public static class ServiceCollectionExstension
{
  public static TService GetNamedService<TService>(this IServiceProvider serviceProvider, string serviceName)
  {
    var namedServiceProvider = serviceProvider.GetRequiredService<INamedServiceProvider>();
    if (namedServiceProvider == null)
      throw new ArgumentException($"Service \"{nameof(INamedServiceProvider)}\" is not registered in container");

    return namedServiceProvider.GetService<TService>(serviceName);
  }


  public static INamedServiceProviderBuilder AsNamedServiceProvider(this IServiceCollection services)
  {
    var builder = new NamedServiceProviderBuilder(services);
    return builder;
  }
}

现在,回到我们一开始的问题,它是如何被解决的呢?

services
  .AsNamedServiceProvider()
  .AddNamedService<ChineseSayHello>("Chinese", ServiceLifetime.Transient)
  .AddNamedService<EnglishSayHello>("English", ServiceLifetime.Transient)
  .Build();
var serviceProvider = services.BuildServiceProvier();
var chineseSayHello = serviceProvider.GetNamedService<ISayHello>("Chinese");
var englishSayHello = serviceProvider.GetNamedService<ISayHello>("English");

这个时候,对调用方而已,依然是熟悉的 ServiceProvider,它只需要传入一个名称来获取服务即可,由此,我们就实现了基于名称的依赖注入。回顾一下它的实现过程,其实是一个逐步推进的过程,我们使用依赖注入,本来是希望依赖抽象,即针对同一个接口,可以无痛地从一种实现切换到另外一种实现。可我们发现,当这些实现同时被注册到容器里的时候,容器一样会迷惑于到底用哪一种实现,这就让我们开始思考,这种基于字典的 IoC 容器设计方案是否存在缺陷。所以,在.NET Core 里的 DI 设计中还引入了工厂的概念,因为并不是所以的 Resolve都可以通过Activator.Create来实现,更不必说 Autofac 和 Castle 中还有子容器的概念,只能说人生不同的阶段总会有不同的理解吧!好了,这篇博客就先写到这里,欢迎大家给我留言,晚安!