views:

186

answers:

2

I'm working on a PoSh project that generates CSharp code, and then Add-Types it into memory.

The new types use existing types in an on disk DLL, which is loaded via Add-Type.

All is well and good untill I actualy try to invoke methods on the new types. Here's an example of what I'm doing:

$PWD = "."
rm -Force $PWD\TestClassOne*
$code = "
namespace TEST{
public class TestClassOne
{
    public int DoNothing()
    {
        return 1;
    }
}
}"
$code | Out-File tcone.cs
Add-Type -OutputAssembly $PWD\TestClassOne.dll -OutputType Library -Path $PWD\tcone.cs
Add-Type -Path $PWD\TestClassOne.dll
$a = New-Object TEST.TestClassOne
"Using TestClassOne"
$a.DoNothing()


"Compiling TestClassTwo"
Add-Type -Language CSharpVersion3 -TypeDefinition "
namespace TEST{
public class TestClassTwo
{
    public int CallTestClassOne()
    {
        var a = new TEST.TestClassOne();
        return a.DoNothing();
    }
}
}" -ReferencedAssemblies $PWD\TestClassOne.dll
"OK"
$b = New-Object TEST.TestClassTwo
"Using TestClassTwo"
$b.CallTestClassOne()

Running the above script gives the following error on the last line:

Exception calling "CallTestClassOne" with "0" argument(s): "Could not load file or assembly 'TestClassOne,...' or one of its dependencies. The system cannot find the file specified." At AddTypeTest.ps1:39 char:20 + $b.CallTestClassOne <<<< () + CategoryInfo : NotSpecified: (:) [], MethodInvocationException + FullyQualifiedErrorId : DotNetMethodException

What am I doing wrong?

+2  A: 

When you output the TestClassTwo to a dll (in the same directory as TestClassOne) and Add-Type it, it works. Or at least at my machine ;) So that's the ugly workaround.

When calling $b.CallTestClassOne() PowerShell tries (from some reason I don't know) to find assembly TestClassOne.dll at these locations:

LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne.DLL
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne/TestClassOne.DLL
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne.EXE
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne/TestClassOne.EXE

This is output from fuslogvw tool. It might be useful for you. The same list of paths can bee seen live using ProcessMonitor.

You might also try this (before calling CallTestClassOne()

[appdomain]::CurrentDomain.add_assemblyResolve({
    $global:x = $args
})
$b.CallTestClassOne()
$x | fl

This will show you what assembly failed and some more info.

I agree that it should work as you expect. So that's why this looks somewhat buggy.

stej
Thanks for that. I'm thinking that Add-Type didn't get the API usability testing it needs.
Scott Weinstein
It looks in those locations because that's the application's (PowerShell's) base directory - [appdomain]::CurrentDomain.BaseDirectory. The CLR loader is going to look there or under certain subdirs of that location unless you add a private probing path to the PowerShell.exe's config file (not recommended).
Keith Hill
@Keith, you are right. However, the question is why PowerShell tries to load the assembly even though the assembly should have been loaded already. That's strange :|
stej
I believe this has to do with which load context is used during the load of each assembly. It appears from Reflector that the first load (Add-Type -Path) uses the LoadFrom context. I suspect the second assembly load uses the Load context. Check out the table on this site. There is one disadvantage listed for the Load context: http://blogs.msdn.com/suzcook/archive/2003/05/29/57143.aspx
Keith Hill
+1  A: 

This happens because any assemblies are looked for by the CLR loader in the application's (PowerShell's) base directory. Of course, it doesn't find your assembly there. The best way to solve this is to hook the AssemblyResolve event as stej mentions but use it to tell the CLR where the assembly is. You can't do this with PowerShell 2.0's Register-ObjectEvent because it doesn't work with events that require a return value (ie the assembly). In this case, let's use more C# via Add-Type to do this work for us. This snippet of code works:

ri .\TestClassOne.dll -for -ea 0

$resolver = @'
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Utils
{
    public static class AssemblyResolver
    {
        private static Dictionary<string, string> _assemblies;

        static AssemblyResolver()
        {
            var comparer = StringComparer.CurrentCultureIgnoreCase;
            _assemblies = new Dictionary<string,string>(comparer);
            AppDomain.CurrentDomain.AssemblyResolve += ResolveHandler;
        }

        public static void AddAssemblyLocation(string path)
        {
            // This should be made threadsafe for production use
            string name = Path.GetFileNameWithoutExtension(path);
            _assemblies.Add(name, path);
        }

        private static Assembly ResolveHandler(object sender, 
                                               ResolveEventArgs args) 
        {
            var assemblyName = new AssemblyName(args.Name);
            if (_assemblies.ContainsKey(assemblyName.Name))
            {
                return Assembly.LoadFrom(_assemblies[assemblyName.Name]);
            }
            return null;
        }
    }
}
'@

Add-Type -TypeDefinition $resolver -Language CSharpVersion3

$code = @'
namespace TEST {
    public class TestClassOne {
        public int DoNothing() {
            return 1;
        }
    }
}
'@
$code | Out-File tcone.cs
Add-Type -OutputAssembly TestClassOne.dll -OutputType Library -Path tcone.cs

# This is the key, register this assembly's location with our resolver utility
[Utils.AssemblyResolver]::AddAssemblyLocation("$pwd\TestClassOne.dll")

Add-Type -Language CSharpVersion3 `
         -ReferencedAssemblies "$pwd\TestClassOne.dll" `
         -TypeDefinition @'
namespace TEST {
    public class TestClassTwo {
        public int CallTestClassOne() {
            var a = new TEST.TestClassOne();
            return a.DoNothing();
        }
    }
}
'@ 

$b = new-object Test.TestClassTwo
$b.CallTestClassOne()
Keith Hill
Good one! ;) However, the Posh behaviour is tricky, because one would expect that the first assembly is already loaded.
stej