NuGet Pack Your Problems
A very basic concept of software programming is “do not repeat yourself (DRY)”, it states we should avoid logic duplication as possible. Duplicated code leads to maintenance concerns basically because it adds many places to fail.
The term has been originally defined in The Pragmatic Programmer book:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
In simple terms, code should have only one place to be maintained and this applies very well 99% of the time.
What about the remaining 1%?
One of the disadvantages of code centralization is that, depending on the number of consumers, you also have one single place to screw up with all of those consumers' logic. Don’t get me wrong, remember I’m scoping this to only 1% of the time. It is always important to understand the balance of what we are doing. There are cases where you need to favor the decoupling of components even if that means code duplication.
Now that we understand the DRY concept we can clearly say that this is the objective of this article. Centralize shared code through NuGet packages.
So, What’s NuGet?
From its official definition at https://www.nuget.org/:
NuGet is the package manager for .NET. The NuGet client tools provide the ability to produce and consume packages. The NuGet Gallery is the central package repository used by all package authors and consumers.
3 main concepts come out of this definition:
- Package
- Producers & Consumers
- Repository
Producers & consumers are very self-explanatory so let’s get to What’s a NuGet Package?
Put simply, a NuGet package is a single ZIP file with the
.nupkg
extension that contains compiled code (DLLs), other files related to that code, and a descriptive manifest that includes information like the package's version number.
Reference: https://docs.microsoft.com/en-us/nuget/what-is-nuget
Short version, the package contains everything our code is about.
Finally, the Repository is the place where all packages are stored and can be published and consumed.
The overall view of this can be understood with the following diagram, please consider that Publisher has been changed to Creator and Repository to Host.
Reference: https://docs.microsoft.com/en-us/nuget/what-is-nuget
NuGet has been out there for quite a long time however even when it is an incredible way to get and share libraries all around the world, I’ve noticed that most of the consumers are just that consumers. So, in order to take advantage of all the benefits of NuGet packaging we have to become something else, that is, we have to become Publishers.
How to become a publisher?
In the previous diagram, we see the nuget.org URL which can be a little bit intimidating from the first sight but, believe me, uploading a package to nuget.org is a lot easier and effortless as you can imagine. NuGet.org is the main global repository with 69.4 billion downloads so in fact, it is widely used.
Let's say you don’t want to become a globally known publisher because you know, it sounds like a lot of work, or probably you cannot expose your packages to the world due to some company restrictions. Even in these scenarios you can still be an incredible publisher for your current company scope and take advantage of all the NuGet packages features.
In this case, we are trying to set up a Private NuGet Repository, there are several ways to do it but probably the simplest one is the Local Feed which is a simple hierarchical folder and is also the main scenario for this article. Since I love simplicity, it makes sense to me.
Versioning
Versioning is a fundamental factor of packaging, it provides control over what is being published and also isolates code changes so they do not conflict. That being said, we should understand what Semantic Versioning is about. It can be understood as the main standard for code change control.
Version Sample → 3.2.1 → MAJOR.MINOR.PATCH
MAJOR version when you make incompatible API changes
MINOR version when you add functionality in a backward-compatible manner
PATCH version when you make backward-compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
Reference: https://semver.org
Everything is about what is being changed or added and every portion of the version is incremented depending on that change.
I have to emphasize the importance of this concept because once a component version has been published and it has been downloaded in the consumer's projects is practically impossible to rollback; the only way to fix a mistake is to publish a new version and let the consumers update when they consider it appropriate.
This is because NuGet consumers actually get a copy of the artifacts of your package not only in their projects but also in NuGet local cache. This feature skips package download if this one has been downloaded already.
NuGet local cache: %USERPROFILE%\.nuget\packages
Recommendation: make sure every component is very well tested before publishing it.
Projects Structure
There are different ways to organize projects; it could be by layers, tiers, types, and whatever that works for you. In my case, I like to follow logical layers. Again, it totally depends on you. Each one of these folders can be an independent project or be decomposed differently. With this last, you can get more granular control over them and all of that’s Ok if it works for you.
Each project is a DotNet Standard library which means they are in fact intended to work on multiple projects.
Package Generation
There are a couple of ways to generate NuGet packages. The first one is trough Visual Studio project, just go to:
Project properties → Package → Generate NuGet package on build
I don’t like this way that much, this is because generating the package adds time to my project compilation and that’s something I don't like to do in every single build. Instead, I prefer the explicit .NET Core CLI command:
dotnet pack [project path]
Reference: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-pack
This is some sort of syntactical sugar for the original NuGet pack command however it also builds, restores dependencies, and generates the packages all at once.
NuGet Pack: https://docs.microsoft.com/en-us/nuget/reference/cli-reference/cli-ref-pack
Once this is done, we should see a message like this:
Successfully created package
‘[ProjectPath]\bin\Debug\[ProjectName].[Version].nupkg’
Setting Up the Folder Repository
We would like to use a shared folder so everyone in the company’s network can access it. Getting that folder configured is not being covered in this article, basically, because depending on your company policies it could be possible that you have to request this to the infrastructure department instead of doing it yourself. The basic requirement is that it must be accessible from every developer machine and probably from some service account if you have a Jenkins deployment server or any other similar.
nuget init [LocalPath]\NuGetPackages \\[RemotePath]\NuGetPackages
Reference: https://docs.microsoft.com/en-us/nuget/reference/cli-reference/cli-ref-init
You should end up with a Project-Version folder structure in your network folder.
Build, Pack and Publish
At this point pretty much everything is done, we only need to build, pack and publish our awesome work so everybody can start using it. As a programmer, I’m always thinking about how to automate stuff and this is not the exception. I’ve created a simple Powershell script that allows us to perform all the required operations we have mentioned at once.
Clear$directory = [System.IO.Path]::GetDirectoryName($myInvocation.MyCommand.Definition)$source = '\\[FolderPath]\NuGetPackages'$localSource = '.\NuGetPackages'$projects = @('[ProjectName].Api@1.1.1', '[ProjectName].Common@1.4.0'## , '[ProjectName].Data@1.0.2'## , '[ProjectName].Service@1.7.0'## , '[ProjectName].Service.MessageBroker@1.0.0')cd $directory$projects | ForEach-Object {Write-Host ******************************************Write-Host Processing Project $_Write-Host ******************************************$parts = $_ -split "@"$project = $parts[0]$version = $parts[1]## dotnet pack $directory\$project\$project.csproj -p:NuspecFile=$directory\$project\$project.nuspecdotnet pack $directory\$project\$project.csproj## Removing packages from local cacheRemove-Item -Recurse -ErrorAction Ignore -Force $env:USERPROFILE\.nuget\packages\$project\$version## Adding packages to remote sourceRemove-Item -Recurse -ErrorAction Ignore -Force $source\$project\$versionnuget add $directory\$project\bin\Debug\$project.$version.nupkg -source $source## Adding packages to local sourceRemove-Item -Recurse -ErrorAction Ignore -Force $localSource\$project\$versionnuget add $directory\$project\bin\Debug\$project.$version.nupkg -source $localSourceWrite-Host DONE}
A few notes about this:
$source. Defines the main target repository which is our network folder.
$localSource. Is a local source control managed repository, we are saving a copy of each produced package so we are protected in case of any failure of the main repository. In my current company, accessing network folders from the VPN is sometimes a bit problematic so this also allows developers to get those packages from a local resource if needed.
$projects. An array of all projects that have to be published, the script iterates this collection and build, pack, and publish them one by one. Just remember to update the project version so it matches the Visual Studio Project otherwise the script is going to fail.
Local Cache Clear
Remove-Item -Recurse -ErrorAction Ignore -Force $env:USERPROFILE\.nuget\packages\$project\$version
This line is important. When creating our packages is very likely to run functional testing in a consumer project which means we have to search and install that package in our consumer project. The consequence of this is NuGet getting a copy of that package also in the local cache; if we do not delete this cached copy we will have a problem trying to refresh/restore that package if any bug or problem is found during the functional test.
In summary, if we find some problems, we fix it in our source project, run the Powershell script again so the package is rebuilt, publish, deleted from the local cache, and finally restored in our consumer project.
Recommendation: Get projects, Powershell scripts, and local source in any source control tool, this way we have versioning protection at all levels of code changes.
Visual Studio Integration
Now that we have created a new NuGet repository, Visual Studio needs to know about it. Just go to:
Toolbar → Tools → Options → Nuget Package Manager → Package Sources
Once this source has been saved we can find their respective items in the Package Manager, just select the respective source.
Jenkins Integration
Jenkins also needs to know about the package source however since this does not have a Visual Studio IDE we need to manually modify the configuration files to include the new source. Go to:
C:\Program Files (x86)\NuGet\Config\Microsoft.VisualStudio.Offline.config
Note: C:\Tools\NuGetPackages is the server local path of our network shared folder. In this case, we are using the same Jenkins server for this NuGet repository.
Conclusions
Package managers are the easiest way to install, upgrade, or remove software dependencies from our projects. Getting advantage of these benefits even in local environments is possible.
Using NuGet.org is always an option. I’ve already mentioned is a lot easier than you might think and I actually use it when no company restrictions are in place. Take a look at it, just make sure everything is well tested before releasing a new version. There is no way to delete a package once it is published and consumers get it.
With NuGet.org there is a List in search results option that reads “Unlisted packages cannot be installed directly and do not show up in search results” this last means you can still install the package but it can be done with the command-line statements. I haven’t tried this option yet but I assume anybody with the package name should be able to install it so it does break the private repository concept that we covered in this scenario.
When I created my first packages I could not imagine how lovely this will be. I can increase or fix the functionality of my shared code effortlessly and also without impacting other company projects, that worths every single piece of effort described in this article.