Search code examples
c#code-generationroslynexpression-evaluationassembly-loading

How to unload an assembly in .NET Core / make it collectible?


How can I unload an assemlby in .NET Core ?

Note:
.NET Core does not support AppDomains.

Background:
I have to evaluate user-generated VisualBasic expressions dynamically.
So to do this, I dynamically compile the expressions with Roslyn.
I load the resulting assemby from the byte array generated by the Roslyn compiler.
Then I create an instance that implements an abstract class (so I don't have to use reflection). Then I call the method EvaluateExpression of the abstract class.
After this is done, I want to unload the loaded assembly (otherwise, I will have the pleasure of memory leaks).
So I unload the the assembly right after I evaluated the expression:

Parameters.AbstractEvaluator x = RoslynExpressionEvaluator.CreateEvaluator(expression, report.Code);
object value = x.EvaluateExpression();
x.LoadContext.Unload();

(loadContext is saved in the abstract class at generation)

Everything works fine so far, but at x.LoadContext.Unload();, I get

System.InvalidOperationException: "Cannot unload non-collectible AssemblyLoadContext."

Is it possible to fix that ?
How can I make an assembly collectible ?
Also, I noticed I can load an assembly with the same class name (no namespace in code as you can see)
How does this fare in a multi-threading environment (aka web) ?
Can I just load and load different versions of the dynamically generated class ad infinitum, until the machine runs out of RAM with no malfunctions ?
Or why does this work at all when loading the same class twice ?

using Microsoft.CodeAnalysis.Operations;

namespace ReportTester
{


    public static class RoslynExpressionEvaluator
    {
        // a utility method that creates Roslyn compilation
        // for the passed code. 
        // The compilation references the collection of 
        // passed "references" arguments plus
        // the mscore library (which is required for the basic
        // functionality).
        private static Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation
            CreateCompilationWithMscorlib
            (
                string assemblyOrModuleName,
                string code,
                Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilationOptions compilerOptions = null,
                System.Collections.Generic.IEnumerable<Microsoft.CodeAnalysis.MetadataReference> references = null)
        {
            // create the syntax tree
            Microsoft.CodeAnalysis.SyntaxTree syntaxTree =
                Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory.ParseSyntaxTree(code, null, "");

            // get the reference to mscore library
            Microsoft.CodeAnalysis.MetadataReference mscoreLibReference =
                Microsoft.CodeAnalysis.AssemblyMetadata
                    .CreateFromFile(typeof(string).Assembly.Location)
                    .GetReference();

            // create the allReferences collection consisting of 
            // mscore reference and all the references passed to the method
            System.Collections.Generic.List<Microsoft.CodeAnalysis.MetadataReference> allReferences =
                new System.Collections.Generic.List<Microsoft.CodeAnalysis.MetadataReference>() { mscoreLibReference  };

            if (references != null)
            {
                allReferences.AddRange(references);
            } // End if (references != null) 

            // create and return the compilation
            Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation compilation =
                Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation.Create
                (
                    assemblyOrModuleName,
                    new[] {syntaxTree},
                    options: compilerOptions,
                    references: allReferences
                );

            return compilation;
        } // End Function CreateCompilationWithMscorlib 


        // emit the compilation result into a byte array.
        // throw an exception with corresponding message
        // if there are errors
        private static byte[] EmitToArray( this Microsoft.CodeAnalysis.Compilation compilation )
        {
            using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
            {
                // emit result into a stream
                Microsoft.CodeAnalysis.Emit.EmitResult emitResult = compilation.Emit(stream);

                if (!emitResult.Success)
                {
                    // if not successful, throw an exception
                    foreach (Microsoft.CodeAnalysis.Diagnostic thisError in emitResult.Diagnostics)
                    { 
                        if(thisError.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)
                            throw new System.Exception(thisError.GetMessage());
                    } // Next thisError 

                } // End if (!emitResult.Success) 

                // get the byte array from a stream
                return stream.ToArray();
            } // End Using stream 

        } // End Function EmitToArray 


        public static Parameters.AbstractEvaluator CreateEvaluator(string expression, string code)
        {
            try
            {
                // the main class Program contain static void Main() 
                // that calls A.Print() and B.Print() methods
                string mainProgramString = @"
Option Strict Off
Option Explicit Off
Option Infer On

Imports ReportTester.Parameters

imports System
Imports System.Collections.Generic
Imports Microsoft.VisualBasic.Strings
Imports Microsoft.VisualBasic.Interaction
Imports Microsoft.VisualBasic.Information



Public Class CodeImplementation
" + code + @"
End Class ' CodeImplementation




Public Class RsEval
    Inherits AbstractEvaluator


    Public Code As CodeImplementation


    Public Sub New()
        Me.New(New ParameterCollection)
    End Sub


    Public Sub New(ByVal allParameters As ParameterCollection)
        MyBase.New(allParameters)
        'code
    End Sub


    Public Overrides Function EvaluateExpression() As Object
        Return " + expression + @"
    End Function


End Class ' RsEval

";

                Microsoft.CodeAnalysis.MetadataReference sysRuntime =
                    Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(
                        typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly.Location);

                Microsoft.CodeAnalysis.MetadataReference vbRuntime =
                    Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(typeof(Microsoft.VisualBasic.Constants)
                        .Assembly
                        .Location);

                Microsoft.CodeAnalysis.MetadataReference sysCorlib =
                    Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(typeof(object).Assembly.Location);

                Microsoft.CodeAnalysis.MetadataReference sysConsole =
                    Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(typeof(System.Console).Assembly.Location);


                Microsoft.CodeAnalysis.MetadataReference reportParameters =
                    Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(typeof(ReportTester.Parameters.ParameterValue).Assembly.Location);


                Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilationOptions co = 
                    new Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilationOptions
                    (
                        Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary
                    );


                co.WithOptionStrict(Microsoft.CodeAnalysis.VisualBasic.OptionStrict.Off);
                co.WithOptionExplicit(false);
                co.WithOptionInfer(true);


                // create the Roslyn compilation for the main program with
                // ConsoleApplication compilation options
                // adding references to A.netmodule and B.netmodule
                Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation mainCompilation =
                    CreateCompilationWithMscorlib
                    (
                        "program",
                        mainProgramString,
                        // note that here we pass the OutputKind set to ConsoleApplication
                        compilerOptions: co,
                        references: new[] {sysRuntime, vbRuntime, sysCorlib, sysConsole, reportParameters }
                    );

                // Emit the byte result of the compilation
                byte[] result = mainCompilation.EmitToArray();

                // System.AppDomain temporaryAppDomain = System.AppDomain.CreateDomain("TemporaryAppDomain");
                // System.Reflection.Assembly assembly = temporaryAppDomain.Load(result);
                // not supported ... 

                // Load the resulting assembly into the domain. 
                System.Reflection.Assembly assembly = System.Reflection.Assembly.Load(result);


                // here we get the Program type and 
                // call its static method Main()
                // to test the program. 

                // get the type Program from the assembly
                System.Type programType = assembly.GetType("RsEval");




                Parameters.AbstractEvaluator x = (Parameters.AbstractEvaluator)System.Activator.CreateInstance(programType);
                x.LoadContext = System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(assembly);


                // Get the static Main() method info from the type
                // System.Reflection.MethodInfo method = programType.GetMethod("EvaluateExpression");
                // invoke Program.Main() static method
                // object retValue = method.Invoke(null, null);

                // System.AppDomain.Unload(temporaryAppDomain);

                return x;
            }
            catch (System.Exception ex)
            {
                System.Console.WriteLine(ex.ToString());
                throw;
            }

            return null;
        } // End Sub Test 


    } // End Class RoslynExpressionEvaluator 


} // End Namespace ReportTester 

Abstract class:

Public MustInherit Class AbstractEvaluator

    Public Parameters As ParameterCollection

    Public LoadContext As System.Runtime.Loader.AssemblyLoadContext


    Public Sub New()
        Me.New(New ParameterCollection)
    End Sub


    Public Sub New(ByVal allParameters As ParameterCollection)
        Me.Parameters = allParameters
    End Sub

    Public Overridable Sub SetValue(ByVal parameterName As String, parameter As ReportParameter)
        Me.Parameters.Parameters(parameterName) = parameter
    End Sub

    Public Overridable Function GetValue(ByVal parameterName As String) As ReportParameter
        Return Me.Parameters.Parameters(parameterName)
    End Function


    Public Overridable Sub SetParameters(ByVal allParameters As ParameterCollection)
        Me.Parameters = Nothing
        Me.Parameters = allParameters
    End Sub


    Public MustOverride Function EvaluateExpression() As Object



    '  Public Parameters As ParameterCollection
    ' Public MustOverride Sub SetCompulsoryParameter()


End Class ' AbstractEvaluator


' example
Public Class ConcreteEvaluator
    Inherits AbstractEvaluator



    Public Sub New()
        Me.New(New ParameterCollection)
    End Sub


    Public Sub New(ByVal allParameters As ParameterCollection)
        MyBase.New(allParameters)
        'code
    End Sub



    'Public Overrides Sub SetCompulsoryParameter()
    'End Sub

    Public Overrides Function EvaluateExpression() As Object
        Dim expression As String = "System.DateTime.Now.AddDays(1+2+3).ToString(""dd.MM.yyyy"")" ' string expression would come from report, compiled with roslyn
        Return " + expression + @"
    End Function


End Class

Solution

  • Hmm, the problem is that it unloads the entire context.
    Which means, if I load the assembly into the Default-Context, aka System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(ms);;
    Then, on unload, it would unload the entire default context.
    Definitely not good... ==>

    System.InvalidOperationException: "Cannot unload non-collectible AssemblyLoadContext."

    So you have to load the assembly in a different context (akin to AppDomain)

    public class CollectibleAssemblyLoadContext : AssemblyLoadContext
    {
        public CollectibleAssemblyLoadContext() : base(isCollectible: true)
        { }
    
        protected override Assembly Load(AssemblyName assemblyName)
        {
            return null;
        }
    }
    
    byte[] result = null; // Assembly Emit-result from roslyn
    System.Runtime.Loader.AssemblyLoadContext context = new CollectibleAssemblyLoadContext();
    System.IO.Stream ms = new System.IO.MemoryStream(result);
    System.Reflection.Assembly assembly = context.LoadFromStream(ms);
    
    
    System.Type programType = assembly.GetType("RsEval");
    MyAbstractClass eval = (MyAbstractClass) System.Activator.CreateInstance(programType);
    eval.LoadContext = context;
    eval.Stream = ms;
    // do something here with the dynamically created class "eval"
    

    and then you can say

    eval.LoadContext.Unload();
    eval.Stream.Dispose();
    

    Bonus if you put that into the IDisposable interface of the abstract class, then you can just use using, if you want to.

    using (Parameters.AbstractEvaluator x = RoslynExpressionEvaluator.CreateEvaluator(expression, report.Code))
    {
        object value = x.EvaluateExpression();
        System.Console.WriteLine(value);
    }
    

    So the abstract class looks like this:

    Public MustInherit Class AbstractEvaluator
        Implements IDisposable
    
        Public Parameters As ParameterCollection
    
        Public LoadContext As System.Runtime.Loader.AssemblyLoadContext
        Public Stream As System.IO.Stream
    
    
    
        Private disposedValue As Boolean ' Dient zur Erkennung redundanter Aufrufe.
    
        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not disposedValue Then
                If disposing Then
                    ' TODO: verwalteten Zustand (verwaltete Objekte) entsorgen.
    
                    If Me.LoadContext IsNot Nothing Then
                        Me.LoadContext.Unload()
                        Me.LoadContext = Nothing
                    End If
    
                    If Me.Stream IsNot Nothing Then
                        Me.Stream.Dispose()
                        Me.Stream = Nothing
                    End If
    
                End If
    
                ' TODO: nicht verwaltete Ressourcen (nicht verwaltete Objekte) freigeben und Finalize() weiter unten überschreiben.
                ' TODO: große Felder auf Null setzen.
            End If
            disposedValue = True
        End Sub
    
        ' TODO: Finalize() nur überschreiben, wenn Dispose(disposing As Boolean) weiter oben Code zur Bereinigung nicht verwalteter Ressourcen enthält.
        'Protected Overrides Sub Finalize()
        '    ' Ändern Sie diesen Code nicht. Fügen Sie Bereinigungscode in Dispose(disposing As Boolean) weiter oben ein.
        '    Dispose(False)
        '    MyBase.Finalize()
        'End Sub
    
        ' Dieser Code wird von Visual Basic hinzugefügt, um das Dispose-Muster richtig zu implementieren.
        Public Sub Dispose() Implements IDisposable.Dispose
            ' Ändern Sie diesen Code nicht. Fügen Sie Bereinigungscode in Dispose(disposing As Boolean) weiter oben ein.
            Dispose(True)
            ' TODO: Auskommentierung der folgenden Zeile aufheben, wenn Finalize() oben überschrieben wird.
            ' GC.SuppressFinalize(Me)
        End Sub
    
    
    
        Public Sub New()
            Me.New(New ParameterCollection)
        End Sub
    
    
        Public Sub New(ByVal allParameters As ParameterCollection)
            Me.Parameters = allParameters
        End Sub
    
        Public Overridable Sub SetValue(ByVal parameterName As String, parameter As ReportParameter)
            Me.Parameters.Parameters(parameterName) = parameter
        End Sub
    
        Public Overridable Function GetValue(ByVal parameterName As String) As ReportParameter
            Return Me.Parameters.Parameters(parameterName)
        End Function
    
    
        Public Overridable Sub SetParameters(ByVal allParameters As ParameterCollection)
            Me.Parameters = Nothing
            Me.Parameters = allParameters
        End Sub
    
    
        Public MustOverride Function EvaluateExpression() As Object
    
        ' Public Parameters As ParameterCollection
        ' Public MustOverride Sub SetCompulsoryParameter()
    
    
    End Class ' AbstractEvaluator
    
    
    ' example
    Public Class ConcreteEvaluator
        Inherits AbstractEvaluator
    
        Class SimplisticExampleCode
    
            Public Function Tomorrow() As System.DateTime
                Return System.DateTime.Now.AddDays(1)
            End Function
    
        End Class
    
        Friend Code As SimplisticExampleCode
    
    
        Public Sub New()
            Me.New(New ParameterCollection)
        End Sub
    
    
        Public Sub New(ByVal allParameters As ParameterCollection)
            MyBase.New(allParameters)
            'code
            Me.Code = New SimplisticExampleCode
        End Sub
    
    
    
        'Public Overrides Sub SetCompulsoryParameter()
        'End Sub
    
        Public Overrides Function EvaluateExpression() As Object
            'Dim expression As String = "System.DateTime.Now.AddDays(1+2+3).ToString(""dd.MM.yyyy"")" ' string expression would come from report, compiled with roslyn
            'Return " + expression + @"
            Return Code.Tomorrow().ToString("dd.MM.yyyy")
        End Function
    
    
    End Class