天天看點

ASP.NET Core搭建多層網站架構【9.2-使用Castle.Core實作動态代理攔截器】

基于ASP.NET Core 3.1 WebApi搭建後端多層網站架構【9.2-使用Castle.Core實作動态代理攔截器】

介紹了如何對業務層方法進行攔截,捕獲業務方法發生的錯誤,然後統一進行日志記錄,避免在每個業務方法中進行try catch捕獲異常

2020/01/31, ASP.NET Core 3.1, VS2019, Autofac.Extras.DynamicProxy 4.5.0, Castle.Core.AsyncInterceptor 1.7.0

摘要:基于ASP.NET Core 3.1 WebApi搭建後端多層網站架構【9.2-使用Castle.Core實作動态代理攔截器】

文章目錄

此分支項目代碼

本章節介紹了如何對業務層方法進行攔截,捕獲業務方法發生的錯誤,然後統一進行日志記錄,避免在每個業務方法中進行try catch捕獲異常。借助Autofac和Castle.Core實作動态代理攔截器,其中使用Castle.Core.AsyncInterceptor包實作異步攔截器。

添加包引用

MS.Component.Aop

類庫中添加以下包引用:

<ItemGroup>
  <PackageReference Include="Autofac.Extras.DynamicProxy" Version="4.5.0" />
  <PackageReference Include="Castle.Core.AsyncInterceptor" Version="1.7.0" />
</ItemGroup>
           
  • Autofac.Extras.DynamicProxy包和Autofac.Extensions.DependencyInjection包中的autofac版本最好一緻,否則可能會出現代理攔截器注冊失敗的情況
  • 可以先按我的包版本使用,確定攔截器無問題,再把包更新到最新版
  • Autofac.Extras.DynamicProxy包是使用Autofac和Castle.Core實作動态代理攔截器
  • Castle.Core.AsyncInterceptor包是實作異步攔截器(否則隻能對同步方法進行攔截)
  • MS.Component.Aop

    類庫要依賴

    MS.WebCore

    類庫。

編寫服務攔截器

MS.Component.Aop

類庫中添加LogAop檔案夾,在該檔案夾下建立

AopHandledException.cs

LogInterceptor.cs

LogInterceptorAsync.cs

AopHandledException.cs

using System;

namespace MS.Component.Aop
{
    /// <summary>
    /// 使用自定義的Exception,用于在aop中已經處理過的異常,在其他地方不用重複記錄日志
    /// </summary>
    public class AopHandledException : ApplicationException
    {
        public string ErrorMessage { get; private set; }
        public Exception InnerHandledException { get; private set; }
        //無參數構造函數
        public AopHandledException()
        {

        }
        //帶一個字元串參數的構造函數,作用:當程式員用Exception類擷取異常資訊而非 MyException時把自定義異常資訊傳遞過去
        public AopHandledException(string msg) : base(msg)
        {
            this.ErrorMessage = msg;
        }
        //帶有一個字元串參數和一個内部異常資訊參數的構造函數
        public AopHandledException(string msg, Exception innerException) : base(msg)
        {
            this.InnerHandledException = innerException;
            this.ErrorMessage = msg;
        }
        public string GetError()
        {
            return ErrorMessage;
        }
    }
}
           

這裡自定義了一個AopHandledException異常類型,目的是為了:

  • 攔截器捕獲異常後已經進行了日志記錄,這個異常可能需要繼續抛出去,此時用AopHandledException異常類型包一層異常,這樣後面再捕獲到的異常就能判斷是從Aop中抛出來的異常了,不再重複記錄日志

LogInterceptor.cs

using Castle.DynamicProxy;

namespace MS.Component.Aop
{
    public class LogInterceptor : IInterceptor
    {
        private readonly LogInterceptorAsync _logInterceptorAsync;

        public LogInterceptor(LogInterceptorAsync logInterceptorAsync)
        {
            _logInterceptorAsync = logInterceptorAsync;
        }

        public void Intercept(IInvocation invocation)
        {
            _logInterceptorAsync.ToInterceptor().Intercept(invocation);
        }
    }
}
           

這個是主攔截器,繼承自IInterceptor,不管是同步方法還是異步方法,都将會走其中的Intercept方法,然後會在LogInterceptorAsync中再去區分是異步方法還是同步方法

LogInterceptorAsync.cs

using Castle.DynamicProxy;
using Microsoft.Extensions.Logging;
using MS.Common.Extensions;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace MS.Component.Aop
{
    public class LogInterceptorAsync : IAsyncInterceptor
    {
        private readonly ILogger<LogInterceptorAsync> _logger;

        public LogInterceptorAsync(ILogger<LogInterceptorAsync> logger)
        {
            _logger = logger;
        }

        /// <summary>
        /// 同步方法攔截時使用
        /// </summary>
        /// <param name="invocation"></param>
        public void InterceptSynchronous(IInvocation invocation)
        {
            try
            {
                //調用業務方法
                invocation.Proceed();
                LogExecuteInfo(invocation, invocation.ReturnValue.ToJsonString());//記錄日志
            }
            catch (Exception ex)
            {
                LogExecuteError(ex, invocation);
                throw new AopHandledException();
            }
        }

        /// <summary>
        /// 異步方法傳回Task時使用
        /// </summary>
        /// <param name="invocation"></param>
        public void InterceptAsynchronous(IInvocation invocation)
        {
            try
            {
                //調用業務方法
                invocation.Proceed();
                LogExecuteInfo(invocation, invocation.ReturnValue.ToJsonString());//記錄日志
            }
            catch (Exception ex)
            {
                LogExecuteError(ex, invocation);
                throw new AopHandledException();
            }
        }

        /// <summary>
        /// 異步方法傳回Task<T>時使用
        /// </summary>
        /// <typeparam name="TResult"></typeparam>
        /// <param name="invocation"></param>
        public void InterceptAsynchronous<TResult>(IInvocation invocation)
        {
            //調用業務方法
            invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
        }
        private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
        {
            try
            {
                //調用業務方法
                invocation.Proceed();
                Task<TResult> task = (Task<TResult>)invocation.ReturnValue;
                TResult result = await task;//獲得傳回結果
                LogExecuteInfo(invocation, result.ToJsonString());

                return result;
            }
            catch (Exception ex)
            {
                LogExecuteError(ex, invocation);
                throw new AopHandledException();
            }
        }

        #region helpMethod
        /// <summary>
        /// 擷取攔截方法資訊(類名、方法名、參數)
        /// </summary>
        /// <param name="invocation"></param>
        /// <returns></returns>
        private string GetMethodInfo(IInvocation invocation)
        {
            //方法類名
            string className = invocation.Method.DeclaringType.Name;
            //方法名
            string methodName = invocation.Method.Name;
            //參數
            string args = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray());


            if (string.IsNullOrWhiteSpace(args))
            {
                return $"{className}.{methodName}";
            }
            else
            {
                return $"{className}.{methodName}:{args}";
            }
        }
        private void LogExecuteInfo(IInvocation invocation, string result)
        {
            _logger.LogDebug("方法{0},傳回值{1}", GetMethodInfo(invocation), result);
        }
        private void LogExecuteError(Exception ex, IInvocation invocation)
        {
            _logger.LogError(ex, "執行{0}時發生錯誤!", GetMethodInfo(invocation));
        }
        #endregion
    }
}
           

這裡是對方法攔截的主要實作:

  • InterceptSynchronous是同步方法攔截時使用
  • InterceptAsynchronous是異步方法傳回Task時使用
  • InterceptAsynchronous是異步方法傳回Task(包含傳回值)時使用
  • invocation.Proceed();

    這句話即是調用真正的業務方法,是以在該方法之前可以做一些權限判斷的内容,在該方法之後可以擷取記錄業務傳回結果
  • 可以在invocation參數中擷取方法名稱、傳遞的參數等資訊
  • 如果做了使用者登入,可以通過構造函數依賴注入IHttpContextAccessor以拿到執行業務者的資訊(其實使用NLog記錄日志時,NLog也能擷取到登入使用者的資訊,就是NetUserIdentity這個參數)
  • 在攔截器中,我對每個業務方法都以Debug的日志等級記錄了調用業務方法的傳回結果

至此對業務方法進行攔截,以Debug的日志等級記錄了調用業務方法的傳回結果,并捕獲所有業務方法的異常已經完成。

封裝Ioc注冊

MS.Component.Aop

類庫中添加

AopServiceExtensions.cs

類:

using Autofac;
using Autofac.Extras.DynamicProxy;
using System;
using System.Reflection;

namespace MS.Component.Aop
{
    public static class AopServiceExtension
    {
        /// <summary>
        /// 注冊aop服務攔截器
        /// 同時注冊了各業務層接口與實作
        /// </summary>
        /// <param name="builder"></param>
        /// <param name="serviceAssemblyName">業務層程式集名稱</param>
        public static void AddAopService(this ContainerBuilder builder, string serviceAssemblyName)
        {
            //注冊攔截器,同步異步都要
            builder.RegisterType<LogInterceptor>().AsSelf();
            builder.RegisterType<LogInterceptorAsync>().AsSelf();

            //注冊業務層,同時對業務層的方法進行攔截
            builder.RegisterAssemblyTypes(Assembly.Load(serviceAssemblyName))
                .AsImplementedInterfaces().InstancePerLifetimeScope()
                .EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy;
                .InterceptedBy(new Type[] { typeof(LogInterceptor) })//這裡隻有同步的,因為異步方法攔截器還是先走同步攔截器 
                ;

            //業務層注冊攔截器也可以使用[Intercept(typeof(LogInterceptor))]加在類上,但是上面的方法比較好,沒有侵入性
        }
    }
}
           

說明:

  • 在AddAopService方法中,傳遞了業務層程式集名稱,使用Autofac統一注冊業務層所有的接口和實作(這樣就不用每個接口都寫一次注冊了),注冊類型為InstancePerLifetimeScope
  • 注冊業務的同時,開啟了代理攔截器:EnableInterfaceInterceptors
  • 開啟代理攔截器的同時,InterceptedBy傳遞了要使用哪些攔截器進行攔截,這裡隻有同步的,因為異步方法攔截器還是先走同步攔截器
  • 業務層注冊攔截器也可以使用[Intercept(typeof(LogInterceptor))]加在類上,但是上面的方法比較好,沒有侵入性

擷取業務層程式集名稱

MS.Services

中添加

ServiceExtensions.cs

using System.Reflection;

namespace MS.Services
{
    public static class ServiceExtensions
    {
        /// <summary>
        /// 擷取程式集名稱
        /// </summary>
        /// <returns></returns>
        public static string GetAssemblyName()
        {
            return Assembly.GetExecutingAssembly().GetName().Name;
        }
    }
}
           

用于擷取業務層的程式集名稱,提供給Autofac進行批量的注冊接口和實作。

注冊Aop服務

MS.WebApi

應用程式中,Startup.cs:

在ConfigureContainer方法裡删掉原先對IBaseService、IRoleService的接口注冊,使用剛寫的Aop注冊,給業務層批量注冊同時開啟代理攔截:

//using MS.Services;
//以上代碼添加至using
//注冊aop攔截器 
//将業務層程式集名稱傳了進去,給業務層接口和實作做了注冊,也給業務層各方法開啟了代理
builder.AddAopService(ServiceExtensions.GetAssemblyName());
           

完成後,代碼如下圖所示

ASP.NET Core搭建多層網站架構【9.2-使用Castle.Core實作動态代理攔截器】

至此,業務層方法的代理攔截都完成了,執行業務時,會在控制台顯示每次業務的調用傳回結果,如果遇到異常,會統一捕獲并記錄日志然後把異常包裝一次再抛出去

啟動項目,打開Postman測試接口:

ASP.NET Core搭建多層網站架構【9.2-使用Castle.Core實作動态代理攔截器】

可以看到傳回結果顯示出來了,可以打斷點看看程式到底怎麼通過攔截器的。

這裡隻是實作了業務層方法攔截,異常日志的捕獲,可以做更多例如權限驗證的攔截器。

項目完成後,如下圖所示

ASP.NET Core搭建多層網站架構【9.2-使用Castle.Core實作動态代理攔截器】