My Experience Publishing A NuGet Package
Look behind the scenes and see my experiences developing my first NuGet package, ZeroRedact.
ZeroRedact is my first public NuGet package hosted on nuget.org. It's a fast, simple, zero allocation redacting library for .NET, with no extra dependencies. You can read about in the launch post: ZeroRedact: A Simple Redaction NuGet Package.
Short version
The experience was fun, a bit overkill, and a little frustrating. Worth it. Would do again.
Long version
Here are all the bits and pieces I ran into along the way and my experiences with them.
Configs for packages
While the docs state which required properties are needed for a package, I had trouble looking around for advice for what other ones are good to add. In the end I looked at what other popular packages were doing such as Newtonsoft.Json and GuardClauses by Ardalis.
In the end I opted for this (source):
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>Nikouu.ZeroRedact</PackageId>
<Version>2.2.0</Version>
<Title>Nikouu.ZeroRedact</Title>
<Authors>Niko Uusitalo</Authors>
<PackageProjectUrl>https://nikouu.github.io/ZeroRedact</PackageProjectUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Description>A fast, simple, zero allocation redacting library for .NET, with no extra dependencies.</Description>
<Summary>A fast, simple, zero allocation redacting library for .NET, with no extra dependencies.</Summary>
<RepositoryUrl>https://github.com/nikouu/ZeroRedact</RepositoryUrl>
<PackageTags>redact redacting mask masking security</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedAllSources>true</EmbedAllSources>
<TargetFrameworkMonikerAssemblyAttributesPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath>
</PropertyGroup>
Ones of interest are:
PackageReadmeFile
Later on in the csproj file I explicitly include the main GitHub readme into the package and this references that.PackageTags
I noticed some people mistake masking for redacting, or the other way around, so I added both to get the package in front of them.PackageLicenseExpression
andPackageRequireLicenseAcceptance
I want users to understand that this is provided "as is".AllowedOutputExtensionsInPackageBuildOutputFolder
,EmbedUntrackedSources
, andEmbedAllSources
Helped with Source Link. More on this later, but it may not be needed from .NET 8 onwards.TargetFrameworkMonikerAssemblyAttributesPath
used for deterministic builds.IncludeSymbols
andSymbolPackageFormat
Creates the snupkg file for being able to debug through the package.
Uploading packages
I create packages for nuget.org with a GitHub Action. These are triggered manually and do not automatically upload. I don't need a new package with a minor version to be uploaded on change, and I have times where I want to create and inspect packages without uploading as well. Meaning the upload step is manual.
Then it's just the case of uploading both via the "Upload" tab at the top of the NuGet page.
Originally I had pre-release alpha packages so I could understand how the ecosystem worked and not be out in the main package list. I also unlisted a few early packages because I would only realise later I missed something in the code or in the process somewhere. I figured it would happen, but it's my first time and you gotta ship.
Package health
There are three points for NuGet package health from the NuGet Package Explorer both on the web or via download:
- Source Link
- Deterministic builds
- Compiler flags
With Source Link, it was easy enough. Turns out it now comes with .NET 8, though this works only with some source control providers like GitHub or Azure Repos. So to make sure it's fine when I'm locally making packages, I've added in AllowedOutputExtensionsInPackageBuildOutputFolder
, EmbedUntrackedSources
, and EmbedAllSources
.
Deterministic builds were easy by following the instructions from clairernovotny.
I had a slight issue with compiler flags. I believe it was because of the preview version of .NET 9 I had on my machine at the time. I think I updated to the newest RC version and it was fine.
Releasing a big bug
Fixed in 2.1.0, a week after launch, I had a terrible bug. Due to the use of pointers and references, two types of email redaction could leak out and manipulate the user's string to redact. There are now tests testing this and an understanding of why it happened, but I felt pretty bad when I realised and quickly pushed out a new version and checks.
However, these things do happen. Critical bugs end up in software and it was reported as fixed in the 2.1.0 release notes and tests written for it. It was a good lesson for me, and I'm glad I got to experience it with in tiny corner of the community.
Docfx
Link to the ZeroRedact docs site.
I wanted to learn how to do professional-ish docs with this project and landed quickly on Docfx. It's free, comes from the dotnet org, and open source. I'm not a huge fan of the tutorial because it's really simplified and I'd love some really in-depth understanding of what all the little flags and toggles do. Turns out others want this too as I had a few problems setting it up how I'd like it.
After a lot of trial and error, it became easier to run it locally with a script:
if (-not (Get-Command docfx -ErrorAction SilentlyContinue)) {
Write-Host "The 'docfx' tool is not installed. Please run the following command to install it:"
Write-Host "dotnet tool update -g docfx"
exit 1
}
$docsJsonPath = Resolve-Path "..\docfx\docfx.json"
$apiFolderPath = Resolve-Path "..\docfx\api"
# Remove the /api folder and its contents
# Gives a fresh API refresh each run
Remove-Item -Path $apiFolderPath -Recurse -Force
docfx metadata $docsJsonPath
docfx build $docsJsonPath --serve --open-browser
The whole docs workflow centers around the docfx.json
file which tells the build process what to do. In the script above I opt to delete the api folder and rebuild that with the metadata
command in case the code API surface has changed, or the XML docs for the surface have changed.
The docs have three tabs:
- Docs
This is the written explanations of what to do and how to do - API
The actual XML docs from the code above the objects, methods, etc - Demo
A WebAssembly demo website that uses the real ZeroRedact NuGet package running in-browser to test and play with.
On each push, the docs are rebuilt and pushed into GitHub pages with the PublishPages.yml action. This also deploys the WASM demo site.
But in the end, it's great. It does exactly what I want it to do.
Public API checks
I ended up seeing a tweet from @nietras1 (maker of Sep) that pointed me to the blog post Preventing breaking changes in public APIs with PublicApiGenerator by Andrew Lock.
In short, I use the PublicApiGenerator with Verify in my unit test project to help me keep track of any changes to the public API surface in ZeroRedact.
WASM
I think web assembly is the bee's knees. I've used it before in other projects, I've complained about it, and I think it's a perfect excuse to create a fun client-only WASM demo site.
The site is hosted in an iFrame inside the docs site, but it's hosted in the same GitHub pages site on another path:
- Docs site demo URL (pointing to the iFrame)
https://nikouu.github.io/ZeroRedact/demo/ - Actual URL
https://nikouu.github.io/ZeroRedact/demoWasm/
Why host it in an iFrame? Then I get to include it there and then inside the docs. Meaning the user never has to leave the space they're learning about ZeroRedact. Though hosting it in an iFrame comes with UX problems such as the page not being tall enough or differing dark mode colours - but all in all it works.
At the time of writing this post, the site itself is messy code but it works. It was also my first time using Bulma and it was alright. The site uses some reflection to show the user what redaction options are available. It's specifically designed this way so if I add more redaction features, they automatically work in the demo site without me writing more code there.
And since there are a few redactors with lots of redactor options, I added a shuffle button to mix it up so users could get a better feel for ZeroRedact more quickly. The shuffle isn't random, the choices are weighted away from boring redactions such as full redactions or fixed redactions - further helping users get an idea of what ZeroRedact can bring.
Pinning to LTS versions
I wish I could easily find guides and information on handling different .NET version over time. But for now I've decided to keep the current version pinned to the LTS version - as of writing this, .NET 8. Meaning even if new features come in to make ZeroRedact faster, they won't be used until .NET 10 when the package moves to that.
Code coverage
I haven't seen proper code coverage since I had a work licence for Visual Studio Ultimate (now Enterprise) and I felt it was time to check out what's around. I ended up looking at Coverlet as it was already part of my unit testing project and visualising it with ReportGenerator.
Glad I did it too, as this helped me find a few new niche bugs.
It was a tad fiddly to setup, but it works and that's why I have a script.
Scripts
I love little scripts for many reasons and two are highlighted here:
- Automation - a classic
- I'm going to forget how all this works in the next few months. It always happens. The scripts don't forget.
The second point is one I want to highlight. For example, working on Docfx in a flurry until done for two weeks will not make how it works stick in my head when the docs need to change again a year on.
Shields.io badges
I feel like they just make a repo look more legit. Shields.io
Constantly wanting to chip away at it
The todo list is long:
- Auto dependabot updates
- Increasing performance
- Thinking of new redactions
- Improving the docs
- Making the demo site look better
- Reworking the cascading options
And more. But I have to slow down at some point. 2.2.0 is stable, works well, and is the image of what I wanted to have released. I need to stop tinkering for awhile if for nothing else but to work on other projects. I can come back later with a fresh view on what needs to improve to ZeroRedact later.
To Conclude
I think I went overboard with what this small project is, but with everything, it was and still is a fantastic learning experience. I can't wait to see where this ends up over time.
Text in header from https://www.textstudio.com/