Skip to main content

mitigate-breaking-changes

Patterns and techniques for mitigating breaking changes during Azure management-plane SDK migration from Swagger/AutoRest to TypeSpec. Covers SDK-side customizations (partial classes, CodeGenType, CodeGenSuppress) and TypeSpec decorator customizations (clientName, access, markAsPageable, alternateType, hierarchyBuilding).

Jump to install

Source facts

Repository
Azure/azure-sdk-for-net
Last source activity
July 28, 2026 at 23:28
Detected SKILL.md language
English
Stars
6,034
Forks
5,179

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
mitigate-breaking-changes
description
Patterns and techniques for mitigating breaking changes during Azure management-plane SDK migration from Swagger/AutoRest to TypeSpec. Covers SDK-side customizations (partial classes, CodeGenType, CodeGenSuppress) and TypeSpec decorator customizations (clientName, access, markAsPageable, alternateType, hierarchyBuilding).
# Skill: mitigate-breaking-changes Patterns and techniques for mitigating breaking changes when migrating or regenerating Azure management-plane .NET SDKs. Use these to preserve backward compatibility in the generated SDK surface. ## When Invoked Trigger phrases: "mitigate breaking changes", "fix breaking change", "customization patterns", "how to keep backward compat", "CodeGenType", "CodeGenSuppress", "markAsPageable", "hierarchyBuilding", "base type change". ## SDK-Side Customizations (in SDK repo) Use **Custom/*.cs** or **Customization/*.cs** partial classes (follow the package's existing structure) for .NET-side fixes. ### Custom code file organization Keep custom code types split into separate files: - Put each custom type/partial type in its own `.cs` file. - Name the file after the class it contains, for example `src/Custom/Models/MyModel.cs` for `public partial class MyModel`. - Align the custom code folder structure with the generated code structure, such as using `Models/` for model customizations and `Extensions/` for extension customizations when those folders exist. - Do not group multiple compatibility types in a single broad file such as `Compatibility.cs`; split them so each file name aligns with the class name. ### Partial class (add members, suppress generated members) ```csharp // src/Custom/Models/MyModel.cs (or src/Customization/Models/MyModel.cs — follow the package's existing convention) namespace Azure.ResourceManager.<Service>.Models { public partial class MyModel { // Add computed properties, rename via [CodeGenMember], etc. } } ``` ### `[CodeGenType]` — Override accessibility or rename a generated type When a generated type is `internal` and `@@access` in `client.tsp` doesn't work (common for nested/wrapper types), use `[CodeGenType]` in Custom code to make it public: ```csharp // src/Custom/Models/MyPublicModel.cs using Microsoft.TypeSpec.Generator.Customizations; namespace Azure.ResourceManager.<Service>.Models { [CodeGenType("OriginalTypeSpecModelName")] public partial class MyPublicModel { } } ``` The `[CodeGenType("...")]` attribute takes the **original TypeSpec model name** (not the C# renamed name). This links the Custom partial class to the generated internal type and overrides its accessibility to `public`. ## TypeSpec Decorator Customizations (in spec repo) These decorators are added to `client.tsp` in the spec repo. ```typespec // Rename parameter @@clientName(Operations.create::parameters.resource, "content"); // Rename path parameter @@Azure.ResourceManager.Legacy.renamePathParameter(Resources.list, "fooName", "name"); // Mark a non-pageable list operation as pageable (returns Pageable<T> instead of Response<ListType>) // Requires: using Azure.ClientGenerator.Core.Legacy; #suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "migration" @@markAsPageable(InterfaceName.operationName, "csharp"); // Suppress warning #suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "migration" ``` ### When to use `@@markAsPageable` When the old SDK returned `Pageable<T>` / `AsyncPageable<T>` for a list operation, but the TypeSpec spec defines the operation as non-pageable (returns a wrapper list type like `FooList`), use `@@markAsPageable` to make the generator produce pageable methods. This is **preferred over** writing custom `SinglePagePageable<T>` wrapper code because: - It reduces custom code that must be maintained - The generated pageable implementation handles diagnostics, cancellation, and error handling correctly - It keeps the SDK surface consistent with other generated methods **Do NOT use `@@markAsPageable` if the operation is already marked with `@list`** — the `@list` decorator already makes the operation pageable, and adding `@@markAsPageable` will cause a compile error. Check the spec's operation definition before adding the decorator. **Requirements:** 1. Add `using Azure.ClientGenerator.Core.Legacy;` to the `client.tsp` imports 2. Add `#suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "migration"` before each `@@markAsPageable` call 3. After adding the decorator, regenerate and remove any custom `[CodeGenSuppress]` + `SinglePagePageable` wrapper code ### `@@alternateType` Decorator When the spec uses older common types that generate incorrect C# types (e.g., `string` instead of `ResourceIdentifier` for ID properties), use `@@alternateType`: ```typespec @@alternateType(MyModel.resourceId, Azure.ResourceManager.CommonTypes.ArmResourceIdentifier, "csharp"); ``` ### `@@hierarchyBuilding` Decorator — Legacy base-type override Do **not** use `@@hierarchyBuilding` for C# base-model/base-type compatibility during MPG migrations. Follow the `mpg-migration` skill instead: verify resource-hierarchy parity first, fix structural resource hierarchy issues in the TypeSpec resource shape, and use SDK-side custom code only for C# base-model/base-type compatibility after the generated surface is stable. `@@hierarchyBuilding` is a legacy escape hatch. Use it only when the migration owner explicitly approves it and no TypeSpec resource-shape fix or SDK-side customization is appropriate. **Syntax:** ```typespec // Requires: using Azure.ClientGenerator.Core.Legacy; #suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "Change the base type back to <TargetBase> for backward compatibility" @@Azure.ClientGenerator.Core.Legacy.hierarchyBuilding(MyResource, Azure.ResourceManager.Foundations.TrackedResource, "csharp" ); ``` **Common target base types:** - `Azure.ResourceManager.Foundations.TrackedResource` — generates `TrackedResourceData` (for resources with location and tags) - `Azure.ResourceManager.Foundations.ProxyResource` — generates `ResourceData` (for proxy/child resources) - `Azure.ResourceManager.Foundations.Resource` — generates `ResourceData` (ARM resource base) **Legacy-only scenarios that require explicit approval:** - The old SDK had `MyData : ResourceData` or `MyData : TrackedResourceData`, the new TypeSpec-generated SDK produces `MyData : SomeOtherType` (e.g., a service-local `Resource` model), and the migration owner has explicitly rejected the normal MPG migration fix path. - The `CannotRemoveBaseTypeOrInterface` API compatibility violation remains after verifying resource-hierarchy parity and attempting the normal SDK-side customization approach. **Requirements:** 1. Add `using Azure.ClientGenerator.Core.Legacy;` to the `client.tsp` imports 2. Add `#suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "..."` before each `@@hierarchyBuilding` call 3. After adding the decorator, regenerate the SDK code **Legacy-approved example** (from KeyVault migration): ```typespec import "@azure-tools/typespec-client-generator-core"; using Azure.ClientGenerator.Core.Legacy; // Fix Vault resource to generate VaultData : TrackedResourceData #suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "Change the base type back to TrackedResource for backward compatibility" @@Azure.ClientGenerator.Core.Legacy.hierarchyBuilding(Vault, Azure.ResourceManager.Foundations.TrackedResource, "csharp" ); ``` ## WirePathAttribute Breaking Changes [MPG only] When the previous SDK version included `WirePathAttribute` on model properties (used by Azure.Provisioning libraries), migrating to TypeSpec may produce ApiCompat `CannotRemoveAttribute` errors for the missing attribute — because the emitter defaults to **not** generating it. ### How to detect - ApiCompat `CannotRemoveAttribute` errors referencing `WirePathAttribute` on model properties - These errors appear during `dotnet pack --no-restore` when the previous SDK release had `WirePathAttribute` on properties but the new generation does not ### Fix Add `enable-wire-path-attribute: true` to the **mgmt emitter options** in `tspconfig.yaml` (in the spec repo): ```yaml options: "@azure-typespec/http-client-csharp-mgmt": emitter-output-dir: "{output-dir}/{service-dir}/{namespace}" namespace: "Azure.ResourceManager.<Service>" enable-wire-path-attribute: true ``` Then regenerate the SDK. If the remaining ApiCompat diff is only `WirePathAttribute` removal, it is acceptable to add targeted entries to the centralized baseline file under `eng/apicompatbaselines/<Project>.xml`. Do not add SDK custom code just to restore `WirePathAttribute`; the maintenance cost is not worth it for this compatibility diff. Do not create a local `ApiCompatBaseline.txt`, do not baseline unrelated ApiCompat errors, and do not disable ApiCompat. ## Extension Resources Extension resources (deployed onto parent resources from different providers) require special handling. ### Parameterized Scopes When the same resource type can be deployed onto multiple parent types (e.g., VM, HCRP, VMSS), use `OverrideResourceName` with parameterized scopes. Each scope generates a separate SDK resource type. The generator may produce duplicate `GetXxxResource()` methods in `MockableArmClient` when multiple entries exist for the same resource — this is a known generator bug requiring deduplication. ### Sub-Resource Operations: Avoid `Read<>` / `Extension.Read<>` for Non-Lifecycle Ops When a sub-resource operation (e.g., getting a report under an assignment) uses `Read<>` or `Extension.Read<>` templates, the ARM library treats it as a **lifecycle read** operation. This causes: - The resource's `resourceType` and `resourceIdPattern` to be set to the sub-resource path - Collections using the wrong REST client - Compile errors: CS1729 (wrong constructor), CS0029 (type mismatch), CS1503 (wrong argument) **Fix**: Change sub-resource Get operations from `Read<>` to `ActionSync<>` (or `Extension.ActionSync<>` for extension resources) with a `@get` decorator: ```typespec // WRONG — ARM library treats this as lifecycle Read reportGet is Ops.Read<Resource, Response = ArmResponse<Report>, ...>; // CORRECT — ActionSync with @get avoids lifecycle misclassification @get reportGet is Ops.ActionSync<Resource, void, Response = ArmResponse<Report>, ...>; ```
View on GitHub