PowerShell wrapper Phase 1: cmdlet generation for the first 15 Graph modules - #7986
PowerShell wrapper Phase 1: cmdlet generation for the first 15 Graph modules#7986Joywambui-maina wants to merge 2 commits into
Conversation
Cmdlet nouns are built from the URL path, singularized word by word, with a small override table for the SDK's hand-tuned names. 66/66 name parity on the 15 pilot modules, 51 unit tests.
Code Coverage OverviewLanguages: C# C# / code-coverage/dotnetThe overall coverage in commit c439641 in the Show a code coverage summary of the most covered files.
Updated |
|
@microsoft-github-policy-service agree [company="{microsoft}"] |
dotnet format --verify-no-changes was failing CI with CHARSET errors since these files were saved without the BOM required by .editorconfig (charset = utf-8-bom).
958a13d to
c439641
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new PowerShellWrapper generation mode to Kiota that produces C#-implemented PowerShell cmdlets for Microsoft Graph, with cmdlet naming derived from URL paths (plus a small set of checked-in overrides) and backed by new golden naming tests.
Changes:
- Introduces
GenerationLanguage.PowerShellWrapperand routeskiota generate -l PowerShellWrapperto a dedicated generation service (GeneratePowerShellWrapperAsync). - Implements path-based noun construction with singularization, seam-collapse rules, and a small override/suppression table to match published Microsoft.Graph cmdlet names.
- Adds PowerShellWrapper naming tests and documentation describing the naming algorithm and verification approach.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Kiota.Builder.Tests/PowerShellWrapper/NamingTests.cs | Adds golden tests for singularization and published cmdlet name resolution/suppression. |
| src/kiota/Handlers/KiotaGenerateCommandHandler.cs | Routes PowerShellWrapper language to the new builder entry point. |
| src/Kiota.Builder/PowerShellWrapper/Singularizer.cs | Implements word/segment singularization rules for cmdlet noun parity. |
| src/Kiota.Builder/PowerShellWrapper/SchemaProperties.cs | Extracts shallow primitive body properties for write-cmdlet parameters. |
| src/Kiota.Builder/PowerShellWrapper/README.md | Documents architecture, naming rules, overrides, and verification/gaps. |
| src/Kiota.Builder/PowerShellWrapper/PowerShellWrapperGenerationService.cs | Orchestrates wrapper emission and GET pairing logic. |
| src/Kiota.Builder/PowerShellWrapper/OperationInfo.cs | Defines operation metadata used for naming/emission decisions. |
| src/Kiota.Builder/PowerShellWrapper/NamingOverrides.cs | Adds checked-in noun overrides and operation suppression data. |
| src/Kiota.Builder/PowerShellWrapper/EmitContext.cs | Supplies module-specific context (namespaces) to emitter templates. |
| src/Kiota.Builder/PowerShellWrapper/CmdletNaming.cs | Builds cmdlet verb/noun/class names and Kiota builder expressions. |
| src/Kiota.Builder/PowerShellWrapper/CmdletEmitter.cs | Emits generated cmdlet class source (C#) for GET/POST/PATCH/DELETE shapes. |
| src/Kiota.Builder/KiotaBuilder.cs | Adds GeneratePowerShellWrapperAsync and avoids unnecessary URI-space work for this language. |
| src/Kiota.Builder/GenerationLanguage.cs | Adds the new PowerShellWrapper enum value and documentation. |
Comments suppressed due to low confidence (6)
src/Kiota.Builder/PowerShellWrapper/CmdletEmitter.cs:86
- HeaderBindingsFor interpolates OpenAPI-derived header names into a generated C# string literal (Headers.Add("...") ) without escaping. If RawName contains
", control characters, etc., the generated cmdlet source becomes uncompilable or injectable. Escape/sanitize RawName for a C# double-quoted literal at emission time.
{{extraIndent}}if (this.IsParameterBound(nameof({{h.PsName}})))
{{extraIndent}} requestConfiguration.Headers.Add("{{h.RawName}}", {{h.PsName}}!);
"""));
src/Kiota.Builder/PowerShellWrapper/CmdletEmitter.cs:274
- Same generated-source injection risk as above: naming.Noun is emitted into a C# string literal in an attribute without escaping. Sanitize before emitting.
[Cmdlet(VerbsCommon.{{naming.VerbName}}, "{{naming.Noun}}")]
src/Kiota.Builder/PowerShellWrapper/CmdletEmitter.cs:379
- listNaming.Noun comes from the OpenAPI path and is emitted into a generated C# string literal in the [Cmdlet] attribute. Escape it for a double-quoted literal context to avoid generated-source injection/uncompilable output from hostile specs.
[Cmdlet(VerbsCommon.Get, "{{listNaming.Noun}}", DefaultParameterSetName = "List")]
src/Kiota.Builder/PowerShellWrapper/CmdletEmitter.cs:444
- Cmdlet noun values are OpenAPI-derived and are written into generated C# string literals in attributes without escaping. Sanitize to prevent generated-source injection and invalid generated code when specs contain unexpected characters.
[Cmdlet(VerbsCommon.{{naming.VerbName}}, "{{naming.Noun}}", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]
src/Kiota.Builder/PowerShellWrapper/CmdletEmitter.cs:501
- Same issue as other [Cmdlet] attributes in this emitter: naming.Noun is interpolated into a generated C# string literal without escaping. Sanitize for double-quoted literal context at emission time.
[Cmdlet(VerbsData.{{naming.VerbName}}, "{{naming.Noun}}", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)]
src/Kiota.Builder/PowerShellWrapper/CmdletEmitter.cs:568
- Same generated-source injection risk: naming.Noun is emitted into a generated C# string literal without escaping. Sanitize to keep generated cmdlets compilable and hardened against hostile OpenAPI inputs.
[Cmdlet(VerbsCommon.{{naming.VerbName}}, "{{naming.Noun}}", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
| { | ||
| if (!first) | ||
| expression.Append('.'); | ||
| expression.Append(segment.ToFirstCharacterUpperCase()); | ||
| } |
| [Parameter(Mandatory = false{{setAttr}}, | ||
| HelpMessage = "Sets the '{{h.RawName}}' request header (for example an ETag for optimistic concurrency; some Graph APIs require it even where the spec marks it optional).")] | ||
| public string? {{h.PsName}} { get; set; } |
|
|
||
| namespace {{ctx.CmdletNamespace}} | ||
| { | ||
| [Cmdlet(VerbsCommon.{{naming.VerbName}}, "{{naming.Noun}}")] |
| var responseSchema = httpMethod == HttpMethod.Get | ||
| ? operation.Responses?["2XX"].Content?["application/json"].Schema | ||
| : null; | ||
| var collectionValueSchema = responseSchema is not null ? FindProperty(responseSchema, "value") : null; |
| private static string EmitNewFor(CmdletNaming naming, EmitContext ctx, OpenApiOperation operation) | ||
| { | ||
| var bodySchema = operation.RequestBody!.Content!["application/json"].Schema!; | ||
| return CmdletEmitter.EmitNew(naming, ctx, ResolveEntityTypeName(bodySchema, ctx.ModelsNamespace), | ||
| SchemaProperties.ExtractPrimitiveProperties(bodySchema), SchemaProperties.HasPasswordProfile(bodySchema)); |
| private static string EmitUpdateFor(CmdletNaming naming, EmitContext ctx, OpenApiOperation operation) | ||
| { | ||
| var bodySchema = operation.RequestBody!.Content!["application/json"].Schema!; | ||
| return CmdletEmitter.EmitUpdate(naming, ctx, ResolveEntityTypeName(bodySchema, ctx.ModelsNamespace), | ||
| SchemaProperties.ExtractPrimitiveProperties(bodySchema), SchemaProperties.HasPasswordProfile(bodySchema)); |
| private static string MapPsType(JsonSchemaType openApiType) => (openApiType & ~JsonSchemaType.Null) switch | ||
| { | ||
| JsonSchemaType.String => "string", | ||
| JsonSchemaType.Boolean => "bool", | ||
| JsonSchemaType.Integer or JsonSchemaType.Number => "int", | ||
| _ => "string", | ||
| }; |
| { | ||
| // Repeated verbatim in every emitted cmdlet. EmitSharedAuth provides the two helpers this | ||
| // block depends on: IsParameterBound and StaticBearerTokenAuthenticationProvider. | ||
| private const string AuthBlock = """ |
There was a problem hiding this comment.
The implementation emits C# via string-interpolation templates instead of CodeDOM + CSharpWriter. I think we can re-architecture the generator to live as a set of PowerShell scripts in the MS Graph PowerShell SDK repo instead of templates inside of Kiota, which goes against Kiota design.
| // Emits PowerShell cmdlet classes that call a Kiota-generated C# client. Bypasses the | ||
| // CodeDOM/refiner/writer pipeline, same shape as plugin generation; see | ||
| // KiotaBuilder.GeneratePowerShellWrapperAsync. | ||
| PowerShellWrapper |
There was a problem hiding this comment.
Using PowerShell as a generation language here is interesting since the wrapper isn't producing an SDK. We should move the wrapper to the MS Graph SDK repo.
Changes proposed in this pull request
PowerShellWrapperas akiota generateoption (-l PowerShellWrapper), routed toits own generation service instead of the CodeDOM/refiner/writer pipeline, the same
shape as plugin generation.
operationId.
(
NamingOverrides.cs, 3 entries, each citing the AutoRest directive it mirrors), andskip operations the published SDK deliberately does not ship.
rule, in
src/Kiota.Builder/PowerShellWrapper/README.md, including known gaps.Why
Generated cmdlet names previously leaked raw plural path segments:
Get-MgUsersMessagesinstead of
Get-MgUserMessage,Get-MgSolutionsBookingBusinessesinstead ofGet-MgBookingBusiness. The nouns were taken from operationIds, which carry whateverplurality the spec author chose. The project requires generated cmdlets to keep the exact
names Microsoft.Graph customers already use.
Deriving the noun from the URL path makes naming deterministic (same OpenAPI input, same
cmdlet name) and gives naming and request routing one source of truth. The published
names are not fully algorithmic: a few come from hand-written AutoRest directives in the
SDK's module configs, so those cases live here as reviewable data with a cited source
each, and the generator itself stays generic.
Validation
51/51 tests passed. CI runs the same suite on this PR.
All 15 modules regenerated from their specs, then every generated cmdlet's HTTP method
and URL joined against
MgCommandMetadata.json(the inventory behindFind-MgGraphCommand): 66 of 66 cmdlets carry the exact published name, previously 30of 66.
Find-MgGraphCommandreturns the same command names this generator now emits.All 15 modules compiled, imported, and exercised against a live tenant: list reads,
item-by-id reads through the parameter-set dispatcher,
-Filterquery binding, writecmdlets, and errors surfacing as error records from the correct cmdlet.