1

I'm doing a C# source generator and I want the developer to influence the output of the generated types based on a class with a specified interface that he will implement.

The interface is declared in a project called Core.dll.

namespace Core
{
    public interface ITask
    {
        void Run();
    }
}

The source generator gets called, compiles the class implementing the interface and executes a method.

using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;
using DynamicCompilationNetStandard2._0;

namespace Generator;

[Generator]
public partial class Generator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var classDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: static (x, _) => x is ClassDeclarationSyntax c,
                transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
            .Where(x => x is not null);

        var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());

        context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Item1, source.Item2, spc));
    }

    private static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes, SourceProductionContext context)
    {
        if (classes.IsDefaultOrEmpty)
        {
            return;
        }

        try
        {
            var source = """
                using System;
                using Core;

                namespace Consumer
                {
                    public class MyTask : ITask
                    {
                        public void Run()
                        {
                            Console.WriteLine("Finished");
                        }
                    }
                }
                """;

            Executor.Execute(source);
        }
        catch (Exception ex)
        {
            throw;
        }
    }
}

The executor class dynamically compiles the class and runs it.

using Basic.Reference.Assemblies;
using Core;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.IO;
using System.Reflection;

namespace DynamicCompilationNetStandard2._0
{
    public static class Executor
    {
        public static void Execute(string source)
        {
            var syntaxTree = SyntaxFactory.ParseSyntaxTree(source);
            var compilation = CSharpCompilation.Create(assemblyName: Path.GetRandomFileName())
                .WithReferenceAssemblies(ReferenceAssemblyKind.NetStandard20)
                .AddReferences(
                    MetadataReference.CreateFromFile(typeof(ITask).Assembly.Location))
                .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
                .AddSyntaxTrees(syntaxTree);

            using (var ms = new MemoryStream())
            {
                var result = compilation.Emit(ms);
                if (!result.Success)
                {
                    throw new Exception(result.ToString());
                }

                ms.Seek(0, SeekOrigin.Begin);
                var assembly = Assembly.Load(ms.ToArray());

                try
                {
                    var types = assembly.GetTypes();
                }
                catch (Exception ex)
                {
                    throw;
                }

                dynamic task = assembly.CreateInstance("Consumer.MyTask");
                task.Run();
            }
        }
    }
}

All the framework references are correctly loaded using reference assemblies by using the nuget package Basic.Reference.Assemblies. The only one not loading is my library Core.dll but it is added as a reference to the CSharpCompilation. When I read the FusionLogs, it's like Roslyn can't load a Dll outside of it's folder.

=== Pre-bind state information ===
LOG: DisplayName = Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
 (Fully-specified)
LOG: Appbase = file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/
LOG: Initial PrivatePath = NULL
Calling assembly : (Unknown).
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: G:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin\Roslyn\csc.exe.Config
LOG: Using host configuration file: 
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).
LOG: Attempting download of new URL file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/Core.DLL.
LOG: Attempting download of new URL file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/Core/Core.DLL.
LOG: Attempting download of new URL file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/Core.EXE.
LOG: Attempting download of new URL file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/Core/Core.EXE.

To verify that everything else is good, I created a console app in .Net 7 executing the same code and it works!

using DynamicCompilationNetStandard2._0;

var source = """
    using System;
    using Core;

    namespace Consumer
    {
        public class MyTask : ITask
        {
            public void Run()
            {
                Console.WriteLine("Finished");
            }
        }
    }
    """;

Executor.Execute(source);

If I put Core.dll manually inside the G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn folder, it works with the source generator but I don't want the developer to copy the file manually.

The question is, how can I make it work with a source generator? It is only a simple Dll to load.

The sources are ready to debug with the source generator. https://github.com/adampaquette/DynamicCompilationTests Make sure to have the component .NET Compiler Platform SDK installed in VS2022.

Thanks!

Update

Here is the Generator.csproj:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <LangVersion>latest</LangVersion>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
        <IsRoslynComponent>true</IsRoslynComponent>
        <PreserveCompilationContext>true</PreserveCompilationContext>
        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Basic.Reference.Assemblies" Version="1.4.1" />
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" />
        <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
        <PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.4.0">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\Core\Core.csproj" />
      <ProjectReference Include="..\DynamicCompilationNetStandard2.0\DynamicCompilationNetStandard2.0.csproj" />
    </ItemGroup>
</Project>

Edit

One fix was to resolve the DLL inside the no context context for the debug to work but when compiling a consumer project the DLL is not found.

AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

private Assembly? CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) =>
    args.Name == "FluentType.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
                ? AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.FullName == args.Name)
                : null;       
Adam Paquette
  • 1,243
  • 1
  • 14
  • 28
  • pls show the reference to Core.dll from the project file, for example if it's a project reference it might need the `OutputItemType="Analyzer"` attribute – Patrick Beynio Dec 21 '22 at 16:25
  • @PatrickBeynio I updated the post to show the Generator.csproj. I also tried to add the following properties to the project reference but it does'nt work. ``` true true true ``` – Adam Paquette Dec 21 '22 at 16:47
  • yea try ``, that should pack Core.dll along the SG so it can find it – Patrick Beynio Dec 21 '22 at 21:57
  • @PatrickBeynio Sadly it is not working, can you try yourself? Clone sources here: https://github.com/adampaquette/DynamicCompilationTests – Adam Paquette Dec 21 '22 at 23:37

1 Answers1

1

Assuming your source generator will be packaged in a nuget package, you will have to manually add all assemblies that the source generator uses to the package.

This means the package needs to contain all assemblies in the transitive closure of the source generator's dependencies. The only exceptions are the target framework assemblies and certain assemblies that will be provided by the roslyn environment at source generation runtime, like System.Collections.Immutable.

Be aware that the latter can differ depending on compilation environment. Compiling in Visual Studio will run the .NET Framework version of MSBuild. Compiling via dotnet CLI will run the .NET Core version of MSBuild. Those are different applications running in different runtimes and will cause different assemblies and/or different assembly versions to be available. If you want your source generator to work for both Visual Studio and the dotnet CLI you need to be careful here.

I once wrote a source generator as a PoC and ran into similar problems. You can find it at https://github.com/shuebner/XsdToSource. Maybe it can help you as a reference.

Other helpful resources include

svenhuebner
  • 342
  • 1
  • 2
  • 10