本篇將介紹Asp.Net Core中一個非常重要的特性:依賴注入,並展示其簡單用法。
第一部分、概念介紹
Dependency Injection:又稱依賴注入,簡稱DI。在以前的開發方式中,層與層之間、類與類之間都是通過new一個對方的實例進行相互調用,這樣在開發過程中有一個好處,可以清晰的知道在使用哪個具體的實現。隨着軟件體積越來越龐大,邏輯越來越復雜,當需要更換實現方式,或者依賴第三方系統的某些接口時,這種相互之間持有具體實現的方式不再合適。為了應對這種情況,就要采用契約式編程:相互之間依賴於規定好的契約(接口),不依賴於具體的實現。這樣帶來的好處是相互之間的依賴變得非常簡單,又稱松耦合。至於契約和具體實現的映射關系,則會通過配置的方式在程序啟動時由運行時確定下來。這就會用到DI。
第二部分、DI的注冊與注入
借用這個系列之前的框架結構,添加如下接口和實現類

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 }

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 }
一、注冊
修改 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方法
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 ,頁面將展示正確的數據。
從上面的例子可以看到,在 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方法。
二、注入
添加三個測試接口和實現類

1 namespace WebApiFrame 2 { 3 public interface ITestOne 4 { 5 6 } 7 8 public class TestOne : ITestOne 9 { 10 11 } 12 }

1 namespace WebApiFrame 2 { 3 public interface ITestTwo 4 { 5 6 } 7 8 public class TestTwo : ITestTwo 9 { 10 11 } 12 }

1 namespace WebApiFrame 2 { 3 public interface ITestThree 4 { 5 6 } 7 8 public class TestThree : ITestThree 9 { 10 11 } 12 }
修改 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方法時,框架會依據注冊的映射關系生成對應的實例,通過控制器的構造函數參數注入到控制器中,並創建控制器實例。
三、構造函數的選擇
上一個例子展示了在.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 異常。
四、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 }

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 }
修改 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 ,會得到上個例子同樣的效果。