How to migrate from System.CodeDom.Compiler to Roslyn API? - c#

I have the following piece of code in .NET Framework 4.8,
sourceCode = $#"
using System;
{string.Format(Constants.Assembly.UsingDirective, Constants.Assembly.DapperNamespace)}
namespace {Constants.Assembly.DynamicTypeNamespace} {{
{sourceCode}
}}";
// Create Compilation Parameters
CompilerParameters compileParams = new CompilerParameters()
{
CompilerOptions = Constants.Assembly.CompileToLibrary,
GenerateInMemory = true
};
compileParams.ReferencedAssemblies.AddRange(baseAssemblyLocations.ToArray());
// Create Code Provider
CSharpCodeProvider provider = new CSharpCodeProvider(new Dictionary<string, string>() {
{ Constants.Assembly.CompilerVersion, Constants.Assembly.CompilerVersion4 }
});
// Attempt compilation
CompilerResults compileResult = provider.CompileAssemblyFromSource(compileParams, sourceCode);
if (compileResult.Errors.Count > 0)
{
throw new Exception(compileResult.Errors[0].ErrorText);
}
// Store the assembly
Assembly = compileResult.CompiledAssembly;
I am looking into Roslyn APIs, but can't get it working using CSharpCompilationOptions.
How should I pass compilerParams, and sourceCode to the CSharpCompilationOptions?

How should I pass compilerParams
You don't, CompilerParameters is part of System.CodeDom.Compiler API, not a Roslyn API
To create a compilation you can use CSharpCompilation.Create:
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
var compilation = CSharpCompilation.Create(
"compilation",
new[] {ParseText(sourceCode)},
GetGlobalReferences(),
options
);
Where ParseText is just:
SyntaxTree ParseText(string s)
{
return CSharpSyntaxTree.ParseText(s, new CSharpParseOptions(LanguageVersion.Latest));
}
And GetGlobalReferences is something like (rename and modify it to find all needed references):
private static PortableExecutableReference[] GetGlobalReferences()
{
var assemblies = new[]
{
typeof(object).Assembly,
typeof(Console).Assembly
};
var returnList = assemblies
.Select(a => MetadataReference.CreateFromFile(a.Location))
.ToList();
//The location of the .NET assemblies
var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
/*
* Adding some necessary .NET assemblies
* These assemblies couldn't be loaded correctly via the same construction as above,
* in specific the System.Runtime.
*/
returnList.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll")));
returnList.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll")));
returnList.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll")));
returnList.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")));
return returnList.ToArray();
}
And then you should be able to emit assembly:
compilation.Emit("path_to_save");
I use similar code to unit test my pet project source generator.

Related

Embedded files with Roslyn compilation

I am looking for an example in how to compile a project using Roslyn. The code below is an example I found in https://github.com/dotnet/roslyn/wiki/FAQ … This examples doesn't cover embedded files. Is that possible?
public class MyTask : Task {
public override bool Execute() {
var projectFileName = this.BuildEngine.ProjectFileOfTaskNode;
var project = ProjectCollection.GlobalProjectCollection.
GetLoadedProjects(projectFileName).Single();
var compilation = CSharpCompilation.Create(
project.GetPropertyValue("AssemblyName"),
syntaxTrees: project.GetItems("Compile").Select(
c => SyntaxFactory.ParseCompilationUnit(
c.EvaluatedInclude).SyntaxTree),
references: project.GetItems("Reference")
.Select(
r => new MetadataFileReference
(r.EvaluatedInclude)));
// Now work with compilation ...
}
}
Yes, it is possible to embed resources into result assembly using Roslyn.
To produce a result assembly CSharpCompilation type has an Emit method. This method has many parameters. One of them is manifestResources, which is responsible for adding embedded resources. You mas specify as many resources as you want. The following code demonstrates how you can use this parameter to emit an assembly with an embedded resource into peStream. It creates a resource with name "resourceName" and content that located at "path-to-resource" path.
void ProduceAssembly(CSharpCompilation compilation, Stream peStream)
{
ResourceDescription[] resources =
{
new ResourceDescription(
"resourceName",
() => File.OpenRead("path-to-resource"),
isPublic: true
)
};
var result = compilation.Emit(peStream, manifestResources: resources);
if (!result.Success)
{
var diagnostics = string.Join(Environment.NewLine, result.Diagnostics);
throw new Exception($"Compilation failed with: {diagnostics}");
}
}
Don't forget to check EmitResult.Success property to ensure that compilation completed successfully. Also ensure that peStream is disposed properly after compilation.

How to force CSharpCodeProvider to compile for a specific target framework?

I've got a solution which contains c# projects, some netstandard 2.0 and others .net4.7. The startup project is of course net47.
At one point, the project creates code using CodeDom and compiles it with CSharpCodeProvider. The problems is that on some machines, it tries to compile the assembly for .netstandard and it fails. The failure is expected: the generated assembly references EF which in only available for full .net framework.
How can I force CSharpCodeProvider to compile against .net47?
public bool GenerateAssembly(
CodeDomBusinessCode compileUnit
, string fileName
, string assembliesPath
, out IEnumerable<string> errors)
{
var provider = new CSharpCodeProvider();
var parameters = new CompilerParameters
{
GenerateExecutable = false,
OutputAssembly = fileName,
GenerateInMemory = false
};
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Runtime.dll");
parameters.ReferencedAssemblies.Add("System.Core.dll");
parameters.ReferencedAssemblies.Add("System.ComponentModel.Composition.dll");
parameters.ReferencedAssemblies.Add(Path.Combine(assembliesPath, "EntityFramework.dll"));
parameters.ReferencedAssemblies.Add("System.ComponentModel.DataAnnotations.dll");
parameters.ReferencedAssemblies.Add(Path.Combine(assembliesPath, "GlobalE.Server.Contracts.dll"));
var results = provider.CompileAssemblyFromDom(parameters, compileUnit.Code);
if (results.Errors.Count > 0)
{
errors = results.Errors.OfType<CompilerError>().Select(x => x.ToString());
return false;
}
errors = null;
return true;
}
The error:
error CS0012: The type 'System.IDisposable' 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'.
UPDATE:
If I change all projects to net47 (so that there is no netstandard project in the solution), the error will disappear, but I want to keep as many projects on netstandard as possible.
based on your error, you should add "netstandard.dll" as references and it may cause by this note that in .net 4.7 the "System.IDisposable" is in "mscorlib.dll" and in .netstatndard is in "netstandard.dll".
Try this
var options = new Dictionary<string, string>
{
{ "CompilerVersion", "v4.7" }
};
var provider = new CSharpCodeProvider(options);

How to get unresolved symbols in Roslyn?

I am building a scripting engine in C# using Roslyn, and I would like to compile a piece of code from the user. In the scripting UI, the user can add references to other C# dlls that I don't know about.
In the user's code, I would like to find the symbols that are resolved looking into the known references, and the symbols that are not resolved.
For instance, I have a a dll that contains this class:
public class A {
public static double Stuff { get; }
}
And the users adds this dll as a reference for his script.
Then in his script, the user writes:
var x = A.Stuff * MyVariable;
return x;
I want to use Roslyn to compile this, and tell me that x and A.Stuff are known symbols and that MyVariable is not, so that I can infer from the code that MyVariable is a user input.
Right now I am doing this:
var syntaxTree = CSharpSyntaxTree.ParseText(usercode,
new CSharpParseOptions(LanguageVersion.Default, DocumentationMode.None, SourceCodeKind.Script));
var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
var userlib = MetadataReference.CreateFromFile(userlibPath);
var compilation = CSharpCompilation.Create("MyCompilation",
syntaxTrees: new[] { syntaxTree }, references: new[] { mscorlib, userlib });
var model = compilation.GetSemanticModel(syntaxTree);
But I don't know how to use the information from the semantic model. This is not very well documented anywhere...
You can try get variable declaration and check it:
var decl = model.GetSymbolInfo(identifier)
.Symbol
?.DeclaringSyntaxReferences
.FirstOrDefault();

.Net - Merging In-memory Assemblies

I have a project where I am dynamically compiling code from a string as such:
public static Assembly BuildItem(Document doc)
{
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = false;
parameters.GenerateInMemory = true;
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults results = provider.CompileAssemblyFromSource(parameters, doc.GetCompileString());
return results.CompiledAssembly;
}
What I'd like to be able to do is take these resulting assembly files and combine them into a single assembly without writing them to disk. I know about ILMerge, and that is currently a fallback plan if I need to do that. Any help is appreciated. Thanks in advance.
When compiling you can pass an array of source files, then they're all in one assembly. e.g
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults results = provider.CompileAssemblyFromSource(parameters, new string [] { source1, source2... })
Alternatively if you really have to call CompileAssemblyFromSource seperately for each source. You could add all the generated assemblies as embedded resources to another assembly using
CSharpCodeProvider provider = new CSharpCodeProvider();
foreach(string assemblyPath in generatedAssemblies)
provider.EmbeddedResources.Add(assemblyPath);
Then...
CompilerResults results = provider.CompileAssemblyFromSource(parameters, source);
...where source is from the following blog which describes how to load assemblies from embedded resources...
http://blogs.msdn.com/b/microsoft_press/archive/2010/02/03/jeffrey-richter-excerpt-2-from-clr-via-c-third-edition.aspx

Compile Assembly on the fly

I have this code:
static void Main(string[] args)
{
CompilerParameters cp = new CompilerParameters
{
GenerateInMemory = true,
IncludeDebugInformation = false,
};
cp.ReferencedAssemblies.AddRange(new string[]{
"System.dll",
"System.Data.dll",
"System.Xml.dll",
"Microsoft.mshtml.dll",
"System.Windows.Forms.dll"
});
Assembly _assembly = Assembly.GetExecutingAssembly();
StreamReader _textStreamReader = new StreamReader(_assembly.GetManifestResourceStream("myprog.restext.txt"));
string src = _textStreamReader.ReadToEnd();
byte[] code = Convert.FromBase64String(src);
src = Encoding.UTF8.GetString(code);
CompilerResults cr = CSharpCodeProvider.CreateProvider("CSharp").
CompileAssemblyFromSource(cp, src);
Assembly asm = cr.CompiledAssembly;
Type typ = asm.GetType("clicker.Program");
MethodInfo method = typ.GetMethod("DoStart");
method.Invoke(null, new[] { (object)args });
}
I thows FileNotFoundException becouse CompileAssemblyFromSource returns the same error. Source using mshtml.
Then I'm trying to compile it using csc.exe, it says:
error CS0006. (no Metadata for "Microsoft.mshtml.dll")
I think it because mshtml is ActiveX library. So The question is how to assemble source usings activeX mshtml.
p.s.
Source has no errors and successfully has compiled from VS but can't be compiled by "on the fly" compilation.
I thows FileNotFoundException
That's normal, Microsoft.mshtml.dll is a primary interop assembly. It is not part of the .NET Framework so cannot be located automatically. It also won't be available on the user's machine, PIAs have to be installed.
The best way to go about it is to ensure that the assembly is present in your build directory so it will be deployed along with your program and can always be found. Project + Add Reference, select Microsoft.mshtml. Select it from the References node and set the Isolated property to False, Copy Local to True. Rebuild and verify that you now have Microsoft.mshtml.dll present in your bin\Debug directory.
And modify your code to pass the full path name to the file. Like this:
var referenceAssemblies = new List<string> {
"System.dll",
"System.Data.dll",
"System.Xml.dll",
"System.Windows.Forms.dll"
};
var homedir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var mshtml = Path.Combine(homedir, "Microsoft.mshtml.dll");
referenceAssemblies.Add(mshtml);
cp.ReferencedAssemblies.AddRange(referenceAssemblies.ToArray());

Categories