How to Properly Load and Use .NET Assemblies in PowerShell: Solving Reflection and Type Loading Issues


2 views

When working with .NET assemblies in PowerShell, you essentially have three main approaches to load them:

# Method 1: Add-Type (traditional approach)
Add-Type -Path "C:\rnd\CloudBerry.Backup.API.dll"

# Method 2: Reflection (more flexible)
[System.Reflection.Assembly]::LoadFrom("C:\rnd\CloudBerry.Backup.API.dll")

# Method 3: Alternative reflection method
[System.Reflection.Assembly]::LoadFile("C:\rnd\CloudBerry.Backup.API.dll")

The error message you're seeing typically indicates one of these common scenarios:

try {
    Add-Type -Path $dllpath -ErrorAction Stop
}
catch [System.Reflection.ReflectionTypeLoadException] {
    $_.Exception.LoaderExceptions | ForEach-Object {
        Write-Warning $_.Message
    }
}

Once you've successfully loaded the assembly via reflection, here's how to properly access its types:

$asm = [System.Reflection.Assembly]::LoadFrom($dllpath)

# Method 1: Using the full type name with namespace
$type = $asm.GetType("CloudBerryLab.Backup.API.BackupProvider")
$type::GetAccounts()

# Method 2: Creating an instance first if needed
$instance = [Activator]::CreateInstance($type)
$instance.MethodName()

# Method 3: Using dynamic approach (PowerShell 5+)
$accounts = $asm.GetType("CloudBerryLab.Backup.API.BackupProvider")::GetAccounts()

If you're still having issues, consider these troubleshooting steps:

# 1. Check assembly dependencies
$asm.GetReferencedAssemblies() | ForEach-Object {
    try {
        [System.Reflection.Assembly]::Load($_)
    }
    catch {
        Write-Warning "Failed to load dependency: $($_.FullName)"
    }
}

# 2. Verify the assembly is actually loaded
[AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object Location -like "*CloudBerry*" |
    Select-Object FullName, Location, GlobalAssemblyCache

For complex scenarios where types still aren't accessible:

# Using PowerShell's type acceleration
$accelerators = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
$accelerators::Add('CBBProvider', "CloudBerryLab.Backup.API.BackupProvider")

# Now you can use the shortcut
[CBBProvider]::GetAccounts()

# Alternative using Set-Type
$asm.GetExportedTypes() | ForEach-Object {
    Set-Type -TypeDefinition "using namespace $($_.Namespace);"
}

When working with .NET assemblies in PowerShell, the most common pain point manifests through the infamous ReflectionTypeLoadException. The error message you're seeing typically indicates one of these scenarios:


# Common failure patterns
Add-Type -Path "C:\rnd\CloudBerry.Backup.API.dll"  # Fails with ReflectionTypeLoadException
[CloudBerryLab.Backup.API.BackupProvider]::GetAccounts()  # TypeNotFound error

Traditional Add-Type might fail where reflection succeeds because of these key differences:


# Method 1: Add-Type (Strict loading)
Add-Type -Path $dllpath  # Requires all dependencies present in same directory

# Method 2: Assembly.LoadFrom (More tolerant)
[Reflection.Assembly]::LoadFrom($dllpath)  # Searches dependencies in loading context

# Method 3: Assembly.LoadFile (Isolated context)
[Reflection.Assembly]::LoadFile($dllpath)  # Treats assembly as independent unit

For CloudBerry's specific case, here's a proven approach:


# Step 1: Create proper loading context
$dllpath = "C:\rnd\CloudBerry.Backup.API.dll"
$resolvedPath = Resolve-Path $dllpath

# Step 2: Load with error handling
try {
    $asm = [Reflection.Assembly]::LoadFrom($resolvedPath)
    
    # Verify loaded types
    $asm.GetExportedTypes() | ForEach-Object {
        Write-Host "Available type: $($_.FullName)"
    }

    # Step 3: Access types through reflection first
    $backupProviderType = $asm.GetType("CloudBerryLab.Backup.API.BackupProvider")
    
    # Step 4: Invoke static methods
    $accounts = $backupProviderType::GetType().GetMethod("GetAccounts").Invoke($null, $null)
    $accounts | Format-Table -AutoSize

} catch [System.Reflection.ReflectionTypeLoadException] {
    Write-Warning "Loader exceptions encountered:"
    $_.LoaderExceptions | ForEach-Object {
        Write-Host " - $($_.Message)"
    }
}

When LoaderExceptions occur, examine missing dependencies:


# Analysis command for any .NET assembly
[Reflection.Assembly]::LoadFrom($dllpath).GetReferencedAssemblies() |
    ForEach-Object {
        try {
            [Reflection.Assembly]::Load($_) | Out-Null
            Write-Host "$($_.Name) - LOADED"
        } catch {
            Write-Warning "$($_.Name) - MISSING"
        }
    }

For production environments, consider registration in GAC:


# Requires elevated session
[System.EnterpriseServices.Internal.Publish]::GacInstall($dllpath)

# Then reference via full strong name
Add-Type -AssemblyName "CloudBerry.Backup.API, Version=1.0.0.1, Culture=neutral, PublicKeyToken=..."

Here's a complete script pattern that handles most edge cases:


function Import-DotNetAssembly {
    param(
        [string]$Path,
        [switch]$Global
    )
    
    $resolvedPath = Resolve-Path $Path -ErrorAction Stop
    
    # Temporary event handler for assembly resolve
    $onAssemblyResolve = [System.ResolveEventHandler] {
        param($sender, $e)
        
        $assemblyPath = Join-Path (Split-Path $resolvedPath) "$($e.Name.Split(',')[0]).dll"
        if (Test-Path $assemblyPath) {
            return [Reflection.Assembly]::LoadFrom($assemblyPath)
        }
        return $null
    }
    
    try {
        # Add resolver
        [System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolve)
        
        # Load main assembly
        $assembly = [Reflection.Assembly]::LoadFrom($resolvedPath)
        
        if ($Global) {
            # Make types globally available
            $assembly.GetExportedTypes() | ForEach-Object {
                [pscustomobject]@{
                    TypeName = $_.FullName
                    Assembly = $assembly
                } | Add-Member -MemberType ScriptMethod -Name "GetInstance" -Value {
                    param($Arguments = @())
                    [Activator]::CreateInstance($this.TypeName, $Arguments)
                } -PassThru
            } | ForEach-Object {
                $script:ExecutionContext.SessionState.PSVariable.Set($_.TypeName.Split('.')[-1], $_)
            }
        }
        
        return $assembly
    } finally {
        [System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolve)
    }
}

# Usage:
$cloudBerry = Import-DotNetAssembly -Path "C:\rnd\CloudBerry.Backup.API.dll" -Global
$BackupProvider.GetAccounts()  # Now works globally