乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 淺析ASP.NET Core引入面向切面編程(AOP)的能力和第三方依賴注入框架Autofac

什麼是面向切面編程

在計算機領域,面向切面編程(Aspect Oriented Program, AOP)是一種編程範式,旨在通過允許跨領域的關注點分離來提高模塊化程度。它通過向現有的代碼添加行爲而不修改代碼本身,而是通過"指向性(pointcut)"規範單獨指定哪些代碼被修改,例如 "當函數的名稱以'set'開頭時,記錄所有的函數調用"。這使得那些不是業務邏輯核心的行爲(如日誌)可以被添加到程序中,而不會使功能的核心代碼變得混亂。

image

面向切面編程包括支持源代碼層面的關注點模塊化的編程方法和工具,而面向切面編程開發是指整個工程學科。

面向切面編程需要將程序邏輯分解成不同的部分(所謂的關注點,功能的凝聚區域)。幾乎所有的編程範式都支持某種程度的分組,並通過提供可用於實現、抽象和組合這些關注點的抽象(例如,函數、程序、模塊、類、方法),將關注點封裝成獨立的實體。有些關注點"跨越"了程序中的多個抽象,並且違背了這些實現的形式。這些關注點被稱爲跨領域關注點或水平關注點

日誌是跨領域關注的典範,因爲一個日誌策略必然會影響到系統的每一個日誌部分。因此,日誌與所有被記錄的類和方法交叉進行。

所有的AOP實現都有一些交叉表達,將每個關注點封裝在一個地方。實現之間的區別在於所提供的構造的力量、安全性和實用性。例如,指定方法的攔截器可以表達有限形式的交叉切割,但對類型安全或調試沒有太多的支持。AspectJ有很多這樣的表達方式,並將它們封裝在一個特殊的類中,即aspect。例如,一個方面可以通過在不同的連接點(程序中的點)應用建議(額外的行爲)來改變基礎代碼的行爲(程序中的非方面部分),這些連接點是由被稱爲pointcut(檢測給定連接點是否匹配)的量化或查詢指定的。一個方面也可以對其他類進行二進制兼容的結構改變,比如增加成員或父類。

基本概念

通常情況下,一個方面的代碼是分散的或糾結的,使其更難理解和維護。它是分散的,因爲功能(如日誌)被分散在一些不相關的功能中,這些功能可能使用它的功能,可能在完全不相關的系統中,不同的源語言,等等。這意味着要改變日誌記錄,可能需要修改所有受影響的模塊。方面不僅與表達它們的系統的主線功能糾纏在一起,而且還相互糾纏在一起。這意味着改變一個關注點需要理解所有糾纏在一起的關注點,或者有一些方法可以推斷出改變的效果。

例如,考慮一個銀行應用程序,它有一個概念上非常簡單的方法來把一筆錢從一個賬戶轉到另一個賬戶:

void transfer(Account fromAcc, Account toAcc, int amount) throws Exception {
  if (fromAcc.getBalance() < amount)
      throw new InsufficientFundsException();

  fromAcc.withdraw(amount);
  toAcc.deposit(amount);
}

然而,這種傳輸方法忽略了部署的應用程序所需要的某些考慮:它缺乏安全檢查來驗證當前用戶是否有執行該操作的授權;數據庫事務應該封裝該操作,以防止意外的數據丟失;爲了診斷,該操作應該被記錄到系統日誌中,等等。

爲了舉例,一個具有所有這些新關注點的版本可以看起來有點像這樣:

void transfer(Account fromAcc, Account toAcc, int amount, User user,
    Logger logger, Database database) throws Exception {
  logger.info("Transferring money...");
  
  if (!isUserAuthorised(user, fromAcc)) {
    logger.info("User has no permission.");
    throw new UnauthorisedUserException();
  }
  
  if (fromAcc.getBalance() < amount) {
    logger.info("Insufficient funds.");
    throw new InsufficientFundsException();
  }

  fromAcc.withdraw(amount);
  toAcc.deposit(amount);

  database.commitChanges();  // Atomic operation.

  logger.info("Transaction successful.");
}

在這個例子中,其他的利益與基本功能(有時稱爲業務邏輯關注)糾纏在一起。交易、安全和日誌都是跨領域關注的典範

現在考慮一下,如果我們突然需要改變(比如說)應用程序的安全考慮,會發生什麼。在程序的當前版本中,與安全有關的操作分散在許多方法中,這樣的改變需要很大的努力。

AOP試圖解決這個問題,它允許程序員在獨立的模塊中表達跨領域的關注,這些模塊被稱爲aspects。aspects可以包含建議(加入到程序中指定點的代碼)和類型間聲明(添加到其他類的結構成員)。例如,一個安全模塊可以包括建議,在訪問銀行賬戶之前執行安全檢查。切點定義了人們可以訪問銀行賬戶的時間(連接點),而建議主體中的代碼定義了安全檢查的實現方式。這樣一來,檢查和地點都可以在一個地方維護。此外,一個好的pointcut可以預見以後的程序變化,所以如果另一個開發者創建了一個新的方法來訪問銀行賬戶,那麼當新方法執行時,建議將適用於新方法。

因此,對於上面的例子,在一個方面實現日誌。

aspect Logger {
  void Bank.transfer(Account fromAcc, Account toAcc, int amount, User user, Logger logger)  {
    logger.info("Transferring money...");
  }

  void Bank.getMoneyBack(User user, int transactionId, Logger logger)  {
    logger.info("User requested money back.");
  }

  // Other crosscutting code.
}

我們可以把AOP看作是一個調試工具或者是一個用戶級工具。建議應該保留給你不能讓函數改變(用戶級)或不想在生產代碼中改變函數(調試)的情況。

應付場景

在兩個類中,都需要在每個方法中做日誌,面向對象就必須在兩個類中都加入日誌的內容,可能日誌內容完全相同,但面向對象的設計讓類與類之間無法聯繫,所以造成不能將這些重複的代碼統一起來。

這種情況下,我們需要在編碼時,當我們需要某個方法的時候,隨意的加入代碼中。這種在運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面編程

切面:將切入的指定方法的代碼片段稱爲切面。

切點:切入到的目標類或目標方法,稱爲切入點。

有了AOP,就可以將幾個類共有的代碼,抽取到一個切片中,等到需要時再切入對象中,從而改變其原有行爲!

面向切面編程在ASP.NET Core

爲什麼需要它

一個架構良好的應用程序有不同的層,這樣,不同的關注點不會進行不必要的交互。假設要設計鬆散耦合、可維護的應用程序,但在開發過程中,發現某些要求可能不適合體系結構,如:

  • 在將數據寫入數據庫之前,必須對數據進行驗證。
  • 應用程序必須具備審計和日誌記錄功能,以進行合理的操作。
  • 應用程序必須維護調試日誌以檢查操作是否正常。
  • 必須測量某些操作的性能,以便了解這些操作是否在要求的範圍內。

所有這些要求都需要大量的工作以及代碼重複。您必須在系統的很多部分添加相同的代碼,這樣就不符合“切勿重複”(DRY) 的原則,維護也更加困難。如果要求有任何變化,都會引起對程序的大量更改。如果我必須在應用程序中添加這類內容,我會想:“爲什麼編譯器不能爲我在多個位置添加這些重複代碼?”,或者“我希望我可以‘向這個方法添加日誌記錄’”。

值得高興的是,確實可以做到這一點:面向切面編程(AOP)。它從跨對象或層的邊界的方面分離出常規代碼。例如,應用程序日誌不綁定到任何應用程序層。它應用於整個程序,應該無所不在。這稱爲"橫切關注點"。

根據維基百科,AOP是旨在通過允許分離橫切關注點來提高模塊化程度的編程模式。它處理髮生在系統多個部分的功能,將這種功能與應用程序核心分開,從而改進關注點的分離,避免代碼重複和耦合。

實現它的方式

AOP的最大優勢是,您只需關注一個位置的特定方面,對其進行編程,根據需要將其應用於所有位置。

AOP有許多用途,如:

  • 在應用程序中實現日誌記錄。
  • 在操作之前使用身份驗證(如僅允許經過身份驗證的用戶執行某些操作)。
  • 爲屬性setter實現驗證或通知(爲實現INotifyPropertyChanged接口的類更改屬性時,調用PropertyChanged事件)。
  • 更改某些方法的行爲。

可以看到,AOP有許多用途,但您使用它時必須小心。它會隱藏一些代碼,而代碼是存在的,在相關方面的每次調用中運行。它可能會有錯誤,嚴重影響應用程序的性能。切面中的細微錯誤可能要耗費很多調試時間。如果切面不在很多位置使用,有時最好是直接添加到代碼中

AOP實現會採用一些常見方法:

  • 使用預處理器(如C++中的預處理器)添加源代碼。
  • 使用後處理器在編譯後的二進制代碼上添加指令。
  • 使用特殊編譯器在編譯時添加代碼。
  • 在運行時使用代碼攔截器攔截執行並添加所需的代碼。

在.NET Framework中,最常用的方法是後處理和代碼攔截。PostSharp(postsharp.net)使用前一方法,CastleDynamicProxy(bit.ly/JzE631)和Unity(unity.codeplex.com)等依賴關係注入容器使用後一方法。這些工具通常使用稱爲DecoratorProxy的設計模式來執行代碼攔截。

Decorator設計模式

Decorator設計模式解決一個常見問題:您有一個類,需要向其添加某種功能。您有幾種選擇:

  • 可以將新功能直接添加到類。但是,這樣類要多承擔一項責任,不符合“單一責任”原則。
  • 您可以創建新類來執行這一功能,然後從舊類調用新類。這會帶來新問題:要是還需要使用不含新功能的類,該怎麼辦?
  • 您可以繼承一個新類,添加新功能,但這樣可能產生許多新的類。例如,假設您有一個用於創建、讀取、更新和刪除(CRUD)數據庫操作的存儲庫類,您需要添加審計。後來,您需要添加數據驗證以確保數據正確更新。此後,您可能還需要對訪問進行身份驗證,以確保只有授權用戶才能訪問這些類。以下是較大的問題:您可以用一些類來實現所有三個方面,可以用一些類僅實現其中兩個方面甚至僅一個方面。最後,會有多少類?
  • 您可以使用方面“修飾”類,從而創建一個使用方面、然後調用舊類的新類。這樣,如果需要一個方面,就對它修飾一次。如果需要兩個方面,就對它修飾兩次,依此類推。假設您訂購一個玩具(我們都是電腦高手,可以是Xbox或智能手機這類玩具)。它需要包裝,以便在商店中展示,也可以得到保護。您訂購時配上禮品包裝(第二次裝飾),用膠帶、彩條、卡片和禮品包裝紙對包裝盒進行裝飾。商店使用第三層包裝(帶泡沫聚苯乙烯球的盒子)發送玩具。玩具有三層裝飾,每層裝飾的功能都不同,各層裝飾相互獨立。購買玩具時您可以不要禮品包裝,可以在商店挑選它時不要外包裝盒,甚至不帶盒子就買下它(有特殊折扣!)。玩具可以有任何裝飾組合,但這些裝飾都不會改變玩具的基本功能。

既然您已瞭解Decorator模式,我將說明如何在C#中實現它。

首先,創建一個接口IRepository<T>

public interface IRepository<T>
{
    void Add(T entity);
    void Delete(T entity);
    void Update(T entity);
    IEnumerable<T> GetAll();
    T GetById(int id);
}

通過Repository<T>類實現它,

public class Repository<T> : IRepository<T>
{
    public void Add(T entity)
    {
        Console.WriteLine("Adding {0}", entity);
    }
    public void Delete(T entity)
    {
        Console.WriteLine("Deleting {0}", entity);
    }
    public void Update(T entity)
    {
        Console.WriteLine("Updating {0}", entity);
    }
    public IEnumerable<T> GetAll()
    {
        Console.WriteLine("Getting entities");
        return null;
    }
    public T GetById(int id)
    {
        Console.WriteLine("Getting entity {0}", id);
        return default(T);
    }
}

使用Repository<T>類添加、更新、刪除和檢索Customer類的元素:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
}

調用示例

[HttpGet]
public int Get()
{
    Console.WriteLine("***\r\n Begin program - no logging\r\n");
    IRepository<Customer> customerRepository = new Repository<Customer>();
    var customer = new Customer
    {
        Id = 1,
        Name = "Customer 1",
        Address = "Address 1"
    };
    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nEnd program - no logging\r\n***");

    return 1;
}

輸出結果

***
 Begin program - no logging

Adding demoForApi31.Models.Customer
Updating demoForApi31.Models.Customer
Deleting demoForApi31.Models.Customer

End program - no logging
***

假設上級要求您向這個類添加日誌記錄。您可以創建一個新類對IRepository<T>進行修飾。它接收這個類,生成並實現同樣的接口。

public class LoggerRepository<T> : IRepository<T>
{
    private readonly IRepository<T> _decorated;
    public LoggerRepository(IRepository<T> decorated)
    {
        _decorated = decorated;
    }
    private void Log(string msg, object arg = null)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(msg, arg);
        Console.ResetColor();
    }
    public void Add(T entity)
    {
        Log("In decorator - Before Adding {0}", entity);
        _decorated.Add(entity);
        Log("In decorator - After Adding {0}", entity);
    }
    public void Delete(T entity)
    {
        Log("In decorator - Before Deleting {0}", entity);
        _decorated.Delete(entity);
        Log("In decorator - After Deleting {0}", entity);
    }
    public void Update(T entity)
    {
        Log("In decorator - Before Updating {0}", entity);
        _decorated.Update(entity);
        Log("In decorator - After Updating {0}", entity);
    }
    public IEnumerable<T> GetAll()
    {
        Log("In decorator - Before Getting Entities");
        var result = _decorated.GetAll();
        Log("In decorator - After Getting Entities");
        return result;
    }
    public T GetById(int id)
    {
        Log("In decorator - Before Getting Entity {0}", id);
        var result = _decorated.GetById(id);
        Log("In decorator - After Getting Entity {0}", id);
        return result;
    }
}

這個新類對已修飾類的方法進行包裝,添加日誌記錄功能。要調用該日誌記錄類,必須對代碼進行略微更改。

[HttpGet]
public int Get()
{
    Console.WriteLine("***\r\n Begin program - logging with decorator\r\n");
    // IRepository<Customer> customerRepository =
    //   new Repository<Customer>();
    IRepository<Customer> customerRepository = new LoggerRepository<Customer>(new Repository<Customer>());
    var customer = new Customer
    {
        Id = 1,
        Name = "Customer 1",
        Address = "Address 1"
    };
    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nEnd program - logging with decorator\r\n***");

    return 1;
}

您只需創建新類,傳遞舊類的實例作爲其構造函數的參數。在執行程序時,可以看到它有日誌記錄。

***
 Begin program - logging with decorator

In decorator - Before Adding demoForApi31.Models.Customer
Adding demoForApi31.Models.Customer
In decorator - After Adding demoForApi31.Models.Customer
In decorator - Before Updating demoForApi31.Models.Customer
Updating demoForApi31.Models.Customer
In decorator - After Updating demoForApi31.Models.Customer
In decorator - Before Deleting demoForApi31.Models.Customer
Deleting demoForApi31.Models.Customer
In decorator - After Deleting demoForApi31.Models.Customer

End program - logging with decorator
***

您可能會認爲:“想法是不錯,但需要大量工作:我必須實現所有類並將方面添加到所有方法。這很難維護。有沒有其他方法可以實現呢?”通過.NET Framework,您可以使用反射來獲取所有方法並加以執行。基類庫(BCL)甚至有可用來執行該實現的RealProxy類(bit.ly/18MfxWo)。

使用RealProxy創建動態代理

RealProxy類提供基本代理功能。它是一個抽象類,必須通過重寫其Invoke方法並添加新功能來繼承。該類在命名空間System.Runtime.Remoting.Proxies中。

class DynamicProxy<T> : RealProxy
{
    private readonly T _decorated;
    public DynamicProxy(T decorated)
        : base(typeof(T))
    {
        _decorated = decorated;
    }
    private void Log(string msg, object arg = null)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(msg, arg);
        Console.ResetColor();
    }
    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        var methodInfo = methodCall.MethodBase as MethodInfo;
        Log("In Dynamic Proxy - Before executing '{0}'",
            methodCall.MethodName);
        try
        {
            var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
            Log("In Dynamic Proxy - After executing '{0}' ",
                methodCall.MethodName);
            return new ReturnMessage(result, null, 0,
                methodCall.LogicalCallContext, methodCall);
        }
        catch (Exception e)
        {
            Log(string.Format(
                "In Dynamic Proxy- Exception {0} executing '{1}'", e),
                methodCall.MethodName);
            return new ReturnMessage(e, methodCall);
        }
    }
}

在該類的構造函數中,必須調用基類的構造函數,傳遞要修飾的類的類型。然後,必須重寫將接收IMessage參數的Invoke方法。它包含一個字典,字典中是爲該方法傳遞的所有參數。IMessage參數會類型轉換爲IMethodCallMessage,這樣,就可以提取參數MethodBase(具有MethodInfo類型)。

接下來的步驟是在調用該方法前添加所需的方面,使用methodInfo.Invoke調用原始方法,調用之後添加該方面。

您不能直接調用代理,因爲DynamicProxy<T>不是IRepository<Customer>。這意味着您不能這樣調用它:

IRepository<Customer> customerRepository =  new DynamicProxy<IRepository<Customer>>( new Repository<Customer>());

要使用經過修飾的存儲庫,您必須使用GetTransparentProxy方法,此方法將返回IRepository<Customer>的實例。所調用的此實例的每個方法都將經歷該代理的Invoke方法。爲了方便實現此過程,您可以創建一個Factory類來創建代理並返回存儲庫的實例:

public class RepositoryFactory
{
    public static IRepository<T> Create<T>()
    {
        var repository = new Repository<T>();
        var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
        return dynamicProxy.GetTransparentProxy() as IRepository<T>;
    }
}
[HttpGet]
public int Get()
{
    Console.WriteLine("***\r\n Begin program - logging with dynamic proxy\r\n");
    // IRepository<Customer> customerRepository =
    //   new Repository<Customer>();
    // IRepository<Customer> customerRepository =
    //   new LoggerRepository<Customer>(new Repository<Customer>());
    IRepository<Customer> customerRepository =
        RepositoryFactory.Create<Customer>();
    var customer = new Customer
    {
        Id = 1,
        Name = "Customer 1",
        Address = "Address 1"
    };

    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nEnd program - logging with dynamic proxy\r\n***");
    return 1;
}

在執行此程序時,結果和前面類似。

***
 Begin program - logging with decorator

In decorator - Before Adding demoForApi31.Models.Customer
Adding demoForApi31.Models.Customer
In decorator - After Adding demoForApi31.Models.Customer
In decorator - Before Updating demoForApi31.Models.Customer
Updating demoForApi31.Models.Customer
In decorator - After Updating demoForApi31.Models.Customer
In decorator - Before Deleting demoForApi31.Models.Customer
Deleting demoForApi31.Models.Customer
In decorator - After Deleting demoForApi31.Models.Customer

End program - logging with decorator
***

可以看到,您已創建一個動態代理,可將方面添加到代碼,無需重複該操作。如果要添加一個新方面,只需創建一個新類,從RealProxy繼承,用它來修飾第一個代理。

如果上級又要求您向代碼中添加授權,以便只有管理員才能訪問存儲庫,則可以創建一個新代理。

class AuthenticationProxy<T> : RealProxy
{
    private readonly T _decorated;
    public AuthenticationProxy(T decorated)
        : base(typeof(T))
    {
        _decorated = decorated;
    }
    private void Log(string msg, object arg = null)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(msg, arg);
        Console.ResetColor();
    }
    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        var methodInfo = methodCall.MethodBase as MethodInfo;
        if (Thread.CurrentPrincipal.IsInRole("ADMIN"))
        {
            try
            {
                Log("User authenticated - You can execute '{0}' ",
                    methodCall.MethodName);
                var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
                return new ReturnMessage(result, null, 0,
                    methodCall.LogicalCallContext, methodCall);
            }
            catch (Exception e)
            {
                Log(string.Format(
                    "User authenticated - Exception {0} executing '{1}'", e),
                    methodCall.MethodName);
                return new ReturnMessage(e, methodCall);
            }
        }
        Log("User not authenticated - You can't execute '{0}' ",
            methodCall.MethodName);
        return new ReturnMessage(null, null, 0,
            methodCall.LogicalCallContext, methodCall);
    }
}

必須更改存儲庫工廠才能調用兩個代理。

public class RepositoryFactory
{
    public static IRepository<T> Create<T>()
    {
        var repository = new Repository<T>();
        var decoratedRepository =
            (IRepository<T>)new DynamicProxy<IRepository<T>>(
            repository).GetTransparentProxy();
        // Create a dynamic proxy for the class already decorated
        decoratedRepository =
            (IRepository<T>)new AuthenticationProxy<IRepository<T>>(
            decoratedRepository).GetTransparentProxy();
        return decoratedRepository;
    }
}

如果將主程序更改爲下面這種,然後運行。

[HttpGet]
public int Get()
{
    Console.WriteLine("***\r\n Begin program - logging and authentication\r\n");
    Console.WriteLine("\r\nRunning as admin");
    Thread.CurrentPrincipal =
        new GenericPrincipal(new GenericIdentity("Administrator"),
        new[] { "ADMIN" });
    IRepository<Customer> customerRepository =
        RepositoryFactory.Create<Customer>();
    var customer = new Customer
    {
        Id = 1,
        Name = "Customer 1",
        Address = "Address 1"
    };
    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nRunning as user");
    Thread.CurrentPrincipal =
        new GenericPrincipal(new GenericIdentity("NormalUser"),
        new string[] { });
    customerRepository.Add(customer);
    customerRepository.Update(customer);
    customerRepository.Delete(customer);
    Console.WriteLine("\r\nEnd program - logging and authentication\r\n***");
    return 1;
}

image

程序執行兩次存儲庫方法。第一次,它以管理員用戶身份運行,並調用這些方法。第二次,它以普通用戶身份運行,並跳過這些方法。

這要容易很多,對吧?請注意,工廠返回IRepository<T>的實例,因此,程序不知道它是否正在使用經過修飾的版本。這符合里氏替換原則,即如果S是T的子類型,則類型T的對象可以替換爲類型S的對象。這種情況下,通過使用IRepository<Customer>接口,可以使用可實現此接口的任何類而不必更改程序。

什麼情況下需要引入第三方容器組件

  • 基於名稱的注入,需要把一個服務按照名稱來區分不同的實現的時候。
  • 基於屬性的注入,直接把服務註冊到某一個類的屬性裏面去,而不需要定義構造函數。
  • 子容器,可以使用第三方框架實現一些特殊的子容器。
  • 基於動態代理的AOP,當我們需要在服務中注入額外的行爲的時候,可以使用動態代理的能力。

.NET Core引入第三方容器的核心擴展點

public interface IServiceProviderFactory<TContainerBuilder> { }

第三方依賴注入容器,都是使用這個類來做爲擴展點,把自己注入到我們整個框架裏面。也就是說我們在使用這些依賴注入框架的時候,我們不需要關注誰家的特性、接口是什麼樣子的,我們只需要使用官方核心的定義就可以了。

實踐理解

https://github.com/TaylorShi/HelloAspectOP

什麼是Autofac

https://autofac.org

Autofac算是.Net社區裏面最老牌的第三方依賴注入框架。

image

獲取安裝Autofac

https://www.nuget.org/packages/Autofac.Extensions.DependencyInjection

dotnet add package Autofac.Extensions.DependencyInjection

image

https://www.nuget.org/packages/Autofac.Extras.DynamicProxy

dotnet add package Autofac.Extras.DynamicProxy

image

準備一些類

public interface IMyService
{
    void ShowCode();
}

public class MyService : IMyService
{
    public void ShowCode()
    {
        Console.WriteLine($"MyService.ShowCode:{this.GetHashCode()}");
    }
}

public class MyServiceV2 : IMyService
{
    public MyNameService NameService { get; set; }

    public void ShowCode()
    {
        Console.WriteLine($"MyServiceV2.ShowCode:{this.GetHashCode()}, NameService是否爲空:{NameService == null}");
    }
}

public class MyNameService
{

}

註冊第三方容器的入口

Program.csCreateHostBuilder方法添加UseServiceProviderFactory來註冊Autofac容器服務,這裏創建一個新的AutofacServiceProviderFactory實例註冊進去。

# demoFor2Api31\Program.cs

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

添加配置容器方法

Startup.cs中添加ConfigureContainer方法。相比之前已經存在的ConfigureServices是將默認的容器注入進去,服務被註冊到默認容器後,在這裏會被Autofac接替,然後執行這裏的ConfigureContainer方法。

public void ConfigureContainer(ContainerBuilder builder)
{

}

常規註冊

通過RegisterTypeAs可以進行Autofac的常規註冊。

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<MyService>().As<IMyService>();
}

Autofac的註冊方式和默認容器的註冊方式有些不同,Autofac先註冊具體的實現,然後告訴它,想把它標記爲哪個服務的類型。

回憶對比下,默認容器的注入方式長這樣:

services.AddSingleton<IMySingletonService, MySingletonService>();

命名註冊

當我們需要把一個服務註冊多次,並且用不同命名來做區分的時候,可以使用命名註冊方式(Named)。

builder.RegisterType<MyServiceV2>().Named<IMyService>("service2");

使用命名註冊

獲取Autofac的根容器。

public ILifetimeScope AutofacContainer { get; private set; }

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    this.AutofacContainer = app.ApplicationServices.GetAutofacRoot();

直接獲取服務

直接從Autofac容器來獲取服務,還可以通過Resolve來獲取。

var service = this.AutofacContainer.Resolve<IMyService>();
service.ShowCode();

輸出結果

MyService.ShowCode:14419821

根據命名獲取服務

從Autofac根容器根據名稱來獲取服務實例,可通過ResolveNamed來獲取,此外還有ResolveKeyed

var service = this.AutofacContainer.ResolveNamed<IMyService>("service2");
service.ShowCode();

從根容器中獲取別名爲service2IMyService對應的實例。

MyServiceV2.ShowCode:14419821, NameService是否爲空:True

從輸出結果來看,這裏確實取到的就是前面註冊別名爲service2的那個MyServiceV2實例。

屬性註冊

屬性註冊的話,只需要在普通註冊的基礎上後面加上PropertiesAutowired即可。

builder.RegisterType<MyServiceV2>().As<IMyService>().PropertiesAutowired();

這時候直接運行,結果是:

MyServiceV2.ShowCode:31226782, NameService是否爲空:True

這個NameService屬性的對象仍然爲空,因爲我們並沒有註冊它。

builder.RegisterType<MyNameService>();
builder.RegisterType<MyServiceV2>().As<IMyService>().PropertiesAutowired();

再次運行,結果就是False了,也就是這時候屬性注入成功了。

MyServiceV2.ShowCode:62669527, NameService是否爲空:False

AOP場景

AOP意味着,我們在不期望改變原有類的情況下,在方法執行時嵌入一些邏輯,讓我們可以在方法執行的切面上任意插入我們的邏輯。

public class MyIInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine($"Intercept before, Method:{invocation.Method.Name}");

        invocation.Proceed();

        Console.WriteLine($"Intercept after, Method:{invocation.Method.Name}");
    }
}

先準備一個切面代碼塊,需要繼承接口IInterceptor,並且實現其Intercept方法。

其中invocation.Proceed()代表執行被切面注入的方法本身,而我們在這前後都增加了輸出。

Castle.DynamicProxy.IInterceptor包含在之前引入的一個Autofac.Extras.DynamicProxy包的依賴包Castle.Core中。

image

我們管MyIInterceptor爲攔截器。

AOP注入

先把攔截器MyIInterceptor註冊到容器裏面。

builder.RegisterType<MyIInterceptor>();

開啓攔截器需要使用InterceptedBy方法,並且把這個攔截器類型(typeof(MyIInterceptor))註冊進去,並且還需要開啓接口攔截器(EnableInterfaceInterceptors)或者開啓類攔截器(EnableClassInterceptors)。

builder.RegisterType<MyIInterceptor>();
builder.RegisterType<MyNameService>();
builder.RegisterType<MyServiceV2>().As<IMyService>().PropertiesAutowired().InterceptedBy(typeof(MyIInterceptor)).EnableInterfaceInterceptors();

常用的是接口攔截器(InterfaceInterceptors),當服務的類型是基於接口設計的,就用接口攔截器,如果我們基於類設計,我們需要把方法設計成虛方法,這樣允許繼承類重載的情況下,這樣子才能攔截到具體方法,這時候就用類攔截器(ClassInterceptors)。

輸出結果

Intercept before, Method:ShowCode
MyServiceV2.ShowCode:5903470, NameService是否爲空:False
Intercept after, Method:ShowCode

這時候我們確實看到,在執行類方法前後執行了我們在攔截器中設定的兩個輸出。

註冊到子容器

通過InstancePerMatchingLifetimeScope不僅可以將服務註冊到子容器,還可以單獨給它命名。

builder.RegisterType<MyNameService>().InstancePerMatchingLifetimeScope("myscope");

從子容器中讀取它

using (var myscope = AutofacContainer.BeginLifetimeScope("myscope"))
{
    var service0 = myscope.Resolve<MyNameService>();
    using (var scope = myscope.BeginLifetimeScope())
    {
        var service1 = scope.Resolve<MyNameService>();
        var service2 = scope.Resolve<MyNameService>();
        Console.WriteLine($"service1=service0: {service0 == service1}");
        Console.WriteLine($"service2=service0: {service0 == service2}");
    }
}

通過BeginLifetimeScope從根容器中按別名讀取它。

輸入結果:

service1=service0: True
service2=service0: True

myscope容器下面,我們再創建任何子容器生命週期,得到都是同一個對象,這個可以用於不期望一個對象在根容器創建時,又期望它在某一定範圍內保持單例模式的場景。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章