Skip to content

Add type-safe Custom API request/response wrappers#16

Merged
mkholt merged 9 commits into
mainfrom
customapi-property-wrappers-a2d
Jun 30, 2026
Merged

Add type-safe Custom API request/response wrappers#16
mkholt merged 9 commits into
mainfrom
customapi-property-wrappers-a2d

Conversation

@mkholt

@mkholt mkholt commented Jun 30, 2026

Copy link
Copy Markdown
Member

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]

mkholt and others added 2 commits June 30, 2026 10:13
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]>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via PluginStepRegistration.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.

Comment thread XrmPluginCore.SourceGenerator/CodeGeneration/CustomApiClassGenerator.cs Outdated
mkholt and others added 2 commits June 30, 2026 10:44
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]>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 44 out of 44 changed files in this pull request and generated 2 comments.

Comment thread XrmPluginCore/Plugin.cs
mkholt and others added 2 commits June 30, 2026 11:21
…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]>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 48 out of 48 changed files in this pull request and generated 2 comments.

Comment thread XrmPluginCore/Plugin.cs
Comment thread XrmPluginCore/Plugin.cs
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]>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 49 changed files in this pull request and generated 2 comments.

Comment thread XrmPluginCore/Helpers/IdentifierSanitizer.cs Outdated
Comment thread XrmPluginCore.SourceGenerator/Helpers/IdentifierHelper.cs Outdated
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]>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 50 changed files in this pull request and generated 6 comments.

Comment thread XrmPluginCore.SourceGenerator/Helpers/RegisterApiHelper.cs
Comment thread XrmPluginCore.SourceGenerator/Helpers/RegisterApiHelper.cs Outdated
Comment thread XrmPluginCore.SourceGenerator/Analyzers/PreferNameofAnalyzer.cs Outdated
Comment thread XrmPluginCore.SourceGenerator/Analyzers/CustomApiNameNotConstantAnalyzer.cs Outdated
return;
}

var handlerArgument = arguments[1].Expression;
Comment thread XrmPluginCore.SourceGenerator/Analyzers/CustomApiHandlerMethodNotFoundAnalyzer.cs Outdated
…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]>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated 1 comment.

@mkholt mkholt merged commit 95b7f9c into main Jun 30, 2026
2 checks passed
@mkholt mkholt deleted the customapi-property-wrappers-a2d branch June 30, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants