Meziantou.Framework.DependencyScanning
3.0.20
Prefix Reserved
dotnet add package Meziantou.Framework.DependencyScanning --version 3.0.20
NuGet\Install-Package Meziantou.Framework.DependencyScanning -Version 3.0.20
<PackageReference Include="Meziantou.Framework.DependencyScanning" Version="3.0.20" />
<PackageVersion Include="Meziantou.Framework.DependencyScanning" Version="3.0.20" />
<PackageReference Include="Meziantou.Framework.DependencyScanning" />
paket add Meziantou.Framework.DependencyScanning --version 3.0.20
#r "nuget: Meziantou.Framework.DependencyScanning, 3.0.20"
#:package Meziantou.Framework.DependencyScanning@3.0.20
#addin nuget:?package=Meziantou.Framework.DependencyScanning&version=3.0.20
#tool nuget:?package=Meziantou.Framework.DependencyScanning&version=3.0.20
Meziantou.Framework.DependencyScanning
A .NET library for scanning source code directories and files to discover and manage project dependencies across multiple package ecosystems and configuration formats.
Features
- Multi-format Support: Scan dependencies from various project files and configuration formats
- Multiple Package Ecosystems: NuGet, npm, PyPI, Docker, Swift packages, Helm Charts, and more
- Parallel Scanning: High-performance parallel file scanning with configurable degree of parallelism
- Dependency Updates: Locate and update dependency versions programmatically
- Customizable Scanning: Filter by file patterns, dependency types, and custom predicates
Supported Dependency Types
The library can detect the following dependency types:
- NuGet - .NET packages from NuGet.org
- Npm - JavaScript packages from npmjs.com
- PyPi - Python packages from PyPI
- DockerImage - Docker container images
- GitReference - Git submodules and references
- DotNetSdk - .NET SDK versions
- DotNetTargetFramework - .NET target frameworks
- GitHubActions - GitHub Actions workflows and reusable workflows
- AzureDevOpsVMPool - Azure DevOps VM pool images
- AzureDevOpsTask - Azure DevOps pipeline tasks
- AzureDevOpsTemplate - Azure DevOps pipeline templates
- HelmChart - Helm chart dependencies
- RenovateConfiguration - Renovate configuration extends
- SwiftPackage - Swift Package Manager dependencies
- RustCrate - Rust crates from Cargo manifests and lockfiles
- GoModule - Go modules from
go.modandgo.sum - JavaPackage - Java packages from Maven and Gradle
- PhpPackage - PHP packages from Composer
- MSBuildProjectReference - MSBuild project references
- DotNetAssemblyReference - .NET assembly file references
- RubyGem - Ruby gem packages
- AgentPlugin - Claude Code and GitHub Copilot plugins referenced by a plugin marketplace
- AgentPluginMarketplace - Claude Code and GitHub Copilot plugin marketplaces
Scanner Notes
- Docker images (Dockerfile
FROM,COPY --from,RUN --mount=...,from=and# syntax=, Docker Compose and Kubernetes manifests, GitHub Actionscontainer,servicesanddocker://references, Azure Pipelines containers and services): the tag is the part after the last:that follows the last/, solocalhost:5000/image:1.0is reported aslocalhost:5000/imageversion1.0. An image pinned by digest (image@sha256:...orimage:tag@sha256:...) is reported with the digest as its version. This version is not updatable, and the digest and tag are available inDependency.Metadataunder thedigestandtagkeys. In a Dockerfile, only references with a tag or a digest are reported (except# syntax=), so build stage names are not mistaken for images. - Dockerfile: the bodies of here-documents (
RUN <<EOF,COPY <<-"EOF" /file) are skipped. When the whole image or the whole tag of aFROMcomes from anARGdeclared with a literal default value before the firstFROM(ARG NODE_VERSION=20thenFROM node:${NODE_VERSION}), the default value is reported, and its location in theARGinstruction is updatable unless severalFROMinstructions use the same argument. Other variable references are reported with a location that is not updatable. - Docker Compose:
compose.yaml,compose.yml,docker-compose.yaml,docker-compose.yml, and theircompose.*.yamlanddocker-compose.*.ymlvariants are scanned forservices.<name>.image. The image of a service that has abuildsection is the name given to the image built from source, so it is not reported. - Kubernetes: YAML manifests of
Pod,PodTemplate,Deployment,StatefulSet,DaemonSet,ReplicaSet,ReplicationController,JobandCronJobobjects, includingListobjects and files with several documents, are scanned for the images ofcontainers,initContainersandephemeralContainers. Custom resources are not scanned. Files that contain{{, such as Helm chart templates, are templates rather than manifests, so they are not scanned. - Variables: names or versions that contain a variable reference, such as
image: node:${NODE_VERSION}, are reported with a location that is not updatable. - MSBuild: every
*.*projfile (.csproj,.vcxproj,.sqlproj,.wixproj,.esproj, ...), except the non-MSBuild.pbxprojand.vdproj, and.propsand.targetsfiles are scanned. Like MSBuild, item types, metadata names and property names are case-insensitive (<packagereference Include="A" version="1.0.0" />), while keywords such asItemGroupandIncludeare not. AnIncludeorUpdatelist, such asInclude="A;B", is reported as one dependency per item, and names and versions are trimmed. When an item declares several packages, their shared version is not updatable. An SDK without a version (Sdk="Name",#:sdk Namein a file-based app) is not reported, as its version is built in or comes fromglobal.json. - File-based apps: like the .NET SDK, the
#:directives are read until the first token of the file. Comments and other preprocessor directives (#nullable,#region,#pragma, ...) do not stop the scan, but#ifdoes. - NuGet:
packages.configandpackages.<project>.configare scanned. Apackages.<project>.configfile is associated with the<project>.*projproject in the same directory, andpackages.configwith the other projects; the package paths of the associated projects (HintPath,ImportandError) are reported too, when the<id>.<version>folder is a whole path segment. The assembly version of aReference(Include="Name, Version=1.0.0.0") is not the package version, so it is not reported as a dependency, and it is not changed when the package version is updated. It is available inDependency.Metadataunder theassemblyVersionkey, and its location, which accepts only valid assembly versions, under theassemblyVersionLocationkey..nuspecdependencies without a version are reported without a version. The resolved versions in NuGet lock files (packages.lock.jsonandpackages.<project>.lock.json) andproject.assets.jsonare reported once per package and version, and are not updatable. - YAML files: a value is only updatable when its text in the file is exactly its value (plain scalars, and single-line quoted scalars without escape sequences). Block scalars (
|,>), escaped or multi-line values are reported with a location that is not updatable. A value referenced through an alias (*name) is reported once. - GitHub Actions: workflows in
.github/workflowsand action metadata files (action.yml,action.yaml) anywhere in the repository are scanned. Local actions and reusable workflows (uses: ./...) are not reported. - Agent plugins: plugin marketplaces (
.claude-plugin/marketplace.json,.github/plugin/marketplace.json) are scanned for plugins hosted in a git repository (github,urlandgit-subdirsources, andowner/repo#refor git URL strings), reported asAgentPlugin, and for plugins published to npm, reported asNpm. The version of a git source is itsshawhen there is one, and itsrefotherwise. Settings files (.claude/settings.json,.github/copilot/settings.jsonand theirsettings.local.jsoncounterparts) are scanned for the git and URL marketplaces declared inextraKnownMarketplaces, reported asAgentPluginMarketplace. Plugins stored next to the marketplace and local marketplaces are not reported. The plugin or marketplace name, theref, thepathand the npmregistryare available inDependency.Metadataunder theplugin,marketplace,ref,pathandregistrykeys. - Azure Pipelines: templates referenced from steps, jobs, stages,
extendsandvariables, deployment job lifecycle hooks,git,github,githubenterpriseandbitbucketrepository resources, andnpmandNuGetpackage resources are reported. The name of a package resource is reported as written (<repository>/<package>on GitHub Packages), and itspackagealias,connectionandtypeare available inDependency.Metadata. A jobcontainerorservicesentry that names a container resource is not reported again. As the resource may be declared in another file, for example the pipeline that includes a template, a jobcontainerorservicesvalue is only reported when it is certainly an image: it has a tag, a digest or a/. Template expressions such as${{ parameters.image }}are not reported. Pipeline resources (resources.pipelines) are not reported. - Helm charts:
Chart.yamlandChart.lockare scanned, andrequirements.yamlandrequirements.lockfor apiVersion v1 charts. The dependency name is the chartname. Therepositoryandaliasvalues are available inDependency.Metadataunder therepositoryandaliaskeys. The entries of the lock files are not updatable, as they record a digest of the dependencies. - Git submodules: the name is the
urlas written in.gitmodules, and the version is the commit recorded in the git index. When the index cannot be read, for example in a source archive or a clone made with--no-checkout, the submodule is reported with anullversion. The submodulename,pathandbranchare available inDependency.Metadata, as well as theurl, which is resolved againstremote.origin.urlwhen it is relative (../other.git), and isnullwhen a relative url cannot be resolved. - npm:
package.jsondependency sections,packageManager, npmoverrides(including nested overrides and their.key), Yarnresolutions, and pnpmpnpm.overridesare scanned. In overrides and resolutions, the package is the last one of the key, without its version selector:"**/a/@scope/b@^1.0.0": "2.0.0"is reported as@scope/bversion2.0.0. Protocol and path specifiers (workspace:,file:,link:, git URLs,github:, tarball URLs,owner/reposhorthands, ...) and override references ($name) are reported with a version location that is not updatable. An alias ("name": "npm:package@1.0.0") is reported as the aliased package, with the alias name inDependency.Metadataunder thealiaskey. ThepackageManageris reported under the name it is written with, and its version is not updatable when a Corepack hash (+sha512...) follows it, as the hash would no longer match.package-lock.jsonandnpm-shrinkwrap.json(lockfile versions 2 and 3) are scanned for the packages installed innode_modules, with versions that are not updatable as their integrity hashes would no longer match; the root project, workspace folders and links are not reported. Lockfile version 1 (npm 6 and earlier),pnpm-lock.yaml,yarn.lockandbun.lockare not scanned. - Python:
*requirements*.txtand*constraints*.txtfiles, their pip-tools.incounterparts (such asrequirements.in), and.txtand.infiles in arequirementsdirectory are scanned. Only exact pins (==) are reported; arbitrary equality (===) is not. - RubyGems:
Gemfile,*.gemspec, andGemfile.lockfiles are scanned.gemandadd_*dependencycalls that start a statement are reported, so a call in a comment, a string, a heredoc or an=beginblock is not. Names and requirements can be written as"...",'...'or%q<...>literals, optionally followed by.freeze, and requirements can be passed as an array. A gem with several requirements is reported with the requirements joined by,and a version location that is not updatable, as is a requirement that contains an escape sequence or an interpolation. Top-level Bundler lockfile specs are reported, and their versions are not updatable. A gem with native extensions has one lockfile entry per platform, such asnokogiri (1.15.0-x86_64-linux): each entry is reported with the version without the platform, and the platform inDependency.Metadataunder theplatformkey. - Cargo:
Cargo.tomldependency sections ([dependencies],[dev-dependencies],[build-dependencies],[workspace.dependencies], and the platform-specific[target.'cfg(...)'.dependencies]and[target.<triple>.dependencies]forms) andCargo.lockpackage entries are scanned. A crate declared over several entries, such as a[dependencies.serde]table or dotted keys (serde.versionandserde.features), is reported once. Registry version requirements are updatable; git, path, workspace, and other non-version specifications are reported as non-updatable. A renamed dependency (json = { package = "serde_json" }) is reported as the crate of itspackage, which is its name location, with the key inDependency.Metadataunder thealiaskey. A key is only updatable when it appears once, and a key or a value only when its text in the file is exactly its value (no escape sequences).Cargo.lockversions are not updatable as their checksums would no longer match, and packages without asource, which are the crates of the workspace and path dependencies, are not reported. - Go modules:
go.modrequire,replace,goandtoolchaindirectives andgo.sumchecksum entries are scanned. Areplacedirective is reported as its replacement module, with the replaced module path inDependency.Metadataunder thereplaceskey; a replacement with a local directory (=> ../fork) is not reported. Thegoandtoolchaindirectives are reported as thegoandtoolchainmodules, which is how the go command names them (go get go@1.23.0 toolchain@go1.23.0), with versions such as1.23.0andgo1.23.0.exclude,retract,toolandgodebugdirectives, and quoted module paths, are not reported.go.modversions are updatable;go.sumversions are not, as their hashes would no longer match. - Python projects:
pyproject.toml,poetry.lock,uv.lock,Pipfile, andPipfile.lockare scanned. Exact versions from the dependency sections ([project]dependencies,[project.optional-dependencies],[dependency-groups],[build-system]requires,[tool.uv]dev-dependencies/constraint-dependencies/override-dependencies, Poetry dependency tables, and Pipfile[packages]/[dev-packages]) and lockfile entries are reported. Requirement strings follow PEP 508 (name[extras] == version ; marker), and Poetry and Pipfile entries can be a string or an inline table with aversionkey. Only exact pins (==, or a bare version in Poetry and Pipfile) are reported; ranges and arbitrary equality (===) are not. Poetry'spythonentry is not a package and is ignored, and so are the local projects of the lockfiles (uv.lockeditable,virtual,pathordirectorysources, andpoetry.lockdirectoryorfilesources). A name or version is not updatable when its TOML string contains an escape sequence or spans several lines. Lockfile versions are not updatable. - Composer:
composer.jsonrequireandrequire-deventries, pluscomposer.lockpackage entries, are scanned. Platform requirements, such asphporext-json, are ignored. - Java: Maven
pom.xmlfiles are read as XML: the dependencies, plugins, plugin dependencies, extensions and parent are reported, including independencyManagement,pluginManagementand profiles, but not the coordinates in a pluginconfigurationor inexclusions. A dependency without a version, such as one managed by an imported BOM, is reported with anullversion. A version that is exactly a reference to a property defined once in thepropertiesof the same pom and not redefined by a profile, such as${junit.version}, is reported with the value of the property, the property name inDependency.Metadataunder thepropertykey, and the location of the property definition. That location is not updatable when the pom references the property more than once, as updating it would change the other references too; references from other poms, such as child modules, are not counted. Other property references are reported as written, with a location that is not updatable. - Gradle:
build.gradleandbuild.gradle.ktsdependencies written as"group:artifact:version"strings or with named arguments (group: 'g', name: 'a', version: '1.0'in Groovy,group = "g", name = "a", version = "1.0"in Kotlin), and plugins declared with a version (id 'x' version '1.0',id("x") version "1.0",kotlin("jvm") version "1.0") are reported. Comments are ignored, and a version that is not a string literal, such as"$version", is not reported. Version catalogs (libs.versions.toml) report[libraries]and[plugins], with a literal version, aversion.refor aversion = { ref = "..." }. A catalog version shared by several libraries or plugins, or written with escape sequences, is not updatable. Gradle plugins are reported asJavaPackagedependencies named after their plugin marker artifact,<id>:<id>.gradle.plugin, which is the artifact Gradle resolves a plugin from; the plugin id is available inDependency.Metadataunder thepluginIdkey. - Swift:
Package.swift,Package@swift-X.Y.swiftandPackage.resolvedfiles are scanned. In a manifest, the version of a dependency is its whole requirement as written, such asfrom: "1.0.0", so that the kind of requirement can change on update. A name or a requirement that is not made of string literals only, such as an interpolated or concatenated URL orfrom: version, is reported as written with a location that is not updatable. Escape sequences in string literals are resolved, and a string that contains one is not updatable. - Renovate:
.json5files are read as JSON with comments and trailing commas. JSON5-only syntax, such as unquoted property names or single-quoted strings, is not supported.
Usage
Scan a Directory
using Meziantou.Framework.DependencyScanning;
// Scan with default options
var dependencies = await DependencyScanner.ScanDirectoryAsync(
"C:\\MyProject",
options: null,
cancellationToken);
foreach (var dependency in dependencies)
{
Console.WriteLine($"{dependency.Type}: {dependency.Name}@{dependency.Version}");
}
Scan with Custom Options
var options = new ScannerOptions
{
// Number of parallel scanning tasks (default: 16)
DegreeOfParallelism = 8,
// Recurse into subdirectories (default: true)
RecurseSubdirectories = true,
// Filter files to scan (FileSystemEntry is in System.IO.Enumeration)
ShouldScanFilePredicate = (ref FileSystemEntry entry) => !entry.FileName.StartsWith('.'),
// Filter directories to recurse into
ShouldRecursePredicate = (ref FileSystemEntry entry) => entry.FileName is not ("node_modules" or "bin"),
// Called when a file cannot be read or parsed. The file is skipped and the scan continues.
OnFileScanFailed = (filePath, exception) => Console.Error.WriteLine($"Cannot scan {filePath}: {exception.Message}"),
};
var dependencies = await DependencyScanner.ScanDirectoryAsync(
@"C:\MyProject",
options,
cancellationToken);
Symbolic links and other reparse points to directories are never followed, so a link cannot make the scan loop or leave the root directory. A symbolic link to a file is scanned only when its target is inside the root directory, because updating its dependencies would write to the target.
Error Handling
A file that cannot be scanned does not stop the scan: when a file cannot be opened or read, or a scanner throws while scanning it (for instance because the file is malformed), the remaining scanners are skipped for that file and the scan continues with the next file. The dependencies reported from the file before the failure are kept. Set ScannerOptions.OnFileScanFailed to be notified with the path of the file and the exception. The callback can be called concurrently, and an exception it throws stops the scan and is reported by the scan method, so it can be used to fail fast:
var options = new ScannerOptions
{
OnFileScanFailed = (filePath, exception) => throw new InvalidOperationException($"Cannot scan '{filePath}'", exception),
};
ScanFileAsync, ScanFilesAsync and ScanDirectoryAsync behave the same way: ScanFileAsync returns the dependencies found before the failure. The in-memory ScanFileAsync overloads, which take no options, skip the failure silently. Cancellation is not a failure: the scan methods throw an OperationCanceledException when their cancellation token is canceled. An exception thrown by the onDependencyFound callback also stops the scan.
Custom File System
ScannerOptions.FileSystem reads and writes files through an IFileSystem. With a custom file system, ScanDirectoryAsync lists the files with IFileSystem.GetFiles(path, "*", searchOption), which should throw a DirectoryNotFoundException when the directory does not exist. ShouldScanFilePredicate and ShouldRecursePredicate take a FileSystemEntry, which only an enumeration of the physical disk can provide, so they are not supported with a custom file system: the scan throws a NotSupportedException. Filter the files in GetFiles instead.
OpenRead can return a stream that cannot seek; it is read into memory. OpenReadWrite must return a seekable stream that supports SetLength, as an update rewrites the file in place.
Filter by Dependency Type
var options = new ScannerOptions
{
// Only scan for specific dependency types
IncludedDependencyTypes = [DependencyType.NuGet, DependencyType.Npm],
};
// Or exclude specific types
var options2 = new ScannerOptions
{
ExcludedDependencyTypes = [DependencyType.DockerImage],
};
Stream Dependencies as They're Found
Files are scanned in parallel, so the callback can be called concurrently from several threads and must be thread-safe. Use a concurrent collection, a lock, or DegreeOfParallelism = 1.
var dependencies = new ConcurrentQueue<Dependency>();
await DependencyScanner.ScanDirectoryAsync(
"C:\\MyProject",
options: null,
onDependencyFound: dependency =>
{
// Called concurrently
dependencies.Enqueue(dependency);
},
cancellationToken);
Scan Individual Files
// Scan a single file
var dependencies = await DependencyScanner.ScanFileAsync(
rootDirectory: "C:\\MyProject",
filePath: "C:\\MyProject\\package.json",
options: null,
cancellationToken);
// Scan multiple specific files
var filePaths = new[]
{
"C:\\MyProject\\package.json",
"C:\\MyProject\\MyProject.csproj"
};
var dependencies = await DependencyScanner.ScanFilesAsync(
rootDirectory: "C:\\MyProject",
filePaths,
options: null,
cancellationToken);
The root directory and the file paths can be relative or contain . and .. segments: they are made absolute before the scanners that depend on the location of the file, such as the GitHub Actions scanner, check them.
Dependencies scanned from in-memory content (ScanFileAsync with a byte[]) have locations that are not updatable, as there is no file to write.
Update Dependency Versions
var dependencies = await DependencyScanner.ScanDirectoryAsync(
"C:\\MyProject",
options: null,
cancellationToken);
// Update all NuGet packages to version 2.0.0
foreach (var dependency in dependencies.Where(d => d.Type == DependencyType.NuGet))
{
if (dependency.VersionLocation?.IsUpdatable == true)
{
await dependency.UpdateVersionAsync("2.0.0", cancellationToken);
}
}
An update never writes blindly: it first checks that the file still holds the expected value at the location, and throws a DependencyScannerException otherwise, for instance when the file was modified since the scan. The expected value is the one passed to Location.UpdateAsync(oldValue, newValue), or, when none is given, the value the location holds: the value found by the scan, then the value written by the last update. So a dependency can be updated several times, and its name and its version can be updated in any order, even when they share a line such as FROM node:18. Dependency.Name and Dependency.Version keep the values found by the scan.
Custom Regex Scanner
For custom file formats, you can use the RegexScanner:
var options = new ScannerOptions
{
Scanners =
[
new RegexScanner
{
FilePatterns = [Glob.Parse("**/*.custom", GlobDialect.Standard, GlobOptions.IgnoreCase)],
DependencyType = DependencyType.DockerImage,
Regex = new Regex(@"image:\s*(?<name>[a-z/]+)(:(?<version>[0-9.]+))?", RegexOptions.ExplicitCapture, TimeSpan.FromSeconds(10))
}
]
};
var dependencies = await DependencyScanner.ScanDirectoryAsync(
"C:\\MyProject",
options,
cancellationToken);
FilePatterns are matched against the path of the file relative to the scanned root directory, for instance build/*.yml or **/*.custom.
The regex pattern must include named groups:
name- The dependency name (required)version- The dependency version (optional)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- Meziantou.Framework.Globbing (>= 5.0.5)
- Meziantou.Framework.Language.Json (>= 4.0.5)
- Meziantou.Framework.Language.Toml (>= 2.0.2)
- Meziantou.Framework.Language.Xml (>= 4.0.3)
- Meziantou.Framework.Yaml (>= 2.0.6)
-
net11.0
- Meziantou.Framework.Globbing (>= 5.0.5)
- Meziantou.Framework.Language.Json (>= 4.0.5)
- Meziantou.Framework.Language.Toml (>= 2.0.2)
- Meziantou.Framework.Language.Xml (>= 4.0.3)
- Meziantou.Framework.Yaml (>= 2.0.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.20 | 53 | 10/11/2026 |
| 3.0.19 | 149 | 9/30/2026 |
| 3.0.18 | 96 | 9/27/2026 |
| 3.0.17 | 157 | 9/20/2026 |
| 3.0.16 | 133 | 9/15/2026 |
| 3.0.15 | 103 | 9/13/2026 |
| 3.0.14 | 122 | 9/11/2026 |
| 3.0.13 | 143 | 9/8/2026 |
| 3.0.12 | 124 | 9/6/2026 |
| 3.0.11 | 170 | 9/3/2026 |
| 3.0.10 | 104 | 9/3/2026 |
| 3.0.9 | 191 | 8/30/2026 |
| 3.0.8 | 116 | 8/29/2026 |
| 3.0.7 | 156 | 8/23/2026 |
| 3.0.6 | 215 | 8/16/2026 |
| 3.0.5 | 261 | 8/9/2026 |
| 3.0.4 | 229 | 7/17/2026 |
| 3.0.3 | 126 | 7/12/2026 |
| 3.0.2 | 201 | 7/7/2026 |
| 3.0.0 | 156 | 7/5/2026 |