.NET 通过源码深究依赖注入原理

依赖注入 (DI) 是.NET中一个非常重要的软件设计模式,它可以帮助我们更好地管理和组织组件,提高代码的可读性,扩展性和可测试性。在日常工作中,我们一定遇见过这些问题或者疑惑。

  1. Singleton服务为什么不能依赖Scoped服务?
  2. 多个构造函数的选择机制?
  3. 源码是如何识别循环依赖的?

虽然我们可能已经知道了答案,但本文将通过阅读CLR源码的方式来学习DI实现机制,同时也更加深入地理解上述问题。如果您不想阅读源码,可以直接跳至文末的解决方案。

一、源码解读

理论知识

理论篇可以先看一下,防止在下文代码不知道这些对象的作用。如果有些概念不是很清晰可以先记着,带入下文源码,应该就可以理解

ServiceProvider: ServiceProvider(依赖注入容器)不仅对外提供GetService()、GetRequiredService()方法,还可以方便地注册和管理应用程序需要的各种服务。
通过创建ServiceProvider的方式,我们可以更好地理解管理和控制服务实例的生命周期和依赖关系。

  1. 应用程序级别的根级ServiceProvider
    .NET Core应用程序通常会使用一个应用程序级别的根级ServiceProvider,它是全局唯一的,并且负责维护所有单例服务的实例。这个实例通常是由WebHostBuilder、HostBuilder或ServiceCollection等类创建和配置的,可以通过IServiceProvider接口来访问。

  2. 每个请求的作用域级别的ServiceProvider
    除了根级ServiceProvider之外,在.NET Core中还可以创建每个请求的作用域级别的ServiceProvider,它通常用于管理Scoped和Transient服务的生命周期和依赖关系。每个作用域级别的ServiceProvider都有自己独立的作用域,可以通过IServiceScopeFactory创建,同时也继承了根级ServiceProvider中注册的所有单例服务的实例。

  3. 自定义级别的ServiceProvider
    在某些情况下,我们可能需要自定义级别的ServiceProvider来满足特定的业务需求,例如,将多个ServiceProvider组合起来以提供更高级别的服务解析和管理功能。此时,我们可以通过实现IServiceProviderFactory接口和IServiceProviderBuilder接口来创建和配置自定义级别的ServiceProvider,从而实现更灵活、可扩展的依赖注入框架。

生命周期管理: 我们可以将依赖注入容器看作一个树形结构,其中root节点的子节点是Scoped节点,每个Scoped节点的子节点是Transient节点(如果存在)。在容器初始化时,会在root节点下创建和缓存所有单例服务的实例,以及创建第一个Scoped节点。每个Scoped节点下都有一个独立的作用域,用于管理Scoped服务的生命周期和依赖关系,同时还继承了父级节点(即root或其他Scoped节点)的所有单例服务的实例。
在处理每个新的请求时,依赖注入容器会创建一个新的Scoped节点,并在该节点下创建和缓存该请求所需的所有Scoped服务的实例。在完成请求处理后,该Scoped节点及其下属的服务实例也将被销毁,从而确保Scoped服务实例的生命周期与请求的作用域相对应。

重要对象

  • IServiceCollection: 用于注册应用程序所需的服务实例,并将其添加到依赖注入容器中。

  • IServiceScopeFactory: 用于创建依赖注入作用域(IServiceScope)的工厂类。每个IServiceScope都可以独立地管理Scoped和Transient类型的服务实例,并在作用域结束时释放所有资源。IServiceScope通过ServiceProvider属性来访问该作用域内的服务实例

  • ServiceProvider: 可以看作是一个服务容器,它可以方便地注册、提供和管理应用程序需要的各种服务。还支持创建依赖注入作用域(IServiceScope),可以更好地管理和控制服务实例的生命周期和依赖关系

  • IServiceProviderFactory: 创建最终的依赖注入容器(IServiceProvider),提供默认的DefaultServiceProviderFactory(也就是官方自带的IOC),也支持自定义的,比如autofac的AutofacServiceProviderFactory工厂。

  • ServiceProviderEngineScope: 实现了IServiceProvider和IDisposable接口,用于创建和管理依赖注入作用域(Scope)。通过使用ServiceProviderEngineScope,我们可以访问依赖注入作用域中的服务实例,并实现Scoped和Transient类型的服务实例的生命周期管理。作用域机制可以帮助我们更好地管理和控制应用程序的各个组件之间的依赖关系

  • CallSiteFactory: 通常由依赖注入容器(如ServiceProvider)在服务解析过程中使用。当容器需要解析某个服务时,它会创建一个CallSiteFactory对象,并使用其中的静态方法来创建对应的ServiceCallSite对象。然后,容器会将这些ServiceCallSite对象组合成一个树形结构,最终构建出整个服务实例的解析树。

  • ServiceCallSite: 表示服务的解析过程。它包含了服务类型、服务的生命周期、以及从容器中获取服务实例的方法等信息

  • CallSiteVisitor: 通常由依赖注入容器(如ServiceProvider)在服务解析过程中使用。当容器需要解析某个服务时,它会创建一个ServiceCallSite的对象图,并将其传递给CallSiteVisitor进行遍历和访问。CallSiteVisitor通过调用不同节点的虚拟方法,将每个节点的信息收集起来,并最终构建出服务实例的解析树。

  • CallSiteValidator 通常由依赖注入容器(如ServiceProvider)在服务解析过程中使用,用于验证ServiceCallSite对象图的正确性。它提供了一组检查方法,可以检测ServiceCallSite对象图中可能存在的循环依赖、未注册的服务类型和生命周期问题等。

阅读源码

以下是源代码的部分删减和修改,以便于更好地理解

为了更好地理解依赖注入的整个流程,可以根据依赖注入容器将其简单理解为以下两个模块:

  • 服务注册:将服务及其对应的生命周期(例如 Singleton、Scoped 或 Transient)添加到依赖注入容器中。
  • 服务提供:在应用程序运行时,依赖注入容器会根据需要创建并提供服务的实例,以满足应用程序中各个类之间的依赖关系。

配置ConfigureServices,将服务对象(ServiceDescriptor)注册到了IServiceCollection集合,构建Host主机的时候,会调用BuildServiceProvide()方法创建IServiceProvider,并获取相关服务。

public IWebHost Build()
{ 
    var hostingServices = BuildCommonServices(out var hostingStartupErrors);// 构建WebHost通用服务

    var hostingServiceProvider = GetProviderFromFactory(hostingServices);
    
    // 获取ServiceProvider
    IServiceProvider GetProviderFromFactory(IServiceCollection collection)
    {
        // 构建IServiceProvider对象
        var provider = collection.BuildServiceProvider(); 
        // 获取服务
        var factory = provider.GetService<IServiceProviderFactory<IServiceCollection>>();

        // 是否使用默认的DefaultServiceProviderFactory类
        if (factory != null && !(factory is DefaultServiceProviderFactory))
        {
            using (provider)
            {
                return factory.CreateServiceProvider(factory.CreateBuilder(collection));
            }
        }
        
        return provider;
    }
}

BuildServiceProvider是IServiceCollection接口的扩展方法。该方法用于创建一个IServiceProvider接口实例,并将已注册到IServiceCollection容器中的服务对象注入到该实例中。

public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
{
    // 生成ServiceProvider对象
    return new ServiceProvider(services, options);
}

ServiceProvider类的构造函数,创建依赖注入容器,并将服务描述信息加载到容器中

internal ServiceProvider(ICollection<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
{
    // 创建一个根级别的服务引擎作用域
    Root = new ServiceProviderEngineScope(this, isRootScope: true); 
    // 获取服务引擎用于解析依赖关系
    _engine = GetEngine();
    // 访问器,动态创建服务
    _createServiceAccessor = CreateServiceAccessor;
    // 缓存已经解析出来的服务实例(线程安全)
    _realizedServices = new ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object?>>();
    
    // CallSiteFactory用于创建和缓存服务的调用站点(ServiceCallSite)
    CallSiteFactory = new CallSiteFactory(serviceDescriptors);
    // 添加内置的服务
    CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
    CallSiteFactory.Add(typeof(IServiceScopeFactory), new ConstantCallSite(typeof(IServiceScopeFactory), Root));
    CallSiteFactory.Add(typeof(IServiceProviderIsService), new ConstantCallSite(typeof(IServiceProviderIsService), CallSiteFactory));
    
    // ValidateScopes属性为true,表示需要验证服务范围
    if (options.ValidateScopes)
    {
        _callSiteValidator = new CallSiteValidator();
    }
    
    // ValidateOnBuild属性为true,需要检查所有服务是否能够成功创建
    if (options.ValidateOnBuild)
    {
        List<Exception>? exceptions = null;
        foreach (ServiceDescriptor serviceDescriptor in serviceDescriptors)
        {
             ValidateService(serviceDescriptor);
        }
    }
}

当ValidateOnBuild属性为true时,进入到ValidateService方法中。ValidateService方法使用CallSiteFactory对象的GetCallSite方法来获取对应的ServiceCallSite对象,并将其保存到callSite变量中。如果callSite不为null,表示该服务可以被成功创建,则调用OnCreate方法。

此时我们需要知道并理解ServiceCallSite 对象

ServiceCallSite记录着从根调用站点到当前服务实例的一条依赖链。在DI容器中,每一个已注册的服务都对应一个ServiceCallSite,而所有的CallSite又组成了整个 DI 系统的拓扑结构。在 DI 系统初始化时,容器会通过递归调用ServiceCallSite上的信息,来完成整个DI容器的配置和初始化。

internal abstract class ServiceCallSite
{
    protected ServiceCallSite(ResultCache cache)
    {
        Cache = cache;
    }
    // 服务类型
    public abstract Type ServiceType { get; }
    // 实现类型
    public abstract Type ImplementationType { get; }
    // 调用链类型(Scope、Singleton、Factory、Constructor、CreateInstance等)
    public abstract CallSiteKind Kind { get; }
    public ResultCache Cache { get; }
    // 是否需要捕获可释放资源,类似IDisposable接口
    public bool CaptureDisposable =>
        ImplementationType == null ||
        typeof(IDisposable).IsAssignableFrom(ImplementationType) ||
        typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);
}

有没有发现,ServiceCallSite和ServiceDescriptor有几分相似。那么他们有什么关系和区别呢?

  • ServiceDescriptor用于描述一个服务实例的信息,包括服务类型、实现类型、生命周期等。在容器注册服务时使用的。

  • ServiceCallSite则表示服务调用链节点,是ServiceDescriptor的运行时表示形式,即在Resolve服务时,ServiceDescriptor会被转换为相应的ServiceCallSite。ServiceCallSite包含了解析服务所需要的全部信息,包括服务类型、实现工厂、参数列表等,它能够通过递归访问自己的子节点来构建出完整的服务调用链。

ValidateService方法验证服务是否能够正常创建

private void ValidateService(ServiceDescriptor descriptor)
{
  // 这个方法中出现了循环依赖和多构造函数
  ServiceCallSite callSite = CallSiteFactory.GetCallSite(descriptor, new CallSiteChain());
  if (callSite != null)
  {
      // 这个方法中进行依赖校验
      OnCreate(callSite);
  }
}

我们先看GetCallSite方法,GetCallSite尝试从缓存中获取,如果缓存中不存在,则创建CreateCallSite。

internal ServiceCallSite GetCallSite(Type serviceType, CallSiteChain callSiteChain) =>
    _callSiteCache.TryGetValue(new ServiceCacheKey(serviceType, DefaultSlot), out ServiceCallSite site) ? site :
    CreateCallSite(serviceType, callSiteChain);

CreateCallSite是非常重要的方法,它负责创建和缓存CallSite对象,并为整个依赖注入容器的服务解析提供了基础支持

private ServiceCallSite CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
{
    var callsiteLock = _callSiteLocks.GetOrAdd(serviceType, static _ => new object());
    // 保证对CallSite缓存的线程安全。由于多个服务之间可能存在依赖关系,因此需要确保同一时间只有一个服务的CallSite被创建和缓存
    lock (callsiteLock)
    {
        // 哦吼,出现了 检查是否存在循环依赖关系,以避免产生无限递归调用
        callSiteChain.CheckCircularDependency(serviceType);
        // 依次尝试创建精确类型、开放泛型类型和IEnumerable类型的CallSite对象,并返回第一个成功创建的对象
        ServiceCallSite callSite = TryCreateExact(serviceType, callSiteChain) ??
                                   TryCreateOpenGeneric(serviceType, callSiteChain) ??
                                   TryCreateEnumerable(serviceType, callSiteChain);

        return callSite;
    }
}

此时,我们发现了判断循环依赖的方法,他是如何实现的呢?我们就要看一下callSiteChain对象了。callSiteChain用于描述服务调用站点(CallSite)之间的依赖关系。callSiteChain使用DIctionary容器存储当前链路上的CallSite。如果容器存在当前服务,说明存在循环依赖。
举个栗子: A->B B->A

  • 创建服务A,将A添加到callSiteChain
  • 创建服务B,将B添加到callSiteChain
  • 此时,又到服务A,callSiteChain存在服务A,判定为循环依赖
public CallSiteChain()
{
    _callSiteChain = new Dictionary<Type, ChainItemInfo>();
}

public void CheckCircularDependency(Type serviceType)
{
    if (_callSiteChain.ContainsKey(serviceType))
    {
        throw new InvalidOperationException(CreateCircularDependencyExceptionMessage(serviceType));
    }
}

我们选择TryCreateExact方法,进入CreateConstructorCallSite方法,该方法创建和缓存ConstructorCallSite对象。此时您就看见多个构造参数是如何进行选择的啦!如果存在多个构造函数,但其中某个构造函数的参数类型是其他构造函数的子集,则返回该构造函数对应的ConstructorCallSite对象

private ServiceCallSite CreateConstructorCallSite(
        ResultCache lifetime,
        Type serviceType,
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType,
        CallSiteChain callSiteChain)
    {
        try
        {
            // 将当前服务加入callSiteChain,以便后续的依赖解析过程中使用
            callSiteChain.Add(serviceType, implementationType);

            // 获取所有构造函数
            ConstructorInfo[] constructors = implementationType.GetConstructors();

            ServiceCallSite[] parameterCallSites = null;
            
            // 0个构造函数
            if (constructors.Length == 0)
            {
                throw new InvalidOperationException(SR.Format(SR.NoConstructorMatch, implementationType));
            }
            // 1个构造函数
            else if (constructors.Length == 1)
            {
                ConstructorInfo constructor = constructors[0];
                ParameterInfo[] parameters = constructor.GetParameters();
                if (parameters.Length == 0)
                {
                    return new ConstructorCallSite(lifetime, serviceType, constructor);
                }

                parameterCallSites = CreateArgumentCallSites(
                    implementationType,
                    callSiteChain,
                    parameters,
                    throwIfCallSiteNotFound: true);

                return new ConstructorCallSite(lifetime, serviceType, constructor, parameterCallSites);
            }
            
            // 多个构造函数如何选择,终于等到你,还好我没放弃0.0
            Array.Sort(constructors,
                (a, b) => b.GetParameters().Length.CompareTo(a.GetParameters().Length));

            ConstructorInfo bestConstructor = null;
            HashSet<Type> bestConstructorParameterTypes = null;
            for (int i = 0; i < constructors.Length; i++)
            {
                ParameterInfo[] parameters = constructors[i].GetParameters();

                ServiceCallSite[] currentParameterCallSites = CreateArgumentCallSites(
                    implementationType,
                    callSiteChain,
                    parameters,
                    throwIfCallSiteNotFound: false);

                if (currentParameterCallSites != null)
                {
                    if (bestConstructor == null)
                    {
                        bestConstructor = constructors[i];
                        parameterCallSites = currentParameterCallSites;
                    }
                    else
                    {
                        // Since we're visiting constructors in decreasing order of number of parameters,
                        // we'll only see ambiguities or supersets once we've seen a 'bestConstructor'.
                        //由于我们以参数数量递减的顺序访问构造函数,

                        //只有在看到“最佳构造函数”后,我们才会看到歧义或超集。
                        if (bestConstructorParameterTypes == null)
                        {
                            bestConstructorParameterTypes = new HashSet<Type>();
                            foreach (ParameterInfo p in bestConstructor.GetParameters())
                            {
                                bestConstructorParameterTypes.Add(p.ParameterType);
                            }
                        }

                        foreach (ParameterInfo p in parameters)
                        {
                            if (!bestConstructorParameterTypes.Contains(p.ParameterType))
                            {
                                // Ambiguous match exception
                                throw new InvalidOperationException(string.Join(
                                    Environment.NewLine,
                                    SR.Format(SR.AmbiguousConstructorException, implementationType),
                                    bestConstructor,
                                    constructors[i]));
                            }
                        }
                    }
                }
            }

            if (bestConstructor == null)
            {
                throw new InvalidOperationException(
                    SR.Format(SR.UnableToActivateTypeException, implementationType));
            }
            else
            {
                Debug.Assert(parameterCallSites != null);
                return new ConstructorCallSite(lifetime, serviceType, bestConstructor, parameterCallSites);
            }
        }
        finally
        {
            callSiteChain.Remove(serviceType);
        }
    }

看到这里,我们已经解决了两个问题:

  • 具有多个构造函数的情况下默认选择使用哪一个构造函数
  • 识别和解决循环依赖的问题

Singleton服务不能依赖Scoped服务,是如何校验的?我们回到刚才OnCreate的地方继续阅读。

private void OnCreate(ServiceCallSite callSite)
{
    _callSiteValidator?.ValidateCallSite(callSite);
}

ValidateCallSite方法,用于验证指定的ServiceCallSite对象是否正确,并将其中包含的作用域服务添加到_scopedServices字典中。

在ValidateCallSite方法中,我们首先使用VisitCallSite方法遍历整个ServiceCallSite对象,并返回其中所包含的作用域服务类型。如果ServiceCallSite对象中存在作用域服务,则将其添加到_scopedServices字典中,以便后续的依赖解析过程中使用。

public void ValidateCallSite(ServiceCallSite callSite)
{
    Type scoped = VisitCallSite(callSite, default);
    if (scoped != null)
    {
        _scopedServices[callSite.ServiceType] = scoped;
    }
}

ValidateCallSite存在VisitScopeCache方法,该方法首先判断当前ServiceCallSite对象是否是IServiceScopeFactory类型,如果是,则直接返回null。否则,我们检查state.Singleton属性是否为null,如果不为null,则说明当前ServiceCallSite对象属于单例服务,并且其中包含作用域服务的注入,此时将抛出InvalidOperationException异常,提示用户检查服务依赖关系是否正确;否则,我们继续递归遍历ServiceCallSite对象图。

protected override Type VisitScopeCache(ServiceCallSite scopedCallSite, CallSiteValidatorState state)
{
    // We are fine with having ServiceScopeService requested by singletons
    if (scopedCallSite.ServiceType == typeof(IServiceScopeFactory))
    {
        return null;
    }
    // ScopedInSingletonException异常!
    if (state.Singleton != null)
    {
        throw new InvalidOperationException(SR.Format(SR.ScopedInSingletonException,
            scopedCallSite.ServiceType,
            state.Singleton.ServiceType,
            nameof(ServiceLifetime.Scoped).ToLowerInvariant(),
            nameof(ServiceLifetime.Singleton).ToLowerInvariant()
            ));
    }

    VisitCallSiteMain(scopedCallSite, state);
    return scopedCallSite.ServiceType;
}

获取服务

以上我们可以归纳为构建IServiceProvider,然后我们通过GetService()方法,看下如何获取服务。

从缓存中获取指定类型的服务,如果缓存中不存在,则调用_createServiceAccessor委托创建一个新的实例,并将其添加到缓存中

internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
{
    // 使用了ConcurrentDictionary来缓存已经解析的服务
    Func<ServiceProviderEngineScope, object> realizedService = _realizedServices.GetOrAdd(serviceType, _createServiceAccessor);

    OnResolve(serviceType, serviceProviderEngineScope);
    
    // 服务的实际实现
    var result = realizedService.Invoke(serviceProviderEngineScope);

    return result;
}

当缓存中不存在的时候,我们使用_createServiceAccessor创建一个新的实例(和上文获取callSite流程一致)。

private Func<ServiceProviderEngineScope, object> CreateServiceAccessor(Type serviceType)
{
    // 取给定服务类型的CallSite对象
    ServiceCallSite callSite = CallSiteFactory.GetCallSite(serviceType, new CallSiteChain());

    if (callSite != null)
    {
        OnCreate(callSite);

        // 服务具有Singleton生命周期,可以优化处理,避免每次获取服务实例时都需要重新创建
        if (callSite.Cache.Location == CallSiteResultCacheLocation.Root)
        {
            object value = CallSiteRuntimeResolver.Instance.Resolve(callSite, Root);
            return scope => value;
        }
        // 服务具有Transient或Scoped生命周期,需要创建并返回一个新的服务实例访问器
        return _engine.RealizeService(callSite);
    }

    return _ => null;
}

二、解决问题

1. Singleton服务依赖Scoped服务

创建一个生命周期为单例的SingletonService和另一个生命周期为作用域的ScopedService,SingletonService服务依赖ScopedService服务。就会报错:

Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: MyTest.Service.ISingletonService Lifetime: Singleton ImplementationType: MyTest.Service.SingletonService': Cannot consume scoped service 'MyTest.Service.IScopedService' from singleton 'MyTest.Service.ISingletonService'.)

解决方法

  • 使用IServiceScopeFactory对象
public class SingletonService : ISingletonService
{
    private readonly IScopedService _scopedService;

    public SingletonService(IServiceScopeFactory serviceScopeFactory)
    {
        _scopedService = serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService<IScopedService>();
    }        
}
  • 使用IServiceProvider对象
public class SingletonService : ISingletonService
{
    private readonly IScopedService _scopedService;

    public SingletonService(IServiceProvider serviceProvider)
    {
        _scopedService = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<IScopedService>();
    }        
}
  • 修改生命周期
    保持生命周期一致,在修改生命周期时我们需要仔细分析服务的依赖关系和实现细节,并根据具体情况进行权衡和调整

2. 构造函数的选择逻辑

  1. 如果实现类没有构造函数,则抛出NoConstructorMatch异常。

  2. 如果实现类只有一个构造函数,判断该构造函数是否无参。如果是无参构造函数,则直接返回ConstructorCallSite;否则,对构造函数的参数创建对应的parameterCallSites,并返回ConstructorCallSite。

  3. 多个构造函数的逻辑

  • 遍历所有构造函数,以参数数量递减的方式访问。
  • 对于每个构造函数,判断其参数类型是否可以解析出来。
    • 如果可以,将该构造函数与对应参数设置为“最佳构造函数”和“最佳参数”,并继续遍历其他构造函数。
    • 若存在多个“最佳构造函数”则判断是否有歧义或超集,有则抛出AmbiguousConstructorException异常;否则,将“最佳构造函数”和“最佳参数”返回。

如果不是很理解选择逻辑,可以结合上文中的CreateConstructorCallSite方法,观看代码会更加直接,便于理解。

3. 解决循环依赖

参考:

  1. http://misko.hevery.com/2008/08/01/circular-dependency-in-constructors-and-dependency-injection/

  2. http://thomaslevesque.com/2020/03/18/lazily-resolving-services-to-fix-circular-dependencies-in-net-core/

  3. 我认为出现循环依赖,是我们代码结构设计有问题,根本解决方案是将依赖关系分解成更小的部分,从而避免出现循环依赖的情况,同时使个代码结构更加清晰、简单。

在这种情况下,真实原因是两个对象中的一个隐藏了另一个对象 C。A 包含 C 或 B 包含 C。我们假设B包含了C。

class A {
  final B b;
  A(B b){
    this.b = b;
  }
}

class B {
  final A a;
  B(A a){
    this.a = a;
  }
}

+---------+      +---------+
|    A    |<-----|  B      |
|         |      |  |  +-+ |
|         |      |  +->|C| |
|         |------+---->| | |
|         |      |     +-+ |
+---------+      +---------+

我们将C单独抽出来,作为一个服务,让A和B都依赖于C,这样就可以解决循环依赖的问题。

                         +---------+
+---------+              |    B    |
|    A    |<-------------|         |
|         |              |         |
|         |    +---+     |         |
|         |--->| C |<----|         |
|         |    +---+     +---------+
+---------+

class C {
  C(){
  }
}

class A {
  final C c;
  A(C c){
    this.c = c;
  }
}

class B {
  final A a;
  final C c;
  B(A a, C c){
    this.a = a;
    this.c = c;
  }
}
  1. 使用 IServiceProvider 对象,GetRequiredService方法去获取实例
class C : IC
{
    private readonly IServiceProvider _services;

    public C(IServiceProvider services)
    {
        _services = services;
    }

    public void Bar()
    {
        ...
        var a = _services.GetRequiredService<IA>();
        a.Foo();
        ...
    }
}
  1. 使用 Lazy
    下边的方法我利用了Lazy类,需要添加一个 IServiceCollection 的扩展,新建一个静态类
public static IServiceCollection AddLazyResolution(this IServiceCollection services)
{
    return services.AddTransient(
        typeof(Lazy<>),
        typeof(LazilyResolved<>));
}

private class LazilyResolved<T> : Lazy<T>
{
    public LazilyResolved(IServiceProvider serviceProvider)
        : base(serviceProvider.GetRequiredService<T>)
    {
    }
}

然后再 Startup.cs 中的 ConfigureServices 方法中这样写

services.AddLazyResolution();

在依赖的类中IA,注入Lazy,当您需要使用时IA,只需访问lazy的值 Value 即可:

class C : IC
{
    private readonly Lazy<IA> _a;

    public C(Lazy<IA> a)
    {
        _a = a;
    }

    public void Bar()
    {
        ...
        _a.Value.Foo();
        ...
    }
}

注意:不要访问构造函数中的值,保存Lazy即可 ,在构造函数中访问该值,这将导致我们试图解决的相同问题。

这个解决方案不是完美的,但是它解决了最初的问题却没有太多麻烦,并且依赖项仍然在构造函数中明确声明,我可以看到类之间的依赖关系。

如果您觉得这篇文章有所收获,还请点个赞并关注。如果您有任何建议或意见,欢迎在评论区留言,非常感谢您的支持和指导!

本文转载于网络 如有侵权请联系删除

相关文章

  • Zoom 妥协!对免费用户开放端到端加密服务

    By超神经内容概要:2020年伊始,世界范围内多国爆发新冠疫情,企业在家办公情加速了视频会议软件的发展,Zoom无疑是其中发展势头最猛,也最具争议性的一个。关键词:Zoom端到端加密隐私保护Zoom周三表示,将在7月份的测试版中,为所有用户提供视频通话的端到端加密服务,允许管理员自主决定是否接受加入会议的申请。Zoom在官方博客中宣布在7月测试版中对所有用户开放端到端加密服务据笔者了解,Zoom的同类产品钉钉、腾讯会议、飞书等,均早已实现端到端加密。Zoom的2020,惊险刺激还有点无奈2020年,视频会议软件Zoom坐上了过山车。先是乘着疫情的东风,在全球范围内开启自动收割模式,用户数在四个月内激增20倍;紧接着爆出漏洞,安全性开始遭受质疑,被媒体、企业、用户多方施压。从3月中下旬开始,Zoom泄露用户隐私数据、被多家企业禁用、遭股东集体诉讼、被多方黑客攻击……4月27日港中大用Zoom考试期间黑客入侵传播不可描述内容3月底,Zoom承认,安全漏洞的重要原因就是没有使用端到端加密服务,标准的浏览器数据加密无法应对网络黑客的频繁攻击。4月,Zoom高薪聘请Facebook前首席安全官A

  • 【从小白到专家】Istio系列之二:核心组件介绍

    导读:Istio,被称作Kubernetes的最佳云原生拍档。我们推出“Istio技术实践”系列专题,在本专题中,将通过技术文章+视频授课的方式,为大家详细阐述Istio微服务治理,及在企业级云平台中的解决方案和实践。同时,您还可以申请试用灵雀云基于原生Istio和Kubernetes的微服务产品ASM!上一篇文章中,我们讲到Istio的基本概念、架构基础。Istio作为ServiceMesh领域的集大成者,提供了流控、安全、遥测等模型,其功能复杂,模块众多,本篇文章会对Istio1.3.5的各组件进行分析,帮助大家了解Istio各组件的职责、以及相互的协作关系。 Istio架构回顾数据平面:数据平面由一组sidecar的代理(Envoy)组成。这些代理调解和控制微服务之间的所有网络通信,并且与控制平面的pilot通讯,接受调度策略。控制平面:控制平面通过管理和配置Envoy来管理流量。此外,控制平面pilot下发xds规则来实施路由策略,mixer收集检测到的监控数据。虽然Istio支持多个平台,但将其与Kubernetes结合使用,其优势会更大,Istio对Kubernetes平台

  • 【全网最全的博客美化系列教程】19.旋转立方体的实现

    全网最全的博客美化系列教程相关文章目录【全网最全的博客美化系列教程】01.添加Github项目链接【全网最全的博客美化系列教程】02.添加QQ交谈链接【全网最全的博客美化系列教程】03.给博客添加一只萌萌哒的小仓鼠【全网最全的博客美化系列教程】04.访客量统计的实现【全网最全的博客美化系列教程】05.公告栏个性时间显示的实现【全网最全的博客美化系列教程】06.推荐和反对炫酷样式的实现【全网最全的博客美化系列教程】07.添加一个分享的按钮吧【全网最全的博客美化系列教程】08.自定义地址栏Logo【全网最全的博客美化系列教程】09.添加"扩大/缩小浏览区域大小"按钮【全网最全的博客美化系列教程】10.小火箭置顶特效的实现【全网最全的博客美化系列教程】11.鼠标点击爱心特效的实现【全网最全的博客美化系列教程】12.修改鼠标图案【全网最全的博客美化系列教程】13.鼠标点击效果升级的实现【全网最全的博客美化系列教程】14.代码高亮设置的实现【全网最全的博客美化系列教程】15.动画幻灯效果的实现【全网最全的博客美化系列教程】16.给博客添加一个打赏的实现【全网最全的博客美化系列

  • Spring模块组成(框架组成、整体架构、体系架构、体系结构)

    Spring是一个轻量级Java开发框架,致力于简化Java开发。Spring总共大约有20个模块,由1300多个不同的文件构成。而这些组件被分别整合在核心容器(CoreContainer)、AOP(AspectOrientedProgramming)和设备支持(Instrmentation)、数据访问与集成(DataAccess/Integeration)、Web、消息(Messaging)、Test等6个模块中。以下是Spring5的模块结构图:组成Spring框架的每个模块集合或者模块都可以单独存在,也可以一个或多个模块联合实现。每个模 块的组成和功能如下:核心容器Spring的核心容器是其他模块建立的基础,有spring-core、spring-beans、spring-context、spring-context-support和spring-expression(Spring表达式语言)等模块组成。spring-core模块:提供了框架的基本组成部分,包括控制反转(InversionofControl,IOC)和依赖注入(DependencyInjection,DI)功能。

  • Django实战-信息资讯-付费下载

    Django网络应用开发的5项基础核心技术包括模型(Model)的设计,URL的设计与配置,View(视图)的编写,Template(模板)的设计和Form(表单)的使用。确认订单,课程购买状态查询,付费下载;确认订单需要POST请求后端传入商品名称、支付方式、订单号、价格,然后对接收的值进行MD5加密。key=md5((goodsname+istype+notify_url+orderid+orderuid+price+return_url+token+uid).encode("utf-8")).hexdigest()复制python内置哈希库对字符串进行md5加密的方法:首先是导入md5加密所需模块:importhashlib然后创建md5对象:m=hashlib.md5()传入需要加密的字符串进行md5加密,然后就可以获取到经过md5加密的字符串了:encodestr=m.hexdigest()。①确认订单deforder_key(request): goodsname=request.POST.get('goodsname') istyp

  • 如何使用 chrome 开发者工具来调试程序以及相关技巧

    很多人看了我之前写的文章,都说不会如何去调试,那今天就和大家分享是我如何去使用chrome开发者工具进行调试的。先说明:以下内容均是我个人在使用开发者工具时自己探索的,相关的功能有可能说得不是很对,如果你发现我说错了,欢迎指出!或者在评论区分享一些别的技巧。1.第一排按钮先说下这几个按钮,从左到右按顺序:跳到下一个断点处,如果后面没有断点了的话,就会停止调试不跳入函数内执行下一行代码,当函数内部逻辑太多或者不重要的时候,可以使用这个向下执行一行代码,会进入函数内部,需要理解函数内部的逻辑时候就可以使用这个跳出当前函数,当你所在的函数内部有循环或者突然觉得这函数可以跳过,就可以使用这个禁止所有断点,不做任何调试,一般很少用程序运行到异常时是否中断的开关,也很少用,我们一般调试别人的程序很少会有异常。上面这几个按钮常用的就前面5个,对进行逆向JS时需要调试时非常重要,需要熟悉使用,当你掌握了如何去用的话,调试程序起来会得心应手。2.watch和callstack这个用于监视变量的值的,比如在一段程序种,你需要关注哪个变量在什么时候变化了,就可以在这里点击右上角的加号进行添加,来观察在调试过

  • Ubuntu 18.04.2 LTS nvidia-docker2 : 依赖: docker-ce (= 5:18.09.0~3-0~ubuntu-bionic)

    平台:Ubuntu18.04.2LTS nvidia-docker2版本:2.0.3错误描述:在安装nvidia-docker2的时候报dpkg依赖错误 nvidia-docker2:依赖:docker-ce(=5:18.09.0~3-0~ubuntu-bionic) 先看一下依赖关系,我的docker-ce版本是17,太低apt-cachemadisonnvidia-docker2nvidia-container-runtime复制解决办法:更换到官方docker安装源,指定版本安装 先卸载所有的旧版本dockersudoapt-getremovedocker*--purge复制安装包以允许通过HTTPS使用存储库sudoapt-getinstall\ apt-transport-https\ ca-certificates\ curl\ software-properties-common复制添加Docker的官方GPG密钥:curl-fsSLhttps://download.docker.com/linux/ubuntu/gpg|sudoapt-keyadd- sudoapt-k

  • 如何查看github星数排行榜?

    先放一个懒人链接(点击直接进入最终页面):https://github.com/search?o=desc&p=1&q=stars%3A%3E50000&s=stars&type=Repositorieshttps://github.com托管了我们这个星球上开放源代码的项目,github的注册用户会为优秀的项目加星(类似朋友圈的点赞),星数越多,说明项目越受欢迎 进入githubhttp://github.com复制 查看星数大于5万的项目stars:>50000复制 查看排名前三的项目(1k是一千的意思) 小结君子性非异也,善假于物也,找到优秀的开源项目,学习它的源码,是提高编程能力最快的方法,即使你不是程序员,也能从github找到一些好的开源工具,提升你的工作效率,当你对github有一定了解之后,或许你会发出这样的感叹:github!啥都有!

  • 那些年,我追过的语言

    程序君也年轻过,年轻的代价就是盲目追随。从MS-DOS6.0开始,程序君就是微软的狂热拥趸。这种狂热自win95走上高潮(有谁还记得win95光盘里带的GoodTimes的MV,请举手),历经《未来之路》,windows2000,最后在dotnet发布后到达顶峰。那段时间,只要微软反对的,就是我反对的。不喜欢NetscapeNavigator,只因为IE;诅咒过SUN,对Java深恶痛绝,因为NC是PC的死敌,SUN妄图革微软的命;使用VisualBasic,啃MFC,不为别的,就因为poweredbyMicrosoft。但VB功能太弱(其实还是我水平太差),MFC太乱,以至于大二时,我在给人打工做软件的时候无奈地选择了Delphi。虽然不怎么喜欢严谨的pascal,但Delphi有让我不得不用的理由。用它写代码清新明快,效率上甩了笨重的MFC几条街,速度上让VB相形见拙。在那个WakeOnLAN才兴起不久的年代,我用dephi做的印象最深刻的一个小feature,是通过一台电脑打开(或者关闭)局域网内几十台电脑。看着自己的软件能『神奇』地唤醒机房里的一群电脑,别提多自豪了。在我上大学

  • 腾讯云数据传输服务错误码数据传输服务API20180330

    功能说明如果返回结果中存在Error字段,则表示调用API接口失败。例如: { "Response":{ "Error":{ "Code":"AuthFailure.SignatureFailure", "Message":"Theprovidedcredentialscouldnotbevalidated.Pleasecheckyoursignatureiscorrect." }, "RequestId":"ed93f3cb-f35e-473f-b9f3-0d451b8b79c6" } }复制 Error中的Code表示错误码,Message表示该错误的具体信息。 错误码列表公共错误码 错误码 说明 ActionOffline 接口已下线。 AuthFailure.InvalidAuthorization 请求头部的Authorization不符合腾讯云标准。 AuthFailure.InvalidSecre

  • 各种性质、定理

    在这里记录一下做题中遇到的各种性质、定理,数论知识偏多,没有什么顺序,只是做到了就记录一下,不断更新。。   费马小定理是数论中的一个重要定理,其内容为:假如p是质数,且(a,p)=1,那么a^(p-1)≡1(modp)。即:假如p是质数,且a,p互质,那么a的(p-1)次方除以p的余数恒等于1。 欧拉定理,(也称费马-欧拉定理)是一个关于同余的性质。欧拉定理表明,若n,a为正整数,且n,a互质,则:   欧拉函数 定义:用于计算p(n),比n小的所有与n互质的数。 计算公式:p(n)=n*(1-1/p1)*(1-1/p2)....*(1-1/pk)【p1,p2,pk都是n的素因子】 另:若n=p1^q1*p2^q2*.....*pk^qk 则,p(n)=(p1-1)*p1^(q1-1)*(p1-1)*p2^(q2-1)......*(pk-1)*pk^(qk-1) 性质:若m,n互质,φ(mn)=φ(m)φ(n)。当n为奇数时,φ(2n)=φ(n) 欧拉定理: a,m互质,a^φ(m)≡1(modm) 例:2,3互质,那么,2^2%3=1 推论:对于互质的数a、n

  • LXD 2.0 系列(十):LXD 和 Juju

    这是LXD2.0系列介绍文章的第十篇。 LXD入门 安装与配置 你的第一个LXD容器 资源控制 镜像管理 远程主机及容器迁移 LXD中的Docker LXD中的LXD 实时迁移 LXD和Juju LXD和OpenStack 调试,及给LXD做贡献 介绍 Juju是Canonical的服务建模和部署工具。它支持非常广泛的云服务提供商,使您能够轻松地在任何云上部署任何您想要的服务。 此外,Juju2.0还支持LXD,既适用于本地部署,也适合开发,并且可以在云实例或物理机上共同协作。 本篇文章将关注本地使用,通过一个没有任何Juju经验的LXD用户来体验。 要求 本篇文章假设你已经安装了LXD2.0并且配置完毕(看前面的文章),并且是在Ubuntu16.04LTS上运行的。 设置Juju 第一件事是在Ubuntu16.04上安装Juju2.0。这个很简单: stgraber@dakara:~$sudoaptinstalljuju Readingpackagelists...Done Buildingdependencytree Readingstateinformation...Don

  • 图解,为多个oracle数据库下添加ArcSde实例

    最开始肯定要先建一个oracle数据库,我假设名称为dbgis   1,   2, 3, 不重新指定就会出现这个错误,因为以前有sde.dbf文件了 4, 5, 6, 7, 8, 如果以前授权成功过就会出现这个错误。   这时可以先不授权,点取消 9,                           10, 安装成功,用ArcCatalog测试一下吧 作者: 吉桂昕 出处: http://www.cnblogs.com/jiguixin 我的新浪微博: http://weibo.com/jiguixin 本文版权归【吉桂昕】和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。

  • Spring Aop中execution的语法

    参考地址:https://blog.csdn.net/zz210891470/article/details/54175107 execution(*com.sample.service.impl..*.*(..)) 解释如下:   符号 含义 execution() 表达式的主体; 第一个”*“符号 表示返回值的类型任意; com.sample.service.impl AOP所切的服务的包名,即,我们的业务部分 包名后面的”..“ 表示当前包及子包 第二个”*“ 表示类名,*即所有类。此处可以自定义,下文有举例 .*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型 AspectJ中的exection表达式小结:   基本语法格式为: execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)  除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。 下面,我们给出各种使用executi

  • hdu6078[优化递推过程] 2017多校4

    这道题一眼看过去好像和最长公共子序列有点像。 一开始只想到暴力的推法, 令dp[i][j][k]表示a[i]=b[j](即以ai,bj为结尾的波浪序列的方案数),且最终状态为k(0,1分别代表下降与上升)的方案数。    所以我们可能需要优化一下,用一个sum[i][j][k]表示枚举到ai时,能构成以bj为结尾且末状态为k的方案和,可以减少对j这一维的枚举。 比如我们在枚举ai+1时,在遍历b中元素时,如果遇到比ai+1大的,那么就加上sum[i][j][1],若遇到比ai+1小的,就加上sum[i][j][0],如果等于,就更新答案,把前面所有的可能全部加起来,并更新dp[i+1][j][k]。 /*hdu6078[优化递推过程]2017多校4*/ #include<bits/stdc++.h> usingnamespacestd; typedeflonglongLL; constLLMOD=998244353LL; intT,m,n,a[2005],b[2005]; LLsum[2005][3],dp[2005][3];//改为滚动数组,优化空间

  • 解决:idea不同端口号同时多次启动同一个项目——模拟集群

    -Dserver.port=复制   https://blog.csdn.net/suixinfeixiangfei/article/details/124401801复制  

  • windows消息机制与实例

    windows发送窗口消息   所需工具:spy++,visualstudio2017,c#语言    技术路线:首先通过spy++获得所要操纵的窗口的句柄,函数的原型声明为:   [DllImport("user32.dll")]      publicstaticexternIntPtrFindWindow(stringlpClassName,stringlpWindowName);   此函数获得目标窗口的句柄,如果要获得某个子窗口的句柄,通过以下函数可获得:   [DllImport("User32.dll")]     publicstaticexternIntPtrFindWindowEx(IntPtrparent,IntPtrchilde,stringstrclass,stringFrmText);   对目标窗口的操作(发送指令),使用的函数原型如下:    [DllImport("user32.dll",CharSet=CharSet.Au

  • 卡尔曼滤波器原理之基本思想(一)

    一、卡尔曼滤波器要解决的问题   首先说一下卡尔曼滤波器要解决的是哪一类问题,这类系统应该如何建模。这里说的是线性卡尔曼滤波器,顾名思意,那就是线性动态的离散系统。这类系统可以用如下两个方程来表示: \[\begin{array}{l} x(n+1)={\bf{F}}(n+1,n)x(n)+{v_1}(n)\\ y(n)={\bf{C}}(n)x(n)+{v_2}(n)\\ \end{array}\]   其中:     x(n)表示系统的状态   F(n+1,n)为状态转移矩阵,表示状态随时间的变化规律。通俗的讲,从当前状态到下一个状态之间有什么关系。   C(n)表示观测值与状态的关系   y(n)表示状态的观测值   v1表示系统过程的噪声   v2表示观测过程中产生的噪声   上面的两个方程中,第一个方程是过程方程,它表示系统状态x(n)随时间的更新过程。第二个方程为测量方程,表示状态x(n)与测量结果y(n)的关系。这里我们要先对这两个方程中的概念做下解释。   首先解释下状态这个概念。状态是对系统特征进行的一个抽象,由预测系统未来特性时所需要的

  • 什么是事务

    事务是逻辑上的一组操作,要么都执行,要么都不执行。 举例说明:场景:转账 假如王XX要给李XX转账10000元,这个转账涉及到两个关键操作就是:将王XX的余额减少10000元,将李XX的余额增加10000元。假如在这两个操作之间突然出现错误比如银行系统崩溃,导致王XX余额减少而李XX的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都失败。

  • 前端基础-css(2)

    一、标准文档流   宏观的将,我们的web页面和ps等设计软件有本质的区别,web网页的制作,是个“流”,从上而下,像“织毛衣”。而设计软件,想往哪里画东西,就去哪里画   标准文档流下有一些现象,比如空白折叠现象、高矮不齐底边对齐现象等   标准文档流等级森严,标签分为两种等级:     - 行内元素     - 块级元素。 1、块级元素和行内元素的区别   行内元素:     a、与其他行内元素并排;     b、不能设置宽、高,默认的宽度,就是文字的宽度;   块级元素:     a、独占一行,不能与其他任何元素并列;     b、能接受宽、高。如果不设置宽度,那么宽度将默认变为父级的100%; 2、块级元素和行内元素的分类   在以前的HTML知识中,我们已经将标签分过类,当时分为了:文本级、容器级。   a、从HTML的角度来讲,标签分为:     文本级标签:p、span、a、b、i、u、em;     容器级标签:div、h系列、li、dt、dd;     PS:为甚么说p是文本级标签呢?因为p里面只能放文字&图片&

  • GPU并行推理的一个疑问

    GPU是并行方面的行家,可是我发现当我利用libtorch,一个GPU同时运行多个模型进行推理时,两个模型的耗时都有大幅度的增加,这是很令人疑惑的问题。 按照CPU线程同步的思路,如果我开启多个线程进行工作,相比于单个线程的工作,其耗时是轻微增加或不增加的。 所以我的内心产生了一个疑惑,难道GPU不支持多个模型并行推理,只支持串行推理?又或者libtorch这个库本身不支持并行推理?   随后,我上网查找了一番: 能不能单GPU并行推理多个模型·Issue#28733·PaddlePaddle/Paddle·GitHub 在同一GPU上使用多个并行进程在TysFraseC++中较慢的推理时间_三行代码(sov5.cn) 果然有人也遇到了类似的问题,结果显示,GPU推理多个模型,其耗时确实大大增加,但仍然不能说明GPU不支持并行推理。   GPU必然可以支持并行,那么可能是cuda的问题。那么能否做一些实验来证明,cuda确实不支持并行推理呢?  我是如此设置实验的,该实验的实验组是一个阻塞形式的(串行)运行两个相同模型的程序,对照组则是开两个线程并行运行两

相关推荐

推荐阅读