Dora.Interception, 為.NET Core度身打造的AOP框架[4]:演示幾個典型應用

来源:https://www.cnblogs.com/artech/archive/2018/01/25/dora2-04.html
-Advertisement-
Play Games

為了幫助大家更深刻地認識Dora.Interception,並更好地將它應用到你的項目中,我們通過如下幾個簡單的實例來演示幾個常見的AOP應用在Dora.Interception下的實現。對於下麵演示的實例,它們僅僅是具有指導性質的應用,所以我會儘可能地簡化,如果大家需要將相應的應用場景移植到具體的... ...


為了幫助大家更深刻地認識Dora.Interception,並更好地將它應用到你的項目中,我們通過如下幾個簡單的實例來演示幾個常見的AOP應用在Dora.Interception下的實現。對於下麵演示的實例,它們僅僅是具有指導性質的應用,所以我會儘可能地簡化,如果大家需要將相應的應用場景移植到具體的項目開發中,需要做更多的優化。源代碼從這裡下載。

目錄
一、對輸入參數的格式化
二、對參數的自動化驗證
三、對方法的返回值進行自動緩存

一、對輸入參數的格式化

我們有一些方法對輸入參數在格式上由一些要求,但是我們有不希望對原始傳入的參數做過多的限制,那麼我們可以通過AOP的方式對輸入參數進行格式化。以如下這段代碼為例,Demo的Invoke方法有一個字元串類型的參數input,我們希望該值總是以大寫的形式存儲下來,但是有希望原始的輸入不區分大小寫,於是我們按照如下的方式在參數上標註一個UpperCaseAttribute。這種類型的格式轉換是通過我們自定義的一個名為ArgumentConversionInterceptor的Interceptor實現的,標準在方法上的ConvertArgumentsAttribute就是它對應的InterceptorAttribute。在Main方法中,我們按照DI的形式創建Demo對象(實際上是Demo代理對象),並調用其Invoke方法,那麼以小寫格式傳入的參數將自動轉換成大寫形式。

  1 class Program
  2 {
  3     static void Main(string[] args)
  4     {
  5         var demo = new ServiceCollection()
  6                 .AddSingleton<Demo, Demo>()
  7                 .BuildInterceptableServiceProvider()
  8                 .GetRequiredService<Demo>();
  9         Debug.Assert(demo.Invoke("foobar") == "FOOBAR");
 10     }
 11 }
 12 public class Demo
 13 {
 14     [ConvertArguments]
 15     public virtual string Invoke([UpperCase]string input)
 16     {
 17         return input;
 18     }
 19 }

接下來我們就利用Dora.Intercecption來實現這個應用場景。對應標註在參數input上的UpperCaseAttribute用於註冊一個對應的ArgumentConvertor,因為它的本質工作是進行參數的轉換,抽象的ArgumentConvertor通過如下這個介面來表示。IArgumentConvertor具有一個唯一的方式Convert來完成針對參數的轉化,該方法的輸入是一個ArgumentConveresionContext對象,通過這個上下文對象我們可以獲取代表當前參數的ParameterInfo對象和參數值。

  1 public interface IArgumentConvertor
  2 {
  3     object Convert(ArgumentConveresionContext context);
  4 }
  5 
  6 public class ArgumentConveresionContext
  7 {
  8     public ParameterInfo ParameterInfo { get; }
  9     public object Value { get; }
 10 
 11     public ArgumentConveresionContext(ParameterInfo parameterInfo, object valule)
 12     {
 13         this.ParameterInfo = parameterInfo;
 14         this.Value = valule;
 15     }
 16 }

就像Dora.Interception將Interceptor和Interceptor的提供刻意分開一樣,我們同樣將提供ArgumentConvertor的ArgumentConvertorProvider通過如下這個介面來表示。

  1 public interface IArgumentConvertorProvider
  2 {
  3     IArgumentConvertor GetArgumentConvertor();
  4 }

簡單起見,我們讓UpperCaseAttribute同時實現IArgumentConvertor和IArgumentConvertorProvider介面。在實現的Convert方法中,我們將輸入的參數轉換成大寫形式,至於實現的另一個方法GetArgumentConvertor,只需要返回它自己就可以了。

  1 [AttributeUsage(AttributeTargets.Parameter)]
  2 public class UpperCaseAttribute : Attribute, IArgumentConvertor, IArgumentConvertorProvider
  3 {
  4     public object Convert(ArgumentConveresionContext context)
  5     {
  6         if (context.ParameterInfo.ParameterType == typeof(string))
  7         {
  8             return context.Value?.ToString()?.ToUpper();
  9         }
 10         return context.Value;
 11     }
 12 
 13     public IArgumentConvertor GetArgumentConvertor()
 14     {
 15         return this;
 16     }
 17 }

我們最後來看看真正完成參數轉換的Interceptor是如何實現的。如下麵的代碼所示,在ArgumentConversionInterceptor的InvokeAsync方法中,我們通過標識方法調用上下文的InvocationContext對象的TargetMethod屬性得到表示目標方法的MethodInfo對象,然後解析出標準在參數上的所有ArgumentConverterProvider。然後通過InvocationContext的Arguments屬性得到對應的參數值,並將參數值和對應的MethodInfo對象創建出ArgumentConveresionContext對象,後者最後傳入由ArgumentConverterProvider提供的ArgumentConvertor作相應的參數。被轉換後的參數被重新寫入由InvocationContext的Arguments屬性表示的參數列表中即可。

  1 public class ArgumentConversionInterceptor
  2 {
  3     private InterceptDelegate _next;
  4 
  5     public ArgumentConversionInterceptor(InterceptDelegate next)
  6     {
  7         _next = next;
  8     }
  9 
 10     public Task InvokeAsync(InvocationContext invocationContext)
 11     {
 12         var parameters = invocationContext.TargetMethod.GetParameters();
 13         for (int index = 0; index < invocationContext.Arguments.Length; index++)
 14         {
 15             var parameter = parameters[index];
 16             var converterProviders = parameter.GetCustomAttributes(false).OfType<IArgumentConvertorProvider>().ToArray();
 17             if (converterProviders.Length > 0)
 18             {
 19                 var convertors = converterProviders.Select(it => it.GetArgumentConvertor()).ToArray();
 20                 var value = invocationContext.Arguments[0];
 21                 foreach (var convertor in convertors)
 22                 {
 23                     var context = new ArgumentConveresionContext(parameter, value);
 24                     value = convertor.Convert(context);
 25                 }
 26                 invocationContext.Arguments[index] = value;
 27             }
 28         }
 29         return _next(invocationContext);
 30     }
 31 }
 32 
 33 public class ConvertArgumentsAttribute : InterceptorAttribute
 34 {
 35     public override void Use(IInterceptorChainBuilder builder)
 36     {
 37         builder.Use<ArgumentConversionInterceptor>(this.Order);
 38     }
 39 }

二、對參數的自動化驗證

將相應的驗證規則應用到方法的參數上,進而實現自動化參數驗證是AOP的一個更加常見的應用場景。一如下的代碼片段為例,還是Demo的Invoke方法,我們在input參數上應用一個MaxLengthAttribute特性,這是微軟自身提供的一個用於限制字元串長度的ValidationAttribute。在這個例子中,我們將字元串長度限製為5個字元以下,並提供了一個驗證錯誤消息。針對對參數實施驗證的是標準在方法上的ValidateArgumentsAttribute提供的Interceptor。在Main方法中,我們按照DI的方式得到Demo對應的代理對象,並調用其Invoke方法。由於傳入的字元串(“Foobar”)的長度為6,所以驗證會失敗,後果就是會拋出一個ValidationException類型的異常,後者被進一步封裝成AggregateException異常。

  1 class Program
  2 {
  3     static void Main(string[] args)
  4     {
  5         var demo = new ServiceCollection()
  6                 .AddSingleton<Demo, Demo>()
  7                 .BuildInterceptableServiceProvider()
  8                 .GetRequiredService<Demo>();
  9             try
 10             {
 11                 demo.Invoke("Foobar");
 12                 Debug.Fail("期望的驗證異常沒有拋出");
 13             }
 14             catch (AggregateException ex)
 15             {
 16                 ValidationException validationException = (ValidationException)ex.InnerException;
 17                 Debug.Assert("字元串長度不能超過5" == validationException.Message);
 18             }
 19     }
 20 }
 21 public class Demo
 22 {
 23     [ValidateArguments]
 24     public virtual string Invoke(
 25         [MaxLength(5, ErrorMessage ="字元串長度不能超過5")]
 26         string input)
 27     {
 28         return input;
 29     }
 30 }

那麼我們看看ValidateArgumentsAttribute和由它提供的Interceptor具有怎樣的實現。從下麵給出的代碼可以看出ValidationInterceptor的實現與上面這個ArgumentConversionInterceptor具有類似的實現,邏輯非常簡單,我就不作解釋的。在這裡我順便說說另一個問題:有一些框架會將Interceptor直接應用到參數上(比如WCF可以定義ParameterInspector來對參數進行檢驗),我覺得從設計上講是不妥的,因為AOP的本質是針對方法的攔截,所以Interceptor最終都只應該與方法進行映射,針對參數驗證、轉化以及其他基於參數的處理都應該是具體某個Interceptor自身的行為。換句話說,應用在參數上的規則是為具體某種類型的Interceptor服務的,這些規則應該由對應的Interceptor來解析,但是Interceptor自身不應該映射到參數上。

  1 public class ValidationInterceptor
  2 {
  3     private InterceptDelegate _next;
  4 
  5     public ValidationInterceptor(InterceptDelegate next)
  6     {
  7         _next = next;
  8     }
  9 
 10     public Task InvokeAsync(InvocationContext invocationContext)
 11     {
 12         var parameters = invocationContext.TargetMethod.GetParameters();
 13         for (int index = 0; index < invocationContext.Arguments.Length; index++)
 14         {
 15             var parameter = parameters[index];
 16             var attributes = parameter.GetCustomAttributes(false).OfType<ValidationAttribute>();
 17             foreach (var attribute in attributes)
 18             {
 19                 var value = invocationContext.Arguments[index];
 20                 var context = new ValidationContext(value);
 21                 attribute.Validate(value, context);
 22             }
 23         }
 24         return _next(invocationContext);
 25     }
 26 }
 27 
 28 public class ValidateArgumentsAttribute : InterceptorAttribute
 29 {
 30     public override void Use(IInterceptorChainBuilder builder)
 31     {
 32         builder.Use<ValidationInterceptor>(this.Order);
 33     }
 34 }

三、對方法的返回值進行自動緩存

有時候我們會定義這樣一些方法:方法自身會進行一些相對耗時的操作並返回最終的處理結果,並且方法的輸入決定方法的輸出。對於這些方法,為了避免耗時方法的頻繁執行,我們可以採用AOP的方式對方法的返回值進行自動緩存,我們照例先來看看最終的效果。如下麵的代碼片段所示,Demo類型具有一個GetCurrentTime返回當前時間,它具有一個參數用來指定返回時間的Kind(Local、UTC或者Unspecified)。該方法上標註了一個CaheReturnValueAttribute提供一個Interceptor來緩存方法的返回值。緩存是針對輸入參數進行的,也就是說,如果輸入參數一致,得到的執行結果就是相同的,Main方法的調試斷言證實了這一點。

class Program
{
    static void Main(string[] args)
    {
        var demo = new ServiceCollection()
                .AddMemoryCache()
                .AddSingleton<Demo, Demo>()
                .BuildInterceptableServiceProvider()
                .GetRequiredService<Demo>();

        var time1 = demo.GetCurrentTime(DateTimeKind.Local);
        Thread.Sleep(1000);
        Debug.Assert(time1 == demo.GetCurrentTime(DateTimeKind.Local));

        var time2 = demo.GetCurrentTime(DateTimeKind.Utc);
        Debug.Assert(time1 != time2);
        Thread.Sleep(1000);
        Debug.Assert(time2 == demo.GetCurrentTime(DateTimeKind.Utc));

        var time3 = demo.GetCurrentTime(DateTimeKind.Unspecified);
        Debug.Assert(time3 != time1);
        Debug.Assert(time3 != time2);
        Thread.Sleep(1000);
        Debug.Assert(time3 == demo.GetCurrentTime(DateTimeKind.Unspecified));
        Console.Read();
    }
} 

public class Demo
{
    [CacheReturnValue]
    public virtual DateTime GetCurrentTime(DateTimeKind dateTimeKind)
    {
        switch (dateTimeKind)
        {
            case DateTimeKind.Local: return DateTime.Now.ToLocalTime();
            case DateTimeKind.Utc: return DateTime.UtcNow;
            default: return DateTime.Now;
        }
    }
}

現在我們實現緩存的CacheInterceptor是如何定義的,不過在這之前我們先來看看作為緩存的Key的定義。緩存的Key是具有如下定義的CacheKey,它由兩部分組成,表示方法的MethodBase和調用方法傳入的參數。

public struct Cachekey
{
public MethodBase Method { get; }
public object[] InputArguments { get; }

public Cachekey(MethodBase method, object[] arguments)
{
    this.Method = method;
    this.InputArguments = arguments;
}

public override bool Equals(object obj)
{
    if (!(obj is Cachekey))
    {
        return false;
    }
    Cachekey another = (Cachekey)obj;   
    if (!this.Method.Equals(another.Method))
    {
        return false;
    }
    for (int index = 0; index < this.InputArguments.Length; index++)
    {
        var argument1 = this.InputArguments[index];
        var argument2 = another.InputArguments[index];
        if (argument1 == null && argument2 == null)
        {
            continue;
        }

        if (argument1 == null || argument2 == null)
        {
            return false;
        }

        if (!argument2.Equals(argument2))
        {
            return false;
        }
    }
    return true;
}

public override int GetHashCode()
{
    int hashCode = this.Method.GetHashCode();
    foreach (var argument in this.InputArguments)
    {
        hashCode = hashCode ^ argument.GetHashCode();
    }
    return hashCode;
}
}

如下所示的是CacheInterceptor的定義,可以看出實現的邏輯非常簡單。CacheInterceptor採用以方法註入形式提供的IMemoryCache 來對方法調用的返回值進行緩存。在InvokeAsync方法中,我們根據當前執行上下文提供的代表當前方法的MethodBase和輸入參數創建作為緩存Key的CacheKey對象。如果根據這個Key能夠從緩存中提取相應的返回值,那麼它會直接將此值保存到執行上下文中,並且終止當前方法的調用。反之,如果返回值尚未被緩存,它會繼續後續的調用,併在調用結束之後將返回值存入緩存,以便後續調用時使用。

public class CacheInterceptor
{
    private readonly InterceptDelegate _next;            
    public CacheInterceptor(InterceptDelegate next)
    {
        _next = next;                    
    }

    public async Task InvokeAsync(InvocationContext context, IMemoryCache cache)
    {
        var key = new Cachekey(context.Method, context.Arguments);
        if (cache.TryGetValue(key, out object value))
        {
            context.ReturnValue = value;
        }
        else
        {
            await _next(context);
            cache.Set(key, context.ReturnValue);
        }
    } 
}

我們標註在GetCurrent方法上的CacheReturnValueAttribute定義如下,它只需要在重寫的Use方法中按照標準的方式註冊上面這個CacheInterceptor即可。順便再說一下,將Interceptor和註冊它的Attribute進行分離還具有一個好處:我可以為Attribute指定一個不同的名稱,比如這個CacheReturnValueAttribute。

[AttributeUsage(AttributeTargets.Method)]
public class CacheReturnValueAttribute : InterceptorAttribute
{
    public override void Use(IInterceptorChainBuilder builder)
    {
        builder.Use<CacheInterceptor>(this.Order);
    }
}


Dora.Interception, 為.NET Core度身打造的AOP框架 [1]:全新的版本
Dora.Interception, 為.NET Core度身打造的AOP框架 [2]:不一樣的Interceptor定義方式
Dora.Interception, 為.NET Core度身打造的AOP框架 [3]:Interceptor的註冊
Dora.Interception, 為.NET Core度身打造的AOP框架 [4]:演示幾個典型應用


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...