C# (.NET Core)的DI依赖注入

依赖注入

B站杨中科老师的.NET(6.0)依赖注入教学

引言

举一个例子,现在都在流行一个叫做自立更生的词汇,一定规模型的企业,都会可以在研究自己的专利技术,降低自己过度依赖于其他企业.从而从这个市场中的依赖关系中给简单化.

概念

依赖注入(Dependency Injection, DI)是控制反转(Inversion of Control, IOC)思想的实现方式.

优点

依赖注入简化模块的组装过程,降低模块之间的耦合度

初识

创建方式由我们手动创建到作为一个消费者工厂去取得或者编译器自动创建的方式.

一个比较醒目的例子就是,在一个类中如果引入了其它的自定义类型包括但不限于(int,double,char)等常见对象.在创建该对象时,会依赖注入地为其属性创建对象.

class Demo
{
	public IDbConnection Conn{get;set;};
    public void InsertDB()
    {
    	IDbCommand cmd=Conn.CreateCommand();    
    }
}

在创建一个Demo的对象时,内部属性IDbConnection将会被自动创建,这种动作被称为依赖依赖注入.

还有一种模式,它更像是一个工厂,通过共同祖先的方式**(作为返回值)和泛型类型(我们想要的)**的方式,来使得像这个工厂去索取我们指定的泛型对象.

IDbConnection conn=ServiceLocator.GetService<IDbConnection>();

可以看出ServiceLocator就像是一个工厂.

dotNET中的DI服务注册

步骤和材料:

  1. 注册服务;
  2. 服务容器:负责管理注册的服务;(材料)
  3. 查询服务:创建对象及关联对象;

对象的属性

Transient(瞬态):每次获取都是新的对象.

Scoped(范围):在一定范围内获取的都是同一个对象.

Singleton(单例):单一对象.

dotNET中使用DI

分为两种类型:

  1. 服务类型(service type);
  2. 实现类型(implementation type);

两者可能相同,也可能不同.服务类型可以是类,也可以是接口.

⭐️建议面向接口编程,更灵活.

dotNET控制反转组件取名为DependencyInjection,但它包含ServiceLacator的功能.

安装库

Install-Package Microsoft.Extensions.DependencyInjection
using Microsoft.Extensions.DependencyInjection;

ServiceColletion创建类容器对象.

基本逻辑如下:

class ServiceColletion
{
    IServiceCollection AddTransient<in T>();//添加注册对象(瞬时的)到this中.
    IServiceCollection AddSingleton<in T>();//注册单一对象.
    IServiceCollection AddScoped<in T>();//注册环境内的对象.
        
        
    ServiceProvider BuildServiceProvider();//获取工厂管理者.
}

class ServiceProvider<T>
{
    T GetService();//工厂,返回出索要的T类型.
}
using System;
using Microsoft.Extensions.DependencyInjection;

namespace 依赖注入
{
    class Afjafjlsa
    {
        public int MyProperty { get; set; }
        public string fs { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ServiceCollection serviceDescriptors = new ServiceCollection();
            serviceDescriptors.AddTransient<Afjafjlsa>();//注册一个瞬时对象.

            using ServiceProvider providerService = serviceDescriptors.BuildServiceProvider();//获取工厂管理者.

            var obj = providerService.GetService<Afjafjlsa>();//获取指定对象.

            obj.fs = "hfsajf";
            obj.MyProperty = 102;

        }
    }
}

总结

安装一些Nuget包之后,引入命名空间,首先是实例化一个工厂容器ServiceCollection,之后根据需求向容器中注册对象,瞬时的:AddTransient<in T>,获取工厂管理者BuildServiceProvider(),由它来为我们服务取得所要的已注册的对象实例GetService<in T>().

依赖注入—服务对象的生命周期

serviceProvider.CreateScope();//创建scope(即不同的环境对象)

若一个类实现了IDisposable接口,则离开作用域之后容器会自动调用对象的Dispose();

注意:不要再长生命周期的对象中引用比它短的生命周期的对象,在dotNET core中,这样做默认会抛异常.

对象的生命周期都是基于工厂管理者的.

生命周期的选择

若类无状态,建议为Singleton;

若类有状态,且有Scope控制,见一位Scoped,因为通常这种Scope控制下的代码都是运行在同一个线程中的,没有并发修改的问题;

在选用Transient(瞬态)时,需谨慎.

IServiceCollection AddTransient<in T>();//添加注册对象(瞬时的)到this中.
    IServiceCollection AddSingleton<in T>();//注册单一对象.
    IServiceCollection AddScoped<in T>();//注册环境内的对象.

首先,我们还是需要创建一个工厂,并且向其中注册不同属性的对象及获得工厂管理员.

 static void Main(string[] args)
        {
            ServiceCollection serviceDescriptors = new ServiceCollection();
           

            using ServiceProvider providerService = serviceDescriptors.BuildServiceProvider();//获取工厂管理者.
			...
   			

        }

随后,我们将要验证几件事情,分别是transient(瞬时的),Scope(范围的),singleton(单一的)

注册一个transient(瞬时的),通过object.ReferenceEquals()来验证这两个对象是否相同,若不相同,则证明猜想是对的.

 static void Main(string[] args)
 {
     ...;
     serviceDescriptors.AddTransient<Afjafjlsa>();//注册一个瞬时对象.(即量产)的对象.
     var obj = providerService.GetService<Afjafjlsa>();
	 var obj1 = providerService.GetService<Afjafjlsa>();
     
	Console.WriteLine(object.ReferenceEquals(obj,obj1));

 }

可得这两个对象是不同的.

注册一个Singleton(单一的),后续操作相同.

 static void Main(string[] args)
 {
     var obj = providerService.GetService<Afjafjlsa>();

     var obj1 = providerService.GetService<Afjafjlsa>();
     Console.WriteLine(object.ReferenceEquals(obj,obj1));
 }

这两个对象是相同的.

注册一个Scope(范围的)对象,但是这个需要使用serviceProvider.CreateScope();来表明不同的环境,才能够保证环境中的对象都是"独立的".

需要想明白的是,内部是如何知道不同的scope(环境)呢,由于我们创建了一个新的scope(环境),需要在一个相同的scope(环境)下,去获取对象.

var scope1 = providerService.CreateScope();
var newScopeService=scope1.ServiceProvider;

而这个newScopeService也默认就是一个工厂管理者,但是相比之下,优先级就低的多了.

//环境1
            using (var scope1 = providerService.CreateScope())
            {
                var sObj = scope1.ServiceProvider.GetService<Afjafjlsa>();
                var sObj1= scope1.ServiceProvider.GetService<Afjafjlsa>();
                Console.WriteLine(object.ReferenceEquals(sObj, sObj1));
            }

sObj===sObj1 ,因为它们是以scope的方式注册,且又在相同的scope去获取.

它们以scope区分,使用相同的scope1.ServiceProvider去获取,都会拿到一个相同的对象.

总结

关于工厂管理者的认识,它的创建方式为:serviceDescriptors.BuildServiceProvider()所获取的管理者对象更像是一个雇主,拥有至高无上的权利,它可以再次创建不同的管理(即Scope).

对象的生命周期都是基于这个雇主而言的,当scope(环境)介入时,产生了范围内的概念.

服务定位器(工厂管理者)

C#中有接口和具体实现类这一说.通常,我们换种名字来称它,接口叫做服务类,具体的实现了接口的类叫做实现类.

其次把若干类中共有的属性,方法,汇总到服务类中.以便后续的管理.

而不同的实现类都可以由一个服务类支撑.

static void Main(string[] args)
{
    ServiceCollection collection = new ServiceCollection();
    collection.AddScoped<IInterface, Afjafjlsa>();
    collection.AddSingleton<IInterface, Afjafjlsa>();
    collection.AddTransient<IInterface, Afjafjlsa>();
}

提供了两个泛型类型参数,第一个为服务类,第二个为具体的实现类.

注意:GetService()更具有一定的针对性,如果在使用Add注册时,提供了服务类,那么在获取的时候也需要用这个服务类去接,否则的话,将会返回NULL.

GetRequiredService<>()方法是必须获取到一个指定服务类的对象,否则抛异常.

我们之前一直有个疑惑,就是服务类的实现是可以由多个,那该如何获取呢?

static void Main(string[] args)
{
    ServiceCollection collection = new ServiceCollection();

    collection.AddTransient<IInterface, SFJl>();
    collection.AddTransient<IInterface, Afjafjlsa>();


}

通过以下接口

IEnumberable<T> GetServices<T>();

static void Main(string[] args)
        {

            using (var factorManag = collection.BuildServiceProvider())
            {
                var services = factorManag.GetServices<IInterface>();
                foreach (var item in services)
                {
                    Console.WriteLine(item.GetType());
                    //通过这种形式,获取到我们想要的具体实现类.
                    if(item is SFJl)
                    {
                        SFJl s = (SFJl)item;
                        s.sa = 10;
                        s.Say();
                    }
                }
            }
        }

若通过GetService<T>()获取注册的多个服务,则之获取注册的最后一个.

总结

对于一个大型的项目而言,服务类所对应的具体实现类通常是很多的,这时,我们可以通过GetServices() 来进行获取,并通过类型判断来得到具体的实现.

在我们非常笃定的认为该类已经实现时,为了避免多余的if判断,可以直接使用GetRequiredService<>()来进行获取.

在获取时,一定要注意GetService()获取的是注册时所给的服务类,如果方法没有找到合适的,则返回NULL.

表格如下:

函数名称参数描述
GetService();GetService<TService>()获取给定的TService,若没有,返回null;若有多个注册,默认获取最后一个.
GetRequiredService();GetRequiredService<TService>()获取给定的TService,若没有,抛异常.
GetServices();GetServices<TService>()获取给定的TService的多个注册.

.net依赖注入

依赖注入具有"传染性".即依赖关系就像是图的数据结构一样.

.net的DI默认是构造函数注入

注入模式

  1. 构造函数注入

  2. 属性注入

示例

class Controller
    {

        private readonly ILog log;

        private readonly ICloudStorage cloudStorage;

        public Controller(ILog log, ICloudStorage cloud)
        {
            this.log = log;
            this.cloudStorage = cloud;
        }

        public void Test()
        {
            //指定配置文件.


            log.Log1("开始上传");
            cloudStorage.Save("1.txt", "萨菲拉斯基法拉家纺");
        }
    }

    /// <summary>
    /// 模拟云存储的服务类.(接口)
    /// </summary>
    interface ICloudStorage
    {
        public void Save(string name, string content);
    }

    /// <summary>
    /// 云存储的实现类
    /// </summary>
    class CloudStorage : ICloudStorage
    {
        private readonly IConfig config;

        public CloudStorage(IConfig config)
        {
            this.config = config;
        }

        public void Save( string name,string content)
        {
            string ser = config.GetValue("server");
            Console.WriteLine($"向{ser}服务器中,文件名:{name}写入了{content}");
        }
    }

    interface IConfig
    {
        public string GetValue(string name);
    }

    class Config : IConfig
    {
        //这里可以是一些具体的配置文件读取.并形成一个特定的键值组合.
        public string GetValue(string name)
        {
            return "hello";
        }
    }


    interface ILog
    {
        public void Log1(string msg);
    }

    class Log : ILog
    {
        public readonly IConfig config;

        public Log(IConfig config)
        {
            this.config = config; 
        }

        public void Log1(string msg)
        {
            string fileName=config.GetValue("log");
            Console.WriteLine("内容:"+msg+";日志文件名:"+fileName);
        }
    }

它们的依赖关系是:

controller依赖了cloudstorage和log .而它俩又共同依赖了config.即依赖关系图是菱形的.

小总结

依赖关系更像是一种设计思想,也在一定程度上降低了它们的耦合度.如果没有所谓的构造函数注入模式,那在我们进行日志文件写入之前,就需要对一个特定的config(配置)进行对象创建.后续如果对配置进行重构,这种代码就显得很麻烦.

在设计时,我们可以先预先想好它们之间的依赖关系.然后根据它们之间的依赖层次,进行构造注入,在某些必要的场合,可以对它们进行预先处理或封装.

DI综合案例

这个是模拟了一个邮箱,然后有一个日志功能可以根据传递的参数来决定日志是按照控制台输出还是以文件的形式输出.

并构造了一个后缀为.ini的配置文件,来供邮箱在进行发送时,通过配置接口来获取详细信息.

简要:配置,日志,邮箱 (服务)

配置服务

服务类
public interface IConfigService
    {
        /// <summary>
        /// 从环境变量中读取配置.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string GetValue(string key);
    }
实现类
public class IniFileConfig: IConfigService
    {
        public string FilePath { get; set; }

        public IniFileConfig(string path)
        {
            FilePath = path;
        }

        /// <summary>
        /// 通过ini配置文件获取指定key的配置属性.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string GetValue(string key)
        {
            var saf=File.ReadAllLines(FilePath).Select(x => x.Split('=')).Select(x => new { key = x[0], val = x[1] }).Single(x=>x.key==key);

            return saf.val;

        }

    }

日志服务

服务类
public interface ILogService
    {
        public bool isFile { get; set; }

        /// <summary>
        /// 一般日志.
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="content"></param>
        ///
        public void LogInfo(string fileName, string content) => Console.WriteLine($"{fileName}---{content}");

        public Task LogInfoAsync(string content) => Task.CompletedTask;

        /// <summary>
        /// 错误等级日志.
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="content"></param>
        public Task LogErrorAsync(string content) => Task.CompletedTask;

        public void LogError(string fileName, string content) => Console.WriteLine($"{fileName}---{content}");

        /// <summary>
        /// 警告等级日志.
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="content"></param>
        public void LogWarn(string fileName, string content)=> Console.WriteLine($"{fileName}---{content}");

        public Task LogWarnAsync( string content) => Task.CompletedTask;

        public string GetInfoFileName() => "";
        public string GetWarnFileName() => "";
        public string GetErrFileName() => "";
    }
实现类
public class FileLogService : ILogService
    {
        private string errLogFileName;
        private string infoLogFileName;
        private string warnLogFileName;

        public bool isFile { get; set; }

        public FileLogService(string errFileName,string infoFileName,string warnFileName)
        {
            errLogFileName = errFileName;
            infoLogFileName = infoFileName;
            warnLogFileName = warnFileName;
            isFile = true;
        }

        public string GetInfoFileName() => infoLogFileName;
        public string GetWarnFileName() => warnLogFileName;
        public string GetErrFileName() => errLogFileName;

        public async Task LogErrorAsync( string content)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine($"[ERROR]---[{DateTime.Now.ToString()}]---{content}");
            
            await File.WriteAllTextAsync(errLogFileName, sb.ToString());
        }

        public async Task LogWarnAsync( string content)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine($"[WARNING]---[{DateTime.Now.ToString()}]---{content}");

            await File.WriteAllTextAsync(warnLogFileName, sb.ToString());
        }

        public async Task LogInfoAsync( string content)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine($"[INFO]---[{DateTime.Now.ToString()}]---{content}");

            await File.WriteAllTextAsync(infoLogFileName, sb.ToString());
        }

    }

邮箱服务

服务类
public interface IMailService
    {
        public  Task SendAsync(string tilte, string to, string msg);
    }
实现类
public class MailService: IMailService
    {
        public readonly ILogService   log;
        public readonly IConfigService config;

		//构造函数注入
        public MailService(ILogService log, IConfigService config)
        {
            this.log = log;
            this.config = config;
        }

        public async Task SendAsync(string tilte, string to, string msg)
        {
            if (log.isFile)
            {
                string emailAddr = config.GetValue("mail");
                string user = config.GetValue("user");
                string pwd = config.GetValue("pwd");
                Console.WriteLine($"邮箱地址:{emailAddr}\t发件人:{user}\t发件人密码:{pwd}");

                Console.WriteLine($"标题为:{tilte},给{to}说:{msg}");
                await log.LogInfoAsync( "开始发送邮件");
            }

            
        }
    }

概述

其实如上所述的代码实现并不是唯一的,因为配置文件在开发过程中是可以指定的.并且,日志文件也可以由框架开发人员指定,那基于此,日志服务里面就要涉及一个构造函数注入,注入的是配置服务.伪代码如下:

interface IConfigService
{
	public string configName{get;set;}
	...
}

class ConfigService:IConfigService
{
    public string configName{get;set;}
    
	public ConfigService(string configName)=>this.configName=configName;
    
    /// <summary>
    /// 通过ini配置文件获取指定key的配置属性.
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    public string GetValue(string key)
    {
        var saf=File.ReadAllLines(configName).Select(x => x.Split('=')).Select(x => new { key = x[0], val = x[1] }).Single(x=>x.key==key);

        return saf.val;

    }
}


public interface ILogService
{
    //写日志文件的方法.
}

public class FileLogService : ILogService
{
  	private readonly IConfigService configSercice;
    
    public FileLogService(IConfigService configSercice)
    {
        this.configSercice=configSercice;
    }
    
    //读取配置信息时,即可通过configSercice进行.
}

当然,这里就可以根据不同的业务场景去进行分析了.

DI综合案例2

实现一个"可覆盖的配置服务"

背景:在一个基于分布式(集群)的web应用程序中,为了便于配置的管理,会将配置放在一个web应用程序的中点处(配置服务器),当该服务器里的配置文件发生改变时,集群里的其他成员也随之改变.

但也会存在一个棘手的问题,当我们想针对某个web应用程序的配置进行改变时,就需要一些临时的配置文件介入其中了.

可覆盖,其实就是相同的变量,根据赋值的时机不同,在使用该变量时,都会获取到最后赋值给给变量的值.

我们这个时候油然而生的一种朴素想法就是,能够获取到注册配置的容器,然后通过一个指针去指向最后一个,来获取最新的配置服务.(注意:在容器层面,我们就不得不要求注册的时序问题了).

创建一种这样的服务类,在注册操作完成时,会自然而然获取一个新的配置.

服务类:IConfigPositionService

/// <summary>
/// 获取最后一次的配置服务.
/// </summary>
public interface IConfigPointService
{
    public IConfigService curServer { get;set;  }

    public void GetNewServer();
}

实现类:ConfigPositionService

public class ConfigPointService : IConfigPointService
    {
        //当前服务.
        //public IConfigService curServer;
        public IConfigService curServer { get; set; }

        public readonly IEnumerable<IConfigService> services;

        /// <summary>
        /// 将当前服务通过构造函数注入的方式,获取一个默认的当前服务.然后,根据注册的配置服务集合,获取最后一个服务.
        /// </summary>
        /// <param name="ser"></param>
        /// <param name="cur"></param>
        public ConfigPointService(IEnumerable<IConfigService> ser/*, IConfigService cur*/)
        {
            services = ser;
            //curServer = cur;
        }

        public void GetNewServer()
        {
            foreach (var item in services)
            {
                if (item != null)
                    curServer = item;//获取到最新的服务.
            }
        }

      
    }

同时,实现了一个扩展类,在进行服务注册时,一般都是通过调用IServiceCollection.AddXXX()来进行调用的,当对一个框架来说,难免就需要了解不少的服务类与实现类.使得效率降低.

/// <summary>
/// 给定一个配置文件名,进行配置文件的可操作性.
/// </summary>
/// <param name="services"></param>
/// <param name="configName"></param>
public static void AddConfigure(this IServiceCollection services, string configName)
{
    //注册次序:ini配置,环境变量配置.

	//调换如下代码顺序将会是不同的结果.
    services.AddScoped(typeof(IConfigService), x => new ConfigService());//注册以环境变量为配置.
    services.AddScoped(typeof(IConfigService), x => new IniFileConfig(configName));//注册以ini文件为配置.


    //注册可覆盖的配置服务器之前,需要获取之前注册过的配置服务的序列.
    using var sc=services.BuildServiceProvider();
    var servicesList = sc.GetServices<IConfigService>();
    //注册可覆盖的配置服务器.
    services.AddScoped(typeof(IConfigPointService), x => new ConfigPointService(servicesList));
}

ini配置文件如下:

mail=fljasflasjf@.com
user=fjaslf
pwd=1231983u1

代码的实现是DI综合案例的一部分.

public class MailService: IMailService
{
    public readonly IConfigPointService configPoint;


    public MailService(ILogService log, IConfigPointService configPoint)
    {
        this.configPoint = configPoint;
    }
    //示例
   	public Test()
    {
        configPoint.GetNewServer();//获取新的服务.
    }
}

总结

DI三部曲:

  1. 注册
  2. 获取工厂管理者.
  3. 根据需要获取对应的服务类.

在编写这些服务时,我们可以发现,更需要关注的是抽象类的编写(在这里特指接口).

在采取在那里需要进行构造函数注入时,可以这样理解,该函数是否需要依赖这个服务,即调用这个服务提供的方法接口等.

在进行注册时,尽量不要注册实体.

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值