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..0d5f219d2
--- /dev/null
+++ b/src/Fallout.Migrate/Steps/RemoveNugetFrameworkPinStep.cs
@@ -0,0 +1,39 @@
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Fallout.Migrate.Common;
+
+namespace Fallout.Migrate.Steps;
+
+///
+/// Removes explicit NuGet.Framework, NuGet.Protocol, NuGet.Packaging, and
+/// NuGet.Resolver package pins from project files. The pins are no longer needed.
+///
+internal sealed class RemoveNugetFrameworkPinStep : IMigrationStep
+{
+ private static readonly Regex explicitPinPattern = new(
+ @"^[ \t]*\r\n]*\bInclude=""NuGet\.Frameworks"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?",
+ RegexOptions.Compiled | RegexOptions.Multiline);
+
+ ///
+ public Task ExecuteAsync(MigrationContext context, Summary summary)
+ {
+ foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "*.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..fa1381551
--- /dev/null
+++ b/src/Fallout.Migrate/Steps/RemoveRelatedNugetPinsStep.cs
@@ -0,0 +1,56 @@
+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
+{
+ private const int MinimumFalloutMajor = 11;
+
+ private static readonly Regex explicitPinPattern = new(
+ @"^[ \t]*\r\n]*\bInclude=""NuGet\.(?:Protocol|Packaging|Resolver)"")(?=[^>\r\n]*\bVersion=""[^""]+"")[^>\r\n]*/>[ \t]*\r?\n?",
+ RegexOptions.Compiled | RegexOptions.Multiline);
+
+ ///
+ public Task ExecuteAsync(MigrationContext context, Summary summary)
+ {
+ if (!RunsFor(context.FalloutVersion))
+ {
+ return Task.CompletedTask;
+ }
+
+ foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, "*.csproj"))
+ {
+ MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static bool RunsFor(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);
+ }
+}
\ No newline at end of file
diff --git a/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs
new file mode 100644
index 000000000..122882353
--- /dev/null
+++ b/tests/Fallout.Migrate.Specs/RemoveNugetFrameworkPinStepSpecs.cs
@@ -0,0 +1,108 @@
+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 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);
+ }
+
+ [Fact]
+ public async Task Explicit_nuget_framework_pin_is_removed()
+ {
+ // Arrange
+ var project = tempDirectory / "Library.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 Nuget_framework_reference_without_version_is_left_unchanged()
+ {
+ // 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..f98b728d5
--- /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 / "Library.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 / "Library.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