Appeler COM et .NET depuis PowerShell en pratique — élargir d'un coup la portée de vos scripts

· · PowerShell, Windows, COM, .NET, Automatisation, Script, Valorisation des actifs existants, Efficacité opérationnelle

« J’ai cherché une applet de commande, mais je ne trouve pas celle qui fait ce que je veux. » — Dès que l’on pratique PowerShell un peu sérieusement, on finit toujours par se heurter à ce mur. Créer des raccourcis en masse, vérifier le contenu d’un ZIP sans le décompresser, manipuler des fenêtres, lire et écrire des fichiers Excel : les demandes que les applets de commande standard ne couvrent pas nous sont soumises quotidiennement par les services informatiques et les responsables d’exploitation des PME.

En réalité, ce qui se trouve de l’autre côté de ce mur est justement le domaine de prédilection de PowerShell. Comme PowerShell est construit sur .NET, vous pouvez appeler directement les types de la bibliothèque de classes .NET sans installation supplémentaire. De plus, Add-Type permet d’intégrer sur-le-champ du code C# ou l’API Win32 (P/Invoke), et New-Object -ComObject permet de manipuler des objets COM comme WScript.Shell ou Excel. Autrement dit, la boîte à outils de « l’automatisation Windows » écrite en VBScript ou en VBA reste utilisable presque telle quelle.

Cela dit, cette puissance vient avec des règles de nettoyage. La manipulation COM d’Excel, en particulier, est un terrain fertile pour le problème classique où « EXCEL.EXE reste actif même une fois le script terminé », et Microsoft déconseille de toute façon l’automatisation d’Office en exécution sans surveillance. Cet article rassemble les schémas pratiques et les pièges de l’appel .NET, d’Add-Type et de la manipulation COM depuis PowerShell, ainsi que la décision de savoir « jusqu’où s’accrocher au script, et à partir de quand passer à un outil C# ».

1. La conclusion, d’abord

  • PowerShell repose sur .NET. Windows PowerShell 5.1 est construit sur .NET Framework, PowerShell 7 sur .NET (l’ancien .NET Core), et vous pouvez appeler directement des méthodes statiques .NET comme [System.IO.Path]::GetFileNameWithoutExtension().1
  • Pour créer une instance, utilisez New-Object ou, à partir de PowerShell 5.0, [Type]::new(). Taper [Type]::new (sans parenthèses) affiche la liste des constructeurs, ce qui permet de vérifier les arguments au fur et à mesure de l’écriture.2
  • Add-Type compile du code source C# à la volée et l’intègre à la session. En lui passant une signature avec DllImport, vous pouvez aussi appeler l’API Win32 via P/Invoke. Les types ajoutés ne peuvent pas être supprimés de la session, et un type de même nom ne peut pas être redéfini.3
  • Les objets COM se créent avec New-Object -ComObject <ProgId>. Le CreateObject("Shell.Application") de VBScript correspond directement à New-Object -ComObject Shell.Application, et les outils de l’époque WSH — comme la création de raccourcis via WScript.Shell — restent utilisables depuis PowerShell.45
  • La durée de vie d’un objet COM est gérée par comptage de références, et côté .NET, c’est le RCW (Runtime Callable Wrapper) qui la détient. Tant qu’une référence subsiste, un processus comme Excel ne se termine pas. Pour la libérer explicitement, on utilise Marshal.ReleaseComObject, mais un usage abusif peut provoquer d’autres incidents, si bien que la consigne officielle est de « ne l’utiliser que lorsque c’est réellement nécessaire ».67
  • Microsoft indique explicitement que l’automatisation d’Office en exécution sans surveillance « n’est pas recommandée et n’est pas prise en charge ». La manipulation COM d’Excel depuis un service, le Planificateur de tâches ou le côté serveur est une configuration non prise en charge, même si elle semble fonctionner. Remplacez les traitements sans surveillance par des bibliothèques de la famille Open XML ou par une intégration CSV.8
  • Les types et méthodes disponibles diffèrent entre 5.1 et 7. Il existe des cas, comme les différences de surcharges de String.Split, où le même code se comporte différemment, et sous 5.1, il faut parfois charger explicitement des assemblys du GAC avec Add-Type.13
  • Quand les appels directs à .NET se multiplient, c’est le moment d’envisager un outil C#. Si la contrainte d’Add-Type qui empêche la redéfinition d’un type, ou des problèmes de distribution, commencent à se faire sentir, c’est le signe que le script approche de ses limites.

2. PowerShell repose sur .NET — les méthodes [Type]:: et New-Object

Les applets de commande PowerShell sont, pour une part, « un emballage pratique pour les tâches d’exploitation » autour de la bibliothèque de classes .NET. Les fonctionnalités non emballées restent accessibles directement en écrivant le nom du type entre crochets. Les méthodes statiques s’appellent avec ::, les méthodes et propriétés d’instance avec ..

# Appelle directement une méthode statique .NET. Pas besoin d'applet de commande, pas d'installation supplémentaire
[System.IO.Path]::GetFileNameWithoutExtension('C:\data\commandes_20260718.csv')  # -> commandes_20260718
[System.IO.Path]::Combine('C:\data', 'archive', '2026-07')                   # -> C:\data\archive\2026-07
[System.Math]::Round(123.456, 1)                                             # -> 123.5

# Si une instance est nécessaire, créez-la avec New-Object ou [Type]::new()
$list = [System.Collections.Generic.List[string]]::new()
$list.Add('server01')

# Taper ::new sans parenthèses renvoie la liste des constructeurs (pratique pour vérifier les arguments)
[System.IO.StreamWriter]::new

[Type]::new() est une syntaxe ajoutée dans PowerShell 5.0 ; plus rapide que New-Object, elle permet en plus de vérifier sur place la liste des surcharges du constructeur, comme dans l’exemple ci-dessus.2 Il y a cependant un point de vigilance : les objets renvoyés par des applets de commande comme Get-Item se voient parfois ajouter des propriétés supplémentaires par PowerShell (des NoteProperty), si bien que leur composition de membres peut ne pas correspondre à celle d’un objet du même type créé directement avec ::new().2 Si vous êtes déjà surpris de constater qu’« une propriété présente lorsqu’on récupère l’objet via l’applet de commande a disparu », souvenez-vous de ce mécanisme.

Voici une recette pratique. Pour le traitement des fichiers ZIP, il existe Compress-Archive / Expand-Archive, mais l’API ZipArchive sur laquelle elles reposent limite la taille des fichiers à 2 Go. Cette limite est documentée officiellement aussi bien côté compression que côté décompression, chacune précisant qu’il s’agit d’« une limite imposée par l’API sous-jacente System.IO.Compression.ZipArchive » et renvoyant vers la référence de cette classe.910 Par ailleurs, une opération fine comme « vérifier seulement la liste du contenu sans décompresser » n’existe pas parmi les applets de commande. C’est là qu’intervient la classe .NET ZipFile.11

# Sous Windows PowerShell 5.1, il faut charger explicitement l'assembly du GAC
# (sous PowerShell 7, l'assembly incluse est chargée automatiquement à la demande, donc cette ligne n'est pas nécessaire)
Add-Type -AssemblyName System.IO.Compression.FileSystem

# D'abord la lecture ── vérifier le contenu du ZIP sans le décompresser
$archive = [System.IO.Compression.ZipFile]::OpenRead('C:\deploy\release.zip')
try {
    $archive.Entries | Select-Object FullName, Length, LastWriteTime
}
finally {
    $archive.Dispose()  # garantit la libération du handle de fichier
}

# Si le contenu ne pose pas de problème, décompresser. S'il existe déjà un fichier de même nom
# dans le dossier de destination, cette version à 2 arguments lève une exception après une
# décompression partielle (elle n'écrase pas). Évitez de décompresser directement dans un dossier
# existant ; il est plus sûr de décompresser à chaque fois dans un nouveau dossier puis de basculer
$dest = "C:\apps\web_$(Get-Date -Format yyyyMMddHHmmss)"
[System.IO.Compression.ZipFile]::ExtractToDirectory('C:\deploy\release.zip', $dest)

Sous .NET Framework, utiliser la classe ZipFile nécessite une référence à l’assembly System.IO.Compression.FileSystem, et il en va de même sous PowerShell 5.1.11 Sous PowerShell 7, l’assembly incluse est chargée automatiquement à la demande, donc cela fonctionne sans Add-Type, mais l’écrire fonctionne dans les deux cas ; pour un script partagé entre les deux versions, il est donc plus prudent de le préciser explicitement.3

3. Intégrer du C# personnalisé et l’API Win32 (P/Invoke) avec Add-Type

Après « ce qui existe dans .NET » vient « ce qui n’existe pas dans .NET ». Si vous passez du code source C# à Add-Type, il est compilé sur-le-champ et le type est ajouté à la session. En passant en plus une signature accompagnée de DllImport à -MemberDefinition, vous pouvez appeler l’API Win32 via P/Invoke. C’est un usage légitime, présenté comme tel dans la documentation officielle.3

# Informer par une boîte de dialogue de la fin d'un traitement long lancé localement (appelle MessageBoxW de user32.dll via P/Invoke)
$signature = @'
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type);
'@

$native = Add-Type -MemberDefinition $signature -Name 'NativeMethods' `
    -Namespace 'Win32' -PassThru

# Afficher avec MB_ICONINFORMATION (0x40)
[void]$native::MessageBoxW([IntPtr]::Zero, 'Le traitement de sauvegarde est terminé.', 'Traitement de longue durée', 0x40)

Une précaution s’impose. Une boîte de dialogue modale est réservée à l’exécution interactive, quand vous êtes vous-même devant le bureau. Dans une session non interactive comme l’exécution sans surveillance du Planificateur de tâches ou un service Windows, le traitement reste bloqué sur une boîte de dialogue que personne ne peut fermer, et ne revient jamais. Pour les notifications des tâches sans surveillance, utilisez des moyens qui n’attendent aucune réponse : fichier journal, journal des événements, envoi d’e-mail, etc.

Voici la même idée de « notifier la fin d’un traitement », réécrite pour une tâche sans surveillance : au lieu d’une boîte de dialogue, on écrit une entrée dans le journal des événements, tout en la conservant également dans un fichier journal.

# Notification version tâche sans surveillance : n'attend aucune réponse, laisse une trace consultable plus tard
$source  = 'KomuraSoft.NightlyJob'   # nom de la source de l'événement (chaîne libre)
$logPath = 'C:\logs\nightly.log'

# L'enregistrement de la source nécessite des droits d'administrateur. Exécutez-le une seule fois
# au moment du déploiement ; le job nocturne lui-même se contente d'écrire, en supposant la source déjà enregistrée
#   New-EventLog -LogName Application -Source 'KomuraSoft.NightlyJob'

Write-EventLog -LogName Application -Source $source -EntryType Information `
    -EventId 1000 -Message 'Le traitement de sauvegarde est terminé.'
Add-Content -LiteralPath $logPath -Value "$(Get-Date -Format o) Le traitement de sauvegarde est terminé."

Dans l’Observateur d’événements, sous Journaux Windows > Application, en filtrant sur le nom de source KomuraSoft.NightlyJob, vous pouvez suivre l’historique. Si votre environnement dispose d’un outil de supervision, vous pouvez lui faire récupérer cet ID d’événement pour déclencher une notification. Notez que New-EventLog / Write-EventLog sont des applets de commande de Windows PowerShell 5.1, supprimées dans PowerShell 7 (voir le tableau du chapitre 6). Pour un job nocturne exécuté sous PowerShell 7, le plus simple est soit de déléguer uniquement la partie notification à powershell.exe, soit de se rabattre sur un enregistrement dans un fichier journal combiné à une notification côté surveillance de fichiers (déclenchement d’événement du Planificateur de tâches, Power Automate, etc.).

Add-Type comporte trois contraintes opérationnelles importantes.3

  • Un type ajouté ne vit que pour cette session. Dans une autre session ou en exécution à distance, il faut relancer Add-Type.
  • Un type de même nom ne peut pas être redéfini. Si vous voulez corriger une signature erronée, changez le nom ou démarrez une nouvelle session. Faites vos essais dans une console jetable.
  • Sous PowerShell 7, si un type de même nom existe déjà, la compilation elle-même est ignorée. Soupçonnez ce comportement quand « le code que vous pensiez avoir corrigé ne semble pas pris en compte ».

Une autre précaution, commune à tout P/Invoke : une erreur dans la signature (type des arguments, jeu de caractères, convention d’appel) peut faire planter tout le processus, pas seulement lever une exception. Les pièges du marshaling des chaînes et des handles sont détaillés dans « Appeler les API Win32 en toute sécurité depuis C# — Guide pratique de P/Invoke (DllImport / LibraryImport / CsWin32) » ; nous vous conseillons de le lire avant d’utiliser sérieusement l’API Win32 avec Add-Type.

4. Appeler COM — New-Object -ComObject et la boîte à outils WSH

COM désigne l’ensemble de composants intégrés à Windows depuis bien avant .NET. On le crée avec New-Object -ComObject <ProgId>, et le Set objShell = CreateObject("Shell.Application") de VBScript correspond directement à $objShell = New-Object -ComObject Shell.Application.4 Les objets issus de WSH (Windows Script Host) — WScript.Shell, WScript.Network, Scripting.FileSystemObject — s’utilisent de la même manière.5 Pour les bases de ce qu’est COM, consultez « Qu’est-ce que COM / ActiveX / OCX ? - Différences et relations expliquées ensemble ».

Une recette pratique représentative est la création en masse de raccourcis. Il n’existe pas d’applet de commande pour créer un raccourci (.lnk), et la documentation officielle elle-même indique que « pour certaines tâches, comme la création de raccourcis, il est plus simple d’utiliser les classes WSH », en donnant un exemple avec WScript.Shell.5

# Scénario : distribuer sur le bureau public de tous les postes un raccourci vers un outil situé sur un dossier partagé
$shortcuts = Import-Csv 'C:\deploy\shortcuts.csv'   # 3 colonnes : Name, Target, Args
$wsh = New-Object -ComObject WScript.Shell

foreach ($item in $shortcuts) {
    $lnkPath = Join-Path 'C:\Users\Public\Desktop' "$($item.Name).lnk"
    $lnk = $wsh.CreateShortcut($lnkPath)   # s'il existe déjà un .lnk, il sera écrasé
    $lnk.TargetPath = $item.Target
    $lnk.Arguments  = $item.Args
    $lnk.Save()
    Write-Host "Créé : $lnkPath"
}

Les membres d’un objet COM se découvrent avec $wsh | Get-Member.5 Le champ d’application est vaste : création de brouillons de courriels dans Outlook, manipulation de dossiers spéciaux via Shell.Application, etc. Cela dit, les fichiers exécutables ActiveX comme Excel.Application (des serveurs COM démarrés dans un processus séparé) traînent avec eux le problème de nettoyage traité au chapitre suivant. La documentation officielle avertit elle aussi que le fait qu’un processus se termine ou non lorsqu’on relâche la référence dépend de l’application concernée, et qu’il faut tester ce comportement de fermeture avant utilisation.5

5. Le nettoyage de COM — le problème du processus EXCEL.EXE qui persiste et « Office sans surveillance n’est pas pris en charge »

5.1. Pourquoi le processus persiste-t-il

La durée de vie d’un objet COM est gérée par comptage de références. Lorsque .NET (donc PowerShell) manipule COM, un proxy appelé RCW (Runtime Callable Wrapper) est créé pour chaque objet COM, et tant que ce RCW est vivant, la référence côté COM n’est pas libérée. Le ramassage des RCW est laissé au bon vouloir du garbage collector.6 Autrement dit, même si vous appelez $excel.Quit(), si une référence subsiste quelque part — y compris le RCW d’un objet intermédiaire non affecté à une variable, comme dans $excel.Workbooks.Open(...) — EXCEL.EXE ne se termine pas.

Le code suivant imbrique trois niveaux de try/finally, une structure difficile à lire à première vue. Voyons d’abord pourquoi on l’écrit ainsi. L’idée est simplement que « plus une étape de nettoyage vient tard, plus on tient absolument à ce qu’elle ne soit jamais sautée » ; on les emboîte donc de l’extérieur vers l’intérieur dans cet ordre de priorité.

  • Le try le plus extérieur : le traitement principal qui manipule Excel. Quoi qu’il échoue ici, on entre forcément dans le nettoyage qui suit.
  • Le premier finally (fermer le classeur) : même si l’enregistrement échoue, on va fermer le classeur ouvert. Mais Close lui-même peut échouer.
  • Le deuxième finally (appeler Quit) : c’est pourquoi on appelle Quit à un endroit qui n’est pas affecté par l’échec de Close. Si Excel ne répond plus, Quit peut lui aussi échouer.
  • Le troisième finally (libérer les RCW et relancer le GC) : c’est pourquoi, indépendamment du succès de Quit, on exécute toujours la libération des références. Si cette étape est sautée, le processus persiste.

Autrement dit, ce triple emboîtement n’est pas un exercice de style : c’est le résultat d’avoir traité, un niveau à la fois, l’hypothèse que « le nettoyage lui-même peut échouer ». À l’inverse, si vous pouvez supposer que ni Close ni Quit n’échoueront jamais, un seul finally suffit.

$excel = New-Object -ComObject Excel.Application
try {
    $excel.DisplayAlerts = $false
    $books = $excel.Workbooks              # récupérer aussi les objets intermédiaires dans des variables pour pouvoir les libérer ensuite
    $book  = $books.Open('C:\work\ventes-mensuelles.xlsx')
    $sheet = $book.Worksheets.Item(1)
    $sheet.Cells.Item(1, 1).Value2 = "Mise à jour : $(Get-Date -Format 'yyyy-MM-dd')"
    $book.Save()
}
finally {
    # C'est justement le chemin où Close échoue qui fait le plus souvent persister EXCEL.EXE,
    # donc on exécute toujours Quit et la libération dans des finally imbriqués
    try {
        if ($book) { $book.Close($false) }
    }
    finally {
        # Même si Quit échoue (Excel ne répond plus, serveur COM déconnecté, etc.), on libère toujours les RCW
        try {
            if ($excel) { $excel.Quit() }
        }
        finally {
            # Décrémenter explicitement le compteur de références des RCW (des enfants vers le parent)
            foreach ($obj in @($sheet, $book, $books, $excel)) {
                if ($obj) { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($obj) }
            }
            # Filet de sécurité : laisser le GC récupérer les RCW qui n'auraient pas été capturés dans une variable
            [System.GC]::Collect()
            [System.GC]::WaitForPendingFinalizers()
            [System.GC]::Collect()
        }
    }
}

Marshal.ReleaseComObject décrémente le compteur de références du RCW et, une fois à zéro, libère la référence côté COM. La documentation officielle avertit toutefois fermement qu’accéder à un RCW déjà libéré provoque des exceptions ou des violations d’accès, et qu’il faut donc « ne l’utiliser que si c’est absolument nécessaire ».7

Dans le code ci-dessus, le fait de récupérer les objets intermédiaires un par un dans des variables relève d’une pratique communément appelée la « règle des deux points ». Si vous enchaînez deux points ou plus, comme dans $excel.Workbooks.Open(...), le RCW correspondant à l’objet intermédiaire Workbooks est créé sans jamais être affecté à une variable, et il ne reste aucun moyen de le libérer. D’où cette règle : au maximum un point par ligne, en faisant toujours transiter les objets intermédiaires par une variable. Les différentes écoles de libération (partisans de ReleaseComObject contre partisans du GC), le détail de la règle des deux points, et la façon d’identifier un processus qui persiste malgré tout, sont détaillés dans « Le problème d’EXCEL.EXE qui reste actif lors de la manipulation d’Excel en C# — schémas de libération des références COM et décision de remplacement ». C’est un article consacré au C#, mais le mécanisme du RCW est identique sous PowerShell.

5.2. Ne pas utiliser Office en exécution sans surveillance, tout simplement

Il existe une décision plus fondamentale encore. Microsoft affirme officiellement que « l’automatisation d’Office depuis des applications clientes ou des composants non interactifs et sans surveillance (y compris ASP, ASP.NET, DCOM, les services NT) n’est actuellement pas recommandée et n’est pas prise en charge ». La raison est qu’Office est conçu pour un usage interactif, et qu’il peut présenter un comportement instable ou des blocages (deadlocks) dans un environnement sans surveillance.8 Des problèmes concrets sont aussi énumérés, comme un traitement bloqué par une boîte de dialogue inattendue, ou une incapacité structurelle à supporter des exécutions parallèles multiples du fait du modèle STA.8 Le STA dont il est question ici est l’abréviation de Single-Threaded Apartment (appartement à thread unique), l’un des modèles de threads de COM. C’est un mécanisme qui concentre les appels à un objet sur le thread qui l’a créé et les traite un par un ; les applications Office sont conçues sur cette hypothèse. C’est pourquoi elles sont structurellement mal adaptées à l’usage consistant à « faire tourner en parallèle plusieurs instances du même traitement sur un seul serveur ». Le modèle de threads de COM lui-même est traité en détail dans « Les fondamentaux STA/MTA de COM - modèles de threads et comment éviter les blocages ».

Autrement dit, une configuration qui pilote Excel via COM depuis un job nocturne du Planificateur de tâches ou depuis un service n’est pas prise en charge, même si elle fonctionne apparemment. Pour les traitements sans surveillance, il est recommandé de la remplacer par une édition directe de fichiers de la famille Open XML, ou par une intégration CSV, sans lancer le logiciel Office lui-même.8 Des recettes de remplacement concrètes pour PowerShell sont rassemblées dans « Recettes pratiques pour le traitement Excel/CSV avec PowerShell », publié en même temps que cet article. Tracez la limite suivante : n’utiliser COM avec Excel que « pour assister le travail d’une personne, sur un bureau où elle est connectée ».

6. Le « .NET disponible » diffère entre 5.1 et 7

Windows PowerShell 5.1 est construit sur la branche .NET Framework 4.5, PowerShell 7 sur .NET (l’ancien .NET Core).1 Tant que l’on se contente d’utiliser les applets de commande, les occasions de percevoir la différence restent limitées, mais dès que l’on commence à appeler .NET directement, comme dans cet article, les différences se manifestent.

Point Windows PowerShell 5.1 PowerShell 7
Fondation Branche .NET Framework 4.51 .NET (mis à jour à chaque version : par exemple 7.4 repose sur .NET 8.0)1
Surcharges de méthodes Peu nombreuses (par ex. String.Split en compte 6)1 Nombreuses (un exemple officiel montre que même Split('pq') donne un résultat différent)1
Chargement d’assemblys Les assemblys du GAC nécessitent souvent un chargement explicite avec Add-Type3 Les assemblys incluses sont chargées automatiquement à la demande3
Fonctionnalités disparues Certaines applets de commande réservées à Windows, comme *-EventLog, ont été supprimées1

Aussi bien « ça marchait en 5.1 mais pas en 7 » que l’inverse peuvent se produire. Pour un script qui doit fonctionner dans les deux environnements, testez-le impérativement dans les deux. La vue d’ensemble de la décision de migration est traitée dans « Différences et migration entre Windows PowerShell 5.1 et PowerShell 7 », publié en même temps que cet article.

7. Les bonnes pratiques (tableau de décision)

Point Options (« recommandé » indiqué explicitement) Critère de décision
Le traitement souhaité n’existe pas comme applet de commande Abandonner / Appeler directement une classe .NET (recommandé) Cherchez d’abord une méthode [Type]::. La plupart des « il ne manque qu’un pas » se comblent du côté de System.IO, System.Text, System.IO.Compression1
Création d’instance New-Object / [Type]::new() (recommandé) À partir de 5.0 seulement, ::new(). Liste des constructeurs visible, plus rapide. Pour COM, seul New-Object -ComObject est possible24
L’API Win32 est nécessaire Se contenter du travail manuel / Add-Type avec P/Invoke (recommandé) Pour quelques API, Add-Type suffit largement. Une signature erronée fait planter tout le processus, donc validez dans une session jetable3
Opérations issues de WSH, comme la création de raccourcis COM (WScript.Shell) (recommandé) Là où il n’y a pas d’applet de commande, COM reste d’actualité. C’est aussi la méthode des exemples officiels5
Manipulation d’Excel pour assister le travail d’une personne COM + nettoyage rigoureux (recommandé) Regroupez Quit, ReleaseComObject et le GC dans un finally. Récupérez aussi les objets intermédiaires dans des variables76
Traitement Excel/Office dans un job sans surveillance COM / méthode qui ne lance pas Office (recommandé) L’automatisation d’Office sans surveillance n’est pas prise en charge. Remplacez par Open XML ou CSV8
Le script a trop grossi S’accrocher à PowerShell / passer à un outil C# (recommandé) Si le code Add-Type atteint des centaines de lignes, si la contrainte de non-redéfinition des types gêne le développement, ou si vous ne voulez pas faire recompiler à chaque déploiement — l’un de ces signes indique qu’il est temps de migrer

La dernière ligne est la décision qui conclut cet article. Quand le C# intégré via Add-Type grossit démesurément, on en est déjà à « envelopper un programme C# dans une coquille PowerShell ». Il devient alors plus rapide, tant pour le développement que pour la maintenance, de basculer vers un projet C# qui bénéficie de la vérification de types et du débogueur de Visual Studio, des paquets NuGet et des tests unitaires. Il existe aussi des façons de rappeler les actifs PowerShell depuis le côté C#, donc la migration n’implique pas de tout réécrire. « Comment exécuter PowerShell depuis C# (CSharp) et récupérer le résultat sous forme d’objets » présente des schémas de passerelle.

8. Conclusion

  • PowerShell est construit sur .NET, et vous pouvez appeler directement la bibliothèque de classes .NET avec [Type]::méthode statique, New-Object et [Type]::new(). Pour une fonctionnalité absente des applets de commande, .NET est le premier réflexe.
  • Add-Type compile du code C# à la volée, et en lui passant une signature DllImport, vous pouvez aussi appeler l’API Win32 via P/Invoke. Les types ne vivent que pour la session et ne peuvent pas être redéfinis sous le même nom.
  • COM se manipule avec New-Object -ComObject. Les outils issus de WSH, comme la création de raccourcis, restent d’actualité.
  • La durée de vie de COM est gérée par comptage de références et par les RCW ; si une référence subsiste, des processus comme EXCEL.EXE persistent. Écrivez le nettoyage — Quit, ReleaseComObject, GC — dans un finally.
  • Microsoft déclare explicitement que l’automatisation d’Office en exécution sans surveillance n’est ni recommandée ni prise en charge. Remplacez les jobs nocturnes par une méthode qui ne lance pas Office.
  • Le .NET sous-jacent diffère entre 5.1 et 7, tout comme les types et surcharges disponibles. Pour un script compatible avec les deux, tester dans les deux est indispensable.
  • Quand Add-Type devient trop volumineux, c’est le signe qu’il faut passer à un outil C#. Des types existants permettent de faire le pont entre le script et l’exécutable.

Articles connexes

Domaines de conseil associés

合同会社小村ソフト (Komura Software LLC) prend en charge l’automatisation des tâches internes avec PowerShell, la conception et la refonte de scripts impliquant des actifs COM (intégration Excel, composants hérités), ainsi que le passage à un outil C# pour un script « qui a trop grossi ». Nous intervenons aussi sur les enquêtes de dysfonctionnement, comme la persistance du processus EXCEL.EXE.

Références

  1. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Sur le fait que Windows PowerShell 5.1 est construit sur .NET Framework 4.5 et que PowerShell 6.0 et ultérieur sont construits sur .NET Core (aujourd’hui .NET), sur la liste des versions de .NET sous-jacentes à chaque version, sur l’exemple où la différence de surcharges de String.Split fait changer le résultat du même code, et sur les applets de commande supprimées en 7.  2 3 4 5 6 7 8 9

  2. Microsoft Learn, about_Object_Creation. Sur la méthode statique new() ajoutée à tous les types .NET dans PowerShell 5.0, sur le fait que taper ::new permet de consulter la liste des surcharges du constructeur, et sur le fait que les objets récupérés via une applet de commande peuvent avoir des NoteProperty ajoutées par PowerShell, ce qui peut faire différer leurs membres de ceux d’un objet créé avec ::new().  2 3 4

  3. Microsoft Learn, Add-Type. Sur le fait qu’Add-Type compile du code source C# et ajoute le type à la session, sur l’exemple officiel de P/Invoke via MemberDefinition (ShowWindowAsync de user32.dll), sur le fait qu’un type ajouté ne vit que pour la session et ne peut pas être redéfini sous le même nom, sur le fait que PowerShell 7 saute la compilation si un type de même nom existe déjà, et sur le fait que 5.1 nécessite Add-Type pour charger les assemblys du GAC alors qu’à partir de 6 elles sont chargées automatiquement.  2 3 4 5 6 7 8

  4. Microsoft Learn, New-Object. Sur le fait que New-Object crée une instance d’objet .NET ou COM, que le paramètre -ComObject prend un ProgId, et que le CreateObject(“Shell.Application”) de VBScript correspond à New-Object -ComObject “Shell.Application”.  2 3

  5. Microsoft Learn, Creating .NET and COM objects (New-Object). Sur la création d’objets WSH tels que WScript.Shell, sur l’indication qu’il est plus simple d’utiliser les classes WSH pour certaines tâches comme la création de raccourcis, avec un exemple concret de CreateShortcut, sur l’application de Get-Member à un objet COM, sur le fait que le comportement de fermeture d’un fichier exécutable ActiveX dépend de l’application et doit être testé au préalable, et sur le fait que New-Object utilise le RCW de .NET.  2 3 4 5 6

  6. Microsoft Learn, Runtime Callable Wrapper. Sur le fait que .NET expose les objets COM via un proxy appelé RCW, qu’un RCW est créé par objet COM, et que le RCW libère sa référence à l’objet COM lorsqu’il est récupéré par le garbage collector.  2 3

  7. Microsoft Learn, Marshal.ReleaseComObject(Object) Method. Sur le fait que ReleaseComObject décrémente le compteur de références du RCW et libère l’objet COM sous-jacent une fois à zéro, sur le fait qu’utiliser un RCW déjà libéré provoque des exceptions ou des violations d’accès, sur l’avertissement officiel de ne l’utiliser que « si c’est absolument nécessaire », et sur la relation avec FinalReleaseComObject.  2 3

  8. Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. Sur le fait que Microsoft ne recommande pas et ne prend pas en charge l’automatisation d’Office depuis des applications clientes ou composants non interactifs et sans surveillance (y compris ASP, ASP.NET, DCOM, les services NT), sur le risque de comportement instable ou de blocages dans un environnement sans surveillance, sur des problèmes concrets comme l’arrêt par une boîte de dialogue ou les contraintes d’exécution multiple liées au STA, et sur le fait que l’édition directe des formats de fichiers Open XML est l’alternative recommandée.  2 3 4 5

  9. Microsoft Learn, Expand-Archive. Sur le fait qu’Expand-Archive utilise l’API System.IO.Compression.ZipArchive, que cette API limite la taille maximale des fichiers à 2 Go, et que cette API .NET traite des fichiers conformes à la spécification officielle du format ZIP de PKWARE. 

  10. Microsoft Learn, Compress-Archive. Sur le fait que Compress-Archive utilise l’API System.IO.Compression.ZipArchive pour compresser, que la limite de 2 Go est explicitement présentée comme une contrainte de l’API sous-jacente (« The API limits the maximum file size to 2GB »), avec la classe ZipArchive citée en référence (le texte de la référence de la classe ZipArchive elle-même ne mentionne pas la valeur de 2 Go, la source primaire de ce chiffre reste donc cette page de l’applet de commande). 

  11. Microsoft Learn, ZipFile Class. Sur le fait que la classe ZipFile fournit des méthodes statiques pour créer, décompresser et ouvrir des fichiers ZIP, sur la nécessité d’une référence à l’assembly System.IO.Compression.FileSystem sous .NET Framework, et sur les usages de CreateFromDirectory/ExtractToDirectory/OpenRead.  2

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Peut-on appeler directement des classes .NET depuis PowerShell ?
Oui. Comme PowerShell est construit sur .NET, vous pouvez écrire le nom d'un type entre crochets, comme [System.IO.Path]::GetFileNameWithoutExtension(), et appeler une méthode statique avec ::. Si une instance est nécessaire, créez-la avec New-Object, ou avec [Type]::new(), disponible à partir de PowerShell 5.0. Même pour un traitement sans applet de commande dédiée, tant qu'il existe dans la bibliothèque de classes .NET, vous pouvez l'utiliser depuis un script sans installation supplémentaire.
Faut-il utiliser New-Object ou [Type]::new() ?
Les deux permettent de créer un objet, mais [Type]::new(), tapé sans arguments, affiche la liste des constructeurs du type, ce qui le rend pratique pour écrire du code en vérifiant les arguments au fur et à mesure. Ses performances sont aussi meilleures. En revanche, la création d'un objet COM n'est possible qu'avec New-Object -ComObject. Utilisez également New-Object si vous devez tenir compte d'environnements antérieurs à PowerShell 5.0.
Pourquoi EXCEL.EXE reste-t-il actif quand on manipule Excel via COM depuis PowerShell ?
La durée de vie d'un objet COM est gérée par comptage de références, et côté .NET, c'est le RCW (Runtime Callable Wrapper) qui détient cette référence. En écrivant $excel.Workbooks.Open(), un RCW invisible est aussi créé pour l'objet intermédiaire Workbooks, et cette référence non affectée à une variable subsiste. Même en appelant Quit(), le processus ne se termine pas tant qu'il reste des références. Une fois le traitement terminé, il faut libérer explicitement avec Marshal.ReleaseComObject, ou mettre les variables à null et laisser GC.Collect() et WaitForPendingFinalizers() faire le ramassage.
Peut-on manipuler Excel via COM sur un serveur ou dans un traitement par lots nocturne ?
Ce n'est pas recommandé. Microsoft déclare officiellement ne pas recommander, ni prendre en charge, l'automatisation des applications Office depuis des applications clientes ou des composants non interactifs et sans surveillance. Office est conçu pour un usage interactif sur bureau, et son exécution sans surveillance peut provoquer un comportement instable ou des blocages. Pour les traitements sans surveillance, la pratique standard est de remplacer cela par des méthodes qui ne lancent pas le logiciel Office lui-même, comme les bibliothèques de la famille Open XML ou une intégration CSV.
Peut-on appeler l'API Win32 avec Add-Type ?
Oui. En passant à -MemberDefinition d'Add-Type une signature C# accompagnée de DllImport, elle est compilée sur-le-champ et devient appelable via P/Invoke pour l'API Win32. La documentation officielle donne d'ailleurs un exemple d'appel à ShowWindowAsync de user32.dll. Cependant, le type ajouté reste présent dans la session et un type de même nom ne peut pas être redéfini, donc relancez une nouvelle session pour vos essais. Une erreur de signature peut faire planter tout le processus, il est donc plus sûr de valider le fonctionnement dans une console jetable.
Y a-t-il des différences dans les appels .NET entre Windows PowerShell 5.1 et PowerShell 7 ?
Oui. 5.1 est construit sur la branche .NET Framework 4.5, PowerShell 7 sur .NET (l'ancien .NET Core), et les types disponibles ainsi que les surcharges de méthodes diffèrent. Par exemple, String.Split a davantage de surcharges en 7, et un exemple officiel montre que le même code peut donner un résultat différent. De plus, en 5.1 il faut souvent charger les assemblys du GAC avec Add-Type, alors qu'en 7 les assemblys incluses sont chargées automatiquement à la demande. Pour un script destiné à fonctionner dans les deux environnements, testez-le dans les deux.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog