无需编码,自动实现“异步 Request-Reply”模式

前言

上次,我们虽然用代码实现了“异步 Request-Reply 模式”,但是需要为每一个长时间操作 API 实现一个对应的 AsyncXXX 操作。

其实,可以尝试用 Source Generators 减少这种重复性劳动。.

实现思路

  • Controller 类必须是partial,这样才能为它额外增加新方法;

  • 为每个长时间操作 API 声明一个 AsyncMethodAttribute,这样 Source Generators 才知道为谁实现对应的同步操作;

  • 遍历所有声明了 AsyncMethodAttribute 的方法,为其编写实现方法。

具体代码

1.添加 AsyncMethodAttribute

向待编译项目加入 AsyncMethodAttribute 代码:

const string asyncMethodAttributeText = @"using System;

namespace AsyncMethodGenerator
{
    public sealed class AsyncMethodAttribute : Attribute
    {
        public AsyncMethodAttribute()
        {
        }
    }
}
";

context.AddSource("AsyncMethodAttribute", SourceText.From(asyncMethodAttributeText, Encoding.UTF8));

2.遍历 AsyncMethodAttribute 声明方法

找到声明了 AsyncMethodAttribute 的所有方法:

private string GenerateMethods(SyntaxList<MemberDeclarationSyntax> members)
{
    StringBuilder stringBuilder = new StringBuilder();

    foreach (var member in members)
    {
        if(member is MethodDeclarationSyntax method && HasAsyncMethodAttribute(method))
        {
            stringBuilder.Append(GenerateAsyncMethod(method));
        }
    }
    
    return stringBuilder.ToString();
}

private bool HasAsyncMethodAttribute(MethodDeclarationSyntax method)
{
    var hasAttribute = false;
    foreach (var attributeList in method.AttributeLists)
    {
        foreach (var attribute in attributeList.Attributes)
        {
            if (attribute.Name.ToString().Equals("AsyncMethod"))
            {
                hasAttribute = true;
            }
        }
    }

    return hasAttribute;
}

3.生成 AsyncMethod 代码

根据原方法定义,生成 AsyncMethod 方法:

private string GenerateAsyncMethod(MethodDeclarationSyntax method)
{
    var stringBuilder = new StringBuilder();

    foreach (var attributeList in method.AttributeLists)
    {
        foreach (var attribute in attributeList.Attributes)
        {
            if (attribute.Name.ToString().Equals("Route"))
            {
                stringBuilder.Append($@"[Route(""async/{attribute.ArgumentList.Arguments[0].ToString().Trim('"')}"")]");
            }
            else
            {
                stringBuilder.Append($"[{attribute}]");
                stringBuilder.Append("\r\n");
            }
        }
    }

    stringBuilder.Append($"public async Task<IActionResult> Async{method.Identifier} ");
    stringBuilder.Append($"({method.ParameterList.Parameters})");
    stringBuilder.Append("\r\n");

    stringBuilder.Append($@"
        {{
            string id = Guid.NewGuid().ToString();
            string responseValue =  $@""/status/{{id}}"";

            _cache.SetString(id, responseValue);

            Task.Factory.StartNew(() =>
            {{
                var result = {GenerateCallingMethod(method)}.Result;
                _cache.SetString(id + ""_result"", JsonConvert.SerializeObject(result));
            }});

            return Accepted(responseValue);
        }}
    ");

    return stringBuilder.ToString();
}

4.使用

现在,就可以在目标项目中使用 AsyncMethod 方法了:

[ApiController]
[Route("[controller]")]
public partial class WeatherForecastController : ControllerBase
{
    ...

    [HttpGet]
    [Route("get")]
    [AsyncMethod]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        ...
    }
}

注意 WeatherForecastController 是 partial class

结论

有了 Source Generators,可以让编译器帮我们自动实现“异步 Request-Reply 模式”了!