Замена отражения генераторами источников

#c# #.net-5 #c#-9.0 #sourcegenerators

Вопрос:

У меня есть фабрика, которая использует отражение, которое я хотел бы заменить на то, которое генерируется генератором источника.

Сгенерированный код должен выглядеть следующим образом:

 using System;

namespace Generated
{
    public static class InsuranceFactory
    {
        public static IInsurance Get(string insuranceName)
        {
            switch (insuranceName)
            {
                case "LifeInsurance":
                    return new Namespace.LifeInsurance();
                case "AutoInsurance":
                    return new AnotherNamespace.AutoInsurance();
                default:
                    throw new Exception($"Insurance not found for name '{insuranceName}'.");
            }
        }
    }
}
 

Используя отражение, я нахожу свои типы такими:

 List<Type> insuranceTypes = new List<Type>();
Type baseInsuranceType = typeof(IInsurance);
IEnumerable<Assembly> assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(o => !IsFrameworkAssembly(o.FullName ?? String.Empty));

foreach (System.Reflection.Assembly a in assemblies)
{
    Type[] types = a.GetTypes();
    insuranceTypes.AddRange(types.Where(t => baseInsuranceType.IsAssignableFrom(t) amp;amp; t.IsClass amp;amp; !t.IsAbstract amp;amp; t.Name.StartsWith(prefix) amp;amp; t.Name.EndsWith(suffix)));
}
 

Как я могу выполнить тот же поиск с помощью объекта GeneratorExecutionContext.Compilation, что и с помощью кода отражения?

Комментарии:

1. Во-первых, IInsurance имеет мало сходства с IAnimal , во-вторых, я бы попытался решить эту проблему традиционно, а не с помощью этого взлома отражения. В-третьих, генераторы источников-замечательная вещь, однако они немного выходят за рамки простых вопросов и ответов

Ответ №1:

Вам придется использовать эквивалентный API, предоставляемый компилятором в контексте выполнения. Затем, в зависимости от того, как вы хотите сгенерировать источник, вы можете сгенерировать исходный текст напрямую или сгенерировать синтаксические узлы, представляющие источник.

Вам нужно будет покопаться, Compilation чтобы найти типы, реализующие ваш интерфейс, а затем создать варианты для каждого из типов.

Вот одна реализация, которую вы могли бы попробовать: (Я не могу протестировать сам генератор, но генерация контента должна работать)

 [Generator]
public class InsuranceFactoryGenerator : ISourceGenerator
{
    const string FactoryNamespaceName = "MyNamespace";
    const string QualifiedInterfaceName = "InsuranceCompany.IInsurance";
    
    public void Execute(GeneratorExecutionContext context)
    {
        var insuranceTypes = GetInsuranceTypes(context.Compilation, context.CancellationToken);
        var factoryClass = GenerateFactoryClass(context.Compilation, insuranceTypes, context.CancellationToken);
        var factoryContent = NamespaceDeclaration(ParseName(FactoryNamespaceName))
            .WithMembers(SingletonList<MemberDeclarationSyntax>(factoryClass));
        context.AddSource("InsuranceFactory", factoryContent.NormalizeWhitespace().ToFullString());
    }

    private IEnumerable<ITypeSymbol> GetInsuranceTypes(Compilation compilation, CancellationToken cancellationToken)
    {
        var type = compilation.GetTypeByMetadataName(QualifiedInterfaceName)
            ?? throw new Exception($"Interface '{QualifiedInterfaceName}' not found in compilation");
        var classDecls = compilation.SyntaxTrees
            .SelectMany(t => t.GetRoot(cancellationToken).DescendantNodes())
            .OfType<ClassDeclarationSyntax>();
        foreach (var classDecl in classDecls)
        {
            var classSymbol = GetInsuranceClassSymbol(compilation, type, classDecl, cancellationToken);
            if (classSymbol != null)
                yield return classSymbol;
        }
    }

    private ITypeSymbol? GetInsuranceClassSymbol(Compilation compilation, ITypeSymbol insuranceSymbol, ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
    {
        if (classDeclaration.BaseList == null) return null;
        var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
        foreach (var baseType in classDeclaration.BaseList.Types)
        {
            var typeSymbol = compilation.GetTypeByMetadataName(baseType.Type.ToString())!;
            var conversion = compilation.ClassifyConversion(typeSymbol, insuranceSymbol);
            if (conversion.Exists amp;amp; conversion.IsImplicit)
                return semanticModel.GetDeclaredSymbol(classDeclaration, cancellationToken);
        }
        return null;
    }

    private ClassDeclarationSyntax GenerateFactoryClass(Compilation compilation, IEnumerable<ITypeSymbol> insuranceTypes, CancellationToken cancellationToken)
    {
        var paramName = "insuranceName";
        return ClassDeclaration("InsuranceFactory")
            .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)))
            .WithMembers(
                SingletonList<MemberDeclarationSyntax>(
                    MethodDeclaration(ParseTypeName(QualifiedInterfaceName), "Get")
                        .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)))
                        .WithParameterList(
                            ParameterList(
                                SingletonSeparatedList<ParameterSyntax>(
                                    Parameter(Identifier(paramName))
                                        .WithType(PredefinedType(Token(SyntaxKind.StringKeyword)))
                                )
                            )
                        )
                        .WithBody(
                            Block(
                                SwitchStatement(IdentifierName("insuranceName"), List(
                                    GenerateCases(compilation, insuranceTypes).Append(
                                        SwitchSection(
                                            SingletonList<SwitchLabelSyntax>(DefaultSwitchLabel()),
                                            SingletonList<StatementSyntax>(
                                                ParseStatement(@$"throw new ArgumentException(nameof({paramName}), $""Insurance not found for name '{{{paramName}}}'."");")
                                            )
                                        )
                                    )
                                ))
                            )
                        )
                )
            );
    }

    private IEnumerable<SwitchSectionSyntax> GenerateCases(Compilation compilation, IEnumerable<ITypeSymbol> insuranceTypes)
    {
        foreach (var insuranceType in insuranceTypes)
        {
            var label = insuranceType.Name!;
            var switchLabel = CaseSwitchLabel(LiteralExpression(SyntaxKind.StringLiteralExpression).WithToken(Literal(label)));
            var typeName = compilation.GetTypeByMetadataName(insuranceType.ToString()!)!;
            var instanceExpression = ReturnStatement(
                ObjectCreationExpression(ParseTypeName(typeName.ToString()!))
                    .WithArgumentList(ArgumentList())
            );
            yield return SwitchSection(
                SingletonList<SwitchLabelSyntax>(switchLabel),
                SingletonList<StatementSyntax>(instanceExpression)
            );
        }
    }
    
    public void Initialize(GeneratorInitializationContext context)
    {
    }
}
 

Это привело бы к созданию источника, который выглядел бы примерно так:

 namespace MyNamespace
{
    public static class InsuranceFactory
    {
        public static InsuranceCompany.IInsurance Get(string insuranceName)
        {
            switch (insuranceName)
            {
                case "MassMutualLifeInsurance":
                    return new InsuranceCompany.MassMutual.MassMutualLifeInsurance();
                case "GeicoLifeInsurance":
                    return new InsuranceCompany.Geico.GeicoLifeInsurance();
                case "GeicoAutoInsurance":
                    return new InsuranceCompany.Geico.GeicoAutoInsurance();
                default:
                    throw new ArgumentException(nameof(insuranceName), $"Insurance not found for name '{insuranceName}'.");
            }
        }
    }
}
 

Однако для ваших целей вы, вероятно, захотите определить атрибут для ваших типов, которые вы хотите использовать на этой фабрике. Таким образом, вы сможете лучше контролировать insurnaceName сгенерированные для случаев.

Комментарии:

1. Спасибо @JeffMercado за то, что нашли время для составления этого подробного ответа. Очень признателен.