Can I make the default AppDomain use shadow copies of certain assemblies?
Asked Answered
V

3

10

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?

Velvety answered 3/9, 2009 at 12:43 Comment(0)
F
6

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();
Flagpole answered 3/9, 2009 at 13:9 Comment(2)
the docs say they are obsolete - is this the same as deprecated? I'll give it a try. Thanks!Velvety
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 calledFlagpole
K
3

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;
            }
        }
    }
}
Kalasky answered 21/8, 2010 at 22:18 Comment(3)
Thanks. I'm pretty much using this technique here: code.google.com/p/revitpythonshell/wiki/…Velvety
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.Kalasky
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,Kalasky
U
1

There is now a Revit plugin for dynamically loading/unloading other Revit plugins so that you can change, recompile, and test without having to reopen the Revit Project. I found it on the Building Coder blog. It comes with the Revit SDK.

Uncommitted answered 7/12, 2012 at 18:51 Comment(1)
thanks, I am aware of both the blog and the AddInManager. This really works! (except, it seems, for XAML WPF forms, but that is another matter I'm not going to bother going after FTM)Velvety

© 2022 - 2024 — McMap. All rights reserved.