Multi-Container ASP .NET Core Web App with Docker Compose

Previously, we have seen how we could containerise our ASP .NET Core 6.0 web app and manage it with docker commands. However, docker commands are mainly for only one image/container. If our solution has multiple containers, we need to use docker-compose to manage them instead.

docker-compose makes things easier because it encompasses all our parameters and workflow into a configuration file in YAML. In this article, I will share my first experience with docker-compose to build mutli-container environments as well as to manage them with simple docker-compose commands.

To help my learning, I will create a simple online message board where people can login with their GitHub account and post a message on the app.

PROJECT GITHUB REPOSITORY

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

Create Multi-container App

We will start with a solution in Visual Studio with two projects:

  • WebFrontEnd: A public-facing web application with Razor pages;
  • MessageWebAPI: A web API project.

By default, the web API project will have a simple GET method available, as shown in the Swagger UI below.

Default web API project created in Visual Studio will have this WeatherForecast API method available by default.

Now, we can make use of this method as a starting point. Let’s have the our client, WebFrontEnd, to call the API and output the result returned by the API to the web page.

var request = new System.Net.Http.HttpRequestMessage();
request.RequestUri = new Uri("http://messagewebapi/WeatherForecast");

var response = await client.SendAsync(request);

string output = await response.Content.ReadAsStringAsync();

In both projects, we will add Container Orchestrator Support with Linux as the target OS. Once we have the docker-compose YAML file ready, we can directly run our docker compose application by simply pressing F5 in Visual Studio.

The docker-compose YAML file for our solution.

Now, we shall be able to see the website output some random weather data returned by the web API.

Congratulations, we’re running a docker compose application.

Configure Authentication in Web App

Our next step is to allow users to login to our web app first before they can post a message on the app.

It’s usually a good idea not to build our own identity management module because we need to deal with a lot more than just building a form to allow users to create an account and type their credentials. One example will be managing and protecting our user’s personal data and passwords. Instead, we should rely on Identity-as-a-Service solutions such as Azure Active Directory B2C.

Firstly, we will register our web app in our Azure AD B2C tenant.

Normally for first-timers, we will need to create a Azure AD B2C tenant first. However, there may be an error message saying that our subscription is not registered to use namespace ‘Microsoft.AzureActiveDirectory’. If you encounter this issue, you can refer to Adam Storr’s article on how to solve this with Azure CLI.

Once we have our Azure AD B2C tenant ready (which is Lunar in my example here), we can proceed to register our web app, as shown below. For testing purposes, we set the Redirect URI to https://jwt.ms, a Microsoft-owned web application that displays the decoded contents of a token. We will update this Redirect URL in the next section below when we link our web app with Azure AD B2C.

Registering a new app “Lunar Message Wall” under the Lunar Tenant.

Secondly, once our web app is registered, we need to create a client secret, as shown below, for later use.

Secrets enable our web app to identify itself to the authentication service when receiving tokens. In addition, please take note that although certificate is recommended over client secret, currently certificates cannot be used to authenticate against Azure AD B2C.

Adding a new client secret which will expire after 6 months.

Thirdly, since we want to allow user authentication with GitHub, we need to create a GitHub OAuth app first.

The Homepage URL here is a temporary dummy data.

After we have registered the OAuth app on GitHub, we will be provided a client ID and client secret. These two information are needed when we configure GitHub as the social identity provider (IDP) on our Azure AD B2C, as shown below.

Configuring GitHub as an identity provider on Azure AD B2C.

Fourthly, we need to define how users interact with our web app for processes such as sign-up, sign-in, password reset, profile editing, etc. To keep thing simple, here we will be using the predefined user flows.

For simplicity, we allow only GitHub sign-in in our user flow.

We can also choose the attributes we want to collect from the user during sign-up and the claims we want returned in the token.

User attributes and token claims.

After we have created the user flow, we can proceed to test it.

In our example here, GitHub OAuth app will be displayed.

Since we specify in our user flow that we need to collect the user’s GitHub display name, there is a field here for the user to enter the display name.

The testing login page from running the user flow.

Setup the Authentication in Frontend and Web API Projects

Now, we can proceed to add Azure AD B2C authentication to our two ASP.NET Core projects.

We will be using the Microsoft Identity Web library, a set of ASP.NET Core libraries that simplify adding Azure AD B2C authentication and authorization support to our web apps.

dotnet add package Microsoft.Identity.Web

The library configures the authentication pipeline with cookie-based authentication. It takes care of sending and receiving HTTP authentication messages, token validation, claims extraction, etc.

For the frontend project, we will be using the following package to add GUI for the sign-in and an associated controller for web app.

dotnet add package Microsoft.Identity.Web.UI

After this, we need to add the configuration to sign in user with Azure AD B2C in our appsettings.json in both projects (The ClientSecret is not needed for the Web API project).

"AzureAdB2C": {
    "Instance": "https://lunarchunlin.b2clogin.com",
    "ClientId": "...",
    "ClientSecret": "...",
    "Domain": "lunarchunlin.onmicrosoft.com",
    "SignedOutCallbackPath": "/signout/B2C_1_LunarMessageWallSignupSignin",
    "SignUpSignInPolicyId": "B2C_1_LunarMessageWallSignupSignin"
}

We will use the configuration above to add the authentication service in Program.cs of both projects.

With the help of the Microsoft.Identity.Web.UI library, we can also easily build a sign-in button with the following code. Full code of it can be seen at _LoginPartial.cshtml.

<a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>

Now, it is time to update the Redirect URI to the localhost. Thus, we need to make sure our WebFrontEnd container has a permanent host port. To do so, we first specify the ports we want to use in the launchsettings.json of the WebFrontEnd project.

"Docker": {
    ...
    "environmentVariables": {
      "ASPNETCORE_URLS": "https://+:443;http://+:80",
      "ASPNETCORE_HTTPS_PORT": "44360"
    },
    "httpPort": 51803,
    "sslPort": 44360
}

Then in the docker-compose, we will specify the same ports too.

services:
  webfrontend:
    image: ${DOCKER_REGISTRY-}webfrontend
    build:
      context: .
      dockerfile: WebFrontEnd/Dockerfile
    ports:
      - "51803:80"
      - "44360:443"

Finally, we will update the Redirect URI in Azure AD B2C according, as shown below.

Updated the Redirect URI to point to our WebFrontEnd container.

Now, right after we click on the Sign In button on our web app, we will be brought to a GitHub sign-in page, as shown below.

The GitHub sign-in page.

Currently, our Web API has only two methods which have different required scopes declared, as shown below.

[Authorize]
public class UserMessageController : ControllerBase
{
    ...
    [HttpGet]
    [RequiredScope("messages.read")]
    public async Task<IEnumerable<UserMessage>> GetAsync()
    {
        ...
    }

    [HttpPost]
    [RequiredScope("messages.write")]
    public async Task<IEnumerable<UserMessage>> PostAsync(...)
    {
        ...
    }
}

Hence, when the frontend needs to send the GET request to retrieve messages, we will first need to get a valid access token with the correct scope.

string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "https://lunarchunlin.onmicrosoft.com/message-api/messages.read" });

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

Database

Since we need to store the messages submitted by the users, we will need a database. Here, we use PostgresSQL, an open-source, standards-compliant, and object-relational database.

To run the PostgresSQL with docker-compose we will update our docker-compose.yml file with the following contents.

services:
  ...
  messagewebapi:
    ...
    depends_on:
     - db

  db:
    container_name: 'postgres'
    image: postgres
    environment:
      POSTGRES_PASSWORD: ...

In our case, only the Web API will interact with the database. Hence, we need to make sure that the db service is started before the messagewebapi. In order to specify this relationship, we will use the depends_on option.

User’s messages can now be stored and listed on the web page.

Next Step

This is just the very beginning of my learning journey of dockerising ASP .NET Core solution. In the future, I shall learn more in this area.

References

[KOSD] Dockerise an ASP .NET Core 6.0 Web App

Let’s assume now we want to dockerise a new ASP .NET Core 6.0 web app project that we have locally.

Now, when we build and run the project, we should be able to view it on localhost as shown below.

The default homepage of a new ASP .NET Core web app.

Before adding the .NET app to the Docker image, first it must be published with the following command.

dotnet publish --configuration Release
Build, run, and publish our ASP .NET Core web app.

Create the Dockerfile

Now, we will create a file named Dockerfile in directory containing the .csproj and open it in VS Code. The content of the Dockerfile is as follows.

FROM mcr.microsoft.com/dotnet/aspnet:6.0

COPY bin/Release/net6.0/publish/ App/
WORKDIR /App
ENTRYPOINT ["dotnet", "Lunar.Dashboard.dll"]

A Dockerfile must begin with a FROM instruction. It specifies the parent image from which we are building. Here, we are using mcr.microsoft.com/dotnet/aspnet:6.0, an image contains the ASP.NET Core 6.0 and .NET 6.0 runtimes for running ASP.NET Core web apps.

The COPY command tells Docker to copy the publish folder from our computer to the App folder in the container. Then the current directory inside of the container is changed to App with the WORKDIR command.

Finally, the ENTRYPOINT command tells Docker to configure the container to run as an executable.

Docker Build

Now that we have the Dockerfile, we can build an image from it.

In order to perform docker build, we first need to navigate the our project root folder and issue the docker build command, as shown below.

docker build -t lunar-dashboard -f Dockerfile .

We assign a tag lunar-dashboard to the image name using -t. We then specify the name of the Dockerfile using -f. The . in the command tells Docker to use the current folder, i.e. our project root folder, as the context.

Once the build is successful, we can locate the newly created image with the docker images command, as highlighted in the screenshot below.

The default docker images command will show all top level images.

Create a Container

Now that we have an image lunar-dashboard that contains our ASP .NET Core web app, we can create a container with the docker run command. 

docker run -d -p 8080:80 --name lunar-dashboard-app lunar-dashboard

When we start a container, we must decide if it should be run in a detached mode, i.e. background mode, or in a foreground mode. By default the container will be running in foreground.

In the foreground mode, the console that we are using to execute docker run will be attached to standard input, output and error. This is not what we want. What we want is after we start up the container, we can still use the console for other commands. Hence, the container needs to be in detached mode. To do so, we use the -d option which will start the container in detached mode.

We then publish a port of the container to the host with -p 8080:80, where 8080 is the host port and 80 is the container port.

Finally, we name our container lunar-dashboard-app with the --name option. If we do not assign a container name with the --name option, then the daemon will generate a random string name for us. Most of the time, the auto-generated name is quite hard to remember, so it’s better for us to give a meaningful name to the container so we can easily refer the container later.

After we run the docker run command, we should be able to find our newly created container lunar-dashboard with the docker ps command, as shown in the following screenshot. The option -a is to show all containers because by default docker ps will show only containers which are running.

Our container lunar-dashboard is now running.

Now, if we visit the localhost at port 8080, we shall be able to see our web app running smoothly.

This web app is hosting on our Docker container.

Running Docker on Windows 11 Home

You may notice that I am using WSL (Windows Subsystem for Linux). This is because to install Docker Desktop, which includes the Docker Engine that builds and containerise our apps, on Windows, we need to have Hyper-V feature enabled. However, Hyper-V is available only on Windows 10/11 Pro, Enterprise, and Education.

Hence, I have no choice but to use WSL, which runs a Linux kernel inside of a lightweight utility VM. WSL provides a mechanism for running Docker (with Linux containers) on the Windows machine.

For those who are interested to read more about this, please refer to a very detailed online tutorial about how to run Docker with WSL, written by Padok SRE Baptiste Guerin.

References

KOSD, or Kopi-O Siew Dai, is a type of Singapore coffee that I enjoy. It is basically a cup of coffee with a little bit of sugar. This series is meant to blog about technical knowledge that I gained while having a small cup of Kopi-O Siew Dai.

WinUI 3 Learning Notes

In November 2021, WinUI 3 was released in Windows App SDK 1.0 Stable.

Two months after that, I decided to give it a try by creating a toy app called “WinUI Desktop” with WinUI 3 to experience the development journey.

The Tweets page of our toy app.

The app has three main pages.

  • Tweets (TwitterPage.xaml): Users can search the tweets by tags.
  • Web Browser (WebViewPage.xaml): Users can view the image of a selected tweet in a larger view here.
  • Canvas (CanvasPage.xaml): Draw random circles and rectangles on a canvas using Win2D library.

In this article, I’ll share about things I have learned while building the toy app above.

PROJECT GITHUB REPOSITORY

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

About WinUI

WinUI 3 is a native UI layer which contains modern controls and styles from Fluent Design for building Windows apps. WinUI 3 is the next generation of the WinUI framework, and is now available for production apps.

Unlike WinUI 2.x, which is a control library that sits on top of UWP, WinUI 3 works with any app supported by the Windows App SDK. We can use WinUI 3 as the entire UI layer for building our desktop app. Another more subtle difference is that WinUI 3 targets .NET 5, whilst UWP still uses .NET Native.

According to Microsoft, WinUI 3 is currently supported for use by full WinUI 3 apps only. We will be able to use WinUI 3 in WPF and WinForms only in a future release via XAML Islands.

WinUI 3 controls and styles are demonstrated in a free app known as WinUI 3 Controls Gallery on Microsoft Store.

Why WinUI 3?

I’d like to share with you some of the benefits of developing with WinUI 3 that I found online.

  • Modern GUI Design: Our apps will be up-to-date with latest visual elements and controls with the latest Fluent Design without requiring an updated Windows SDK;
  • Backward Compatible: WinUI 3 provides backward compatibility with a wide range of Windows 10 versions;
  • UX Stack Decoupling: In WinUI 3, UX stack and control library completely decoupled from the OS and the Windows 10 SDK. Hence, unlike UWP, WinUI 3 allows us to use the latest UI controls without updating Windows;
  • Open-Source: WinUI is an open-source project hosted on Github;
  • Latest .NET Support: We can use .NET 5 or later for developing WinUI 3 app but currently there is no official support for .NET 5 UWP yet.

Introduction of Windows App SDK (Project Reunion)

WinUI 3 now comes as a feature in the current releases of the Windows App SDK.

Highlights of Windows App SDK. (Image Source: Microsoft GitHub)

The Windows App SDK is a set of unified developer components and tools that can be used in a consistent way by any desktop app on Windows 11 and backwards compatible all the way until Windows 10, version 1809.

The Windows App SDK provides a broad set of Windows APIs—with implementations that are decoupled from the OS, and released to developers via NuGet packages.

We need to take note that the Windows App SDK is not meant to replace the Windows SDK which will continue to work as is.

Finally, since Windows App SDK is backward compatible, we don’t need to write version adaptive code, as long as our users are at least on Windows 10 (version 1809).

Hot Reload

In May 2021, Microsoft announced the availability of the .NET Hot Reload experience.

Due to compat-breaking changes between prerelease versions of the Windows App SDK, Hot Reload in WinUI 3 does not have backwards/forwards compatibility. Hence, in the future, Visual Studio 17.0 GA (aka VS 2022) and beyond will support Windows App SDK 1.0 GA and beyond, up until Windows App SDK 2.0.

Hot Reload, Live Visual Tree, and Live Property explorer support. (Image Source: Microsoft GitHub)

High Performance Data Binding with {x:Bind}

Data binding is a way for the UI to display data, and optionally to stay in sync with that data. Data binding allows us to separate the concern of data from the concern of UI.

Currently, there are two ways of doing data binding, i.e. {Binding} and {x:Bind} markup extensions. The binding objects created by both are mostly functionally equivalent. For {x:Bind}, there will be no reflection needed because code is generated along with XAML files that contain controls and DataTemplates, in .g.cs and .g.i.cs files. Hence, {x:Bind} will have great performance, provide compile-time validation of our binding expressions, and support debugging by set breakpoints in the .g.cs code files.

Since {x:Bind} is new for Windows 10, {x:Bind} is currently only supported in the both UWP and WinUI but not in WPF even though all of them using XAML for frontend.

Microsoft.Toolkit.Mvvm

In order to decoupling front-end and back-end codes, there is a UI architectural design pattern, the MVVM (Model-View-ViewModel), introduced by Microsoft in 2005 in order to support the development of XAML apps in WPF, UWP, and now WinUI.

In Nov 2020, Michael A. Hawker from Microsoft and Sergio Pedri from .NET Foundation announced the MVVM Toolkit is officially available in Windows Community Toolkit. MVVM Toolkit is a modern, fast, and modular MVVM library.

The view models in our WinUI app require the property change notification support. Hence, we will make our view models inherit from the ObservableObject base class.

ObservableObject provides a base implementation for INotifyPropertyChanged and exposes the PropertyChanged event. In addition, it provides a series of SetProperty methods that can be used to easily set property values and to automatically raise the appropriate events. The following code snippet shows how we implement that in the view model for a page called TwitterPage in the app.

public class TwitterPageViewModel : ObservableObject
{
    private string _searchingQuery;

    ...

    public string SearchingQuery
    {
        get => _searchingQuery;
        private set => SetProperty(ref _searchingQuery, value);
    }

    ...
}

The provided SetProperty method checks the current value of the property, and updates it if different, and then also raises the relevant events automatically.

Then we can do data binding accordingly in the XAML of the TwitterPage.

<TextBlock Margin="15" FontSize="48" Foreground="White"
           Text="{Binding SearchingQuery}" />

In the MVVM Toolkit, in order to bind commands between the view model and a GUI element, we need to use RelayCommand or AsyncRelayCommand which supports asynchronous operations. Both of them are ICommand implementations that can expose a method or delegate to the view.

For example, if we would like to have a button that can refresh the tweets in the TwitterPage when the button is clicked, we will do the following in the view model of the page.

public class TwitterPageViewModel : ObservableObject
{
    ...

    public TwitterPageViewModel()
    {
        ...

        RefreshTweetsCommand = new AsyncRelayCommand(RefreshTweetsAsync);
    }

    public ICommand RefreshTweetsCommand { get; }

    private async Task RefreshTweetsAsync() => await FetchTweetsAsync(_searchingQuery);

}

Then we can bind the command to a button on the TwitterPage as shown below.

<Button ...
    Command="{Binding RefreshTweetsCommand}" />

AppConfig in WinUI 3

Since we need to make API calls to Twitter, we have to provide relevant API keys and secrets. Instead of hardcoding the keys in the project, it’s better to keep them in another file appsettings.json which is also ignored in Git.

{
  "TwitterConfig": {
    "ConsumerApiKey": "...",
    "ConsumerApiSecret": "...",
    "AccessToken": "...",
    "AccessTokenSecret": "..."
  }
}

In order to use the JSON-based configuration in our WinUI app, we will need to install the following two NuGet packages first.

  • Microsoft.Extensions.Configuration;
  • Microsoft.Extensions.Configuration.Json.

To contain all the configuration logic in one place, we create an AppConfig class with the configuration pipeline as shown below.

public class AppConfig
{
    private readonly IConfigurationRoot _configurationRoot;

    public AppConfig()
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Package.Current.InstalledLocation.Path)
            .AddJsonFile("appsettings.json", optional: false);

        _configurationRoot = builder.Build();
    }

    ...
}

Then within the same AppConfig class, we can retrieve the Twitter API keys as shown below.

public TwitterConfig TwitterConfiguration 
{
    get 
    {
        var config = _configurationRoot.GetSection(nameof(TwitterConfig)).GetChildren();

        var twitterConfig = new TwitterConfig 
        {
            ConsumerApiKey = config.First(c => c.Key == "ConsumerApiKey").Value,
            ConsumerApiSecret = config.First(c => c.Key == "ConsumerApiSecret").Value,
            AccessToken = config.First(c => c.Key == "AccessToken").Value,
            AccessTokenSecret = config.First(c => c.Key == "AccessTokenSecret").Value
        };

        return twitterConfig;
    }
}

Finally, since we will be retrieving tweets in the view model of the TwitterPage, we will use the AppConfig class as follows in the view model.

public class TwitterPageViewModel : ObservableObject
{
    private AppConfig _appConfig = new();

    ...

    public TwitterPageViewModel()
    {
        _twitterClient = new TwitterClient(
            _appConfig.TwitterConfiguration.ConsumerApiKey,
            _appConfig.TwitterConfiguration.ConsumerApiSecret,
            _appConfig.TwitterConfiguration.AccessToken,
            _appConfig.TwitterConfiguration.AccessTokenSecret);

        ...
    
    }
    ...
}

Chromium-based WebView Control

In November 2020, Microsoft announced that WebView2 was generally available for use in production Windows Forms, WPF, and WinUI apps. WebView2 is a new embedded web control built on top of Microsoft Edge (Chromium). This means that we now have access to the latest web tech in our desktop app as well. 

Viewing Twitter image on WebView2 control. (Image Used: @CuppyDraws)

In WinUI 3 (as well as UWP and WPF), we can use the NavigationView control for top-level navigation in our app.

<NavigationView x:Name="MainNavigationView"
    AlwaysShowHeader="False"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    ...>
    <NavigationView.MenuItems>
        <NavigationViewItem Content="Tweets" Tag="Twitter" ... />
        ...
    </NavigationView.MenuItems>
    <Frame x:Name="ContentFrame" />
</NavigationView>

The NavigationView doesn’t perform any navigation tasks automatically. When we tap on a navigation item, an ItemInvoked event will be raised. Hence, we can make use of the ItemInvoked event and set a convention such that the Tag of the navigation will tell the app which page it should be navigated to once it’s tapped, as demonstrated in the code below.

public sealed partial class MainWindow : Window
{
    ...

    public void SetCurrentNavigationViewItem(NavigationViewItem item, object parameter)
    {
        if (item == null || item.Tag == null) return;

        ContentFrame.Navigate(
            Type.GetType($"Lunar.WinUI3Playground.Pages.{item.Tag}Page"), parameter);

        MainNavigationView.Header = item.Content;
        MainNavigationView.SelectedItem = item;
    }

    void MainNavigationView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args)
    {
        SetCurrentNavigationViewItem(
            (NavigationViewItem)sender.SelectedItem, null);
    }

    ...
}

NavigationView has a built-in back button; but there is also no backwards navigation implemented automatically. Hence, we need to handle that manually.

<NavigationView
    x:Name="MainNavigationView"
    ...
    BackRequested="MainNavigationView_BackRequested"
    IsBackEnabled="{x:Bind ContentFrame.CanGoBack, Mode=OneWay}">

We need to handle the BackRequested event which is raised when the back button on the NavigationView is tapped.

void MainNavigationView_BackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args)
{
    if (ContentFrame.CanGoBack)
    {
        ContentFrame.GoBack();
    }
}

We also bind the IsBackEnabled property to the CanGoBack property of our navigation frame, i.e. ContentFrame so that the back button is only enabled when we are allowed to navigate backwards.

Dark Mode and Light Mode

We can control the application UI theme to be either light or dark theme via a settings page.

By default, the NavigationView will have a Settings button because its IsSettingsVisible is true by default. Since the default tag of the Settings button is “Settings”, we can simply link it up to our settings page by creating a SettingsPage. We can use the SettingsPagePage.xaml from the Windows Template Studio as the reference of our SettingsPage.

Settings page copied from the Windows Template Studio.

We first need to create a static class ThemeSelectorService. In this service, we will save and load the app theme in a setting called “AppBackgroundRequestedTheme” in the ApplicationData.LocalSettings.

Then we will also set the app theme in the service as follows.

public static void SetRequestedTheme()
{
    ((FrameworkElement)((MainWindow)(Application.Current as App).MainWindow).Content).RequestedTheme = Theme;
}

Finally, we will initialise the service when the app is launched normally, as demonstrated below.

protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    ThemeSelectorService.Initialize();
}

Win2D in WinUI 3

Win2D is a Windows Runtime API for immediate mode 2D graphics rendering with GPU acceleration. It utilizes the power of Direct2D, and integrates seamlessly with XAML. Previously, Win2D is available for UWP only. Now the team is moving Win2D onto WinUI 3. However, we can already make use of the Win2D library now in WinUI 3 with Microsoft.Graphics.Win2D.

Drawing circles and rectangles on the Win2D CanvasControl.

In this app, instead of directly drawing to the CanvasControl, we draw to the Command List Drawing Session (CLDS) where command list is an intermediate object that stores the results of rendering for later use.

void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    ...

    CanvasCommandList cl = new CanvasCommandList(sender);
    using (CanvasDrawingSession clds = cl.CreateDrawingSession())
    {
        ...
        clds.DrawCircle(...);
        ...
        clds.DrawRectangle(...);
        ...
    }

    args.DrawingSession.DrawImage(cl);
}

Deploy WINUI 3 App to Microsoft Store

After we have successfully reserved an app name on Microsoft Store, we can then associate our app with it in Visual Studio, as shown below. The following popup is shown after we right-click on WinUI 3 project in the Solution Explorer and choose “Package and Publish” then “Associate App with the Store…”.

Selecting an app name reserved on Microsoft Store.

We will then be alerted when we try to build our WinUI 3 project again because our Package.appxmanifest file has an error, as shown below.

The error says the required attribute “PhonePublisherId” is missing.

If we’re to open the file in XML editor, we will notice that PhoneIdentity is being added without us knowing, as highlighted in the following screenshot. This line should not be needed because our WinUI 3 app is not going to be made available on Windows Phone (in fact, there is no more Windows Phone).

We need to delete the PhoneIdentity to proceed.

Hence, the error will be gone after we delete the PhoneIdentity line. Now, we can continue to package and publish our app to the Microsoft Store, as shown below.

Uploading the msix of our WinUI 3 app to the Microsoft Store.

Finally, we also need to take note that currently in Windows App SDK version 1.0, only MSIX packaged apps that are full trust or have the packageManagement restricted capability have the permission to use the deployment API to install the main and singleton package dependencies.

References

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