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

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

Сгенерированный код должен выглядеть так:

      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}'.");
            }
        }
    }
}

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

      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) && t.IsClass && !t.IsAbstract && t.Name.StartsWith(prefix) && t.Name.EndsWith(suffix)));
}

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

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 && 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созданные для кейсов.

Другие вопросы по тегам