Search code examples
.net-5appdomain

Recompile assemblies to separate appdomains in NET 5


I have a NET 5.0 console application, from which I am trying to compile and execute external code BUT also be able to update the code, unload the previously created appdomain and re-compile everything.

This is my entire static class that handles code compilation and assembly loading

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

namespace Scripting
{
    public static class ScriptCompiler
    {
        public static Dictionary<string, AppDomain> _appDomainDict = new();
        
        public static object CompileScript(string scriptpath)
        {
            var tree = SyntaxFactory.ParseSyntaxTree(File.ReadAllText(scriptpath));
            
            //Adding basic references
            List<PortableExecutableReference> refs = new List<PortableExecutableReference>();
            var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
            refs.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll")));
            refs.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll")));
            refs.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Private.CoreLib.dll")));
            refs.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll")));
            refs.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")));


            // A single, immutable invocation to the compiler
            // to produce a library
            string hash_name = scriptpath.GetHashCode();

            if (_appDomainDict.ContainsKey(hash_name))
            {
                AppDomain.Unload(_appDomainDict[hash_name]);
                _appDomainDict.Remove(hash_name);
            }
            
            AppDomain new_domain = AppDomain.CreateDomain(hash_name);
            _appDomainDict[hash_name] = new_domain;

            var compilation = CSharpCompilation.Create(hash_name)
              .WithOptions(
                new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
                                             optimizationLevel: OptimizationLevel.Release,
                                             allowUnsafe:true))
              .AddReferences(refs.ToArray())
              .AddSyntaxTrees(tree);
            MemoryStream ms = new MemoryStream();
            EmitResult compilationResult = compilation.Emit(ms);
            ms.Seek(0, SeekOrigin.Begin);
            if (compilationResult.Success)
            {
                // Load the assembly
                Assembly asm = new_domain.Load(ms.ToArray());
                
                object main_ob = asm.CreateInstance("SomeClass");
                ms.Close();
                return main_ob;
            }
            else
            {
                foreach (Diagnostic codeIssue in compilationResult.Diagnostics)
                {
                    string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()}," +
                        $" Location: { codeIssue.Location.GetLineSpan()}," +
                        $" Severity: { codeIssue.Severity}";
                    Callbacks.Logger.Log(typeof(NbScriptCompiler), issue, LogVerbosityLevel.WARNING);
                }
                return null;
            }
        }

    }
}

Its all good when I am trying load the assembly in the current domain and execute from the instantiated object. The problem with this case is that since I wanna do frequent updates to the code, even if I make sure that the assembly names are different. I'll end up loading a ton of unused assemblies to the current domain.

This is why I've been trying to create a new domain and load the assembly there. But for some reason I get a platform not supported exception. Is this not possible to do in NET 5? Are there any workarounds or am I doing something wrong here.


Solution

  • Ok, it turns out that AppDomain support for NET Core + is very limited and in particular there seems to be only one appdomain

    On .NET Core, the AppDomain implementation is limited by design and does not provide isolation, unloading, or security boundaries. For .NET Core, there is exactly one AppDomain. Isolation and unloading are provided through AssemblyLoadContext. Security boundaries should be provided by process boundaries and appropriate remoting techniques.

    Source: https://learn.microsoft.com/en-us/dotnet/api/system.appdomain?view=net-6.0

    And indeed, when trying to use AssemblyLoadContext and create object instances through these contexts everything worked like a charm!

    One last note is that if the created context is not marked as collectible, its not possible to unload it. But this can be very easily set during AssemblyLoadContext construction.