tags:

views:

339

answers:

2

A short explanation of why I want to do this:

I am busy writing a plugin for Autodesk Revit Architecture 2010. Testing my plugin code is extremly cumbersome, as I have to restart Autodesk for each debug session, manually load a Revit project, click on the Add-Ins tab and then start my plugin. This is just taking too long.

I have written a second plugin that hosts an IronPython interpreter. This way, I can play around with the API provided by Revit. But eventually, the code has to be rewritten in C# - and debugged.

Easy, I thought: Just load the plugins DLL from the IronPython script and exercise it. This does work, but once loaded, I cannot recompile in Visual Studio, as the DLL is now loaded in Revits AppDomain.

Easy, I thought (with a little help from StackOverflow): Just load the DLL in a new AppDomain. Alas, the RevitAPI objects can't be marshaled to another AppDomain, as they don't extend MarshalByRefObject.

I think I might be onto something with shadow copies. ASP.NET seems to be doing this. But reading the documentation on MSDN, it seems I can only specify this when creating an AppDomain.

Can I change this for the current (default) AppDomain? Can I force it to use shadow copies of DLLs from a specific directory?

+1  A: 

I don't know what you are trying to do but there are some deprecated methods to turn on ShadowCopy on the current AppDomain.

AppDomain.CurrentDomain.SetCachePath(@"C:\Cache");
AppDomain.CurrentDomain.SetShadowCopyPath(AppDomain.CurrentDomain.BaseDirectory);
AppDomain.CurrentDomain.SetShadowCopyFiles();
lubos hasko
the docs say they are obsolete - is this the same as deprecated? I'll give it a try. Thanks!
Daren Thomas
I can't get it to work...
Daren Thomas
It works for me. I've modified my answer with working example. Copy and paste it into your Main() method. Also make sure your Main() method doesn't directly reference your other assemblies because .NET will load them before `SetShadowCopyFiles()` will get called
lubos hasko
+1  A: 

Sometimes it's not possible to modify the Main() method code because, for example, you're writing a plug-in and it's instantiated by a manager.

In that case i suggest you copy the assembly and pdb (and dependent ones in AssemblyResolve event) to a temp location and load them from there with Assembly.LoadFile() (not LoadFrom()).

Pros: - no dll locking. - every time the target assembly is recompiled you get access to the new version (that's why .LoadFile()). - the whole assembly is fully available in AppDomain.CurrentDomain.

Cons: - file copying is necessary. - assemblys can't be unloaded and that could be inconvenient because resources are no freed.

Regards,

PD: This code does the work.

/// <summary>
/// Loads an assembly without locking the file
/// Note: the assemblys are loaded in current domain, so they are not unloaded by this class
/// </summary>
public class AssemblyLoader : IDisposable
{
    private string _assemblyLocation;
    private string _workingDirectory;
    private bool _resolveEventAssigned = false;

    /// <summary>
    /// Creates a copy in a new temp directory and loads the copied assembly and pdb (if existent) and the same for referenced ones. 
    /// Does not lock the given assembly nor pdb and always returns new assembly if recopiled.
    /// Note: uses Assembly.LoadFile()
    /// </summary>
    /// <param name="assemblyOriginalPath"></param>
    /// <returns></returns>
    public Assembly LoadFileCopy(string assemblyLocation)
    {
        lock (this)
        {
            _assemblyLocation = assemblyLocation;

            if (!_resolveEventAssigned)
            {
                _resolveEventAssigned = true;

                AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(AssemblyFileCopyResolveEvent);
            }

            //  Create new temp directory
            _workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(_workingDirectory);

            //  Generate copy
            string assemblyCopyPath = Path.Combine(_workingDirectory, Path.GetFileName(_assemblyLocation));
            System.IO.File.Copy(_assemblyLocation, assemblyCopyPath, true);

            //  Generate copy of referenced assembly debug info (if existent)
            string assemblyPdbPath = _assemblyLocation.Replace(".dll", ".pdb");
            if (File.Exists(assemblyPdbPath))
            {
                string assemblyPdbCopyPath = Path.Combine(_workingDirectory, Path.GetFileName(assemblyPdbPath));
                System.IO.File.Copy(assemblyPdbPath, assemblyPdbCopyPath, true);
            }

            //  Use LoadFile and not LoadFrom. LoadFile allows to load multiple copies of the same assembly
            return Assembly.LoadFile(assemblyCopyPath);
        }
    }

    /// <summary>
    /// Creates a new copy of the assembly to resolve and loads it
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    /// <returns></returns>
    private Assembly AssemblyFileCopyResolveEvent(object sender, ResolveEventArgs args)
    {
        string referencedAssemblyFileNameWithoutExtension = System.IO.Path.GetFileName(args.Name.Split(',')[0]);

        //  Generate copy of referenced assembly
        string referencedAssemblyPath = Path.Combine(Path.GetDirectoryName(_assemblyLocation), referencedAssemblyFileNameWithoutExtension + ".dll");
        string referencedAssemblyCopyPath = Path.Combine(Path.GetDirectoryName(args.RequestingAssembly.Location), referencedAssemblyFileNameWithoutExtension + ".dll");
        System.IO.File.Copy(referencedAssemblyPath, referencedAssemblyCopyPath, true);

        //  Generate copy of referenced assembly debug info (if existent)
        string referencedAssemblyPdbPath = Path.Combine(Path.GetDirectoryName(_assemblyLocation), referencedAssemblyFileNameWithoutExtension + ".pdb");
        if (File.Exists(referencedAssemblyPdbPath))
        {
            string referencedAssemblyPdbCopyPath = Path.Combine(Path.GetDirectoryName(args.RequestingAssembly.Location), referencedAssemblyFileNameWithoutExtension + ".pdb");
            System.IO.File.Copy(referencedAssemblyPath, referencedAssemblyCopyPath, true);
        }

        //  Use LoadFile and not LoadFrom. LoadFile allows to load multiple copies of the same assembly
        return Assembly.LoadFile(referencedAssemblyCopyPath);
    }


    public void Dispose()
    {
        Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_resolveEventAssigned)
            {
                AppDomain.CurrentDomain.AssemblyResolve -= new ResolveEventHandler(AssemblyFileCopyResolveEvent);

                _resolveEventAssigned = false;
            }
        }
    }
}
Eu_UY
Thanks. I'm pretty much using this technique here: http://code.google.com/p/revitpythonshell/wiki/FeaturedScriptLoadplugin
Daren Thomas
The problem with the technique you refer to is that if you recompile an assembly referenced from your main assembly, and you have already loaded it, this referenced assembly is not reloaded (updated). E.g. you load an assembly named A that depends on other assembly named B.
Eu_UY
E.g: 1) you load an assembly named A wich contains a Type A1 that uses another type B1 that is located on a different assembly named B (assembly A references assembly B). 2) without shutting down the app domain (withour closing your application), you add a new type B2 to the assembly B and reference B2 from the type A1 from assembly A. 3) you load again: here the new type B2 can't be found and you get an error because, the second time, .Net does not resolve again assembly B (it was just resolved once in step 1).Don't know if I was clear enough.Regards,
Eu_UY