My first steps into .NET Core & NuGet
My last Terninger post ended with my plan to migrate it to .NET Standard and NuGet. And I’ve always known this would be a reasonably big move.
.NET Core 2.1 LTS landed a while back, and unlike 1.0, it has a much wider feature set; considerably fewer missing APIs when compared to the desktop frameworks. I’ve started a new work project targeting .NET Core, so getting a bit of experience in there.
I’m planning to migrate Make Me a Password to ASP.NET Core, so I can host it on a non-Windows box. Make Me a Password is my consumer of Terninger, but it also consumes the Readable Passphrase Generator. So, to kill a few birds with one stone, I decided to port the Readable Passphrase Generator to .NET Standard and .NET Core.
Readable Passphrase Generator has two main artefacts:
- The KeePass Plugin - its original use case (complete with a strong name).
- And a Console App, to allow usage outside of KeePass.
Those both need to stay. But in addition, I’d like to produce:
- .NET Standard NuGet packages for the generator; that is, the C# API.
- A .NET Core console app, to prove it works (and perhaps widen its reach further).
On top of that, whatever I end up with needs to be easy for me to maintain. I don’t make many changes (other than adding new words to the dictionary from time to time), so building and releasing needs to be dead simple.
So here’s the account of how I got there (fortunately, its just an account, not a saga).
First up, I needed to decide what .NET Standard to target.
.NET Standard is an abstract API which library developers can target and different .NET frameworks can implement. Target lower versions of .NET Standard and your library is available to more frameworks, but has less APIs available to it.
Readable Passphrase Generator originally targeted .NET 3.5 (wow, that’s old now!), and its API usage is pretty minimal. Only recently I upgraded it to target .NET 4.0 (and that was only because I couldn’t get 3.5 to work on a new laptop)!
So I decided to target .NET Standard 1.3, which supports .NET 4.6. It covers off everything I need.
Next up, I tried using the new DLLs with the KeePass plugin. And it didn’t work. So I need to build two copies, one for .NET Standard 1.3, and one for .NET 4.0.
So, do I multi-target or have multiple project files, one for each platform?
Multiple projects used to be the way to go, but the .NET Core new project format allows you to target multiple frameworks in a single project via the
Didn’t take me long to realise more projects means more maintenance, so I went with
Lets have a look at the project file (complete file is here).
It is light weight compared to the old project file, most of the basic properties are there to support the NuGet package - just the
TargetFrameworks tag is enough to make it build.
You can specify multiple frameworks, and I’ve got the two I’m supporting.
OK, that tiny project file is a bit of a lie.
Multi-targeting gets interesting because each framework needs special attention.
Condition attribute becomes your new best friend.
The .NET Standard side of things includes some NuGet packages:
And .NET 4.0 includes the strong name signing stuff.
One of my biggest take aways targeting multiple frameworks is you end up spending more time directly editing the project file. Rather than managing merge conflicts, you’re changing framework conditionals. All in all, that’s a pretty good trade off!
At this point I was down to fixing a bunch of compiler errors. Most were pretty easy. Solutions involved either a) working out an alternate way of doing the same thing compatible in both frameworks, or b) conditional compiler directives based on frameworks.
- There’s no more
AssemblyInfo.cs, the new project file takes care of that. Delete that file.
Serialisableattribute .NET Standard - conditionals to sort that out.
ApplicationExceptionis no more. I just changed to throwing
And one other little change: using
RandomNumberGenerator.Create() instead of
To allow for a slightly more cross platform random number generator.
The more difficult problem was reflection. Readable Passphrase Generator uses reflection in two places 1) to load the dictionary file, and 2) to load and save phrase definitions.
Problem 1: I use the entry point assembly to find the directory containing the XML dictionary file.
But there’s no
Assembly.GetEntryAssembly() in .NET Standard 1.3.
It only appears in 1.5.
After a bunch of research, I couldn’t find a solution to this one.
So I changed the behaviour slightly: I used
Not as reliable as
GetEntryAssembly but good enough for my purposes.
(And, once I published NuGets, I ended up with a different solution to the default dictionary - read on!).
Problem 2: Loading and saving phrase definitions involves reflecting over some attributes.
But .NET Standard uses
GetTypeInfo() (plus some extension packages) instead of
However, .NET 4.0 doesn’t know about
So more compiler conditionals, and a wrapper extension method.
That’s about it, simple project yields reasonably easy problems to fix. And now it compiles!
Now I needed to fix some simple build scripts (cmd files). I’m not interested in cross platform builds, so its just some tweaks to the cmd files.
I get to use the dotnet command for this (rather than the old
msbuild; although I believe
dotnet is wrapping
- dotnet clean: clean the project / solution.
- dotnet build: build the project / solution.
- dotnet publish: build the project / solution ready for distribution.
- dotnet pack: build nuget package for project.
Script changes for building the console app involved creating new zip files for
.NET Standard err… .NET Core.
This was when I realised the framework for entry points (eg: console apps) can’t be .NET Standard.
.NET Standard is for library or package developers, .NET Core is for actually running applications.
The KeePass Plugin build can use
dotnet build to get .NET 4.0 binaries, but you then need to copy everything required to a separate folder to build the PLGX file.
Including the old project file, because KeePass uses it to build the PLGX (particularly to work out references).
Finally, I get to testing! Slightly more complicated than before:
- KeePass plugin on Windows
- KeePass plugin Linux (OK, I’ll admit to not testing this one personally)
- Console app on .NET 4.0 Windows
- Console app on Mono Linux
- Console app on .NET Core Windows
- Console app on .NET Core Linux
The last step was to get the project out onto NuGet.
And again, the big picture decision I needed to make was either one package for everything, or one package per project?
The guidelines on NuGet’s doco pushed me to the direction of one per project. Which, at this point, meant 2 packages:
Wordsproject (which contains interfaces, abstract classes and data entities; it’s only useful on its own if you want to build your own dictionary loader).
Coreproject (which has all the useful stuff).
The dictionary had been distributed as a separate (gzipped) xml file. But that would mean you’d acquire code from NuGet, then the dictionary (which is really quite essential) would need to come from somewhere else. Which sounds like a silly idea.
So I created a new project & package for the
This contains the gzipped dictionary as an embedded resource, and a simple method to load it.
Next problem: you need to acquire 2 packages to use the passphrase generator:
That strikes me as extra work which 99% of people wouldn’t care for.
So I created yet another new project & package
This one is a (highly simplified) meta package, like Microsoft.AspNet.App.
Effectively, its a project with a static
Generator.Create() method, and dependencies on
So now, 99% of consumers can just use ReadablePassphrase, and anyone who wants to do something unusual (like use a different dictionary, or use different code to load the dictionary) can reference individual packages.
In retrospect, I probably should have just bundled everything into one package, but at least I learned something!
As I created each NuGet package, I realised I couldn’t refer to each project any longer.
ProjectReference elements in the project file don’t work; after all, each of my projects are in NuGets now, and for
dotnet pack to figure out the dependencies, it needs to work in terms of packages instead.
ProjectReference elements get replaced with
And I add a
NuGet.config file to my top level solution folder, to tell NuGet to find my local (as yet, unpublished) packages on my computer:
<?xml version="1.0" encoding="utf-8"?>
So as each package is created in my
releases folder, consumers of it find the package on my computer.
Finally, the ultimate test: consume the generator from Make Me A Password.
This was just like any other NuGet package: I deleted the old DLLs from the lib folder, and included ReadablePassphrase via NuGet. And, for sake of setting a good example, I followed my own documentation for using the generator.
And it worked!
Except, Make Me a Password lets you download the dictionary. Which works fine when the dictionary is a stand alone file, but less so when its wrapped up as an embedded resource.
I’ve subsequently added a method to the
DefaultDictionary package which provides the
Stream of the dictionary.
But it came too late for the NuGet.
dotnet pack can create package versions with debug symbols with the
However, this doesn’t include the PDB files in the main package, but creates an additional symbol package which includes the DLLs and PDBs.
I haven’t worked out how I should publish the symbol packages just yet. And, I’d prefer to simply include PDBs with the main package. I’ll sort that out next time I deploy.
A recent change in my deployments was to provide signatures and hashes for all binaries (PLGX, and console apps).
These changes took me from 2 binaries to sign to 11! That was 1 PLGX and 1 ZIP, and is now 1 PLGX, 2 ZIPs (Console for .NET Desktop and .NET Core) and 8 NUPKG files (4 normal, 4 debug symbol)! I’ll need to work out an automated way of producing them, because it’s painful to do it manually.
NuGet has recently added support for package signing, however, it uses X.509 code signing certificates. And those aren’t free - the cheapest I could find is around AU$50. So, until I get a few donations, I won’t be code signing yet.
Well, it wasn’t exactly trivial, but certainly wasn’t too hard either. I’ve created my first .NET Standard library, first .NET Core project, and first NuGet packages. And, hopefully, made it just a little bit easier for people to consume the Readable Passphrase Generator.
Overall, the differences between .NET Desktop, .NET Core and .NET Standard are pretty small. Reflection is the main outstanding still, but no where as bad as the .NET Core 1.0 days, and some helper methods and compiler conditionals work around the problems well enough.
I suspect Terninger will be easier, because it isn’t going to be multi-targeting frameworks (yet). I’ll see how I go!