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;