這篇會介紹一下Unity的一些配置和做法,但不會特別介紹什麼叫做Dependency Injection是什麼,如果想了解何謂di請另外自行爬文
前言
其實早在三年前筆者就有用di來方便注入,並且方便的stub做單元測試,以前用的都是autofac,雖然也曾經研究過使用unity的做法,但在那個時期unity對web api的支援比autofac比較慢,所以筆者就一直使用autofac,但因為目前的專案上面決定使用unity的方式,所以我就研究了一下包括如何用code的方式來配置,甚至是用xml的方式來配置,還有自動配置的方式一些紀錄,首先看一下我的專案配置,假設我有一個web api,然後參考著另一個專案是service,如圖示例。
導覽
- 安裝必要的package,並以xml的方式來實做
- 把注入的部份,改成Lazy loading的方式
- 以code的方式來實做
- 不用建構子去注入
- 使用字串來分成不同注入類別
- 使用自動解析的方式
- 使用屬性解析的方式
- 實做interceptor
- 使用policy來實做attribute
- 結論
安裝必要的package,並以xml的方式來實做
使用xml的優點是不需要重新compiler就可以把實做類別給換掉,而且速度也很快,但是換來的代價則是維護困難,實做囉嗦所以視專案團隊需要,換言之就是說如果我們用xml的方式,如果要把實做換掉,只要在iis上面改一下web.config的配置就好了,就不需要改完程式碼還要重新上版,首先就安裝一下unity和unity web api的部份囉。
接著就到App_Start新增UnityConfig.cs的配置吧
public static class UnityConfig
{
public static void RegisterComponents()
{
var container = new UnityContainer();
container.LoadConfiguration();
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
}
}
接著是到Global.asax裡面,註冊unity到生命週期裡面
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
UnityConfig.RegisterComponents(); //新增加此行去註冊Unity
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
接著到Web.config新增關於Unity配置的部份
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
<!--如果要使用xml去配置unity的話,下面這行就必須要加入-->
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration" />
</configSections>
<!--下面的區段則是配置Unity的部份-->
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension,Microsoft.Practices.Unity.Interception.Configuration" />
<!--下面是命名空間的部份-->
<assembly name="BookStore.Service" />
<namespace name="BookStore.Service" />
<container>
<!--下面則是介面和實做類別的對應-->
<register type="IMessageService" mapTo="MessageService" />
</container>
</unity>
接著來看一下IMessageService和MessageService吧,其實很簡單的程式碼,如下示例
public interface IMessageService
{
string Get();
void Set(string message);
}
public class MessageService : IMessageService
{
private string _hello="hello";
public string Get()
{
return _hello;
}
public void Set(string message)
{
_hello = message;
}
}
然後我們直接簡單的在web api ,新增一個action來實測吧
public class ValueController : ApiController
{
IMessageService _messageService;
public ValueController(IMessageService messageService)
{
_messageService = messageService;
}
public IHttpActionResult Get()
{
return Ok(_messageService.Get());
}
}
結果如圖示例
把注入的部份,改成Lazy loading的方式
原本我們是使用直接注入的方式,但是如果我們注入了十個介面,然後我們可能在呼叫方法的時候,只有使用到一個實做類別,但是預設也一樣要把十個介面都實做出來,這樣子的方式就非常不好,所以我們一樣很簡單的方式,來把原本直接注入改成Lazy Loading的方式吧,使用DI來實做比我們直接寫一個類別還要更簡單。
public class ValueController : ApiController
{
Lazy<IMessageService> _messageService;
public ValueController(Lazy<IMessageService> messageService)
{
_messageService = messageService;
}
public IHttpActionResult Get()
{
return Ok(_messageService.Value.Get());
}
}
以code的方式來實做
其實建議是用code來實做,除了擁有強型別的優點,更好的是配置的方式也比xml更乾淨更簡單,但是請先把之前web.config設定的unity的部份都刪除乾淨,就不必保留了,看一下關於UnityConfig的部份,只要改成如下就ok了,web.config完全不需要動
public static void RegisterComponents()
{
var container = new UnityContainer();
RegistAll(container);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
}
private static void RegistAll(UnityContainer container)
{
container.RegisterType<IMessageService, MessageService>();
}
其實unity可以用Resolve的方式來注入類別,但是我們需要得到unityConfig的Unity類別,所以我把程式碼改成如下
public static IUnityContainer RegisterUnity()
{
var container = new UnityContainer();
RegistAll(container);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
return container;
}
//為了從呼叫端得到UnityContainer的實體,但又不想在名稱讓人誤會,乾脆直接呼叫RegisterUnity,並改成回傳RegisterUnity
public static IUnityContainer GetUnityInstance()
{
return RegisterUnity();
}
public class ValueController : ApiController
{
IMessageService _messageService;
public ValueController()
{
var container = UnityConfig.GetUnityInstance(); //取得實體
_messageService = container.Resolve<IMessageService>(); //直接在此解析
}
public IHttpActionResult Get()
{
return Ok(_messageService.Get());
}
}
現在新增一個NoticeService來實做IMessageService的類別,程式碼如下
public class NoticeService : IMessageService
{
private string _hello = "hello notice";
public string Get()
{
return _hello;
}
public void Set(string message)
{
_hello = message;
}
}
因為我們已經有解析了一個MessageService,所以我們使用字串的方式來解析
public static class UnityConfig
{
public static IUnityContainer RegisterUnity()
{
var container = new UnityContainer();
RegistAll(container);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
return container;
}
public static IUnityContainer GetUnityInstance()
{
return RegisterUnity();
}
private static void RegistAll(UnityContainer container)
{
container.RegisterType<IMessageService, MessageService>(); //原本的MessageService
container.RegisterType<IMessageService, NoticeService>("Notice"); //解析名稱為Notice
}
}
public class ValueController : ApiController
{
IMessageService _messageService;
public ValueController()
{
var container = UnityConfig.GetUnityInstance(); //取得實體
_messageService = container.Resolve<IMessageService>("Notice"); //傳入解析名稱為Notice
}
public IHttpActionResult Get()
{
return Ok(_messageService.Get());
}
}
結果
還有另一種方式,也就是使用屬性解析的方式,但是在測試的時候就得自行注意,必須要記得去把屬性注入替換成我們stub的物件
public class ValueController : ApiController
{
[Dependency]
public IMessageService _messageService { get; set; }
public IHttpActionResult Get()
{
return Ok(_messageService.Get());
}
}
//結果為hello
或者我們也可以傳入NoticeService所定義的字串名稱
public class ValueController : ApiController
{
[Dependency("Notice")]
public IMessageService _messageService { get; set; }
public IHttpActionResult Get()
{
return Ok(_messageService.Get());
}
}
//結果為hello notice
使用自動解析的方式
各位可以想像一下,如果我們的介面和類別有上千支的時候,那我們手動解析就會變得非常龐大,我們也可以使用自動解析的方式來實做,請參考如下示例
public static class UnityConfig
{
public static void RegisterComponents()
{
var container = new UnityContainer();
RegistAll(container);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
}
private static void RegistAll(UnityContainer container)
{
container.RegisterTypes(
AllClasses.FromLoadedAssemblies(),
WithMappings.FromMatchingInterface,
WithName.Default,
overwriteExistingMappings: true
);
}
}
我們也可以去定義一個規則配置,比如我都是以檔案結尾為service的方式命名,所以可以改成如下方式
public static class UnityConfig
{
public static void RegisterComponents()
{
var container = new UnityContainer();
RegistAll(container);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
}
private static void RegistAll(UnityContainer container)
{
container.RegisterTypes(
AllClasses.FromLoadedAssemblies().Where(x => x.Name.EndsWith("Service")), //這邊改成尋找檔名去解析
WithMappings.FromMatchingInterface,
WithName.Default,
overwriteExistingMappings: true
);
}
}
但是使用自動解析的話請注意哦,因為每次iis重啟的話,他需要去尋找所有配置,所以檔案越多的話,就會造成第一次啟動過慢,在iis的話可能只有第一個人有感覺,但在程式重build的話,就會造成每一次重整都過慢,所以需視狀況慎用啊。
實做interceptor
如果我們想要在每個介面和實做的時候,做一些邏輯的話,我們就可以使用這種方式,這個也可以稱為Aspect Oriented Programming(AOP),另一個學名是Crosscutting Concerns(橫切面),其實也就是一種裝飾者模式的實現,那unity如何實做Interceptor呢?官方有提供第三方的package供我們方便實做,請先至nuget下載囉。
接著我們在webapi的專案,新增一支LoggingInterceptionBehavior.cs
public class LoggingInterceptionBehavior : IInterceptionBehavior
{
private Logger logger = LogManager.GetCurrentClassLogger(); //這邊使用了nlog
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
// Before invoking the method on the original target.
WriteLog(String.Format(
"Invoking method {0} at {1}",
input.MethodBase, DateTime.Now.ToLongTimeString()));
// Invoke the next behavior in the chain.
var result = getNext()(input, getNext);
// After invoking the method on the original target.
if (result.Exception != null)
{
WriteLog(String.Format(
"Method {0} threw exception {1} at {2}",
input.MethodBase, result.Exception.Message,
DateTime.Now.ToLongTimeString()));
}
else
{
WriteLog(String.Format(
"Method {0} returned {1} at {2}",
input.MethodBase, result.ReturnValue,
DateTime.Now.ToLongTimeString()));
}
return result;
}
public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
}
public bool WillExecute
{
get { return true; }
}
private void WriteLog(string message)
{
logger.Info(message);
}
}
然後在去修改UnityConfig.cs,改成如下
public static class UnityConfig
{
public static void RegisterComponents()
{
var container = new UnityContainer();
RegistAll(container);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
}
private static void RegistAll(UnityContainer container)
{
container.AddNewExtension<Interception>();
container.RegisterTypes(
AllClasses.FromLoadedAssemblies().Where(x => x.Name.EndsWith("Service")),
WithMappings.FromMatchingInterface,
WithName.Default,
overwriteExistingMappings: true,
getInjectionMembers: t => new InjectionMember[]
{
new Interceptor<InterfaceInterceptor>(),
new InterceptionBehavior<LoggingInterceptionBehavior>(), //這邊加入我們剛剛實做的那支類別
}
);
}
}
如果我們是一支一支解析的話,就要改成如下的方式
public static class UnityConfig
{
public static void RegisterComponents()
{
var container = new UnityContainer();
RegistAll(container);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
}
private static void RegistAll(UnityContainer container)
{
container.AddNewExtension<Interception>();
container.RegisterType<IMessageService, MessageService>(
new Interceptor<InterfaceInterceptor>(),
new InterceptionBehavior<LoggingInterceptionBehavior>()
);
}
}
當我呼叫了web api的時候,就可以看到我的目錄多了一個log的目錄,然後記下我們剛剛的interceptor寫下的log部份
使用policy來實做attribute
用policy的方式,可以在我們想實做的地方掛上attribute來實做,官方一樣有提供package方便我們實做,首先就來安裝一下吧
接著一下修改一下UnityConfig.cs的部份
public static class UnityConfig
{
public static void RegisterComponents()
{
var container = new UnityContainer();
RegistAll(container);
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
}
private static void RegistAll(UnityContainer container)
{
container.AddNewExtension<Interception>();
container.RegisterTypes(
AllClasses.FromLoadedAssemblies().Where(x => x.Name.EndsWith("Service")),
WithMappings.FromMatchingInterface,
WithName.Default,
overwriteExistingMappings: true,
getInjectionMembers: t => new InjectionMember[]
{
new Interceptor<InterfaceInterceptor>(),
new InterceptionBehavior<PolicyInjectionBehavior>() //把這邊改成註冊Policy的方式
}
);
}
}
因為我要掛在service層的程式上面,所以我在service層建了一個資料夾命名為Aop,然後新增兩支類別,如下圖示例
Aop/LoggingCallHandler
public class LoggingCallHandler : ICallHandler
{
private Logger logger = LogManager.GetCurrentClassLogger();
public IMethodReturn Invoke(IMethodInvocation input,
GetNextHandlerDelegate getNext)
{
// Invoke the next handler in the chain
var result = getNext().Invoke(input, getNext);
// After invoking the method on the original target
if (result.Exception != null)
{
WriteLog(String.Format("Method {0} threw exception {1} at {2}",
input.MethodBase, result.Exception.Message,
DateTime.Now.ToLongTimeString()));
}
else
{
WriteLog(String.Format("Method {0} returned {1} at {2}",
input.MethodBase, result.ReturnValue,
DateTime.Now.ToLongTimeString()));
}
return result;
}
public int Order
{
get;
set;
}
private void WriteLog(string message)
{
logger.Info(message);
}
}
Aop/LoggingCallHandlerAttribute
public class LoggingCallHandlerAttribute : HandlerAttribute
{
public int Order { get; set; }
public LoggingCallHandlerAttribute()
{
}
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new LoggingCallHandler() { Order = Order };
}
}
接著我們看一下IMessageService的部份,我們就把剛剛實做的Attribute,掛在想實做的地方,可以直接掛在介面,甚至只掛在類別的部份,在此我是直接掛在介面的部份
public interface IMessageService
{
[LoggingCallHandler]
string Get();
void Set(string message);
}
public class MessageService : IMessageService
{
private string _hello="hello";
public string Get()
{
return _hello;
}
public void Set(string message)
{
_hello = message;
}
}
結論
很快的我們簡單的完成了一些unity操作的部份,包括了AOP的實做,接著我們想要在AOP裡面實做什麼,就自行實做和研究囉,如果筆者有任何錯誤的地方,再請多多指正一下。