Metalama简介2.利用Aspect在编译时进行消除重复代码
上文介绍到Aspect是Metalama的核心概念,它本质上是一个编译时的AOP切片。下面我们就来系统说明一下Metalama中的Aspect。
Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架
本文讲些什么
- 关于Metalama中Aspect的基础
- 一些关于Aspect的示例,最终目的是通过本篇的介绍,将在编译时自动为类型添加
INotifyPropertyChanged,实现如下效果:- 自动添加接口
- 自动添加接口实现
- 改写属性的set和get

关于Aspect
在前面的文章中我们已经介绍了使用Metalama编写简单的AOP。但是例子过于简单,也只是在代码前后加了两个Console.WriteLine,并没有太大的实际参考意义。下面我就以几个实际例子,来体现Metalama在复用代码方面的好处。
对于Metalama中的Aspect分为以下两种API
1.Aspect基础API
- TypeAspect 对类型进行编译时代码插入,见示例3
- MethodAspect
- PropertyAspect
- ParameterAspect
- EventAspect
- FieldAspect
- FieldOrPropertyAspect
- ConstructorAspect
2.Override API(重写式API)
重写试API使用更方便、更直观,与上面基础API等价,但是更容易使用
- OverrideMethodAspect 对方法进行编译时代码插入,请见下面示例1
- OverrideFieldOrPropertyAspect 对字段或属性进行编译时代码插入,请见下面示例2
- OverrideEventAspect 对事件进行编译时插入代码
以 MethodAspect 和 OverrideMethodAspect 为例,以下代码等价。
基础API MethodAspect
public class LogAttribute : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// 为方法添加重写
builder.Advices.OverrideMethod(builder.Target,nameof(this.MethodLog));
}
[Template]// 这个Template必须要加
public dynamic MethodLog()
{
Console.WriteLine(meta.Target.Method.ToDisplayString() + " 开始运行.");
var result = meta.Proceed();
Console.WriteLine(meta.Target.Method.ToDisplayString() + " 结束运行.");
return result;
}
}
Override API
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;
}
}
下面针对各种情况举一些试例。
根据每个例子的不同也分别介绍如何对方法、字段、属性进行重写。
关于meta类
通过上面的示例我们可以看到,无论是在基础API中还是Override API中,在定义AOP方法时,都使用到了meta。 meta是一个方便在Aspect中访问当前AOP上下文的工具类
常用的成员有:
| 成员 | 说明 |
|---|---|
meta.Proceed() |
等同于执行AOP作用目标直接执行,例如方法Aspect中就是原方法直接执行,属性的get中就是获取值,属性的Set中就是赋值value |
meta.Target |
当前AOP的作用目标,如作用目标是个方法则通过 meta.Target.Method 调用,如果目标是个属性则通过 meta.Target.Propery 调用 |
meta.This |
等同于使用在AOP作用目标中的this,例如可以用于获取AOP目标所在类的其它属性,方法 |
meta.ThisStatic |
用于访问AOP作用目标中的静态类型 |
示例1对方法:实现一个重试N次的功能
在平时的代码中,有这种场景,例如,我调用一个方法或API,他有一定的概率失败,例如发生了网络异常,所以我们就要设定一个重试机制(以重试3次然后放弃为例)。
假设我们有一个方法,代码详见示例中的RetryDemo。
static int _callCount;
// 此方法第一二次调用会失败,第三次会成功
static void MyMethod()
{
_callCount++;
Console.WriteLine($"当前是第{_callCount}次调用.");
if (_callCount <= 2)
{
Console.WriteLine("前两次直接抛异常:-(");
throw new TimeoutException();
}
else
{
Console.WriteLine("成功 :-)");
}
}
如果我们直接编写代码,可以使用类似以下逻辑处理。
for (int i = 0; i < 3; i++)
{
try
{
MyMethod();
break;
}
catch (Exception ex)
{
// Console.WriteLine(ex);
}
}
这样的话,对于不同的方法我们就会出现大量的重试逻辑。
那么使用Metalama我们如何进行代码改造,去掉复用代码呢。
第一步,我们需要创建一个可以修改方法的AOP的Attribute,如下:
internal class RetryAttribute : OverrideMethodAspect
{
// 重试次数
public int RetryCount { get; set; } = 3;
// 应用到方法的切面模板
public override dynamic? OverrideMethod()
{
for (var i = 0; ; i++)
{
try
{
return meta.Proceed(); // 这是实际调用方法的位置
}
catch (Exception e) when (i < this.RetryCount)
{
Console.WriteLine($"发生异常 {e.Message.GetType().Name}. 1秒后重试.");
Thread.Sleep(1000);
}
}
}
}
这里可以看到定义这个Attribute时,使用了Metalama提供的基类OverrideMethodAspect此基类是用于为方法添加编译时切面代码的Attribute.
然后我们将这个Attribute加到方法定义上。
static int _callCount;
[Retry(RetryCount = 5)]
static void MyMethod()
{
_callCount++;
Console.WriteLine($"当前是第{_callCount}次调用.");
if (_callCount <= 2)
{
Console.WriteLine("前两次直接抛异常:-(");
throw new TimeoutException();
}
else
{
Console.WriteLine("成功 :-)");
}
}
这样在编译时Metalama就会将代码编译为如下图所示。

而RetryAttribute编译后则会变为

也就是会将原有的OverrideMethod自动实现为throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.")。
最终调用结果为
当前是第1次调用.
前两次直接抛异常:-(
发生异常 String. 1秒后重试.
当前是第2次调用.
前两次直接抛异常:-(
发生异常 String. 1秒后重试.
当前是第3次调用.
成功 :-)
示例2对属性:INotifyPropertyChanged自动属性的实现
在很多处理逻辑中我们会用到INotifyPropertyChanged如我们要获取以下类的属性更改:
public class MyModel
{
public int Id { get; set; }
public string Name { get; set; }
}
我们可以这么做:
using System.ComponentModel;
public class MyModel: INotifyPropertyChanged
{
private int _id { get; set; }
public int Id {
get {
return _id;
}
set
{
if (this._id != value)
{
this._id = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Id"));
}
}
}
private string _name;
public string Name
{
get
{
return _name;
}
set
{
if (this._name != value)
{
this._name = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
但是这里,要将自动属性进行展开,并产生大量字段,对于这里的重复代码,我们可以用Metalama进行处理,我们最终要代码实现为如下:
public class MyModel: INotifyPropertyChanged
{
[NotifyPropertyChanged]
public int Id { get; set; }
[NotifyPropertyChanged]
public string Name { get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
}
当然我们也要实现NotifyPropertyChangedAttribute:
public class NotifyPropertyChangedAttribute : OverrideFieldOrPropertyAspect
{
public override dynamic OverrideProperty
{
// 保留原本get的逻辑
get => meta.Proceed();
set
{
// 判断当前属性的Value与传入value是否相等
if (meta.Target.Property.Value != value)
{
// 原本set的逻辑
meta.Proceed();
// 这里的This等同于调用类的This
meta.This.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(meta.Target.Property.Name));
}
}
}
}
这样就可以实现上面相同的效果。
示例3对类型:进一步实现INotifyPropertyChanged自动属性
刚才对属性在编译时生成INotifyPropertyChanged实现的代码中,其实可以再进一步优化,INotifyPropertyChanged接口的实现也可以通过Metalama进一步省去,最终代码为:
[TypeNotifyPropertyChanged]
public class MyModel
{
public int Id { get; set; }
public string Name { get; set; }
}
那么TypeNotifyPropertyChangedAttribute又应该怎么实现呢,Type Aspect并没有对应的Override实现,所以要使用TypeAspect。
internal class TypeNotifyPropertyChangedAttribute : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// 当前类实现一个接口
builder.Advices.ImplementInterface(builder.Target, typeof(INotifyPropertyChanged));
// 获取所有符合要求的属性
var props = builder.Target.Properties.Where(p => !p.IsAbstract && p.Writeability == Writeability.All);
foreach (var property in props)
{
//用OverridePropertySetter重写属性或字段
//参数1 要重写的属性 参数2 新的get实现 参数3 新的set实现
builder.Advices.OverrideFieldOrPropertyAccessors(property, null, nameof(this.OverridePropertySetter));
}
}
// Interface 要实现什么成员
[InterfaceMember]
public event PropertyChangedEventHandler? PropertyChanged;
// 也可以没有这个方法,直接调用 meta.This 这里只是展示另一种调用方式,更加直观
[Introduce(WhenExists = OverrideStrategy.Ignore)]
protected void OnPropertyChanged(string name)
{
this.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(name));
}
// 重写set的模板
[Template]
private dynamic OverridePropertySetter(dynamic value)
{
if (value != meta.Target.Property.Value)
{
meta.Proceed();
this.OnPropertyChanged(meta.Target.Property.Name);
}
return value;
}
}
这样就可以实现和以上相同效果的代码,以后再添加实现INotifyPropertyChanged的类,只要添加以上Attribute即可。
减少代码入侵
上面的示例3中,其实对方法还是有一定入侵的,至少要标记一个Attribute,Metalama还提供了其它无入侵的方式来为类或方法添加Aspect,我们将在后面来介绍。
先上个代码
internal class Fabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// 添加 TypeNotifyPropertyChangedAttribute 到符合规则的类上
// 当前筛选以 Model 结尾的本项目中的类型添加 TypeNotifyPropertyChangedAttribute
amender.WithTargetMembers(c =>
c.Types.Where(t => t.Name.EndsWith("Model"))
).AddAspect(t => new TypeNotifyPropertyChangedAttribute());
}
}
调试
调试 Aspect 的 Attribute时,尚不能使用断点直接调试,但可以通过以下方法:
在编译配置中除Debug或Release外还有一个LamaDebug。选择使用LamaDebug即可直接对Metalama的项目进行调试。
- 在编译时就会调用的内容中,如BuildAspect,使用
System.Diagnostics.Debugger.Break(). - 在Template方法或Override中, 使用
meta.DebugBreak。
如果是想以附加进程等方式添加断点调试,可以参考官方文档https://doc.metalama.net/aspects/debugging-aspects
Metalama简介2.利用Aspect在编译时进行消除重复代码的更多相关文章
- 【译】利用Lombok消除重复代码
当你在写Getter和Setter时,一定无数次的想过,为什么会有POJO这么烂的东西.你不是一个人!(不是骂人-)无数的开发人员花费了大量的时间来写这种样板代码,而他们本来可以利用这些时间做出更有价 ...
- C#编译时出现“不安全代码只会在使用 /unsafe 编译的情况下出现”错误的解决
原因是:在编译的代码里面有不安全类型unsafe方法或类!解决方法:将项目属性页中生成下的“允许不安全代码”复选框打上对勾即可,方法如下:项目属性对话框->生成->允许不安全代码块 选中即 ...
- 利用Oracle的row_number() over函数消除重复的记录
.select d.id,d.outer_code from dict_depts_source d order by outer_code(查看重复数据) .select d.id,d.outer_ ...
- Metalama简介3.自定义.NET项目中的代码分析
本系列其它文章 使用基于Roslyn的编译时AOP框架来解决.NET项目的代码复用问题 Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架 Metalama简介2.利用Aspect在 ...
- Metalama简介4.使用Fabric操作项目或命名空间
使用基于Roslyn的编译时AOP框架来解决.NET项目的代码复用问题 Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架 Metalama简介2.利用Aspect在编译时进行消除重 ...
- Metalama简介5.配合VisualStudio自定义重构或快速操作功能
使用基于Roslyn的编译时AOP框架来解决.NET项目的代码复用问题 Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架 Metalama简介2.利用Aspect在编译时进行消除重 ...
- Metalama简介1. 不止是一个.NET跨平台的编译时AOP框架
Metalama是一个基于微软编译器Roslyn的元编程的库,可以解决我在开发中遇到的重复代码的问题.但是其实Metalama不止可以提供编译时的代码转换,更可以提供自定义代码分析.与IDE结合的自定 ...
- C# 9 新特性:代码生成器、编译时反射
前言 今天 .NET 官方博客宣布 C# 9 Source Generators 第一个预览版发布,这是一个用户已经喊了快 5 年特性,今天终于发布了. 简介 Source Generators 顾名 ...
- 利用APT实现Android编译时注解
摘要: 一.APT概述 我们在前面的java注解详解一文中已经讲过,可以在运行时利用反射机制运行处理注解.其实,我们还可以在编译时处理注解,这就是不得不说官方为我们提供的注解处理工具APT (Anno ...
随机推荐
- 关于IIS站点最大并发量分析
关于IIS站点最大并发量分析,会有如下这个疑问:IIS站点最大并发量是多少? 一般为: IIS站点最大并发量=队列长度+进程数量[即最大工作进程数] 通过这个公式,可以基本评估出一个IIS站点的最 ...
- PCIe Tandem PROM 方法
PCIe Tandem PROM 方法 什么是Tandem PROM? 简单总结:市面多数的FPGA都是SRAM型,需要在上电时从外部存储器件完成代码的加载,对于具有PCIe功能的SRAM FPGA而 ...
- ElasticSearch7.3学习(十五)----中文分词器(IK Analyzer)及自定义词库
1. 中文分词器 1.1 默认分词器 先来看看ElasticSearch中默认的standard 分词器,对英文比较友好,但是对于中文来说就是按照字符拆分,不是那么友好. GET /_analyze ...
- hanoi(老汉诺塔问题新思维)
#include <stdio.h> //第一个塔为初始塔,中间的塔为借用塔,最后一个塔为目标塔 int i=1;//记录步数 void move(int n, char from,cha ...
- Fegin 的使用
- Error running 'App': Command line is too long. Shorten command line for App or also for Spring Boot default configuration.
找到标签 <component name="PropertiesComponent">.在标签里加一行 : <property name="dynam ...
- kafka 分布式(不是单机)的情况下,如何保证消息的顺序消费?
Kafka 分布式的单位是 partition,同一个 partition 用一个 write ahead log 组织, 所以可以保证 FIFO 的顺序.不同 partition 之间不能保证顺序. ...
- Math类有哪些常用的方法
public static int abs(int a) , public static long abs(long a), public static float abs(float a), pu ...
- 一个 Redis 实例最多能存放多少的 keys?List、Set、 Sorted Set 他们最多能存放多少元素?
理论上 Redis 可以处理多达 232 的 keys,并且在实际中进行了测试,每个实 例至少存放了 2 亿 5 千万的 keys.我们正在测试一些较大的值.任何 list.set. 和 sorted ...
- chubby 是什么,和 zookeeper 比你怎么看?
chubby 是 google 的,完全实现 paxos 算法,不开源.zookeeper 是 chubby的开源实现,使用 zab 协议,paxos 算法的变种.