65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AspectDN 在 .NET 框架上进行面向切面编程

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2023 年 7 月 15 日

CPOL

9分钟阅读

viewsIcon

5199

downloadIcon

144

如何将 AspectDN 的切面直接织入 .NET 二进制文件

引言

AspectDN 是一个用于将通知织入二进制 .NET 程序集的库。切面、通知和切点用接近 C#5 的语言描述。

AspectDN 可用于织入

  • 代码片段
  • 新类、结构、接口、枚举和委托
  • 新成员,如字段、属性、事件、构造函数

根据通知的类型,切点可以是

  • Assemblies
  • Types
  • 字段
  • 属性
  • 方法
  • 异常
  • 事件

以下示例摘自以下文章:面向切面编程 - 使用 RealProxy 类进行面向切面编程 | Microsoft,该文章说明了如何使用动态代理添加新功能,如日志记录或身份验证方法。

我们将做同样的示例,但这次使用 AspectDN 直接处理二进制文件(且无需重新编译初始应用程序的源代码)。

必备组件

请下载 zip 文件并解压缩。它包含:

  • 一个VS Application 目录,其中包含我们将用于创建初始应用程序二进制文件的 Visual Studio 应用程序。
  • 一个AspectRepository 01 目录,其中包含第一个修改的织入项目。
  • 一个AspectRepository 02 目录,其中包含第二个修改的织入项目。

AspectDN 可在此处下载:此处

由于我们处理的是二进制文件,因此您有时需要查看它们的变化。我建议使用 ILSPY,这是一个强大的工具。

目标应用程序

描述

假设您开发了一个用于管理客户存储库的应用程序。该应用程序被广泛使用并安装了许多次。该应用程序包含几个类:

  • 一个 customer
        public class Customer
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Address { get; set; }
        }
  • 一个 interface
    	public interface IRepository<T>
    	{
    		void Add(T entity);
    		void Delete(T entity);
    		void Update(T entity);
    		IEnumerable<T> GetAll();
    		T GetById(int id);
    	}
  • 一个实现上述 interfacerepository 类。
    	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);
    			}
    		}
  • 最后,一个 static main 方法为该目录提供数据。
    	static void Main(string[] args)
    	{
    		Console.WriteLine("***\r\n Begin program \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 \r\n***");
    		Console.ReadLine();
    	}

由于 AspectDN 处理二进制文件,并且在我们深入研究切面之前,您需要使用 VSSourceApplication 中的 Visual Studio 项目生成 repository.exe 应用程序。然后打开命令提示符并运行 Repository.exe,如下所示:

	...\VS SourceApplication\bin\Debug\repository.exe

您将获得以下结果:

操作步骤

最初,您将仅被要求为新部署添加新功能。

  • 应用程序必须具有身份验证系统,在任何查询或更新之前使用。
  • 应用程序必须对敏感操作进行审计和日志记录。

可能的解决方案

您有多种选择:

  • 复制源代码并在该副本中进行代码更改。
  • 更改原始应用程序源代码,并在config 文件中添加新设置,例如,以检查是否需要使用日志,具体取决于安装。
  • 直接将更改添加到部署的二进制文件中,而不更改原始应用程序源代码。

第一个解决方案将耗费大量时间,因为需要维护多个应用程序。

第二个解决方案将使代码更加复杂,并且需要重新安装所有客户端,存在引入错误的风险。

第三个解决方案仍然是最简单的,并且没有前两种解决方案的缺点。此解决方案可以使用 AspectDN 实现。

第一次更改(AspectDN 项目 01)

在第一部分中,我们将仅添加日志要求。

织入您的第一个切面

在详细介绍切面声明之前,我们将首先运行 AspectDN 项目并执行织入。

在您之前下载的AspectDN 目录下的命令提示符中执行以下命令。

	xxx\AspectDN\ApsectDb.exe -window

出现一个窗口,允许您加载项目。AspectDN 项目是一个 XML 文件,包含多个标签和属性,使我们能够加载织入上下文。

aspectdn 项目文件由以下部分组成:

	<?xml version="1.0" encoding="utf-8" ?>
	<AspectDN>
		<Project	name="AspectRepository"
					language="CS5" 
					projectDirectoryPath="XXXX\AspectProject01" 
					logPath="..\" 
					sourceTargetPath="XXXX\VS SourceApplication\bin\debug" 
					outputTargetPath="..\target" > 	
	
			<!-- c# aspect sources  -->
			<AspectSourceFiles>
				<AspectSourceFile filename ="AspectRepository.CS5_ASP"/>
			</AspectSourceFiles>
	
		</Project>
	
	</AspectDN>
  1. 属性 name 定义项目名称(AspectRepository
  2. 属性 language 定义项目中使用的语言(目前仅支持 C#5)
  3. 属性 projectDirectoryPath 定义包含文件源(aspectsadvicespointcuts)的目录的根目录。
  4. 属性 logPath 定义包含日志的目录路径,其中保存织入错误。
  5. 属性 sourceTargetPath 定义包含原始可执行应用程序(二进制文件而非源代码)的目录路径。
  6. 属性 outputTargetPath 定义织入切面后包含应用程序的目录路径。
  7. AspectSourceFiles 定义包含要织入的切面、切点和通知的所有源。所有源文件必须包含在目录中。

请注意,当路径以 ../ 开头时,表示该路径是目录的子目录。

更改为您的目录后,加载项目配置文件,然后单击织入按钮。

织入操作的顺序显示在窗口的右侧区域。如果一切顺利,您将看到以下事件:

转到 'AspectDNProject 01' 目录的target 子目录并运行 repository.exe。结果如下:

如果我们使用 ILSPY 或 ILDASM 查看新文件,我们可以看到以下差异:

让我们看看我们编写的切面以实现此结果。

切面声明

打开项目目录中的源文件 AspectRepository.CS5_Asp

首先,我们将描述用于将新成员织入 repository 类的切面。

AspectDN 中,所有通知、切点和切面都封装在一个或多个包(等同于 C# 中的命名空间)中,并如此声明。

	using System;
	using System.Threading;

	package RepositoryAspects
	{
		// place aspects, pointcuts and advices
	}

一个切面总是由以下几部分组成:

  • 一个切点,可能还有一个连接点
  • 一个通知
  • 一个原型成员的映射

以下类型成员切面用于织入我们的新方法 Log

	/* add a new member */
	addLogMethod => // (1) name of the aspect
		extend classes : classes.Name=="Repository`1" // (2) pointcut  
		with type members // aspect kind
		{
			// (3) method Log
			private void Log(string msg, object arg = null)
			{
				Console.ForegroundColor = ConsoleColor.Red;
				Console.WriteLine(msg, arg);
				Console.ResetColor();
			}
		}
  1. addLogMethod 是切面的名称。
  2. 切点表明我们正在扩展目标应用程序中包含的一个类,该类是一个名为 Repository 的泛型类(一个参数)。
  3. 通知描述了一个 Log 方法,该方法声明得就像在 C# 类中声明一样。

然后,我们声明将用于跟踪 Repository 类中 CRUD 方法的代码切面。此处仅描述 Add 方法,因为其他方法声明方式相同,除了少数细节。

	// add log in add method
	codeAspectAdd => // (1) name of the aspect
		extend around body methods : methods.Name == "Add" && 
               methods.DeclaringType.Name == "Repository`1" // (2) pointcut
		with
		{
			// (3) members of the target we need 
			prototype members
			{
				<#T>; // type parameter
				#T #Entity; // field
				void #Log(string msg, object arg); // method
			}
			
			// (4) chunk of code
			#Log("Before Adding {0}", #Entity);
			[around anchor];
			#Log("After Adding {0}", #Entity);
		} where #Log = Log, <#T> : T, #Entity= entity; // (5) prototype member mappings
  1. 切面 codeAspectAdd 的名称。
  2. 切点表明我们要将代码块织入 Repository~1Add 方法的主体内。
  3. 原型成员声明用于描述当前通知中使用的目标方法内部上下文。建议尽量实现与目标的松耦合,以确保切面的重用性。这些成员始终以 # 开头,以区别于切面声明变量。
    • <#T> 是一个泛型参数原型,它必须在目标(类或方法)中有对应的项。
    • #Entity 是目标中存在的字段、属性、变量或方法参数原型。
    • void #Log(string msg, object msg) 是目标中现有方法的原型。在这种情况下,它由前一个切面织入。
  4. 要织入的代码块由 C#5 语句组成。[around anchor] 指令指示指令将被拆分为两部分:连接点之前和之后的部分。

  5. 原型成员的名称与目标的实际成员名称进行映射。

通过使用 System.DiagnosticStackFame 来获取方法名称(addupdatedelete),可以只创建一个代码切面来处理所有 CRUD 方法。

更改的第二部分(Aspectdn 项目 02)

现在,我们为添加、修改和删除客户添加基本的身份验证检查。这些权限将仅授予具有 ADMIN 角色的用户。

执行织入

在解释第二部分中使用的切面之前,我们将执行与第一部分相同的操作,即织入切面并查看结果。我们现在处理 'AspectDNProject 02' 目录中定义的项目。更改配置文件中的目录为您的目录后,运行 aspectdn -window 命令,加载项目并织入。

在目标目录中运行织入的应用程序。使用下面的命令,不带任何参数。

要使用具有相应权限的用户进行检查,请使用以下命令运行 EXE 及其任意参数:

我们应用了哪些更改?

例如,如果您使用 ILSpy,您将在织入的应用程序中看到与原始应用程序不同的更改。

而不是在 repository~1 类中定义 LOG 方法,而是创建了一个新的 Logger 类。

另一个新类负责身份验证。它使用运行进程的用户或服务帐户的身份。日志记录器将用于跟踪身份验证。

Repository~1 的 CRUD 方法中,已织入新代码以检查身份验证。日志记录器将用于跟踪身份验证。

最后,在 main 方法中,为了能够模拟具有或不具有相应权限的身份验证,如果传递了参数,则会更改身份。

切面声明

打开项目目录中的源文件 AspectRepository.CS5_Asp

我们声明了一个类型切面,用于将两个新类织入 Repository.Exe 程序集。

	/* add Lgger and Authentification class */
	typeAspect =>
		extend assemblies : assemblies.Name == "Repository.exe" // (1) pointcut
		with types
		{
			// (2) Logger class declaration
			public static class Logger
			{
				public static void Log(string msg, object arg)
				{
					Console.ForegroundColor = ConsoleColor.Green;
					Console.WriteLine(msg, arg);
					Console.ResetColor();
				}
			}  
			// (2) Authentification class declaration
			public static class Authentication
			{
				public static bool Check(string msg)
				{
					if (Thread.CurrentPrincipal.IsInRole("ADMIN"))
					{
						Logger.Log("User authenticated - You can execute '{0}' ", msg);
						return true;
					}

					Logger.Log("User not authenticated - You can't execute '{0}' ", msg);
					return false;
				}
			}  
		} namespace target Repository; // (3) target namespace
  1. 这次,切点设置在 Repository.exe 程序集上。
  2. 我们在此类型切面中声明了两个类,就像在 C# 项目中声明类一样。请注意,Authentication 类使用 Logger
  3. 对于类型切面,必须在目标中指明类将被织入的命名空间。

对于 CRUD 方法,我们声明了以下代码切面,该切面使用前两个类。

	// add the authentifcation for the add method
	codeAspectAdd => 
		extend around body methods : methods.Name == "Add" && 
               methods.DeclaringType.Name == "Repository`1" // pointcut
		with
		{
			// target members
			prototype members 
			{
				<#T>;
				#T #Entity;
			}
			
			if (!typeAspect.Authentication.Check("Add"))
                return;
			
			typeAspect.Logger.Log("Before Adding {0}", #Entity);
			[around anchor];
			typeAspect.Logger.Log("After Adding {0}", #Entity);
			
		} where typeAspect.Authentication from Repository, 
          typeAspect.Logger from Repository, <#T> : T, #Entity= entity;

要使用类型切面中的类型,必须在类型名称前加上类型通知(在本例中为与切面相同的名称),并且您需要指定它们属于目标的哪个命名空间。

让我们看看用于更改 main 方法中用户身份的最后一个代码切面。

	codeAspectTest =>
	extend before body methods : methods.Name == "Main"
	with
	{
		prototype members
		{
			string[] #args;
		}
		if (#args!=null && #args.Length>0)
		{
			Thread.CurrentPrincipal =
				new GenericPrincipal(new GenericIdentity("Administrator"),
				new[] { "ADMIN" });
		}
		
	} where #args = args;

如您所见,我们声明了一个 string 数组作为原型成员,它映射了方法参数。所有指令均使用 C#5 语言。

结论

与需要代码更改才能工作的 RealProxy 类不同,AspectDN 以极大的简便性和极少的努力将切面直接织入可执行文件中。

使用的语言接近 C#5,因此开发人员可以轻松快速地编写切面。

有很多教程供您探索和发现,例如如何使用 AspectDN 定制分层应用程序(包括持久化),方法是添加新的功能要求。

历史

  • 2023 年 7 月 15 日:初始版本
© . All rights reserved.