1

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;
        }
    }
}
5
  • Can you get access to the generated code and post that? Commented Aug 5, 2021 at 6:58
  • The full errors (with stack trace) should tell you at which position error occurs. Is the last snippet generated code? Look at line 3, what's that? Commented Aug 5, 2021 at 6:58
  • Added the generated code, also all the errors are thrown by 22'th line. The 3'rd line in the executable is c# 9.0 top-level-statement execution. Commented Aug 5, 2021 at 7:03
  • You do understand that there is no line numbers in you post, don't you? There are several pieces of code that get compiled and run at very different points in time - most of them long enough to have "22th line"... Please look at the post as if you don't have any other information and see if what is there makes sense. Edit to improve... Commented Aug 5, 2021 at 7:06
  • The part of the generated code contains 23 lines of code so it will be 1 line before the last one also I provided a minimal reproduction code which makes the lines a no brainer. The question of @Sinatr was addressed to the grenerated code. Commented Aug 5, 2021 at 7:13

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

3 Comments

I'd suggest opposite to excluding: make a custom attribute, explicitly add it to needed types and only process those.
You are absolutly right! Also @Sinatr, thanks for the suggestion but the CompilerGenerated are already decorated with an attribute so I think I'll go with vernou's suggestion to try to ignore those classes. Do you know the way to ignore compilation? Tried to find it in the docs but no luck.
@IgalKF, no sorry. If you post other question, maybe someone more knowledgeable can help you?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.