Registering a generated VB.Net DLL assembly programmatically without locking the DLL

4.9k Views Asked by At

I've got a simple VB.Net 4 WinForms application that does basic code generation. The code generation creates a DLL assembly perfectly fine, but each time the DLL is generated it needs to be registered programmatically with the GAC. The reason it must be registered is that it is a COM object that when deployed gets called via CreateObject from a VB6 application. Eww, I know.

All of this works fine: the DLL generation, registering programmatically and using the generated DLL from the VB6 application.

The problem is that my application that does the code generation can only generate the DLL once before the DLL is locked out by the process with no way to unlock it without stopping the EXE and starting it up again. This obviously prevents the user of the code generation tool from making a change and recompiling the DLL without restarting the application.

The code that causes the lock is the following (on the line defining the variable "asm"):

Public Function RegisterAssembly() As Boolean
    Dim success As Boolean = False

    Try
        Dim asm As [Assembly] = [Assembly].LoadFile(Path.Combine(My.Computer.FileSystem.SpecialDirectories.Temp, "my.dll"))
        Dim regasm As New RegistrationServices()

        success = regasm.RegisterAssembly(asm, AssemblyRegistrationFlags.None)
    Catch ex As Exception
        success = False
        Throw ex
    End Try

    Return success
End Function

I've tried throwing the assembly definition into a different AppDomain as I've seen in a number of articles on the web, but none of the implementations that I've come up with has worked. In fact, nearly all of them ended up with the generated assembly then being defined in BOTH of the AppDomains. I've also tried doing a ReflectionOnly assembly load but in order to the the register functions the assembly must be loaded in active mode not reflection mode.

The actual error that it throws is this wonderful gem:

Error Number: BC31019
Error Message: Unable to write to output file 'C:\Users\Me\AppData\Local\Temp\my.dll': The process cannot access the file because it is being used by another process. 

If anyone has an answer for me on what I can do to fix this, I'd be greatly appreciative! Like I said, it works great the first DLL compile but subsequent compiles of the DLL fail because the assembly is locked by the application process.

My full compiler class definition is below. I've left in some of the additional stuff I've tried, sorry for the clutter:

Imports System.CodeDom.Compiler
Imports System.Text
Imports System.IO
Imports System.Reflection
Imports System.Runtime.InteropServices

Public Class ExitCompiler

Private _errorMessageContents As String = ""
Private _errorCount As Integer = -1

Public ReadOnly Property ErrorMessageText As String
    Get
        Return _errorMessageContents
    End Get
End Property

Public ReadOnly Property ErrorCount As Integer
    Get
        Return _errorCount
    End Get
End Property

Public Function Compile(ByVal codeFileInfo As FileInfo) As Boolean
    Dim success As Boolean = False
    Dim codeContents As String = CodeReader.ReadAllContents(codeFileInfo.FullName)

    success = Compile(codeContents)

    Return success
End Function

Public Function Compile(ByVal codeContents As String) As Boolean
    _errorMessageContents = ""

    'asmAppDomain = AppDomain.CreateDomain("asmAppDomain", AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.RelativeSearchPath, AppDomain.CurrentDomain.ShadowCopyFiles)

    LogLoadedAssemblies(AppDomain.CurrentDomain)
    ' LogLoadedAssemblies(asmAppDomain)

    Try
        ' Remove output assemblies from previous compilations
        'RemoveAssembly()
    Catch uaEx As UnauthorizedAccessException
        Throw uaEx
    End Try

    Dim success As Boolean = False
    Dim outputFileName As String = Path.Combine(My.Computer.FileSystem.SpecialDirectories.Temp, "my.dll")
    Dim results As CompilerResults

    Dim codeProvider As New VBCodeProvider()
    Dim parameters As New CompilerParameters()

    parameters.TreatWarningsAsErrors = False
    parameters.CompilerOptions = "/optimize"
    parameters.TempFiles = New TempFileCollection(My.Computer.FileSystem.SpecialDirectories.Temp, False)
    parameters.OutputAssembly = outputFileName
    parameters.ReferencedAssemblies.Add("System.dll")
    parameters.ReferencedAssemblies.Add("System.Data.dll")
    parameters.ReferencedAssemblies.Add("System.Xml.dll")

    results = codeProvider.CompileAssemblyFromSource(parameters, codeContents)

    _errorCount = results.Errors.Count

    If _errorCount > 0 Then
        success = False

        'There were compiler errors
        Dim sb As New StringBuilder
        For Each compileError As CompilerError In results.Errors
            sb.AppendLine()
            sb.AppendLine("Line number: " & compileError.Line)
            sb.AppendLine("Error Number: " & compileError.ErrorNumber)
            sb.AppendLine("Error Message: " & compileError.ErrorText)
            sb.AppendLine()
        Next

        _errorMessageContents = sb.ToString()
    Else
        success = True

        ' Successful compile, now generate the TLB (Optional)
        'success = GenerateTypeLib()

        If success Then
            ' Type lib generated, now register with GAC
            Try
                success = RegisterAssembly()
            Catch ex As Exception
                success = False
            End Try
        End If
    End If

    Return success
End Function

'Private Function GenerateTypeLib() As Boolean
'    Dim success As Boolean = False

'    Try
'        Dim asm As [Assembly] = [Assembly].ReflectionOnlyLoadFrom(My.Computer.FileSystem.SpecialDirectories.Temp & "\my.dll")
'        Dim converter As New TypeLibConverter()
'        Dim eventHandler As New ConversionEventHandler()

'        Dim typeLib As UCOMICreateITypeLib = CType(converter.ConvertAssemblyToTypeLib(asm, My.Computer.FileSystem.SpecialDirectories.Temp & "\my.tlb", 0, eventHandler), UCOMICreateITypeLib)
'        typeLib.SaveAllChanges()

'        success = True
'    Catch ex As Exception
'        success = False
'        Throw ex
'    End Try

'    Return success
'End Function

Public Function RegisterAssembly() As Boolean
    Dim success As Boolean = False

    Try
        Dim asm As [Assembly] = [Assembly].LoadFile(Path.Combine(My.Computer.FileSystem.SpecialDirectories.Temp, "my.dll"))
        Dim regasm As New RegistrationServices()

        success = regasm.RegisterAssembly(asm, AssemblyRegistrationFlags.None)
    Catch ex As Exception
        success = False
        Throw ex
    End Try

    Return success
End Function

Public Sub RemoveAssembly()
    'AppDomain.Unload(asmAppDomain)

    File.Delete(Path.Combine(My.Computer.FileSystem.SpecialDirectories.Temp, "my.dll"))
    'File.Delete(Path.Combine(My.Computer.FileSystem.SpecialDirectories.Temp, "my.tlb"))
End Sub

Private Shared Sub LogLoadedAssemblies(appDomain__1 As AppDomain)
    Dim sb As New StringBuilder

    sb.AppendLine("Loaded assemblies in appdomain: " & appDomain__1.FriendlyName)
    For Each loadedAssembly As Assembly In AppDomain.CurrentDomain.GetAssemblies()
        sb.AppendLine("- " & loadedAssembly.GetName().Name)
    Next

    MessageBox.Show(sb.ToString())
End Sub

'Private Shared Function CurrentDomain_ReflectionOnlyAssemblyResolve(sender As Object, args As ResolveEventArgs) As Assembly
'    Return System.Reflection.Assembly.ReflectionOnlyLoad(args.Name)
'End Function
End Class
3

There are 3 best solutions below

1
On

When using CompileAssemblyFromSource you are not only compiling, you are loading the Assembly into the current process, which would then be normal to not have it released until the the appdomain is unloaded.

Have you considered just using the command line compilers vbc? It would perform the compilation independent of the app domain and then the DLL would only be created on disk, no references to it would lock it?

1
On

needs to be registered programmatically with the GAC. The reason it must be registered is that it is a COM object

You do not need to put you .NET-based COM objects into the GAC. You just need to register them with the standard .NET regasm utility, and you can invoke the regasm utility as an external process using the System.Diagnostics.Proces object.

Is there another reason you are using the GAC for your COM object?

5
On

The way that we handled this for automatically installing .Net installer classes, which has the same issue with loading and locking the DLL in the current appdomain, is to perform the registration in a new appdomain.

Before I get to that, however, one additional shortcut that you might consider is using a process to call regsvr32 to register to DLL silently.

The following code is specific to our solution, but should give you an idea:

''' <summary>
''' Register the specified file, using the mechanism specified in the .Registration property
''' </summary>
''' <param name="sFileName"></param>
''' <remarks></remarks>
Private Sub Register(ByVal sFileName As String)
    ' Exceptions are ignored

    Dim oDomain As AppDomain = Nothing
    Try
        Dim oSetup As New System.AppDomainSetup()

        With oSetup
            .ApplicationBase = ApplicationConfiguration.AppRoot
            .ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
            .LoaderOptimization = LoaderOptimization.SingleDomain
            .DisallowBindingRedirects = False
            .DisallowCodeDownload = True
        End With

        ' Launch the application
        oDomain = AppDomain.CreateDomain("AutoUpdater", AppDomain.CurrentDomain.Evidence, oSetup)

        oDomain.CreateInstance(GetType(FileRegistration).Assembly.FullName, GetType(FileRegistration).ToString, True, Reflection.BindingFlags.Default, Nothing, New Object() {sFileName}, Nothing, Nothing, AppDomain.CurrentDomain.Evidence)
    Catch theException As Exception
        ' Suppress errors to the end user
        Call ReportError(theException, True)
    Finally
        If oDomain IsNot Nothing Then
            AppDomain.Unload(oDomain)
        End If
    End Try
End Sub

Class FileRegistration:

''' <summary>
''' This class is used to register .Net installer files. This functionality is contained in its own class because it is 
''' launched in a separate appdomain in order to prevent loading the files into the host appdomain (which locks 
''' them and makes them unavailable until the host application is shutdown).
''' </summary>
''' <remarks></remarks>
Public Class FileRegistration
    Inherits MarshalByRefObject

    Public Sub New(ByVal sFileName As String)
        ' Exceptions are ignored
        Try
            Using oInstaller As New System.Configuration.Install.AssemblyInstaller(sFileName, New String() {"/logfile=autoupdate.log"})
                Dim htSavedState As New Collections.Hashtable()

                oInstaller.Install(htSavedState)
                oInstaller.Commit(htSavedState)
            End Using
        Catch theException As Exception
            ' Suppress errors to the end user
            Call ReportError(theException, True)
        End Try
    End Sub