.NET静态代码织入——肉夹馍(Rougamo) 发布1.1.0

肉夹馍(//github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应用启动的初始化时间让服务更快可用,同时还能对静态方法进行AOP。
上一篇文章 中介绍了1.0.0版本肉夹馍的功能,1.0.0版本能够进行的AOP操作主要是日志记录以及APM操作,给出的示例项目也是OpenTelemetry的APM项目。在上一篇文章的评论以及github issue中都有朋友询问是否能处理异常以及修改返回值等操作,最终拖了较长一段时间于近期发布了1.1.0版本实现了这些功能。

快速开始

# 添加NuGet引用
dotnet add package Rougamo.Fody
public class TestService
{
    [Fact]
    public async void Test1()
    {
        var v1 = await M1();
        Assert.Null(v1);

        var v2 = Sum(1, null);
        Assert.Equal(-1, v2);

        var v3 = await M2();
        Assert.Empty(v3);
    }

    [MuteException]
    public async Task<string> M1()
    {
        throw new NotImplementedException();
    }

    [ArgNullCheck]
    public int Sum(int? a, int? b)
    {
        return a.Value + b.Value;
    }

    [ReturnNullCheck]
    public async Task<string> M2()
    {
        await Task.Yield();
        return null;
    }
}

public class MuteExceptionAttribute : MoAttribute
{
    public override void OnException(MethodContext context)
    {
        if (context.RealReturnType == typeof(string))
        {
            context.HandledException(this, null);
        }
    }
}

public class ArgNullCheckAttribute : MoAttribute
{
    public override void OnEntry(MethodContext context)
    {
        foreach (var arg in context.Arguments)
        {
            if (arg == null)
            {
                context.ReplaceReturnValue(this, -1);
            }
        }
    }
}

public class ReturnNullCheckAttribute : MoAttribute
{
    public override void OnSuccess(MethodContext context)
    {
        if (context.ReturnValue == null)
        {
            context.ReplaceReturnValue(this, string.Empty);
        }
    }
}

在上面的示例代码中MuteExceptionAttribute重写了OnException通过MethodContext.HandledException表明异常已处理并将返回值设置为null
ArgNullCheckAttribute重写了OnEntry通过MethodContext.ReplaceReturnValue设置了返回值,由于OnEntry是在执行方法前调用,这种方式会在OnEntry执行完毕之后直接将ReplaceReturnValue设置的返回值作为方法的返回值直接返回,一般参数验证、缓存逻辑会用到;
ReturnNullCheckAttribute重写了OnSuccess通过MethodContext.ReplaceReturnValue修改了实际的返回值,示例中通过这种方式避免返回null值。

注意事项

  • 如果方法是async Task那么MethodContext.RealReturnType取值为typeof(void),如果是async Task<T>那么取值为typeof(T),但如果返回值为TaskTask<T>但并没有使用async写法,那么其值就是typeof(Task)typeof(Task<T>),这样设定的好处是,你设置的返回值类型与该属性的值相同即可,不用考虑方法是否异步
  • 不论是异常处理还是设置/修改返回值,设置的返回值类型必须与方法定义的返回类型(MethodContext.RealReturnType)相同,类型不同时运行时会报错
  • OnExit中调用MethodContext.ReplaceReturnValue无法修改返回值

补充说明

上一篇文章 中由于是第一篇文章,介绍的东西较多,部分功能并没有在文章中详细说明,本篇由于篇幅较短,所以会补上一些说明,不过这里也不会介绍全部的,详细的介绍可以移步 github(//github.com/inversionhourglass/Rougamo)

Iterator / AsyncIterator 不支持修改返回值和异常处理

IteratorAsyncIterator也就是下面的写法

public IEnumerable<int> Iterator(int count)
{
    yield return 1;
    yield return 2;
    yield return 3;
}

public async IAsyncEnumerable<int> AsyncIterator(int count)
{
    yield return 3;
    await Task.Yield();
    yield return 2;
    await Task.Yield();
    yield return 1;
}

之所以不支持,是因为它们并不直接返回一个集合,而是返回一个状态机(StateMachine),使用foreach迭代时实际每次迭代执行状态机的MoveNext方法获取本次迭代的返回值,考虑到实现这种特殊机制的复杂性以及平时使用的频率,当前对此种类型不进行支持。

Iterator / AsyncIterator 不支持记录返回值

同样的,IteratorAsyncIterator默认也无法通过MethodContext.ReturnValue获取方法的返回值,但可以通过FodyWeavers.xmlRougamo节点增加属性配置enumerable-returns="true"来记录IteratorAsyncIterator的返回值到MethodContext.ReturnValue

<Weavers xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Rougamo enumerable-returns="true" />
</Weavers>

这个设定是因为状态机并没有保存所有的元素到一个集合中,每个元素都是一次一次调用MoveNext执行代码返回的,如果你使用foreach遍历IteratorAsyncIterator,并且对每次遍历的元素使用玩之后并没有进行保存,那么上一个元素可能在你遍历下一个元素时被GC回收。记录它们的返回值的实现方式是额外建立一个集合保存每次迭代的元素值,这种方式对上面说的的foreach遍历的情况来说会产生额外的内存消耗,而如果迭代器的元素很多,或者每个元素本身很占内存,那么这种方式可能会额外占用大量内存空间,所以开启这个开关前需要考虑一番。

最后

如果在使用肉夹馍的过程中遇到了什么问题,或者希望增加一些什么样的功能,欢迎到github(//github.com/inversionhourglass/Rougamo)里提issue,不过对于新功能,可能会有一个较长的周期才能完成并发布正式版。
随着SourceGenerator的应用越来越广泛,Mono.Cecil的应用场景被进一步压缩,一开始提到的动态代理现在也能通过SourceGenerator在编译时生成代理类,这是一件好事,相比晦涩易错的IL,SourceGenerator提供的语法树更加方便易懂且不易出错,但这并不代表Mono.Cecil应该退场了(至少现在不是),Mono.Cecil虽然门槛高,但他的功能也同样强大,直接修改IL是SourceGenerator和`Emit所无法做到的(至少现在是这样),如果在以后的编程之路中遇到了SourceGenerator和`Emit无法解决的问题,希望你能想起还有Mono.CecilFody这条路,如果有时间可以尝试一下,也希望肉夹馍这个项目能给你带来一些参考价值。