NuGet und das .nuspec Universum
Die Paketverwaltung NuGet ist im .Net Ecosystem gut integriert und eröffnet die DLL-Hell auf einem neuen Level! Ich tauche ein in NuGet und das .nuspec Universum!
Da es mir viel Zeit gekostet hat, all die Möglichkeiten herauszufinden, die Nuget bietet und ich keinen Blog gefunden habe, der einen schnellen Überblick bringt, teile ich in diesem Post die Inputs, die mir viel Zeit erspart hätten.
Ich erkläre Möglichkeiten, wie ein NuGet Package definiert und erstellt werden kann. Zu dem zeige ich einige Properties, die es zum Erstellen der NuGet Packages gibt.
Überblick
Interessierte NuGet Newbies können sich die Grundlagen auf der Microsoft Seite über Nuget oder hier im Noser Blog von Erik holen.
Wie kann ich ein Package erstellen?
Ich gehe hier auf drei Tools ein, die das Erstellen der Packages ermöglichen: MSBuild.exe, nuget.exe und dotnet-cli.
Weitere Tools sind auf Microsoft.com beschrieben.
Warum gibt es unterschiedliche Tools?
Um den Unterschied zu verstehen, muss man wissen, dass es neben der Umstellung von .Net-Framework auf .NET (und .NET Core/.NET Standard) auch noch eine Format-Umstellung in der .csproj Datei gibt: SDK-Stil (neu) und der Nicht SDK-Stil (alt). Nate McMaster hat die wichtigsten Unterschiede beschrieben.
Für den alten Nicht SDK-Stil kannst du die MSBuild.exe oder die nuget.exe verwenden, jedoch nicht die dotnet.exe.
Für den neuen SDK-Stil kann man die MSBuild.exe (msbuild -t:pack) oder die dotnet.exe nutzen.
Der SDK-Stil ist dann wichtig, wenn das NuGet Package aus der .csproj Datei erstellt wird. Eine andere Möglichkeit ist eine Nuget Definitionsdatei (.nuspec) zu erstellen und mittels nuget.exe das Package zu generieren. Das kann in manchen Fällen Sinn machen, ABER dann ist eine weitere Datei zu pflegen.
Properties in der csproj-Datei
Common properties
Ein Nuget Package beinhaltet typischerweise folgende übliche Properties.
<PropertyGroup> <PackageId>NoserService.For.All</PackageId> <Authors>daniela.simon@noser.com</Authors> <Company>Noser Engineering AG</Company> <version>1.1.1</version> <Description> This Service will fix all your issues... ;-) </Description> <NoPackageAnalysis>true</NoPackageAnalysis> <IncludeBuildOutput>true</IncludeBuildOutput> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <PackageOutputPath>./bin</PackageOutputPath> </PropertyGroup>
Content files
Zusätzlich zu den eigentlichen Assemblies, kann ein Nuget Package weitere Dateien beinhalten, zum einen gewöhnliche Dateien wie Bilder oder Text-Dateien, zum anderen können das weitere Binaries sein.
Gewöhnliche Dateien
<ItemGroup> <Content Include="..\txt\Ineedthistextfile.txt"> <Pack>true</Pack> <PackagePath>contentFiles/any/net40</PackagePath> <PackageCopyToOutput>true</PackageCopyToOutput> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup>
Weitere relevante binaries
<ItemGroup> <Content Include=".\Bin\Release\net40\IncludesomesmallLib.dll"> <Pack>true</Pack> <PackagePath>lib/net40</PackagePath> <PackageCopyToOutput>true</PackageCopyToOutput> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup>
dotnet tool properties
Seit .Net Core 2.1 können Nuget Packages auch Konsolen-Apps als „dotnet tool“ zur Verfügung stellen. Das hat den Vorteil, dass dotnet tools die Apps per cmd line installieren (Download & Install), aktualisieren und deinstalliert. So können sie automatisiert deployed und gestartet werden, zum Beispiel für Services.
Dafür muss man das NuGet Package mit folgenden Parametern erweitern.
<PropertyGroup> <PackAsTool>true</PackAsTool> <ToolCommandName>NoserService</ToolCommandName> <IsPackable>true</IsPackable> </PropertyGroup>
dotnet tool nutzt den ToolCommandName später auf der Machine, um die App für den Aufruf zu registrieren. Man kann bei der Installation entscheiden, ob die App auf dem ganzen System (–global) erreichbar sein soll, oder nur per Installationspfad (–local).
dotnet.exe
Wenn du die dotnet.exe nicht kennst, dann wird es Zeit und das kannst du hier nachholen.
Mit ihr kannst du nicht nur dein Projekt/deine Solution kompilieren, du kannst auch einen restore (nuget packages holen) oder einen „pack“ machen, um ein Nuget Package explizit zu erstellen. Dieser Befehl bietet weitere Parameter an.
<PackageVersion>$(PackageVersion)</PackageVersion>
$VERSION = "0.0.1" dotnet pack -p:Configuration=Release -p:PackageVersion=$VERSION -p:RepositoryUrl=$CI_PROJECT_URL -p:RepositoryType='git' -p:RepositoryBranch=$CI_COMMIT_BRANCH -p:RepositoryCommit=$CI_COMMIT_SHA
Bei meinem Beispiel verwende ich unter anderem Variablen aus der Gitlab Pipeline.
msbuild.exe
Dasselbe bietet auch msbuild.exe an, jedoch unterscheidet sich die Syntax. Hier braucht die Projekt Datei (.csproj) jedoch keinen Eintrag <PackageVersion>$(PackageVersion)</PackageVersion>, msbuild.exe erstellt diesen mit dem Befehl /p:Version=$Version automatisch.
msbuild /r /p:Configuration=Release .\thisproject.csproj /t:rebuild /t:pack /p:RepositoryUrl=$CI_PROJECT_URL /p:RepositoryType="git" /p:Version=$VERSION /p:RepositoryBranch=$CI_COMMIT_BRANCH /p:RepositoryCommit=$CI_COMMIT_SHA
nuget.exe und nuspec
Um ein Package mittels .nuspec zu erstellen, gibt es zusätzlich nützliche Parameter. Die .nuspec Datei definiert sie so:
<metadata> <projectUrl>https://gitlab.com/HereIsMyProject</projectUrl> <version>1.0.0</version> <repository type="git" url="$url$" branch="$branch$" commit="$commit$" /> <description>Package description could be added here</description> <releaseNotes>Summary of changes made in this release of the package.</releaseNotes> </metadata>
Danach werden sie während des Packing Prozesses gesetzt:
nuget pack Any.NugetPackage.nuspec -version $VERSION -p url=$CI_PROJECT_URL -p branch=$CI_COMMIT_BRANCH -p commit=$CI_COMMIT_SHA
Wie verteile ich mein NuGet Package?
Wenn ich das NuGet Package nun erstellt habe, möchte ich es erst einmal testen. Dafür bietet NuGet die Möglichkeit im lokalen System einen Speicherort zu definieren. Wenn dann das Package funktioniert, kann es in das eigentliche Repository hochgeladen werden.
Publish Dateisystem
Wenn man in der csproj Datei als PackageOutputPath einen lokalen Ordner angibt, wird beim „packen“ das NuGet Package in den Ordner kopiert:
<PropertyGroup> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <PackageOutputPath>C:\Temp\</PackageOutputPath> <\PropertyGroup>
Damit man dieses Package nun im gewünschen Projekt nutzen kann, muss man dort die NuGet.config anpassen:
<configuration> <packageSources> <add key="NoserLocalPublish" value="C:\Temp"/> </packageSources> </configuration>
Und natürlich das Package im Projekt mit der PackageId referenzieren: <PackageReference Include=“NoserService.For.All“ />
Publish Repository
Entweder gibt es für das Repository auch eine WebApp, die man für den Upload benutzen kann oder man benutzt die Commandline mit der nuget.exe oder der dotnet.exe. Die WebApp hilft leider nicht beim automatischen Hochladen, jedoch kann der Provider der WebApp auch eine Schnittstelle (z.b. REST) anbieten.
Wie nutze ich NuGet Packages?
Eine benutzerfreundliche Möglichkeit ist der Nuget Package Manager im Visual Studio, den eigentlich jeder kennt. Er bietet die Möglichkeit nach Packages zu suchen, ihre Versionen zu prüfen und sie zu installieren, aktualisieren oder deinstallieren. Wenn man ein Paket auf einem Projekt verändert, wird die .csproj Datei des Projektes automatisch angepasst.
Dann gibt es noch die nuget.exe, die über die command line benutzt wird. Um nach einem Package zu suchen, gibt es den Befehl „nuget list packagId“. Weitere Optionen sind auf microsoft beschrieben.
Des Weiteren ermöglicht die dotnet.exe auch nuget commands auszuführen und mit „dotnet restore“ können alle Packages eines Projekts/einer Solution geholt werden.
NuGet.Config
Die NuGet.Config definiert die Repositories, die für die Packages verwendet werden. Der Standardwert ist das öffentliche api.nuget.org/v3 Repository. Eigene firmeninterne Repositories können in dieser NuGet.Config definiert werden. Des Weiteren definiert die config den Pfad für die Zwischenspeicherung der Packages, bevor dann die nötige Version (Packageversion, sowie dotnet Version) in den binary Ordner des C# Projektes gezogen wird. Microsoft stellt eine standard NuGet.Config zur Verfügung. Weitere settings sind ebenfalls auf microsoft.com zu finden.
Auf einem Computer können mehrere NuGet.Config Dateien benutzt werden. NuGet sucht immer vom Projektpfad ausgehend den Verzeichnispfad nach oben, bis er eine NuGet.Config findet.
Weitere detaillierte Informationen liefert microsoft.
Reference Packages in .csproj
Die Properties „PrivateAssets“, „IncludeAssets“ und „ExcludeAssets“ haben mir bei unserem Projekt geholfen. Diese Properties definieren, ob nur das aktuelle Projekt die angezogenen Assets (Assemblies, Files…) verwendet, oder weiter an die höheren, referenzierten Projekte weitergegeben werden sollen.
<PackageReference Include="AnyPackageId" Version="[5.6.6]"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <ExlcudeAssets>None</ExcludeAssets> </PackageReference>
Wir haben zum Beispiel ein Assembly, welches wir für das Kompilieren eines Projektes (Base-Projekt) benötigen. Dieses Projekt wird auf verschiedenen Systemen verwendet, die jedoch eine spezifische Version dieses Assemblies brauchen. Nun können wir beim Kompilieren des Base-Projekts ein Common-Assembly benutzen und die Referenz mit „PrivateAssets=All“ definieren.
<ItemGroup Condition=" '$(Configuration)' == 'Release' "> <PackageReference Include="WeNeedYou_Common" Version="1.*"> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup>
So wird das Assembly für das Base-Projekt verwendet. Jedoch verweisen wir dann auf den eigentlichen Systemen auf das spezifische Assembly.
<ItemGroup Condition=" '$(Configuration)' == 'Release' "> <PackageReference Include="WeNeedYou_ToHaveAGoodTime" Version="1.*" /> </ItemGroup>
Denn ohne diesem PrivateAssets Verweis würde beim Build auf den spezifischen Projekten die *.dll Datei nach Zufallsprinzip überschrieben werden.
Zudem können diese Properties auch auf ProjectReferences angewendet werden:
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ProjectReference Include="TheNoserProject\.TheNoserProject.csproj" PrivateAssets="all"/> </ItemGroup>
Versionierung mit NuGet
Dass es für Pakete Versionen braucht ist ja bekannt, nur wie löst Nuget das nun? Wie man die Versionen bei der Paketerstellung zuweist, habe ich oben schon in den Beispielen beschrieben. Wie nutze ich die Versionen nun, wenn ich die Packages verwende?
<PackageReference Include="WeNeedYou_Common" Version="1.0.*" />
Man kann mit Klammern definieren, ob man strikt diese Version möchte, oder wie im Beispiel ab 1.0. immer die Neueste. Alle möglichen Definitionen sind hier aufgelistet.
In der NuGet.Config kann man definieren, wie Nuget handeln soll, wenn die angegebene Version nicht verfügbar ist, oder bei der Version „*“ verwendet und welche „nächste“ Version NuGet dann anziehen soll.
<add key="dependencyVersion" value="Highest" />
Wie auf microsoft beschrieben, kann man dieses Property auch während des restores definieren.
NuGet what you get
NuGet bietet einige Properties an, die nicht sonderlich bekannt sind. Wenn man diese aber kennt, kann man viele Probleme lösen und NuGet Packages nützlich generieren und einsetzen.
Mein Ziel war es mit diesem Blog einen Überblick über die Optionen zu geben, die NuGet und das .nuspec Universum bieten und ich hoffe, die meisten Informationen und hilfreichsten Links hier gesammelt zu haben.