diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReference.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReference.cs
new file mode 100644
index 00000000000..5687f15d3f0
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReference.cs
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.Core;
+using OpenRewrite.Xml;
+using ExecutionContext = OpenRewrite.Core.ExecutionContext;
+
+namespace OpenRewrite.CSharp.Recipes;
+
+///
+/// Adds a <FrameworkReference> to a .csproj file's project root if one
+/// with a matching Include doesn't already exist. The reference is placed in a
+/// dedicated <ItemGroup> appended to the project. No-op when the SDK
+/// is Microsoft.NET.Sdk.Web, which already imports
+/// Microsoft.AspNetCore.App implicitly.
+///
+public class AddFrameworkReference : ScanningRecipe
+{
+ public override string DisplayName => "Add framework reference";
+
+ public override string Description =>
+ "Adds a `` to a .csproj if it isn't already present.";
+
+ [Option(DisplayName = "Framework name",
+ Description = "The shared framework name to reference.",
+ Example = "Microsoft.AspNetCore.App")]
+ public string FrameworkName { get; set; } = "";
+
+ [Option(DisplayName = "Trigger package glob",
+ Description = "Optional glob: only add the framework reference when a `` " +
+ "matching this glob is present in the project. Leave blank to always add.",
+ Example = "Microsoft.AspNetCore.*",
+ Required = false)]
+ public string? TriggerPackageGlob { get; set; }
+
+ public override DotNetBuildContext GetInitialValue(ExecutionContext ctx) => DotNetBuildContext.GetOrCreate(ctx);
+
+ public override ITreeVisitor GetScanner(DotNetBuildContext acc) => new BuildContextScanner();
+
+ public override ITreeVisitor GetVisitor(DotNetBuildContext acc)
+ {
+ return Preconditions.Check(
+ new IsProjectFile(),
+ new AddFrameworkReferenceVisitor(FrameworkName, TriggerPackageGlob));
+ }
+}
diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReferenceVisitor.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReferenceVisitor.cs
new file mode 100644
index 00000000000..696055347f0
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReferenceVisitor.cs
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.Core;
+using OpenRewrite.Xml;
+using ExecutionContext = OpenRewrite.Core.ExecutionContext;
+
+namespace OpenRewrite.CSharp.Recipes;
+
+///
+/// Visitor that adds a <FrameworkReference> to a .csproj file root if
+/// one with a matching Include doesn't already exist. Skips projects whose Sdk
+/// attribute already implicitly imports the same framework
+/// (Microsoft.NET.Sdk.Web implicitly references
+/// Microsoft.AspNetCore.App).
+///
+public class AddFrameworkReferenceVisitor(string frameworkName, string? triggerPackageGlob = null) : XmlVisitor
+{
+ private bool _alreadyPresent;
+ private bool _triggerMatched;
+
+ public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx)
+ {
+ _alreadyPresent = false;
+ _triggerMatched = string.IsNullOrEmpty(triggerPackageGlob);
+
+ // Skip when the SDK already imports the same framework implicitly.
+ var sdk = document.Root.GetAttributeValue("Sdk");
+ if (sdk != null && SdkImplicitlyReferences(sdk, frameworkName))
+ return document;
+
+ var d = (Document)base.VisitDocument(document, ctx);
+
+ if (_alreadyPresent || !_triggerMatched)
+ return d;
+
+ var tag = $"";
+ var itemGroup = TagExtensions.BuildTag(
+ $"\n {tag}\n ");
+ DoAfterVisit(new AddToTagVisitor(d.Root, itemGroup));
+ DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor());
+ return d;
+ }
+
+ public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx)
+ {
+ var t = (Tag)base.VisitTag(tag, ctx);
+ if (t.Name == "FrameworkReference")
+ {
+ var include = t.GetAttributeValue("Include");
+ if (include == frameworkName)
+ _alreadyPresent = true;
+ }
+ else if (!string.IsNullOrEmpty(triggerPackageGlob) && t.Name == "PackageReference")
+ {
+ var include = t.GetAttributeValue("Include");
+ if (include != null && GlobMatcher.Matches(include, triggerPackageGlob!))
+ _triggerMatched = true;
+ }
+ return t;
+ }
+
+ private static bool SdkImplicitlyReferences(string sdk, string framework)
+ {
+ return sdk switch
+ {
+ "Microsoft.NET.Sdk.Web" => framework is "Microsoft.AspNetCore.App" or "Microsoft.NETCore.App",
+ "Microsoft.NET.Sdk.Worker" => framework is "Microsoft.AspNetCore.App" or "Microsoft.NETCore.App",
+ "Microsoft.NET.Sdk.BlazorWebAssembly" => framework is "Microsoft.AspNetCore.App" or "Microsoft.NETCore.App",
+ _ => false
+ };
+ }
+}
diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/CsprojRecipeActivator.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/CsprojRecipeActivator.cs
index 36ee3bdbcc3..d2b5a029c45 100644
--- a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/CsprojRecipeActivator.cs
+++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/CsprojRecipeActivator.cs
@@ -28,5 +28,8 @@ public void Activate(RecipeMarketplace marketplace)
marketplace.Install(new UpgradeNuGetPackageVersion(), CsprojCategory);
marketplace.Install(new ChangeDotNetTargetFramework(), CsprojCategory);
marketplace.Install(new FindNuGetPackageReference(), CsprojCategory);
+ marketplace.Install(new RemoveMSBuildProperty(), CsprojCategory);
+ marketplace.Install(new RemoveDotNetCliToolReference(), CsprojCategory);
+ marketplace.Install(new AddFrameworkReference(), CsprojCategory);
}
}
diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReference.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReference.cs
new file mode 100644
index 00000000000..e6106676e7f
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReference.cs
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.Core;
+using OpenRewrite.Xml;
+using ExecutionContext = OpenRewrite.Core.ExecutionContext;
+
+namespace OpenRewrite.CSharp.Recipes;
+
+///
+/// Removes <DotNetCliToolReference> entries (matching by glob on the
+/// Include attribute) from .csproj files. CLI tool references are obsolete starting
+/// with .NET Core 3.0 — they were the netcoreapp2.x mechanism for shipping per-project
+/// CLI tools, and have since been replaced by global / local tools and SDK-built-in
+/// commands (e.g. dotnet watch).
+///
+public class RemoveDotNetCliToolReference : ScanningRecipe
+{
+ public override string DisplayName => "Remove DotNetCliToolReference";
+
+ public override string Description =>
+ "Removes a `` element from .csproj files. " +
+ "Use `*` to remove every CLI tool reference.";
+
+ [Option(DisplayName = "Tool name",
+ Description = "The CLI tool package name to remove. Supports glob patterns. " +
+ "Use `*` to remove all CLI tool references.",
+ Example = "Microsoft.DotNet.Watcher.Tools")]
+ public string ToolName { get; set; } = "";
+
+ public override DotNetBuildContext GetInitialValue(ExecutionContext ctx) => DotNetBuildContext.GetOrCreate(ctx);
+
+ public override ITreeVisitor GetScanner(DotNetBuildContext acc) => new BuildContextScanner();
+
+ public override ITreeVisitor GetVisitor(DotNetBuildContext acc)
+ {
+ return Preconditions.Check(
+ new IsProjectFile(),
+ new RemoveDotNetCliToolReferenceVisitor(ToolName));
+ }
+}
diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReferenceVisitor.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReferenceVisitor.cs
new file mode 100644
index 00000000000..ae9c46d39b3
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReferenceVisitor.cs
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.Core;
+using OpenRewrite.Xml;
+using ExecutionContext = OpenRewrite.Core.ExecutionContext;
+
+namespace OpenRewrite.CSharp.Recipes;
+
+///
+/// Visitor that removes a <DotNetCliToolReference> element from
+/// .csproj files. Supports glob patterns for the tool name.
+///
+public class RemoveDotNetCliToolReferenceVisitor(string toolName) : XmlVisitor
+{
+ private bool _modified;
+
+ public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx)
+ {
+ _modified = false;
+ var d = (Document)base.VisitDocument(document, ctx);
+ if (_modified)
+ DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor());
+ return d;
+ }
+
+ public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx)
+ {
+ var t = (Tag)base.VisitTag(tag, ctx);
+ if (t.Name == "DotNetCliToolReference")
+ {
+ var include = t.GetAttributeValue("Include");
+ if (include != null && GlobMatcher.Matches(include, toolName))
+ {
+ _modified = true;
+ DoAfterVisit(new RemoveContentVisitor(t, true, false));
+ }
+ }
+ return t;
+ }
+}
diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildProperty.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildProperty.cs
new file mode 100644
index 00000000000..fb996f0dd83
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildProperty.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.Core;
+using OpenRewrite.Xml;
+using ExecutionContext = OpenRewrite.Core.ExecutionContext;
+
+namespace OpenRewrite.CSharp.Recipes;
+
+///
+/// Removes an MSBuild property element (e.g. RuntimeFrameworkVersion) from a
+/// PropertyGroup in .csproj files. Useful for stripping legacy properties that
+/// are no longer applicable after upgrading the target framework.
+///
+public class RemoveMSBuildProperty : ScanningRecipe
+{
+ public override string DisplayName => "Remove MSBuild property";
+
+ public override string Description =>
+ "Removes an MSBuild property element (e.g. ``) from " +
+ "`` in .csproj files.";
+
+ [Option(DisplayName = "Property name",
+ Description = "The MSBuild property element name to remove (case-sensitive).",
+ Example = "RuntimeFrameworkVersion")]
+ public string PropertyName { get; set; } = "";
+
+ public override DotNetBuildContext GetInitialValue(ExecutionContext ctx) => DotNetBuildContext.GetOrCreate(ctx);
+
+ public override ITreeVisitor GetScanner(DotNetBuildContext acc) => new BuildContextScanner();
+
+ public override ITreeVisitor GetVisitor(DotNetBuildContext acc)
+ {
+ return Preconditions.Check(
+ new IsProjectFile(),
+ new RemoveMSBuildPropertyVisitor(PropertyName));
+ }
+}
diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildPropertyVisitor.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildPropertyVisitor.cs
new file mode 100644
index 00000000000..0c315028689
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildPropertyVisitor.cs
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.Core;
+using OpenRewrite.Xml;
+using ExecutionContext = OpenRewrite.Core.ExecutionContext;
+
+namespace OpenRewrite.CSharp.Recipes;
+
+///
+/// Visitor that removes an MSBuild property element nested inside a
+/// PropertyGroup in .csproj files. Can be used standalone in custom recipe
+/// edit phases.
+///
+public class RemoveMSBuildPropertyVisitor(string propertyName) : XmlVisitor
+{
+ private bool _modified;
+
+ public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx)
+ {
+ _modified = false;
+ var d = (Document)base.VisitDocument(document, ctx);
+ if (_modified)
+ DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor());
+ return d;
+ }
+
+ public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx)
+ {
+ var t = (Tag)base.VisitTag(tag, ctx);
+ if (t.Name == propertyName && IsInPropertyGroup())
+ {
+ _modified = true;
+ DoAfterVisit(new RemoveContentVisitor(t, true, false));
+ }
+ return t;
+ }
+
+ private bool IsInPropertyGroup()
+ {
+ // Walk parents up to find the enclosing element.
+ var parent = Cursor.Parent;
+ while (parent != null)
+ {
+ if (parent.Value is Tag pTag)
+ return pTag.Name == "PropertyGroup";
+ parent = parent.Parent;
+ }
+ return false;
+ }
+}
diff --git a/rewrite-csharp/csharp/OpenRewrite/Test/RewriteTest.cs b/rewrite-csharp/csharp/OpenRewrite/Test/RewriteTest.cs
index 80308f9166b..b39f4ea6ee5 100644
--- a/rewrite-csharp/csharp/OpenRewrite/Test/RewriteTest.cs
+++ b/rewrite-csharp/csharp/OpenRewrite/Test/RewriteTest.cs
@@ -86,13 +86,17 @@ protected void RewriteRun(Action configure, params SourceSpec[] spec
foreach (var spec in specs)
{
SourceFile source;
+ var isCsFile = spec.SourcePath != null &&
+ spec.SourcePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase);
if (spec.SourcePath != null && IsCsprojPath(spec.SourcePath) && csprojParsed != null)
{
source = csprojParsed[spec.SourcePath];
}
- else if (spec.SourcePath != null)
+ else if (spec.SourcePath != null && !isCsFile)
{
- // Remote-parsed source (e.g., XML via Java RPC)
+ // Remote-parsed source (e.g., XML via Java RPC). C# files always use local
+ // parsing — even with a custom source path — because the Java peer doesn't
+ // ship a C# parser.
var rpc = RewriteRpcServer.Current
?? throw new InvalidOperationException(
$"Parsing {spec.SourcePath} requires an RPC connection. " +
@@ -103,10 +107,11 @@ protected void RewriteRun(Action configure, params SourceSpec[] spec
else
{
// Local C# parsing
+ var localSourcePath = spec.SourcePath ?? "source.cs";
SemanticModel? semanticModel = null;
if (metadataReferences != null)
{
- var syntaxTree = CSharpSyntaxTree.ParseText(spec.Before, path: "source.cs");
+ var syntaxTree = CSharpSyntaxTree.ParseText(spec.Before, path: localSourcePath);
var compilation = CSharpCompilation.Create("TestCompilation")
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(metadataReferences)
@@ -114,7 +119,7 @@ protected void RewriteRun(Action configure, params SourceSpec[] spec
semanticModel = compilation.GetSemanticModel(syntaxTree);
}
- source = parser.Parse(spec.Before, semanticModel: semanticModel);
+ source = parser.Parse(spec.Before, sourcePath: localSourcePath, semanticModel: semanticModel);
// Verify no non-whitespace content leaked into Space fields
if (validations.WhitespaceInSpaces)
diff --git a/rewrite-csharp/csharp/OpenRewrite/Test/RpcFixture.cs b/rewrite-csharp/csharp/OpenRewrite/Test/RpcFixture.cs
index b72d7cd0c69..3cf80836b85 100644
--- a/rewrite-csharp/csharp/OpenRewrite/Test/RpcFixture.cs
+++ b/rewrite-csharp/csharp/OpenRewrite/Test/RpcFixture.cs
@@ -74,11 +74,7 @@ public void Reset()
private static ProcessStartInfo CreateJavaProcessStartInfo()
{
- var cpFile = Environment.GetEnvironmentVariable("RPC_TEST_SERVER_CLASSPATH")
- ?? throw new InvalidOperationException(
- "RPC_TEST_SERVER_CLASSPATH environment variable not set. " +
- "Run './gradlew :rewrite-csharp:rpcTestClasspath' to generate the classpath file.");
-
+ var cpFile = ResolveClasspathFile();
var classpath = File.ReadAllText(cpFile).Trim();
var psi = new ProcessStartInfo("java", "org.openrewrite.maven.rpc.JavaRewriteRpc")
{
@@ -92,6 +88,100 @@ private static ProcessStartInfo CreateJavaProcessStartInfo()
return psi;
}
+ ///
+ /// Resolves the rewrite-csharp RPC test server classpath file.
+ ///
+ /// Order of resolution:
+ /// 1. RPC_TEST_SERVER_CLASSPATH env var, if it points at an existing file
+ /// whose mtime isn't older than the SDK assembly that's currently loaded.
+ /// A stale env var (pointing at an unrelated rewrite checkout's leftover
+ /// classpath) silently linking the test process to outdated Java JARs is
+ /// a recurring source of cryptic "recipe didn't fire" failures.
+ /// 2. The classpath file at a deterministic location relative to the loaded
+ /// OpenRewrite SDK assembly: walk up from the assembly until we hit
+ /// rewrite-csharp/csharp/OpenRewrite/{bin,obj}, then the classpath
+ /// file is at rewrite-csharp/build/rpc-test-server-classpath.txt.
+ ///
+ private static string ResolveClasspathFile()
+ {
+ var sdkRelativeFile = AutoLocateClasspathFile();
+ var envFile = Environment.GetEnvironmentVariable("RPC_TEST_SERVER_CLASSPATH");
+
+ // Prefer the SDK-relative file when it exists and is at least as fresh as
+ // any env-pointed file — guards against a stale env var inherited from a
+ // different rewrite checkout.
+ if (sdkRelativeFile != null && File.Exists(sdkRelativeFile))
+ {
+ if (string.IsNullOrEmpty(envFile) || !File.Exists(envFile) ||
+ File.GetLastWriteTimeUtc(sdkRelativeFile) >=
+ File.GetLastWriteTimeUtc(envFile))
+ {
+ return sdkRelativeFile;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(envFile) && File.Exists(envFile))
+ return envFile;
+
+ if (sdkRelativeFile != null && File.Exists(sdkRelativeFile))
+ return sdkRelativeFile;
+
+ throw new InvalidOperationException(
+ "Cannot locate the Java RPC test server classpath. Either set " +
+ "RPC_TEST_SERVER_CLASSPATH, or run " +
+ "`./gradlew :rewrite-csharp:rpcTestClasspath` from the rewrite SDK " +
+ "checkout. Searched SDK-relative path: " +
+ (sdkRelativeFile ?? "(could not derive from loaded SDK assembly)"));
+ }
+
+ private static string? AutoLocateClasspathFile()
+ {
+ // Two layout cases to handle:
+ // (a) Test runs INSIDE the rewrite SDK repo: walk up from the SDK assembly
+ // until we find `/rewrite-csharp/csharp/OpenRewrite/`.
+ // (b) Test runs in a CONSUMING project (e.g. recipes-csharp) that pulls in
+ // the SDK via ProjectReference. The DLL is copied to the test project's
+ // bin/, so the assembly path doesn't reach the SDK source. Instead, walk
+ // up from the assembly looking for an `external/openrewrite/rewrite`
+ // symlink (the convention for source-linked SDK in Conductor / consumer
+ // repos), then derive the classpath from the symlink target.
+ var sdkAssembly = typeof(OpenRewrite.CSharp.CSharpParser).Assembly.Location;
+ if (string.IsNullOrEmpty(sdkAssembly))
+ return null;
+
+ var dir = Path.GetDirectoryName(sdkAssembly);
+ while (dir != null)
+ {
+ // Case (a): inside the SDK repo.
+ if (Path.GetFileName(dir) == "OpenRewrite")
+ {
+ var parent = Path.GetDirectoryName(dir);
+ var grandparent = parent != null ? Path.GetDirectoryName(parent) : null;
+ if (parent != null && grandparent != null &&
+ Path.GetFileName(parent) == "csharp" &&
+ Path.GetFileName(grandparent) == "rewrite-csharp")
+ {
+ return Path.Combine(grandparent, "build", "rpc-test-server-classpath.txt");
+ }
+ }
+
+ // Case (b): consuming repo with `external/openrewrite/rewrite` symlink.
+ var symlink = Path.Combine(dir, "external", "openrewrite", "rewrite");
+ if (Directory.Exists(symlink))
+ {
+ var sdkMarker = Path.Combine(symlink,
+ "rewrite-csharp", "csharp", "OpenRewrite", "OpenRewrite.csproj");
+ if (File.Exists(sdkMarker))
+ {
+ return Path.Combine(symlink,
+ "rewrite-csharp", "build", "rpc-test-server-classpath.txt");
+ }
+ }
+ dir = Path.GetDirectoryName(dir);
+ }
+ return null;
+ }
+
public void Dispose()
{
RewriteRpcServer.SetCurrent(null);
diff --git a/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/AddFrameworkReferenceTests.cs b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/AddFrameworkReferenceTests.cs
new file mode 100644
index 00000000000..0a07b41a082
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/AddFrameworkReferenceTests.cs
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.CSharp.Recipes;
+using OpenRewrite.Test;
+
+namespace OpenRewrite.Tests.CSharp.Recipes;
+
+public class AddFrameworkReferenceTests : RewriteTest
+{
+ [Fact]
+ public void AddsWhenTriggerMatches()
+ {
+ RewriteRun(
+ spec => spec.SetRecipe(new AddFrameworkReference
+ {
+ FrameworkName = "Microsoft.AspNetCore.App",
+ TriggerPackageGlob = "Microsoft.AspNetCore.*"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+
+
+
+
+
+ """,
+ """
+
+
+ net10.0
+
+
+
+
+
+
+
+
+ """
+ )
+ );
+ }
+
+ [Fact]
+ public void NoChangeWhenSdkImplicitlyImports()
+ {
+ // Microsoft.NET.Sdk.Web already imports Microsoft.AspNetCore.App
+ RewriteRun(
+ spec => spec.SetRecipe(new AddFrameworkReference
+ {
+ FrameworkName = "Microsoft.AspNetCore.App"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+
+
+ """
+ )
+ );
+ }
+
+ [Fact]
+ public void NoChangeWhenAlreadyPresent()
+ {
+ RewriteRun(
+ spec => spec.SetRecipe(new AddFrameworkReference
+ {
+ FrameworkName = "Microsoft.AspNetCore.App"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+
+
+
+
+
+ """
+ )
+ );
+ }
+
+ [Fact]
+ public void NoChangeWhenTriggerNotMatched()
+ {
+ RewriteRun(
+ spec => spec.SetRecipe(new AddFrameworkReference
+ {
+ FrameworkName = "Microsoft.AspNetCore.App",
+ TriggerPackageGlob = "Microsoft.AspNetCore.*"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+
+
+
+
+
+ """
+ )
+ );
+ }
+}
diff --git a/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveDotNetCliToolReferenceTests.cs b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveDotNetCliToolReferenceTests.cs
new file mode 100644
index 00000000000..64cae992423
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveDotNetCliToolReferenceTests.cs
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.CSharp.Recipes;
+using OpenRewrite.Test;
+
+namespace OpenRewrite.Tests.CSharp.Recipes;
+
+public class RemoveDotNetCliToolReferenceTests : RewriteTest
+{
+ [Fact]
+ public void RemoveSpecificCliTool()
+ {
+ RewriteRun(
+ spec => spec.SetRecipe(new RemoveDotNetCliToolReference
+ {
+ ToolName = "Microsoft.DotNet.Watcher.Tools"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+
+
+
+
+
+
+ """,
+ """
+
+
+ net10.0
+
+
+
+
+
+ """
+ )
+ );
+ }
+
+ [Fact]
+ public void RemoveAllCliToolsViaWildcard()
+ {
+ RewriteRun(
+ spec => spec.SetRecipe(new RemoveDotNetCliToolReference
+ {
+ ToolName = "*"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+
+
+
+
+
+
+ """,
+ """
+
+
+ net10.0
+
+
+ """
+ )
+ );
+ }
+}
diff --git a/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveMSBuildPropertyTests.cs b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveMSBuildPropertyTests.cs
new file mode 100644
index 00000000000..c4d7a1a7f78
--- /dev/null
+++ b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveMSBuildPropertyTests.cs
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using OpenRewrite.CSharp.Recipes;
+using OpenRewrite.Test;
+
+namespace OpenRewrite.Tests.CSharp.Recipes;
+
+public class RemoveMSBuildPropertyTests : RewriteTest
+{
+ [Fact]
+ public void RemovesProperty()
+ {
+ RewriteRun(
+ spec => spec.SetRecipe(new RemoveMSBuildProperty
+ {
+ PropertyName = "RuntimeFrameworkVersion"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+ 2.1.1
+
+
+ """,
+ """
+
+
+ net10.0
+
+
+ """
+ )
+ );
+ }
+
+ [Fact]
+ public void NoChangeWhenPropertyAbsent()
+ {
+ RewriteRun(
+ spec => spec.SetRecipe(new RemoveMSBuildProperty
+ {
+ PropertyName = "RuntimeFrameworkVersion"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+
+
+ """
+ )
+ );
+ }
+
+ [Fact]
+ public void OnlyMatchesInsidePropertyGroup()
+ {
+ // A child element named identically inside an ItemGroup or elsewhere should not be touched.
+ RewriteRun(
+ spec => spec.SetRecipe(new RemoveMSBuildProperty
+ {
+ PropertyName = "RuntimeFrameworkVersion"
+ }),
+ CsProj(
+ """
+
+
+ net10.0
+
+
+
+
+
+ """
+ )
+ );
+ }
+}