Moderne adds C#: Native code transformation for the .NET ecosystem

Andrew Stakhov
|
May 5, 2026
Moderne extends automated code transformation to C# and .NET
Contents

Key Takeaways

  • .NET 8 and 9 reach end of life in November 2026, and Microsoft's .NET Upgrade Assistant won't support versions beyond .NET 9 — Moderne's migration recipes pick up exactly where it left off.
  • Running "Migrate to .NET 10" doesn't just update your target framework — it handles hundreds of API changes, NuGet upgrades, and breaking change mitigations automatically.
  • One recipe run across your entire portfolio replaces weeks of manual hunting for deprecated patterns and obsolete APIs.
  • With nearly 700 ready-to-run recipes, Moderne ensures the same transformations run identically across dozens or hundreds of .NET repos.

Moderne has extended its platform to support C# and .NET, bringing the same large-scale, automated code transformation that Java teams have relied on for years to the .NET ecosystem. The release includes a ground-up C# SDK for writing transformation recipes, along with a catalog of nearly 700 recipes available on day one.

Timing matters: .NET 8 and 9 reach end of life in November 2026 — and starting now, a major .NET version will go out of support every November on that same cadence. Teams on unsupported frameworks lose security patches and Microsoft support coverage. If your organization is on .NET 8 or 9 today, the migration clock is already running.

What’s shipping: A full C# SDK with 700 ready-to-run recipes

The rewrite-csharp SDK provides a complete Lossless Semantic Tree (LST) for C#. Every language construct from properties, pattern matching, and LINQ queries to preprocessor directives, nullable reference types, and interpolated strings, is represented as a strongly-typed, immutable tree node. Because the tree is lossless, transformations preserve whitespace, comments, and formatting exactly as the original author wrote them.

The SDK includes:

  • CSharpVisitor<T>: A type-safe visitor with dispatch methods for every C# syntax element
  • CSharpTemplate: Code generation using C# interpolated strings with typed placeholders
  • CSharpPattern: Structural pattern matching against code fragments
  • Capture<T>: Type-safe placeholders that bridge patterns and templates

The initial recipe catalog ships across three modules:

Module Recipes Coverage
Code Quality ~420 Style, simplification, performance, redundancy, LINQ, formatting, naming
.NET Migration ~150 .NET Core 1.0 through .NET 10, ASP.NET Framework to Core
TUnit Migration 22 xUnit to TUnit test framework migration

Every recipe is implemented in native C#.

Recipe categories

Code quality recipes from popular .NET static analysis rules 

The code quality module aligns with established .NET static analysis rules from Roslynator and Meziantou. The logic is largely the same, but instead of operating on a single repo at a time, these recipes run across your entire portfolio through the Moderne platform.

Style (189 recipes): Automatically modernize code to use file-scoped namespaces, pattern matching for null checks, the nameof operator, explicit access modifiers, expression-bodied members, and more. Teams no longer need to debate style in code review; enforce it programmatically.

Simplification (72 recipes): Collapse verbose patterns into their idiomatic equivalents: boolean comparisons with literals, null-coalescing chains, if statements that should be ternaries, and over-specified lambda expressions. Less code, fewer bugs.

Performance (57 recipes): Surface and fix performance pitfalls that are invisible to the naked eye: suboptimal StringBuilder.Append chains, missing CancellationToken propagation, accidental boxing of value types, and LINQ pipelines that do more work than necessary.

Redundancy (56 recipes): Strip out code that adds noise without value: redundant casts, async/await wrappers on simple returns, unused member declarations, and unnecessary syntax on records. Smaller diffs, easier reviews.

LINQ (25 recipes): Modernize LINQ usage to take advantage of newer .NET APIs: combine chained .Where().Select() calls, replace .OrderBy() with .Order(), and optimize .Count() usage patterns.

Formatting (18 recipes) and Naming (11 recipes): Enforce consistent brace placement, documentation structure, and naming conventions for fields and parameters.

.NET Core 1.0 to .NET 10 migration

Migration recipes cover the full history of .NET, from .NET Core 1.0 all the way to .NET 10. Running “Upgrade to .NET 10” automatically chains through every intermediate version, applying the right target framework changes, NuGet package upgrades, API replacements, and breaking change mitigations at each step. This includes:

  • Target framework updates in .csproj files across the full range from netcoreapp1.0 through net10.0 (including netstandard1.x and netstandard2.x)
  • NuGet package upgrades for Microsoft.AspNetCore.*, Microsoft.EntityFrameworkCore*, Microsoft.Extensions.*, System.Text.Json, and more
  • Obsolete API replacement: cryptographic providers (AesCryptoServiceProviderAes.Create()), threading APIs (Thread.VolatileRead to Volatile.Read), convenience APIs (new Random().Next() to Random.Shared.Next()), and dozens more
  • ASP.NET Framework to Core: type migrations for controllers, action results, routing, authentication, and HTTP context across ASP.NET Core 2.x and 3.x
  • Breaking change detection: “Find” recipes that flag code affected by changes that require human judgment, marked with clear TODO comments explaining the issue

For teams still running .NET Core 1.x or 2.x in production, a single recipe invocation can leapfrog them to .NET 10, handling every intermediate API change along the way. We have tested this against real repositories, and in several cases the result compiles and runs without additional manual work.

Microsoft’s .NET Upgrade Assistant, which was the official tool for these transitions, has been deprecated and will not support versions beyond .NET 9. Moderne’s migration recipes pick up where that tool left off.

Test framework migration

A complete xUnit to TUnit migration path: attribute mapping ([Fact][Test], [Theory][Test]), lifecycle conversion (constructors to [Before(Test)], IDisposable[After(Test)]), fixture migration, assertion rewriting, and dependency updates.

What this means for .NET teams

Run once, fix everything

Instead of manually hunting through codebases for obsolete APIs, deprecated patterns, or style violations, teams can run a single recipe across their entire .NET portfolio through the Moderne Platform. A “Migrate to .NET 10” recipe doesn’t just update the target framework; it handles the hundreds of individual API changes, NuGet version bumps, and breaking-change mitigations that a real version upgrade requires.

Consistency at scale

For organizations with dozens or hundreds of .NET repositories, the Moderne platform ensures that migrations and code quality standards are applied uniformly. The same transformation runs identically whether the repo has 10 files or 10,000.

Lossless transformations

Because the LST models code at a semantic level rather than using string manipulation or regex, transformations respect your code’s existing formatting, comments, and style. A recipe that replaces x == null with x is null preserves surrounding whitespace, doesn’t touch string literals that happen to contain “== null”, and understands nullable type semantics well enough to skip cases where the simplification would change behavior.

From automated to complete

The recipes handle the deterministic work: framework upgrades, API replacements, NuGet bumps, and style enforcement are well-defined transformations that run identically every time, across every repo. How much that covers depends on the codebase. We have tested full migrations from .NET Core 1.0 to .NET 10 that compile and run without manual intervention. Other codebases need more work, particularly those with custom abstractions or patterns that fall outside what any recipe can safely resolve without human judgment.

For that remaining work, Moderne provides agent tools compatible with any AI coding agent, giving you targeted, context-aware assistance to complete what automation started.

Writing your own C# recipes

The SDK supports two recipe authoring styles: direct LST manipulation for fine-grained control, and template-based for concise pattern-match-and-replace operations.

Template style: Pattern + Replace

The template approach is the fastest way to write recipes. You define a pattern to match and a template to replace it with, using Capture<T> placeholders to carry matched subtrees between the two.

Here’s a real recipe that replaces  Random().Method(...) with Random.Shared.Method(...):

using OpenRewrite.Core;
using OpenRewrite.CSharp;
using OpenRewrite.CSharp.Template;
using OpenRewrite.Java;
using static OpenRewrite.Java.J;
 
public class UseRandomShared : Recipe
{
    public override string DisplayName => "Use Random.Shared";
 
    public override string Description =>
        "Replace new Random().Method(...) with Random.Shared.Method(...). " +
        "Available since .NET 6.";
 
    public override ITreeVisitor<ExecutionContext> GetVisitor()
    {
        var method = Capture.Of<Identifier>("method");
        var args = Capture.Variadic<Expression>("args");
 
        var pat = CSharpPattern.Expression($"new Random().{method}({args})");
        var tmpl = CSharpTemplate.Expression($"Random.Shared.{method}({args})");
 
        return new Visitor(pat, tmpl);
    }
 
    private class Visitor(CSharpPattern pat, CSharpTemplate tmpl)
        : CSharpVisitor<ExecutionContext>
    {
        public override J VisitMethodInvocation(
            MethodInvocation mi, ExecutionContext ctx)
        {
            mi = (MethodInvocation)base.VisitMethodInvocation(mi, ctx);
            if (pat.Match(mi, Cursor) is { } match)
            {
                return (J)tmpl.Apply(Cursor, values: match)!;
            }
            return mi;
        }
    }
}

The key insight: Capture.Variadic<Expression>("args") matches any number of arguments, so this single recipe handles new Random().Next(), new Random().Next(100), and new Random().NextDouble(). The method name and argument list flow through automatically.

LST style: Direct tree manipulation

For recipes that need conditional logic, type inspection, or multi-node coordination, you work directly with the LST. Here’s a recipe that replaces if (x == null) throw new ArgumentNullException.ThrowIfNull(x)) guard clauses with the modern ArgumentNullException.ThrowIfNull(x):

public class UseThrowIfNull : Recipe
{
    public override string DisplayName =>
        "Use ArgumentNullException.ThrowIfNull()";
 
    public override ITreeVisitor<ExecutionContext> GetVisitor()
        => new Visitor();
 
    private class Visitor : CSharpVisitor<ExecutionContext>
    {
        public override J VisitIf(If ifStatement, ExecutionContext ctx)
        {
            ifStatement = (If)base.VisitIf(ifStatement, ctx);
 
            if (ifStatement.ElsePart != null)
                return ifStatement;
 
            var param = ExtractNullCheckedParam(
                ifStatement.Condition.Tree.Element);
            if (param == null) return ifStatement;
 
            var throwStmt = ExtractThrow(
                ifStatement.ThenPart.Element);
            if (throwStmt == null) return ifStatement;
 
            if (!IsArgumentNullException(throwStmt, param))
                return ifStatement;
 
            return BuildThrowHelperCall(
                ifStatement.Prefix,
                "ArgumentNullException", "ThrowIfNull", param);
        }
    }
}

This recipe handles three different null-check syntaxes (== null, null ==, is null), validates the exception type and parameter name, and only transforms simple guard clauses, leaving if-else blocks untouched. All of this is possible because the LST carries full semantic information about types, operators, and code structure.

Composing recipes

Individual recipes compose into higher-level migrations. The UpgradeToDotNet10 recipe chains through every version:

public class UpgradeToDotNet10 : Recipe
{
    public override string DisplayName => "Migrate to .NET 10";
 
    public override List<Recipe> GetRecipeList() =>
    [
        new UpgradeToDotNet9(),
        new ChangeDotNetTargetFramework
        {
            OldTargetFramework = "net9.0",
            NewTargetFramework = "net10.0"
        },
        new UpgradeNuGetPackageVersion
        {
            PackageName = "Microsoft.EntityFrameworkCore*",
            NewVersion = "10.0.x"
        },
        new MlDsaSlhDsaSecretKeyToPrivateKey(),
        new FormOnClosingRename(),
        // ... 30+ more individual recipes
    ];
}

A single mod run invocation applies the entire chain across your codebase.

Getting started

The recipe catalog continues to grow. Whether you’re migrating from .NET Core 1.x to .NET 10, enforcing code quality standards across your organization, or building custom recipes for your own frameworks, the C# SDK gives .NET developers the same transformation power that Java teams have relied on for years, now available through the Moderne Platform. 

With .NET 8 and 9 support ending November 2026, there’s no better time to run your first migration.

* The C# language parser and all recipes are Moderne proprietary, and are now available through the Moderne CLI.