3
votes

I have a custom MSBuild task that peeks inside an assembly to get some attribute meta-data.

Assembly assembly = Assembly.ReflectionOnlyLoadFrom(AssemblyFile)

This is used by our automated build/release process, and has been working perfectly against assemblies used by and referenced from class libraries, console apps and web projects. The MSBuild task is called after another MSBuild process has compiled the projects.

It stopped working yesterday when I added a WPF project which referenced this particular assembly - a .NET 3.5 class library.

System.IO.FileLoadException: API restriction: The assembly 'file:///bogus.dll' has already loaded from a different location. 
It cannot be loaded from a new location within the same appdomain. 
at System.Reflection.Assembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection) 
at System.Reflection.Assembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection) 
at System.Reflection.Assembly.InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection) 
at System.Reflection.Assembly.InternalLoadFrom(String assemblyFile, Evidence securityEvidence, Byte[] hashValue, AssemblyHashAlgorithm hashAlgorithm, Boolean forIntrospection, StackCrawlMark& stackMark) 
at System.Reflection.Assembly.ReflectionOnlyLoadFrom(String assemblyFile) 
at RadicaLogic.MSBuild.Tasks.GetAssemblyAttribute.Execute() 
at Microsoft.Build.BuildEngine.TaskEngine.ExecuteInstantiatedTask(EngineProxy engineProxy, ItemBucket bucket, TaskExecutionMode howToExecuteTask, ITask task, Boolean& taskResult)

I know it's WPF related because no exception is thrown if I change the AssemblyFile to point to another assembly in the same solution which isn't referenced by the WPF project.

The exception message mentions that

... already loaded from a different location.

It cannot be loaded from a new location within the same appdomain.

Note the part about the same appdomain.

So I modified the code to catch this particular exception and look in CurrentDomain:

Assembly assembly = null;
try
{
    assembly = Assembly.ReflectionOnlyLoadFrom(AssemblyFile);
}
catch (FileLoadException)
{
    List<string> searched = new List<string>();
    foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
    {
        if (Path.GetFileName(asm.CodeBase).Equals(Path.GetFileName(AssemblyFile), 
            StringComparison.OrdinalIgnoreCase))
        {
            message = string.Format("Found assembly {0} in current domain", 
                asm.CodeBase);
            MSBuildHelper.Log(this, message, MessageImportance.High);
            assembly = asm;
            break;
        }
        else
        {
            searched.Add(Path.GetFileName(asm.CodeBase));
        }
    }
    if (assembly == null)
    {
        message = string.Format(
            "Unable to find {0} after looking in current domain assemblies {1}",
            Path.GetFileName(AssemblyFile), string.Join(", ", searched.ToArray()));
        MSBuildHelper.Log(this, message, MessageImportance.High);                    
    }
}

It goes without saying that the assembly in question wasn't in the current domain (which may make sense, since another MSBuild process is spawned which does the compilation), so assuming the error message is true, how do I go about figuring out where it lives? It's confusing because the error message to me suggests that it should be CurrentDomain.

Or can someone with more WPF experience explain why this assembly is still floating around in an app domain after a successful build?

Here's another question from someone else who has hit this exception.

1
Just as an aside, you should probably do this lookup in a separate AppDomain. That will make it easier to avoid these kinds of problems.Jamie Penney
@Jamie - The only time I've hit this is with WPF referenced project, and this is being triggered after another (separate) MSBuild process has finished compilation. So will a new AppDomain help? Also consider that the assembly wasn't found in the CurrentDomain.si618
@Si I just meant in general. Dynamically loading assemblies tends to blow up when you don't have control of what is loaded into the current AppDomain - future developers might include a new assembly which loads an incompatible version of a shared assembly, or an expected assembly might be removed. None of this is checked at compile time, so you really want a fresh AppDomain where you can explicitly set which assemblies should be loaded. Plus, when you are done checking through the assemblies, you can kill the AppDomain and free up some memory. But that only applies to long running processes.Jamie Penney
Ok, thanks Jamie, will give that a go if I can find some spare time :)si618
hey, I just faced the same problem! For some reason ReflectionOnlyLoadFrom doesn't add assemblies to whatever it is CurrentDomain.GetAssemblies() queries, but they're still being added to the domain. I guess you tried to load some assembly that exists more than once and when you tried to load it the second time, the above exception occured. Solution would be to create one appdomain/ReflectionOnlyLoadFrom, get the info you need and make the appdomain disappear again.Steffen Winkler

1 Answers

2
votes

My solution was to go open source :) Using Cecil to get Attribute from AssemblyFile:

bool found = false;
string value = string.Empty;
Type attributeType = Type.GetType(Attribute);
AssemblyDefinition assembly = AssemblyFactory.GetAssembly(AssemblyFile);
foreach (CustomAttribute attribute in assembly.CustomAttributes)
{
    if (attribute.Constructor.DeclaringType.Name == attributeType.Name)
    {
        value = attribute.ConstructorParameters[0].ToString();
        found = true;
    }
}

Update for Jays comment:

Typically I use: AssemblyFileVersion as the Attribute property value, and have the setter logic fill in the missing pieces :)

Here's how the property is defined:

string attribute;
[Required]
public string Attribute
{
    get { return attribute; }
    set
    {
        string tempValue = value;
        if (!tempValue.StartsWith("System.Reflection."))
        {
            tempValue = "System.Reflection." + tempValue;
        }
        if (!value.EndsWith("Attribute"))
        {
            tempValue += "Attribute";
        }
        attribute = tempValue;
    }
}

Unit test showing Attribute property value sans required prefix or suffix:

[Test]
public void Execute_WithoutSystemReflectionPrefixOrAttributeSuffix_ReturnsExpectedResult()
{
    string version = getAssemblyFileVersion();
    Assert.IsNotNull(version, "Expected AssemblyFileVersionAttribute to contain value");

    task.AssemblyFile = assemblyFile;
    task.Attribute = "AssemblyFileVersion";
    task.Value = "Bogus";

    result = task.Execute();

    Assert.IsTrue(result, "Expected execute to pass on valid assembly and attribute name");

    Assert.AreEqual(task.Value, version,
        "Expected task value to match assembly file version attribute value");
}