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 elementCSharpTemplate: Code generation using C# interpolated strings with typed placeholdersCSharpPattern: Structural pattern matching against code fragmentsCapture<T>: Type-safe placeholders that bridge patterns and templates
The initial recipe catalog ships across three modules:
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
.csprojfiles across the full range fromnetcoreapp1.0throughnet10.0(includingnetstandard1.xandnetstandard2.x) - NuGet package upgrades for
Microsoft.AspNetCore.*,Microsoft.EntityFrameworkCore*,Microsoft.Extensions.*,System.Text.Json, and more - Obsolete API replacement: cryptographic providers (
AesCryptoServiceProvider→Aes.Create()), threading APIs (Thread.VolatileReadtoVolatile.Read), convenience APIs (newRandom().Next()toRandom.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.

