Skip to content

Instantly share code, notes, and snippets.

@guibranco
Created September 16, 2025 16:29
Show Gist options
  • Select an option

  • Save guibranco/ca35bd1049e3473e680bdb1278b40143 to your computer and use it in GitHub Desktop.

Select an option

Save guibranco/ca35bd1049e3473e680bdb1278b40143 to your computer and use it in GitHub Desktop.
Check NuGet conflicts between dependencies
#r "nuget: NuGet.Protocol, 6.9.1"
#r "nuget: NuGet.Versioning, 6.9.1"
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Text.Json;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
Console.WriteLine("🔍 Starting NuGet dependency consistency check...");
var resolvedPackagesJson = "resolved-packages.json";
await ExportResolvedPackages(resolvedPackagesJson);
var sw = Stopwatch.StartNew();
var resolved = await ParseResolvedPackages(jsonFile);
sw.Stop();
Console.WriteLine($"✅ Loaded resolved packages in {sw.ElapsedMilliseconds}ms");
var failed = false;
foreach (var pkg in resolved)
{
foreach (var dependent in resolved)
{
if (pkg.Key == dependent.Key)
continue;
var declaredRanges = await GetDeclaredDependencies(dependent.Key, dependent.Value);
if (declaredRanges.TryGetValue(pkg.Key, out var expectedRange))
{
if (!expectedRange.Satisfies(pkg.Value))
{
Console.WriteLine(
$"❌ {pkg.Key} @ {pkg.Value} is too new for {dependent.Key}, which requires {expectedRange}"
);
failed = true;
}
}
}
}
if (failed)
{
Console.WriteLine("🚫 Dependency version conflicts found.");
Environment.Exit(1);
}
else
{
Console.WriteLine("✅ All dependencies are within expected ranges.");
}
async Task ExportResolvedPackages(string outputFile)
{
var psi = new ProcessStartInfo("dotnet", "list package --include-transitive --format json")
{
RedirectStandardOutput = true,
};
using var process = Process.Start(psi);
var output = await process.StandardOutput.ReadToEndAsync();
await File.WriteAllTextAsync(outputFile, output);
process.WaitForExit();
}
async Task<Dictionary<string, NuGetVersion>> ParseResolvedPackages(string jsonFile)
{
using var stream = File.OpenRead(jsonFile);
using var doc = await JsonDocument.ParseAsync(stream);
var dict = new ConcurrentDictionary<string, NuGetVersion>(StringComparer.OrdinalIgnoreCase);
if (!doc.RootElement.TryGetProperty("projects", out var projectsElement))
{
Console.WriteLine("⚠️ No 'projects' section found.");
return new Dictionary<string, NuGetVersion>(dict);
}
Parallel.ForEach(
projectsElement.EnumerateArray(),
project =>
{
if (!project.TryGetProperty("frameworks", out var frameworksElement))
return;
foreach (var framework in frameworksElement.EnumerateArray())
{
void ExtractPackages(JsonElement packages)
{
foreach (var pkg in packages.EnumerateArray())
{
var id = pkg.GetProperty("id").GetString();
var version = pkg.GetProperty("resolvedVersion").GetString();
if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(version))
{
dict.AddOrUpdate(
id,
NuGetVersion.Parse(version),
(_, existing) =>
NuGetVersionComparer.Default.Compare(
NuGetVersion.Parse(version),
existing
) > 0
? NuGetVersion.Parse(version)
: existing
);
}
}
}
if (framework.TryGetProperty("topLevelPackages", out var topLevelPackages))
ExtractPackages(topLevelPackages);
if (framework.TryGetProperty("transitivePackages", out var transitivePackages))
ExtractPackages(transitivePackages);
}
}
);
return new Dictionary<string, NuGetVersion>(dict);
}
async Task<Dictionary<string, VersionRange>> GetDeclaredDependencies(
string packageId,
NuGetVersion version
)
{
var output = new Dictionary<string, VersionRange>(StringComparer.OrdinalIgnoreCase);
string url = $"https://www.nuget.org/api/v2/package/{packageId}/{version}";
string tempPath = Path.Combine(Path.GetTempPath(), $"{packageId}.{version}.nupkg");
using var client = new HttpClient();
var bytes = await client.GetByteArrayAsync(url);
await File.WriteAllBytesAsync(tempPath, bytes);
using var zip = ZipFile.OpenRead(tempPath);
foreach (var entry in zip.Entries)
{
if (entry.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase))
{
using var reader = new StreamReader(entry.Open());
var content = await reader.ReadToEndAsync();
var doc = new System.Xml.XmlDocument();
doc.LoadXml(content);
var nsMgr = new System.Xml.XmlNamespaceManager(doc.NameTable);
nsMgr.AddNamespace("ns", "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd");
var dependencies = doc.SelectNodes("//ns:dependency", nsMgr);
foreach (System.Xml.XmlNode node in dependencies)
{
var id = node.Attributes["id"]?.Value;
var range = node.Attributes["version"]?.Value;
if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(range))
{
output[id] = VersionRange.Parse(range);
}
}
}
}
return output;
}
List<(string Id, NuGetVersion Version)> GetDeclaredDependencies(
string packageId,
NuGetVersion version
)
{
var deps = new List<(string, NuGetVersion)>();
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var path = Path.Combine(
userProfile,
".nuget",
"packages",
packageId.ToLowerInvariant(),
version.ToString().ToLowerInvariant(),
$"{packageId.ToLowerInvariant()}.nuspec"
);
if (!File.Exists(path))
{
Console.WriteLine($"❌ .nuspec not found for {packageId} {version} at: {path}");
return deps;
}
var doc = new XmlDocument();
doc.Load(path);
var nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("n", "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd");
var dependencyNodes = doc.SelectNodes("//n:dependencies/n:group/n:dependency", nsmgr);
foreach (XmlNode dep in dependencyNodes)
{
var id = dep.Attributes["id"]?.Value;
var ver = dep.Attributes["version"]?.Value;
if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(ver))
{
try
{
var range = VersionRange.Parse(ver);
if (range.HasLowerBound)
deps.Add((id, range.MinVersion));
}
catch
{
Console.WriteLine($"⚠️ Could not parse version range for {id}: {ver}");
}
}
}
return deps;
}
using System.Xml;
using System.Text.RegularExpressions;
public class NuGetConfig
{
public Dictionary<string, string> PackageSources { get; set; } = new();
public Dictionary<string, List<string>> SourceToPatterns { get; set; } = new();
// pattern -> list of feeds
public Dictionary<string, List<string>> PatternToSources { get; set; } = new();
public static NuGetConfig Parse(string configPath)
{
var config = new NuGetConfig();
var doc = new XmlDocument();
doc.Load(configPath);
var nsmgr = new XmlNamespaceManager(doc.NameTable);
var packageSourcesNode = doc.SelectSingleNode("//configuration/packageSources");
if (packageSourcesNode != null)
{
var clear = packageSourcesNode.SelectSingleNode("clear");
if (clear != null)
{
config.PackageSources.Clear();
}
foreach (XmlNode add in packageSourcesNode.SelectNodes("add"))
{
var key = add.Attributes["key"]?.Value;
var value = add.Attributes["value"]?.Value;
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value))
{
config.PackageSources[key] = value;
}
}
}
var sourceMappingNode = doc.SelectSingleNode("//configuration/packageSourceMapping");
if (sourceMappingNode != null)
{
foreach (XmlNode packageSource in sourceMappingNode.SelectNodes("packageSource"))
{
var key = packageSource.Attributes["key"]?.Value;
if (!string.IsNullOrEmpty(key))
{
config.SourceToPatterns[key] = new List<string>();
foreach (XmlNode patternNode in packageSource.SelectNodes("package"))
{
var pattern = patternNode.Attributes["pattern"]?.Value;
if (!string.IsNullOrWhiteSpace(pattern))
{
config.SourceToPatterns[key].Add(pattern);
if (!config.PatternToSources.ContainsKey(pattern))
config.PatternToSources[pattern] = new List<string>();
if (config.PackageSources.TryGetValue(key, out var url))
{
config.PatternToSources[pattern].Add(url);
}
}
}
}
}
}
return config;
}
// Get all source base URLs
public List<string> GetAllSourceUrls()
{
return PackageSources.Values.ToList();
}
// Get matching sources for a given package id
public List<string> GetSourcesForPackage(string packageId)
{
var matched = new List<string>();
foreach (var (pattern, sources) in PatternToSources)
{
if (PatternMatches(packageId, pattern))
matched.AddRange(sources);
}
return matched.Distinct().ToList();
}
private static bool PatternMatches(string text, string pattern)
{
var regex = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
return Regex.IsMatch(text, regex, RegexOptions.IgnoreCase);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment