通过设计时代码生成重写通用方法

问题描述 投票:0回答:1

我想问一个关于我想用 C# 中的代码生成器(或其他方法)解决的问题的一个相当通用的问题,我想知道是否可以用这样的方法来做到这一点..

我遇到的问题是 - 有一个第三方 API 公开了

OpenAPI
规范。我正在使用
nswag studio
生成 C# 客户端代码。

问题是规范中似乎包含无效数据。因此,我不想依赖第三方 api 提供商来解决问题(我已经报告过) - 我想以自己的方式解决问题..

问题来自于从 API 端点检索数据的方法,如下所示(简化版本):

public virtual async Task<SomeResponseType> GetDataAsync(CancellationToken cancellationToken) {
  var response = GetResponse();
  return ParseResponse<SomeResponseType>(resonse);
}

问题在于返回类型。正确的类型是对象数组,即

SomeResponseType[]
,否则我会收到 JSON 反序列化异常。

鉴于我不想每次出现规范错误时都重写整个方法 - 我希望 C# 为我生成这样的方法!

我正在寻求的解决方案(我认为)必须对代码生成做一些事情。

编译器是否有可能以某种方式为我生成一个方法重写,该方法重写将接受给定类型重写,而不是默认类型?例如

public virtual async Task<SomeResponseType[]> GetDataAsync2(CancellationToken cancellationToken) {
  var response = GetResponse();
  return ParseResponse<SomeResponseType[]>(response);
}

理论上,使用代码生成 - 是否可以创建一个新方法并用属性标记它,以便代码生成逻辑拾取它并基于原始方法生成代码?沿着这些思路的东西应该触发生成,其中

GetDataAsync
参数是用作基础的方法..

    [CodeGen("GetDataAsync")]
    public partial Task<TradingPairsInfoResponse[]> GetDataAsync2(CancellationToken cancellationToken);

欢迎任何想法、评论和提示..

我提前道歉..我从未使用过代码生成器,所以我不知道它们有什么能力。在我花了 4 个小时研究这个主题并意识到我试图解决的问题无法以这种方式解决之前,我想向社区学习……

使用

.net7 +

c# .net-core code-generation
1个回答
0
投票

在研究了该主题并进行了无数实验之后,我能够创建一个源生成器。目的是首先制作一些可以工作的东西,因此可以对下面的代码进行大量改进和优化,因此如果您打算使用它 - 请重新测试并根据您的需求进行调整

NswagGeneratedClient

public partial class NswagGeneratedClient
{
    [GenerateGenericOverride(nameof(GetDataAsync))]           
    public partial Task<DataType[]> GetDataAsync2(CancellationToken cancellationToken);
}

属性接受强制参数 - 我们要定位的方法名称。基本上会发生什么 - 将生成新方法,但目标方法的泛型将被新部分方法中提供的泛型替换。

GenerateGenericOverrideAttribute

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class GenerateGenericOverrideAttribute : Attribute 
{
    public string MethodName { get; }

    public GenerateGenericOverrideAttribute(string methodName)
    {
        MethodName = methodName;
    }
}

AugmentingGenerator
- 驻留在单独的源生成器 .net 标准库中。在互联网上查找此主题以获取更多详细信息。不然帖子会大好几倍。


namespace ClientGenerator
{
    [Generator]
    public class AugmentingGenerator : ISourceGenerator
    {
        public void Initialize(GeneratorInitializationContext context)
        {
            context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
        }

        public void Execute(GeneratorExecutionContext context)
        {
            MySyntaxReceiver syntaxReceiver = (MySyntaxReceiver)context.SyntaxReceiver;
            if (syntaxReceiver == null || string.IsNullOrEmpty(syntaxReceiver.ClassName))
                return;

            MethodDeclarationSyntax targetMethod = null;
            foreach (var tree in context.Compilation.SyntaxTrees)
            {
                var treeRoot = tree.GetRoot();
                if (treeRoot == null) 
                    continue;

                foreach (var node in treeRoot.ChildNodes().OfType<BaseNamespaceDeclarationSyntax>())
                {
                    var nsName = GetName(node);
                    if (nsName != syntaxReceiver.NamespaceName)
                        continue;

                    foreach (var node2 in node.ChildNodes().OfType<ClassDeclarationSyntax>())
                    {
                        var cName = GetName(node2);
                        if (cName != syntaxReceiver.ClassName)
                            continue;

                        foreach (var node3 in node2.ChildNodes().OfType<MethodDeclarationSyntax>())
                        {
                            var mName = GetName(node3);
                            if (mName != syntaxReceiver.OldMethodName)
                                continue;

                            var gName = GetGenericName(node3);
                            if (string.IsNullOrEmpty(gName))
                                continue;

                            targetMethod = node3;
                        }
                    }
                }
            }

            if (targetMethod == null)
                return;

            var oldGenericName = GetGenericName(targetMethod);
            if (string.IsNullOrEmpty(oldGenericName))
                return;

            var targetMethodCode = targetMethod.GetText().ToString();
            targetMethodCode = targetMethodCode.Replace(syntaxReceiver.OldMethodName, syntaxReceiver.NewMethodName);
            targetMethodCode = targetMethodCode.Replace(oldGenericName, syntaxReceiver.NewGenericName);
            targetMethodCode = targetMethodCode.Replace("public virtual", "public partial");

            SourceText sourceText = SourceText.From($@"
namespace {syntaxReceiver.NamespaceName};

public partial class {syntaxReceiver.ClassName}
{{
{targetMethodCode}
}}", Encoding.UTF8);

            context.AddSource($"{syntaxReceiver.ClassName}.Generated.cs", sourceText);
        }

        public static string GetName(TypeDeclarationSyntax node)
        {
            return node.Identifier.ValueText?.Trim();
        }

        public static string GetName(MethodDeclarationSyntax node)
        {
            return node.Identifier.ValueText?.Trim();
        }
        
        public static string GetGenericName(MemberDeclarationSyntax node)
        {
            var sn = node as SyntaxNode;
            var cn = node.ChildNodes().OfType<QualifiedNameSyntax>().FirstOrDefault();
            if (cn != null)
            {
                sn = cn;
            }

            var omgn = sn.ChildNodes().OfType<GenericNameSyntax>().FirstOrDefault();
            if (omgn == null)
                return "";

            return omgn.TypeArgumentList.GetText().ToString().Trim();
        }

        public static string GetName(BaseNamespaceDeclarationSyntax node)
        {
            string result = "";

            var nsIdentifer = node.ChildNodes().OfType<IdentifierNameSyntax>().FirstOrDefault();
            if (nsIdentifer == null)
                return result;

            return nsIdentifer.Identifier.ValueText?.Trim();
        }

        class MySyntaxReceiver : ISyntaxReceiver
        {
            public string NamespaceName { get; private set; }
            public string ClassName { get; private set; }
            public string NewMethodName { get; private set; }
            public string NewGenericName { get; private set; }
            public string OldMethodName { get; private set; }

            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                if (syntaxNode is MethodDeclarationSyntax mds)
                {
                    var attribute = mds.AttributeLists
                        .SelectMany(al => al.Attributes)
                        .FirstOrDefault(a => a.Name.GetText().ToString() == "GenerateGenericOverride");

                    if (attribute != null)
                    {
                        string newMethodName = GetName(mds);

                        var newGenericName = GetGenericName(mds);
                        if (string.IsNullOrEmpty(newGenericName))
                            return;

                        var oldMethodName = "";
                        var arguments = attribute.ArgumentList?.Arguments;
                        foreach (var syntax in arguments)
                        {
                            oldMethodName = syntax.Expression.ToString();
                            oldMethodName = GetStringBetween(oldMethodName, "\"", "\"");
                            oldMethodName = GetStringBetween(oldMethodName, "(", ")");

                            break;
                        }

                        if (string.IsNullOrEmpty(oldMethodName))
                            return;

                        var classNode = mds.Parent;
                        if (classNode is ClassDeclarationSyntax cds)
                        {
                            string className = GetName(cds);

                            var nsNode = cds.Parent;
                            if (nsNode is FileScopedNamespaceDeclarationSyntax nsds)
                            {
                                string namespaceName = GetName(nsds);
                                
                                NamespaceName = namespaceName;
                                ClassName = className;
                                NewMethodName = newMethodName;
                                NewGenericName = newGenericName;
                                OldMethodName = oldMethodName;
                            }
                        }
                    }
                }
            }

            public string GetStringBetween(string original, string start, string end)
            {
                int startIndex = original.IndexOf(start);
                if (startIndex == -1)
                {
                    return original;
                }

                startIndex += start.Length;
                int endIndex = original.IndexOf(end, startIndex);
                if (endIndex == -1)
                {
                    return original;
                }

                return original.Substring(startIndex, endIndex - startIndex);
            }
        }
    }
}

欢迎对生成器代码进行任何建议的更改。由于这是我的第一个发电机,我对目前的结果感到满意..

© www.soinside.com 2019 - 2024. All rights reserved.