Замена отражения генераторами источников
У меня есть фабрика, использующая 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
созданные для кейсов.