系列目錄
前言
本來計划是五篇文章的,每章發個半小時隨便翻翻就能懂,但是第一篇發了之后,我發現.NET環境下很多人對IoC和DI都很排斥,搞得評論區異常熱鬧。
同一個東西,在Java下和在.NET下能有這么大的差異,也是挺有意思的一件事情。
所以我就把剩下四篇內容精簡再精簡,合成一篇了,權當是寫給自己的一個備忘記錄了。
GitHub源碼地址:https://github.com/WangRui321/Ray.EssayNotes.AutoFac
源碼是一個虛構的項目框架,類似於樣例性質的代碼或者測試程序,里面很多注釋,對理解DI,或怎么在MVC、WebApi和Core Api分別實現依賴注入有很好的幫助效果。
所以,以下內容,配合源碼食用效果更佳~
第一部分:詳解AutoFac用法
名詞解釋
老規矩,理論先行。
組件(Components)
一串聲明了它所提供服務和它所消費依賴的代碼。
可以理解為容器內的基本單元,一個容器內會被注冊很多個組件,每個組件都有自己的信息:比如暴露的服務類型、生命周期域、綁定的具象對象等。
服務(Services)
一個在提供和消費組件之間明確定義的行為約定。
和項目中的xxxService不同,AutoFac的服務是對容器而言的,可以簡單的理解為上一章講的組件的暴露類型
(即對外開放的服務類型),也就是As方法里的東西:
builder.RegisterType<CallLogger>()
.As<ILogger>()
.As<ICallInterceptor>();
這里,針對同一個注冊對象(CallLogger),容器就對外暴露了兩個服務(service),ILogger服務和ICallInterceptor服務。
生命周期作用域(LifeTimeScope)
- 生命周期
指服務實例在你的應用中存在的時長:從開始實例化到最后釋放結束。
- 作用域
指它在應用中能共享給其他組件並被消費的作用域。例如, 應用中有個全局的靜態單例,那么該全局對象實例的 "作用域" 將會是整個應用。
- 生命周期作用域
其實是把這兩個概念組合在了一起, 可以理解為應用中的一個工作單元。后面詳細講。
怎么理解它們的關系
容器是一個自動售貨機
,組件是放在里面的在售商品
,服務是商品的出售名稱
。
把商品(項目里的具象對象)放入自動售貨機(容器)上架的過程叫注冊
;
注冊的時候會給商品貼上標簽,標注該商品的名稱,這個名稱就叫服務
;
我們還可以標注這個商品的適用人群和過期時間等(生命周期作用域
);
把這個包裝后的商品放入自動售貨機后,它就變成了在售商品(組件
)。
當有顧客需要某個商品時,他只要對着售貨機報一個商品名(服務
名),自動售貨機找到對應商品,拋出給客戶,這個拋給你的過程,就叫做注入
你;
而且這個售貨機比較智能,拋出前還可以先判斷商品是不是過期了,該不該拋給你。
注冊組件
即在容器初始化時,向容器內添加對象的操作。AutoFac封裝了以下幾種便捷的注冊方法:
反射注冊
直接指定注入對象與暴露類型,使用RegisterType<T>()
或者RegisterType(typeof(T))
方法:
builder.RegisterType<StudentRepository>()
.As<IStudentRepository>();
builder.RegisterType(typeof(StudentService))
.As(typeof(IStudentService));
實例注冊
將實例注冊到容器,使用RegisterInstance()
方法,通常有兩種:
- new出一個對象注冊:
var output = new StringWriter();
builder.RegisterInstance(output).As<TextWriter>();
- 注冊項目已存在單例:
builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();
Lambda表達式注冊
builder.Register(x => new StudentRepository())
.As<IStudentRepository>();
builder.Register(x => new StudentService(x.Resolve<IStudentRepository>()))
.As<IStudentService>();
利用拉姆達注冊可以實現一些常規反射無法實現的操作,比如一些復雜參數注冊。
泛型注冊
最常見的就是泛型倉儲的注冊:
builder.RegisterGeneric(typeof(BaseRepository<>))
.As(typeof(IBaseRepository<>))
.InstancePerLifetimeScope();
條件注冊
通過加上判斷條件,來決定是否執行該條注冊語句。
- IfNotRegistered
表示:如果沒注冊過xxx,就執行語句:
builder.RegisterType<TeacherRepository>()
.AsSelf()
.IfNotRegistered(typeof(ITeacherRepository));
只有當ITeacherRepository服務類型沒有被注冊過,才會執行該條注冊語句。
- OnlyIf
表示:只有...,才會執行語句:
builder.RegisterType<TeacherService>()
.AsSelf()
.As<ITeacherService>()
.OnlyIf(x =>
x.IsRegistered(new TypedService(typeof(ITeacherRepository)))||
x.IsRegistered(new TypedService(typeof(TeacherRepository))));
只有當ITeacherRepository服務類型或者TeacherRepository服務類型被注冊過,才會執行該條注冊語句。
程序集批量注冊
最常用,也最實用的一個注冊方法,使用該方法最好要懂點反射
的知識。
/// <summary>
/// 通過反射程序集批量注冊
/// </summary>
/// <param name="builder"></param>
public static void BuildContainerFunc8(ContainerBuilder builder)
{
Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssemblies();
builder.RegisterAssemblyTypes(assemblies)//程序集內所有具象類(concrete classes)
.Where(cc =>cc.Name.EndsWith("Repository")|//篩選
cc.Name.EndsWith("Service"))
.PublicOnly()//只要public訪問權限的
.Where(cc=>cc.IsClass)//只要class型(主要為了排除值和interface類型)
//.Except<TeacherRepository>()//排除某類型
//.As(x=>x.GetInterfaces()[0])//反射出其實現的接口,默認以第一個接口類型暴露
.AsImplementedInterfaces();//自動以其實現的所有接口類型暴露(包括IDisposable接口)
builder.RegisterGeneric(typeof(BaseRepository<>))
.As(typeof(IBaseRepository<>));
}
如上會批量注冊項目中所有的Repository和Service。
屬性注入
講屬性注入之前,要先看下構造注入。
- 構造注入
即解析的時候,利用構造函數注入,形式如下:
/// <summary>
/// 學生邏輯處理
/// </summary>
public class StudentService : IStudentService
{
private readonly IStudentRepository _studentRepository;
/// <summary>
/// 構造注入
/// </summary>
/// <param name="studentRepository"></param>
public StudentService(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
}
在構造函數的參數中直接寫入服務類型,AutoFac解析該類時,就會去容器內部已存在的組件中查找,然后將匹配的對象注入到構造函數中去。
- 屬性注入
屬性注入與構造注入不同,是將容器內對應的組件直接注入到類內的屬性中去,形式如下:
/// <summary>
/// 教師邏輯處理
/// </summary>
public class TeacherService : ITeacherService
{
/// <summary>
/// 用於屬性注入
/// </summary>
public ITeacherRepository TeacherRepository { get; set; }
public string GetTeacherName(long id)
{
return TeacherRepository?.Get(111).Name;
}
}
要使用這種屬性注入,在注冊該屬性所屬類的時候,需要使用PropertiesAutowired()
方法額外標注,如下:
builder.RegisterType<TeacherService>().PropertiesAutowired();
這樣,容器在解析並實例化TeacherService類時,便會將容器內的組件與類內的屬性做映射,如果相同則自動將組件注入到類內屬性種。
- 注意
屬性注入爭議性很大,很多人稱這是一種_反模式_,事實也確實如此。
使用屬性注入會讓代碼可讀性變得極其復雜(而復雜難懂的代碼一定不是好的代碼,不管用的技術有多高大上)。
但是屬性注入也不是一無是處,因為屬性注入有一個特性:
在構造注入的時候,如果構造函數的參數中有一個對象在容器不存在,那么解析就會報錯。
但是屬性注入就不一樣了,當容器內沒有與該屬性類型對應的組件時,這時解析不會報異常,只會讓這個屬性保持為空類型(null)。
利用這個特性,可以實現一些特殊的操作。
暴露服務
即上面提到的As<xxx>()
函數,AutoFac提供了以下三種標注暴露服務類型的方法:
以其自身類型暴露服務
使用AsSelf()
方法標識,表示以其自身類型暴露,也是當沒有標注暴露服務的時候的默認選項。
如下四種寫法是等效的:
builder.RegisterType<StudentService>();//不標注,默認以自身類型暴露服務
builder.RegisterType<StudentService>().AsSelf();
builder.RegisterType<StudentService>().As<StudentService>();
builder.RegisterType<StudentService>().As(typeof(StudentService));
以其實現的接口(interface)暴露服務
使用As()
方法標識,暴露的類型可以是多個,比如CallLogger類實現了ILogger接口和ICallInterceptor接口,那么可以這么寫:
builder.RegisterType<CallLogger>()
.As<ILogger>()
.As<ICallInterceptor>()
.AsSelf();
程序集批量注冊時指定暴露類型
- 方法1:自己指定
public static void BuildContainerFunc8(ContainerBuilder builder)
{
Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssemblies();
builder.RegisterAssemblyTypes(assemblies)//程序集內所有具象類(concrete classes)
.Where(cc =>cc.Name.EndsWith("Repository")|//篩選
cc.Name.EndsWith("Service"))
.As(x=>x.GetInterfaces()[0])//反射出其實現的接口,並指定以其實現的第一個接口類型暴露
}
- 方法2:以其實現的所有接口類型暴露
使用AsImplementedInterfaces()
函數實現,相當於一個類實現了幾個接口(interface)就會暴露出幾個服務,等價於上面連寫多個As()的作用。
public static void BuildContainerFunc8(ContainerBuilder builder)
{
Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssemblies();
builder.RegisterAssemblyTypes(assemblies)//程序集內所有具象類(concrete classes)
.Where(cc =>cc.Name.EndsWith("Repository")|//篩選
cc.Name.EndsWith("Service"))
.AsImplementedInterfaces();//自動以其實現的所有接口類型暴露(包括IDisposable接口)
}
生命周期作用域
相當於UnitWork(工作單元)的概念。下面羅列出了AutoFac與.NET Core的生命周期作用域,並作了簡要的對比。
AutoFac的生命周期作用域
下面講下AutoFac定義的幾種生命周期作用域,上一篇評論里也有人提了,關於生命周期作用域這塊確實不是很好理解,所以下面每中類型我都寫了一個例子程序,這些例子程序對理解很有幫助,只要能讀懂這些例子程序,就一定能弄懂這些生命周期作用域。(例子項目源碼里都有,可以去試着實際運行下,更易理解)
瞬時單例(Instance Per Dependency)
也叫每個依賴一個實例。
即每次從容器里拿出來的都是全新對象,相當於每次都new出一個。
在其他容器中也被標識為 'Transient'(瞬時) 或 'Factory'(工廠)。
- 注冊
使用InstancePerDependency()
方法標注,如果不標注,這也是默認的選項。以下兩種注冊方法是等效的:
//不指定,默認就是瞬時的
builder.RegisterType<Model.StudentEntity>();
//指定其生命周期域為瞬時
builder.RegisterType<Model.StudentEntity>().InstancePerDependency();
- 解析:
using (var scope = Container.Instance.BeginLifetimeScope())
{
var stu1 = scope.Resolve<Model.StudentEntity>();
Console.WriteLine($"第1次打印:{stu1.Name}");
stu1.Name = "張三";
Console.WriteLine($"第2次打印:{stu1.Name}");
var stu2 = scope.Resolve<Model.StudentEntity>();
Console.WriteLine($"第2次打印:{stu2.Name}");
}
上面解析了2次,有兩個實例,stu1和stu2指向不同的兩塊內存,彼此之間沒有關系。
打印結果:
全局單例(Single Instance)
即全局只有一個實例,在根容器和所有嵌套作用域內,每次解析返回的都是同一個實例。
- 注冊
使用SingleInstance()
方法標識:
builder.RegisterType<Model.StudentEntity>().SingleInstance();
- 解析:
//直接從根域內解析(單例下可以使用,其他不建議這樣直接從根域內解析)
var stu1 = Container.Instance.Resolve<Model.StudentEntity>();
stu1.Name = "張三";
Console.WriteLine($"第1次打印:{stu1.Name}");
using (var scope1 = Container.Instance.BeginLifetimeScope())
{
var stu2 = scope1.Resolve<Model.StudentEntity>();
Console.WriteLine($"第2次打印:{stu2.Name}");
stu1.Name = "李四";
}
using (var scope2 = Container.Instance.BeginLifetimeScope())
{
var stu3 = scope2.Resolve<Model.StudentEntity>();
Console.WriteLine($"第3次打印:{stu3.Name}");
}
上面的stu1、stu2、stu3都是同一個實例,在內存上它們指向同一個內存塊。
打印結果:
域內單例(Instance Per Lifetime Scope)
即在每個生命周期域內是單例的。
- 注冊
使用InstancePerLifetimeScope()
方法標識:
x.RegisterType<Model.StudentEntity>().InstancePerLifetimeScope();
- 解析
//子域一
using (var scope1 = Container.Instance.BeginLifetimeScope())
{
var stu1 = scope1.Resolve<Model.StudentEntity>();
Console.WriteLine($"第1次打印:{stu1.Name}");
stu1.Name = "張三";
var stu2 = scope1.Resolve<Model.StudentEntity>();
Console.WriteLine($"第2次打印:{stu2.Name}");
}
//子域二
using (var scope2 = Container.Instance.BeginLifetimeScope())
{
var stuA = scope2.Resolve<Model.StudentEntity>();
Console.WriteLine($"第3次打印:{stuA.Name}");
stuA.Name = "李四";
var stuB = scope2.Resolve<Model.StudentEntity>();
Console.WriteLine($"第4次打印:{stuB.Name}");
}
如上,在子域一中,雖然解析了2次,但是2次解析出的都是同一個實例,即stu1和stu2指向同一個內存塊Ⅰ。
子域二也一樣,stuA和stuB指向同一個內存塊Ⅱ,但是內存塊Ⅰ和內存塊Ⅱ卻不是同一塊。
打印結果如下,第1次和第3次為null:
匹配域內單例(Instance Per Matching Lifetime Scope)
即每個匹配的
生命周期作用域一個實例。
該域類型其實是上面的“域內單例”的其中一種,不一樣的是它允許我們給域“打標簽”,只要在這個特定的標簽域內就是單例的。
- 注冊
使用InstancePerMatchingLifetimeScope(string tagName)
方法注冊:
builder.RegisterType<Worker>().InstancePerMatchingLifetimeScope("myTag");
- 解析
//myScope標簽子域一
using (var myScope1 = Container.Instance.BeginLifetimeScope("myTag"))
{
var stu1 = myScope1.Resolve<Model.StudentEntity>();
stu1.Name = "張三";
Console.WriteLine($"第1次打印:{stu1.Name}");
var stu2 = myScope1.Resolve<Model.StudentEntity>();
Console.WriteLine($"第2次打印:{stu2.Name}");
//解析了2次,但2次都是同一個實例(stu1和stu2指向同一個內存塊Ⅰ)
}
//myScope標簽子域二
using (var myScope2 = Container.Instance.BeginLifetimeScope("myTag"))
{
var stuA = myScope2.Resolve<Model.StudentEntity>();
Console.WriteLine($"第3次打印:{stuA.Name}");
//因為標簽域內已注冊過,所以可以解析成功
//但是因為和上面不是同一個子域,所以解析出的實例stuA與之前的並不是同一個實例,指向另一個內存塊Ⅱ
}
//無標簽子域三
using (var noTagScope = Container.Instance.BeginLifetimeScope())
{
try
{
var stuOne = noTagScope.Resolve<Model.StudentEntity>();//會報異常
Console.WriteLine($"第4次正常打印:{stuOne.Name}");
}
catch (Exception e)
{
Console.WriteLine($"第4次異常打印:{e.Message}");
}
//因為StudentEntity只被注冊到帶有myScope標簽域內,所以這里解析不到,報異常
}
打印結果:
需要注意:
- 第3次打印為null,不同子域即使標簽相同,但也是不同子域,所以域之間不是同一個實例
- 在其他標簽的域內(包括無標簽域)解析,會報異常
每次請求內單例(Instance Per Request)
該種類型適用於“request”類型的應用,比如MVC和WebApi。
其實質其實又是上一種的“指定域內單例”的一種特殊情況:AutoFac內有一個靜態字符串叫Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag
,其值為"AutofacWebRequest"
,當“指定域內單例”打的標簽是這個字符串時,那它就是“每次請求內單例”了。
- 注冊
使用InstancePerRequest()
方法標注:
builder.RegisterType<Model.StudentEntity>().InstancePerRequest();
也可以使用上面的域內單例的注冊法(但是不建議):
//使用靜態字符串標記
builder.RegisterType<Model.StudentEntity>().InstancePerMatchingLifetimeScope(Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag);
//或者直接寫明字符串
builder.RegisterType<Model.StudentEntity>().InstancePerMatchingLifetimeScope("AutofacWebRequest");
這里用控制台程序不好舉例子就不寫解析代碼了,要理解“每次請求內單例”的作用,最好的例子就是EF中的DBContext,我們在一次request請求內,即使是用到了多個Service和多個Repository,也只需要一個數據庫實例,這樣即能減少數據庫實例初始化的消耗,還能實現事務的功能。
.NET Core的生命周期作用域(Service lifetimes)
相比於AutoFac的豐富復雜,.NET Core就比較簡單粗暴了,只要3種類型:
瞬時實例(Transient)
與AutoFac的瞬時實例(Instance Per Dependency)相同,每次都是全新的實例。
使用AddTransient()
注冊:
services.AddTransient<IStudentService, StudentService>();
請求內單例(Scoped)
其意義與AutoFac的請求內單例(Instance Per Request)相同,但實際如果真正在.NET Core中使用使用AutoFac的話,應該使用AutoFac的域內單例(Instance Per LifetimeScope)來代替。
原因是.NET Core框架自帶的DI(Microsoft.Extensions.DependencyInjection
)全權接管了請求和生命周期作用域的創建,所以AutoFac無法控制,但是使用域內單例(Instance Per LifetimeScope)可以實現相同的效果。
使用AddScoped()
注冊:
services.AddScoped<IStudentService, StudentService>();
單例(Singleton)
與AutoFac的單例(Single Instance)相同。
使用AddSingleton();
注冊:
services.AddSingleton<StudentEntity>();
第二部分:.NET Framework Web程序AutoFac注入
MVC項目
思路很簡單,三步走:
-
新建AutoFac容器
-
初始化容器,向容器注冊所有需要的依賴對象
-
將AutoFac解析器設置為系統的依賴解析器(Dependency Resolver)
MVC容器
除了AutoFac主包之外,還需要Nuget導入AutoFac.Mvc5包:
容器代碼:
using System;
using System.Linq;
using System.Reflection;
//
using Autofac;
using Autofac.Integration.Mvc;
//
using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;
namespace Ray.EssayNotes.AutoFac.Infrastructure.Ioc
{
/// <summary>
/// .net framework MVC程序容器
/// </summary>
public static class MvcContainer
{
public static IContainer Instance;
/// <summary>
/// 初始化MVC容器
/// </summary>
/// <param name="func"></param>
/// <returns></returns>
public static System.Web.Mvc.IDependencyResolver Init(Func<ContainerBuilder, ContainerBuilder> func = null)
{
//新建容器構建器,用於注冊組件和服務
var builder = new ContainerBuilder();
//注冊組件
MyBuild(builder);
func?.Invoke(builder);
//利用構建器創建容器
Instance = builder.Build();
//返回針對MVC的AutoFac解析器
return new AutofacDependencyResolver(Instance);
}
public static void MyBuild(ContainerBuilder builder)
{
Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssembliesWeb();
//注冊倉儲 && Service
builder.RegisterAssemblyTypes(assemblies)//程序集內所有具象類(concrete classes)
.Where(cc => cc.Name.EndsWith("Repository") |//篩選
cc.Name.EndsWith("Service"))
.PublicOnly()//只要public訪問權限的
.Where(cc => cc.IsClass)//只要class型(主要為了排除值和interface類型)
.AsImplementedInterfaces();//自動以其實現的所有接口類型暴露(包括IDisposable接口)
//注冊泛型倉儲
builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>));
//注冊Controller
//方法1:自己根據反射注冊
//builder.RegisterAssemblyTypes(assemblies)
// .Where(cc => cc.Name.EndsWith("Controller"))
// .AsSelf();
//方法2:用AutoFac提供的專門用於注冊MvcController的擴展方法
Assembly mvcAssembly = assemblies.FirstOrDefault(x => x.FullName.Contains(".NetFrameworkMvc"));
builder.RegisterControllers(mvcAssembly);
}
}
}
這里Init()
初始化函數返回類型變成了System.Web.Mvc.IDependencyResolver
接口,即MVC的系統依賴解析器。
AutoFac自己封裝了一個AutofacDependencyResolver
類(AutoFac依賴解析器類)實現了這個接口,所以直接new一個AutofacDependencyResolver類返回,等下把這個AutoFac依賴解析器類設置為MVC的系統依賴解析器就可以了。
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Web.Mvc;
namespace Autofac.Integration.Mvc
{
/// <summary>
/// Autofac implementation of the <see cref="T:System.Web.Mvc.IDependencyResolver" /> interface.
/// </summary>
public class AutofacDependencyResolver : IDependencyResolver
{
//內部實現
//......
}
項目主程序:
- Global.asax啟動項
啟動時初始化容器,並把AutoFac生成的解析器設置為系統依賴解析器:
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
//
using Ray.EssayNotes.AutoFac.Infrastructure.Ioc;
namespace Ray.EssayNotes.AutoFac.NetFrameworkMvc
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
//初始化容器,並返回適用於MVC的AutoFac解析器
System.Web.Mvc.IDependencyResolver autoFacResolver = MvcContainer.Init();
//將AutoFac解析器設置為系統DI解析器
DependencyResolver.SetResolver(autoFacResolver);
}
}
}
其中DependencyResolver.SetResolver()
為MVC封裝的一個靜態方法,用於設置MVC的依賴解析器。
其參數只要是實現了System.Web.Mvc.IDependencyResolver
接口的對象都可以,AutoFac自己封裝的解析器AutofacDependencyResolver
類實現了這個接口,所以可以傳進來,從而實現了讓AutoFac接管MVC的依賴注入。
- 學生控制器:
直接利用構造注入就可以了:
using System.Web.Mvc;
//
using Ray.EssayNotes.AutoFac.Service.IService;
namespace Ray.EssayNotes.AutoFac.NetFrameworkMvc.Controllers
{
/// <summary>
/// 學生Api
/// </summary>
public class StudentController : Controller
{
private readonly IStudentService _studentService;
/// <summary>
/// 構造注入
/// </summary>
/// <param name="studentService"></param>
public StudentController(IStudentService studentService)
{
_studentService = studentService;
}
/// <summary>
/// 獲取學生姓名
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public string GetStuNameById(long id)
{
return _studentService.GetStuName(id);
}
}
}
運行調用Api
WebApi項目
和MVC一樣,思路很簡單,三步走:
-
新建AutoFac容器
-
初始化容器,向容器注冊所有需要的依賴對象
-
將AutoFac解析器設置為系統的依賴解析器(Dependency Resolver)
Api容器
除了AutoFac主包之外,還需要Nuget導入AutoFac.WebApi2包:
容器代碼:
using System;
using System.Linq;
using System.Reflection;
//
using Autofac;
using Autofac.Integration.WebApi;
//
using Ray.EssayNotes.AutoFac.Repository.Repository;
using Ray.EssayNotes.AutoFac.Repository.IRepository;
namespace Ray.EssayNotes.AutoFac.Infrastructure.Ioc
{
/// <summary>
/// .NET Framework WebApi容器
/// </summary>
public static class ApiContainer
{
public static IContainer Instance;
/// <summary>
/// 初始化Api容器
/// </summary>
/// <param name="func"></param>
public static System.Web.Http.Dependencies.IDependencyResolver Init(Func<ContainerBuilder, ContainerBuilder> func = null)
{
//新建容器構建器,用於注冊組件和服務
var builder = new ContainerBuilder();
//注冊組件
MyBuild(builder);
func?.Invoke(builder);
//利用構建器創建容器
Instance = builder.Build();
//返回針對WebApi的AutoFac解析器
return new AutofacWebApiDependencyResolver(Instance);
}
public static void MyBuild(ContainerBuilder builder)
{
var assemblies = Helpers.ReflectionHelper.GetAllAssembliesWeb();
//注冊倉儲 && Service
builder.RegisterAssemblyTypes(assemblies)//程序集內所有具象類(concrete classes)
.Where(cc => cc.Name.EndsWith("Repository") |//篩選
cc.Name.EndsWith("Service"))
.PublicOnly()//只要public訪問權限的
.Where(cc => cc.IsClass)//只要class型(主要為了排除值和interface類型)
.AsImplementedInterfaces();//自動以其實現的所有接口類型暴露(包括IDisposable接口)
//注冊泛型倉儲
builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>));
//注冊ApiController
//方法1:自己根據反射注冊
//Assembly[] controllerAssemblies = assemblies.Where(x => x.FullName.Contains(".NetFrameworkApi")).ToArray();
//builder.RegisterAssemblyTypes(controllerAssemblies)
// .Where(cc => cc.Name.EndsWith("Controller"))
// .AsSelf();
//方法2:用AutoFac提供的專門用於注冊ApiController的擴展方法
Assembly mvcAssembly = assemblies.FirstOrDefault(x => x.FullName.Contains(".NetFrameworkApi"));
builder.RegisterApiControllers(mvcAssembly);
}
}
}
這里Init()
初始化函數返回類型變成了System.Web.Http.Dependencies.IDependencyResolver
接口,即WebApi的系統依賴解析器。
AutoFac自己封裝了一個AutofacWebApiDependencyResolver
類(AutoFac針對WebApi的依賴解析器類)實現了這個接口,所以直接new一個AutofacWebApiDependencyResolver類返回,等下把這個AutoFac依賴解析器類設置為WebApi的系統依賴解析器就可以了。
WebApi主程序
- Global.asax啟動項
在項目啟動時初始化容器:
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
//
using Ray.EssayNotes.AutoFac.Infrastructure.Ioc;
namespace Ray.EssayNotes.AutoFac.NetFrameworkApi
{
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
//初始化容器,並返回適用於WebApi的AutoFac解析器
System.Web.Http.Dependencies.IDependencyResolver autoFacResolver = ApiContainer.Init();
//獲取HttpConfiguration
HttpConfiguration config = GlobalConfiguration.Configuration;
//將AutoFac解析器設置為系統DI解析器
config.DependencyResolver = autoFacResolver;
}
}
}
這里跟上面的MVC項目不太一樣,是通過HttpConfiguration對象來設置依賴解析器的,但是原理相同,不贅述了。
- 學生控制器:
直接利用構造函數注入即可:
using System.Web.Http;
//
using Ray.EssayNotes.AutoFac.Service.IService;
namespace Ray.EssayNotes.AutoFac.NetFrameworkApi.Controllers
{
/// <summary>
/// 學生Api
/// </summary>
public class StudentController : ApiController
{
private readonly IStudentService _studentService;
/// <summary>
/// 構造注入
/// </summary>
/// <param name="studentService"></param>
public StudentController(IStudentService studentService)
{
_studentService = studentService;
}
/// <summary>
/// 獲取學生姓名
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Route("Student/GetStuNameById")]
public string GetStuNameById(long id)
{
return _studentService.GetStuName(123);
}
}
}
運行調用接口
第三部分:.NET Core的DI
自帶的DI
與.NET Framework不同,.NET Core把DI提到了非常重要的位置,其框架本身就集成了一套DI容器。
針對其自帶DI,主要理解兩個對象,IServiceCollection和 IServiceProvider。
- IServiceCollection
用於向容器注冊服務,可以和AutoFac的ContainerBuilder(容器構建器)類比。
- IServiceProvider
負責從容器中向外部提供實例,可以和AutoFac的解析的概念類比。
注冊的地方就在主程序下的startup類中。
但是其本身的注冊語法並沒有AutoFac那么豐富,泛型注冊、批量注冊這些全都沒有,只有下面這種最基礎的一個一個注冊的形式:
using System.Linq;
using System.Reflection;
//
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Extensions;
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Helpers;
namespace Ray.EssayNotes.AutoFac.CoreApi
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//注冊
//自定義注冊
//注冊倉儲
services.AddScoped<ITeacherRepository, TeacherRepository>();
services.AddScoped<IStudentRepository, StudentRepository>();
services.AddScoped<IBaseRepository<StudentEntity>, BaseRepository<StudentEntity>>();
services.AddScoped<IBaseRepository<TeacherEntity>, BaseRepository<TeacherEntity>>();
services.AddScoped<IBaseRepository<BookEntity>, BaseRepository<BookEntity>>();
//注冊Service
services.AddScoped<IStudentService, StudentService>();
services.AddScoped<ITeacherService, TeacherService>();
services.AddScoped<IBookService, BookService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
所以,大家通常都會自己去擴展這些注冊方法,以實現一些和AutoFac一樣的便捷的注冊操作,下面我根據反射寫了一個小擴展,寫的比較簡單潦草,可以參考下:
擴展
擴展代碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
//
using Microsoft.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Model;
using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;
using Ray.EssayNotes.AutoFac.Service.IService;
using Ray.EssayNotes.AutoFac.Service.Service;
namespace Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Extensions
{
/// <summary>
/// asp.net core注冊擴展
/// </summary>
public static class RegisterExtension
{
/// <summary>
/// 反射批量注冊
/// </summary>
/// <param name="services"></param>
/// <param name="assembly"></param>
/// <param name="serviceLifetime"></param>
/// <returns></returns>
public static IServiceCollection AddAssemblyServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)
{
var typeList = new List<Type>();//所有符合注冊條件的類集合
//篩選當前程序集下符合條件的類
List<Type> types = assembly.GetTypes().
Where(t => t.IsClass && !t.IsGenericType)//排除了泛型類
.ToList();
typeList.AddRange(types);
if (!typeList.Any()) return services;
var typeDic = new Dictionary<Type, Type[]>(); //待注冊集合<class,interface>
foreach (var type in typeList)
{
var interfaces = type.GetInterfaces(); //獲取接口
typeDic.Add(type, interfaces);
}
//循環實現類
foreach (var instanceType in typeDic.Keys)
{
Type[] interfaceTypeList = typeDic[instanceType];
if (interfaceTypeList == null)//如果該類沒有實現接口,則以其本身類型注冊
{
services.AddServiceWithLifeScoped(null, instanceType, serviceLifetime);
}
else//如果該類有實現接口,則循環其實現的接口,一一配對注冊
{
foreach (var interfaceType in interfaceTypeList)
{
services.AddServiceWithLifeScoped(interfaceType, instanceType, serviceLifetime);
}
}
}
return services;
}
/// <summary>
/// 暴露類型可空注冊
/// (如果暴露類型為null,則自動以其本身類型注冊)
/// </summary>
/// <param name="services"></param>
/// <param name="interfaceType"></param>
/// <param name="instanceType"></param>
/// <param name="serviceLifetime"></param>
private static void AddServiceWithLifeScoped(this IServiceCollection services, Type interfaceType, Type instanceType, ServiceLifetime serviceLifetime)
{
switch (serviceLifetime)
{
case ServiceLifetime.Scoped:
if (interfaceType == null) services.AddScoped(instanceType);
else services.AddScoped(interfaceType, instanceType);
break;
case ServiceLifetime.Singleton:
if (interfaceType == null) services.AddSingleton(instanceType);
else services.AddSingleton(interfaceType, instanceType);
break;
case ServiceLifetime.Transient:
if (interfaceType == null) services.AddTransient(instanceType);
else services.AddTransient(interfaceType, instanceType);
break;
default:
throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, null);
}
}
}
}
利用這個擴展,我們在startup里就可以用類似AutoFac的語法來注冊了。
主程序
注冊代碼:
using System.Linq;
using System.Reflection;
//
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Extensions;
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Helpers;
namespace Ray.EssayNotes.AutoFac.CoreApi
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//注冊
//自定義批量注冊
Assembly[] assemblies = ReflectionHelper.GetAllAssembliesCoreWeb();
//注冊repository
Assembly repositoryAssemblies = assemblies.FirstOrDefault(x => x.FullName.Contains(".Repository"));
services.AddAssemblyServices(repositoryAssemblies);
//注冊service
Assembly serviceAssemblies = assemblies.FirstOrDefault(x => x.FullName.Contains(".Service"));
services.AddAssemblyServices(serviceAssemblies);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
其實AutoFac針對.NET Core已經幫我們集成了一套注冊的擴展,我們可以通過兩種方式把AutoFac引入.NET Core:一種是將AutoFac容器作為輔助容器,與.NET Core的DI共存,我們可以同時向兩個容器里注冊組件;一種是讓AutoFac容器接管.NET Core的DI,注冊時只選擇往Autofac容器中注冊。
下面就分別實現下這兩種引入AutoFac的方式。
AutoFac作為輔助注冊
Core容器
先按照之前寫AutoFac容器的方法,新建一個針對Core的AutoFac容器:
using System;
//
using Microsoft.Extensions.DependencyInjection;
//
using Autofac;
using Autofac.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;
namespace Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc
{
/// <summary>
/// Core的AutoFac容器
/// </summary>
public static class CoreContainer
{
/// <summary>
/// 容器實例
/// </summary>
public static IContainer Instance;
/// <summary>
/// 初始化容器
/// </summary>
/// <param name="services"></param>
/// <param name="func"></param>
/// <returns></returns>
public static IServiceProvider Init(IServiceCollection services, Func<ContainerBuilder, ContainerBuilder> func = null)
{
//新建容器構建器,用於注冊組件和服務
var builder = new ContainerBuilder();
//將Core自帶DI容器內的服務遷移到AutoFac容器
builder.Populate(services);
//自定義注冊組件
MyBuild(builder);
func?.Invoke(builder);
//利用構建器創建容器
Instance = builder.Build();
return new AutofacServiceProvider(Instance);
}
/// <summary>
/// 自定義注冊
/// </summary>
/// <param name="builder"></param>
public static void MyBuild(this ContainerBuilder builder)
{
var assemblies = Helpers.ReflectionHelper.GetAllAssembliesCoreWeb();
//注冊倉儲 && Service
builder.RegisterAssemblyTypes(assemblies)
.Where(cc => cc.Name.EndsWith("Repository") |//篩選
cc.Name.EndsWith("Service"))
.PublicOnly()//只要public訪問權限的
.Where(cc => cc.IsClass)//只要class型(主要為了排除值和interface類型)
.AsImplementedInterfaces();//自動以其實現的所有接口類型暴露(包括IDisposable接口)
//注冊泛型倉儲
builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>));
/*
//注冊Controller
Assembly[] controllerAssemblies = assemblies.Where(x => x.FullName.Contains(".CoreApi")).ToArray();
builder.RegisterAssemblyTypes(controllerAssemblies)
.Where(cc => cc.Name.EndsWith("Controller"))
.AsSelf();
*/
}
}
}
主程序
在主程序中新建一個StartupWithAutoFac類,用於注冊。
StartupWithAutoFac代碼:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//
using Autofac;
//
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc;
namespace Ray.EssayNotes.AutoFac.CoreApi
{
public class StartupWithAutoFac
{
public IConfiguration Configuration { get; }
public StartupWithAutoFac(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
/// <summary>
/// 利用該方法可以使用AutoFac輔助注冊,該方法在ConfigureServices()之后執行,所以當發生覆蓋注冊時,以后者為准。
/// 不要再利用構建器去創建AutoFac容器了,系統已經接管了。
/// </summary>
/// <param name="builder"></param>
public void ConfigureContainer(ContainerBuilder builder)
{
builder.MyBuild();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
這里其他地方與原startup都相同,只是多了一個ConfigureContainer()方法,在該方法內可以按照AutoFac的語法進行自由注冊。
然后修改program類,將AutoFac hook進管道,並將StartupWithAutoFac類指定為注冊入口:
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace Ray.EssayNotes.AutoFac.CoreApi
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
//第一種:使用自帶DI
//.UseStartup<Startup>();
//第二種:添加AutoFac作為輔助容器
.HookAutoFacIntoPipeline()
.UseStartup<StartupWithAutoFac>();
//第三種:添加AutoFac接管依賴注入
//.UseStartup<StartupOnlyAutoFac>();
}
}
AutoFac接管注冊
容器
還是上面的CoreContainer容器。
主程序
主程序新建一個StartupOnlyAutoFac類,
代碼如下:
using System;
//
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc;
namespace Ray.EssayNotes.AutoFac.CoreApi
{
public class StartupOnlyAutoFac
{
public IConfiguration Configuration { get; }
public StartupOnlyAutoFac(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
return CoreContainer.Init(services);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
這里直接改了ConfigureServices()
方法的返回類型,然后在該方法內直接利用AutoFac注冊。
最后當然也要更改下program類,指定StartupOnlyAutoFac類為注冊入口。
代碼:
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace Ray.EssayNotes.AutoFac.CoreApi
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
//第一種:使用自帶DI
//.UseStartup<Startup>();
//第二種:添加AutoFac作為輔助容器
//.HookAutoFacIntoPipeline()
//.UseStartup<StartupWithAutoFac>();
//第三種:添加AutoFac接管依賴注入
.UseStartup<StartupOnlyAutoFac>();
}
}
運行調用
- StudentController
using Microsoft.AspNetCore.Mvc;
//
using Ray.EssayNotes.AutoFac.Service.IService;
namespace Ray.EssayNotes.AutoFac.CoreApi.Controllers
{
[ApiController]
public class StudentController : ControllerBase
{
private readonly IStudentService _studentService;
public StudentController(IStudentService studentService)
{
_studentService = studentService;
}
[Route("Student/GetStuNameById")]
public string GetStuNameById(long id)
{
return _studentService.GetStuName(id);
}
}
}
- 調用