Search code examples
c#dependency-injectionroslync#-9.0csharp-source-generator

C# 9.0 source generator dependency injection registration not supporting async methods


The Issue:

Trying to implement an auto dependency injection registrator, my conventions are very strict so it will be very useful.

I'm having issues with registering classes containing async methods, the container seems to adress to these methods while registering the class.

About the project:

  • The Soruce generator is netstandard2.0
  • The Models and the Executable are net5.0
  • When the method is not asynchronous everything works perfectly
  • The MetadataReference are not actually needed I just want to avoid answers addressing to those

TL;DL

Some of the reproduction errors:

  • CS0103 The name 'd__0' does not exist in the current context
  • CS0103 The name 'TestMethod' does not exist in the current context
  • CS1525 Invalid expression term '<'
  • CS1002 ; expected

The code:

Source Generator Project:

namespace Test.Build.Tools
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.Text;

    /// <summary>
    /// Auto register source generator.
    /// </summary>
    [Generator]
    public class AutoRegisterSourceGenerator : ISourceGenerator
    {
        /// <inheritdoc/>
        public void Initialize(GeneratorInitializationContext context)
        {
        }

        /// <inheritdoc/>
        public void Execute(GeneratorExecutionContext context)
        {
            StringBuilder stringBuilder = new("namespace Test.Extensions.DependencyInjection\n"
                                            + "{\n"
                                            + "    using System;\n"
                                            + "    using System.Threading.Tasks;\n"
                                            + "    using Microsoft.Extensions.DependencyInjection;\n");
            List<string> namespaces = new();

            string defaultPath = typeof(object).Assembly.Location.Replace("mscorlib", "{0}");

            List<MetadataReference> references = new()
            {
                { MetadataReference.CreateFromFile(string.Format(defaultPath, "System.Threading.Tasks")) }
            };

            var types = GetAllTypes(context.Compilation);
            var neededTypes = types.Where(t =>
            {
                string @namespace = t.ContainingNamespace.ToString();

                if (@namespace.Contains("Test")
                && !t.Interfaces.IsEmpty
                && t.TypeKind == TypeKind.Class)
                {
                    namespaces.Add(t.ContainingNamespace.ToString());
                    namespaces.Add(t.Interfaces[0].ContainingNamespace.ToString());
                    return true;
                }

                return false;
            }).ToList();

            namespaces.Distinct().OrderBy(n => n.ToString()).ToList().ForEach(n => stringBuilder.Append($"    using {n};\n"));

            stringBuilder.Append(
                "    /// <summary>\n" +
                "    /// Service registrator class.\n" +
                "    /// </summary>\n" +
                "    public static class ServicesRegistrator\n" +
                "    {\n" +
                "        /// <summary>\n" +
                "        /// Register dependency injection instances.\n" +
                "        /// </summary>\n" +
                "        /// <param name=\"services\">Startup services.</param>\n" +
                "        /// <returns>The given <see cref=\"IServiceCollection\"/> instance.</returns>\n" +
                "        public static IServiceCollection RegisterDomainModel(this IServiceCollection services)\n" +
                "        {\n");

            foreach (var type in neededTypes)
            {
                stringBuilder.Append($"            services.AddScoped<I{type.Name}, {type.Name}>();");
                stringBuilder.AppendLine();
            }

            stringBuilder.Append("            return services;\n" +
                "        }\n" +
                "    }\n" +
                "}\n");

            context.Compilation.AddReferences(references);

            context.AddSource("ServicesRegistrator", SourceText.From(stringBuilder.ToString(), Encoding.UTF8));
        }

        IEnumerable<INamedTypeSymbol> GetAllTypes(Compilation compilation) =>
            GetAllTypes(compilation.GlobalNamespace);

        IEnumerable<INamedTypeSymbol> GetAllTypes(INamespaceSymbol @namespace)
        {
            foreach (var type in @namespace.GetTypeMembers())
                foreach (var nestedType in GetNestedTypes(type))
                    yield return nestedType;

            foreach (var nestedNamespace in @namespace.GetNamespaceMembers())
                foreach (var type in GetAllTypes(nestedNamespace))
                    yield return type;
        }

        IEnumerable<INamedTypeSymbol> GetNestedTypes(INamedTypeSymbol type)
        {
            yield return type;
            foreach (var nestedType in type.GetTypeMembers()
                .SelectMany(nestedType => GetNestedTypes(nestedType)))
                yield return nestedType;
        }
    }
}

Models Project:

namespace TestClasses
{
    using System.Threading.Tasks;

    public interface ITestClass
    {
        public Task TestMethod();
    }
}

namespace TestClasses.Model
{
    using System.Threading.Tasks;

    public class TestClass : ITestClass
    {
        public async Task TestMethod()
        {
            await Task.CompletedTask;
        }
    }
}

Executable

using Executable;

Program.Rgister();

namespace Executable
{
    using Microsoft.Extensions.DependencyInjection;
    using Test.Extensions.DependencyInjection;
    using TestClasses;

    public class Program
    {
        public static void Rgister()
        {
            IServiceCollection services = new ServiceCollection();
            services.RegisterDomainModel();

            var x = services.BuildServiceProvider().GetRequiredService<ITestClass>();

            x.TestMethod();
        }
    }
}

Update:

The Generated Code:

namespace Test.Extensions.DependencyInjection
{
    using System;
    using System.Threading.Tasks;
    using Microsoft.Extensions.DependencyInjection;
    using TestClasses;
    using TestClasses.Model;
    /// <summary>
    /// Service registrator class.
    /// </summary>

    public static class ServicesRegistrator
    {
        /// <summary>
        /// Register dependency injection instances.
        /// </summary>
        /// <param name="services">Startup services.</param>
        /// <returns>The given <see cref="IServiceCollection"/> instance.</returns>
        public static IServiceCollection RegisterDomainModel(this IServiceCollection services)
        {
            services.AddScoped<ITestClass, TestClass>();
            return services;
        }
    }
}

Solution

  • async/await it's a sugar syntax that is interpreted by the compiler. After compilation, async method is replaced by a generated class. You can check this with tool like ILSpy (in ILSpy, open "View\Show all types and members".

    With your model code, we can see the generated DLL contain this class :

    // TestClasses.Model.TestClass.<TestMethod>d__0
    using System;
    using System.Diagnostics;
    using System.Runtime.CompilerServices;
    using System.Threading.Tasks;
    
    [CompilerGenerated]
    private sealed class <TestMethod>d__0 : IAsyncStateMachine
    {
        ...
    }
    

    Many sugar keyword (like yield) produce this type of class after compilation. In your generator, you need ignore this classes. To do this, you must check if the class has the attribute CompilerGeneratedAttribute.

    Maybe it's possible to generate injection code before generated classes are generated.