Add type-safe Custom API request/response wrappers#16
Merged
Conversation
Mirror the type-safe image feature for Custom APIs. The typed overload
RegisterAPI<TService>(name, handlerMethodName) opts in: the source generator
emits {ApiName}Request/{ApiName}Response classes (named after the API, in the
plugin's namespace) from the AddRequestParameter/AddResponseProperty calls, plus
an internal ActionWrapper that marshals InputParameters into the request and the
returned response into OutputParameters. The handler signature adapts when no
request parameters (no argument) or no response properties (void return) are
declared.
- Public API/runtime: new typed RegisterAPI overload; CustomApiRegistration
carries the handler method; wrapper discovery unified behind
PluginStepRegistration.WrapperTypeName (computed at registration time for both
plugin steps and Custom APIs).
- Source generator: CustomApiGenerator + parser, metadata, type mapper, code
generator, and shared helpers.
- Diagnostics + fixers: XPC4004 (handler not found), XPC4005/XPC4006 (signature
mismatch warning/error), and XPC3001 extended to the Custom API handler arg.
- Generated code is backwards compatible with consumers that have nullable
reference types disabled (incl. .NET Framework / C# 7.3): reference-type `?`
annotations and the `#nullable enable` directive are emitted only when NRT is
enabled; nullable value types are always emitted. This also fixes the image
generator emitting `string?` (and CS8669) on NRT-off projects.
- Tests: generator output, analyzer/fixer, and end-to-end runtime coverage.
- Docs: CHANGELOG, CLAUDE.md, README, and rules/XPC400{4,5,6}.md.
Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
Resolved CHANGELOG.md conflict by folding the unreleased v1.3.1 entry (image [Obsolete] mirroring) into the pending v1.4.0 release. Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for type-safe Custom API request/response wrappers (mirroring the existing type-safe image experience), including runtime registration/discovery changes, a new incremental source generator pipeline, new analyzers + code fixes, and expanded tests/docs. It also refines generated-code nullable reference type (NRT) emission to remain compatible with NRT-off / older language-version consumers.
Changes:
- Runtime: new typed
RegisterAPI<TService>(name, handlerMethodName)overload, plus unified wrapper discovery viaPluginStepRegistration.WrapperTypeName. - Source generator: new Custom API parser/metadata/type-mapping/codegen + shared helpers; new diagnostics XPC4004–XPC4006 and updates to XPC3001 coverage.
- Tests/docs: generator/analyzer/code-fix coverage, runtime end-to-end tests, and documentation updates (CHANGELOG/README/CLAUDE/rules).
Reviewed changes
Copilot reviewed 42 out of 42 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| XrmPluginCore/Plugins/PluginStepRegistration.cs | Simplifies registration payload and adds WrapperTypeName as unified runtime discovery key. |
| XrmPluginCore/Plugin.cs | Adds typed Custom API registration overload and unifies wrapper discovery for steps + Custom APIs. |
| XrmPluginCore/Helpers/IdentifierSanitizer.cs | Runtime identifier sanitization to match generator naming for wrapper discovery. |
| XrmPluginCore/CustomApis/CustomApiRegistration.cs | Adds handler-name based registration mode for type-safe Custom APIs. |
| XrmPluginCore/CustomApis/CustomApiConfigBuilder.cs | Exposes Custom API name for wrapper discovery and generator alignment. |
| XrmPluginCore/CHANGELOG.md | Documents v1.4.0 features/fixes (type-safe Custom APIs + NRT gating). |
| XrmPluginCore.Tests/TypeSafeCustomApiTests.cs | End-to-end runtime tests for request/response marshalling via generated wrapper. |
| XrmPluginCore.Tests/TestCustomApis/TypeSafe/TypeSafeCustomApiService.cs | Test service implementing the typed handler signature. |
| XrmPluginCore.Tests/TestCustomApis/TypeSafe/TypeSafeCustomApi.cs | Test plugin registering a type-safe Custom API with request/response declarations. |
| XrmPluginCore.SourceGenerator/rules/XPC4006.md | Adds rule doc for signature mismatch when generated types exist (error). |
| XrmPluginCore.SourceGenerator/rules/XPC4005.md | Adds rule doc for signature mismatch before types exist (warning). |
| XrmPluginCore.SourceGenerator/rules/XPC4004.md | Adds rule doc for missing handler method (error) + code fix. |
| XrmPluginCore.SourceGenerator/Parsers/RegistrationParser.cs | Plumbs nullable-annotations flag into step/image metadata for NRT-safe codegen. |
| XrmPluginCore.SourceGenerator/Parsers/CustomApiRegistrationParser.cs | New parser for typed Custom API registration + fluent parameter/property chain. |
| XrmPluginCore.SourceGenerator/Models/PluginStepMetadata.cs | Tracks nullable-annotations enabled flag in metadata equality/hash. |
| XrmPluginCore.SourceGenerator/Models/CustomApiMetadata.cs | New Custom API metadata model (names, types, request/response shape, NRT flag). |
| XrmPluginCore.SourceGenerator/Helpers/RegisterApiHelper.cs | New helper utilities for locating/analyzing typed RegisterAPI<TService> chains. |
| XrmPluginCore.SourceGenerator/Helpers/NullableHelper.cs | Centralizes NRT gating logic for generated output (and safe type display). |
| XrmPluginCore.SourceGenerator/Helpers/IdentifierHelper.cs | Generator-side identifier sanitizer matching runtime naming rules. |
| XrmPluginCore.SourceGenerator/Helpers/CustomApiParameterTypeMapper.cs | Maps CustomApiParameterType → CLR types for generated request/response. |
| XrmPluginCore.SourceGenerator/Helpers/CustomApiHandlerSyntaxHelper.cs | Shared handler signature syntax builder for analyzers and code fixes. |
| XrmPluginCore.SourceGenerator/Helpers/CustomApiGenerationContext.cs | Resolves expected request/response type names for diagnostics and fixers. |
| XrmPluginCore.SourceGenerator/Generators/PluginImageGenerator.cs | Threads nullable-annotations flag into step wrapper generation pipeline. |
| XrmPluginCore.SourceGenerator/Generators/CustomApiGenerator.cs | New incremental generator emitting request/response/action-wrapper for Custom APIs. |
| XrmPluginCore.SourceGenerator/DiagnosticDescriptors.cs | Adds descriptors for XPC4004/XPC4005/XPC4006. |
| XrmPluginCore.SourceGenerator/Constants.cs | Adds Custom API method/class suffix constants + diagnostic property keys. |
| XrmPluginCore.SourceGenerator/CodeGeneration/WrapperClassGenerator.cs | Gates #nullable enable emission in generated plugin-step wrapper files. |
| XrmPluginCore.SourceGenerator/CodeGeneration/Indent.cs | Adds deeper indentation constant used by Custom API generator output. |
| XrmPluginCore.SourceGenerator/CodeGeneration/CustomApiClassGenerator.cs | New codegen for Custom API Request/Response/ActionWrapper types. |
| XrmPluginCore.SourceGenerator/CodeFixes/FixCustomApiHandlerSignatureCodeFixProvider.cs | New code fix to rewrite handler signature to match declared API shape. |
| XrmPluginCore.SourceGenerator/CodeFixes/CreateCustomApiHandlerMethodCodeFixProvider.cs | New code fix to create missing handler method with correct signature. |
| XrmPluginCore.SourceGenerator/Analyzers/PreferNameofAnalyzer.cs | Extends XPC3001 coverage to typed Custom API handler-name argument. |
| XrmPluginCore.SourceGenerator/Analyzers/CustomApiHandlerSignatureMismatchAnalyzer.cs | New analyzer reporting XPC4005/XPC4006 based on signature/type existence. |
| XrmPluginCore.SourceGenerator/Analyzers/CustomApiHandlerMethodNotFoundAnalyzer.cs | New analyzer reporting XPC4004 when handler method is missing. |
| XrmPluginCore.SourceGenerator/AnalyzerReleases.Unshipped.md | Declares new rules XPC4004–XPC4006 for analyzer release tracking. |
| XrmPluginCore.SourceGenerator.Tests/Helpers/TestFixtures.cs | Adds fixture source for typed Custom API generator/analyzer tests. |
| XrmPluginCore.SourceGenerator.Tests/Helpers/GeneratorTestHelper.cs | Adds helper to run CustomApiGenerator in tests. |
| XrmPluginCore.SourceGenerator.Tests/Helpers/CompilationHelper.cs | Makes nullable context configurable for NRT-on/off generator tests. |
| XrmPluginCore.SourceGenerator.Tests/GenerationTests/CustomApiClassGenerationTests.cs | Validates generated Request/Response/ActionWrapper output across scenarios and NRT modes. |
| XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/CustomApiHandlerDiagnosticsTests.cs | Tests new diagnostics + code fixes for handler creation/signature repair. |
| README.md | Documents type-safe Custom API usage and adds new analyzer rules to table. |
| CLAUDE.md | Adds internal documentation for type-safe Custom API design and diagnostics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace the hardcoded C# keyword list in CustomApiClassGenerator with SyntaxFacts.GetKeywordKind, which is the purpose-built Roslyn API: it matches exactly the reserved keywords that need verbatim @-escaping and excludes contextual keywords (value/select/from), which are valid identifiers. Adds a test covering the escaping (e.g. "Class" -> "@Class"). Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
…, namespace-aware type matching Addresses code review feedback for the Custom API generator, and applies the same namespace-aware fix to the existing image analyzer for consistency. Custom API generator: - Escape reserved-keyword property names (sanitized unique names like "event"/"namespace") at every emission site: property declarations, the request object initializer, the response constructor assignment, and the response member access in the wrapper. Contextual keywords (yield/value) are left unescaped since they are valid identifiers. - Qualify the response constructor assignment with `this.` so it sets the property instead of self-assigning the parameter when the property and parameter resolve to the same identifier (lowercase unique names) - which previously left the property unset and warned CS1717. Signature analyzers (Custom API and images): - Match the generated request/response/image types by namespace + name instead of short name only, so a same-named type in a different namespace is no longer mistaken for the generated type. Falls back to name-only when the expected namespace can't be resolved. Tests for each case, including a compile check of the generated output. Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
…nstant The typed RegisterAPI<TService>(name, handlerMethodName) overload names its generated classes after the API, so a non-constant name produced no generated ActionWrapper and failed at runtime with no explanation. A dedicated analyzer now reports XPC3006 (Warning) at the name argument, pointing the developer to use nameof(...), a const, or a string literal. The action-based overloads are unaffected. Includes rule doc, tests, and changelog/README/CLAUDE entries. Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
RegisterAPI<TService>(name, handlerMethodName) now throws ArgumentException when name or handlerMethodName is null/whitespace. Previously such a registration produced no action and no handler reference, so Execute() silently treated it as 'no registration' instead of failing, leading to a confusing runtime no-op. Adds a Theory covering each invalid combination. Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
Runtime wrapper-type discovery used GetType().Namespace, which is null for plugin classes in the global namespace, producing a leading-dot type name that never matched the generated types. The source generator places such types under the literal "GlobalNamespace". The runtime now mirrors that fallback (Plugin.ChildClassNamespace) for both plugin-step and Custom API wrapper discovery. Adds an end-to-end test with a global-namespace Custom API plugin and a cross-reference comment in the generator. Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
Both IdentifierSanitizer (runtime) and IdentifierHelper (generator) replaced a leading digit with '_' in the loop AND then prepended another '_', dropping the digit and collapsing names that differ only by their leading digit (e.g. "1Foo"/"2Foo" both became "__Foo"). Digits are now kept in the loop and only the leading-digit prefix is added, so "1Foo" -> "_1Foo". The two implementations remain identical (runtime discovery must match generator emission). Adds a generation test and an end-to-end test with a digit-starting API name. Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
| return; | ||
| } | ||
|
|
||
| var handlerArgument = arguments[1].Expression; |
…alue Analyzers and parsers resolved the Custom API name, handler and parameter unique names (and the plugin step's event/stage/handler) by ordinal position. Named arguments may be supplied in any legal order, so positional indexing could resolve the wrong value or skip generation. Introduce a shared ArgumentBinder that maps each argument expression to its parameter symbol (honoring named and positional forms) and route RegisterAPI/RegisterStep argument lookups through it. Generate request marshalling with the strongly-typed ParameterCollection.TryGetValue<T> overload instead of Contains(...) ? (T)[...] : default, matching the pattern used elsewhere. The optional value-type case uses the non-nullable underlying type as the generic argument so the boxed input value matches. Co-Authored-By: Claude <[email protected]> via Conducktor <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mirror the type-safe image feature for Custom APIs. The typed overload
RegisterAPI<TService>(name, handlerMethodName)opts in: the source generatoremits
{ApiName}Request/{ApiName}Responseclasses (named after the API, in theplugin's namespace) from the
AddRequestParameter/AddResponsePropertycalls, plusan internal ActionWrapper that marshals InputParameters into the request and the
returned response into OutputParameters. The handler signature adapts when no
request parameters (no argument) or no response properties (void return) are
declared.
carries the handler method; wrapper discovery unified behind
PluginStepRegistration.WrapperTypeName (computed at registration time for both
plugin steps and Custom APIs).
generator, and shared helpers.
mismatch warning/error), and XPC3001 extended to the Custom API handler arg.
reference types disabled (incl. .NET Framework / C# 7.3): reference-type
?annotations and the
#nullable enabledirective are emitted only when NRT isenabled; nullable value types are always emitted. This also fixes the image
generator emitting
string?(and CS8669) on NRT-off projects.Co-Authored-By: Claude [email protected] via Conducktor [email protected]