Last time we built a NuGet package with NuGet Package Explorer and uploaded it from our local device to an Azure DevOps feed. Those were some out of the box situations that most of us, hopefully, don’t encounter that much. So, what if you followed standard practices and stored your code in a Git repo on Azure. How hard is it to get it into a NuGet package and share it through a feed? Let’s explore!

Prerequisites

Before we start, make sure you have a feed set up in Azure (see my earlier post here1 on how to set that up) and a C# library project as a Git repo. If you want, you can download an example project from my GitHub page at brunolatte-allphi/NuGetDemo (github.com) 2. The example has a simple library project with a NuGet dependency on the Newtonsoft Json package for each scenario. All the newer project templates for .NET (standard, core, 5.0) come with packaging out of the box. The more classic .NET Framework projects do not have a ‘package’ option. Knowing that business’ always take a long time to get into the newest tech, we’ll look at both scenarios .

.NET package

Going the .NET way is the easiest and will be common practice soon, so let’s start with that. Like I said before, all .NET projects have packaging support out of the box. It’s as simple as going to the project properties Package tab and filling in the necessary fields. You can even build a local package to make sure it all looks good or drop it directly on NuGet.org. Maybe use NuGet Package Explorer3 to take a look inside.

Next step is to create a pipeline in Azure DevOps. For this demo I’ll be creating a single pipeline to keep it simple. Know that you can easily split everything up in different pipelines and create a Release pipeline with triggers and actions to be taken at each step, but that would go beyond the scope of this post.
What we need is 3 steps to get our package to the feed.

  1. Build the project
  2. Pack the project into a .nupkg file
  3. Push the .nupkg to the feed

On your DevOps page, choose the Pipelines tab and create a new pipeline from a Git repo. Select the ‘Starter pipeline’ template to start configuring. You can remove all the default templated text under ‘steps:’.

For this pipeline we’ll only need the .NET Core task for each of the steps. I find it easiest to use the assistant to set most of the necessary options and then tweak the yaml if needed (e.g., displayName).

Step 1: build

Step 2: pack

Step 3: push

The full yaml looks like this:

trigger:
- master

pool:
  vmImage: ubuntu-latest

steps:
- task: DotNetCoreCLI@2
  displayName: 'Building solution'
  inputs:
    command: 'build'
    projects: '**/NuGetCoreDemo.sln'

- task: DotNetCoreCLI@2
  displayName: 'Packing package'
  inputs:
    command: 'pack'
    packagesToPack: '**/QuoteGeneratorCore.csproj'
    versioningScheme: 'byPrereleaseNumber'
    majorVersion: '1'
    minorVersion: '0'
    patchVersion: '0'

- task: DotNetCoreCLI@2
  displayName: 'Pushing package'
  inputs:
    command: 'push'
    packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
    nuGetFeedType: 'internal'
    publishVstsFeed: 'f1c710n41-elda-4d6e-9004-2d57c69c326e/356408c6-bl0l-48b0-bea3-864cf57a9666'

By default, automatic version numbering is turned off. With this you’ll need to manually set the major-, minor- and patchVersion for a package prior to running the pipeline. Because NuGet packages must be unique I chose automatic version numbering by date and time in step 2. This turns the package into a pre-release version, which is fine when you’re playing around with it and don’t always want to increase the version number. In a real-world scenario, we could use a dedicated “stable” branch where the build pipeline would be triggered every time a merge happens (probably just the master branch when you use Git feature branching). You can then use automatic version numbering using a counter expression for yaml (Expressions – Azure Pipelines | Microsoft Docs4). There’s more then 1 way to do versioning and it is something we can explore in another post.
The selected target feed in step 3 will be translated to its GUID identifier in the yaml, so don’t just copy-paste, it won’t work.
I also use the full name for the solution and project because I have multiple solutions in the Git repo. If you want to know more on this file matching pattern syntax, take a look at File matching patterns reference – Azure Pipelines | Microsoft Docs5.

Tip: use the displayName property to name your tasks, it will make it easier when you look at the job execution of a pipeline.

.NET Framework package

As said before, .NET Framework projects don’t have a ‘Package’ tab in the project properties window. So, where do you set the necessary information? This is done through the ‘Assembly Information’ on the ‘Application’ tab of the project properties.

You’ll notice that there are fewer options. The most important difference is that you can’t set a different name for your NuGet package name. It will take the assembly’s name of your project. It’s a shame they didn’t use the title here, that would have been a better choice in my opinion. But it is what it is . You also can’t build a local .nupkg, not out of the box anyway. You’ll need to use a post build event for that.

For the pipeline we’ll need   to accomplish the same as the .NET version, where we only needed to use the same task type 3 times. Here we’ll need the MSBuild and NuGet task and I needed 1 extra step, to restore any NuGet packages used in the project before building it. (Maybe the .NET Core task does this automatically, if needed you can restore with the same .NET Core task.)
Below is the full yaml.

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: NuGetCommand@2
  displayName: 'Restoring packages'
  inputs:
    command: 'restore'
    restoreSolution: '**/NuGetClassicDemo.sln'

- task: MSBuild@1
  displayName: 'Building solution'
  inputs:
    solution: '**/NuGetClassicDemo.sln'

- task: NuGetCommand@2
  displayName: 'Packing package'
  inputs:
    command: 'pack'
    packagesToPack: '**/QuoteGenerator.csproj'
    versioningScheme: 'byPrereleaseNumber'
    majorVersion: '1'
    minorVersion: '0'
    patchVersion: '0'

- task: NuGetCommand@2
  displayName: 'Push package'
  inputs:
    command: 'push'
    packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg'
    nuGetFeedType: 'internal'
    publishVstsFeed: 'f1c710n41-elda-4d63-9014-2d11c69c3666/356668c6-2371-48b0-bea3-864cf57a9666'

If you need more control over the package, you’ll need to use a .nuspec file. If you give the .nuspec file the same name as the assembly, you won’t even need to change the pipeline. It will just pick up the .nuspec file and use it for the packaging step. Also, if you have dependencies that are not automatically resolved by NuGet e.g., dependencies on dependencies and you want them included, you’ll need to use a .nuspec file (this also applies to .NET packages).
Below is an example.

The $version$ is a replacement token which gets replaced when the package is created, more info at .nuspec File Reference for NuGet | Microsoft Docs6.

Conclusion

If all went well, you should end up with a NuGet package in your Artifacts feed on Azure DevOps. Here you can manage your packages, see how many users have downloaded it etc. There’s more stuff to explore, but I’ll leave that up to you.

Creating and deploying a NuGet package isn’t difficult and with the new .NET versions, it’s even easier. Coming from a .NET framework background myself, it’s refreshing to see .NET being so simple. I’d probably still use a .nuspec file, just to have a single workflow that fits all situations.

Well, that’s enough NuGet exploring for me. Hope you liked it and I’ll see you in the next one.

All the links

1 https://www.allphi.eu/2021/05/azure-nuget-feed/

2 https://github.com/brunolatte-allphi/NuGetDemo

3 https://www.allphi.eu/2021/04/nuget-package-explorer/

4 https://docs.microsoft.com/en-gb/azure/devops/pipelines/process/expressions?view=azure-devops#counter

5 https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/file-matching-patterns?view=azure-devops

6 https://docs.microsoft.com/en-us/nuget/reference/nuspec#replacement-tokens

Overzicht