🇧🇷 Prefere ler em português? Leia aqui.
Over the last three days of my 30 Days of .NET, we saw what happens when we run dotnet run, explored the structure of a .NET project, and learned how to use the CLI without relying on Visual Studio.
But there's something that shows up in pretty much every real-world project and that we tend to handle on autopilot: NuGet packages.
When we need some functionality, we look for a library that solves our problem, install the package, and keep developing. I've done this countless times myself without thinking too much about what was happening behind that installation, especially when the package is well-known and used by thousands of other developers. By the way, be careful with that.
The thing is, installing a package doesn't necessarily mean adding just one library to our project. Sometimes we're bringing in several other dependencies along with it, and we don't always know what they are or where they came from.
So today, we're going to take a closer look at how NuGet works, where these dependencies come from, and how we can check whether the packages we're using are up to date and secure.
What is NuGet?
NuGet is the package manager for the .NET ecosystem. It allows developers to distribute, share, and reuse libraries across different projects, making dependency management easier. If you've worked with JavaScript or Python, you can think of it as the equivalent of npm or pip. These packages can be made available through different sources, with NuGet.org being the most well-known public repository.
If we open the Dapper page on NuGet.org, for example, we can find information about the package, its available versions, the frameworks it's compatible with, its dependencies, and links to its repository and documentation. It's worth taking a look at this information before installing a library, especially to check whether it meets our project's needs and to better understand what we're adding.
On Day 02, we saw that we can add Dapper to our project using the terminal. After installation, the package appears in our .csproj through a PackageReference, which tells .NET which dependency we're using and which version was declared for the project.
So far, it seems pretty simple, but there's an important part of this process that isn't always visible when we only look at the .csproj.
Dependencies That Bring Other Dependencies
When we install a NuGet package, we might be adding much more than the library we chose. A package can also depend on other packages to work properly, and those packages can have their own dependencies, and so on.
This is where we need to understand the difference between direct and transitive dependencies.
A direct dependency is one that we explicitly add to our project, like we did with Dapper. A transitive dependency, on the other hand, is one that becomes part of the project because another package needs it to work.
We can view these dependencies using the command:
dotnet package list --include-transitive
The result shows the top-level packages, which are the direct dependencies, and the transitive packages used by the project. Depending on the libraries installed, this list can be huge.
And why does this matter so much?
Because when we add a dependency to our project, we're also trusting, to some extent, the packages it depends on. This doesn't mean transitive dependencies are bad. After all, they're a normal part of how package managers work, but we need to understand that they're also part of our application.
An example of how this can become a problem happened in March 2026, involving Axios, a package widely used to make HTTP requests in JavaScript applications.
In this case, an attacker managed to publish compromised versions of Axios on npm. These versions included a new dependency called plain-crypto-js, which contained malicious code. So anyone installing one of the affected versions could end up bringing in and executing that code without ever choosing to install the malicious package directly.
The incident was documented by the Google Threat Intelligence Group and is an example of a software supply chain attack.
Although it happened in the npm ecosystem, this example helps us understand a risk that also exists in NuGet and other package managers.
It's not enough to know just the library we're installing. We also need to consider the dependencies that come along with it.
dotnet restore — Where Do Packages Come From?
Now that we know a package can depend on several others, another question comes up: how does .NET find all these dependencies and get our project running? We can use the dotnet restore command.
dotnet restore
When we run this command, .NET analyzes the dependencies declared in the project, resolves which versions need to be used, and checks the configured package sources to find what's needed.
NuGet also uses a global packages cache, which is usually stored in the .nuget/packages folder inside the user's directory. This means that if a package version has already been downloaded, it can be reused by other projects without needing to download it again.
That's why we don't need to commit all the libraries used by our application to Git. When someone else clones our repository, .NET can read the declared dependencies and restore the required packages, as long as it has access to the sources where they're available.
How Can We Tell if Our Dependencies Are Up to Date and Secure?
Installing a library and never looking at it again might seem harmless, especially when everything keeps working, but the packages we use also receive updates, bug fixes, and security vulnerability fixes.
Using the .NET CLI, we can identify packages that need these updates without having to visit each package's NuGet page. The dotnet package list --outdated command shows which packages have newer versions available:
This command compares the packages used by the project with the versions available from the configured sources and shows which ones can be updated.
But there's an important difference between a package being outdated and a package being vulnerable.
Not every older version has a security issue, and not every update needs to be applied immediately, since some updates can introduce changes that are incompatible with our code.
dotnet package list --vulnerable --include-transitive
The command above checks vulnerability information available from audit sources and shows the affected packages, allowing us to identify issues that need attention.
And this is where it's worth remembering the Axios case we saw earlier. These checks are important, but they don't mean we're completely protected against every malicious package. They depend on vulnerabilities that have already been identified and recorded, so a recently compromised package might not show up in any of these checks yet.
That's why keeping our dependencies secure doesn't simply mean updating everything to the latest version or running a command every once in a while.
We need to evaluate which libraries we're using, keep track of important updates, and understand the impact these dependencies can have on our application.
Wrapping Up
In the rush of development, it's very easy to install a package to solve a problem and never think about it again. If the library works, the project builds, and the feature is done, we usually move on to the next task without paying much attention to what was added.
But today we saw that there's quite a lot happening behind a simple installation. NuGet needs to resolve dependencies, find the required packages, restore their files, and make sure everything is available for the build process. And as Uncle Ben would say, "With great power comes great responsibility." That convenience of having ready-made code comes with the responsibility of keeping track of the libraries we use.
We don't need to be suspicious of every package we install, but understanding where our dependencies come from and how we can check their status is already an important step toward keeping our projects safer and easier to maintain.
Oh, and it's worth checking out the NuGet statistics page!
See you tomorrow!






Top comments (0)