Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replacing Reflection with Source Generators

I have a factory that uses Reflection which I would like to replace with one generated by a Source Generator.

The generated code should look like this:

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

Using Reflection, I find my types like this:

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)));
}

How can I do the same search through GeneratorExecutionContext.Compilation object as I do via Reflection code?

like image 525
Sandy Avatar asked Oct 16 '25 17:10

Sandy


1 Answers

You'll have to use an equivalent API provided by the compiler through the execution context. Then depending on how you want to generate the source, you could generate the source text directly or generate the syntax nodes that represent the source.

You'll need to dig around in the Compilation to find the types that implement your interface, then generate the cases for each of the types.

Here's one implementation you could try: (I'm not able to test the generator itself but the content generation should work)

[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)
    {
    }
}

This would produce source that would look something like this:

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

For your purposes, you'll probably want to define an attribute on your types that you want to participate in this factory however. That way you can better control the insurnaceName generated for the cases.

like image 148
Jeff Mercado Avatar answered Oct 18 '25 05:10

Jeff Mercado



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!