diff --git a/tools/AzDev/AzDev/AzDev.format.ps1xml b/tools/AzDev/AzDev/AzDev.format.ps1xml
index 95ca968c7f10..6e4678aa919e 100644
--- a/tools/AzDev/AzDev/AzDev.format.ps1xml
+++ b/tools/AzDev/AzDev/AzDev.format.ps1xml
@@ -50,5 +50,31 @@
+
+ PSPackageDepDiff
+
+ AzDev.Models.PSModels.PSPackageDepDiff
+
+
+
+
+
+
+ DepName
+
+
+ OldVersion
+
+
+ NewVersion
+
+
+ ParentDep
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/AzDev/AzDev/AzDev.psd1 b/tools/AzDev/AzDev/AzDev.psd1
index 16357d7a65c2..8b1afebe8be6 100644
--- a/tools/AzDev/AzDev/AzDev.psd1
+++ b/tools/AzDev/AzDev/AzDev.psd1
@@ -76,7 +76,8 @@ FunctionsToExport = 'Connect-DevCommonRepo', 'Disconnect-DevCommonRepo'
CmdletsToExport = 'Get-DevContext', 'Set-DevContext',
'Get-DevModule', 'Get-DevProject',
'Update-DevAssembly',
- 'Open-DevSwagger'
+ 'Open-DevSwagger',
+ 'Compare-DevPackageDep'
# Variables to export from this module
VariablesToExport = '*'
diff --git a/tools/AzDev/CHANGELOG.md b/tools/AzDev/CHANGELOG.md
index f124d2a2698c..8deebe022457 100644
--- a/tools/AzDev/CHANGELOG.md
+++ b/tools/AzDev/CHANGELOG.md
@@ -10,6 +10,9 @@
- Quick start templates
- Versioning and publishing AzDev module
+## 2025/11/24
+- Feature: new cmdlet `Compare-DevPackageDep` to compare dependencies between two versions of a nuget package.
+
## 2025/8/26
- Feature: Recognize AutoRest.PowerShell version (v3/v4) for AutoRest-based projects and show as `SubType` in `Get-DevProject` output.
diff --git a/tools/AzDev/README.md b/tools/AzDev/README.md
index 5c37e4ac8a18..82b41424cae5 100644
--- a/tools/AzDev/README.md
+++ b/tools/AzDev/README.md
@@ -10,6 +10,7 @@ Like many other tools, this module targets `net8.0` so always run it in PowerShe
- [Features](#features)
- [Repo inventory](#repo-inventory)
- [Update Assemblies in `src/lib`](#update-assemblies-in-srclib)
+ - [Compare NuGet package dependencies](#compare-nuget-package-dependencies)
- [Connect azure-powershell and azure-powershell-common](#connect-azure-powershell-and-azure-powershell-common)
- [Autorest helper](#autorest-helper)
- [Open swagger online](#open-swagger-online)
@@ -83,6 +84,41 @@ Update-DevAssembly
# Check in all the changes
```
+### Compare NuGet package dependencies
+
+`Compare-DevPackageDep` compares dependencies between two versions of a NuGet package and reports the differences. This is particularly useful when upgrading package versions to understand the impact on the dependency tree.
+
+The cmdlet not only reports direct dependency changes (added/removed/updated), but also recursively compares changed dependencies to show all transitive dependency changes.
+
+```powershell
+# Compare two versions of Azure.Core (TargetFramework defaults to netstandard2.0)
+PS /> Compare-DevPackageDep -PackageName "Azure.Core" -OldVersion "1.47.3" -NewVersion "1.50.0"
+
+DepName OldVersion NewVersion ParentDep
+------- ---------- ---------- ---------
+System.ClientModel 1.6.1 1.8.0 Azure.Core
+System.Threading.Tasks.Extensions 4.5.4 4.6.0 Azure.Core
+System.Runtime.CompilerServices.Unsafe 4.5.3 6.1.0 System.Threading.Tasks.Extensions
+
+# Specify a different target framework
+PS /> Compare-DevPackageDep -PackageName "Newtonsoft.Json" -OldVersion "13.0.1" -NewVersion "13.0.3" -TargetFramework "net462"
+
+# Use -Debug to see all dependencies for both versions
+PS /> Compare-DevPackageDep -PackageName "Azure.Core" -OldVersion "1.47.3" -NewVersion "1.50.0" -Debug
+DEBUG: Comparing Azure.Core from 1.47.3 to 1.50.0 for netstandard2.0
+DEBUG: [DefaultPackageComparisonService] Old version 1.47.3 dependencies:
+DEBUG: - Microsoft.Bcl.AsyncInterfaces 8.0.0
+DEBUG: - System.ClientModel 1.6.1
+DEBUG: - System.Diagnostics.DiagnosticSource 8.0.1
+...
+```
+
+**Parameters:**
+- `PackageName` (required): The NuGet package name to compare
+- `OldVersion` (required): The old/baseline version
+- `NewVersion` (required): The new version to compare against
+- `TargetFramework` (optional): Target framework (default: `netstandard2.0`). Supports tab completion for common values: `netstandard2.0`, `net45`, `net46`, `net47`, `net461`, `net462`
+
### Connect azure-powershell and azure-powershell-common
Help you connect the azure-powershell and azure-powershell-common repositories for developing or debugging.
diff --git a/tools/AzDev/Tests/ServiceTests/PackageComparisonServiceTests.cs b/tools/AzDev/Tests/ServiceTests/PackageComparisonServiceTests.cs
new file mode 100644
index 000000000000..037b079afd71
--- /dev/null
+++ b/tools/AzDev/Tests/ServiceTests/PackageComparisonServiceTests.cs
@@ -0,0 +1,181 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// 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 System.Collections.Generic;
+using System.Linq;
+using AzDev.Models.Dep;
+using AzDev.Services;
+using AzDev.Services.Assembly;
+using AzDev.Services.Dep;
+using Moq;
+
+namespace AzDev.Tests.ServiceTests
+{
+ public class PackageComparisonServiceTests
+ {
+ [Fact]
+ public void ComparePackageDependencies_DetectsVersionChange()
+ {
+ // Arrange
+ var mockNugetService = new Mock();
+
+ // Mock old version dependencies
+ mockNugetService.Setup(x => x.GetPackageDependencies("Azure.Core", "1.47.3", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "System.ClientModel", Version = "1.6.1" },
+ new PackageDep { Id = "System.Text.Json", Version = "8.0.6" }
+ });
+
+ // Mock new version dependencies
+ mockNugetService.Setup(x => x.GetPackageDependencies("Azure.Core", "1.50.0", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "System.ClientModel", Version = "1.8.0" },
+ new PackageDep { Id = "System.Text.Json", Version = "8.0.6" }
+ });
+
+ // Mock System.ClientModel dependencies (for recursive comparison)
+ mockNugetService.Setup(x => x.GetPackageDependencies("System.ClientModel", "1.6.1", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "System.Text.Json", Version = "8.0.6" }
+ });
+
+ mockNugetService.Setup(x => x.GetPackageDependencies("System.ClientModel", "1.8.0", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "System.Text.Json", Version = "8.0.6" }
+ });
+
+ var service = new DefaultDepComparisonService(mockNugetService.Object, NoopLogger.Instance);
+
+ // Act
+ var differences = service.ComparePackageDependencies(
+ "Azure.Core",
+ "1.47.3",
+ "1.50.0",
+ "netstandard2.0").ToList();
+
+ // Assert
+ Assert.NotEmpty(differences);
+ var systemClientModelChange = differences.FirstOrDefault(d => d.DepName == "System.ClientModel");
+ Assert.NotNull(systemClientModelChange);
+ Assert.Equal("Azure.Core", systemClientModelChange.ParentDep);
+ Assert.Equal("1.6.1", systemClientModelChange.OldVersion);
+ Assert.Equal("1.8.0", systemClientModelChange.NewVersion);
+
+ // Verify INugetService was called
+ mockNugetService.Verify(x => x.GetPackageDependencies("Azure.Core", "1.47.3", "netstandard2.0"), Times.Once);
+ mockNugetService.Verify(x => x.GetPackageDependencies("Azure.Core", "1.50.0", "netstandard2.0"), Times.Once);
+ }
+
+ [Fact]
+ public void ComparePackageDependencies_DetectsAddedDependency()
+ {
+ // Arrange
+ var mockNugetService = new Mock();
+
+ mockNugetService.Setup(x => x.GetPackageDependencies("TestPackage", "1.0.0", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "Dependency1", Version = "1.0.0" }
+ });
+
+ mockNugetService.Setup(x => x.GetPackageDependencies("TestPackage", "2.0.0", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "Dependency1", Version = "1.0.0" },
+ new PackageDep { Id = "Dependency2", Version = "2.0.0" }
+ });
+
+ var service = new DefaultDepComparisonService(mockNugetService.Object, NoopLogger.Instance);
+
+ // Act
+ var differences = service.ComparePackageDependencies(
+ "TestPackage",
+ "1.0.0",
+ "2.0.0",
+ "netstandard2.0").ToList();
+
+ // Assert
+ var addedDep = differences.FirstOrDefault(d => d.DepName == "Dependency2");
+ Assert.NotNull(addedDep);
+ Assert.Null(addedDep.OldVersion);
+ Assert.Equal("2.0.0", addedDep.NewVersion);
+ Assert.Equal("TestPackage", addedDep.ParentDep);
+ }
+
+ [Fact]
+ public void ComparePackageDependencies_DetectsRemovedDependency()
+ {
+ // Arrange
+ var mockNugetService = new Mock();
+
+ mockNugetService.Setup(x => x.GetPackageDependencies("TestPackage", "1.0.0", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "Dependency1", Version = "1.0.0" },
+ new PackageDep { Id = "Dependency2", Version = "2.0.0" }
+ });
+
+ mockNugetService.Setup(x => x.GetPackageDependencies("TestPackage", "2.0.0", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "Dependency1", Version = "1.0.0" }
+ });
+
+ var service = new DefaultDepComparisonService(mockNugetService.Object, NoopLogger.Instance);
+
+ // Act
+ var differences = service.ComparePackageDependencies(
+ "TestPackage",
+ "1.0.0",
+ "2.0.0",
+ "netstandard2.0").ToList();
+
+ // Assert
+ var removedDep = differences.FirstOrDefault(d => d.DepName == "Dependency2");
+ Assert.NotNull(removedDep);
+ Assert.Equal("2.0.0", removedDep.OldVersion);
+ Assert.Null(removedDep.NewVersion);
+ Assert.Equal("TestPackage", removedDep.ParentDep);
+ }
+
+ [Fact]
+ public void ComparePackageDependencies_SameVersions_ReturnsEmpty()
+ {
+ // Arrange
+ var mockNugetService = new Mock();
+
+ mockNugetService.Setup(x => x.GetPackageDependencies("TestPackage", "1.0.0", "netstandard2.0"))
+ .Returns(new List
+ {
+ new PackageDep { Id = "Dependency1", Version = "1.0.0" }
+ });
+
+ var service = new DefaultDepComparisonService(mockNugetService.Object, NoopLogger.Instance);
+
+ // Act
+ var differences = service.ComparePackageDependencies(
+ "TestPackage",
+ "1.0.0",
+ "1.0.0",
+ "netstandard2.0").ToList();
+
+ // Assert
+ Assert.Empty(differences);
+ }
+ }
+}
diff --git a/tools/AzDev/spec/package-comparing-cmdlet.md b/tools/AzDev/spec/package-comparing-cmdlet.md
new file mode 100644
index 000000000000..9d2861409f62
--- /dev/null
+++ b/tools/AzDev/spec/package-comparing-cmdlet.md
@@ -0,0 +1,26 @@
+# A package dependency comparing cmdlet
+
+The goal of this cmdlet is to compare two versions of a nuget package and report the differences about dependencies between them.
+
+Note the cmdlet not only reports added/removed dependencies, but also reports dependencies that changed their version between the two package versions. For changed dependencies, it compares the old and new versions recursively to report all changes in the dependency tree.
+
+## Example usage
+
+```powershell
+Compare-DevPackageDep -PackageName "Azure.Core" -OldVersion "1.47.3" -NewVersion "1.50.0" -TargetFramework "netstandard2.0"
+
+# Output:
+DepName OldVersion NewVersion ParentDep
+----------- ---------- ---------- -------------
+System.ClientModel 1.6.1 1.8.0 Azure.Core
+```
+
+## Cmdlet design considerations
+
+- `TargetFramework` is optional and defaults to `netstandard2.0` if not specified. Add argument completer (not validate set) for it. Possible values: `netstandard2.0`, `net45`, `net46`, `net47`, `net461`, `net462`.
+- In debug mode, list all dependencies with their versions for both old and new package versions.
+
+## Implementation details
+
+- Leverage `INugetService` to get dependencies of a package version. Do not re-implement nuget package reading logic.
+- Mock `INugetService` in tests to avoid downloading packages from nuget.org.
diff --git a/tools/AzDev/src/Cmdlets/Dep/ComparePackageDepCmdlet.cs b/tools/AzDev/src/Cmdlets/Dep/ComparePackageDepCmdlet.cs
new file mode 100644
index 000000000000..b92ad73c7f04
--- /dev/null
+++ b/tools/AzDev/src/Cmdlets/Dep/ComparePackageDepCmdlet.cs
@@ -0,0 +1,73 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// 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 System.Linq;
+using System.Management.Automation;
+using AzDev.Models.PSModels;
+using AzDev.Services;
+using AzDev.Services.Dep;
+
+namespace AzDev.Cmdlets.Dep
+{
+ [Cmdlet("Compare", "DevPackageDep")]
+ [OutputType(typeof(PSPackageDepDiff))]
+ public class ComparePackageDepCmdlet : DevCmdletBase
+ {
+ [Parameter(Mandatory = true, Position = 0)]
+ public string PackageName { get; set; }
+
+ [Parameter(Mandatory = true, Position = 1)]
+ public string OldVersion { get; set; }
+
+ [Parameter(Mandatory = true, Position = 2)]
+ public string NewVersion { get; set; }
+
+ [Parameter(Mandatory = false, Position = 3)]
+ [ArgumentCompleter(typeof(TargetFrameworkCompleter))]
+ public string TargetFramework { get; set; } = "netstandard2.0";
+
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+
+ var packageComparisonService = AzDevModule.GetService();
+
+ WriteDebug($"Comparing {PackageName} from {OldVersion} to {NewVersion} for {TargetFramework}");
+
+ var differences = packageComparisonService.ComparePackageDependencies(
+ PackageName,
+ OldVersion,
+ NewVersion,
+ TargetFramework);
+
+ WriteObject(differences.Select(d => new PSPackageDepDiff(d)), true);
+ }
+ }
+
+ public class TargetFrameworkCompleter : IArgumentCompleter
+ {
+ public System.Collections.Generic.IEnumerable CompleteArgument(
+ string commandName,
+ string parameterName,
+ string wordToComplete,
+ System.Management.Automation.Language.CommandAst commandAst,
+ System.Collections.IDictionary fakeBoundParameters)
+ {
+ var frameworks = new[] { "netstandard2.0", "net45", "net46", "net47", "net461", "net462" };
+ return frameworks
+ .Where(f => f.StartsWith(wordToComplete, System.StringComparison.OrdinalIgnoreCase))
+ .Select(f => new CompletionResult(f, f, CompletionResultType.ParameterValue, f));
+ }
+ }
+}
diff --git a/tools/AzDev/src/Models/Dep/PackageDep.cs b/tools/AzDev/src/Models/Dep/PackageDep.cs
new file mode 100644
index 000000000000..d12a192fb99d
--- /dev/null
+++ b/tools/AzDev/src/Models/Dep/PackageDep.cs
@@ -0,0 +1,32 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// 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.
+// ----------------------------------------------------------------------------------
+
+namespace AzDev.Models.Dep
+{
+ ///
+ /// Represents a NuGet package dependency.
+ ///
+ public class PackageDep
+ {
+ ///
+ /// The package ID (name) of the dependency.
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// The version of the dependency.
+ ///
+ public string Version { get; set; }
+ }
+}
diff --git a/tools/AzDev/src/Models/Dep/PackageDepDiff.cs b/tools/AzDev/src/Models/Dep/PackageDepDiff.cs
new file mode 100644
index 000000000000..4254bab67e89
--- /dev/null
+++ b/tools/AzDev/src/Models/Dep/PackageDepDiff.cs
@@ -0,0 +1,42 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// 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.
+// ----------------------------------------------------------------------------------
+
+namespace AzDev.Models.Dep
+{
+ ///
+ /// Represents a difference in package dependencies between two versions.
+ ///
+ public class PackageDepDiff
+ {
+ ///
+ /// The name of the dependency that changed.
+ ///
+ public string DepName { get; set; }
+
+ ///
+ /// The old version of the dependency (null if newly added).
+ ///
+ public string OldVersion { get; set; }
+
+ ///
+ /// The new version of the dependency (null if removed).
+ ///
+ public string NewVersion { get; set; }
+
+ ///
+ /// The parent dependency that caused this change.
+ ///
+ public string ParentDep { get; set; }
+ }
+}
diff --git a/tools/AzDev/src/Models/PSModels/PSPackageDepDiff.cs b/tools/AzDev/src/Models/PSModels/PSPackageDepDiff.cs
new file mode 100644
index 000000000000..81cab039a6a6
--- /dev/null
+++ b/tools/AzDev/src/Models/PSModels/PSPackageDepDiff.cs
@@ -0,0 +1,37 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// 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 AzDev.Models.Dep;
+
+namespace AzDev.Models.PSModels
+{
+ ///
+ /// PowerShell output model for package dependency differences.
+ ///
+ public class PSPackageDepDiff
+ {
+ public string DepName { get; set; }
+ public string OldVersion { get; set; }
+ public string NewVersion { get; set; }
+ public string ParentDep { get; set; }
+
+ public PSPackageDepDiff(PackageDepDiff diff)
+ {
+ DepName = diff.DepName;
+ OldVersion = diff.OldVersion;
+ NewVersion = diff.NewVersion;
+ ParentDep = diff.ParentDep;
+ }
+ }
+}
diff --git a/tools/AzDev/src/Services/Assembly/DefaultNugetService.cs b/tools/AzDev/src/Services/Assembly/DefaultNugetService.cs
index f65afe8650c3..775cbc7194d2 100644
--- a/tools/AzDev/src/Services/Assembly/DefaultNugetService.cs
+++ b/tools/AzDev/src/Services/Assembly/DefaultNugetService.cs
@@ -13,12 +13,15 @@
// ----------------------------------------------------------------------------------
using System;
+using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.IO.Compression;
using System.Linq;
using System.Threading;
+using AzDev.Models.Dep;
using NuGet.Common;
+using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
@@ -86,5 +89,45 @@ public string DownloadAssembly(string packageName, string packageVersion, string
return destAssemblyPath;
}
+
+ public IEnumerable GetPackageDependencies(
+ string packageName,
+ string packageVersion,
+ string targetFramework)
+ {
+ _logger.Debug($"[{nameof(DefaultNugetService)}] Getting dependencies for {packageName} version {packageVersion} for {targetFramework}");
+
+ using var packageStream = new MemoryStream();
+ _findPackageByIdResource.Value.CopyNupkgToStreamAsync(
+ packageName,
+ new NuGetVersion(packageVersion),
+ packageStream,
+ _cache,
+ NullLogger.Instance,
+ default).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ using var packageReader = new PackageArchiveReader(packageStream);
+ var dependencyGroups = packageReader.GetPackageDependencies();
+
+ var framework = NuGetFramework.Parse(targetFramework);
+ var reducer = new FrameworkReducer();
+ var nearestFramework = reducer.GetNearest(framework, dependencyGroups.Select(g => g.TargetFramework));
+
+ if (nearestFramework == null)
+ {
+ _logger.Debug($"[{nameof(DefaultNugetService)}] No compatible dependency group found for {targetFramework}");
+ return Enumerable.Empty();
+ }
+
+ var dependencyGroup = dependencyGroups.FirstOrDefault(g => g.TargetFramework.Equals(nearestFramework));
+ var nugetDependencies = dependencyGroup?.Packages ?? Enumerable.Empty();
+
+ // Convert NuGet SDK model to our model
+ return nugetDependencies.Select(d => new PackageDep
+ {
+ Id = d.Id,
+ Version = d.VersionRange.MinVersion.ToString()
+ });
+ }
}
}
diff --git a/tools/AzDev/src/Services/Assembly/INugetService.cs b/tools/AzDev/src/Services/Assembly/INugetService.cs
index 1fd33e53caca..1eb22faf50ea 100644
--- a/tools/AzDev/src/Services/Assembly/INugetService.cs
+++ b/tools/AzDev/src/Services/Assembly/INugetService.cs
@@ -12,6 +12,9 @@
// limitations under the License.
// ----------------------------------------------------------------------------------
+using System.Collections.Generic;
+using AzDev.Models.Dep;
+
namespace AzDev.Services.Assembly
{
///
@@ -29,5 +32,17 @@ internal interface INugetService
///
/// Path to the downloaded assembly, excluding runtime assemblies.
string DownloadAssembly(string packageName, string packageVersion, string targetFramework, string destinationPath, bool downloadRuntimes);
+
+ ///
+ /// Gets the dependencies of a package version for a specific target framework.
+ ///
+ /// The name of the package.
+ /// The version of the package.
+ /// The target framework.
+ /// Collection of package dependencies.
+ IEnumerable GetPackageDependencies(
+ string packageName,
+ string packageVersion,
+ string targetFramework);
}
}
diff --git a/tools/AzDev/src/Services/AzDevModule.cs b/tools/AzDev/src/Services/AzDevModule.cs
index 4f7d2c4ac3be..dfa2eac35f71 100644
--- a/tools/AzDev/src/Services/AzDevModule.cs
+++ b/tools/AzDev/src/Services/AzDevModule.cs
@@ -16,6 +16,7 @@
using System.IO.Abstractions;
using System.Runtime.CompilerServices;
using AzDev.Services.Assembly;
+using AzDev.Services.Dep;
using Microsoft.Extensions.DependencyInjection;
[assembly: InternalsVisibleTo("Tests")]
@@ -36,6 +37,7 @@ public static void Initialize()
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/tools/AzDev/src/Services/Dep/DefaultDepComparisonService.cs b/tools/AzDev/src/Services/Dep/DefaultDepComparisonService.cs
new file mode 100644
index 000000000000..1bb73c09be98
--- /dev/null
+++ b/tools/AzDev/src/Services/Dep/DefaultDepComparisonService.cs
@@ -0,0 +1,115 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using AzDev.Models.Dep;
+using AzDev.Services.Assembly;
+
+namespace AzDev.Services.Dep
+{
+ internal class DefaultDepComparisonService : IDepComparisonService
+ {
+ private readonly INugetService _nugetService;
+ private readonly ILogger _logger;
+
+ public DefaultDepComparisonService(INugetService nugetService, ILogger logger)
+ {
+ _nugetService = nugetService;
+ _logger = logger;
+ }
+
+ public IEnumerable ComparePackageDependencies(
+ string packageName,
+ string oldVersion,
+ string newVersion,
+ string targetFramework)
+ {
+ _logger.Debug($"[{nameof(DefaultDepComparisonService)}] Comparing {packageName} from {oldVersion} to {newVersion} for {targetFramework}");
+
+ var oldDeps = _nugetService.GetPackageDependencies(packageName, oldVersion, targetFramework).ToList();
+ var newDeps = _nugetService.GetPackageDependencies(packageName, newVersion, targetFramework).ToList();
+
+ // Debug mode: list all dependencies
+ _logger.Debug($"[{nameof(DefaultDepComparisonService)}] Old version {oldVersion} dependencies:");
+ foreach (var dep in oldDeps)
+ {
+ _logger.Debug($" - {dep.Id} {dep.Version}");
+ }
+ _logger.Debug($"[{nameof(DefaultDepComparisonService)}] New version {newVersion} dependencies:");
+ foreach (var dep in newDeps)
+ {
+ _logger.Debug($" - {dep.Id} {dep.Version}");
+ }
+
+ var results = new List();
+
+ // Find dependencies that were added or changed
+ foreach (var newDep in newDeps)
+ {
+ var oldDep = oldDeps.FirstOrDefault(d => d.Id.Equals(newDep.Id, StringComparison.OrdinalIgnoreCase));
+
+ if (oldDep == null)
+ {
+ // Dependency was added
+ results.Add(new PackageDepDiff
+ {
+ DepName = newDep.Id,
+ OldVersion = null,
+ NewVersion = newDep.Version,
+ ParentDep = packageName
+ });
+ }
+ else if (!oldDep.Version.Equals(newDep.Version))
+ {
+ // Dependency version changed - add the immediate change
+ results.Add(new PackageDepDiff
+ {
+ DepName = newDep.Id,
+ OldVersion = oldDep.Version,
+ NewVersion = newDep.Version,
+ ParentDep = packageName
+ });
+
+ // Recursively compare the changed dependency
+ var recursiveChanges = ComparePackageDependencies(
+ newDep.Id,
+ oldDep.Version,
+ newDep.Version,
+ targetFramework);
+
+ results.AddRange(recursiveChanges);
+ }
+ }
+
+ // Find dependencies that were removed
+ foreach (var oldDep in oldDeps)
+ {
+ if (!newDeps.Any(d => d.Id.Equals(oldDep.Id, StringComparison.OrdinalIgnoreCase)))
+ {
+ results.Add(new PackageDepDiff
+ {
+ DepName = oldDep.Id,
+ OldVersion = oldDep.Version,
+ NewVersion = null,
+ ParentDep = packageName
+ });
+ }
+ }
+
+ return results;
+ }
+ }
+}
diff --git a/tools/AzDev/src/Services/Dep/IDepComparisonService.cs b/tools/AzDev/src/Services/Dep/IDepComparisonService.cs
new file mode 100644
index 000000000000..2267f179dcc3
--- /dev/null
+++ b/tools/AzDev/src/Services/Dep/IDepComparisonService.cs
@@ -0,0 +1,39 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// 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 System.Collections.Generic;
+using AzDev.Models.Dep;
+
+namespace AzDev.Services.Dep
+{
+ ///
+ /// Interface for comparing package dependencies.
+ ///
+ internal interface IDepComparisonService
+ {
+ ///
+ /// Compares dependencies between two versions of a package.
+ ///
+ /// The name of the package to compare.
+ /// The old version of the package.
+ /// The new version of the package.
+ /// The target framework (e.g., "netstandard2.0").
+ /// A list of dependency differences.
+ IEnumerable ComparePackageDependencies(
+ string packageName,
+ string oldVersion,
+ string newVersion,
+ string targetFramework);
+ }
+}