使用基于Roslyn的编译时AOP框架来解决.NET项目的代码复用问题
Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架
Metalama简介2.利用Aspect在编译时进行消除重复代码
Metalama简介3.自定义.NET项目中的代码分析
Fabric
通过修改项目、命名空间、类型来达到一些效果,这引起修改包括:添加Aspect
或添加代码分析
前文中我们写过一个简单的Aspect
:
public class LogAttribute : OverrideMethodAspect { public override dynamic? OverrideMethod() { Console.WriteLine(meta.Target.Method.ToDisplayString() + " 开始运行."); var result = meta.Proceed(); Console.WriteLine(meta.Target.Method.ToDisplayString() + " 结束运行."); return result; } }
当我们使用它时,我们要在对应的方法上添加这个Attribute
:
[Log] private static int Add(int a, int b) //... ...
那么当我们有一个Aspect
要在项目中大量使用时,在每个方法上添加这个Aspect
当然是一种方法,但是这种方法有2个缺点:
[Log]
此时我们就可以使用Fabric
为所有符合要求的方法添加指定的Aspect
:
internal class Fabric : ProjectFabric { // 这个是重写项目的Fabric中修改项目的方法 public override void AmendProject(IProjectAmender amender) { // 添加 LogAttribute 到符合规则的方法上 // 为名为 Add 且 private 的方法添加 LogAttribute amender.WithTargetMembers(c => c.Types.SelectMany(t => t.Methods) .Where(t => t.Name == "Add" && t.Accessibility == Metalama.Framework.Code.Accessibility.Private) ).AddAspect(t => new LogAttribute()); } }
这样就可以在不入侵现有代码的情况下为指定的方法添加Aspect
。
上文中我们提到,我们可以通过Aspect
为代码添加代码分析,当我们要将一个包含(且仅包含)代码分析的Aspect
应用于一批代码时,当然我们可以按本文示例1
中的方法,直接使用Fabric
将包含代码分析的Aspect
应用于指定代码。
但还有另外一种方法,我们可以直接在Fabric
中定义应用于指定代码的代码分析。
下面示例,我们验证所有类中的私有字段必须符合 _camelCase
,并且使用一个NamespaceFabric
来实现:
namespace FabricCamelCaseDemo; class Fabric : NamespaceFabric { private static readonly DiagnosticDefinition<string> _warning = new( "DEMO04", Severity.Warning, "'{0}'必须使用驼峰命名法并以'_'开头"); // 这个是命名空间的Fabric中修改命名空间规则 的方法 public override void AmendNamespace(INamespaceAmender amender) { // 取所有非static 的private的字段,并添加代码分析 amender.WithTargetMembers(c => c.AllTypes.SelectMany(t=>t.Fields) .Where(t => t.Accessibility == Accessibility.Private && !t.IsStatic ) ) //preview 0.5.8之前为 RegisterFinalValidator .Validate(this.FinalValidator); } private void FinalValidator(in DeclarationValidationContext context) { var fullname = context.Declaration.ToDisplayString(); var fieldName = fullname.Split('.').LastOrDefault(); if (fieldName!=null && (!fieldName.StartsWith("_") || !char.IsLower(fieldName[1]))) { context.Diagnostics.Report(_warning.WithArguments(fieldName)); } } }
当然因为当前使用的是NamespaceFabric
所以该规则只应用于当前命名空间如,我们如果在另外一个命名空间中定义一个违反规则的字段的话,并不会有警告。
namespace FabricCamelCase; internal class OtherNamespace { int count = 0; int _total = 0; public int Add() { count++; _total++; return count + _total; } }
开始前伪造一个需求,假设我有一个类AddUtils
专门处理加法操作,它里面应该有从2个到15个参数的Add方法15个(当然我知道,可以使用params
等方法实现,所以这里是个伪需求)。
最终效果为
public class AddUtils { public int Add2(int x1, int x2) { var result = 0; result += x1; result += x2; return 2; } public int Add3(int x1, int x2, int x3) { var result = 0; result += x1; result += x2; result += x3; return 3; } // 以此类推... 下面省去若干方法 }
那么我们可以用Metalama
如此实现
using System.Reflection.Emit; using Metalama.Framework.Aspects; using Metalama.Framework.Fabrics; public class AddUtils { private class Fabric : TypeFabric { // 实现的方法体 [Template] public int MethodTemplate() { var num = (int) meta.Tags["nums"]!; var result = 0; foreach (var targetParameter in meta.Target.Parameters) { result += targetParameter.Value; } return num; } public override void AmendType(ITypeAmender amender) { for (var i = 2; i < 15; i++) { // 生成一个方法 var methodBuilder = amender.Advices.IntroduceMethod( amender.Type, nameof(this.MethodTemplate), tags: new TagDictionary { ["nums"] = i }); // 方法名 methodBuilder.Name = "Add" + i; // 添加参数 for (int parameterIndex = 1; parameterIndex <= i; parameterIndex++) { methodBuilder.AddParameter($"x{parameterIndex}", typeof(int)); } } } } }
本章源代码:https://github.com/chsword/metalama-demo
Metalama官方文档: https://doc.metalama.net/
Metalama Nuget包: https://www.nuget.org/packages/Metalama.Framework/0.5.11-preview