This is based on the Stack Overflow question: How to load an assembly as reflection-only in a new AppDomain?
I am attempting to determine the runtime version of an assembly, but that assembly could be loaded multiple times as I traverse through nested folders. Loading the assembly directly using
[Reflection.Assembly]::ReflectionOnlyLoadFrom($assembly)
will therefore not work, as the assembly can only be loaded once in the app-domain.
Given the following function to load an assembly in a separate AppDomain:
function Load-AssemblyInNewAppDomain($assembly)
{
Write-Host $assembly.FullName
$domain = [AppDomain]::CreateDomain([Guid]::NewGuid())
$domain.DoCallback
({
$loaded = [Reflection.Assembly]::Load($assembly)
$runtime = $loaded.ImageRuntimeVersion
Write-Host $runtime
})
}
This outputs the contents of the delegate to the console, rather than executing it:
OverloadDefinitions
-------------------
void DoCallBack(System.CrossAppDomainDelegate callBackDelegate)
void _AppDomain.DoCallBack(System.CrossAppDomainDelegate theDelegate)
$loaded = [Reflection.Assembly]::Load($assembly)
$runtime = $loaded.ImageRuntimeVersion
Write-Host $runtime
Note that the results are the same, whether I use PowerShell 4 or 5
Any help/guidance appreciated
First thought: don't muck around with AppDomains at all and use a completely separate process. Those are (relatively) easily launched from PowerShell, at least. The drawback is that it's potentially much slower if you're doing this for lots of files.
$myAssemblyPath = "C:\..."
$getImageRuntimeVersion = {
[Reflection.Assembly]::ReflectionOnlyLoadFrom($input).ImageRuntimeVersion
}
$encodedCommand = [Convert]::ToBase64String(
[Text.Encoding]::Unicode.GetBytes($getImageRuntimeVersion)
)
$imageRuntimeVersion = $myAssemblyPath | powershell -EncodedCommand $encodedCommand
So, is there no way at all to do this with AppDomains in PowerShell? Well, there is, but it's not pretty. You can't use AppDomain.DoCallBack because, as you've discovered, PowerShell can't remote delegates that way (because, under the covers, it produces dynamic methods).
However, it's easy to host the PowerShell runtime, and all PowerShell objects know how to serialize (a requirement for cross-domain remoting), so invoking a PowerShell script in another AppDomain is fairly simple (but still ugly):
$scriptInvokerAssembly = [System.IO.Path]::GetTempFileName() + ".dll"
Add-Type -OutputAssembly $tempAssembly -TypeDefinition #"
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Management.Automation;
public class ScriptInvoker : MarshalByRefObject {
public IEnumerable<PSObject> Invoke(ScriptBlock scriptBlock, PSObject[] parameters) {
using (var powerShell = PowerShell.Create()) {
powerShell.Commands.AddScript(scriptBlock.ToString());
if (parameters != null) {
powerShell.AddParameters(parameters);
}
return powerShell.Invoke();
}
}
}
"#
[Reflection.Assembly]::LoadFile($scriptInvokerAssembly) | Out-Null
Function Invoke-CommandInTemporaryAppDomain([ScriptBlock] $s, [object[]] $arguments) {
$setup = New-Object System.AppDomainSetup
$setup.ApplicationBase = Split-Path ([ScriptInvoker].Assembly.Location) -Parent
$domain = [AppDomain]::CreateDomain([Guid]::NewGuid(), $null, $setup)
$scriptInvoker = $domain.CreateInstanceAndUnwrap(
[ScriptInvoker].Assembly.FullName, [ScriptInvoker]
);
$scriptInvoker.Invoke($s, $arguments)
[AppDomain]::Unload($domain)
}
And now you can do
Invoke-CommandInTemporaryAppDomain {
[Reflection.Assembly]::ReflectionOnlyLoadFrom($args[0]).ImageRuntimeVersion
} $myAssemblyPath
Note that we have to generate a temporary assembly on disk and have AppDomain load it from there. This is ugly, but you can't have Add-Type produce an in-memory assembly, and even if you do end up with a byte[] getting that to load in another AppDomain is anything but trivial because you can't hook AppDomain.AssemblyResolve in PowerShell. If this command was packaged in a module, you'd compile the assembly containing the ScriptInvoker ahead of time, so I don't see working around this as a priority.
You can't run DoCallback via powershell alone. But DoCallBack does work with some inline C#. As Jeroen says it's ugly, but this works:
$assm = "C:\temp\so\bin\dynamic-assembly.dll"
Add-Type -TypeDefinition #"
using System.Reflection;
using System;
namespace Example
{
public class AppDomainUtil
{
public void LoadInAppDomain(AppDomain childDomain, string assemblyName)
{
childDomain.SetData("assemblyName", assemblyName);
childDomain.DoCallBack( new CrossAppDomainDelegate(LoadAssembly)) ;
}
public static void LoadAssembly()
{
string assemblyName = (string)AppDomain.CurrentDomain.GetData("assemblyName");
// console not available from another domain
string log = "c:\\temp\\hello.txt";
System.IO.File.WriteAllText(log, string.Format("Hello from {0}\r\n",AppDomain.CurrentDomain.FriendlyName));
System.IO.File.AppendAllText(log, string.Format("Assembly to load is {0}\r\n",assemblyName));
Assembly loaded = Assembly.Load(assemblyName);
System.IO.File.AppendAllText(log, string.Format("Assemblyloaded: {0}\r\n",loaded.FullName));
}
}
}
"# -OutputAssembly $assm -OutputType Library # must set output assembly otherwise assembly generated in-memory and it will break with Type errors.
Add-Type -Path $assm
function Load-AssemblyInNewAppDomain([string]$assembly) {
Write-Host "Parent domain: $([AppDomain]::CurrentDomain.FriendlyName)"
$util = New-Object Example.AppDomainUtil
$ads = New-Object System.AppDomainSetup
$cd = [AppDomain]::CurrentDomain
# set application base
$ads.ApplicationBase = [IO.path]::GetDirectoryName( $assm )
[System.AppDomain]$newDomain = [System.AppDomain]::CreateDomain([System.Guid]::NewGuid().ToString(), $null, $ads);
Write-Host "Created child domain: $($newDomain.FriendlyName)"
$util.LoadInAppDomain($newDomain, $assembly)
}
Testing it out:
PS C:\WINDOWS\system32> Load-AssemblyInNewAppDomain "".GetType().Assembly.FullName
Parent domain: PowerShell_ISE.exe
Created child domain: 61ab2dbb-8b33-4e7e-84db-5fabfded53aa
PS C:\WINDOWS\system32> cat C:\temp\hello.txt
Hello from 61ab2dbb-8b33-4e7e-84db-5fabfded53aa
Assembly to load is mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Assemblyloaded: mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Related
I have a PowerShell class, which I want to automatically be convertable from string.
So I defined a TypeConverter like this:
class StringToAcmeStateConverter : System.Management.Automation.PSTypeConverter {
[bool] CanConvertFrom([object] $object, [Type] $destinationType) {
if($object -is [string]) {
return Test-Path ([string]$object);
}
return $false;
}
[bool] CanConvertTo([object] $object, [Type] $destinationType) {
return $false
}
[object] ConvertFrom([object] $sourceValue, [Type] $destinationType,
[IFormatProvider] $formatProvider, [bool] $ignoreCase)
{
if($null -eq $sourceValue) { return $null; }
if(-not $this.CanConvertFrom($sourceValue, $destinationType)) {
throw [System.InvalidCastException]::new();
}
$paths = [AcmeStatePaths]::new($sourceValue);
return [AcmeDiskPersistedState]::new($paths, $false, $true);
}
[object] ConvertTo([object] $sourceValue, [Type] $destinationType,
[IFormatProvider] $formatProvider, [bool] $ignoreCase)
{
throw [System.NotImplementedException]::new();
}
}
[System.ComponentModel.TypeConverter([StringToAcmeStateConverter])]
<# abstract #> class AcmeState {
AcmeState() {
if ($this.GetType() -eq [AcmeState]) {
throw [System.InvalidOperationException]::new("This is intended to be abstract - inherit To it.");
}
}
<# omitted #>
}
(Full code listing here: https://raw.githubusercontent.com/PKISharp/ACMESharpCore-PowerShell/deep-state/ACME-PS/internal/classes/AcmeState.ps1)
But PowerShell now throws a NullRefException from inside the pipeline.
How would I make PS use the Converter correctly.
Update
Since this question did not contain enough information for a full repro, I created a gist containing the current (failing) code of the module: gist.github.com/glatzert/ba32f291b9155e6d19c29fbe9594a7c5
TypeConversion with PowerShell classes has some non-obvious issues.
My first approach with the TypeConverter-Attribute fails with either an NullRefException or UnknownTypeException (this depends on the order of the classes in your *.ps1).
I dug into the Types.ps1xml and created the following xml:
<Types>
<Type>
<Name>AcmeState</Name>
<TypeConverter>
<TypeName>StringToAcmeStateConverter</TypeName>
</TypeConverter>
</Type>
</Types>
and added TypesToProcess to the .psd1 pointing to the aforementioned ps1xml.
This will fail, stating the Converter is not known, which probably means, that PowerShell will process that file either in another context as the module or prior to loading the module.
To fix that problem, I removed the TypesToProcess from the .psd1 and added Update-TypeData Types.ps1xml as last line to my module, thus it will be run automatically during module import - and voila! This works.
TLDR:
If you want to register a TypeConverter defined in a PowerShell class, you need to use Update-TypeData
How to access a type defined by Add-Type -TypeDefinition "..." in another Add-Type -TypeDefinition "..."?
In the following code example, in spite of the same namespace, a compiler cannot find the UserCode type.
Add-Type -TypeDefinition #"
namespace SampleCode {
public struct UserCode {
public string Name;
public string Id;
}
}
"#
#.... do something ....
Add-Type -TypeDefinition #"
namespace SampleCode {
public struct UserInformation {
public UserCode User;
public string Note;
}
}
"#
# => Error ... Add-Type : <temporary source path>(3) : The type or namespace name
# 'UserCode' could not be found (are you missing a using directive or an assembly
# reference?)
In order to reference the first .NET assembly from the second .NET assembly, you need to persist the first .NET assembly to disk. Once you've done this, you can reference the first .NET assembly from the second .NET assembly using the -ReferencedAssemblies parameter of the Add-Type cmdlet.
IMPORTANT: If you use two different namespaces in your two different C# code blocks, make sure that you declare your using statements appropriately in the second code block.
Here is an example, showing you how to:
Dynamically compile a new .NET assembly to disk
Import a dynamic assembly that references another assembly on-disk
Part 1 - Assembly #1
The first .NET Assembly must be output to disk so that we can reference it. Therefore, we will use the -OutputAssembly and -OutputType parameters of the Add-Type cmdlet to create it.
$Csharp = #"
using System;
using System.Reflection;
namespace Widget {
public class UserCode {
public static int GetValue() {
return 2;
}
}
}
"#
# Define the output path for the new .NET Assembly
$OutputAssembly = '{0}\Widget.dll' -f $env:USERPROFILE;
# Compile the code and output a new .NET assembly
Add-Type -TypeDefinition $Csharp -OutputAssembly $OutputAssembly -OutputType Library;
# Load the .NET Assembly
[System.Reflection.Assembly]::LoadFile($OutputAssembly);
Part 2 - Assembly #2
We don't have to worry about compiling the second .NET assembly to disk, because we are not referencing it from any other assemblies. Therefore, we can simply use the Add-Type cmdlet to compile it into a temporary, in-memory assembly. We must make sure that we use the -ReferencedAssemblies parameter to reference the .NET Assembly that we compiled in Part 1.
# Now that we have a compiled .NET Assembly, we need to write our new code
# that references it (make sure to include all appropriate C# "using" statements)
# Define the second set of C# code
$CsharpCode2 = #"
using Widget;
namespace NASA {
public class Shuttle {
public int Boosters;
public Shuttle() {
this.Boosters = UserCode.GetValue();
}
}
}
"#;
# Try to compile the new code, but wait, we get an error ...
# ... because this code is dependent on the code contained in the
# Widget assembly
$Assembly2 = Add-Type -TypeDefinition $CsharpCode2 -PassThru;
# We have to reference the .NET Assembly that defines the UserCode type
$Assembly2 = Add-Type -TypeDefinition $CsharpCode2 -ReferencedAssemblies $OutputAssembly -PassThru;
# Now we can successfully create the NASA.Shuttle object;
$Shuttle = New-Object -TypeName NASA.Shuttle;
# View the number of boosters the Shuttle instance has
Write-Host -Object $Shuttle.Boosters;
This isn't possible with dynamic assemblies (which are generated by default if you don't specify other parameters.)
You can generate dlls with Add-Type and reference them later, so something like:
Add-Type -OutputAssembly foo1.dll ...
Add-Type -ReferencedAssemblies foo1.dll ...
should work.
I want to enumerate all the AppDomains in the current process from PowerShell. The process happens to be Visual Studio, which is hosting StudioShell. To do that I need to instantiate CorRuntimHost, which is part of mscoree.tlb, so I can adapt this C# code..
I tried to get the proper name of CorRunTimeHost and pass it to New-Object -COMObject "objectName". Based on this forum posting, I searched the registry and I think the correct name is CLRMetaData.CorRuntimeHost. However, while New-Object -ComObject 'CLRMetaData.CorRuntimeHost' -Strict does return an object, it only exposes the methods intrinsic to a COM object.
Based on this stackoverflow question I tried [Activator]::CreateInstance(). However, the following two statements give me the same problem as New-Object, namely I can't call the ICorRuntimeHost::EnumDomains() method.
$corRuntimeHost = [Activator]::CreateInstance([Type]::GetTypeFromProgID('CLRMetaData.CorRuntimeHost'));
$enumerator = $null;
$corRuntimeHost.EnumDomains([ref]$enumerator);
Method invocation failed because [System.__ComObject] doesn't contain a method named 'EnumDomains'.
At line:1 char:1
+ $corRuntimeHost.EnumDomains([ref]$enumerator)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
To get it working in PowerShell 3.0 I ended up having to use an AssemblyBuilder. Below is the working code:
The problem seems to be that there is no public constructor for mscoree.CorRuntimeHostClass in .NET 4.0 but there is in 3.5.
I later tested this on a Windows 7 VM with powershell 2.0 and now this code will work in PowerShell 2.0 and 3.0.
$tlbName = Split-Path -Parent ([AppDomain]::CurrentDomain.GetAssemblies() | Where { $_.Location -Match '\\mscorlib.dll$' }).Location
$tlbName = Join-Path $tlbName 'mscoree.tlb';
$csharpString = #"
//adapted from here http://blog.semanticsworks.com/2008/04/enumerating-appdomains.html
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
public class ListProcessAppDomains
{
[DllImport( `"oleaut32.dll`", CharSet = CharSet.Unicode, PreserveSig = false )]
private static extern void LoadTypeLibEx
(String strTypeLibName, RegKind regKind,
[MarshalAs( UnmanagedType.Interface )] out Object typeLib);
private enum RegKind
{
Default = 0,
Register = 1,
None = 2
}
private class ConversionEventHandler : ITypeLibImporterNotifySink
{
public void ReportEvent( ImporterEventKind eventKind, int eventCode, string eventMsg )
{
Console.Error.WriteLine("Kind: {0} Code: {1} Message");
}
public Assembly ResolveRef( object typeLib )
{
string stackTrace = System.Environment.StackTrace;
Console.WriteLine("ResolveRef ({0})", typeLib);
Console.WriteLine(stackTrace);
return null;
}
}
public static AssemblyBuilder LoadMsCoreeDll( ref Object typeLib ) {
ConversionEventHandler eventHandler = new ConversionEventHandler();
string assemblyName = "PoshComWrapper.dll";
LoadTypeLibEx( #"$($tlbName)", RegKind.None, out typeLib );
TypeLibConverter typeLibConverter = new TypeLibConverter();
return typeLibConverter.ConvertTypeLibToAssembly( typeLib, assemblyName, 0, eventHandler, null, null, null, null );
}
}
"#
# So we can run this scipt multiple times
try { [ListProcessAppDomains] } catch { Add-Type -TypeDefinition $csharpString }
function Get-AppDomain {
$typeLib = $null;
$assemblyBuilder = [ListProcessAppDomains]::LoadMsCoreeDll([ref] $typeLib)
$corRuntimeHostClass = $assemblyBuilder.CreateInstance('PoshComWrapper.CorRuntimeHostClass')
$enumHandle = [IntPtr]::Zero
$corRuntimeHostClass.EnumDomains([ref] $enumHandle);
$appDomain = $null;
do
{
$corRuntimeHostClass.NextDomain($enumHandle, [ref] $appDomain);
if ($appDomain -ne $null -and $appDomain.GetType() -eq [AppDomain]) { $appDomain; }
} while ($appDomain -ne $null)
}
Get-AppDomain
I'm interested in getting the contents of a shelveset at the command prompt. Now, you would think that a cmdlet such as Get-TfsShelveset, available in the TFS Power Tools, would do this. You might also think that "tf.exe shelvesets" would do this.
However, unless I've missed something, I'm appalled to report that neither of these is the case. Instead, each command requires you to give it a shelveset name, and then simply regurgitates a single line item for that shelveset, along with some metadata about the shelveset such as creationdate, displayname, etc. But as far as I can tell, no way to tell what's actually in the shelf.
This is especially heinous for Get-TfsShelveset, which has the ability to include an array of file descriptors along with the Shelveset object it returns. I even tried to get clever, thinking that I could harvest the file names from using -WhatIf with Restore-TfsShelveset, but sadly Restore-TfsShelveset doesn't implement -WhatIf.
Please, someone tell me I'm wrong about this!
tf status /shelveset:name
will list out the content of the named shelveset (you can also supplier an owner: see tf help status).
With the TFS PowerToy's PowerShell snapin:
Get-TfsPendingChange -Shelveset name
for the same information.
It is possible to construct a small command-line application that uses the TFS SDK, which returns the list of files contained in a given shelveset.
The sample below assumes knowledge of the Shelveset name & it's owner:
using System;
using System.IO;
using System.Collections.ObjectModel;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Common;
using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
namespace ShelvesetDetails
{
class Program
{
static void Main(string[] args)
{
Uri tfsUri = (args.Length < 1) ? new Uri("TFS_URI") : new Uri(args[0]);
TfsConfigurationServer configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(tfsUri);
ReadOnlyCollection<CatalogNode> collectionNodes = configurationServer.CatalogNode.QueryChildren(
new[] { CatalogResourceTypes.ProjectCollection },
false, CatalogQueryOptions.None);
CatalogNode collectionNode = collectionNodes[0];
Guid collectionId = new Guid(collectionNode.Resource.Properties["InstanceId"]);
TfsTeamProjectCollection teamProjectCollection = configurationServer.GetTeamProjectCollection(collectionId);
var vcServer = teamProjectCollection.GetService<VersionControlServer>();
Shelveset[] shelves = vcServer.QueryShelvesets(
"SHELVESET_NAME", "SHELVESET_OWNER");
Shelveset shelveset = shelves[0];
PendingSet[] sets = vcServer.QueryShelvedChanges(shelveset);
foreach (PendingSet set in sets)
{
PendingChange[] changes = set.PendingChanges;
foreach (PendingChange change in changes)
{
Console.WriteLine(change.FileName);
}
}
}
}
}
Invoking this console app & catching the outcome during execution of the powershell should be possible.
Try:
tfpt review
/shelveset:shelvesetName;userName
You may also need to add on the server option so something like:
tfpt review /shelveset:Code Review;jim
/sever:company-source
I think this is what you are looking for.
This is what I ended up with, based on pentelif's code and the technique in the article at http://akutz.wordpress.com/2010/11/03/get-msi/ linked in my comment.
function Get-TfsShelvesetItems
{
[CmdletBinding()]
param
(
[string] $ShelvesetName = $(throw "-ShelvesetName must be specified."),
[string] $ShelvesetOwner = "$env:USERDOMAIN\$env:USERNAME",
[string] $ServerUri = $(throw "-ServerUri must be specified."),
[string] $Collection = $(throw "-Collection must be specified.")
)
$getShelvesetItemsClassDefinition = #'
public IEnumerable<PendingChange> GetShelvesetItems(string shelvesetName, string shelvesetOwner, string tfsUriString, string tfsCollectionName)
{
Uri tfsUri = new Uri(tfsUriString);
TfsConfigurationServer configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(tfsUri);
ReadOnlyCollection<CatalogNode> collectionNodes = configurationServer.CatalogNode.QueryChildren( new[] { CatalogResourceTypes.ProjectCollection }, false, CatalogQueryOptions.None);
CatalogNode collectionNode = collectionNodes.Where(node => node.Resource.DisplayName == tfsCollectionName).SingleOrDefault();
Guid collectionId = new Guid(collectionNode.Resource.Properties["InstanceId"]);
TfsTeamProjectCollection teamProjectCollection = configurationServer.GetTeamProjectCollection(collectionId);
var vcServer = teamProjectCollection.GetService<VersionControlServer>();
var changes = new List<PendingChange>();
foreach (Shelveset shelveset in vcServer.QueryShelvesets(shelvesetName, shelvesetOwner))
{
foreach (PendingSet set in vcServer.QueryShelvedChanges(shelveset))
{
foreach ( PendingChange change in set.PendingChanges )
{
changes.Add(change);
}
}
}
return changes.Count == 0 ? null : changes;
}
'#;
$getShelvesetItemsType = Add-Type `
-MemberDefinition $getShelvesetItemsClassDefinition `
-Name "ShelvesetItemsAPI" `
-Namespace "PowerShellTfs" `
-Language CSharpVersion3 `
-UsingNamespace System.IO, `
System.Linq, `
System.Collections.ObjectModel, `
System.Collections.Generic, `
Microsoft.TeamFoundation.Client, `
Microsoft.TeamFoundation.Framework.Client, `
Microsoft.TeamFoundation.Framework.Common, `
Microsoft.TeamFoundation.VersionControl.Client `
-ReferencedAssemblies "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Client.dll", `
"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Common.dll", `
"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.VersionControl.Client.dll" `
-PassThru;
# Initialize an instance of the class.
$getShelvesetItems = New-Object -TypeName "PowerShellTfs.ShelvesetItemsAPI";
# Emit the pending changes to the pipeline.
$getShelvesetItems.GetShelvesetItems($ShelvesetName, $ShelvesetOwner, $ServerUri, $Collection);
}
Spent a few days trying to do this as well, this always popped up on google so here is what I found to help future generations:
To get the contents of the shelveset (at least with Team Explorer Everywhere),
use the command: tf difference /shelveset:<Shelveset name>
That will print out the contents of the shelveset and give filenames in the form :
<Changetype>: <server file path>; C<base change number>
Shelved Change: <server file path again>;<shelveset name>
So if your file is contents/test.txt
in the shelveset shelve1 (with base revision 1), you will see :
edit: $/contents/file.txt;C1
Shelved Change: $/contents/file.txt;shelve1
After that, using the tf print command
(or view if not using TEE) on $/contents/file.txt;shelve1 should get you the contents :
tf print $/contents/file.txt;shelve1
Shows you what is in the file.txt in shelveset shelve1
If you want get shelveset changes from server by using tfs command
Using power shell:
Get-TfsPendingChange -Server http://example.com/org -Shelveset shelvsetName
Using vs commands:
c:\projects>tf shelvesets BuddyTest_23
more info about this please see here
https://learn.microsoft.com/en-us/azure/devops/repos/tfvc/shelvesets-command?view=azure-devops
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?
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()
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.