Exporter des classes depuis des modules PowerShell

Ce serait pratique si les classes de modules étaient aussi faciles à consommer que les fonctions de modules, non ? On s’attend naturellement à ce que Import-Module expose tout ce dont on a besoin, mais la consommation des classes a des contraintes supplémentaires au moment de l’analyse qui compliquent la conception des modules.

Image de couverture de cet article

Cet article utilise les mêmes exemples que le dépôt ModuleExportClasses.

Mais on ne peut pas exporter directement les classes

PowerShell ne permet pas d’exporter des classes avec Export-ModuleMember ni via les clés du manifeste de module.

Les paramètres d’Export-ModuleMember n’incluent pas l’export de classes

Cela mène généralement à l’une de ces situations :

L’instruction using module fonctionne, mais peut être fragile

Si un module définit une classe :

# .\MyModule\MyModule.psm1
class MyModuleClass {
    static [string] DoStuff() {
        return "I'm busy!"
    }
}

vous pouvez la consommer avec :

# .\myScript.ps1
using module .\MyModule
[MyModuleClass]::DoStuff()

Le problème apparaît lorsqu’on chaîne des modules et dans l’usage interactif.

Exemple montrant des problèmes de résolution de classes dans un usage mixte de modules

Si les instructions sont évaluées dans le même bloc d’analyse, cela peut fonctionner :

Des instructions using module dans un même bloc peuvent résoudre l’usage des classes

Introduction aux accélérateurs de type

Les accélérateurs de type sont des alias pour des types .NET. Nous pouvons nous appuyer sur la classe interne System.Management.Automation.TypeAccelerators pour enregistrer des alias vers nos classes de module lors de l’import du module.

# Get the internal TypeAccelerators class
$typeAcceleratorsClass = [psobject].Assembly.GetType(
    'System.Management.Automation.TypeAccelerators'
)

$typeAcceleratorsClass::Get.GetEnumerator() | Select-Object -First 3

Inspection des accélérateurs de type enregistrés

Exporter des alias de classes lors de l’import du module

Le modèle pratique consiste à ajouter la logique d’enregistrement à la fin du fichier du module, après la déclaration des classes :

$typesToExportWithNamespace = @(
    'TheModuleClass'
)

$typeAcceleratorsClass = [psobject].Assembly.GetType(
    'System.Management.Automation.TypeAccelerators'
)
$moduleName = $MyInvocation.MyCommand.ScriptBlock.Module.Name
$existingTypeAccelerators = $typeAcceleratorsClass::Get

foreach ($typeToExport in $typesToExportWithNamespace) {
    $fullTypeToExport = '{0}.{1}' -f $moduleName, $typeToExport
    $type = $typeToExport -as [System.Type]
    if (-not $type) {
        throw "Type '$typeToExport' not found."
    }
    if ($fullTypeToExport -in $existingTypeAccelerators.Keys) {
        Write-Warning "Overriding type accelerator '$fullTypeToExport'."
    }

    $null = $typeAcceleratorsClass::Add($fullTypeToExport, $type)
}

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    foreach ($typeName in $typesToExportWithNamespace) {
        $null = $typeAcceleratorsClass::Remove('{0}.{1}' -f $moduleName, $typeName)
    }
}.GetNewClosure()

Après l’import du module :

Utilisation de l’accélérateur de type exporté après l’import du module

L’ordre de chargement reste important

Les accélérateurs de type ne sont enregistrés que lorsque le code d’import s’exécute. Si le module source ne peut pas être trouvé, ou s’il est importé trop tard, les appels échouent encore.

L’appel de classe échoue avant l’enregistrement de l’accélérateur de type à l’import

Après l’import du module source, le même chemin d’appel se résout :

Le même flux réussit après l’import préalable du module

Échec d’import du module dû à la résolution de PSModulePath

La mise à jour de PSModulePath, ou l’installation des modules dans les emplacements attendus, résout cela :

$psmodpath = $Env:PSModulePath -split [io.path]::PathSeparator
if ($psmodpath -notcontains $PWD.Path) { $psmodpath = @($PWD.Path) + $psmodpath }
$env:PSModulePath = $psmodpath -join [io.path]::PathSeparator

Mise à jour de PSModulePath utilisée pour localiser le module requis

Le module dépendant et l’usage de la classe réussissent ensuite :

Appel réussi une fois les dépendances et le chemin des modules corrects

Contournement pour le nommage qualifié

Comme les classes PowerShell n’offrent pas ici l’ergonomie de véritables espaces de noms, une convention de nommage pratique consiste à exporter les accélérateurs sous la forme [ModuleName.ClassName]. Cela réduit les collisions tout en gardant des sites d’appel lisibles.

Limites et compromis

Références