vb.net.net-6.0roslynroslyn-code-analysiscodedom

Set a value to a field in a compiled assembly from an object that exists outside the compiled assembly


I am currently upgrading an old application from .Net Framework 4.5.2 to .Net 6. The application currently compiles code through CodeDom and then executes. In .Net 6, compiling through CodeDom is no longer supported so I have been converting the code to instead use Microsoft.CodeAnalysis (Roslyn). I have managed to compile the code in .Net 6 and eliminate all errors and warnings.

What I cannot figure out is how to set a value to a field in the compiled code from an object that exists outside the compiled code.

In CodeDom you can use System.Reflection.FieldInfo and compiledCode.GetType("Script").GetField("app") to obtain the field. Then use .SetValue to set the value. So far I have not found an equivalent command for Roslyn.

Here is an example of the CodeDom code that works. 'app' is the field the value is being set to.

'CodeDom example
'Add referenced assemblies
Dim compileParams As System.CodeDom.Compiler.CompilerParameters = New CompilerParameters
compileParams.ReferencedAssemblies.Add("system.dll")
compileParams.ReferencedAssemblies.Add("system.xml.dll")
compileParams.ReferencedAssemblies.Add("system.data.dll")
compileParams.ReferencedAssemblies.Add("Microsoft.VisualBasic.dll")
compileParams.ReferencedAssemblies.Add("Microsoft.VisualBasic.Compatibility.dll")
compileParams.ReferencedAssemblies.Add("mscorlib.dll")
compileParams.ReferencedAssemblies.Add(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly.Location) & "\CustomDLL.dll")
compileParams.CompilerOptions = "/t:library"
compileParams.GenerateInMemory = True
compileParams.GenerateExecutable = False

'Script example. Formatted for readability.
Dim sb As StringBuilder = New StringBuilder
sb.Append(
"Option Strict Off
 Option Explicit On
 Imports System
 Imports System.XML
 Imports System.Data
 Imports Microsoft.VisualBasic
 Imports Microsoft.VisualBasic.Compatibility
 Imports CustomDLL

 Module Script 
      Public app as Object

      Sub ProcessName()
           Call app.Execute(app.statements.executeStatement(1))
           Call app.Execute(app.statements.executeStatement(2))
           ...
      End Sub
 End Module")

'Compile
Dim codeProvider As VBCodeProvider = New VBCodeProvider
Dim compileResults As CompilerResults = codeProvider.CompileAssemblyFromSource(compileParams, sb.ToString)

'Set value for 'app' in compiled program
'objApp is a global object that exists outside the compiled program and its class is defined by the 'CustomDLL'
Dim field As System.Reflection.FieldInfo
field = compileResults.CompiledAssembly.GetType("Script").GetField("app")
field.SetValue(Nothing, CType(objApp, Object))

'Execute
Dim assemb As System.Reflection.Assembly = compileResults.CompiledAssembly
Dim method As System.Reflection.MethodInfo = assemb.GetType("Script").GetMethod(processName)
Dim returnObj As Object = method.Invoke(Nothing, Nothing)

Here is an example of the converted code for .Net 6. Need to set the value for 'app'.

'Roslyn example
'Add reference paths
Dim refPaths = {GetType(System.Object).GetTypeInfo().Assembly.Location,
                GetType(Console).GetTypeInfo().Assembly.Location,
                Path.Combine(Path.GetDirectoryName(GetType(System.Runtime.GCSettings).GetTypeInfo().Assembly.Location), "System.Runtime.dll"),
                Path.Combine(Path.GetDirectoryName(GetType(System.Runtime.GCSettings).GetTypeInfo().Assembly.Location), "System.Data.dll"),
                Path.Combine(Path.GetDirectoryName(GetType(System.Runtime.GCSettings).GetTypeInfo().Assembly.Location), "System.XML.dll"),
                System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly.Location) & "\CustomDLL.dll"}
Dim references As As Microsoft.CodeAnalysis.MetadataReference() = refPaths.[Select](Function(r) MetadataReference.CreateFromFile(r)).ToArray()

'Script example. Formatted for readability.
Dim sb As StringBuilder = New StringBuilder
sb.Append(
"Option Strict Off
 Option Explicit On
 Imports System
 Imports System.XML
 Imports System.Data
 Imports Microsoft.VisualBasic
 Imports Microsoft.VisualBasic.Compatibility
 Imports CustomDLL

 Module Script 
      Public app as Object

      Sub ProcessName()
           Call app.Execute(app.statements.executeStatement(1))
           Call app.Execute(app.statements.executeStatement(2))
           ...
      End Sub
 End Module")
 Dim syntax As Microsoft.CodeAnalysis.SyntaxTree = VisualBasicSyntaxTree.ParseText(sb.ToString())

'Compile options
Dim assemblyName As String = Path.GetRandomFileName()
Dim compileOpts As New VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
                                                             embedVbCoreRuntime:=True)

'Compile
Dim objVBCompilation As Microsoft.CodeAnalyis.VisualBasic.VisualBasicCompilation = VisualBasicCompilation.Create(assemblyName,
                                                syntaxTrees:={syntax},
                                                references:=references,
                                                options:=compileOpts)

'How do I set a value to 'app' like in the CodeDom example?

I can see that the field 'app' exists when I search through the symbols.

Dim sybmols As IEnumerable(Of ISymbol) = objVBCompilation.GetSymbolsWithName("app")
If symbols IsNot Nothing Then
    For Each symbol As ISymbol In symbols
        
    Next
End If

But I am unable to convert the symbol or a sub-part of the sybmol to FieldInfo. I have also not found a method for setting the value for the symbol.

I can also see that 'app' exists in the SyntaxTree, but again I have not found a way to set the value there.


Solution

  • After spending several days trying to figure this out, I managed to succeed after posting the question to StackOverflow...

    I needed to take the compiled code, load it into System.Reflection.Assembly and then find the field and set it. I was also able to follow up with MethodInfo to call the correct process.

    Using ms As New MemoryStream
        Dim objEmitResult As EmitResult = objVBCompilation.Emit(ms)
        If objEmitResult.Success = True Then
            ms.Seek(0, SeekOrigin.Begin)
    
            'Set field 'app'
            objAssembly = AssemblyLoadContext.Default.LoadFromStream(ms)
            Dim type As Type = objAssembly.GetType("Script")
            Dim field As FieldInfo = type.GetField("app")
            field.SetValue(Nothing, CType(objApp, Object))
        End If
    End Using