Asp.net Core 3.1基於AspectCore實現AOP,實現事務、緩存攔截器


注:下面涉及的代碼文件可查看我的LessSharp框架Github

 

最近想給我的框架加一種功能,就是比如給一個方法加一個事務的特性Attribute,那這個方法就會啟用事務處理。給一個方法加一個緩存特性,那這個方法就會進行緩存。

這個也是網上說的面向切面編程AOP。

AOP的概念也很好理解,跟中間件差不多,說白了,就是我可以任意地在方法的前面或后面添加代碼,這很適合用於緩存、日志等處理。

 

在net core2.2時,我當時就嘗試過用autofac實現aop,但這次我不想用autofac,我用了一個更輕量級的框架,AspectCore。

用起來非常非常的簡單,但一開始還是走了一點彎路,主要是網上都是net core3以下的教程,3以下的使用方法跟之前有一些不同。

先安裝NuGet包,包名:AspectCore.Extensions.DependencyInjection

然后在Program.cs類中增加一行代碼,這是net core 3的不同之處,這句添加的代碼,意思就是用AspectCore的IOC容器替換內置的。因為AOP需要依靠IOC實現,所以必須得替換掉內置的IOC。

public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
            //用AspectCore替換默認的IOC容器
            .UseServiceProviderFactory(new DynamicProxyServiceProviderFactory());
}

然后在Startup.cs類中的ConfigureServices中添加代碼。(其實這個加不加都可以,如果需要配置就加,例如全局的攔截器、只攔截哪些匹配的服務,因為我只用特性進行攔截,所以我就什么也沒配置)

services.ConfigureDynamicProxy(o=> { 
      //添加AOP的配置
});

這樣AOP就配置好了,是不是很簡單。

當然使用方面也需要注意一下,可以在接口、接口的方法、類,類的virtual方法上進行攔截。還有如果你想攔截控制器的action的話,那需要在ConfigureService里AddControllerAsServices

services.AddControllers()
//把控制器當成服務
.AddControllersAsServices()

下面我列出我的事務攔截器代碼,如果是特性攔截,就繼承AbstractInterceptorAttribute,如果要寫一個全局攔截器,就AbstractInterceptor,然后在ConfigureDynamicProxy中進行配置,這個我就不介紹了

如果你的攔截器是放在其他項目的,那要記得添加AspectCore.Core包,不要只添加AspectCore.Abstractions,我一開始就只添加了AspectCore.Abstractions,一直沒發現IsAsync、UnwrapAsyncReturnValue等一些擴展方法。

public class TransactionInterceptorAttribute : AbstractInterceptorAttribute
    {
        public async override Task Invoke(AspectContext context, AspectDelegate next)
        {
            var dbContext = context.ServiceProvider.GetService<AppDbContext>();
            //先判斷是否已經啟用了事務
            if (dbContext.Database.CurrentTransaction == null)
            {
                await dbContext.Database.BeginTransactionAsync();
                try
                {
                    await next(context);
                    dbContext.Database.CommitTransaction();
                }
                catch (Exception ex)
                {
                    dbContext.Database.RollbackTransaction();
                    throw ex;
                }
            }
            else
            {
                await next(context);
            }
        }
    }

然后我就可以這么優雅地使用事務了

我再列出我的緩存攔截器,(感謝網友的提醒,我做了一下修改,針對異步方法返回值的處理),對了,下面的ICacheHelper是我定義的一個緩存助手接口,用的是redis,我會在后面寫一篇博客

public class CacheInterceptorAttribute : AbstractInterceptorAttribute
    {
        /// <summary>
        /// 緩存秒數
        /// </summary>
        public int ExpireSeconds { get; set; }

        public async override Task Invoke(AspectContext context, AspectDelegate next)
        {
            //判斷是否是異步方法
            bool isAsync = context.IsAsync();
            //if (context.ImplementationMethod.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) != null)
            //{
            //    isAsync = true;
            //}
            //先判斷方法是否有返回值,無就不進行緩存判斷
            var methodReturnType = context.GetReturnParameter().Type;
            if (methodReturnType == typeof(void) || methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask))
            {
                await next(context);
                return;
            }
            var returnType = methodReturnType;
            if (isAsync)
            {
                //取得異步返回的類型
                returnType = returnType.GenericTypeArguments.FirstOrDefault();
            }
            //獲取方法參數名
            string param = CommonHelper.ObjectToJsonString(context.Parameters);
            //獲取方法名稱,也就是緩存key值
            string key = "Methods:" + context.ImplementationMethod.DeclaringType.FullName + "." + context.ImplementationMethod.Name;
            var cache = context.ServiceProvider.GetService<ICacheHelper>();
            //如果緩存有值,那就直接返回緩存值
            if (cache.HashExists(key, param))
            {
                //反射獲取緩存值,相當於cache.HashGet<>(key,param)
                var value = typeof(ICacheHelper).GetMethod(nameof(ICacheHelper.HashGet)).MakeGenericMethod(returnType).Invoke(cache, new[] { key, param });
                if (isAsync)
                {
                    //判斷是Task還是ValueTask
                    if (methodReturnType == typeof(Task<>).MakeGenericType(returnType))
                    {
                        //反射獲取Task<>類型的返回值,相當於Task.FromResult(value)
                        context.ReturnValue = typeof(Task).GetMethod(nameof(Task.FromResult)).MakeGenericMethod(returnType).Invoke(null, new[] { value });
                    }
                    else if (methodReturnType == typeof(ValueTask<>).MakeGenericType(returnType))
                    {
                        //反射構建ValueTask<>類型的返回值,相當於new ValueTask(value)
                        context.ReturnValue = Activator.CreateInstance(typeof(ValueTask<>).MakeGenericType(returnType), value);
                    }
                }
                else
                {
                    context.ReturnValue = value;
                }
                return;
            }
            await next(context);
            object returnValue;
            if (isAsync)
            {
                returnValue = await context.UnwrapAsyncReturnValue();
                //反射獲取異步結果的值,相當於(context.ReturnValue as Task<>).Result
                //returnValue = typeof(Task<>).MakeGenericType(returnType).GetProperty(nameof(Task<object>.Result)).GetValue(context.ReturnValue);

            }
            else
            {
                returnValue = context.ReturnValue;
            }
            cache.HashSet(key, param, returnValue);
            if (ExpireSeconds > 0)
            {
                cache.SetExpire(key, TimeSpan.FromSeconds(ExpireSeconds));
            }

        }
    }

 

我還弄了一個緩存刪除攔截器,作用就是帶有這個特性的方法執行后,會刪除相關緩存值

為什么有這個設計呢,比如說我給一個方法 GetUserList 加了緩存,那我數據改變了怎么辦,我想在User數據改變時,把這個緩存刪除掉,那我就可以在SaveUser方法上加上我這個緩存刪除攔截器,那這個方法執行后,就會把相關的緩存刪除掉了

public class CacheDeleteInterceptorAttribute : AbstractInterceptorAttribute
{
    private readonly Type[] _types;
    private readonly string[] _methods;

    /// <summary>
    /// 需傳入相同數量的Types跟Methods,同樣位置的Type跟Method會組合成一個緩存key,進行刪除
    /// </summary>
    /// <param name="Types">傳入要刪除緩存的類</param>
    /// <param name="Methods">傳入要刪除緩存的方法名稱,必須與Types數組對應</param>
    public CacheDeleteInterceptorAttribute(Type[] Types, string[] Methods)
    {
        if (Types.Length != Methods.Length)
        {
            throw new ApiFailException(ApiFailCode.OPERATION_FAIL, "Types必須跟Methods數量一致");
        }
        _types = Types;
        _methods = Methods;
    }

    public async override Task Invoke(AspectContext context, AspectDelegate next)
    {
        var cache = context.ServiceProvider.GetService<ICacheHelper>();
        await next(context);
        for (int i = 0; i < _types.Length; i++)
        {
            var type = _types[i];
            var method = _methods[i];
            string key = "Methods:" + type.FullName + "." + method;
            cache.Delete(key);
        }
    }
}

 

AOP的實現原理我也想象了一下:

要實現AOP,需要依靠IOC容器,因為它是我們類的管家,那能被攔截的類必須是IOC注入的,自己new出來的是不受攔截的。如果我想在A方法前面添加點代碼,那我告訴IOC,把代碼給它,那IOC在注入A方法所在類時,會繼承它生成一個派生類,然后重寫A方法,所以攔截方法必須得為virtual,然后A方法里寫上我要添加的代碼,再base.A()這樣。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM