使用Visual Studio Code開發Asp.Net Core WebApi學習筆記(六)-- 依賴注入

本篇將介紹Asp.Net Core中一個很是重要的特性:依賴注入,並展現其簡單用法。html

第一部分、概念介紹

Dependency Injection:又稱依賴注入,簡稱DI。在之前的開發方式中,層與層之間、類與類之間都是經過new一個對方的實例進行相互調用,這樣在開發過程當中有一個好處,能夠清晰的知道在使用哪一個具體的實現。隨着軟件體積愈來愈龐大,邏輯愈來愈複雜,當須要更換實現方式,或者依賴第三方系統的某些接口時,這種相互之間持有具體實現的方式再也不合適。爲了應對這種狀況,就要採用契約式編程:相互之間依賴於規定好的契約(接口),不依賴於具體的實現。這樣帶來的好處是相互之間的依賴變得很是簡單,又稱鬆耦合。至於契約和具體實現的映射關係,則會經過配置的方式在程序啓動時由運行時肯定下來。這就會用到DI。編程

 

第二部分、DI的註冊與注入

借用這個系列以前的框架結構,添加以下接口和實現類 api

 1 using System.Collections.Generic;
 2 using WebApiFrame.Models;
 3 
 4 namespace WebApiFrame.Repositories
 5 {
 6     public interface IUserRepository
 7     {
 8         IEnumerable<User> GetAll();
 9 
10         User GetById(int id);
11     }
12 }
IUserRepository.cs
 1 using System.Collections.Generic;
 2 using System.Linq;
 3 using WebApiFrame.Models;
 4 
 5 namespace WebApiFrame.Repositories
 6 {
 7     public class UserRepository : IUserRepository
 8     {
 9         private IList<User> list = new List<User>()
10         {
11             new User(){ Id = 1, Name = "name:1", Sex = "Male" },
12             new User(){ Id = 2, Name = "name:2", Sex = "Female" },
13             new User(){ Id = 3, Name = "name:3", Sex = "Male" },
14         };
15 
16         public IEnumerable<User> GetAll()
17         {
18             return list;
19         }
20 
21         public User GetById(int id)
22         {
23             return list.FirstOrDefault(i => i.Id == id);
24         }
25     }
26 }
UserRepository.cs

1、註冊

修改 Startup.cs 的ConfigureServices方法,將上面的接口和實現類注入到DI容器裏框架

1         public void ConfigureServices(IServiceCollection services)
2         {
3             // 注入MVC框架
4             services.AddMvc();
5 
6             // 註冊接口和實現類的映射關係
7             services.AddScoped<IUserRepository, UserRepository>();
8         }

修改 UsersController.cs 的構造函數和Action方法async

 1 using System;
 2 using Microsoft.AspNetCore.Mvc;
 3 using WebApiFrame.Models;
 4 using WebApiFrame.Repositories;
 5 
 6 namespace WebApiFrame.Controllers
 7 {
 8     [Route("api/[controller]")]
 9     public class UsersController : Controller
10     {
11         private readonly IUserRepository userRepository;
12 
13         public UsersController(IUserRepository userRepo)
14         {
15             userRepository = userRepo;
16         }
17 
18         [HttpGet]
19         public IActionResult GetAll()
20         {
21             var list = userRepository.GetAll();
22             return new ObjectResult(list);
23         }
24 
25         [HttpGet("{id}")]
26         public IActionResult Get(int id)
27         {
28             var user = userRepository.GetById(id);
29             return new ObjectResult(user);
30         }
31 
32         #region 其餘方法
33         // ......
34         #endregion
35     }
36 }

啓動程序,分別訪問地址 http://localhost:5000/api/users 和 http://localhost:5000/api/users/1 ,頁面將展現正確的數據。ide

從上面的例子能夠看到,在 Startup.cs 的ConfigureServices的方法裏,經過參數的AddScoped方法,指定接口和實現類的映射關係,註冊到DI容器裏。在控制器裏,經過構造方法將具體的實現注入到對應的接口上,便可在控制器裏直接調用了。函數

除了在ConfigureServices方法裏進行註冊外,還能夠在Main函數裏進行註冊。註釋掉 Startup.cs ConfigureServices方法裏的注入代碼,在 Program.cs 的Main函數裏添加註入方法測試

 1 using Microsoft.AspNetCore.Hosting;
 2 using Microsoft.Extensions.DependencyInjection;
 3 using WebApiFrame.Repositories;
 4 
 5 namespace WebApiFrame
 6 {
 7     public class Program
 8     {
 9         public static void Main(string[] args)
10         {
11             var host = new WebHostBuilder()
12                 .UseKestrel()
13                 .ConfigureServices(services=>
14                 {
15                     // 註冊接口和實現類的映射關係
16                     services.AddScoped<IUserRepository, UserRepository>();
17                 })
18                 .UseStartup<Startup>()
19                 .Build();
20 
21             host.Run();
22         }
23     }
24 }

此方法等效於 Startup.cs 的ConfigureServices方法。ui

2、注入

添加三個測試接口和實現類spa

 1 namespace WebApiFrame
 2 {
 3     public interface ITestOne
 4     {
 5         
 6     }
 7 
 8     public class TestOne : ITestOne
 9     {
10         
11     }
12 }
ITestOne.cs
 1 namespace WebApiFrame
 2 {
 3     public interface ITestTwo
 4     {
 5         
 6     }
 7 
 8     public class TestTwo : ITestTwo
 9     {
10         
11     }
12 }
ITestTwo.cs
 1 namespace WebApiFrame
 2 {
 3     public interface ITestThree
 4     {
 5 
 6     }
 7 
 8     public class TestThree : ITestThree
 9     {
10 
11     }
12 }
ITestThree.cs

修改 Startup.cs 的ConfigureServices方法,將接口和實現類的映射關係註冊到DI容器

 1         public void ConfigureServices(IServiceCollection services)
 2         {
 3             // 注入MVC框架
 4             services.AddMvc();
 5 
 6             // 註冊接口和實現類的映射關係
 7             services.AddScoped<ITestOne, TestOne>();
 8             services.AddScoped<ITestTwo, TestTwo>();
 9             services.AddScoped<ITestThree, TestThree>();
10         }

添加 DemoController.cs 類

 1 using System.Threading.Tasks;
 2 using Microsoft.AspNetCore.Http;
 3 using Microsoft.AspNetCore.Mvc;
 4 
 5 namespace WebApiFrame
 6 {
 7     [Route("[controller]")]
 8     public class DemoController : Controller
 9     {
10         private readonly ITestOne _testOne;
11         private readonly ITestTwo _testTwo;
12         private readonly ITestThree _testThree;
13 
14         public DemoController(ITestOne testOne, ITestTwo testTwo, ITestThree testThree)
15         {
16             _testOne = testOne;
17             _testTwo = testTwo;
18             _testThree = testThree;
19         }
20 
21         [HttpGet("index")]
22         public async Task Index()
23         {
24             HttpContext.Response.ContentType = "text/html";
25             await HttpContext.Response.WriteAsync($"<h1>ITestOne => {_testOne}</h1>");
26             await HttpContext.Response.WriteAsync($"<h1>ITestTwo => {_testTwo}</h1>");
27             await HttpContext.Response.WriteAsync($"<h1>ITestThree => {_testThree}</h1>");
28         } 
29     }
30 }

啓動程序,訪問地址 http://localhost:5000/demo/index ,頁面顯示了每一個接口對應的實現類

一般依賴注入的方式有三種:構造函數注入、屬性注入、方法注入。在Asp.Net Core裏,採用的是構造函數注入。

在之前的Asp.Net MVC版本里,控制器必須有一個無參的構造函數,供框架在運行時調用建立控制器實例,在Asp.Net Core裏,這不是必須的了。當訪問控制器的Action方法時,框架會依據註冊的映射關係生成對應的實例,經過控制器的構造函數參數注入到控制器中,並建立控制器實例。

3、構造函數的選擇

上一個例子展現了在.Net Core裏採用構造函數注入的方式實現依賴注入。當構造函數有多個,而且參數列表不一樣時,框架又會採用哪個構造函數建立實例呢?

爲了更好的演示,新建一個.Net Core控制檯程序,引用下面兩個nuget包。DI容器正是經過這兩個包來實現的。

"Microsoft.Extensions.DependencyInjection": "1.0.0"
"Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0"

一樣新建四個測試接口和實現類,並在Main函數添加註冊代碼。最終代碼以下

 1 using Microsoft.Extensions.DependencyInjection;
 2 using System;
 3 
 4 namespace DiApplicationTest
 5 {
 6     public class Program
 7     {
 8         public static void Main(string[] args)
 9         {
10             IServiceCollection services = new ServiceCollection();
11             services.AddScoped<ITestOne, TestOne>()
12                 .AddScoped<ITestTwo, TestTwo>()
13                 .AddScoped<ITestThree, TestThree>()
14                 .AddScoped<ITestApp, TestApp>()
15                 .BuildServiceProvider()
16                 .GetService<ITestApp>();
17 
18             Console.ReadLine();
19         }
20     }
21 
22     public interface ITestOne { }
23     public interface ITestTwo { }
24     public interface ITestThree { }
25 
26     public class TestOne : ITestOne { }
27     public class TestTwo : ITestTwo { }
28     public class TestThree : ITestThree { }
29 
30     public interface ITestApp { }
31     public class TestApp : ITestApp
32     {
33         public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree)
34         {
35             Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})");
36         }
37     }
38 }

啓動調試,在cmd窗口能夠看見打印內容

這裏註冊了四個接口和對應的實現類,其中一個接口的實現類 TestApp.cs 擁有一個三個參數的構造函數,這三個參數類型分別是其餘三個接口。經過GetServices方法經過惟一的一個構造函數建立了 TestApp.cs 的一個實例。

接下來在 TestApp.cs 裏添加一個有兩個參數的構造函數,同時修改Main函數內容,去掉一個接口的註冊

 1     public class TestApp : ITestApp
 2     {
 3         public TestApp(ITestOne testOne, ITestTwo testTwo)
 4         {
 5             Console.WriteLine($"TestApp({testOne}, {testTwo})");
 6         }
 7 
 8         public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree)
 9         {
10             Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})");
11         }
12     }
 1         public static void Main(string[] args)
 2         {
 3             IServiceCollection services = new ServiceCollection();
 4             services.AddScoped<ITestOne, TestOne>()
 5                 .AddScoped<ITestTwo, TestTwo>()
 6                 //.AddScoped<ITestThree, TestThree>()
 7                 .AddScoped<ITestApp, TestApp>()
 8                 .BuildServiceProvider()
 9                 .GetService<ITestApp>();
10 
11             Console.ReadLine();
12         }

再次啓動調試,查看cmd窗口打印內容

當有多個構造函數時,框架會選擇參數都是有效注入接口的構造函數建立實例。在上面這個例子裏, ITestThree.cs 和 TestThree.cs 的映射關係沒有註冊到DI容器裏,框架在選擇有效的構造函數時,會過濾掉含有ITestThree接口類型的參數的構造函數。

接下來在 TestApp.cs 再添加一個構造函數。爲了方便起見,我給每一個構造函數添加了編號標識一下。

 1     public class TestApp : ITestApp
 2     {
 3         // No.1
 4         public TestApp(ITestOne testOne)
 5         {
 6             Console.WriteLine($"TestApp({testOne})");
 7         }
 8 
 9         // No.2
10         public TestApp(ITestOne testOne, ITestTwo testTwo)
11         {
12             Console.WriteLine($"TestApp({testOne}, {testTwo})");
13         }
14 
15         // No.3
16         public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree)
17         {
18             Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})");
19         }
20     }

再次啓動調試,查看cmd窗口打印內容

結果顯示框架選擇了No.2號構造函數。框架會選擇參數列表集合是其餘全部有效的構造函數的參數列表集合的超集的構造函數。在這個例子裏,有No.1和No.2兩個有效的構造函數,No.2的參數列表集合爲[ITestOne, ITestTwo],No.1的參數列表集合爲[ITestOne],No.2是No.1的超集,因此框架選擇了No.2構造函數建立實例。

接下來修改下 TestApp.cs 的構造函數,取消Main函數裏 ITestThree.cs 註冊代碼的註釋

 1     public class TestApp : ITestApp
 2     {
 3         // No.2
 4         public TestApp(ITestOne testOne, ITestTwo testTwo)
 5         {
 6             Console.WriteLine($"TestApp({testOne}, {testTwo})");
 7         }
 8 
 9         // No.4
10         public TestApp(ITestTwo testTwo, ITestThree testThree)
11         {
12             Console.WriteLine($"TestApp({testTwo}, {testThree})");
13         }
14     }

啓動調試,發現會拋出一個 System.InvalidOperationException 異常,異常內容代表框架沒法選擇一個正確的構造函數,不能建立實例。

在這個例子裏,兩個構造函數的參數列表集合分別爲[ITestOne, ITestTwo]和[ITestTwo, ITestThree],由於誰也沒法是對方的超集,因此框架不能繼續建立實例。

總之,框架在選擇構造函數時,會依次遵循如下兩點規則:

1. 使用有效的構造函數建立實例

2. 若是有效的構造函數有多個,選擇參數列表集合是其餘全部構造函數參數列表集合的超集的構造函數建立實例

若是以上兩點都不知足,則拋出 System.InvalidOperationException 異常。

4、Asp.Net Core默認註冊的服務接口

框架提供了但不限於如下幾個接口,某些接口能夠直接在構造函數和 Startup.cs 的方法裏注入使用

 

第三部分、生命週期管理

框架對注入的接口建立的實例有一套生命週期的管理機制,決定了將採用什麼樣的建立和回收實例。

下面經過一個例子演示這三種方式的區別

在第二部分的第二點的例子裏添加如下幾個接口和實現類

 1 using System;
 2 
 3 namespace WebApiFrame
 4 {
 5     public interface ITest
 6     {
 7         Guid TargetId { get; }
 8     }
 9 
10     public interface ITestTransient : ITest { }
11     public interface ITestScoped : ITest { }
12     public interface ITestSingleton : ITest { }
13 
14     public class TestInstance : ITestTransient, ITestScoped, ITestSingleton
15     {
16         public Guid TargetId
17         {
18             get
19             {
20                 return _targetId;
21             }
22         }
23 
24         private Guid _targetId { get; set; }
25 
26         public TestInstance()
27         {
28             _targetId = Guid.NewGuid();
29         }
30     }
31 }
ITest.cs
 1 namespace WebApiFrame
 2 {
 3     public class TestService
 4     {
 5         public ITestTransient TestTransient { get; }
 6         public ITestScoped TestScoped { get; }
 7         public ITestSingleton TestSingleton { get; }
 8 
 9         public TestService(ITestTransient testTransient, ITestScoped testScoped, ITestSingleton testSingleton)
10         {
11             TestTransient = testTransient;
12             TestScoped = testScoped;
13             TestSingleton = testSingleton;
14         }
15     }
16 }
TestService.cs

修改 Startup.cs 的ConfigureServices方法裏添加註冊內容

 1         public void ConfigureServices(IServiceCollection services)
 2         {
 3             // 注入MVC框架
 4             services.AddMvc();
 5 
 6             // 註冊接口和實現類的映射關係
 7             services.AddTransient<ITestTransient, TestInstance>();
 8             services.AddScoped<ITestScoped, TestInstance>();
 9             services.AddSingleton<ITestSingleton, TestInstance>();
10             services.AddTransient<TestService, TestService>();
11         }

修改 DemoController.cs 內容

 1 using System.Threading.Tasks;
 2 using Microsoft.AspNetCore.Http;
 3 using Microsoft.AspNetCore.Mvc;
 4 
 5 namespace WebApiFrame
 6 {
 7     [Route("[controller]")]
 8     public class DemoController : Controller
 9     {
10         public ITestTransient _testTransient { get; }
11         public ITestScoped _testScoped { get; }
12         public ITestSingleton _testSingleton { get; }
13         public TestService _testService { get; }
14 
15         public DemoController(ITestTransient testTransient, ITestScoped testScoped, ITestSingleton testSingleton, TestService testService)
16         {
17             _testTransient = testTransient;
18             _testScoped = testScoped;
19             _testSingleton = testSingleton;
20             _testService = testService;
21         }
22 
23         [HttpGet("index")]
24         public async Task Index()
25         {
26             HttpContext.Response.ContentType = "text/html";
27             await HttpContext.Response.WriteAsync($"<h1>Controller Log</h1>");
28             await HttpContext.Response.WriteAsync($"<h6>Transient => {_testTransient.TargetId.ToString()}</h6>");
29             await HttpContext.Response.WriteAsync($"<h6>Scoped => {_testScoped.TargetId.ToString()}</h6>");
30             await HttpContext.Response.WriteAsync($"<h6>Singleton => {_testSingleton.TargetId.ToString()}</h6>");
31             
32             await HttpContext.Response.WriteAsync($"<h1>Service Log</h1>");
33             await HttpContext.Response.WriteAsync($"<h6>Transient => {_testService.TestTransient.TargetId.ToString()}</h6>");
34             await HttpContext.Response.WriteAsync($"<h6>Scoped => {_testService.TestScoped.TargetId.ToString()}</h6>");
35             await HttpContext.Response.WriteAsync($"<h6>Singleton => {_testService.TestSingleton.TargetId.ToString()}</h6>");
36         }
37     }
38 }

啓動調試,連續兩次訪問地址 http://localhost:5000/demo/index ,查看頁面內容

對比內容能夠發現,在同一個請求裏,Transient對應的GUID都是不一致的,Scoped對應的GUID是一致的。而在不一樣的請求裏,Scoped對應的GUID是不一致的。在兩個請求裏,Singleton對應的GUID都是一致的。

 

第三部分、第三方DI容器

除了使用框架默認的DI容器外,還能夠引入其餘第三方的DI容器。下面以Autofac爲例,進行簡單的演示。

引入Autofac的nuget包

"Autofac.Extensions.DependencyInjection": "4.0.0-rc3-309"

在上面的例子的基礎上修改 Startup.cs 的ConfigureServices方法,引入autofac的DI容器,修改方法返回值

 1         public IServiceProvider ConfigureServices(IServiceCollection services)
 2         {
 3             // 注入MVC框架
 4             services.AddMvc();
 5 
 6             // autofac容器
 7             var containerBuilder = new ContainerBuilder();
 8             containerBuilder.RegisterType<TestInstance>().As<ITestTransient>().InstancePerDependency();
 9             containerBuilder.RegisterType<TestInstance>().As<ITestScoped>().InstancePerLifetimeScope();
10             containerBuilder.RegisterType<TestInstance>().As<ITestSingleton>().SingleInstance();
11             containerBuilder.RegisterType<TestService>().AsSelf().InstancePerDependency();
12             containerBuilder.Populate(services);
13 
14             var container = containerBuilder.Build();
15             return container.Resolve<IServiceProvider>();
16         }

啓動調試,再次訪問地址 http://localhost:5000/demo/index ,會獲得上個例子一樣的效果。

相關文章
相關標籤/搜索