Module PowerShell exportant ses classes
Ne serait-il pas formidable de pouvoir exporter des classes à partir d’un module PowerShell ? Vous savez, lorsque vous importez un module, ses fonctions publiques sont disponibles dans votre session. Ne pourrait-on pas faire de même pour les classes ?
N’hésitez pas à suivre ce post avec le code de l’exemples ici : ModuleExportClasses. Ce contenu provient de la partie avancée du cours PowerShell Fundamentals, et nous l’utilisons dans Maitriser PowerShell Universal.
Mais, c’est pas possible !?
Si vous avez suivi l’évolution de PowerShell, vous savez probablement qu’il n’y a aucun moyen d’exporter une classe à partir d’un module. Rien dans du manifeste du module, ni dans la fonction Export-ModuleMember non plus…

L’instruction using
Un script PowerShell doit commencer par l’instruction using module pour utiliser les types définis dans un module. Spécifiez le nom du module ou son chemin d’accès pour identifier l’endroit où les types sont définis.
Soit un module qui définit la classe suivante et sa méthode statique DoStuff() :
# .\MyModule\MyModule.psm1
class MyModuleClass
{
static [string] DoStuff()
{
return 'I''m busy!'
}
}Pour utiliser cette méthode statique dans un script ou un autre module, nous devons alors écrire l’instruction using comme ci-dessous :
# ./myScript.ps1
using module .\MyModule
# do stuff
[MyModuleClass]::DoStuff()C’est un peu lourd et parfois pas tout à fait intuitif.
Essayez de faire les déclarations using dans le shell pour les modules ./MyModule et ./OtherModule…

Mais si vous exécutez les instructions « using » dans le même bloc, cela fonctionne :

D’accord, cela fait l’affaire. Cependant, ce serait beaucoup mieux si nous pouvions simplement exporter les classes que nous avons sélectionnées dans notre module PowerShell. De cette façon, nos utilisateurs peuvent les utiliser directement lorsqu’ils importent le module !
Présentation des accélérateurs de type
Les TypeAccelerator sont des alias pour les types .NET, généralement utilisés pour raccourcir le nom d’un long type, sans utiliser l’instruction using namespace…
Vous pouvez trouver les accélérateurs de type enregistrés en utilisant la méthode statique de la classe TypeAccelerators :
# Get the internal TypeAccelerators class to use its static methods.
$typeAcceleratorsClass = [psobject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)
$typeAcceleratorsClass::Get
# ...
$typeAcceleratorsClass::Get.GetEnumerator() | Select -First 3
Key Value
--- -----
Alias System.Management.Automation.AliasAttribute
AllowEmptyCollection System.Management.Automation.AllowEmptyCollectionAttribute
AllowEmptyString System.Management.Automation.AllowEmptyStringAttributeSi vous sélectionnez les 3 premiers, vous devriez voir que [Alias] est en fait un accélérateur de type pour [System.Management.Automation.AliasAttribute].
Et si nous enregistrions des accélérateurs de type pour nos classes de modules lors de l’import ?
Exporter des accélérateurs de type pour les classes de modules
Nous ajoutons maintenant l’extrait suivant au bas du PSM1. En effet, nous devons l’ajouter après que les classes ont été déclarées.
# At the bottom of ./TheModule/TheModule.psm1
# inspired from https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.5
$typesToExport = @(
'TheModuleClass'
)
# Get the internal TypeAccelerators class to use its static methods.
$typeAcceleratorsClass = [psobject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)
$existingTypeAccelerators = $typeAcceleratorsClass::Get
foreach ($typeToExport in $typesToExport)
{
# validate the type exists
$type = $TypeToExport -as [System.Type]
if (-not $type)
{
Write-Warning -Message (
'Unable to export {0}. Type not found.' -f $typeToExport
)
}
else
{
$null = $TypeAcceleratorsClass::Add($TypeToExport, $Type)
}
}Et nous testons dans le Shell:

Sans utiliser l’instruction using module ./TheModule, nous pouvons maintenant utiliser la classe du module une fois que celui-ci est importé. Nous pouvons même choisir les classes qui seront disponibles lors de l’importation du module.
Vérifions maintenant dans la classe TypeAccelerators ce qui est disponible :
$typeAcceleratorsClass = [psobject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)
$existingTypeAccelerators = $typeAcceleratorsClass::Get
$existingTypeAccelerators.GetEnumerator() | Select -last 1Nous disposons également du dernier accélérateur de type ajouté :

Utilisation de types exportés dans d’autres modules
Maintenant, si je commence à partir d’un shell propre, et que j’importe le module nommé UsingTheModule qui a une fonction comme celle-ci :
function Invoke-TheModuleClassMethod
{
[CmdletBinding()]
param
(
# No param required
)
[TheModuleClass]::DoStuff()
}L’importation du module fonctionnera parfaitement. En effet, l’utilisation de [TheModuleClass] se fait dans la définition du script (scriptblock). Cependant, l’invocation de la commande échouera avec InvalidOperation : Impossible de trouver le type [TheModuleClass]…

Les types sont exportés (l’accélérateur de type correspondant est enregistré) uniquement lorsque le module est importé. En effet, c’est à ce moment-là que le code de TheModule.psm1 est exécuté.
Importer le module et exécuter à nouveau le même code :

Pour utiliser le type (accélérateur) avec notre module sans étape manuelle supplémentaire, il faut d’abord importer le module. Sinon, l’analyseur PowerShell se plaindra que le type n’existe pas.
Dans un module, ceci est réalisé avec le paramètre RequiredModules dans le manifeste du module. Dans le manifeste du module UsingTheModule.psd1, nous pouvons ajouter TheModule à la liste des RequiredModules.
Repartons d’un terminal neuf :

Encore une erreur !
Mais il sait qu’il doit charger le module requis TheModule, mais il ne le trouve pas ! C’est un problème de $Env:PSModulePath, ajoutons le répertoire courant.
$psmodpath = $Env:PSModulePath -split [io.path]::PathSeparator
if ($psmodpath -notcontains $PWD.Path) { $psmodpath = @($PWD.Path) + $psmodpath }
$env:PSModulePath = $psmodpath -join [io.path]::PathSeparator
$Env:PSModulePathCe qui se passe comme suit :

Et maintenant, nous pouvons réessayer !

Absence d’espace de noms et solution de contournement
L’un des inconvénients de l’utilisation de classes dans les modules PowerShell est qu’elles ne prennent pas en charge les namespaces. Notre classe [TheModuleClass] n’a pas de nom pleinement qualifié. Cette absence inclurait le nom du module dans lequel elle est définie. Par conséquent, la classe est difficile à découvrir et il existe un risque de conflit de nom de classe.
Rappelez-vous quand hyper-v et vmware ont tous deux proposé le nom de la cmdlet New-VM ?
Il y a des discussions à ce sujet depuis de nombreuses années maintenant. J’ai peu d’espoir de le voir mis en œuvre de sitôt. Il convient de mentionner que les types PowerShell sont des types .NET définis au moment de l’exécution et qu’ils posent des problèmes…
De plus, il s’agirait d’un changement qui ne verrai pas le jour dans PowerShell 5.1, et qui aurait donc un impact limité.
Mais avec les Type Accelerators, nous n’avons pas à nous limiter à leur nom exact, ce sont des alias de toute façon…
Rien ne nous empêche d’exporter l’alias [TheModule.TheModuleClass], et c’est ce que j’utilise souvent dorénavant. Le suffixe ci-dessous est ce que j’ajoute habituellement à mes projets, en utilisant Sampler (qui mériterait quelques articles aussi !).
#region TypeAccelerator export for classes
# If classes/types are specified here, they won't be fully qualified (careful with conflicts)
$typesToExportAsIs = @(
# 'APIEndpoint'
)
# The type accelerators created will be ModuleName.ClassName (to avoid conflicts with other modules until you use 'using moduleName'
$typesToExportWithNamespace = @(
'TheModuleClass'
)
# inspired from https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.5
# Always clobber an existing type accelerator, but
# warn if a type accelerator with the same name exists.
function Get-CurrentModule
{
<#
.SYNOPSIS
This is a Private function to always be able to retrieve the module info even outside
of a function (i.e. PSM1 during module loading)
.DESCRIPTION
This function is only meant to be used from the psm1, hence not exported.
.EXAMPLE
$m = Get-CurrentModule
#>
[OutputType([System.Management.Automation.PSModuleInfo])]
param
()
# Get the current module
$MyInvocation.MyCommand.ScriptBlock.Module
}
# Get the internal TypeAccelerators class to use its static methods.
$typeAcceleratorsClass = [psobject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)
$moduleName = (Get-CurrentModule).Name
$existingTypeAccelerators = $typeAcceleratorsClass::Get
foreach ($typeToExport in @($typesToExportWithNamespace + $typesToExportAsIs))
{
if ($typeToExport -in $typesToExportAsIs)
{
$fullTypeToExport = $TypeToExport
}
else
{
$fullTypeToExport = '{0}.{1}' -f $moduleName,$TypeToExport
}
$type = $TypeToExport -as [System.Type]
if (-not $type)
{
$Message = @(
"Unable to register type accelerator '$fullTypeToExport' for '$typeToExport'"
"Type '$typeToExport' not found."
) -join ' - '
throw [System.Management.Automation.ErrorRecord]::new(
[System.InvalidOperationException]::new($Message),
'TypeAcceleratorTypeNotFound',
[System.Management.Automation.ErrorCategory]::InvalidOperation,
$fullTypeToExport
)
}
else
{
if ($fullTypeToExport -in $existingTypeAccelerators.Keys)
{
$Message = @(
"Overriding type accelerator '$($fullTypeToExport)' with '$($Type.FullName)'"
'Accelerator already exists.'
) -join ' - '
Write-Warning -Message $Message
}
else
{
Write-Verbose -Message "Added type accelerator '$($fullTypeToExport)' for '$($Type.FullName)'."
}
$null = $TypeAcceleratorsClass::Add($fullTypeToExport, $Type)
}
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
foreach ($TypeName in $typesToExportWithNamespace)
{
$fullTypeToExport = '{0}.{1}' -f $moduleName,$TypeName
$null = $TypeAcceleratorsClass::Remove($fullTypeToExport)
}
}.GetNewClosure()
#endregion
TypeAccelerator comme classes exportées pour les modules
J’utilise cela depuis un certain temps maintenant, et j’aime cette approche. J’ai tendance à exporter la plupart des types dont j’ai besoin avec le nom du module ([ModuleName.ClassName]), et parfois (rarement) j’ajoute un thème ou une catégorie si j’ai beaucoup de classes de ce module ([ModuleName.Category.ClassName]).
Évidemment, ce n’est pas parfait. Nous n’avons pas d’intellisense ou d’autocomplétion lorsque nous écrivons du code avec ce nom complet. Mais c’est un compromis que je suis heureux de faire pour un code plus clair et une utilisation plus facile des classes.
Le code est assez générique. Il suffit de remplir une liste avec les noms des types que l’on souhaite exporter. Je l’ajouterai bientôt à Sampler en tant qu’option…
Ce billet étant terminé, j’ouvre la porte au prochain ! Nous l’utiliserons avec un attribut personnalisé pour les endpoints d’API PowerShell Universal.
Discover more from SynEdgy
Subscribe to get the latest posts sent to your email.