Skip to content
Draft
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
12 changes: 6 additions & 6 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<ItemGroup>
<PackageVersion Include="Microsoft.Build" Version="17.15.0-preview-25277-114" />
<PackageVersion Include="Microsoft.Build.Locator" Version="1.9.1" />
<PackageVersion Include="Spectre.Console" Version="0.50.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta7.25380.108" />
<PackageVersion Include="Spectre.Console" Version="0.51.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.1.25451.107" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.Sbom.Api" Version="2.2.8" />
<PackageVersion Include="CycloneDX.Core" Version="6.0.5" />
Expand All @@ -15,9 +15,9 @@
<PackageVersion Include="NuGet.Versioning" Version="6.14.0" />
<PackageVersion Include="NuGet.Packaging" Version="6.14.0" />
<PackageVersion Include="NuGet.Frameworks" Version="6.14.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
</Project>
258 changes: 258 additions & 0 deletions README-DependencyGraph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
# Recursive NuGet Dependency Graph Feature

This document describes the new recursive NuGet dependency graph functionality added to the `bld` tool, which was implemented based on the existing `NugetMetadataService.GetLatestVersionWithFrameworkCheckAsync` logic.

## Overview

The new dependency graph feature recursively enumerates packages in `DependencyGroups` using version ranges to determine all transitively referenced packages. It provides both a hierarchical graph structure and a flat list of all packages in the dependency tree.

## Key Features

### Efficient Caching
- **Request Caching**: Packages are cached to avoid duplicate NuGet API requests
- **Pre-population**: Existing package references from `OutdatedService` are pre-cached to minimize redundant network calls
- **Smart Deduplication**: The same package referenced multiple times is fetched only once

### Graph Structure
- **Tree Representation**: `DependencyGraphNode` objects form a hierarchical tree
- **Flat List**: All packages are also provided in a flattened `PackageReference` collection
- **Metadata Rich**: Each package includes version, target framework, depth, prerelease status, and more

### Performance Optimizations
- **Parallel Processing**: Uses `Parallel.ForEachAsync` for concurrent package resolution
- **Depth Limiting**: Configurable maximum depth to prevent infinite traversal
- **Cycle Detection**: Built-in cycle detection to handle circular dependencies

## Architecture

### Core Components

#### 1. `DependencyGraphModels.cs`
Contains the data models for representing dependency graphs:

```csharp
// Represents a single node in the dependency tree
internal record DependencyGraphNode {
public string PackageId { get; init; }
public string Version { get; init; }
public string TargetFramework { get; init; }
public IReadOnlyList<DependencyGraphNode> Dependencies { get; init; }
public int Depth { get; init; }
// ... more properties
}

// Complete dependency graph with both tree and flat representations
internal record PackageDependencyGraph {
public IReadOnlyList<DependencyGraphNode> RootPackages { get; init; }
public IReadOnlyList<PackageReference> AllPackages { get; init; }
public IReadOnlyList<UnresolvedPackage> UnresolvedPackages { get; init; }
// ... analysis properties
}
```

#### 2. `RecursiveDependencyResolver.cs`
The core service that performs recursive dependency resolution:

```csharp
internal class RecursiveDependencyResolver {
// Resolves all transitive dependencies with caching and cycle detection
public async Task<PackageDependencyGraph> ResolveTransitiveDependenciesAsync(
IEnumerable<string> rootPackageIds,
DependencyResolutionOptions options,
Dictionary<string, OutdatedService.PackageInfoContainer>? existingPackageReferences = null,
CancellationToken cancellationToken = default)
}
```

#### 3. `DependencyGraphService.cs`
High-level service that orchestrates dependency graph building and analysis:

```csharp
internal class DependencyGraphService {
// Builds comprehensive dependency graph from OutdatedService package references
public async Task<PackageDependencyGraph> BuildDependencyGraphAsync(
Dictionary<string, OutdatedService.PackageInfoContainer> allPackageReferences,
bool includePrerelease = false,
int maxDepth = 5,
CancellationToken cancellationToken = default)

// Analyzes the graph for patterns and statistics
public DependencyGraphAnalysis AnalyzeDependencyGraph(PackageDependencyGraph graph)
}
```

#### 4. `OutdatedServiceExtensions.cs`
Extension methods that integrate with the existing `OutdatedService`:

```csharp
// Extension method for Dictionary<string, PackageInfoContainer>
public static async Task<PackageDependencyGraph> BuildAndShowDependencyGraphAsync(
this Dictionary<string, OutdatedService.PackageInfoContainer> allPackageReferences,
IConsoleOutput console,
bool includePrerelease = false,
int maxDepth = 5,
bool showAnalysis = true,
CancellationToken cancellationToken = default)
```

## Usage Examples

### Basic Usage in OutdatedService

The new functionality integrates seamlessly with the existing `OutdatedService`:

```csharp
// In OutdatedService.cs - new method added
public async Task<int> BuildDependencyGraphAsync(
string rootPath,
bool includePrerelease = false,
int maxDepth = 5,
bool showAnalysis = true,
string? exportPath = null,
CancellationToken cancellationToken = default)
{
// ... discover packages (similar to CheckOutdatedPackagesAsync)

// Build dependency graph using extension method
var dependencyGraph = await allPackageReferences.BuildAndShowDependencyGraphAsync(
_console,
includePrerelease,
maxDepth,
showAnalysis,
cancellationToken);

// Export if requested
if (!string.IsNullOrEmpty(exportPath)) {
await dependencyGraph.ExportDependencyGraphAsync(exportPath, "json", _console);
}

return 0;
}
```

### Direct Usage

You can also use the components directly:

```csharp
// Create resolver
var options = new NugetMetadataOptions();
using var httpClient = NugetMetadataService.CreateHttpClient(options);
var resolver = new RecursiveDependencyResolver(httpClient, options, console);

// Configure resolution
var resolutionOptions = new DependencyResolutionOptions {
MaxDepth = 5,
AllowPrerelease = false,
TargetFrameworks = new[] { "net8.0" }
};

// Resolve dependencies
var dependencyGraph = await resolver.ResolveTransitiveDependenciesAsync(
rootPackageIds,
resolutionOptions,
existingPackageReferences, // Pre-populate cache
cancellationToken);

// Analyze results
var graphService = new DependencyGraphService(console);
var analysis = graphService.AnalyzeDependencyGraph(dependencyGraph);
```

## Integration with Existing Code

### How it leverages GetLatestVersionWithFrameworkCheckAsync

The implementation reuses the existing NuGet metadata retrieval logic:

1. **Same API Calls**: Uses `NugetMetadataService.GetLatestVersionWithFrameworkCheckAsync` for all package lookups
2. **Framework Compatibility**: Leverages the same framework compatibility logic with `FrameworkReducer` and `DefaultCompatibilityProvider`
3. **Dependency Groups**: Recursively processes the `Dependencies` property from `PackageVersionResult.Dependencies`
4. **Version Ranges**: Respects version range constraints from `Dependency.Range` when resolving child packages

### Cache Integration with OutdatedService

The resolver intelligently integrates with `OutdatedService.allPackageReferences`:

```csharp
// Pre-populate cache with existing package references
private async Task PrePopulateCacheAsync(
Dictionary<string, OutdatedService.PackageInfoContainer> existingPackageReferences,
DependencyResolutionOptions options,
CancellationToken cancellationToken)
{
// For each existing package, fetch and cache its metadata
// This ensures packages already discovered by OutdatedService are not fetched again
}
```

## Output and Analysis

### Console Output
The functionality provides rich console output including:
- Progress indicators during resolution
- Summary tables showing package counts and statistics
- Dependency analysis with most common packages
- Version conflict detection and reporting
- Performance metrics

### Export Formats
Dependency graphs can be exported in multiple formats:
- **JSON**: Complete graph structure with all metadata
- **CSV**: Flat list of packages with key properties
- **DOT**: GraphViz format for visualization

### Analysis Features
- **Package Distribution**: Microsoft vs third-party package breakdown
- **Depth Analysis**: Distribution of packages by dependency depth
- **Common Dependencies**: Most frequently referenced packages across the tree
- **Version Conflicts**: Detection of packages with multiple versions
- **Unresolved Packages**: Tracking of packages that couldn't be resolved

## Performance Characteristics

### Efficient Network Usage
- **Caching**: Aggressive caching prevents duplicate API calls
- **Parallel Processing**: Concurrent resolution of independent packages
- **Pre-population**: Reuses existing OutdatedService lookups

### Memory Efficiency
- **Deduplication**: Packages appearing multiple times are stored once in the flat list
- **Streaming**: Processes packages as they're discovered rather than loading everything upfront
- **Disposal**: Proper disposal of HTTP clients and resources

### Scalability
- **Depth Limiting**: Prevents runaway recursion in complex dependency trees
- **Cycle Detection**: Handles circular dependencies gracefully
- **Configurable Limits**: Adjustable parallelism and depth limits

## Error Handling

The implementation includes comprehensive error handling:
- **Network Failures**: Graceful handling of API timeouts and failures
- **Invalid Packages**: Tracking of packages that cannot be resolved
- **Version Conflicts**: Detection and reporting without stopping resolution
- **Circular Dependencies**: Cycle detection with configurable behavior

## Future Enhancements

Potential areas for future improvement:
- **Caching Persistence**: Save cache to disk for subsequent runs
- **Incremental Updates**: Only resolve changed packages
- **Visualization**: Built-in graph visualization capabilities
- **Conflict Resolution**: Automatic resolution of version conflicts
- **Policy Engine**: Configurable policies for dependency selection

## Testing

Basic unit tests are provided in `RecursiveDependencyResolverTests.cs`:
- Resolution of simple packages
- Depth limiting validation
- Multiple root package handling
- Cache effectiveness verification

Integration tests could be added to validate:
- Real-world dependency trees
- Performance characteristics
- Export functionality
- Analysis accuracy
18 changes: 0 additions & 18 deletions TestSln.sln

This file was deleted.

25 changes: 19 additions & 6 deletions bld.Tests/NuGetFrameworkTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//using XUnit.Framework;

using bld.Infrastructure;
using NuGet.Frameworks;
using Xunit.Abstractions;

Expand All @@ -8,18 +9,17 @@ namespace bld.Tests;
public class DotNetTests(ITestOutputHelper Console) {
[Fact]
public void PathCombineLinux() {
Assert.Throws<ArgumentNullException>(() => Path.Combine("/mnt/d/tests", null, "child"));
Assert.Throws<ArgumentNullException>(() => Path.Combine("/mnt/d/tests", null!, "child"));
}

[Fact]
public void PathCombineWin() {
Assert.Throws<ArgumentNullException>(() => Path.Combine("d:\\tests", null, "child"));
Assert.Throws<ArgumentNullException>(() => Path.Combine("d:\\tests", null!, "child"));
}
}
public class NuGetFrameworkTests(ITestOutputHelper Console) {
[Fact]
public void Test1() {
string[] tfms = new[]

string[] tfms = new[]
{
".NETStandard,Version=v2.0",
".NETFramework,Version=v4.7.2",
Expand All @@ -31,15 +31,28 @@ public void Test1() {
".NETFramework4.6.2",
"net8.0",
"net9.0",
"net10.0",
"net10",
"net100",
"net9",
"net9000",
"net472x",
};

[Fact]
public void Test1() {


foreach (var tfm in tfms) {
var framework = NuGetFramework.Parse(tfm);
string fx = framework.Framework;
string normalizedTfm = framework.GetShortFolderName();
Console.WriteLine($"Original: {tfm}, Normalized: {normalizedTfm}");

Console.WriteLine($"Original: {tfm}, Fx '{fx}' Standard: '{normalizedTfm}'");

// Test that our normalization handles the net100 -> net10.0 case
Assert.NotEqual("net100", normalizedTfm);

}
}
}
Loading