Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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;

/// <summary>
/// Adds a <c>&lt;FrameworkReference&gt;</c> to a .csproj file's project root if one
/// with a matching Include doesn't already exist. The reference is placed in a
/// dedicated <c>&lt;ItemGroup&gt;</c> appended to the project. No-op when the SDK
/// is <c>Microsoft.NET.Sdk.Web</c>, which already imports
/// <c>Microsoft.AspNetCore.App</c> implicitly.
/// </summary>
public class AddFrameworkReference : ScanningRecipe<DotNetBuildContext>
{
public override string DisplayName => "Add framework reference";

public override string Description =>
"Adds a `<FrameworkReference>` 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 `<PackageReference>` " +
"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<ExecutionContext> GetScanner(DotNetBuildContext acc) => new BuildContextScanner();

public override ITreeVisitor<ExecutionContext> GetVisitor(DotNetBuildContext acc)
{
return Preconditions.Check(
new IsProjectFile(),
new AddFrameworkReferenceVisitor(FrameworkName, TriggerPackageGlob));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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;

/// <summary>
/// Visitor that adds a <c>&lt;FrameworkReference&gt;</c> 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
/// (<c>Microsoft.NET.Sdk.Web</c> implicitly references
/// <c>Microsoft.AspNetCore.App</c>).
/// </summary>
public class AddFrameworkReferenceVisitor(string frameworkName, string? triggerPackageGlob = null) : XmlVisitor<ExecutionContext>
{
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 = $"<FrameworkReference Include=\"{frameworkName}\" />";
var itemGroup = TagExtensions.BuildTag(
$"<ItemGroup>\n {tag}\n </ItemGroup>");
DoAfterVisit(new AddToTagVisitor<ExecutionContext>(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
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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;

/// <summary>
/// Removes <c>&lt;DotNetCliToolReference&gt;</c> 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. <c>dotnet watch</c>).
/// </summary>
public class RemoveDotNetCliToolReference : ScanningRecipe<DotNetBuildContext>
{
public override string DisplayName => "Remove DotNetCliToolReference";

public override string Description =>
"Removes a `<DotNetCliToolReference>` 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<ExecutionContext> GetScanner(DotNetBuildContext acc) => new BuildContextScanner();

public override ITreeVisitor<ExecutionContext> GetVisitor(DotNetBuildContext acc)
{
return Preconditions.Check(
new IsProjectFile(),
new RemoveDotNetCliToolReferenceVisitor(ToolName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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;

/// <summary>
/// Visitor that removes a <c>&lt;DotNetCliToolReference&gt;</c> element from
/// .csproj files. Supports glob patterns for the tool name.
/// </summary>
public class RemoveDotNetCliToolReferenceVisitor(string toolName) : XmlVisitor<ExecutionContext>
{
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<ExecutionContext>(t, true, false));
}
}
return t;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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;

/// <summary>
/// Removes an MSBuild property element (e.g. <c>RuntimeFrameworkVersion</c>) from a
/// <c>PropertyGroup</c> in .csproj files. Useful for stripping legacy properties that
/// are no longer applicable after upgrading the target framework.
/// </summary>
public class RemoveMSBuildProperty : ScanningRecipe<DotNetBuildContext>
{
public override string DisplayName => "Remove MSBuild property";

public override string Description =>
"Removes an MSBuild property element (e.g. `<RuntimeFrameworkVersion>`) from " +
"`<PropertyGroup>` 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<ExecutionContext> GetScanner(DotNetBuildContext acc) => new BuildContextScanner();

public override ITreeVisitor<ExecutionContext> GetVisitor(DotNetBuildContext acc)
{
return Preconditions.Check(
new IsProjectFile(),
new RemoveMSBuildPropertyVisitor(PropertyName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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;

/// <summary>
/// Visitor that removes an MSBuild property element nested inside a
/// <c>PropertyGroup</c> in .csproj files. Can be used standalone in custom recipe
/// edit phases.
/// </summary>
public class RemoveMSBuildPropertyVisitor(string propertyName) : XmlVisitor<ExecutionContext>
{
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<ExecutionContext>(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;
}
}
Loading