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).
58
66%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.github/skills/mitigate-breaking-changes/SKILL.mdPatterns 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.
Trigger phrases: "mitigate breaking changes", "fix breaking change", "customization patterns", "how to keep backward compat", "CodeGenType", "CodeGenSuppress", "markAsPageable", "hierarchyBuilding", "base type change".
Use Custom/*.cs or Customization/*.cs partial classes (follow the package's existing structure) for .NET-side fixes.
Keep custom code types split into separate files:
.cs file.src/Custom/Models/MyModel.cs for public partial class MyModel.Models/ for model customizations and Extensions/ for extension customizations when those folders exist.Compatibility.cs; split them so each file name aligns with the class name.// 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 typeWhen 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:
// 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.
These decorators are added to client.tsp in the spec repo.
// 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"@@markAsPageableWhen 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:
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:
using Azure.ClientGenerator.Core.Legacy; to the client.tsp imports#suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "migration" before each @@markAsPageable call[CodeGenSuppress] + SinglePagePageable wrapper code@@alternateType DecoratorWhen the spec uses older common types that generate incorrect C# types (e.g., string instead of ResourceIdentifier for ID properties), use @@alternateType:
@@alternateType(MyModel.resourceId, Azure.ResourceManager.CommonTypes.ArmResourceIdentifier, "csharp");@@hierarchyBuilding Decorator — Legacy base-type overrideDo 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:
// 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:
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.CannotRemoveBaseTypeOrInterface API compatibility violation remains after verifying resource-hierarchy parity and attempting the normal SDK-side customization approach.Requirements:
using Azure.ClientGenerator.Core.Legacy; to the client.tsp imports#suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "..." before each @@hierarchyBuilding callLegacy-approved example (from KeyVault migration):
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"
);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.
CannotRemoveAttribute errors referencing WirePathAttribute on model propertiesdotnet pack --no-restore when the previous SDK release had WirePathAttribute on properties but the new generation does notAdd enable-wire-path-attribute: true to the mgmt emitter options in tspconfig.yaml (in the spec repo):
options:
"@azure-typespec/http-client-csharp-mgmt":
emitter-output-dir: "{output-dir}/{service-dir}/{namespace}"
namespace: "Azure.ResourceManager.<Service>"
enable-wire-path-attribute: trueThen 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 (deployed onto parent resources from different providers) require special handling.
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.
Read<> / Extension.Read<> for Non-Lifecycle OpsWhen 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:
resourceType and resourceIdPattern to be set to the sub-resource pathFix: Change sub-resource Get operations from Read<> to ActionSync<> (or Extension.ActionSync<> for extension resources) with a @get decorator:
// 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>, ...>;0afb185
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.