I'm developing a .Net Standard 2.0 library offering dynamic compilation but there are always missing references.
A consumer using .Net Core, calls an Api with his source code as text, implementing a known interface by the library.
//Consumer Program.cs
using DynamicCompilationNetStandard2._0;
var source = """
using DynamicCompilationNetStandard2._0;
using System.Linq.Expressions;
namespace Consumer
{
public class MyTask : ITask
{
public void CanRun<T>(Expression<Func<T, bool>> predicate)
{
}
public void Run()
{
Console.WriteLine("Finished");
}
}
}
""";
Executor.Execute(source);
The library's Executor class:
using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;
using System.Linq.Expressions;
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))
.AddReferences(ReferenceAssemblies.NetStandard20)
.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()); //CRASH
}
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
try
{
var types = assembly.GetTypes();
}
catch (Exception ex)
{
throw; //CRASH
}
dynamic task = assembly.CreateInstance("Consumer.MyTask");
task.Run();
}
}
}
public interface ITask
{
void CanRun<T>(Expression<Func<T, bool>> predicate);
void Run();
}
I'm using basic-reference-assemblies to add references to reference assemblies instead of implementations. Still I get theses errors:
How can I import missing references and fixing this simple project?
Here is the source code: https://github.com/adampaquette/DynamicCompilationTests
Thanks!
Ok the problem was an implicit reference to System. After adding using System; in the source string, it worked.
Related
I am trying to read the types out of an assembly that contains Entity Framework Core, but I am getting this error:
Could not load file or assembly '..file path..\TestRoslyn\Database\bin\Debug\net5.0\Database.dll'. The system cannot find the file specified.
The code I am using to read the types is pretty simple:
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;
namespace TestRoslyn
{
class Program
{
static async Task Main(string[] args)
{
if (!MSBuildLocator.IsRegistered)
MSBuildLocator.RegisterDefaults();
using var w = MSBuildWorkspace.Create();
// Substitute your file location
var basePath = #"C:\Users\username\source\repos\";
var slnFile = #$"{basePath}TestRoslyn\TestRoslyn.sln";
var sln = await w.OpenSolutionAsync(slnFile);
foreach (var p in sln.Projects)
{
var asm = Assembly.LoadFrom(p.OutputFilePath);
foreach(var t in asm.GetTypes())
Console.WriteLine($"{p.OutputFilePath}\t{t.FullName}");
}
}
}
}
This works as is. However, when I add a simple project to the solution that references nuget package Microsoft.EntityFrameworkCore (5.0) with one file:
using Microsoft.EntityFrameworkCore;
namespace Database
{
public class AppContext: DbContext
{
}
}
(To keep it simple, I didn't include anything in the AppContext class except the base class.)
When I add a project with the EntityFrameworkCore nuget package to the solution above with just this class I get the error indicated above.
Not sure what the cause is? Do I need to somehow load the nuget package into the workspace? If so, how?
Use AssemblyDependencyResolver and custom AssemblyLoadContext (from System.Runtime.Loader) to help load assemblies with dependencies.
Here is the custom AssemblyLoadContext, allowing to resolve dependencies:
using System;
using System.Reflection;
using System.Runtime.Loader;
namespace TestRoslyn
{
public class LoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public LoadContext(string assemblyPath)
{
_resolver = new AssemblyDependencyResolver(assemblyPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}
}
It can be used like this:
foreach (var p in sln.Projects)
{
string outputPath = p.OutputFilePath;
AssemblyName assemblyName = AssemblyName.GetAssemblyName(outputPath);
LoadContext loadContext = new LoadContext(outputPath);
Assembly asm = loadContext.LoadFromAssemblyName(assemblyName);
foreach (var t in asm.GetTypes())
Console.WriteLine($"{p.OutputFilePath}\t{t.FullName}");
}
See details here and here.
I created Person.dll and register it(regsvcs.exe) in Command Promt for Visual Studio 2019. As a result of registration, I got Person.tlb. I tried to add Person.tlb in console project as reference COM component but I got warning MSB3290.
warning MSB3290: Failed to create the wrapper assembly for type
library "{8b1098cb-d453-4dc7-96ac-52df54d0a2ce}". Type library
'Person' was exported from a CLR assembly and cannot be re-imported as
a CLR assembly.
How I can to add Person.tlb in console project using reflection?
Person.dll:
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using System.Runtime.InteropServices;
using System.EnterpriseServices;
namespace COM
{
[ClassInterface(ClassInterfaceType.None)]
public class Person : ServicedComponent, COM.IPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsMale { get; set; }
public void Persist(string FilePath)
{
StreamWriter oFile = new StreamWriter(FilePath);
XmlSerializer oXmlSerializer = new XmlSerializer(typeof(Person));
oXmlSerializer.Serialize(oFile, this);
oFile.Flush();
oFile.Close();
}
static public Person Retrieve(string FilePath)
{
StreamReader oFile = new StreamReader(FilePath);
XmlSerializer oXmlSerilizer = new XmlSerializer(typeof(Person));
Person oPerson = oXmlSerilizer.Deserialize(oFile) as Person;
return oPerson;
}
}
}
Console project:
using System;
namespace Test10
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
COM.Person per = new COM.Person();
per.FirstName = "Maxim";
per.LastName = "Donax";
per.Persist(#" C:\myFile.xml ");
}
}
}
I used other way: created Person.dll in Visual Studio and registerd it(regsvcs.exe). After use reference Person.tlb in Visual Basic 6.0.
I am using online c# compiler just to determine the class and method name.See the code given below, I am intentionally generating an error.
Expected Output is:
Hello, world!
ExceptionTest
, basically from where the exception has been generated.
OUTPUT, I m getting is
Hello, world!
System.Reflection.RuntimeMethodInfo
//Rextester.Program.Main is the entry point for your code. Don't change it.
//Compiler version 4.0.30319.17929 for Microsoft (R) .NET Framework 4.5
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Diagnostics;
namespace Rextester
{
public class Program
{
public static void Main(string[] args)
{
try
{
//Your code goes here
Console.WriteLine("Hello, world!");
var abc = new Xyz();
abc.ExTest();
}
catch(Exception ex)
{
Console.WriteLine(new StackTrace().GetFrame(1).GetMethod().DeclaringType.FullName);
}
}
}
public class Xyz
{
public void ExTest()
{
var abc = new Abc();
abc.ExceptionTest();
}
}
public class Abc
{
public void ExceptionTest()
{
throw new Exception();
}
}
}
Please note, this is compiled on an online tool http://rextester.com/. I havent ran it on Visual Studio.
Simply, you could use Exception TargetSite;
catch(Exception ex)
{
Console.WriteLine(ex.TargetSite.Name);
}
I am trying to add support for System.Web.Mvc.HtmlHelper to a CLI app for compiling Razor templates, but although it compiles it fails at runtime with:
System.TypeLoadException: Could not load type 'HtmlHelper`1' from assembly '/Users/oligofren/src/razor-cli/build/System.Web.Mvc.dll'.
How should I proceed in fixing this?
I am not well versed in the core of .NET (here in Mono version), so I can't say if I have done anything wrong here. I have added all the assemblies to the build folder (where the exe ends up) and I also try to manually load the required assemblies before RazorEngine tries to compile the assemblies.
How can I resolve this?
Full source code
// See also tips on building cli apps with razorengine: https://github.com/Antaris/RazorEngine/blob/master/src/source/RazorEngine.Hosts.Console/RazorEngine.Hosts.Console.csproj
using System;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
using Moq;
using System.IO;
using Newtonsoft.Json.Linq;
using RazorEngine;
using RazorEngine.Templating; // For extension methods.
using RazorEngine.Configuration;
using RazorEngine.Text;
public class RazorCli
{
static public void Main (string[] args)
{
CheckCommandLine(args);
string template = ReadFile(args[0]);
JObject model = ParseModel(args[1]);
// try to load the required assemblies
//http://stackoverflow.com/a/23496144/200987
System.Reflection.Assembly.Load("System.Web");
System.Reflection.Assembly.Load("System.Web.Mvc");
var result = CompileTemplate(template, model);
Console.WriteLine (result);
}
private static string CompileTemplate (string template, JObject model)
{
string res = "";
var config = new TemplateServiceConfiguration();
// You can use the #inherits directive instead (this is the fallback if no #inherits is found).
config.BaseTemplateType = typeof(MyClassImplementingTemplateBase<>);
try
{
using (var service = RazorEngineService.Create(config))
{
res = service.RunCompile(template, "templateKey", null, model);
}
}
catch( RazorEngine.Templating.TemplateCompilationException ex )
{
Console.WriteLine (ex);
System.Environment.Exit(1);
}
return res;
}
/* Cannot dispatch a dynamic object to extension methods */
private static JObject ParseModel(string fileName){
string json = ReadFile(fileName);
return JObject.Parse(json);
}
private static void CheckCommandLine(string[] args){
if(args.Length != 2){
Usage();
System.Environment.Exit(1);
}
}
private static void Usage(){
string usage = "Usage: razor-cli <partial.cshtml> <model.json>\n";
Console.WriteLine(usage);
}
private static String ReadFile(string filename)
{
string result;
using (StreamReader sr = new StreamReader(filename))
{
result = sr.ReadToEnd();
}
return result;
}
}
public class MyHtmlHelper
{
public IEncodedString Raw(string rawString)
{
return new RawString(rawString);
}
}
// https://antaris.github.io/RazorEngine/TemplateBasics.html
public abstract class MyClassImplementingTemplateBase<T> : TemplateBase<T>
{
public MyClassImplementingTemplateBase()
{
Html = MvcHelpers.CreateHtmlHelper<Object>();
}
public HtmlHelper Html { get; set; }
}
// Ripped straight from a SO Q/A
// http://stackoverflow.com/questions/17271688/mocking-viewcontext-to-test-validation-error-messages
public class MvcHelpers {
public static HtmlHelper<TModel> CreateHtmlHelper<TModel>(ViewDataDictionary dictionary = null)
{
if (dictionary == null)
dictionary = new ViewDataDictionary { TemplateInfo = new TemplateInfo() };
var mockViewContext = new Mock<ViewContext>(
new ControllerContext(
new Mock<HttpContextBase>().Object,
new RouteData(),
new Mock<ControllerBase>().Object),
new Mock<IView>().Object,
dictionary,
new TempDataDictionary(),
new Mock<TextWriter>().Object);
var mockViewDataContainer = new Mock<IViewDataContainer>();
mockViewDataContainer.Setup(v => v.ViewData).Returns(dictionary);
return new HtmlHelper<TModel>(mockViewContext.Object, mockViewDataContainer.Object);
}
}
Details on how I run this can be seen in the Makefile, if that helps.
Further details
Installed Mono 4.2.2.0 using Homebrew on OS X 10.11.4.
I'm compiling assembly at runtime and link it via adding to new domain. I use it and then unload domain. But when I try to compile again during same run I can't get access to that assembly because it currently in use.
Here are some of my code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ClassLibrary2
{
public interface IExtension
{
String GetExtensionName();
}
}
My assembly
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ClassLibrary2;
namespace ClassLibrary1
{
public class Extension1 : MarshalByRefObject, IExtension
{
public Extension1()
{
}
public string GetExtensionName()
{
return "Extension 1 from " + AppDomain.CurrentDomain.FriendlyName;
}
}
}
And the app that uses it
namespace ConsoleApplication7
{
class Program
{
static IEnumerable<IExtension> extensions;
static void Main(string[] args)
{
// Create app domain
AppDomain domain = CreateDomain(Directory.GetCurrentDirectory());
try
{
// Get extensions
extensions = EnumerateExtensions(domain);
foreach (IExtension extension in extensions)
// Execute extension method in separate domain.
Console.WriteLine(extension.GetExtensionName());
// Unload domain
UnloadDomain(domain);
}
finally
{
domain = null;
GC.Collect(2);
extensions = null;
}
Console.ReadKey();
}
private static IEnumerable<IExtension> EnumerateExtensions(AppDomain domain)
{
IEnumerable<string> fileNames = Directory.EnumerateFiles(domain.BaseDirectory, "*.dll");
if (fileNames != null)
{
foreach (string assemblyFileName in fileNames)
{
foreach (string typeName in GetTypes(assemblyFileName, typeof(IExtension), domain))
{
System.Runtime.Remoting.ObjectHandle handle;
try
{
handle = domain.CreateInstanceFrom(assemblyFileName, typeName);
}
catch (MissingMethodException)
{
continue;
}
object obj = handle.Unwrap();
IExtension extension = (IExtension)obj;
yield return extension;
}
}
}
}
private static IEnumerable<string> GetTypes(string assemblyFileName, Type interfaceFilter, AppDomain domain)
{
Assembly asm = domain.Load(AssemblyName.GetAssemblyName(assemblyFileName));
Type[] types = asm.GetTypes();
foreach (Type type in types)
{
if (type.GetInterface(interfaceFilter.Name) != null)
{
yield return type.FullName;
}
}
}
static AppDomain CreateDomain(string path)
{
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = path;
return AppDomain.CreateDomain("Temporary domain", null, setup);
}
static void UnloadDomain(AppDomain domain)
{
AppDomain.Unload(domain);
}
}
}
So in Main() during Console.ReadKey(); assembly still locked and I can't get access to it (can't delete it via Windows for example).
Is there way to solve this?
I think the domain.Load is hooking up the assembly to your program try to the load inside the Extension1 try to move the GetTypes to the class Extension1
I don't remember but I think domain.Load just runs Assembly.LoadFrom( and that's what is connecting your application the the DLL.
Ok, I solved this problem. I used shadow copy, just configured shadow copy in that other domain and it worked for me.