愛流浪的小風

技術隨手寫

使用Asp.Net MVC打造Web Api (20) - 整合AOP功能

| Comments

在我們前面的文章中,經常利用客製化ActionFilter來撰寫一些通用的邏輯(例如:Log、Authorize等),並且可以很輕鬆的掛載到函式上面來套用,這樣的做法可以讓function中只專注在自己本分的工作,而通用的工作就交給ActionFilter來實現,這樣的做法其實就叫做AOP(Aspect-Oriented Programming),而Asp.Net MVC迷人處之一也是內建了AOP,讓我們可以遵循SoC(Seperated of Concern)的原則來進行開發,而今天要介紹的是如何將這種做法延伸帶到UI層之外,讓他們同樣可以享受到AOP的好處。

大家可以從Github ApiSample - Tag Day18開始今天的練習

AOP實現原理

大家還記得我們在開始開發Api的時候,就使用了DI Framework來做為我們Api的核心組成架構嗎?透過DI Framework可以讓我們程式碼的耦合性降低,甚至可以在執行時期改變介面(Interface)所對應實現的個體(Instance)

而Autofac除了這個好處之外,他還可以用Proxy Pattern的方式在對介面注入實體時,動態產生一個Proxy Class,讓我們可以輕鬆嵌入AOP的程式碼!

實作AOP,替Service加上Log

若要讓Autofac支援AOP,我們必須另外安裝一個Library,來讓Autofac具有AOP的功能,我們使用Nuget來安裝它

  1. 使用Nuget安裝Autofac.Extra: Castle Dynamic Proxy Support

  2. 在Extensions專案實作LogInterceptor,在執行function的前後加上Log

    public class LogInterceptor : IInterceptor
    {
        public ILogger Logger { get; set; }
    
        public LogInterceptor(ILogger logger)
        {
            this.Logger = logger;
        }
    
        public void Intercept(IInvocation invocation)
        {
    
            this.Logger.Debug("呼叫方法:{0}, 參數:{1}... ",
                invocation.Method.Name,
                string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));
    
            invocation.Proceed();
    
            this.Logger.Debug("執行結果:{0}.", invocation.ReturnValue);
        }
    }    
    
  3. 修改BL的ServiceModule,讓Class支援AOP

    public class ServiceModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            var service = Assembly.Load("ApiSample.BL.Services");
    
            builder.RegisterAssemblyTypes(service)
                   .AsImplementedInterfaces()
                   .EnableInterfaceInterceptors();                      
        }
    }
    
  4. 修改BL的ExtensionModule,註冊Interceptor

    public class ExtensionModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            var mappings = Assembly.Load("ApiSample.Utility.Extensions");
    
            builder.RegisterAssemblyTypes(mappings)
                   .AsImplementedInterfaces();
    
            builder.RegisterAssemblyTypes(mappings)
                   .Where(i => i.Name.EndsWith("Interceptor"))
                   .AsSelf();            
        }
    }
    
  5. 在ProductService上加上Interceptor,讓Autofac知道這個Class需要AOP

    [Intercept(typeof(LogInterceptor))]
    public class ProductService : IProductService
    {
    }        
    
  6. 執行查詢,可以看到成功的產生Log

透過Mobule指定Interceptor

除了透過在Class或Interface上面加上Attribute來讓Autofac會注入Interceptor之外,也可以透過在Module中設定Class所需要的Interceptor,這麼一來我們可以讓Interceptor變成不會綁死在某一隻Class或Interface上面,而是更能夠依照實際需求很有彈性的在需要的時候才指定Interceptor。

  1. 將原本ProductService的Interceptor移除

    public class ProductService : IProductService
    {
    }        
    
  2. 在Service Module中直接指定ProductService需要LogInterceptor

    protected override void Load(ContainerBuilder builder)
    {
        var service = Assembly.Load("ApiSample.BL.Services");
    
        builder.RegisterAssemblyTypes(service)
               .AsImplementedInterfaces()
               .EnableInterfaceInterceptors();
    
        builder.RegisterType<ProductService>()
               .As<IProductService>()
               .EnableInterfaceInterceptors()
               .InterceptedBy(typeof(LogInterceptor));
    }
    
  3. 重新執行查詢,Log檔也有正確的產生!

使用AOP進行權限檢查

雖然我們已經在Controller做了一層防護,但有時候我們希望可以限定某個Class能夠確認是登入的使用者才能呼叫,那我們也可以透過AOP的方式來實作

  1. 在Utility的Extensions建立AuthInterceptor,來檢查執行方法的使用者是否擁有權限

    public class AuthInterceptor : IInterceptor
    {
        private string role;
    
        public AuthInterceptor(string role)
        {
            if (string.IsNullOrWhiteSpace(role))
            {
                throw new ArgumentNullException("role");
            }
    
            this.role = role;
        }
    
        public void Intercept(IInvocation invocation)
        {
            if (!Thread.CurrentPrincipal.Identity.IsAuthenticated ||
                !Thread.CurrentPrincipal.IsInRole(this.role))
            {
                throw new UnauthorizedAccessException();
            }
    
            invocation.Proceed();
        }
    }
    
  2. 修改BL的ExtensionsModule,增加AuthInterceptor的設定

    public class ExtensionModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            var mappings = Assembly.Load("ApiSample.Utility.Extensions");
    
            builder.RegisterAssemblyTypes(mappings)
                   .AsImplementedInterfaces();
    
            builder.RegisterAssemblyTypes(mappings)
                   .Where(i => i.Name.EndsWith("Interceptor"))
                   .AsSelf();
    
            builder.Register(i => new AuthInterceptor("Administrator"))
                   .As<AuthInterceptor>();
        }
    }
    
  3. 修改BL的ServiceModule,增加AuthInterceptor

    public class ServiceModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            var service = Assembly.Load("ApiSample.BL.Services");
    
            builder.RegisterAssemblyTypes(service)
                   .AsImplementedInterfaces()
                   .EnableInterfaceInterceptors();
    
            builder.RegisterType<ProductService>()
                   .As<IProductService>()
                   .EnableInterfaceInterceptors()
                   .InterceptedBy(typeof(LogInterceptor), typeof(AuthInterceptor));
        }
    }
    
  4. 設定中斷點檢測,有產生Log也有檢查權限!

本日小結

透過AOP的模式,我們可以將程式職責區分得更加清楚,並且更易於重複使用。再加上DI Framework讓AOP的Interceptor不需要把邏輯跟其它邏輯綁死,而是可以透過修改替換Module來決定現在需要哪些Interceptor,這些都是讓程式碼具有更高彈性的地方!關於今天的內容,歡迎大家一起討論^_^

Comments

comments powered by Disqus