[C#]Unity的配置還有AOP和Policy Injection的做法

  • 3832
  • 0
  • C#
  • 2017-05-31

這篇會介紹一下Unity的一些配置和做法,但不會特別介紹什麼叫做Dependency Injection是什麼,如果想了解何謂di請另外自行爬文

前言

其實早在三年前筆者就有用di來方便注入,並且方便的stub做單元測試,以前用的都是autofac,雖然也曾經研究過使用unity的做法,但在那個時期unity對web api的支援比autofac比較慢,所以筆者就一直使用autofac,但因為目前的專案上面決定使用unity的方式,所以我就研究了一下包括如何用code的方式來配置,甚至是用xml的方式來配置,還有自動配置的方式一些紀錄,首先看一下我的專案配置,假設我有一個web api,然後參考著另一個專案是service,如圖示例。

導覽

  1. 安裝必要的package,並以xml的方式來實做
  2. 把注入的部份,改成Lazy loading的方式
  3. 以code的方式來實做
  4. 不用建構子去注入
  5. 使用字串來分成不同注入類別
  6. 使用自動解析的方式
  7. 使用屬性解析的方式
  8. 實做interceptor
  9. 使用policy來實做attribute
  10. 結論

 

安裝必要的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裡面實做什麼,就自行實做和研究囉,如果筆者有任何錯誤的地方,再請多多指正一下。