API PowerShell Universal avec [APIEndpoint()]
Je cherche toujours à rendre l’automatisation plus facile à faire passer à
l’échelle dans de vraies équipes. Récemment, j’ai créé un petit module,
synedgy.universal.helper,
pour simplifier la manière dont je définis et importe les points de terminaison
d’API PowerShell Universal.
L’idée clé est de garder les métadonnées de point de terminaison au plus près de la fonction PowerShell à laquelle elles appartiennent, au lieu de dupliquer les définitions dans des fichiers séparés.
![Illustration personnalisée de [APIEndpoint()] pour cet article](/_astro/apiendpoint-cover.BOf3tKrK_tO0li.webp)
API PowerShell Universal en pratique
PowerShell Universal peut exposer des API REST directement depuis PowerShell et générer une documentation Swagger/OpenAPI à partir de vos définitions d’endpoints.
Les modèles de points de terminaison documentés sont :
- Transmettre directement un
-ScriptBlockàNew-PSUApiEndpoint, ce qui est rapide pour les prototypes - Utiliser des liaisons de module et de commande pour une structure à long terme plus propre
Les deux fonctionnent, mais si une API grandit, la prolifération des points de
terminaison dans endpoints.ps1 devient vite difficile à maintenir.

Conseils de production avant les modèles personnalisés
Deux habitudes aident dès le départ :
- Garder la logique de point de terminaison dans des modules PowerShell, pas dans de grands blocs de script intégrés
- Exécuter les API dans des environnements PowerShell Universal dédiés
L’isolation des environnements améliore le démarrage et le contrôle opérationnel, car les modules peuvent être préchargés et les charges API peuvent être redémarrées indépendamment.
$newPSUEnvironmentParams = @{
Name = 'PSUModuleEnv'
Variables = @('*')
Description = 'Environment for PSUModule API'
Type = 'PowerShell7'
Path = 'pwsh'
Arguments = '-NoLogo'
Modules = @('PSUModule')
DisableImplicitWinCompat = $true
PSModulePath = (Split-Path -Path $PSScriptRoot -Parent)
PersistentRunspace = $true
}
New-PSUEnvironment @newPSUEnvironmentParams
Là où le modèle habituel des points de terminaison devient maladroit
J’ai rencontré trois points de friction récurrents :
- Décalage de casse des paramètres : PowerShell utilise PascalCase, tandis que de nombreux consommateurs d’API s’attendent à camelCase.
- Responsabilités séparées : la logique de fonction se trouve dans un fichier, tandis que les métadonnées de point de terminaison se trouvent ailleurs.
- Changements d’environnement en masse : déplacer des points de terminaison entre environnements peut devenir un travail répétitif de recherche-remplacement.
Le modèle de l’attribut [APIEndpoint()]
Le modèle consiste à annoter la fonction elle-même avec les métadonnées de point de terminaison :
function Get-Something {
[CmdletBinding()]
[ApiEndpoint(
Path = '/getSomething',
Method = @('GET'),
Description = 'Get a string using the someThing parameter.',
Environment = 'PowerShell 7',
ContentType = 'text/event-stream; charset=utf-8'
)]
param (
[Parameter()]
[string]
$SomeThing
)
Get-ResultFromSomewhere -SomeThing $SomeThing
}
Puis importez tous les points de terminaison annotés depuis le module :
# .universal/endpoints.ps1
# Assumes synedgy.universal.helper is available in PSModulePath.
Import-PSUEndpoint -Module PSUModule -Environment PSUModuleEnv
Cela donne un point unique pour l’intention du point de terminaison et l’implémentation de la fonction.
Ce que fait Import-PSUEndpoint
Import-PSUEndpoint découvre les fonctions exportées avec [APIEndpoint()]
lorsque IsEndpoint = $true, puis génère des blocs de script de points de
terminaison qui :
- Reflètent la signature de la fonction
- Exposent les paramètres côté API en camelCase
- Retransmettent
@PSBoundParametersà la fonction d’origine par splatting
Exemple de forme générée :
{
[CmdletBinding()]
param (
[Parameter()]
[string]
$someThing
)
Get-Something @PSBoundParameters
}
Définition de l’attribut personnalisé
L’attribut est une classe PowerShell normale qui hérite de System.Attribute.
class APIEndpoint : System.Attribute {
[bool]$IsEndpoint = $true
[string]$Name
[string]$Version = 'v1'
[string]$Path
[string]$Description
[ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD')]
[string[]]$Method
[bool]$Authentication = $false
[string[]]$Role
[string]$Tag
[int]$Timeout
[string]$Environment
[string]$ContentType = 'application/json; charset=utf-8'
[ValidateSet('Information', 'Warning', 'Error', 'Verbose', 'Debug')]
[string[]]$LogLevel = @('Information')
[scriptblock]$Parameters
APIEndpoint () {}
}
Une passe de découverte simple ressemble à ceci :
Get-Command -Module PSUModule | Where-Object {
$_.ScriptBlock.Attributes.Where{
$_.TypeId.ToString() -eq 'APIEndpoint' -and
$_.IsEndpoint -eq $true
}
}
Dépendance et réutilisation
Pour réutiliser l’attribut entre modules, exposez-le via des accélérateurs de
type comme décrit dans
Exporter des classes depuis des modules PowerShell,
puis déclarez synedgy.universal.helper comme module requis.
Limites et évolutions futures
Deux points restent importants :
- Chargement des fils d’exécution/modules et verrous de fichiers : importer des modules sur le fil d’exécution principal de PSU peut garder les descripteurs de DLL ouverts plus longtemps que souhaité pendant le déploiement.
- Traduction des paramètres switch : là où la gestion des points de
terminaison a encore des difficultés avec
[switch], les signatures générées peuvent traduire vers[bool]avant le splatting.