C# Language Modernization Skill
Upgrade C# source code to use language features available in a target C# version. This skill handles both single-version upgrades and multi-version jumps (e.g., C# 7.0 → C# 13), applying features cumulatively from each intermediate version.
The workflow is designed to minimize token usage: mechanical changes are batched through
dotnet format (zero LLM tokens), while the LLM focuses on judgment-heavy transformations
that require semantic understanding.
Entry points
This skill supports two invocation modes:
Standalone invocation
The user directly asks to modernize C# code. The skill handles everything: version detection, assessment, transformation, and reporting.
Sub-step invocation (from parent scenario)
A parent scenario (e.g., TFM upgrade) invokes this skill with a known version range. When the version range is already determined by the caller, skip Step 1 (version detection) and begin at Step 2 (load references). The parent scenario provides:
- Source C# version — the version the code currently uses
- Target C# version — the version to modernize to
- Scope preference — conservative / default / aggressive (if already known)
Version ↔ .NET Target Framework mapping
| C# Version | .NET TFM | LangVersion |
|---|---|---|
| 7.0 | .NET Framework 4.7+ / .NET Core 2.0 | 7 |
| 7.1–7.3 | .NET Core 2.0–2.1 | 7.1–7.3 |
| 8.0 | .NET Core 3.x | 8.0 |
| 9.0 | .NET 5 | 9.0 |
| 10.0 | .NET 6 | 10.0 |
| 11.0 | .NET 7 | 11.0 |
| 12.0 | .NET 8 | 12.0 |
| 13.0 | .NET 9 | 13.0 |
| 14.0 | .NET 10 | 14.0 |
| 15.0 | .NET 11 | 15.0 |
Step 1: Determine the upgrade range
Skip this step if invoked as a sub-step with a known version range.
Identify the current C# version and the target C# version.
How to detect current version:
- Check the
.csprojfor<LangVersion>— if present, that's the version. - If absent, infer from
<TargetFramework>:net6.0→ C# 10,net7.0→ C# 11,net8.0→ C# 12,net9.0→ C# 13,net10.0→ C# 14,net11.0→ C# 15netcoreapp3.x→ C# 8,net5.0→ C# 9net48/net472etc. (Framework) → C# 7.3 (unless LangVersion overridden)
- If ambiguous, ask the user.
How to determine target version:
- If the user specifies a version, use it.
- If they say "latest" or "modernize", use the version implied by their TFM.
- If they're also upgrading TFM, use the new TFM's default.
Step 2: Load applicable reference files
Read ALL reference files from (current_version + 1) through target_version, inclusive.
Also read dotnet-format-rules.md which maps IDE analyzer rules to features.
For example, upgrading from C# 9 → C# 12 means reading:
csharp-10.mdcsharp-11.mdcsharp-12.mddotnet-format-rules.md
Reference files are located in the same directory as this skill:
csharp-7.md (covers 7.0 through 7.3)
csharp-8.md
csharp-9.md
csharp-10.md
csharp-11.md
csharp-12.md
csharp-13.md
csharp-14.md
csharp-15.md
dotnet-format-rules.md
Read the files using the view tool before scanning any code.
Step 3: Review breaking changes
Each reference file includes a Breaking changes section listing compiler breaking changes introduced in that C# version's SDK range. Before transforming code, review these for every version in the upgrade range.
Breaking changes can cause:
- Compile errors in code that previously compiled (new diagnostics, keyword restrictions)
- Behavioral changes (different overload resolution, new warnings)
- Keyword conflicts (e.g.,
fieldin C# 14,extensionin C# 14,with()in C# 15)
If the codebase contains patterns affected by breaking changes, fix them before applying modernizations — either in the assessment report (Step 5) as a prerequisite, or as the first action in Phase 2.
Step 4: Feature tiers
Every feature in the reference files is categorized into one of three tiers:
🟢 ALWAYS-APPLY (Safe modernizations)
Syntactic simplifications with no semantic risk. Don't change public API surface, don't alter runtime behavior. Apply in every upgrade unless the user explicitly opts out.
Many of these have dotnet format fixers (marked with IDE rule IDs in the reference files).
🟡 RECOMMEND (Good defaults, flag for review)
Improve code quality but involve judgment calls — may change API contracts, alter class
hierarchies, or require broader refactoring. Applied by default but flagged in the assessment
for review. Some have dotnet format fixers (e.g., IDE0290 for primary constructors).
🔴 OPT-IN (Only when explicitly requested)
Major features that affect architecture, require project-wide enabling, or have significant trade-offs. Only applied when the user specifically asks.
Step 5: Discovery scan and assessment
This is the critical token-saving step. Before transforming any code, produce an assessment report that the user can review. The assessment uses cheap operations (grep, file listing, compiler diagnostics) to identify what needs changing without reading every file into context.
5a: Quick scan with grep/find
Use shell commands to find files with modernizable patterns. Do NOT read file contents into context yet. Examples:
# File-scoped namespaces: files with block-scoped namespace (C# 10)
grep -rl "^namespace " --include="*.cs" src/ | head -50
# Target-typed new candidates (C# 9) — lines with redundant type on new
grep -rn "= new [A-Z].*();" --include="*.cs" src/ | head -30
# Using blocks that could be declarations (C# 8)
grep -rn "using (" --include="*.cs" src/ | head -30
# Null checks that could use 'is not null' (C# 9)
grep -rn "!= null" --include="*.cs" src/ | head -30
# Old-style switch statements (C# 8 switch expressions)
grep -rn "switch (" --include="*.cs" src/ | head -30
# Array/list initializations that could use collection expressions (C# 12)
grep -rn "new List<\|new \[\]\|Array\.Empty" --include="*.cs" src/ | head -30
# ESC character literals (C# 13)
grep -rn '\\x1[Bb]\|\\u001[Bb]' --include="*.cs" src/ | head -20
# Escaped strings that might benefit from raw literals (C# 11)
grep -rn '\\\"' --include="*.cs" src/ | head -20
# Variables named 'field' in property accessors — breaking change in C# 14
grep -rn 'field' --include="*.cs" src/ | head -20
# Collection expressions with with() calls — potential C# 15 breaking change
grep -rn 'with(' --include="*.cs" src/ | head -20
Adapt grep patterns to the actual version range — skip patterns for versions not in scope. These counts feed the assessment without consuming tokens on file content.
5b: Optionally run dotnet format in dry-run mode
If the project builds successfully, run dotnet format in verify mode to get an exact count
of what it can fix:
dotnet format <solution.sln> --verify-no-changes --severity info \
--diagnostics IDE0090 IDE0063 IDE0161 IDE0083 IDE0300 IDE0301 IDE0306 \
2>&1 | tail -20
The output lists every file that would be changed and which diagnostic triggered it.
5c: Write the assessment file
Write a csharp-language-modernization-assessment.md file to the project root (or working directory).
This file serves as both the plan for the user to review and the execution checklist for
subsequent steps.
Assessment file structure:
# C# Modernization Assessment
**Project:** {solution/project name}
**Current version:** C# {X}
**Target version:** C# {Y}
**Date:** {date}
## Summary
| Category | Est. files | Method |
|----------|-----------|--------|
| ⚠️ BREAKING CHANGES | ~{N} | Fix manually — must resolve first |
| 🟢 ALWAYS-APPLY (dotnet format) | ~{N} | `dotnet format` — automated |
| 🟢 ALWAYS-APPLY (LLM-only) | ~{N} | LLM — no fixer available |
| 🟡 RECOMMEND | ~{N} | LLM — flagged for review |
| 🔴 OPT-IN (not applied) | ~{N} | Not applied unless requested |
## Phase 0: Breaking changes (must resolve first)
These are compiler breaking changes that may cause build failures or behavioral changes
when upgrading the C# version. Fix before applying any modernizations.
| Breaking change | C# Ver | Est. files | Severity | Mitigation |
|----------------|--------|-----------|----------|------------|
| {description} | {ver} | ~{N} | error/warning/behavior | {fix} |
## Phase 1: dotnet format (automated, zero LLM tokens)
These changes are mechanical and handled entirely by Roslyn analyzers.
### .editorconfig additions:
{generated .editorconfig snippet — see dotnet-format-rules.md}
### Diagnostics to apply:
| IDE Rule | Feature | Est. files | Tier |
|----------|---------|-----------|------|
| IDE0161 | File-scoped namespaces | ~{N} | 🟢 |
| IDE0090 | Target-typed `new` | ~{N} | 🟢 |
| IDE0083 | `is not null` pattern | ~{N} | 🟢 |
| IDE0300–0306 | Collection expressions | ~{N} | 🟢 |
| IDE0290 | Primary constructors | ~{N} | 🟡 |
| ... | ... | ... | ... |
### Command:
dotnet format {solution} --severity info --diagnostics {IDs}
## Phase 2: LLM transformations
These require semantic understanding — no automated fixer exists.
### 🟢 ALWAYS-APPLY (LLM-only, no fixer)
| Feature | C# Ver | Est. files | Detection signal |
|---------|--------|-----------|-----------------|
| Raw string literals | 11 | ~{N} | Escaped quotes in strings |
| List patterns | 11 | ~{N} | Length/index checks |
| ... | ... | ... | ... |
### 🟡 RECOMMEND (LLM, flagged for review)
| Feature | C# Ver | Est. files | Trade-off |
|---------|--------|-----------|-----------|
| Record conversion | 9 | ~{N} | Changes equality semantics |
| init-only setters | 9 | ~{N} | Blocks post-construction mutation |
| Global usings | 10 | ~{N} | Reduces per-file explicitness |
| required members | 11 | ~{N} | Must verify framework support |
| ... | ... | ... | ... |
## Phase 3: Opt-in (not applied unless requested)
| Feature | C# Ver | Impact | Notes |
|---------|--------|--------|-------|
| Nullable reference types | 8 | ~{N} files | Project-wide, many warnings |
| Generic math interfaces | 11 | ~{N} types | Reshapes type hierarchy |
| ... | ... | ... | ... |
## Recommended execution order
0. Phase 0 → fix breaking changes → build → commit
1. Phase 1 → build → test → commit (mechanical changes)
2. Confirm Phase 2 scope with user
3. Phase 2 → build → test → commit (LLM changes)
4. Phase 3 if requested → build → test → commit
Step 6: Handle user scope preferences
The user may indicate scope in several ways:
- "Apply all features" / "go aggressive" → Apply ALWAYS + RECOMMEND + OPT-IN. Include 🟡 tier IDE rules (like IDE0290) in the dotnet format pass too.
- "Just safe changes" / "conservative" → Apply only ALWAYS-APPLY (both format and LLM). Exclude 🟡 tier IDE rules from dotnet format.
- "Upgrade to C# 12" (no qualifier) → Default: ALWAYS + RECOMMEND. Include 🟡 tier IDE rules in dotnet format where available.
- "Enable NRT" or names a specific feature → Apply that feature (even if OPT-IN) plus all ALWAYS-APPLY changes.
- "What can I modernize?" → Discovery mode. Run Steps 1–5 only, produce the assessment file, stop. Don't transform anything.
After the assessment is written, present the summary to the user and ask them to confirm scope before proceeding to execution — unless they've already indicated a clear preference.
Step 7: Execute Phase 1 — dotnet format
7a: Prepare the .editorconfig
Check if an .editorconfig already exists in the project/solution root.
- If it exists: Merge the needed rules. Don't overwrite existing settings — only add rules for features the project doesn't already configure. Note any conflicts in the assessment.
- If it doesn't exist: Create a minimal
.editorconfigwith the required rules.
Use dotnet-format-rules.md to look up the exact option names and values. Only
include rules for versions in the upgrade range (source+1 through target).
7b: Run dotnet format
dotnet format <solution.sln> --severity info --diagnostics <IDs>
Build the diagnostic ID list from dotnet-format-rules.md, filtered to the
relevant version range and the appropriate tier based on user scope preference.
7c: Build and verify
dotnet build <solution.sln>
If build fails after dotnet format, inspect errors. Most common issues:
- Collection expressions producing type ambiguity → revert those specific changes
- Primary constructors conflicting with partial classes → revert
- Expression-bodied members exceeding readability for complex bodies → revert
7d: Update the assessment file
Mark Phase 1 items as complete. Record actual change counts from dotnet format output.
Step 8: Execute Phase 2 — LLM transformations
Now read and transform files that need LLM judgment. Process files one-at-a-time to minimize context window usage.
Processing order
- Work through features in ascending C# version order (older first) — some later features build on earlier ones.
- Within a version, process 🟢 ALWAYS-APPLY first, then 🟡 RECOMMEND.
- Group related files when possible (e.g., all record candidates together).
For each file
- Read the file content into context.
- Apply all applicable transformations from the loaded reference files.
- Follow "when NOT to apply" guidance from the reference files.
- For 🟡 RECOMMEND changes, note what changed and why in the assessment file.
- Write the transformed file.
- Release — don't keep old file content in context for subsequent files.
Preserve semantics
Never change runtime behavior. If a transformation is ambiguous (e.g., a class looks like a record candidate but has mutable state and side-effectful methods), skip it and note why in the assessment file under a "Skipped" section.
Step 9: Final report
Update the assessment file with execution results:
## Execution results
### Phase 1 (dotnet format): ✅ Complete
- {N} files modified
- Diagnostics applied: {list}
### Phase 2 (LLM): ✅ Complete
| Feature | Files changed | Notes |
|---------|--------------|-------|
| Raw string literals | 4 | JSON templates in Services/ |
| Record conversion | 3 | PersonDto, OrderDto, AddressDto |
| ... | ... | ... |
### Skipped (with reasons)
| File | Feature | Reason |
|------|---------|--------|
| OrderService.cs | Primary constructor | Complex init, 12 dependencies |
| MathHelper.cs | Generic math | Would reshape type hierarchy |
### Not applied (opt-in)
- Nullable reference types: ~47 files affected. Enable with `<Nullable>enable</Nullable>`.
Important considerations
Feature interactions across versions
Some features from later versions enhance earlier-version features. Apply them together:
- C# 9
is not null+ C# 11 list patterns → richer pattern matching chains - C# 10 file-scoped namespaces + C# 12 primary constructors → dramatically shorter files
- C# 9 records + C# 10 record structs → complete value-type story
Don't over-modernize
Not every piece of code benefits from every feature. The reference files include "when NOT to apply" guidance for each feature — follow it.
Respect existing code style
Check for an existing .editorconfig that may encode team preferences. If the codebase
consistently uses a pattern (e.g., explicit types over var), don't force modernization on
style-preference features unless the user says to.
NRT is special
Nullable reference types are always 🔴 OPT-IN. If the user asks to "modernize to C# 8+" and doesn't mention NRT, note it in the assessment but don't enable it.
Commit strategy
Recommend the user commit in phases for clean git history and easy revert boundaries:
- Commit 1:
.editorconfigchanges +dotnet formatresults (mechanical) - Commit 2: LLM-driven 🟢 ALWAYS-APPLY changes
- Commit 3: LLM-driven 🟡 RECOMMEND changes
- Commit 4: Any 🔴 OPT-IN changes (if requested)