# Modernizing .NET code with Roslyn and Droids

> .NET modernization with Roslyn pairs analyzers, code fixes, and dotnet format with Droids that handle what no code fix covers. How to move .NET Framework and older .NET code to .NET 10 with deterministic checks on every step, using Factory.

URL: https://factory.com/articles/dotnet-modernization-roslyn
Published: 2026-11-14
Updated: 2026-11-14

.NET modernization with Roslyn uses the compiler platform's analyzers and code fixes for the changes a machine can make the same way every time, and a coding agent for the changes that need judgment. Roslyn rules and `dotnet format` handle syntax modernization, style, and known API replacements. Droids, the coding agents in Factory, take the build failures, unsupported APIs, and test repairs that remain, and write new analyzers when a pattern repeats.

The timing matters. As of November 2026, the [.NET support policy](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core) lists .NET 10 as the active long-term support release through November 14, 2028. .NET 8 and .NET 9 both reached end of support on November 10, 2026, the day .NET 11 shipped as a standard-term release. Any estate still on .NET Framework, .NET 8, or older now has a supported target and a reason to move.

## The deterministic .NET toolchain in 2026

Roslyn is the .NET Compiler Platform. An analyzer inspects syntax trees and semantic models and reports a diagnostic with an ID. A code fix rewrites the code to resolve that diagnostic. The .NET SDK ships hundreds of these rules: `IDE` rules for code style, such as `IDE0161` for file-scoped namespaces and `IDE0090` for target-typed `new`, and `CA` rules for code quality and compatibility, such as `CA1416` for calls to platform-specific APIs. The same analyzer and code fix APIs target Visual Basic, and `dotnet format` runs against Visual Basic projects, so VB.NET code follows the same loop.

`dotnet format` is the batch runner for those rules. The [`dotnet format` reference](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format) documents three subcommands. `whitespace` applies formatting, `style` applies code style fixes, and `analyzers` applies fixes from analyzers, including third-party ones. Each accepts `--diagnostics` to limit the run to specific IDs and `--severity` to set the minimum level. `--verify-no-changes` makes any of them a check that exits non-zero when it would edit a file.

The project-level tools have changed. Microsoft now marks [.NET Upgrade Assistant](https://learn.microsoft.com/en-us/dotnet/core/porting/upgrade-assistant-overview) as officially deprecated and points users to its GitHub Copilot upgrade agent, which assesses a solution, plans the upgrade, applies changes, and validates the build. The `try-convert` tool for converting legacy project files to SDK-style was archived in May 2024. Moderne also publishes C# recipes for OpenRewrite under its proprietary license.

Factory has no built-in .NET converter or language mode. Droids run the SDK's own tools.

## Commands that make up the deterministic pass

Each command below ships with the .NET SDK. Run them from the solution directory with the target rules enabled in `.editorconfig`, for example `csharp_style_namespace_declarations = file_scoped:warning`.

<div className="not-prose my-6 flex flex-col gap-3 [&_button]:!w-auto">
  <div className="grid grid-cols-1 gap-y-5 md:grid-cols-[max-content_1fr] md:gap-x-4">
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Style pass.</strong> Converts to file-scoped namespaces and target-typed new across the solution.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Analyzer pass.</strong> Applies every available code fix for analyzer diagnostics at warning level or above.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Re-verify.</strong> Exits non-zero if any formatter, style, or analyzer fix would still change a file.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Build gate.</strong> Treats remaining warnings, including CA1416 platform warnings, as errors.</span>
  </div>
</div>

Commit the output of each `dotnet format` run on its own. A reviewer can confirm a style commit by rerunning the same command on the parent commit and comparing the result, which is far faster than reading thousands of mechanical edits.

## Where .NET modernization with Roslyn stops

Code fixes cover syntax and well-defined API replacements. A move to .NET 10 needs more than that.

Project files are the first layer. Legacy `.csproj` files list every source file and carry `packages.config` references, while SDK-style projects use globbing and `PackageReference`. Converting them is mechanical in principle, but each conversion needs a build to prove that nothing was dropped.

Runtime and framework changes are the second. ASP.NET applications depend on `System.Web` and `HttpContext`, which have no direct equivalent in ASP.NET Core. Microsoft's [ASP.NET Framework to ASP.NET Core guide](https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/) recommends incremental migration for most production apps for that reason. Code that relied on `BinaryFormatter`, whose implementation was removed in .NET 9, needs a new serialization choice. Windows-only APIs flagged by `CA1416` need either a platform guard or a replacement.

Those decisions are the Droid's work. It reads the build output, groups the failures by cause, and fixes one class at a time, so a commit that replaces `BinaryFormatter` does not also touch authentication middleware. For each choice with more than one reasonable answer, such as `System.Text.Json` versus a contract-compatible serializer for data already stored on disk, it writes the tradeoff into the pull request for the owner to confirm. Where coverage is thin, it adds characterization tests against the old build first, recording the current outputs for representative inputs. The same tests then run unchanged against the migrated code with `dotnet test`, so a behavior change surfaces as a failing assertion.

## How Factory runs the Roslyn loop

Factory is a platform for running a software factory, where Droids take work from a signal to a reviewed pull request. The loop for a .NET project keeps Roslyn in charge of every change it can make and the build in charge of deciding what is left.

Roslyn pass</span><br/><span style='color:#8A8380 !important'>dotnet format style and analyzers</span>"]
    B["<span style='color:#FAFAFA !important;font-weight:600'>Build and test gate</span><br/><span style='color:#8A8380 !important'>dotnet build -warnaserror, dotnet test</span>"]
    C{"<span style='color:#FAFAFA !important;font-weight:600'>Gate passes?</span>"}
    D["<span style='color:#FAFAFA !important;font-weight:600'>Droid handles residue</span><br/><span style='color:#8A8380 !important'>System.Web, removed APIs, new analyzers</span>"]
    E["<span style='color:#FAFAFA !important;font-weight:600'>Deterministic re-run</span><br/><span style='color:#8A8380 !important'>dotnet format --verify-no-changes, full tests</span>"]
    F["<span style='color:#FAFAFA !important;font-weight:600'>Human review</span><br/><span style='color:#8A8380 !important'>tool commits and agent commits</span>"]
    A --> B --> C
    C -- no --> D --> E
    C -- yes --> E
    E --> F
`}
/>

**Formatting on every edit.** Factory [hooks](https://docs.factory.com/harness/hooks) run shell commands at lifecycle events. A `PostToolUse` hook on `Edit`, `Create`, and `ApplyPatch` can run `dotnet format --verify-no-changes --include` on the file Droid just changed. When the script exits with code `2`, its stderr goes back to Droid as feedback, so a style or analyzer violation is fixed before the next edit rather than at review time.

```json title=".factory/hooks.json"
{
  "PostToolUse": [
    {
      "matcher": "Edit|Create|ApplyPatch",
      "hooks": [
        {
          "type": "command",
          "command": "\"$FACTORY_PROJECT_DIR\"/.factory/hooks/dotnet-format-check.sh",
          "timeout": 120
        }
      ]
    }
  ]
}
```

**Project facts in AGENTS.md.** The [AGENTS.md file](https://docs.factory.com/harness/agents-md) records the solution path, target framework, test command, and any project that must stay on .NET Framework during an incremental migration. Droid reads it before writing code.

**The playbook as a skill.** A skill in `.factory/skills/dotnet-upgrade/SKILL.md` lists the order of operations. Convert one project to SDK-style, retarget it, run the `dotnet format` passes, build, fix residue, and verify, with each step in its own commit. Skills load when a task matches, so every session follows the same sequence.

## Writing new analyzers and scaling across solutions

A repeated manual fix is a candidate for a new Roslyn rule. Suppose a codebase calls an internal `LegacyLogger.Write(string)` in four thousand places and the target is `ILogger`. The Droid writes a `DiagnosticAnalyzer` that reports each call, a `CodeFixProvider` that rewrites it, and tests that assert the diagnostic location and the fixed source. Microsoft's [analyzer and code fix tutorial](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix) shows the test pattern, where `VerifyCodeFixAsync` compares marked-up input with the expected output. The Droid also adds cases that must not trigger the rule.

Once the analyzer ships as a NuGet package, `dotnet format analyzers --diagnostics` with its rule ID applies it everywhere deterministically. The agent wrote the rule once. The tool applies it thousands of times, and the rule keeps reporting any new call that appears later.

For many projects, [Droid Exec](https://docs.factory.com/droid-exec/overview) runs Droid headless in CI. The pattern from Factory's [lint autofix recipe](https://docs.factory.com/droid-exec/recipes/lint-autofix) carries over directly. A deterministic tool lists the targets, one `droid exec` run handles each target in isolation, and the tool re-verifies. For .NET, the target list is the set of projects whose build fails after the tool commits, and each run uses `--auto medium` so it can build and test locally.

A portfolio of solutions with shared libraries needs ordering, since libraries have to move before their consumers. Factory [Missions](https://docs.factory.com/missions/overview) plan that work as milestones with validation along the way, and list brownfield migrations as a primary use case.

## Frequently asked questions

### Which .NET version should a modernization target?

As of November 2026, .NET 10 is the current long-term support release, supported until November 14, 2028. .NET 8 and .NET 9 reached end of support on November 10, 2026, the same day .NET 11 shipped as a standard-term release. Check the .NET support policy page before choosing a target.

### Is .NET Upgrade Assistant still the recommended tool?

Microsoft lists .NET Upgrade Assistant as officially deprecated and points users to its GitHub Copilot upgrade agent. The try-convert tool for SDK-style project conversion was archived in May 2024. Teams can still use the Roslyn analyzers and dotnet format that ship with the .NET SDK as deterministic tools in any workflow.

### Does Factory have a built-in .NET mode or converter?

No. Factory runs the .NET SDK tools directly. Hooks run dotnet format and the build after edits, skills hold the migration playbook, Droid Exec fans work out per project, and Missions coordinate migrations across many solutions. Droids also write custom Roslyn analyzers and code fixes with tests.

### What does dotnet format fix?

dotnet format applies whitespace rules, code style rules, and analyzer code fixes configured in .editorconfig. The style and analyzers subcommands accept a list of diagnostic IDs and a minimum severity, and --verify-no-changes exits non-zero when any change would be made, which makes it usable as a CI gate.

### Does this approach work for VB.NET?

Yes, for the deterministic layer. The .NET Compiler Platform SDK supports analyzers and code fixes for both C# and Visual Basic, and dotnet format runs against Visual Basic projects. The Droid follows the same loop on VB.NET code.

## Further reading

- [Deterministic code modernization with Factory](https://factory.com/articles/deterministic-code-modernization)
- [Java modernization with OpenRewrite and Droids](https://factory.com/articles/java-modernization-openrewrite)
- [Legacy modernization with private AI agents](https://factory.com/articles/legacy-modernization-private-ai)
- [How to run a runtime upgrade with coding agents](https://factory.com/articles/coding-agent-runtime-upgrades)
