diff --git a/src/Fallout.Migrate/Migration.cs b/src/Fallout.Migrate/Migration.cs
index 55f2ab655..05f64f422 100644
--- a/src/Fallout.Migrate/Migration.cs
+++ b/src/Fallout.Migrate/Migration.cs
@@ -27,6 +27,8 @@ internal sealed class Migration(AbsolutePath rootDirectory, bool dryRun, TextWri
new VerifyBuildTargetFrameworkStep(),
new ConfirmMigrationStep(),
new RewriteCsprojsStep(),
+ new RemoveNugetFrameworkPinStep(),
+ new RemoveRelatedNugetPinsStep(),
new BumpDotNetVersionStep(),
new RewriteCsFilesStep(),
new RewriteBootstrapScriptsStep(),
diff --git a/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs
new file mode 100644
index 000000000..7b47276be
--- /dev/null
+++ b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Fallout.Migrate.Common;
+
+namespace Fallout.Migrate.Steps;
+
+///
+/// Removes explicit NuGet.Frameworks package pins from project files. The pins are no longer needed.
+///
+internal sealed class RemoveNugetFrameworkPinStep : IMigrationStep
+{
+ ///
+ /// Matches an entire single-line, self-closing
+ /// PackageReference element for NuGet.Frameworks
+ /// that explicitly declares a non-empty Version attribute,
+ /// including its indentation and trailing line ending.
+ ///
+ ///
+ ///
+ /// Intended for removing explicitly versioned
+ /// NuGet.Frameworks package references from MSBuild XML
+ /// files such as .csproj, .props, and .targets.
+ ///
+ ///
+ /// The Include and Version attributes may appear
+ /// in any order. Additional attributes, such as
+ /// PrivateAssets, are allowed before, between, or after
+ /// the required attributes.
+ ///
+ ///
+ /// Matching requirements:
+ ///
+ ///
+ /// -
+ ///
+ /// The element must begin on a line containing only optional
+ /// leading spaces or tabs.
+ ///
+ ///
+ /// -
+ ///
+ /// The element must contain
+ /// Include="NuGet.Frameworks".
+ ///
+ ///
+ /// -
+ ///
+ /// The element must contain a non-empty
+ /// Version="..." attribute.
+ ///
+ ///
+ /// -
+ ///
+ /// The element must be self-closing and contained on one line.
+ ///
+ ///
+ ///
+ ///
+ /// The expression consumes the entire matching line, including
+ /// its indentation, optional trailing whitespace, and line
+ /// ending (if present). Replacing matches with an empty string
+ /// therefore removes the package reference without leaving
+ /// an empty line.
+ ///
+ ///
+ /// Example:
+ ///
+ ///
+ /// <PackageReference Version="7.9.0" PrivateAssets="all" Include="NuGet.Frameworks" />
+ ///
+ ///
+ /// Limitations:
+ /// The expression is case-sensitive, supports double-quoted
+ /// attributes only, and does not match multiline elements.
+ /// It does not validate XML structure or NuGet version syntax
+ /// and may match package references inside XML comments.
+ ///
+ ///
+ private static readonly Regex explicitPinPattern = new(
+ @"^[ \t]*]*[ \t]Include=""NuGet\.Frameworks"")(?=[^\r\n>]*[ \t]Version=""[^""]+"")[^\r\n>]*?/>[ \t]*(?:\r?\n|$)",
+ RegexOptions.Compiled | RegexOptions.Multiline);
+
+ ///
+ public Task ExecuteAsync(MigrationContext context, Summary summary)
+ {
+ Version.TryParse(context.FalloutVersion, out var versionGate);
+
+ if (versionGate < Version.Parse("10.5.0"))
+ {
+ return Task.CompletedTask;
+ }
+
+ foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "_build.csproj"))
+ {
+ MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static RewriteResult Rewrite(string original)
+ {
+ var edits = 0;
+ var content = explicitPinPattern.Replace(original, _ =>
+ {
+ edits++;
+ return string.Empty;
+ });
+
+ return new RewriteResult(content, edits);
+ }
+}
diff --git a/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs b/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs
new file mode 100644
index 000000000..df42dd86a
--- /dev/null
+++ b/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs
@@ -0,0 +1,151 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Fallout.Migrate.Common;
+
+namespace Fallout.Migrate.Steps;
+
+///
+/// Removes explicit NuGet.Protocol, NuGet.Packaging, and NuGet.Resolver
+/// package pins from project files when migrating to Fallout 11 or later.
+///
+internal sealed class RemoveRelatedNugetPinsStep : IMigrationStep
+{
+ ///
+ /// The minimum Fallout major version that requires this step. On migrations below that version ths step is skipped.
+ ///
+ private const int MinimumFalloutMajor = 11;
+
+ ///
+ /// Matches an entire single-line, self-closing
+ /// PackageReference element for NuGet.Protocol,
+ /// NuGet.Packaging, or NuGet.Resolver that explicitly
+ /// declares a non-empty Version attribute, including its
+ /// indentation and trailing line ending.
+ ///
+ ///
+ ///
+ /// Intended for removing explicitly versioned NuGet package
+ /// references from MSBuild XML files such as .csproj,
+ /// .props, and .targets.
+ ///
+ ///
+ /// The Include and Version attributes may appear
+ /// in any order. Additional attributes, such as
+ /// PrivateAssets, are allowed before, between, or after
+ /// the required attributes.
+ ///
+ ///
+ /// Matching requirements:
+ ///
+ ///
+ /// -
+ ///
+ /// The element must begin on a line containing only optional
+ /// leading spaces or tabs.
+ ///
+ ///
+ /// -
+ ///
+ /// The element must contain an Include attribute
+ /// identifying one of the following packages:
+ /// NuGet.Protocol, NuGet.Packaging,
+ /// or NuGet.Resolver.
+ ///
+ ///
+ /// -
+ ///
+ /// The element must contain a non-empty
+ /// Version="..." attribute.
+ ///
+ ///
+ /// -
+ ///
+ /// The element must be self-closing and contained on one line.
+ ///
+ ///
+ ///
+ ///
+ /// Two positive lookaheads independently verify the package name
+ /// and version attribute. Their positions allow either attribute
+ /// to appear first, with arbitrary additional attributes between
+ /// them.
+ ///
+ ///
+ /// The expression consumes the entire matching line, including
+ /// its indentation, optional trailing whitespace, and line
+ /// ending (if present). Replacing matches with an empty string
+ /// therefore removes the package reference without leaving
+ /// an empty line.
+ ///
+ ///
+ /// Matching examples:
+ ///
+ ///
+ /// <PackageReference Include="NuGet.Protocol" Version="6.0.0" />
+ /// <PackageReference Version="7.9.0" Include="NuGet.Packaging" />
+ /// <PackageReference Version="7.9.0" PrivateAssets="all" Include="NuGet.Resolver" />
+ /// <PackageReference Include="NuGet.Protocol" PrivateAssets="all" Version="6.0.0" />
+ ///
+ ///
+ /// Limitations:
+ /// Matching is case-sensitive and supports double-quoted
+ /// attributes only. Multiline elements are not supported.
+ /// The expression does not validate XML structure or NuGet
+ /// version syntax and may match elements inside XML comments.
+ ///
+ ///
+ private static readonly Regex explicitPinPattern = new(
+ @"^[ \t]*]*[ \t]Include=""NuGet\.(?:Protocol|Packaging|Resolver)"")(?=[^\r\n>]*[ \t]Version=""[^""]+"")[^\r\n>]*?/>[ \t]*(?:\r?\n|$)",
+ RegexOptions.Compiled | RegexOptions.Multiline);
+
+ ///
+ public Task ExecuteAsync(MigrationContext context, Summary summary)
+ {
+ if (!RunsForMinimumRequiredVersion(context.FalloutVersion))
+ {
+ return Task.CompletedTask;
+ }
+
+ foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "_build.csproj"))
+ {
+ MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// if the major version can be parsed as a
+ /// non-negative integer and is greater than or equal to
+ /// ; otherwise, .
+ /// Returns for ,
+ /// empty, or unparseable version strings.
+ ///
+ ///
+ /// The major version is parsed using
+ /// and .
+ /// Minor, patch, and subsequent version segments are ignored and
+ /// are not validated.
+ ///
+ private static bool RunsForMinimumRequiredVersion(string version)
+ {
+ int separatorIndex = version?.IndexOf('.') ?? -1;
+ string majorSegment = separatorIndex == -1 ? version : version[..separatorIndex];
+
+ return int.TryParse(majorSegment, NumberStyles.None, CultureInfo.InvariantCulture, out int major) &&
+ major >= MinimumFalloutMajor;
+ }
+
+ private static RewriteResult Rewrite(string original)
+ {
+ var edits = 0;
+ var content = explicitPinPattern.Replace(original, _ =>
+ {
+ edits++;
+ return string.Empty;
+ });
+
+ return new RewriteResult(content, edits);
+ }
+}
diff --git a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs
new file mode 100644
index 000000000..c631eacdd
--- /dev/null
+++ b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs
@@ -0,0 +1,165 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Fallout.Common.IO;
+using Fallout.Migrate.Common;
+using Fallout.Migrate.Steps;
+using FluentAssertions;
+using Xunit;
+
+namespace Fallout.Migrate.Specs;
+
+public class RemoveNugetFrameworkPinStepSpecs : IDisposable
+{
+ private const string TestFalloutVersion = "10.5.0";
+
+ private readonly AbsolutePath tempDirectory;
+ private readonly MigrationContext context;
+ private readonly Summary summary = new();
+
+ public RemoveNugetFrameworkPinStepSpecs()
+ {
+ tempDirectory = AbsolutePath.Temp("fallout-remove-nuget-framework-pin");
+ context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null)
+ {
+ FalloutVersion = TestFalloutVersion
+ };
+ }
+
+ [Fact]
+ public async Task Explicit_nuget_framework_pin_is_removed()
+ {
+ // Arrange
+ var project = tempDirectory / "_build.csproj";
+ project.WriteAllText(
+ """
+
+
+
+
+
+
+ """, eofLineBreak: false);
+
+ // Act
+ await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary);
+
+ // Assert
+ project.ReadAllText().Should().Be(
+ """
+
+
+
+
+
+ """);
+
+ summary.FilesChanged.Should().Be(1);
+ summary.EditCount.Should().Be(1);
+ }
+
+ [Fact]
+ public async Task Explicit_nuget_framework_pin_is_left_alone_for_version_prior_10_5()
+ {
+ // Arrange
+ var project = tempDirectory / "_build.csproj";
+ var input =
+ """
+
+
+
+
+
+
+ """;
+ project.WriteAllText(input, eofLineBreak: false);
+ context.FalloutVersion = "10.4.0";
+
+ // Act
+ await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary);
+
+ // Assert
+ project.ReadAllText().Should().Be(input);
+
+ summary.FilesChanged.Should().Be(0);
+ summary.EditCount.Should().Be(0);
+ }
+
+
+ [Fact]
+ public async Task Nuget_framework_reference_without_version_is_left_unchanged()
+ {
+ // Arrange
+ var project = tempDirectory / "_build.csproj";
+ const string input =
+ """
+
+
+
+
+
+ """;
+
+ project.WriteAllText(input, eofLineBreak: false);
+
+ // Act
+ await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary);
+
+ // Assert
+ project.ReadAllText().Should().Be(input);
+ summary.EditCount.Should().Be(0);
+ }
+
+ [Fact]
+ public async Task Nuget_framework_reference_is_left_unchanged_in_non_build_projects()
+ {
+ // Arrange
+ var project = tempDirectory / "Library.csproj";
+ const string input =
+ """
+
+
+
+
+
+ """;
+
+ project.WriteAllText(input, eofLineBreak: false);
+
+ // Act
+ await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary);
+
+ // Assert
+ project.ReadAllText().Should().Be(input);
+ summary.EditCount.Should().Be(0);
+ }
+
+ [Fact]
+ public async Task Missing_nuget_framework_pin_is_left_unchanged()
+ {
+ // Arrange
+ var project = tempDirectory / "build" / "_build.csproj";
+ const string input =
+ """
+
+
+ net10.0
+
+
+ """;
+
+ project.WriteAllText(input, eofLineBreak: false);
+
+ // Act
+ await new RemoveNugetFrameworkPinStep().ExecuteAsync(context, summary);
+
+ // Assert
+ project.ReadAllText().Should().Be(input);
+ summary.EditCount.Should().Be(0);
+ }
+
+ public void Dispose()
+ {
+ tempDirectory.DeleteDirectory();
+ }
+}
diff --git a/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs
new file mode 100644
index 000000000..85aa03941
--- /dev/null
+++ b/tests/Fallout.Migrate.Specs/RemoveRelatedNugetPinsStepSpecs.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Fallout.Common.IO;
+using Fallout.Migrate.Common;
+using Fallout.Migrate.Steps;
+using FluentAssertions;
+using Xunit;
+
+namespace Fallout.Migrate.Specs;
+
+public class RemoveRelatedNugetPinsStepSpecs : IDisposable
+{
+ private readonly AbsolutePath tempDirectory;
+ private readonly Summary summary = new();
+
+ public RemoveRelatedNugetPinsStepSpecs()
+ {
+ tempDirectory = AbsolutePath.Temp("fallout-remove-related-nuget-pins");
+ }
+
+ [Theory]
+ [InlineData("NuGet.Protocol", "11.0.0")]
+ [InlineData("NuGet.Packaging", "11.0.0-preview.1")]
+ [InlineData("NuGet.Resolver", "12.0.0")]
+ public async Task Explicit_related_nuget_pin_is_removed_for_v11_or_later(
+ string packageName,
+ string falloutVersion)
+ {
+ // Arrange
+ var project = tempDirectory / "_build.csproj";
+ project.WriteAllText(
+ $"""
+
+
+
+
+
+ """, eofLineBreak: false);
+ var context = CreateContext(falloutVersion);
+
+ // Act
+ await new RemoveRelatedNugetPinsStep().ExecuteAsync(context, summary);
+
+ // Assert
+ project.ReadAllText().Should().Be(
+ """
+
+
+
+
+ """);
+ summary.FilesChanged.Should().Be(1);
+ summary.EditCount.Should().Be(1);
+ }
+
+ [Theory]
+ [InlineData("10.9.0")]
+ [InlineData(null)]
+ public async Task Explicit_related_nuget_pin_is_left_unchanged_before_v11(string falloutVersion)
+ {
+ // Arrange
+ var project = tempDirectory / "_build.csproj";
+ const string input =
+ """
+
+
+
+
+
+ """;
+
+ project.WriteAllText(input, eofLineBreak: false);
+ var context = CreateContext(falloutVersion);
+
+ // Act
+ await new RemoveRelatedNugetPinsStep().ExecuteAsync(context, summary);
+
+ // Assert
+ project.ReadAllText().Should().Be(input);
+ summary.EditCount.Should().Be(0);
+ }
+
+ private MigrationContext CreateContext(string falloutVersion) =>
+ new(tempDirectory, dryRun: false, TextWriter.Null)
+ {
+ FalloutVersion = falloutVersion
+ };
+
+ public void Dispose()
+ {
+ tempDirectory.DeleteDirectory();
+ }
+}
\ No newline at end of file