依赖注入

一、依赖注入与控制反转是什么

控制反转(inversion of control,IoC)是设计模式中非常重要的思想,而依赖注入(dependency injection,DI)是控制反转思想的一种重要的实现方式

IOC:控制反转,是一个理论,概念,思想。
描述的:把对象的创建,赋值,管理工作都交给代码之外的容器实现,也就是对象的创建是有其它外部资源完成

控制:创建对象,对象的属性赋值,对象之间的关系管理。

反转:把原来的开发人员管理,创建对象的权限转移给代码之外的容器实现。由容器代替开发人员管理对象。创建对象,给属性赋值。

在传统的软件开发中,代码依赖的对象都是由调用者自己编写代码进行创建和组装的.

如下代码:

1
2
3
var connStr = ConfigurationManager.ConnectionStrings["connStr1"] ConnectionString;
SqlConnection conn = new SqlConnection(connStr);

以上的代码是数据库的链接代码,通常我们都会写到一个配置文件中。

这里我们自己获取配置文件中的链接字符串,然后自己创建链接sql server数据库的链接对象SqlConnection

这个过程,需要我们程序员自己非常清楚每个类的作用,以及如何构建和组装他们。

例如:开发人员需要知道“使用ConfigurationManager类读取连接字符串”这个知识点,并且需要知道要把连接字符串作为参数来调用SqlConnection的构造方法,以创建数据库连接对象。开发人员必须了解这些细节。这增加了开发人员的负担,使开发人员无法专注于业务逻辑代码的开发。同时,这些代码也与ConfigurationManager、SqlConnection等类强耦合。如果需要把从配置文件读取改为从环境变量读取或者改为连接MySQL数据库,就要对代码进行修改。

控制反转的目的就是把“创建和组装对象”操作的控制权从业务逻辑的代码中转移到框架中,这样业务代码中只要说明“我需要某个类型的对象”,框架就会帮助我们创建这个对象

关于控制反转的实现主要有如下两种方式。

第一种:服务定位:假设框架中有一个类叫做ServiceLocator,只要调用它的GetService方法就可以获取想要的对象,至于对象是如何创建的我们不用关心,示例代码如下所示:(如下代码是伪代码)

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

第二种方式:依赖注入

假设框架中有一个自动为类的属性赋值的功能,只要在代码中通过属性声明说明我们需要什么类型的对象,框架就会创建这个对象。这个框架我们叫做”容器”,而这个对象我们叫做“服务”

示例代码如下所示:

1
2
3
4
5
6
7
8
class Demo
{
public IDbConnection Conn { get; set; }
public void InsertDB()
{
IDbCommand cmd = Conn.CreateCommand();
}
}

在上面的代码中,我们创建了一个Conn属性,当创建Demo这个类的对象的时候,框架会自动的给Conn这个属性赋值。

使用比较多的还是依赖注入。

我们把负责提供对象的注册和获取功能的框架叫作“容器”,注册到容器中的对象叫作“服务”(service)

从以上代码可以看出,依赖注入的方式更简单,只要容器给它们赋值即可,不需要像服务定位器那样需要我们通过代码去获取服务。因此我们优先选择依赖注入的方式,只有在依赖注入不满足要求的情况下,才使用服务定位器。

综上所述,控制反转就是把“我创建对象”,变成“我要对象”。实现控制反转的时候,我们可以采用依赖注入或者服务定位器两种方式。程序启动的时候,需要把服务注册到容器中,由容器负责服务的管理。

二、生命周期简介

依赖注入框架中注册的服务有一个重要的概念叫作“生命周期”,通俗地说就是“获取服务的时候是创建一个新对象还是用之前的对象”。依赖注入框架中服务的生命周期有3种。

(1)瞬态(transient):每次被请求的时候都会创建一个新对象。缺点是生成的对象比较多,会浪费内存,所以谨慎使用Transient

(2) 范围(scoped):在给定的范围内,多次请求共享同一个服务对象,服务每次被请求的时候都会返回同一个对象;在不同的范围内,服务每次被请求的时候会返回不同的对象。这个范围可以由框架定义,也可以由开发人员自定义。在ASP.NET Core中,服务默认的范围是一次HTTP请求,也就是在同一次HTTP请求中,不同的注入会获得同一个对象;在不同的HTTP请求中,不同的注入会获得不同的对象。这种方式适用于在同一个范围内共享同一个对象的情况。

(3)单例(singleton):全局共享同一个服务对象。这种生命周期可以节省创建新对象的资源。为了避免并发修改等问题,单例的服务对象最好是无状态对象.

怎样选择?

如果一个类没有状态,也就是没有属性和成员变量,可以定义成singleton.

Asp.net core中,每次请求对应一个线程,在这个线程内没有并发修改的问题,所以会使用scoped

三、服务注册

下面我们来看一下具体的服务注册的方式来实现控制反转演示代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

ITestService t = new TestServiceImpl();
t.Name = "laowang";
t.SayHi();
public interface ITestService
{
public string Name { get; set; }
public void SayHi();

}
public class TestServiceImpl : ITestService
{
public string? Name { get; set; }

public void SayHi()
{
Console.WriteLine($"I'm {Name}");
}
}

在上面的代码中创建了TestServiceImpl类,让其实现了ITestService这个接口。

同时,我们就可以通过new TestServiceImpl();的方式来创建实例,这是我们以前常用的写法。

但是,现在创建实例的操作应该通过IOC容器来完成。

实现控制反转有依赖注入与服务注册,这里我们先来看一下服务注册的方式:

首先通过NuGet安装对应的开发包。

1
Install-Package Microsoft.Extensions.DependencyInjection

然后在代码中引用对应的命名空间:

1
using Microsoft.Extensions.DependencyInjection

服务注册的具体 实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
services.AddTransient<TestServiceImpl>(); // 向容器中注册服务
using (ServiceProvider sp = services.BuildServiceProvider()) // 创建服务定位器
{
TestServiceImpl t= sp.GetService<TestServiceImpl>()!; //从容器中获取服务。注意这里的叹号。
t.Name = "test";
t.SayHi();
}

public interface ITestService
{
public string Name { get; set; }
public void SayHi();

}
public class TestServiceImpl : ITestService
{
public string? Name { get; set; }

public void SayHi()
{
Console.WriteLine($"I'm {Name}");
}
}

在上面的代码中,我们首先创建了用于服务注册的容器,容器的接口是IServiceCollection,其默认实现的类就是ServiceCollection.

当然在IServiceCollection接口中定义了AddTransient、AddScoped和AddSingleton这3组扩展方法,分别用来注册瞬态、范围和单例服务.

注册完成后,我们调用IServiceCollection的BuildServiceProvider方法创建一个ServiceProvider对象,这个ServiceProvider对象就是一个服务定位器。由于ServiceProvider对象实现了IDisposable接口,因此需要使用using对其进行资源的释放

在我们需要获取服务的时候,可以调用ServiceProvider类的GetService方法。

看到上面的代码,大家内心可能会想:“简单的一个new TestServiceImpl能够完成的事情被你搞得这么复杂,有什么意义?”不要急,很多技术开始的时候都让人感觉“多此一举”,比如刚接触“反射技术”的时候,读者肯定也产生过类似的疑问,但是我们用到的很多框架都是基于反射技术实现的。

四、生命周期演示

在这一小节中,我们演示一下关于注册服务的时候三种不同的生命周期,看一下是否和前面分析的一样。

1
2
3
4
5
6
7
8
9
10
11
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
services.AddTransient<TestServiceImpl>(); // 瞬态
using (ServiceProvider sp = services.BuildServiceProvider())
{
TestServiceImpl t= sp.GetService<TestServiceImpl>()!;
t.Name = "test";
t.SayHi();
TestServiceImpl t1 = sp.GetService<TestServiceImpl>()!;
Console.WriteLine(object.ReferenceEquals(t, t1));
}

我们知道上面注册的是瞬态状态下的生命周期,然后又获取了一个新的对象t1,然后通过object.ReferenceEquals方法,比较tt1是否是通过一个对象,如果是同一个对象返回的是true,不是同一个对象返回的是false.

执行结果是false,说明tt1不是同一个对象,也就是说,每次调用GetService方法获取到的都是新的对象。

下面再来看一下单例模式下的情况

1
2
3
4
5
6
7
8
9
10
11
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
services.AddSingleton<TestServiceImpl>(); // 这里修改成了单例模式
using (ServiceProvider sp = services.BuildServiceProvider())
{
TestServiceImpl t= sp.GetService<TestServiceImpl>()!;
t.Name = "test";
t.SayHi();
TestServiceImpl t1 = sp.GetService<TestServiceImpl>()!;
Console.WriteLine(object.ReferenceEquals(t, t1)); // 返回的结果是true
}

在以上的代码中,我们通过单例的方式注册了服务,然后在通过object.ReferenceEquals(t, t1)比较的时候返回的结果是true,说明t与t1是两个相同的对象。

下面再来看一下范围(scoped).

在控制台程序中,范围是我们程序员自己来确定的,但是在Asp.net core中是有框架来确定的,也就是前面所说的一次请求就是一个范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
/*services.AddSingleton<TestServiceImpl>();*/
services.AddScoped<TestServiceImpl>(); // 这里修改成了AddScoped
using (ServiceProvider sp = services.BuildServiceProvider())
{
using (IServiceScope scope1 = sp.CreateScope()) //通过sp.CreateScope()方法创建了一个scope范围,这里的using表示的就是一个范围
{
// 注意:在这里要想获取GetService方法获取服务,需要通过scope1.ServiceProvider方式来获取。
TestServiceImpl t = scope1.ServiceProvider.GetService<TestServiceImpl>()!;
t.Name = "test";
t.SayHi();
TestServiceImpl t1 = scope1.ServiceProvider.GetService<TestServiceImpl>()!;
Console.WriteLine(object.ReferenceEquals(t, t1)); // 这里的结果是true
}


在上面的代码中通过sp.CreateScope()方法,创建了一个scope范围,由于IServiceScope实现了``IDisposable接口,所以这里需要using,而using就表示一个范围。

在一个范围内,tt1就是同一个对象。

下面我们再来看一下不同范围的情况,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
/*services.AddSingleton<TestServiceImpl>();*/
services.AddScoped<TestServiceImpl>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
TestServiceImpl test;//创建了test
using (IServiceScope scope1 = sp.CreateScope())
{

TestServiceImpl t = scope1.ServiceProvider.GetService<TestServiceImpl>()!;
t.Name = "test";
t.SayHi();
TestServiceImpl t1 = scope1.ServiceProvider.GetService<TestServiceImpl>()!;
Console.WriteLine(object.ReferenceEquals(t, t1));
test = t; // 将t对象赋值给了test
}
// 创建了另外一个范围scope2
using (IServiceScope scope2 = sp.CreateScope())
{
TestServiceImpl t = scope2.ServiceProvider.GetService<TestServiceImpl>()!;
t.Name = "test";
t.SayHi();
TestServiceImpl t1 = scope2.ServiceProvider.GetService<TestServiceImpl>()!;
Console.WriteLine(object.ReferenceEquals(t, t1)); // 在同一个范围内t与t1是同一个对象
Console.WriteLine(object.ReferenceEquals(test,t)); // 不在同一范围内,test与t就不是同一个对象,所以这里得到的结果是false.
}


}

在上面的代码中,我们又创建了一个新的范围。

首先,把第一个范围内创建的对象t赋值给了test,然后再另外一个范围内比较testt是否是同一个对象,发现不是。

这也就说明了,不同范围内创建的对象是不相同的。

如果在注册服务的时候,还是单例的方式,那么上面代码中在不同范围内创建的对象都是同一个对象。

1
services.AddSingleton<TestServiceImpl>();

五、服务定位器

1
services.AddSingleton<TestServiceImpl>();

在上面注册服务的时候,我们写到都是具体的实现类型,而不是接口。

这里我们最好写成接口的形式,面向接口编程有很对的好处。

1
2
3
4
5
6
7
8
9
10
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
services.AddSingleton<ITestService,TestServiceImpl>();// 这里注册服务的时候,指定了接口与具体实现
using (ServiceProvider sp = services.BuildServiceProvider())
{
// 获取的是实现了ITestService接口的服务
ITestService testService = sp.GetService<ITestService>()!;
testService.Name = "test";
testService.SayHi();
}

在上面的代码中,当我们获取服务的时候,只要通过GetService方法获取实现了ITestService接口的服务。

当然,随着项目业务复杂以后,通过AddSingleton注册服务的和通过GetService方法获取服务的不是同一个人。

注册服务的是某个框架,而通过GetService方法获取服务的是写具体业务的开发人员。写业务的开发人员,不用关心服务是怎么来的,只是通过GetService方法来获取服务就可以了。

这里还需要强调一点,关于Addxxx方法是有很多重载的,这里以AddSingleton方法来进行说明。

1
2
/*services.AddSingleton(typeof(ITestService), typeof(TestServiceImpl));*/
services.AddSingleton(typeof(ITestService), new TestServiceImpl());// 这里是自己手动创建了TestServiceImpl对象,不是通过框架来创建对象,这种方式适合传递参数的情况。

下面我们再来看一下关于GetService方法的问题。

该方法的定义如下所示,它是一个泛型的方法。

1
public static T? GetService<T>(this IServiceProvider provider)

所以我们在调用GetService方法的时候,传递的是ITestService,定义的变量的类型也是ITestService

1
2
3
4
5
6
7
8
9
10
11
12
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
/*services.AddSingleton<ITestService,TestServiceImpl>();*/
/*services.AddSingleton(typeof(ITestService), typeof(TestServiceImpl));*/
services.AddSingleton(typeof(ITestService), new TestServiceImpl());
using (ServiceProvider sp = services.BuildServiceProvider())
{

TestServiceImpl testService = sp.GetService<TestServiceImpl>()!;
testService.Name = "test";
testService.SayHi();
}

当运行上面的程序的时候,程序会出错,testService的值为null.

说明:第一:这里GetService来获取服务的时候,所需要的泛型类型,一定是在调用AddSingleton方法的时候,注册的类型。在上面的代码中,AddSingleton注册的类型是ITestService.

所以这里的GetService方法获取服务的时候,指定的类型应该是ITestService而不是TestServiceImpl.

当然,如果在调用AddSingleton方法的时候,指定的类型是TestServiceImpl,这时候在调用GetService方法获取对应的服务的时候,类型是TestServiceImpl是可以的。

1
services.AddSingleton(typeof(TestServiceImpl), new TestServiceImpl());

但是,我们前面也提到过,最好使用接口。

第二:通过出错的信息,我们可以看出:GetService如果找不到服务,返回值是null

当然,GetService方法还有非泛型的情况,如下代码所示:

1
2
3
4
5
6
7
services.AddSingleton(typeof(ITestService), new TestServiceImpl());// 注意:这里要将类型修改为ITestService
using (ServiceProvider sp = services.BuildServiceProvider())
{
ITestService testService =(ITestService)sp.GetService(typeof(ITestService))! ;// 这里进行了类型的转换。注意!号的使用
testService.Name = "test";
testService.SayHi();
}

当然,我们一般都是使用泛型的应用。

除了使用GetService方法获取服务意外,也可以使用GetRequiredService方法 来获取服务。

该方法有泛型和非泛型两种情况,下面我们只是看一下它的泛型情况就可以了。

这里需要注意的是GetRequiredService方法,在找不到具体的服务的时候,并不会返回null,而是直接抛异常,这是与GetService方法的区别,GetService方法在找不到服务的时候,返回的是null.

1
2
3
4
5
6
7
8
services.AddSingleton(typeof(ITestService), new TestServiceImpl());
using (ServiceProvider sp = services.BuildServiceProvider())
{
/* ITestService testService =(ITestService)sp.GetService(typeof(ITestService))! ;*/
ITestService testService=sp.GetRequiredService<ITestService>(); // 这里使用了GetRequiredService方法。
testService.Name = "test";
testService.SayHi();
}

在上面的代码中通过GetRequiredService方法来获取服务。

1
ITestService testService=sp.GetRequiredService<TestServiceImpl>();

上面的代码很明显无法获取TestServiceImpl这个服务,所以会抛出以下异常信息。

1
2
System.InvalidOperationException:“No service for type 'TestServiceImpl' has been registered.”

如果想想获取多个服务,可以使用GetServices这个方法来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface ITestService
{
public string Name { get; set; }
public void SayHi();

}
public class TestServiceImpl : ITestService
{
public string? Name { get; set; }

public void SayHi()
{
Console.WriteLine($"I'm {Name}");
}
}
public class TestServiceImpl2 : ITestService
{
public string? Name { get; set; }

public void SayHi()
{
Console.WriteLine($"I'm {Name}");
}
}

在上面的代码中,我们又定义了TestServiceImpl2类,实现了ITestService接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
/*services.AddSingleton<ITestService,TestServiceImpl>();*/
/*services.AddSingleton(typeof(ITestService), typeof(TestServiceImpl));*/
services.AddSingleton(typeof(ITestService), new TestServiceImpl());
services.AddSingleton(typeof(ITestService), new TestServiceImpl2());// 这里注册了TestServiceImpl2服务
using (ServiceProvider sp = services.BuildServiceProvider())
{
/* ITestService testService =(ITestService)sp.GetService(typeof(ITestService))! ;*/
/* ITestService testService=sp.GetRequiredService<TestServiceImpl>();*/
IEnumerable<ITestService>tests= sp.GetServices<ITestService>(); // 获取注册的所有服务
foreach(ITestService it in tests)
{
it.Name =it.GetType().ToString();
it.SayHi();
}
}

在上面的代码中,又注册了TestServiceImpl2服务。

下面通过GetServices获取了实现了ITestService接口的所有服务,返回的是IEnumerable这个泛型接口。

下面进行了遍历。

获取每个服务的类型,赋值给了Name属性,然后调用了SayHi方法。

六、依赖注入

前面我们实现控制反转的方式都是通过服务定位的方式来实现的。

在这一小节中我们将通过依赖注入的方式来实现控制反转。

.NetDI(依赖注入)默认采用的是构造函数的注入。也就是通过构造函数完成服务的注入

下面看一下基本的代码实现:

在解决方法中,又重新创建了一个项目,并且在该项目下面按照了包Install-Package Microsoft.Extensions.DependencyInjection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

/*日志接口*/
interface ILog
{
public void Log(string message);
}
class LogImpl : ILog
{
private readonly IConfig config;
public LogImpl(IConfig config)
{
this.config = config;
}

public void Log(string message)
{
string configMsg = config.GetValue("日志");
Console.WriteLine(configMsg);
Console.WriteLine("日志信息:"+message);
}
}
/*配置接口*/
interface IConfig
{
public string GetValue(string name);// 根据配置的名字获取具体的配置信息
}
class ConfigImpl : IConfig
{
public string GetValue(string name)
{
return $"获取了{name}的配置信息";
}
}

在上面的代码中,我们首先定义了一个IConfig接口,然后定义类ConfigImpl实现了IConfig接口中的GetValue方法。

同时还定义了ILog接口,在该接口中声明的Log方法是完成日志记录的。

LogImpl类中实现了ILog接口中的Log方法。

当然,在进行日志记录的时候,需要获取相关的配置信息。

所以在LogImpl类中添加了对应的构造函数,该构造函数的参数就是注入的配置对象。

对应的config实例,我们一般都是通过readonly来修饰,防止 后面重新给config赋值。

LogImpl这个类中,我们不用关心config是怎么来的,我们只要知道框架会提供一个config的服务就可以了。

这也就是说明,写业务代码的开发人员,只需要要服务就可以了,但是服务是怎么创建的,怎么来的不用关心。

下面继续完善代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
services.AddScoped<Controller>();// 这里为了简单一些,没有实现Controller类的接口。
services.AddScoped<ILog, LogImpl>();
services.AddScoped<IConfig, ConfigImpl>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
var c=sp.GetRequiredService<Controller>();
c.Test();
}

class Controller
{
private readonly ILog log;
public Controller(ILog log)
{
this.log = log;
}
public void Test()
{
Console.WriteLine("开始业务");
this.log.Log("aaaaaaa");
Console.WriteLine("业务结束");
}
}

在上面的代码中首先创建了services这个服务的容器,下面向该容器中注册了Controller等服务。

然后通过容器对象中的BuildServiceProvider方法,创建了ServiceProvider对象,在通过该对象中的GetRequiredService方法获取Controller服务,最后调用Controller服务中的test方法。

同时下面也创建了Controller这个类,在该类的构造函数中完成了ILog服务的注入。

在其test方法中调用了ILog服务中的Log方法,完成了日志的记录。

当然在Log方法中也读取了对应的配置信息。

完整的案例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
services.AddScoped<Controller>();
services.AddScoped<ILog, LogImpl>();
services.AddScoped<IConfig, ConfigImpl>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
var c=sp.GetRequiredService<Controller>();
c.Test();
}

class Controller
{
private readonly ILog log;
public Controller(ILog log)
{
this.log = log;
}
public void Test()
{
Console.WriteLine("开始业务");
this.log.Log("aaaaaaa");
Console.WriteLine("业务结束");
}
}

/*日志接口*/
interface ILog
{
public void Log(string message);
}
class LogImpl : ILog
{
private readonly IConfig config;
public LogImpl(IConfig config)
{
this.config = config;

}

public void Log(string message)
{
string configMsg = config.GetValue("日志");
Console.WriteLine(configMsg);
Console.WriteLine("日志信息:"+message);
}
}
/*配置接口*/
interface IConfig
{
public string GetValue(string name);// 根据配置的名字获取具体的配置信息
}
class ConfigImpl : IConfig
{
public string GetValue(string name)
{
return $"获取了{name}的配置信息";
}
}

思考:这种依赖注入到底有什么好处?

如果日志的配置信息需要从数据库中来获取应该怎样进行处理呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*配置接口*/
interface IConfig
{
public string GetValue(string name);// 根据配置的名字获取具体的配置信息
}
class ConfigImpl : IConfig
{
public string GetValue(string name)
{
return $"获取了{name}的配置信息";
}
}
class DBConfigImpl : IConfig
{
public string GetValue(string name)
{
return $"从数据库中获取了{name}的配置信息";
}
}

在上面的代码中,我们又定义了DBConfigImpl类,实现了IConfig接口中的GetValue

1
2
3
4
5
ServiceCollection services = new ServiceCollection();
services.AddScoped<Controller>();
services.AddScoped<ILog, LogImpl>();
/*services.AddScoped<IConfig, ConfigImpl>();*/
services.AddScoped<IConfig, DBConfigImpl>(); // 这里注册了DBConfigImpl

同时在注册配置服务的时候,换成了DBConfigImpl.

这时候运行程序,发现日志所需要的配置信息是从数据中获取的。

但是问题是我们并没有修改Controller类,LogImpl类中的代码(**LogImpl类根本就不关心日志的配置信息是从哪获取的,只要实现了IConfig接口就行**),这也就是说通过依赖注入降低了模块之间的耦合。系统变得更加的灵活了。

七、案例

要求:我们开发一个传统分层的项目,项目结构如下:

UI层,业务逻辑层,数据访问层。

UI层访问业务逻辑成,业务逻辑层访问数据访问层。数据层通过ADO.NET来操作数据库。

首先创建Model层,在该层中创建了一个UserInfo.cs类,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Model
{
public class UserInfo
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Password { get; set; }
}
}

创建DAL层,在该层中先创建接口IUserDAO.cs,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DAL
{
public interface IUserDAO
{
public UserInfo GetUserInfo(string userName);
}
}

DAL层中需要引入Model层。

下面还需要在DAL层中创建UserDAO.cs类来实现IUserDAO.cs这个接口。UserDAO.cs类中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using Model;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace DAL
{
public class UserDAO : IUserDAO
{
private readonly IDbConnection conn;
public UserDAO(IDbConnection conn)
{
this.conn = conn;
}
public UserInfo GetUserInfo(string userName)
{
// 这里应该是调用SqlHelper中的相应方法完成查询.
using (var dat = SqlHelper.ExecuteQuery(conn, $"select * from userInfo where Name={userName}"))
{
if (dat.Rows.Count <= 0) return null;

DataRow row = dat.Rows[0];
int id = (int)row["id"];
string uname = (string)row["Name"];
string password = (string)row["Password"];

return new UserInfo() { Id = id, Name = uname, Password = password };
};



}
}
}

UserDAO类中通过构造方法的形式依赖注入了IDConnect.

所实现的方法GetUserInfo中通过SqlHelper类操作数据库。(把SqlHelper类添加到了DAL层中)

下面创建了BLL层,同时在该层中创建了接口IUserBLL.cs,该接口中的代码如下所示:

1
2
3
4
5
6
7
8
9
namespace BLL
{
public interface IUserBLL
{
// 检查用户名密码是否匹配
public bool CheckLogin(string userName, string password);
}
}

下面又创建了类UserBLL.cs来实现以上的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BLL
{
public class UserBLL : IUserBLL
{
private readonly IUserDAO userDAO;
public UserBLL(IUserDAO userDao)
{
this.userDAO = userDao;
}
public bool CheckLogin(string userName, string password)
{

var user=userDAO.GetUserInfo(userName);
if (user == null)
{
return false;
}
else
{
return user.Password == password;
}

}
}
}

在上面的代码中也是通过构造方法注入了IUserDAO服务,

注意这里要求注入的是IUserDAO接口,而不是UserDAO类,因为我们要面向接口编程,而不是面向实现编程。由于这里是基于接口编程的

同时在BLL层中要引入DAL层。

下面看一下UI层中的代码,在Program.cs文件中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using BLL;
using DAL;
using Microsoft.Extensions.DependencyInjection;
using System.Data;
using System.Data.SqlClient;

ServiceCollection services = new ServiceCollection();
services.AddScoped<IDbConnection>(sp =>
{
string connStr = "Data Source=.;Initial Catalog=TestDB;uid=sa;pwd=123456";
var conn = new SqlConnection(connStr);
conn.Open();
return conn;
});
services.AddScoped<IUserDAO, UserDAO>();

services.AddScoped<IUserBLL, UserBLL>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
var userBll=sp.GetService<IUserBLL>();

bool b = userBll.CheckLogin("admin", "12345688888");
Console.WriteLine(b);
}

在上面代码的最开始注册了生命周期为范围IDbConnection服务。

同时又注册了IUserDAO,IUserBLL两个服务。

最后通过sp这个Provider对象中的GetService方法获取了IUserBLL服务。从而完成了该服务中的CheckLogin方法的调用。

这样做的好处就是:如果以后需要更换数据库。例如更换成MySQL数据库,只需要将上面的代码中SqlConnection换成MySQLConnection即可,而UserDAO,UserBLL中的代码都是不需要修改的。

说明:数据库TestDB中有一个userInfo表,该表的字段Id是主键,Name表示用户名,Password表示密码。

今日总结-Kilgour

首先将了要停止使用new这个关键字,采用一种引得框架(或者说新的方法)- 加入Microsoft.Extensions.DependencyInjectionNuget包来实现对象的自主创建和回收。并且介绍了相关的语法和用法(七、总结)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using Microsoft.Extensions.DependencyInjection;
ServiceCollection service = new ServiceCollection();//创建一个服务列表(服务器)用于存放各种服务(transient、scoped、singleton)
service.AddScoped<Controller> ();//存放服务
service.AddScoped<ILog, Log> ();
//service.AddScoped<IConfig, Config>();如有业务变化则无需改动核心代码
service.AddScoped<IConfig, Config2>();

using (var sp = service.BuildServiceProvider())//服务列表(服务器)创建一个服务提供者用于调用你存储的各种服务
{
var controller = sp.GetRequiredService<Controller>();
controller.Test();
}

public class Controller//为了简洁因此这个没有实现接口、但是在实际应用中一定要采用接口形式
{
private readonly ILog _log;
public Controller(ILog log)
{
this._log = log;
}

public void Test()
{
Console.WriteLine("业务的开始");
_log.WriteLog("hello world");
Console.WriteLine("业务的结束");
}
}

public interface ILog
{
public void WriteLog(string message);
}

public class Log : ILog
{
private readonly IConfig _config;

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

public void WriteLog(string message)
{
string str = _config.GetValue("日志");
Console.WriteLine(str);
Console.WriteLine("然后打印日志信息" + message);
}
}

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

public class Config : IConfig
{
public string GetValue(string name)
{
return $"首先获取该服务{name}的配置信息";
}
}
public class Config2 : IConfig
{
public string GetValue(string name)
{
return $"首先获取服务器的{name}配置信息";
}
}

创建json配置文件存放SQL连接字符串

  • 该Json配置文件应防止于Debug文件目录下,但是每次要手动放置。因此右键点击Json文件属性将复制到输出目录改为如果较新则复制

在JSON文件中如上图创建连接数据库字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Microsoft.Extensions.Configuration;
//using Microsoft.Extensions.Configuration.Json;
ConfigurationBuilder cb = new ConfigurationBuilder();
// 加载指定的配置文件
// 第一个参数:加载的配置文件的路径
// 第二个参数optional:设置为false,表示如果在加载配置文件的过程中,没有找到文件,会抛出异常。如果设置为了true,表示找不到配置文件,也不会抛出异常。
// 第三个参数:建议设置为true,文件修改后,能够重新加载
cb.AddJsonFile("config.Json",optional:false,reloadOnChange:true);
// 通过IConfigurationRoot来获取配置文件中具体的某个配置的节点。
IConfigurationRoot root = cb.Build();
// 读取的配置文件中name这个节点
string str = root["name"]!;
Console.WriteLine("用户名是:"+ str);
string address = root.GetSection("proxy:address").Value!;
Console.WriteLine("地址是:"+ address);
// GetConnectionString专门用来读取配置文件中的ConnectionStrings配置节点,所以配置节点不能随意乱写。
Console.WriteLine("连接数据库字符串是:" + root.GetConnectionString("str"));