Migration logic for Azure SDK for .NET data-plane libraries migrating from AutoRest/Swagger to TypeSpec-based generation. Uses MCP tools from the generator-agent server for automated deterministic fixes.
61
72%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Fix and improve this skill with Tessl
tessl review fix ./.github/skills/dpg-migration/SKILL.mdData-plane migration workflow for Azure SDK for .NET libraries.
The workflow uses MCP tools from the generator-agent server to automate all deterministic, rule-based fixes (field renames, missing usings, type pattern replacements, nullable annotations, etc.) so the LLM only reasons about non-deterministic errors.
Trigger phrases: "migrate service X", "data-plane migration", "dpg migration", "migrate to TypeSpec", "swagger to TypeSpec migration", "upgrade generator", "generator migration help", "migrate with MCP tools", "use generator-agent tools", "tool-assisted migration", "MCP migration", "automated build fix".
This skill requires the SDK repository and the spec repo:
| Path | Purpose | Required? |
|---|---|---|
Current repository (azure-sdk-for-net) | Azure SDK for .NET mono-repo. Data-plane SDK packages typically live under sdk/<service>/<package>/. | Always |
Sibling spec folder (../azure-rest-api-specs) | Full or sparse-checkout of the Azure REST API Specs repo. Used for spec-side changes (client.tsp, tspconfig.yaml edits) and as the local spec path for code generation after any spec modification. | Always |
Sibling TypeSpec folder (../typespec) | Clone of the microsoft/typespec repo. | Only for diagnosing generator bugs |
The local spec repo path (localSpecsPath) is always required — ask the user for it at the start if not found at ../azure-rest-api-specs. Code generation starts in remote mode (using the commit pinned in tsp-location.yaml) and switches to local mode (/p:LocalSpecRepo=<path>) only after client.tsp or tspconfig.yaml has been modified locally. Once in local mode, all subsequent generation calls use the local spec path — see Generation Mode for details.
The MCP server provides deterministic fix tools for the build-fix cycle. Configure in .vscode/mcp.json:
{
"servers": {
"generator-agent": {
"command": "dotnet",
"args": ["run", "--project", "sdk/tools/Azure.GeneratorAgent/src/Azure.GeneratorAgent.csproj", "--framework", "net10.0"]
}
}
}Available MCP Tools:
| Tool | When to Use | What It Does |
|---|---|---|
discover_project | First step of any migration — call before all other tools | Returns comprehensive project context: detected plane, package name, service name, emitter config, tsp-location.yaml fields, custom code folder, API surface files, and specsRepoPath (auto-detected at ../azure-rest-api-specs sibling of SDK repo root, null if not found — ask the user for the path when null). |
build_and_classify | First step of every build-fix iteration | Runs dotnet build, parses output, classifies each error as deterministic or requires-reasoning |
batch_fix | After build_and_classify returns deterministic errors | Applies multiple deterministic fixes in one call |
regex_replacement | Field renames, type pattern replacements | Regex find/replace in a file |
add_using_directive | CS0246/CS0103 for a known type (47 type→namespace mappings) | Adds a missing using directive |
remove_using_directive | CS0246 for *.Rest.* or Autorest.* namespaces | Removes using directives matching a pattern |
nullable_annotation_fix | CS8625 or CS8600 errors | Adds ? nullable annotation on the error line |
rename_codegen_type | CS0246 for mismatched *ModelFactory or *ClientBuilderExtensions | Updates [CodeGenType] attribute to match generated type |
fetch_to_fromlro | CS0103/CS1061 for Fetch method calls in custom LRO code | Replaces Fetch(response) with Model.FromLroResponse(response) |
add_codegen_suppress | CS0111 (duplicate member definition from custom + generated clash) | Adds [CodeGenSuppress] attribute to custom partial class, scanning Generated/ for member signature |
classify_errors | Re-classify errors after partial fixes | Classifies errors against the deterministic fix registry |
validate_tsp_config | Before code generation (if not already validated by commit_iteration) | Validates tspconfig.yaml emitter section: checks @azure-typespec/http-client-csharp key exists, required fields present (emitter-output-dir, namespace, model-namespace), and no invalid properties. Works on file path or string content. |
commit_iteration | At migration start and whenever code generation fails with a spec-level error. | Finds a valid spec commit remotely. Supports strict (commitOverride), auto-resolve (iterates forward), and fallback (fixes tspconfig.yaml locally when localSpecsPath is provided — no git commit needed). Returns useLocalSpecs=true when tspconfig was fixed locally. See Phase 2 for details. |
pregen_cleanup | Before first code generation | Removes IncludeAutorestDependency from .csproj |
snapshot_generated | After code generation, before build-fix cycle | Takes SHA-256 snapshot of all Generated/ files. Enables auto-verification in build_and_classify. |
verify_generated_unchanged | After build-fix cycle completes | Compares Generated/ files against snapshot. Reports violations (modified/deleted/added). Set autoRevert=true to auto-restore via git checkout. |
migrate_test_samples | After src builds, before test build | Moves Generated/Samples/ to Samples/ |
finalize_migration | After ALL builds succeed | Runs Export-API.ps1 and Update-Snippets.ps1 |
run_tests | After build succeeds, to verify tests pass | Runs dotnet test --no-build with configurable filter (defaults to excluding live tests) |
The discover_project tool auto-resolves most variables below — it is called in Phase 0 as the first step of any migration. This skill applies only when Plane = dpg.
For the purposes of diagnosing generator bugs, the data-plane emitter is located in this repo under eng/packages/http-client-csharp/. The base emitter is in the microsoft/typespec repo under packages/http-client-csharp/.
| Variable | Example | discover_project field | Description |
|---|---|---|---|
LIBRARY_PATH | sdk/communication/Azure.Communication.Messages | LibraryPath | Relative path to the SDK package directory |
PACKAGE_NAME | Azure.Communication.Messages | PackageName | Full NuGet package / directory name |
SERVICE_NAME | communication | ServiceName | The folder name immediately after sdk/ |
EMITTER_PACKAGE_JSON_PATH | see below | EmitterPackageJsonPath | Path to the emitter package.json |
| Plane | dpg | Plane | Should resolve to dpg for this skill |
SPECS_REPO_PATH | ../azure-rest-api-specs | SpecsRepoPath | Absolute path to local spec clone (null if not found — ask user) |
| Plane | EMITTER_PACKAGE_JSON_PATH | Target emitter in tspconfig.yaml |
|---|---|---|
| DPG | eng/azure-typespec-http-client-csharp-emitter-package.json | azure-typespec/http-client-csharp |
The following variables come from tsp-location.yaml (returned in discover_project's TspLocationFields when the file exists):
| Variable | Example | TspLocationFields key | Description |
|---|---|---|---|
SPEC_DIR | specification/<service>/<data-plane-spec-dir> | directory | TypeSpec directory in azure-rest-api-specs |
SPEC_COMMIT | 37052dfa3f... | commit | Commit SHA from the spec repo |
The following variable is not auto-resolved by discover_project — determine it from the spec:
| Variable | Example | Description |
|---|---|---|
API_VERSION | 2025-01-01 | Target API version from the spec |
Migrations often span multiple sessions and machines. To enable seamless pickup, a migration-status.md file is stored in the SDK package directory and committed to the migration branch.
{LIBRARY_PATH}/migration-status.mdExample: sdk/communication/Azure.Communication.Messages/migration-status.md
# Migration Status — {PACKAGE_NAME}
**Tracking Issue:** [#{ISSUE_NUMBER}](https://github.com/Azure/azure-sdk-for-net/issues/{ISSUE_NUMBER})
**Last Updated:** {DATE}
## PRs
| PR | URL | Status |
|----|-----|--------|
| **Spec** | {URL or "Not created"} | {Draft/Open/Merged} |
| **SDK** | {URL or "Not created"} | {Draft/Open/Merged} |
| **Generator** | {URL or "N/A"} | {Draft/Open/Merged/N/A} |
## Branches
| Repo | Branch | Fork Remote |
|------|--------|-------------|
| azure-sdk-for-net | `{branch}` | `{remote}` |
| azure-rest-api-specs | `{branch}` | `{remote}` |
## Phase Tracker
**Status legend:** ✅ Done | 🔄 In Progress | ❌ Blocked | ⏭️ Not Started
| Phase | Status | Notes |
|-------|--------|-------|
| Phase 0 — Sync & Resume | {status} | |
| Phase 1 — Discovery & Planning | {status} | |
| Phase 2 — Create/Update tsp-location.yaml | {status} | |
| Phase 3 — Handle Legacy Configuration | {status} | |
| Phase 4 — Update Custom Code | {status} | |
| Phase 5 — Code Generation | {status} | |
| Phase 6 — Build-Fix Cycle | {status} | |
| Phase 7 — Changelog | {status} | |
| Phase 8 — Test Project Build | {status} | |
| Phase 9 — Test Execution | {status} | |
| Phase 10 — Finalization | {status} | |
| Phase 11 — Create Pull Requests | {status} | |
| Phase 12 — Verify and Summarize | {status} | |
## ApiCompat Baseline Summary
| Error Type | Count | Action |
|-----------|-------|--------|
| MembersMustExist | {N} | Fix with custom code |
| TypesMustExist | {N} | Fix with @@clientName |
| CannotChangeAttribute | {N} | Acceptable to baseline |
| ... | | |
## Known Issues
- {issue description and link}
## Next Steps
1. {next action}Commit and push migration-status.md to the migration branch at these points:
migration-status.md (or update if resuming)git add {LIBRARY_PATH}/migration-status.md
git commit -m "Update migration status for {PACKAGE_NAME}"
git pushRemove migration-status.md from the branch when the migration is complete (Phase 12). It should not be included in the final PR merge to main.
Before any migration work:
Call discover_project with the absolute path to the SDK package directory. This returns the detected plane, package name, service name, emitter config, tsp-location.yaml fields, custom code folder, API surface files, and SpecsRepoPath — all in one call. If SpecsRepoPath is null, ask the user for the path to their local azure-rest-api-specs clone.
{LIBRARY_PATH}/migration-status.md exists on the current branch.
Merge the latest main branch into all repos.
Create migration-status.md with all phases marked ⏭️ (or update existing with Phase 0 marked ✅). Commit and push to the migration branch.
Use explore agents in parallel:
../azure-rest-api-specs/specification/<service>/ for main.tsp / tspconfig.yaml. Determine TypeSpec vs Swagger.tsp-location.yaml (already migrated, returned in discover_project's HasTspLocation) or src/autorest.md (legacy Swagger, returned in HasAutorestMd)..tsp files for @clientName("...", "csharp") and @@clientName decorators. Check for back-compatible.tsp.api/<PACKAGE_NAME>.net*.cs (listed in discover_project's ApiSurfaceFiles) — extract all public type names for later rename resolution.src/autorest.md, extract rename-mapping and prepend-rp-prefix entries.Custom/, Customization/, or Customized/ (returned in discover_project's CustomCodeFolder).If tsp-location.yaml exists with a commit and directory: Proceed directly to Phase 2 — the commit_iteration tool will validate the spec remotely. Do NOT check the local spec repo or read local tspconfig.yaml.
If no tsp-location.yaml exists (fresh migration): Search ../azure-rest-api-specs/specification/<service>/ for main.tsp / tspconfig.yaml to determine the spec directory. Also inventory existing csharp customizations in spec (search .tsp files for @clientName("...", "csharp") and @@clientName decorators, check for back-compatible.tsp).
Present a summary plan and ask the user to confirm.
After confirmation, update migration-status.md to mark Phase 1 as ✅ and save.
tsp-location.yamlGoal: Point the library at the correct spec and emitter.
{LIBRARY_PATH}/tsp-location.yaml exists.
repo, directory, and commit fields. Skip to step 3.tsp-location.yaml with the following fields:
directory: specification/<service>/<data-plane-spec-dir>
commit: <latest commit SHA from azure-rest-api-specs main>
repo: Azure/azure-rest-api-specs
cleanup: true
emitterPackageJsonPath: <value from discover_project's EmitterPackageJsonPath>directory by searching ../azure-rest-api-specs/specification/<service>/ for a TypeSpec project (main.tsp + tspconfig.yaml) that targets the same service.emitterPackageJsonPath is set to the correct plane-specific value (from discover_project's EmitterPackageJsonPath). Update it if missing or incorrect — this field is required for code generation to use the right emitter.commit field with the resolved SHA.directory path no longer exists, search for TypeSpec projects with similar service names.Call commit_iteration to resolve the correct spec commit. The tool handles all validation and iteration internally.
commitOverride. The tool validates strictly — if invalid, tell the user why. Do NOT iterate or fall back.sdkProjectPath, tspLocationPath, specsRelativeDirectory, and localSpecsPath. The tool auto-resolves by validating the current commit and iterating forward if needed.tspconfig.yaml locally and returns useLocalSpecs=true. Set gen_mode to local in session state — Phase 5 uses this to decide how to run code generation.Additional tsp-location.yaml fields:
directory must point to the folder containing main.tsp and tspconfig.yaml.additionalDirectories array for shared TypeSpec libraries.pregen_cleanup with the project path — removes <IncludeAutorestDependency>true</IncludeAutorestDependency> from .csproj files.If src/autorest.md exists:
namespace, title, require URL, output-folder, and rename directives.autorest.md after the TypeSpec generation path is working — git history preserves it.client.tsp in the SDK repo. The TypeSpec source lives in the spec repo.In all non-Generated .cs files under {LIBRARY_PATH}/src/:
add_using_directive — Add using Microsoft.TypeSpec.Generator.Customizations; where [CodeGen attributes are used.regex_replacement — Replace CodeGenClient → CodeGenTyperegex_replacement — Replace CodeGenModel → CodeGenTypeThese are also handled by MCP tools during build-fix, but can be applied before code generation:
regex_replacement — _pipeline → Pipeline (field → property)remove_using_directive — Remove using AutoRest.CSharp.Core;regex_replacement — _serializedAdditionalRawData → _additionalBinaryDataPropertiesregex_replacement — serializedAdditionalRawData → additionalBinaryDataPropertiesregex_replacement — FromCancellationToken(cancellationToken) → cancellationToken.ToRequestContext()Goal: Regenerate code with the new TypeSpec emitter.
Code generation operates in one of two modes:
SELECT value FROM session_state WHERE key = 'gen_mode';| Mode | When | Command |
|---|---|---|
| Remote (default) | Commit from tsp-location.yaml resolved successfully, no local spec edits made | dotnet build /t:GenerateCode |
| Local | After ANY local modification to client.tsp or tspconfig.yaml, or when commit_iteration returned useLocalSpecs=true | dotnet build /t:GenerateCode /p:LocalSpecRepo=<localSpecsPath> |
Transition rules:
client.tsp or tspconfig.yaml, or when commit_iteration returns useLocalSpecs=true. This is a one-way transition.-- Set at migration start (Phase 0):
INSERT OR REPLACE INTO session_state (key, value) VALUES ('gen_mode', 'remote');
-- After first spec edit or useLocalSpecs=true:
UPDATE session_state SET value = 'local' WHERE key = 'gen_mode';Pass localSpecsPath only when in local mode:
dotnet build /t:GenerateCodedotnet build /t:GenerateCode /p:LocalSpecRepo=<localSpecsPath>Always use dotnet build /t:GenerateCode. Do NOT use tsp-client update.
If generation fails, diagnose the cause:
Customization file issue: Check if a customization file is causing the failure. Temporarily remove or fix it, then re-run. If it still fails with no customizations → generator bug.
Spec-level error (e.g., invalid-schema, duplicate-example-file, or any TypeSpec compiler error that is not a C# build error):
commit_iteration (no commitOverride, always pass localSpecsPath) — it finds the next valid commit.commit_iteration fixes tspconfig.yaml locally and returns useLocalSpecs=true. Switch to local mode and use /p:LocalSpecRepo=<localSpecsPath> for all subsequent generation.After generation:
src/Generated/ for output files — verify file contents changed, not just file names.git diff --stat to confirm the scope of changes. A typical migration touches hundreds of files with significant content changes.snapshot_generated with the project path to lock down the Generated/ directory. This enables auto-verification in every subsequent build_and_classify call — any accidental modification to Generated/ files will be detected and reverted automatically.Build errors, ApiCompat, tests, and API export are handled in their dedicated phases (Phase 6, 8, 9, 10).
Goal: Get the library to compile successfully through iterative error fixing.
You (the LLM) are the orchestrator — call MCP tools for deterministic fixes, reason about non-deterministic errors using the classification trees below.
| Macro | Command |
|---|---|
[GENERATE] | dotnet build /t:GenerateCode (remote mode) or dotnet build /t:GenerateCode /p:LocalSpecRepo=<localSpecsPath> (local mode — see Phase 5 Generation Mode) |
[TEST] | Call run_tests MCP tool (or dotnet test --no-build --filter "TestCategory!=Live" 2>&1 | Select-Object -Last 30) |
LOOP:
1. Call `build_and_classify` → builds and classifies ALL errors
2. IF zero errors → EXIT LOOP
3. IF error count is not decreasing after 3 iterations → escalate to user
4. Separate errors into deterministic and requires-reasoning buckets
5. Apply ALL deterministic fixes via `batch_fix`
6. For requires-reasoning errors, classify and apply fixes in batch
7. Regenerate based on what changed:
- Spec changes (client.tsp edits) → [GENERATE]
- Generator attribute changes → [GENERATE]
- Generator code changes → rebuild the data-plane emitter, then [GENERATE]
- Customization-only changes → no regeneration needed
- Stale custom file (e.g., custom `FooResource.cs` exists but
`Generated/FooResource.cs` was renamed to `BarResource.cs` or removed) →
rename custom file to match the new generated name, or remove it if
the generated type no longer exists. Then [GENERATE].
- Error in Generated/ file with custom class present →
remove custom file, then [GENERATE] to isolate cause.
If error gone → custom file was the cause. Re-introduce incrementally.
If error persists with zero custom files → generator bug, STOP and report.
8. GOTO 1Max 10 iterations. If still failing, escalate to user.
Generated/ Auto-Guard: When a snapshot exists (taken in Phase 5), every
build_and_classifycall automatically verifies that no Generated/ files were modified. If violations are detected, they are auto-reverted viagit checkoutand reported in the build result undergeneratedGuard. Additionally,regex_replacement,add_using_directive,remove_using_directive, andnullable_annotation_fixwill refuse to modify files inside Generated/ directories.
Key decision points for regeneration:
[CodeGenType], [CodeGenSuppress], [CodeGenMember], or [CodeGenSerialization] attributes → [GENERATE] before rebuildingclient.tsp → switch to local mode (if not already), then [GENERATE] before rebuilding.cs code (no generator attributes) → no regeneration needed⛔ NEVER create or modify ApiCompatBaseline.txt to suppress ApiCompat errors. This is a hard rule — no exceptions.
⛔ NEVER remove or modify ApiCompatVersion from the .csproj to suppress these errors. The version must stay to enforce compatibility.
ApiCompat errors surface when dotnet build detects breaking changes vs the previously shipped API. Run dotnet pack --no-restore to get the full list. For each error, create a backward-compat shim in Custom/BackwardCompat/:
| ApiCompat Rule | What It Means | Fix |
|---|---|---|
MembersMustExist (constructor) | Protected constructor removed from abstract type | Add protected TypeName(params) : base(params) { } in Custom/BackwardCompat/<TypeName>.cs. Never edit Generated/ files. |
MembersMustExist (method/property) | Method signature changed (e.g., IReadOnlyDictionary → IDictionary) | Create forwarding overloads in Custom/BackwardCompat/<ClientName>.cs with the old IReadOnlyDictionary parameter type that convert and delegate to the new IDictionary method. Add #pragma warning disable AZC0002 if overloads lack CancellationToken. For async forwarding methods, always use ConfigureAwait(false) on the awaited call. Never edit Generated/ files. |
MembersMustExist (ModelFactory) | ModelFactory overload signature changed (usually a parameter's enum type was removed) | Add forwarding overload with old signature in Custom/BackwardCompat/<ModelFactoryName>.cs. Accept the removed enum param and discard it, delegating to the new method. Mark [EditorBrowsable(Never)]. If the removed param's type no longer exists, create a stub struct in MissingEnumTypes.cs. |
MembersMustExist (SerializedAdditionalRawData) | Protected field renamed to _additionalBinaryDataProperties | Re-declare the old field in Custom/BackwardCompat/<TypeName>.cs: protected internal IDictionary<string, BinaryData> SerializedAdditionalRawData; with #pragma warning disable SA1307 and SA1401. |
MembersMustExist (missing setter) | Property lost its setter | [CodeGenSuppress("Property")] + re-declare with { get; set; } |
MembersMustExist (missing enum value) | Enum value removed/renamed | Add [EditorBrowsable(EditorBrowsableState.Never)] deprecated value in custom partial |
MembersMustExist (property type changed) | Property type changed (e.g., custom enum → string) | Use [CodeGenSuppress("Property")] to suppress the generated property, then re-declare it in a custom partial class returning the old type. Create a stub type (e.g., a readonly struct with implicit operator from string) in MissingEnumTypes.cs, marked [EditorBrowsable(Never)]. Never edit Generated/ files. |
TypesMustExist | Type renamed/removed | @@clientName to restore old name, OR create stub type in custom code |
TypesMustExist (extension class renamed) | DI extension class renamed (e.g., AIFooClientBuilderExtensions → FooClientBuilderExtensions) | Create a stub public static class with the old name containing extension methods that delegate to the new class. Use global:: namespace qualifiers if the stub is inside Microsoft.Extensions.Azure (bare Azure.X resolves wrong). Copy any [RequiresUnreferencedCode]/[RequiresDynamicCode] attributes from the target method. |
CannotMakeMemberNonVirtual | virtual → non-virtual (abstract type) | Add protected constructor shim to keep type inheritable (check for serialization constructor conflicts first — see constructor row above) |
CannotSealType | Type effectively sealed (private ctor) | Same as above — add protected constructor to keep type inheritable |
File organization: Place backward-compat shims in Custom/BackwardCompat/, with each file named after the type it extends:
<TypeName>.cs — e.g., PersistentAgentsModelFactory.cs for ModelFactory shims, SomeClient.cs for client method shims, MessageDeltaTextAnnotation.cs for field renamesMissingEnumTypes.cs — stub types for removed enums (these don't map to a single generated type)Mark all backward-compat shims with:
[EditorBrowsable(EditorBrowsableState.Never)] — hide from IntelliSense#pragma warning disable AZC0002 if needed for missing CancellationTokenawait ... .ConfigureAwait(false) (library best practice)PersistentAgentsModelFactory.cs — When a ModelFactory method's old signature had an enum parameter that no longer exists (e.g., MessageDeltaChunkObject), create a forwarding overload that accepts the old enum and discards it:
// Custom/BackwardCompat/PersistentAgentsModelFactory.cs
#nullable disable
using System.ComponentModel;
namespace Azure.AI.Agents.Persistent
{
public static partial class PersistentAgentsModelFactory
{
// backward-compat: old overload had MessageDeltaChunkObject param that no longer exists
[EditorBrowsable(EditorBrowsableState.Never)]
public static MessageDeltaChunk MessageDeltaChunk(
string id,
MessageDeltaChunkObject @object, // old enum param — accepted but discarded
MessageDelta delta)
{
return MessageDeltaChunk(id: id, delta: delta);
}
}
}MessageDeltaTextAnnotation.cs — When the protected field SerializedAdditionalRawData was renamed to _additionalBinaryDataProperties:
// Custom/BackwardCompat/MessageDeltaTextAnnotation.cs
using System;
using System.Collections.Generic;
namespace Azure.AI.Agents.Persistent
{
public abstract partial class MessageDeltaTextAnnotation
{
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
#pragma warning disable SA1401 // Fields should be private
protected internal IDictionary<string, BinaryData> SerializedAdditionalRawData;
#pragma warning restore SA1401
#pragma warning restore SA1307
}
}SomeClient.cs async pattern — Always use ConfigureAwait(false) in async forwarding overloads:
#pragma warning disable AZC0002
public partial class SomeClient
{
[EditorBrowsable(EditorBrowsableState.Never)]
public virtual async Task<Response<Model>> CreateAsync(
IReadOnlyDictionary<string, string> metadata,
CancellationToken cancellationToken)
{
return await CreateAsync(
(IDictionary<string, string>)(metadata?.ToDictionary(k => k.Key, k => k.Value)),
cancellationToken).ConfigureAwait(false);
}
}
#pragma warning restore AZC0002[CodeGenType] attribute mismatch: When a custom partial class uses [CodeGenType("X")], the value X must exactly match the generated class name. If the generated name changed during migration, the partial classes silently fail to merge — methods from both sides become invisible, producing dozens of spurious MembersMustExist errors. Always verify the generated class name before assuming individual methods are missing.
XML cref references break after signature changes: After changing method parameter types (e.g., IDictionary → IReadOnlyDictionary), XML doc comments in custom files that reference the old signatures via cref attributes produce CS1574 errors. Scan custom *.cs files for cref attributes containing the old type and update them.
ModelFactory class name mismatch: The generated ModelFactory class name may differ from what the custom partial class expects. If most ModelFactory methods show as MembersMustExist errors, check whether the generated and custom ModelFactory classes have matching names/[CodeGenType] attributes before creating individual shims.
The generator reads existing customization files ([CodeGenSuppress], [CodeGenType], [CodeGenMember], [CodeGenSerialization]) and produces different output based on them:
Generated/ are often caused by stale customization files, not generator bugs.[GENERATE].[CodeGenType], [CodeGenSuppress], [CodeGenMember], [CodeGenSerialization]), you MUST regenerate with [GENERATE] before rebuilding. The generator reads these attributes at generation time — building without regenerating will use stale generated code that doesn't reflect your customization changes.Given: error in file F with message M
1. IF F is under `src/Generated/`:
a. Find the corresponding customization file
b. Fix the customization to match new generated code
c. Run [GENERATE] then [BUILD]
d. If error persists after removing all customizations → generator bug
→ STOP: Do NOT suppress. Report to user with full details (see Generator Bug Reporting).
2. IF F is under custom code:
a. Renamed/missing type → update custom code or add rename
b. Internal type → use an accessibility fix in the spec or CodeGenType
3. IF error is structural in Generated/ with correct spec → generator bug
→ STOP: Do NOT suppress. Report to user with full details (see Generator Bug Reporting).These patterns are specific to data-plane migrations. Apply them during the shared skill's "Build-Fix Cycle" phase, in addition to the common customization patterns.
GeneratorPageableHelpers → CollectionResult patternGeneratorPageableHelpers.CreatePageable or similar, replace it with the corresponding generated CollectionResult type from the Generated/ folder.CollectionResult type exists in Generated/:
[CodeGenSuppress] attribute that suppresses the internal method which would create the CollectionResult.[CodeGenSuppress] attribute.CollectionResult type.CollectionResult types manually — they must be generated.ToRequestContent() removalRequestContent.foo.ToRequestContent() with just foo.using RequestContent content = details.ToRequestContent(); → using RequestContent content = details;using statement; only remove the .ToRequestContent() call.FromCancellationToken replacementRequestContext context = FromCancellationToken(cancellationToken);RequestContext context = cancellationToken.ToRequestContext();ModelFactory type namesModelFactory has a different name than the generated type ending in ModelFactory, update the CodeGenType attribute in the custom type to match the generated type name.ClientBuilderExtensions type namesClientBuilderExtensions has a different name than the generated type ending in ClientBuilderExtensions, update the CodeGenType attribute in the custom type to match the generated type name.Fetch methods are replaced by static FromLroResponse methods on the response models.ResponseModel.FromLroResponse(response) instead of calling Fetch methods.Fetch methods manually — call the generated FromLroResponse method.FromResponse method removalFromResponse methods have been removed from models.Response to the model type instead.var model = ModelType.FromResponse(response); → var model = (ModelType)response;Generated/.[CodeGenType("SpecName")] — Link custom class to generated counterpart.[CodeGenSuppress("MemberName", typeof(...))] — Suppress a generated member.[CodeGenMember("MemberName")] — Link custom property to generated counterpart.Custom/, Customization/, or Customized/.//) explaining why the customization exists (e.g., backward-compat shim for removed enum value, property rename from flattening change). This helps future maintainers understand the intent and decide whether the shim can be removed.<summary> on partial type declarations in custom code — Partial types in customization files do not need an XML doc summary on the type itself; the generated partial already carries the summary and it will be merged by the compiler. Adding a summary here risks leaking internal justification into the released NuGet package's XML documentation. Only add XML docs on new members (properties, methods) introduced in custom code.[EditorBrowsable(EditorBrowsableState.Never)] — Deprecated shim members (e.g., restored enum values, renamed properties) should be hidden from IntelliSense to guide users toward the new API surface.Further reading: C# Customization Guide, TypeSpec client customization.
Do NOT hand-edit metadata.json — it is auto-generated.
## <VERSION> (Unreleased)
### Features Added
- Migrated from Swagger to TypeSpec-based generationmigrate_test_samples with the project path — moves tests/Generated/Samples/ to tests/Samples/. Do NOT run [GENERATE] in tests.build_and_classify on the test project path — same build-fix loop as Phase 6, but for test code.
src/Generated/ for actual new type names before writing fixes.batch_fix for deterministic errors, reason about the rest.run_tests with the project path — runs dotnet test --no-build --filter "TestCategory!=Live" and returns structured pass/fail results.build_and_classify, then run_tests again.Call finalize_migration with the project path — runs Export-API.ps1 and Update-Snippets.ps1 automatically.
Or run manually from the repository root:
.\eng\scripts\Export-API.ps1 -ServiceDirectory SERVICE_NAME
.\eng\scripts\Update-Snippets.ps1 -ServiceDirectory SERVICE_NAMEOnce dotnet build passes, create separate PRs for each category:
| Category | Repository | PR needed? |
|---|---|---|
| Spec changes | azure-rest-api-specs | If any spec files were modified |
| Generator changes | azure-sdk-for-net | If generator code was fixed |
| SDK migration | azure-sdk-for-net | Always |
During the iteration loop, changes fall into three categories. Identify which ones apply by reviewing the modified files across both repositories.
../azure-rest-api-specs), create a branch and commit all spec changes.tsp format "*.tsp" in the TypeSpec project directory to ensure formatting passes CI validation.Azure/azure-rest-api-specs.Add csharp client customizations for <Service> migrationeng/packages/http-client-csharp/.Azure/azure-sdk-for-net.[Generator] Fix <description> for <Service> migrationeng/packages/http-client-csharp/eng/scripts/Generate.ps1).tsp-location.yaml with the final spec commit SHA from Step 2.LocalSpecRepo to verify CI-reproducible generation:
cd sdk\<service>\<PACKAGE_NAME>\src
dotnet build /t:GenerateCodedotnet build still passes.Azure/azure-sdk-for-net.<PACKAGE_NAME>: Migrate to TypeSpec (API version <API_VERSION>)After all PRs are created, report:
migration-status.md with all PR links and mark Phase 11 as done. Commit and push.CodeGenType attributes needing manual review.git diff before committing.pre-commit-checks skill.migration-status.md from the branch — it should not be included in the final merge to main:
git rm {LIBRARY_PATH}/migration-status.md
git commit -m "Remove migration status file — migration complete"
git pushBefore reporting a generator bug, ALWAYS:
[GENERATE].Do NOT manually fix Generated/ code. Do NOT silently suppress generator bugs with [CodeGenSuppress] or other workarounds.
When a generator bug is confirmed, you MUST stop and report the bug to the user in detail. Do NOT silently work around it with [CodeGenSuppress] + custom implementation. The user needs to understand what is broken so they can decide how to proceed (fix the generator, file an issue, or explicitly choose a workaround).
Required information to present to the user:
After presenting the bug details, ask the user how they want to proceed:
eng/packages/http-client-csharp/)[CodeGenSuppress] workaround only with the user's explicit approval1. CONFIRM it's a generator bug (zero custom files, clean regeneration, same errors)
2. REPORT the bug to the user with full details (see Generator Bug Reporting above)
3. WAIT for user decision before proceeding
4. IF user chooses to fix generator:
- Edit under eng/packages/http-client-csharp/
- Rebuild the emitter (`npm run build`)
- Regenerate the affected SDK with `dotnet build /t:GenerateCode`
- Clean up stale custom workarounds
- Run Generate.ps1 to verify no regressions
5. IF user explicitly approves workaround:
- [CodeGenSuppress] + custom implementation
- Add a code comment documenting the generator bug and linking to any filed issueNever edit files under Generated/ — they are overwritten by codegen. This rule is enforced deterministically at three levels:
regex_replacement, add_using_directive, remove_using_directive, and nullable_annotation_fix refuse to write to Generated/ paths.build_and_classify auto-verifies Generated/ file integrity every build when a snapshot exists (taken via snapshot_generated in Phase 5).verify_generated_unchanged with autoRevert=true after the build-fix cycle to catch any modifications made outside MCP tools (e.g., via edit, create, or powershell).Never hand-edit metadata.json — it is auto-generated.
Never use tsp-client update — use dotnet build /t:GenerateCode.
Never create or add entries to ApiCompatBaseline.txt — this file must never be used to bypass breaking changes. Always mitigate ApiCompat errors through spec-side fixes (@@clientName) or SDK custom code shims in Custom/BackwardCompat/ (see ApiCompat Error Handling in Phase 6 and error-reference.md). Do NOT suppress them via baseline files.
Never disable ApiCompat or package validation in .csproj — do NOT set <EnablePackageValidation>false</EnablePackageValidation>, <RunApiCompat>false</RunApiCompat>, or any other property that disables API compatibility checks. These checks exist to catch breaking changes and must always remain enabled. Fix the underlying breaking changes instead.
Never bump the major version of an Azure SDK package.
Preserve git history — prefer renames over delete+create.
Never manually edit files under src/Generated/ — this is strictly forbidden. All generated code must come from the generator. If generated code has a bug (e.g., references a non-existent method, wrong type), fix it through:
@@clientName, @@alternateType, @@access) in client.tsp[CodeGenSuppress] in src/Custom/ to suppress the broken member and provide a corrected replacementNote: [CodeGenSuppress] only takes effect when the custom files exist before code generation. After adding custom files, regenerate with dotnet build /t:GenerateCode so the generator reads them and honors the suppression.
During the build-fix loop, Copilot operates autonomously. These actions are permitted without user confirmation:
@@clientName, @@access, @@markAsPageable, @@alternateType, and other decorators to client.tsp — these are safe, reversible, and csharp-scoped.[CodeGenType]/[CodeGenSuppress]/[CodeGenMember] only when needed.autorest.md after extracting directives — git history preserves it.dotnet build /t:GenerateCode. Always regenerate after modifying custom files that contain generator attributes.These actions require explicit user approval (use ask_user):
.tsp files beyond client.tsp — e.g., changing main.tsp, model definitions, or operation signatures. These affect all languages, not just C#.Generate.ps1 to verify scope first.ApiCompatBaseline.txt entries — this should almost never be done.Proceed without asking the user except when:
Always update and push migration-status.md before ending a session, even if the migration is incomplete. This ensures the next session (possibly on a different machine) can resume from the exact point where work stopped.
tsp-location.yamlautorest.md if neededdotnet build, collect errors, populate SQL table.tsp-client update — use dotnet build /t:GenerateCode.metadata.json.autorest.md — only add @@clientName for names that actually cause build errors after generation. Check existing spec decorators to avoid duplicates.@@access) before custom code ([CodeGenType]).tsp-location.yaml with pushed spec commit before creating PR.CodeChecks.ps1 before pushing: pwsh eng\scripts\CodeChecks.ps1 -ServiceDirectory <service>localSpecsPath — ask at migration start, not mid-flow.client.tsp/tspconfig.yaml edit. Never mix modes within a migration.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.