C#之依赖注入DI(DependencyInjection)

依赖注入实际上是一种设计模式,它可以有效降低模块之间的耦合度。

基本思路:

  • 创建ServiceCollection对象

  • 用ServiceCollection对象进行注册服务

  • 用ServiceCollection创建ServiceProvider对象,通过ServiceProvider的GetService方法获取服务

而服务分为transient,scoped,singleton三种,其中transient是每次获取都是新的对象,scoped 是只有在范围以内的才是同一个对象,而singleton永远取到的是同一个对象,下面分别进行演示。

transient服务

using System;
using Microsoft.Extensions.DependencyInjection;
namespace DITest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            ServiceCollection services = new ServiceCollection();
            services.AddTransient<TestService>();
            using(var sp= services.BuildServiceProvider())
            {
                TestService t = sp.GetService<TestService>();
                t.Name = "JohnYang";
                t.SayHi();
                TestService t1 = sp.GetService<TestService>();
                Console.WriteLine(Object.ReferenceEquals(t, t1));
            }
            
        }
    }
    public class TestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine(Name);
        }
    }
}

output:

JohnYang
False

这确实也验证了transient服务,每次获取都是新的对象。

singleton服务

using System;
using Microsoft.Extensions.DependencyInjection;
namespace DITest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            ServiceCollection services = new ServiceCollection();
            //services.AddTransient<TestService>();
            services.AddSingleton<TestService>();
            //services.AddScoped<TestService>();
            using (var sp= services.BuildServiceProvider())
            {
                TestService t = sp.GetService<TestService>();
                t.Name = "JohnYang";
                t.SayHi();
                TestService t1 = sp.GetService<TestService>();
                Console.WriteLine(Object.ReferenceEquals(t, t1));
            }
            
        }
    }
    public class TestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine(Name);
        }
    }
}

output:

JohnYang
True

Scoped

using System;
using Microsoft.Extensions.DependencyInjection;
namespace DITest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            ServiceCollection services = new ServiceCollection();
            //services.AddTransient<TestService>();
            //services.AddSingleton<TestService>();
            services.AddScoped<TestService>();
            using (var sp= services.BuildServiceProvider())
            {
                TestService t, t1, t2;
                //指定范围
                using(IServiceScope scope = sp.CreateScope())
                {
                    //在scope中获取Scope相关的对象,需要用scope.ServiceProvider而不是sp!!
                    t = scope.ServiceProvider.GetService<TestService>();
                    t.Name = "JohnYang";
                    t.SayHi();
                    t1 = scope.ServiceProvider.GetService<TestService>();
                    Console.WriteLine(Object.ReferenceEquals(t, t1));
                }
                using (IServiceScope scope2 = sp.CreateScope())
                {
                    //在scope中获取Scope相关的对象,需要用scope.ServiceProvider而不是sp!!
                    t2 = scope2.ServiceProvider.GetService<TestService>();
                    Console.WriteLine(Object.ReferenceEquals(t2, t));
                }

            }
            
        }
    }
    public class TestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine(Name);
        }
    }
}

output:

JohnYang
True
False

结果也验证了,在同一个范围是同一个服务,但不同范围,获取的不是同一个服务的结论。

需要注意的事项:

  • 不要再长声明周期的对象中引用比它短的生命周期的对象,因为短的生命周期的对象被销毁的时候,长声明周期的对象对它的引用将受影响。

  • 声明周期的选择:如果类无状态(无属性和成员变量),建议为singleton;如果类有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下的代码都是运行在同一个线程中的,没有并发修改的问题;在使用Transient的时候要谨慎。

服务定位器

接口的形式:

using System;
using Microsoft.Extensions.DependencyInjection;
namespace DITest
{
    internal class Program
    {
        static void Main()
        {
            ServiceCollection services = new ServiceCollection();
           services.AddScoped<ITestService,TestService>();//第一个是服务的接口,第二个是实现服务的对象
            using(var sp = services.BuildServiceProvider())
            {
                ITestService testService = sp.GetService<ITestService>();
                testService.Name = "JohnYang";
                testService.SayHi();
                Console.WriteLine(testService.GetType());
            }
        }
        
    }
   public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi();
    }
    public class TestService:ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine(Name);
        }
    }
}

output:

JohnYang
DITest.TestService

GetService<T>中的T必须与AddXXX<T,T1>中的T是一致的,否则,取不到,返回null,以上面例子来讲,如果GetService<TestService>就报错,因为注册的是ITestServie,而不是TestSerive

T GetRequiredService<T>()如果获取不到对象,则抛异常。

IEnumerable<T> GetServices<T>()适用于可能有很多满足条件的服务。

using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
namespace DITest
{
    internal class Program
    {
        static void Main()
        {
            ServiceCollection services = new ServiceCollection();
           services.AddScoped<ITestService,TestService>();//第一个是服务的接口,第二个是实现服务的对象
            using(var sp = services.BuildServiceProvider())
            {
                IEnumerable<ITestService> testServices = sp.GetServices<ITestService>();
                foreach(var t in testServices)
                {
                    Console.WriteLine(t.GetType());
                }
            }
        }
        
    }
   public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi();
    }
    public class TestService:ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine(Name);
        }
    }
}

output:

DITest.TestService

当注册了多个服务的时候,GetServices返回的是所有的实现的对象,而GetServie返回的是最后一个注册的服务。

IEnumerable<object> GetServices(Type serviceType)

依赖注入的“传染性”

依赖注入是有“传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值,但是如果一个对象是程序员手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的类型参数就不会被自动赋值。.NET的DI默认是构造函数注入。

这也是依赖注入非常强大的地方,通过DI创建的对象,该对象构造函数中的参数也会自动的被创建。

Demo如下:

using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
namespace DITest
{
    internal class Program
    {
        static void Main()
        {
           ServiceCollection services = new ServiceCollection();
            //注册各种服务
            services.AddScoped<Controller>();
            services.AddScoped<ILog, LogImpl>();
            services.AddScoped<IStorage, StorageImpl>();
            services.AddScoped<IConfig, ConfigImpl>();

            using(var sp = services.BuildServiceProvider())
            {
                Controller controller = sp.GetRequiredService<Controller>();
                controller.Test();
            }
            Console.ReadKey();
        }
        
    }
    class Controller
    {
        private readonly ILog log;
        private readonly IStorage storage;
        public Controller(ILog log, IStorage storage)//构造函数注入
        {
            this.log = log;
            this.storage = storage;
        }

        public void Test()
        {
            log.Log("开始上传");
            storage.Save("asdkks", "1.txt");
            log.Log("上传完毕");
        }
    }

    /// <summary>
    /// 日志服务
    /// </summary>
    interface ILog
    {
        public void Log(string msg);
    }
    /// <summary>
    /// 日志实现类
    /// </summary>
    class LogImpl : ILog
    {
        public void Log(string msg)
        {
            Console.WriteLine("日志:"+msg);
        }
    }
    /// <summary>
    /// 配置服务
    /// </summary>
    interface IConfig
    {
        public string GetValue(string name);
    }
    /// <summary>
    /// 配置实现类
    /// </summary>
    class ConfigImpl : IConfig
    {
        public string GetValue(string name)
        {
            return "hello";
        }
    }

    interface IStorage
    {
        public void Save(string content, string name);
    }

    class StorageImpl : IStorage
    {
        private readonly IConfig _config;
        public StorageImpl(IConfig config)//构造函数注入,当DI创建StorageImpl时候,框架自动创建IConfig服务
        {
            _config = config;
        }

        public void Save(string content, string name)
        {
            string server=_config.GetValue("server");
            Console.WriteLine($"向服务器{server}的文件名{name}上传{content}");
        }
    }
}

output:

日志:开始上传
向服务器hello的文件名1.txt上传asdkks
日志:上传完毕

如果后续,更改配置,则业务代码不用动,只需要

class DbConfigImpl : IConfig
    {
        public string GetValue(string name)
        {
            return "hello db";
        }
    }

然后,把之前IConfig的服务更改为DbConfigImpl,就可以了。


services.AddScoped<IConfig, DbConfigImpl>

因此,降低了模块之间的耦合度。

ServiceCollection的简单使用

在C#中,ServiceCollection是一个集合,用于注册和管理应用程序的依赖项。它是ASP.NET Core应用程序中的核心服务容器,负责管理服务的生命周期和依赖注入。下面详细介绍一下ServiceCollection的用法:

注册服务:
使用ServiceCollection可以通过AddTransient、AddScoped和AddSingleton方法向容器中注册服务。这些方法分别表示短暂、作用域和单例服务的生命周期。例如:

services.AddTransient<IMyService, MyService>();
services.AddScoped<IMyService, MyService>();
services.AddSingleton<IMyService, MyService>();

注册配置:
可以使用Configure方法向容器中注册配置。例如:

services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

这将在容器中注册一个名为"MyOptions"的配置实例,并将其绑定到appsettings.json文件中的对应节点。

注册中间件:
中间件是ASP.NET Core应用程序中的重要概念,它允许将请求和响应传递给下一个中间件,以便进行处理。可以使用UseMiddleware方法向容器中注册中间件。例如:

app.UseMiddleware<MyMiddleware>();

这将注册一个名为"MyMiddleware"的中间件,它将在请求管道中处理请求。

注册过滤器:
过滤器是ASP.NET Core应用程序中用于处理请求和响应的组件。可以使用AddMvc或AddControllers方法向容器中注册过滤器。例如:

services.AddMvc(options => {
    options.Filters.Add<MyFilter>();
});

这将向MVC中注册一个名为"MyFilter"的过滤器,它将在请求管道中处理请求。

注册日志:
可以使用AddLogging方法向容器中注册日志服务。例如:

services.AddLogging();

这将注册日志服务,并向容器中添加ILoggerFactory和ILogger实现。

自定义注册:
除了上述方法外,还可以通过使用Add方法手动注册服务。例如:

services.Add(new ServiceDescriptor(typeof(IMyService), new MyService()));

这将手动注册一个名为"MyService"的服务,并将其实现类型指定为"MyService"。

总之,ServiceCollection是ASP.NET Core应用程序中非常重要的一部分,它允许开发人员管理应用程序中的依赖项和服务,并为应用程序提供扩展性和可维护性。
在ASP.NET Core应用程序中,services.AddOptions()方法用于将选项配置注册到应用程序的服务容器中。选项配置是一种将应用程序配置和代码分离的方法,它允许开发人员在运行时更改应用程序的行为而不需要更改代码。选项配置可以从不同的源(如appsettings.json文件、环境变量或命令行参数)中读取,以便在不同的环境中配置应用程序。

services.AddOptions()方法是ASP.NET Core提供的一种便捷方法,用于将选项配置添加到服务容器中。它会将IOptions、IOptionsSnapshot和IOptionsMonitor服务注册到容器中,以便应用程序的其他组件可以使用这些服务来访问选项配置。例如:

services.AddOptions<MyOptions>()
    .Bind(Configuration.GetSection("MyOptions"))
    .ValidateDataAnnotations();

在上面的示例中,services.AddOptions()方法将注册一个名为MyOptions的选项配置服务,并将其绑定到appsettings.json文件中的MyOptions节点。.ValidateDataAnnotations()方法用于启用数据注释验证,以确保选项配置符合指定的数据注释规则。通过这种方式,应用程序的其他组件可以使用IOptions服务来访问选项配置,并执行必要的验证和转换。

services.Configure(e=>config.GetSection("DB").Bind(e)),解释下这个代码,以及Bind的意思:
这段代码的作用是将应用程序配置文件(通常是appsettings.json)中的名为"DB"的配置节绑定到一个名为"DbSetting"的对象上,并将其注册为依赖项注入的服务。

具体地说,services.Configure(e => config.GetSection("DB").Bind(e))方法接受一个泛型类型参数DbSetting,表示要配置的选项类型。它还接受一个Lambda表达式,用于在运行时绑定选项值。

Lambda表达式中的e => config.GetSection("DB").Bind(e)代码表示将DbSetting对象的属性绑定到名为"DB"的配置节中的相应键值对。具体来说,config.GetSection("DB")方法表示从应用程序配置文件中获取名为"DB"的节,然后调用Bind(e)方法将其绑定到DbSetting对象的属性上。这样,当应用程序需要使用DbSetting对象时,它会从服务容器中获取该对象,并根据应用程序配置文件中的值进行配置。

Bind方法的作用是将配置节中的键值对绑定到指定对象的属性上。它接受一个参数,表示要绑定的对象。在上面的示例中,Bind(e)表示将配置值绑定到Lambda表达式中的参数e表示的DbSetting对象的属性上。

serviceProvider.CreateScope什么意思?
在ASP.NET Core应用程序中,serviceProvider.CreateScope()方法用于创建一个新的作用域,该作用域使用应用程序的服务容器作为其父级。作用域是一种轻量级的上下文,在作用域内创建的对象通常只在该作用域中有效,并且可以在作用域结束时自动释放。

具体来说,serviceProvider.CreateScope()方法返回一个IServiceScope对象,该对象表示新创建的作用域。可以使用该对象的ServiceProvider属性访问该作用域的服务容器,并在该作用域内使用该容器创建新的服务实例。

例如,以下代码创建了一个作用域,并在该作用域中获取MyService服务的实例:

using(var scope = serviceProvider.CreateScope())
{
    var scopedServices = scope.ServiceProvider;
    var myService = scopedServices.GetRequiredService<MyService>();
    //使用myService进行操作...
}

在上面的代码中,CreateScope()方法创建了一个新的作用域,并在using块中使用该作用域。然后,scopedServices.GetRequiredService()方法从该作用域的服务容器中获取MyService服务的实例。在using块结束时,作用域将被释放,任何在该作用域内创建的服务实例也将被释放。

C# DI中,如果类A依赖类B,在注册服务的时候,先写的IserviceCollection.AddXXX(A),后写的IserviceCollection.AddXXX(B),有影响没?

在C#的依赖注入(Dependency Injection)中,通常情况下,注册服务的顺序并不会对依赖关系造成直接的影响。

当使用依赖注入容器(如.NET Core中的IServiceCollection)注册服务时,服务的注册顺序通常只会影响到解析依赖关系的顺序。也就是说,如果类A依赖于类B,而你先注册A,后注册B,那么在解析A时,容器会自动解析B并将其注入到A的构造函数或属性中。

以下是一个示例说明注册服务顺序的影响:

services.AddSingleton<A>();
services.AddSingleton<B>();

上述代码中,先注册了A,后注册了B。当容器需要解析A时,会检查A的构造函数或属性的依赖关系,并自动解析B,并将其注入到A中。

然而,如果类A和类B之间存在循环依赖,即A依赖B,同时B也依赖A,那么注册服务的顺序将会变得重要。在这种情况下,你需要使用更高级的依赖注入模式,如构造函数注入、属性注入或方法注入,以避免循环依赖导致的问题。

综上所述,一般情况下,注册服务的顺序并不会对依赖注入造成直接影响。但在处理循环依赖时,注册服务的顺序可能需要特别注意。

posted @ 2022-09-03 23:15  JohnYang819  阅读(2156)  评论(0编辑  收藏  举报