Publish C# Library to NuGet Gallery with Azure DevOps

It’s always a good idea to not only make our libraries open-source, but also publish them as a package for public to use if our libraries can make the life of other developers better.

In this article, I’d like to share how we can use the pipeline in Azure DevOps to auto build and publish a C# library that has its source code on GitHub to the NuGet Gallery, the .NET package repository.

PROJECT GITHUB REPOSITORY

The complete source code of this project can be found at https://github.com/goh-chunlin/WordpressRssFeed.

As you can see in the csproj file of the library project, we have the following properties are required to create a package.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
	<TargetFramework>netstandard2.0</TargetFramework>
        ...
	<PackageId>WordpressRssFeed</PackageId>
	<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
	<Description>A reusable codes library for reading WordPress RSS feeds.</Description>
	<Authors>Goh Chun Lin</Authors>
	<Copyright>Copyright 2022, Goh Chun Lin</Copyright>
	<PackageTags>Wordpress</PackageTags>
	<Company>Goh Chun Lin</Company>
	<RepositoryType>git</RepositoryType>
        <RepositoryUrl>https://github.com/goh-chunlin/WordpressRssFeed</RepositoryUrl>
	<PackageReleaseNotes>Please refer to README.</PackageReleaseNotes>
    </PropertyGroup>

    ...

</Project>

Azure DevOps Project

On Azure DevOps, we can create a pipeline which has source code sitting in a GitHub repository. We simply select “GitHub” as the source and then choose the correct repository and branch which contains the code that the pipeline should build.

Selecting a repository and branch on the GitHub as the pipeline source.

The DevOps project is made open to public. So, you can view its build history at https://dev.azure.com/gohchunlin/WordpressRssFeed/_build.

Build Number

Before we begin to look at the pipeline tasks, we need to setup the build number properly because we will later use it to version our package.

Firstly, we will setup BuildConfiguration, MajorVersion, MinorVersion, and BuildRevision as shown in the following screenshot.

We will start our first stable build from 1.0.0.

Next, we need to format the build number with MajorVersion, MinorVersion, and BuildRevision as shown below.

We will use the standard format $(MajorVersion).$(MinorVersion).$(BuildRevision) for build number.

NuGet 6 and .NET 6

Currently (Jan 2022), the latest version of NuGet.exe is 6.0.0 and the latest .NET SDK is 6.0.1. The main reason why we choose to use the latest versions is because we’d like to use the latest feature where we can pack a README.md file in our NuGet package and have it fully rendered on NuGet.org! This feature was newly introduced in May 2021 and was still in preview back then with NuGet 5.10 Preview 2 and .NET SDK 5.0.300 Preview.

Using .NET SDK 6.0.101.

Restore, Build, Test, Pack

We then need to point the restore and build tasks to our library csproj, as shown below.

The library project is built in release mode, which is specified in BuildConfiguration variable.

After the build succeeds, we can proceed to run our test cases in the test project, as shown in the following screenshot.

It’s important to test the library first before publishing it to the public.

We can pack our library once the test cases are all green. The package will be created in the $(Build.ArtifactStagingDirectory) directory, as shown below. Here, we need to make sure that we setup the package versioning properly. In order to make things easier, we will simply use the build number as our package version.

Using the build number as the package version.

If you are interested about the output of the pack task, you can also publish it to the drop as shown in the “Publish Artifact” as shown in the screenshot below. Otherwise, you can skip this task.

This task is optional. It is only for you to download the output of the “dotnet pack” task.

Publish Package to NuGet.org

Since our package is created in $(Build.ArtifactStagingDirectory), so we can specify the path to publish as shown in the screenshot below.

This pipeline has been linked with NuGet.org through an API key. So package will be uploaded directly to NuGet.org.

Add Readme File

Firstly, we will have a README.md file in the root of our library project, as demonstrated below.

Then we need to reference the README.md in the project file as follows.

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        ...
	<PackageReadmeFile>README.md</PackageReadmeFile>
    </PropertyGroup>

    ...

    <ItemGroup>
	<None Include="README.md" Pack="true" PackagePath="" />
    </ItemGroup>
</Project>

Specify Icon

We can specify the icon for our NuGet package with an image resolution of 128×128 (size is limited to 1MB).

Previously, we could simply host the image online and then specify its URL in the <PackageIconUrl> property. However, starting with NuGet 5.3 and Visual Studio 2019 version 16.3, pack task raises the NU5048 warning if the package metadata only specifies PackageIconUrl property.

Now, we shall use the <PackageIcon> property to specify the icon file path, relative to the root of the library project. In addition, we also need to make sure that the file is included in the package.

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        ...
	<PackageIcon>logo.png</PackageIcon>
    </PropertyGroup>

    ...

    <ItemGroup>
	<None Include="logo.png" Pack="true" Visible="false" PackagePath="" />
    </ItemGroup>
</Project>

License

If you pay attention to the log of the “NuGet Push” task, you will notice that there is a warning about the license information, as shown below.

There is a warning saying that all published packages should have license information specified.

To solve this issue, we can use the <PackageLicenseExpression> property to specify the license of our package. If we’re licensing the package under a common license, like MIT or GPL 3.0, we can use the associated SPDX license identifier.

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        ...
	<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
    </PropertyGroup>

    ...

</Project>

After specifying the license, the warning above will go away.

Alternatively, you can also choose to package a license file by using the <PackageLicenseFile> property to specify the package path, relative to the root of the package. This is similar to how we add an icon to the package, so I won’t repeat the steps of doing that here.

Please take note that only one of PackageLicenseExpression and PackageLicenseFile can be specified at a time. Also, PackageLicenseUrl is deprecated.

Conclusion

It takes a while for our package to be published on NuGet.org. We have to wait a bit longer if it’s the first release of our package.

Once the package is searchable on the NuGet Gallery, we shall be able to see our beautiful package page as shown below.

Oh noes, the build status badge cannot be loaded.

There is one thing to take note here is that due to security and privacy concerns, NuGet.org restricts the domains from which images and badges can be rendered to trusted hosts.

Hence, you may notice that the badge of “Build status” in the screenshot above cannot be loaded. This is because my Azure DevOps is on the old domain, i.e. *.visualstudio.com. The visualstudio.com unfortunately is not one of the trusted domains.

To solve that issue, we should get the badge from the new domain of Azure DevOps, i.e. dev.azure.com, instead.

We can get our status badge from Azure DevOps.

After the update is done, we should be able to see a complete homepage of our NuGet package as shown in the following screenshot.

The homepage of our WordPressRssFeed library (https://www.nuget.org/packages/WordpressRssFeed/).

References

Automated GUI Testing of UWP Apps Using Appium and Azure DevOps

There is a popular yet simple checklist on how good a software team is from Joel Spolsky, who has been the CEO of Stack Overflow until last year (2019). The checklist is called the Joel Test. The test has only 12 items but 7 of them are related about DevOps, debugging, and testing.

Software testing makes sure that the software is doing exactly what it is supposed to do and it also points out all the problems and errors found in the software. Hence, involving testing as early as possible and as frequent as possible is a key to build a quality software which will be accepted by the customers or the clients.

There are many topics I’d love to cover about testing. However, in this article, I will only focus on my recent learning about setting up automated GUI testing for my UWP program on Windows 10.

Appium

One of the key challenges in testing UWP app is to do the GUI testing. In the early stage, it’s possible to do that manually by clicking around the app. However, as the app grows larger, testing it manually is very time consuming. After sharing my thoughts with my senior Riza Marhaban, he introduced me a test automation framework called Appium.

What is Appium? Appium is basically an open source test automation framework for iOS, Android, and Windows apps. Here it says Windows apps because besides UWP, using it to test WPF app is possible as well.

Together with Windows App Driver which enables Appium by using new APIs added in Windows 10 Anniversary Edition, we can use them to do GUI test automation on Windows apps. The following video demonstrates the results of GUI testing with Appium in my demo project Lunar.Paint.Uwp.

Here, I will list down those great tutorials about automated GUI testing of UWP apps using Appium which are ranked top in Google Search:

Some of them were written about four years ago when Barack Obama was still the President of the USA. In addition, none of them continues the story with DevOps. Hence, my article here will talk about GUI testing with Appium from the beginning of a new UWP project until it gets built on Azure DevOps.

🎨  Barack Obama served as the 44th president of the United States from 2009 to 2017. (Image Credit: CBS News) 🎨 

Getting Started with Windows Template Studio for UWP

Here, I will start a new UWP project using Windows Template Studio.

🎨  Configuring the Testing of the UWP app with Win App Driver. 🎨 

There is one section in the project configuration called Testing, as shown in the screenshot above. In order to use Appium, we need to add the testing with Win App Driver feature. After that, we shall see a Test Project suffixed with “Tests.WinAppDriver” being added.

By default, the test project has already come with necessary NuGet packages, such as Appium.WebDriver and MSTest.

🎨  NuGet packages in the test project. 🎨 

Writing GUI Test Cases: Setup

The test project comes with a file called BasicTest.cs. In the file, there are two important variables, i.e. WindowsApplicationDriverUrl and AppToLaunch.

The WindowsApplicationDriverUrl is pointing to the server of WinAppDriver which we will install later. Normally we don’t need to change it as the default value will be “http://127.0.0.1:4723&#8221;.

The AppToLaunch variable is the one we need to change. Here, we need to replace the part before “!App” with the Package Family Name, which can be found in the Packaging tab of the UWP app manifest, as shown in the screenshot below.

🎨  Package Family Name 🎨 

Take note that there is a line of comment right above the AppToLaunch variable. It says, “The app must also be installed (or launched for debugging) for WinAppDriver to be able to launch it.” This is a very important line. It means when we are testing locally, we need to make sure the latest of our UWP app is deployed locally. Also, it means that the UWP app needs to be available on the Build Agent which we will talk about in later part of this article.

I will not go through on how to write the test cases as they are available on my GitHub project: https://github.com/goh-chunlin/Lunar.Paint.Uwp/tree/master/Lunar.Paint.Uwp.Tests.WinAppDrive. Instead, I will highlight a few important points here.

Writing GUI Test Cases: AccessibilityId

In the test cases, to identify the GUI element in the program, we need to use

AppSession.FindElementByAccessibilityId(<The AccessibilityId of the GUI Element>);

By default, the AccessibilityId is mapped to the x:Name of the XAML control in our UWP app. For example, we have a “Enter” button as follow.

<Button x:Name="WelcomeScreenEnterButton"
        Content="Enter"... />

To access this button, in the test code, we can do like the following.

var welcomeScreenEnterButton = AppSession.FindElementByAccessibilityId("WelcomeScreenEnterButton");

Of course, if we want to have an AccessibilityId which is different from the Name of the XAML control (or the XAML control doesn’t have a Name), then we can specify the AccessibilityId in the XAML directly as follows.

<Button x:Name="WelcomeScreenEnterButton"
        AutomationProperties.AutomationId="EnterButton"
        Content="Enter"... />

Then to access this button, in the test code, we need to use EnterButton instead.

var welcomeScreenEnterButton = AppSession.FindElementByAccessibilityId("EnterButton");

Writing GUI Test Cases: AccessibilityName

The method above works well with XAML controls which are having simple text as the content. If the content property is not string, for example if the XAML control is a Grid that consists of many other XAML controls or the XAML control is a custom user control, then Appium will fail to detect the control with the AccessibilityId with the following exception message “OpenQA.Selenium.WebDriverException: An element could not be located on the page using the given search parameters”.

Thanks to GeorgiG from UltraPay, there is a solution to this problem. As GeorgiG pointed out in his post on Stack Overflow, the workaround is to overwrite the AutomationProperties.Name with a non-empty string value, such as “=”.

🎨  My comment on GeorgiG’s solution. 🎨 

Hence, in my demo project, I have the following code for a Grid.

<Grid x:Name="WelcomeScreen" AutomationProperties.Name="-">
    ...
</Grid>

Then in the test cases, I can easily access the Grid with the following code.

var welcomeScreen = AppSession.FindElementByAccessibilityId("WelcomeScreen");

Writing GUI Test Cases: Inspect Tool

The methods listed out above work fine for the XAML controls in our program. How about for the prompt? For example, when user clicks on the “Open” button and an “Open” window is prompted. How do we instruct Appium to react to that?

Here, we will need a tool called Inspect.

We first need to access the Developer Command Prompt for Visual Studio. Then we type “Inspect” to launch the Inspect tool.

🎨  Launched the “Inspect” tool from the Developer Command Prompt for VS 2019. 🎨 

Next, we can mouse over the Open prompt to find out the AccessibilityId of the GUI element that we need to access. For example, the AccessibilityId of the area where we key in the file name is 1148, as shown in the screenshot below.

🎨  Highlighted in red is the AccessibilityId of the File Name text input area. 🎨 

This explains why in the test cases, we have the following code to access it.

var openFileText = AppSession.FindElementByAccessibilityId("1148");

There is also a very good tutorial on how to deal with the Save prompt in the WinAppDriver sample on GitHub. In the sample, it shows how to interact with the Save prompt in the Notepad via Appium.

Alright, that’s all for how to write GUI test cases for our UWP app with Appium. I have the some simple test cases written in my demo project which has its source code available on my GitHub repo, please feel free to review it: https://github.com/goh-chunlin/Lunar.Paint.Uwp/tree/master/Lunar.Paint.Uwp.Tests.WinAppDriver.

🎨  All GUI test cases passed! 🎨 

Azure DevOps Build Pipeline Setup

Now, we have done our software test locally. How do we make the testing to be part of our build pipeline on Azure DevOps?

This turns out to be quite a complicated setup. Here, I setup the build pipeline based on the .NET Desktop pipeline in the template.

🎨  .NET Desktop build pipeline. 🎨 

Next, we need to make sure the pipeline is building our solution with VS2019 on Windows 10 at least. Otherwise, we will receive the error “Error CS0234: The type or namespace name ‘ApplicationModel’ does not exist in the namespace ‘Windows’ (are you missing an assembly reference?)” in the build pipeline.

🎨  The “Agent Specification” of the pipeline needs to be at least “windows-2019”. 🎨 

Now, if we queue our pipeline again, we will encounter a new error which states that “Error APPX0104: Certificate file ‘xxxxxx.pfx’ not found.” This is because for UWP app, we need to package our app with a cert. However, by default, the cert will not be committed to the Git repository. Hence, there is no such cert in the build pipeline.

To solve this problem, we need to first head to the Library of the Pipelines and add the following Variable Group.

🎨  This is basically the file name of the cert and its password. 🎨 

Take note that, the required cert is now still not yet available on the pipeline. Hence, we need to upload the cert as one of the Secured Files in the Library as well.

🎨  Uploaded pfx to the Azure DevOps Pipeline Library. 🎨 

So, how to we move this cert from the Library to the build pipeline? We need the following task.

🎨  Download secure file from the Library. 🎨 

This is not enough because the task will only copy the cert to a temporary storage on the build agent. However, when the agent tries to build, it will still be searching for the cert in the project folder of our UWP app, i.e. Lunar.Paint.Uwp.

Hence, as shown in the screenshot above, we have two more powershell script tasks to do a little more work.

The first script is to add the cert to the certificate store in the build agent. The script can be found on Damien Aicheh’s excellent tutorial about installing cert on Azure DevOps pipeline.

🎨  Installing the cert to the store. 🎨 

The second script after it is to copy the cert from the temporary storage in the build agent to the project folder.

🎨  Copy the cert to our UWP app project folder. 🎨 

Oh ya, as you can see in the screenshot above, I am using NuGet 5.5.1. By default, the NuGet is 4.4.1 in the template. I am worried that it may cause some problems as it did when it was building UWP NuGet library, so I change it to 5.5.1, which is the latest stable version.

With these three new tasks, the build task should be executed correctly.

🎨  Build solution task. 🎨 

Here, my BuildPlatform is x64 and the BuildConfiguration is set to release. Also in the MSBuild Arguments, I specify the PackageCertificatePassword because otherwise it will throw an error in the build process saying “[error]C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Microsoft\VisualStudio\v16.0\AppxPackage\Microsoft.AppXPackage.Targets(828,5): Error : Certificate could not be opened: Lunar.Paint.Uwp_TemporaryKey.pfx.”

Introduction of WinAppDriver to the Build Pipeline

Okay, so how do we run the test cases above on Azure DevOps?

Actually, it only requires the following five steps as highlighted in the following screenshot.

🎨  The five steps for GUI testing. 🎨 

Firstly, we need to start the WinAppDriver.

Secondly, we need to introduce two tasks after it to execute some PowerShell scripts. Before showing what they are doing, we need to recall one thing.

Remember the one line of comment above the AppToLaunch variable in our test project? It says, “The app must also be installed (or launched for debugging) for WinAppDriver to be able to launch it.” Hence, we must install the UWP app using the files in AppxPackages generated by the Build task. This is what the two Powershell tasks are doing.

The first Powershell task is to import the cert to the store.

Import-Certificate -FilePath $(Build.ArtifactStagingDirectory)\AppxPackages\Lunar.Paint.Uwp_1.0.0.0_Test\Lunar.Paint.Uwp_1.0.0.0_x64.cer -CertStoreLocation 'Cert:\LocalMachine\Root' -Verbose

The second task, as shown in the following screenshot, is to install the UWP app using Add-AppDevPackage.ps1. Take note that here we need to do SilentContinue else it will wait for user interaction and cause the pipeline to be stuck.

🎨  Run the PowerShell file generated by Build Solution task directly to install our UWP app. 🎨 

At the point of writing this article, the Windows Template Studio automatically sets the Targeting of the UWP app to be “Windows 10, version 2004 (10.0; Build 19041)”. However, the Azure DevOps pipeline is still not yet updated to Windows 10 v2004, so we should lower the Target Version to be v1903 and minimum version to be v1809 in order to have the project built successfully on the Azure DevOps pipeline.

Thirdly, we will need the test with VsTest. This task exists in the default template and nothing needs to be changed here.

Fourthly, we need to stop the WinAppDriver.

That’s all. Now when the Build Pipeline is triggered, we can see the GUI test cases are being run as well.

🎨  Yay, our GUI test cases are being tested successfully. 🎨 

In addition, Azure DevOps will also give us a nice test report for each of our builds, as shown in the following the screenshot.

🎨  Test report in Azure DevOps. 🎨 

Conclusion: To Be Continued

Well, this is actually just the beginning of testing journey. I will continue to learn more about software testing especially in the DevOps part and share with you all in the future.

Feel free to leave a comment here to share with other readers and me about your thoughts. Thank you!

🎨 To be continued… (Image Credit: JoJo’s Bizarre Adventure) 🎨

Project Links