I am trying to use Roslyn to compile C# code at runtime. My compilation function is as follows:
using System.Collections.Immutable;
using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
namespace RuntimeCompiler;
public static class RuntimeCompiler
{
public static void Compile(string sourceCode, string outputPath, string assemblyName="MyAssembly")
{
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
var compilation = CSharpCompilation.Create(assemblyName)
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(syntaxTree);
using var stream = new FileStream(outputPath, FileMode.Create);
var result = compilation.Emit(stream);
if (result.Success) return;
throw new Exception($"Compilation failed");
}
}
and my driver function is below:
namespace RuntimeCompiler;
public class main
{
public static void Main()
{
const string sourceCode = @"
namespace UtilityLibraries;
public static class StringLibrary
{
public static bool StartsWithUpper(this string? str)
{
if (string.IsNullOrWhiteSpace(str))
return false;
char ch = str[0];
return char.IsUpper(ch);
}
}";
const string outputPath = @"/tmp/mylib3.dll";
RuntimeCompiler.Compile(sourceCode, outputPath, "ass3");
}
}
The code works, and I can see my dll being generated. The problem is, when I try to use the dll in a separate ConsoleApp like below:
using UtilityLibraries;
StringLibrary.StartsWithUpper("asdasd");
Compiler complains:
The type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e'.
I also examined the value of typeof(object).Assembly.Location
, it is /usr/share/dotnet/shared/Microsoft.NETCore.App/6.0.25/System.Private.CoreLib.dll
so it appears to me that the requested assembly is already added.
Any thoughts?
(In case anyone is interested in taking a closer look, I uploaded a minimally reproducible example here)
The problem you have is related to what assemblies you are adding. Start from this comment @github:
This behavior is "By Design". On .NET Core all of the core types are actually located in a private DLL (believe the name is
System.Private.Corlib
). The mscorlib library, at runtime, is largely a collection of type forwarders. The C# compiler is unable to resolve these type forwarders and hence issues an error because it can't locateSystem.Object
.This is a bit of a known issue when working on .NET Core. There is a stricter separation of runtime and compile time assemblies. Attempting to use runtime assemblies as compile references is not supported and frequently breaks do to the structure of the runtime assemblies.
And then check the Runtime-code-generation-using-Roslyn-compilations-in-.NET-Core-App.md doc. What you are actually doing is compiling against runtime (implementation) assemblies which breaks adding compiled dll as reference to the project. You want to compile against reference ones to do that:
Compile against reference (contract) assemblies
This is what the compiler does when invoked from msbuild. You need to decide what reference assemblies to use (e.g.
netstandard1.5
). Once you decide, you need to get them from nuget packages and distribute them with your application, e.g. in a form of embedded resources. Then in your application extract the binaries from resources and create MetadataReferences for them.
To find out what reference assemblies you need and where to get them you can create an empty .NET Core library, set the target framework to the one you need to target, build usingmsbuild /v:detailed
and look forcsc.exe
invocation in msbuild output. The command line will list all references the C# compiler uses to build the library (look for/reference
command line arguments).
I have changed 2 things to make the code to work:
var assemblyPath = "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\6.0.25\\ref\\net6.0";
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
var compilation = CSharpCompilation.Create(assemblyName)
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")))
.AddSyntaxTrees(syntaxTree);
<ItemGroup>
<Reference Include="ass3">
<HintPath>ass3.dll</HintPath>
</Reference>
</ItemGroup>
and
const string outputPath = @"/tmp/ass3.dll";
I would argue that this is a bit unconventional approach to the problem. If you want "dynamic"/build-time code generation - consider using source generators
If source generator is not an option - consider generating a project and compiling a whole project
If you still want to utilize your way you can try something like the following (see this comment @github):
Add to the generator .csproj:
<PropertyGroup>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
Add Microsoft.Extensions.DependencyModel
nuget and use it to get the references:
MetadataReference[] refs = DependencyContext.Default.CompileLibraries // filter out some libs?
.SelectMany(cl => cl.ResolveReferencePaths())
.Select(asm => MetadataReference.CreateFromFile(asm))
.ToArray();
Also you can try compiling against .NET Standard references.
Your current approach allows to load assembly dynamically and call methods with reflection. For example:
var assembly = Assembly.Load(File.ReadAllBytes(outputPath));
var type = assembly.GetType("UtilityLibraries.StringLibrary");
var methodInfo = type.GetMethod("StartsWithUpper", BindingFlags.Public | BindingFlags.Static);
var result = methodInfo.Invoke(null, new Object[] { null });