.NET Core 3.x 基於AspectCore實現AOP,實現事務、快取攔截器

China Soft發表於2024-06-19

最近想給我的框架加一種功能,就是比如給一個方法加一個事務的特性Attribute,那這個方法就會啟用事務處理。給一個方法加一個快取特性,那這個方法就會進行快取。這個也是網上說的面向切面程式設計AOP。
AOP的概念也很好理解,跟中介軟體差不多,說白了,就是我可以任意地在方法的前面或後面新增程式碼,這很適合用於快取、日誌等處理。
在net core2.2時,我當時就嘗試過用autofac實現aop,但這次我不想用autofac,我用了一個更輕量級的框架,AspectCore。

先安裝NuGet包,包名:AspectCore.Extensions.DependencyInjection,然後在Program.cs類中增加一行程式碼,這是.NET Core 3.x的不同之處,這句新增的程式碼,意思就是用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的話,那需要在ConfigureServiceAddControllerAsServices

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);
		}
	}
}
PLAINTEXT 複製 全屏

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

相關文章