Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Roslyn compilation - how to reference a .NET Standard 2.0 class library

Tags:

c#

roslyn

I created a console application project (targeting .NET Core 3.0) and a class library (targeting .NET Standard 2.0). The console application tries to use the Roslyn compiler to compile some C# code that references that previously created class library. I'm hitting some major issues though.

Here's the code for the console application (note that most of it is example code from https://github.com/joelmartinez/dotnet-core-roslyn-sample/blob/master/Program.cs):

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; //nuget Microsoft.CodeAnalysis.CSharp
using Microsoft.CodeAnalysis.Emit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;

//This is a class library I made in a separate project in the solution and added as a reference to the console application project.
//The important bit for the reproduction of the issue is that ClassLibrary1 targets .NET Standard 2.0.
using ClassLibary1;

namespace RoslynIssueRepro
{
    class Program
    {
        static void Main(string[] args)
        {
            string codeToCompile =
                @"
                using ClassLibary1;
                using System;

                namespace RoslynCompileSample
                {
                    public class Writer
                    {
                        public void Execute()
                        {
                            //1. this next line of code introduces the issue during Roslyn compilation (comment it out and everything runs fine).
                            //It causes the code to reference a .NET Standard 2.0 class library (and this console app targets .NET Core 3.0).
                            //Note: If the referenced class library targets .NET Core 3.0, everything works fine.
                            //The error looks like this:
                            //  CS0012: The type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'.
                            Console.WriteLine(Class1.DoStuff());

                            Console.WriteLine(""Freshly compiled code execution done!"");
                        }
                    }
                }";

            var refPaths = new[] {
                typeof(System.Object).GetTypeInfo().Assembly.Location,
                typeof(Console).GetTypeInfo().Assembly.Location,
                Path.Combine(Path.GetDirectoryName(typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly.Location), "System.Runtime.dll"),
                typeof(Class1).GetTypeInfo().Assembly.Location,

                //2. So adding a reference to netstandard.dll to alleviate the issue does not work.
                //Instead it causes even more compilation errors of this form:
                //  CS0518: Predefined type 'System.Object' is not defined or imported
                //  CS0433: The type 'Console' exists in both 'System.Console, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' and 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'
                //Go ahead and try it by uncommenting the line below:
                //Environment.ExpandEnvironmentVariables(@"C:\Users\%USERNAME%\.nuget\packages\netstandard.library\2.0.0\build\netstandard2.0\ref\netstandard.dll")
            };

            RoslynCompileAndExecute(codeToCompile, refPaths);
        }

        #region example code from https://github.com/joelmartinez/dotnet-core-roslyn-sample/blob/master/Program.cs
    private static void RoslynCompileAndExecute(string codeToCompile, string[] refPaths)
    {
        Write("Let's compile!");

        Write("Parsing the code into the SyntaxTree");
        SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(codeToCompile);

        string assemblyName = Path.GetRandomFileName();

        MetadataReference[] references = refPaths.Select(r => MetadataReference.CreateFromFile(r)).ToArray();

        Write("Adding the following references");
        foreach (var r in refPaths)
            Write(r);

        Write("Compiling ...");
        CSharpCompilation compilation = CSharpCompilation.Create(
            assemblyName,
            syntaxTrees: new[] { syntaxTree },
            references: references,
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        using (var ms = new MemoryStream())
        {
            EmitResult result = compilation.Emit(ms);

            if (!result.Success)
            {
                Write("Compilation failed!");
                IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
                    diagnostic.IsWarningAsError ||
                    diagnostic.Severity == DiagnosticSeverity.Error);

                foreach (Diagnostic diagnostic in failures)
                {
                    Console.Error.WriteLine("\t{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
                }
            }
            else
            {
                Write("Compilation successful! Now instantiating and executing the code ...");
                ms.Seek(0, SeekOrigin.Begin);

                Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
                var type = assembly.GetType("RoslynCompileSample.Writer");
                var instance = assembly.CreateInstance("RoslynCompileSample.Writer");
                var meth = type.GetMember("Execute").First() as MethodInfo;
                meth.Invoke(instance, null);
            }
        }
    }
    static Action<string> Write = Console.WriteLine;
    #endregion
    }
}

and the code for the ClassLibrary1 is just this:

namespace ClassLibary1
{
    public static class Class1
    {
        public static string DoStuff()
        {
            return "asdfjkl";
        }
    }
}

I've commented two places in code with //1 and //2. The first one is the line that introduces the first issue, and causes the compilation to fail. The second spot (currently commented out) is an attempt to work around the first issue by adding a reference to the netstandard.dll file (sorry if the path isn't portable, just where I happened to find it on my machine), but it does not fix anything, only introducing more cryptic errors.

Any idea on the approach I should take to get this code to work?

like image 355
Anssssss Avatar asked Nov 13 '19 16:11

Anssssss


People also ask

Can a .NET standard library reference a .NET framework library?

Implementation is separate from specification. As you said, if you add a . NET framework library to a . NET standard library, then you can no longer run it on .

Is Roslyn part of .NET core?

Roslyn, the . NET Compiler Platform, empowers the C# compiler on . NET Core and allows developers to leverage the rich code analysis APIs to perform code generation, analysis and compilation.

What is roslyn code generation?

NET Compiler Platform ("Roslyn") SDK. Source Generators let C# developers inspect user code as it is being compiled. The generator can create new C# source files on the fly that are added to the user's compilation. In this way, you have code that runs during compilation.


2 Answers

The first error occur because your referenced library targets netstandard and the console app compilation which references this library must reference netstandard.dll to correctly resolve all corresponding types. So you should add reference to nestandard.dll but it's not all and here you get the second error.

When you try to reference netsandard directly or by transitive you must provide the nestandard.dll corresponding by the target platform. And this netstandard will have a huge of forwarding types to the types on the current target platform. If you look at the @"C:\Users\%USERNAME%\.nuget\packages\netstandard.library\2.0.0\build\netstandard2.0\ref\netstandard.dll" you will find that this netstandard.dll doesn't contain forwards but contains all types directly and of course it contains System.Console. ( I think, it contains all types directly because it is from nuget package which doesn't depend on any target platform, but really not sure for that). And when you try to add it and System.Console.dll by typeof(Console).GetTypeInfo().Assembly.Location you actually get two System.Console in a compilation.

So to resolve this ambiguous you can add netstandard not from this nuget package, but from you current target platform which has all needed forwards. For the .netcore30, for example, you can use netstandard from path_to_dotnet_sdks\packs\Microsoft.NETCore.App.Ref\3.0.0\ref\netcoreapp3.0\ (be careful this assemblies and assemblies from nuget package above just for referencing, they doesn't contain real logic). Also you may try to remove reference on System.Console.dll and keep reference on @"C:\Users\%USERNAME%\.nuget\packages\netstandard.library\2.0.0\build\netstandard2.0\ref\netstandard.dll"

like image 182
George Alexandria Avatar answered Oct 24 '22 23:10

George Alexandria


Since you're using typeof(X).Assembly to locate all other referenced assemblies, which will implicitly return the assemblies loaded into your App Domain. I would recommend locating the matching netstandard in the same way. However, since netstandard doesn't directly define any types, the best method I've found is to search for it by name;

AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "netstandard")
like image 4
Jeremy Lakeman Avatar answered Oct 24 '22 21:10

Jeremy Lakeman