<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mallibone.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mallibone.com/" rel="alternate" type="text/html" /><updated>2026-07-15T19:43:07+00:00</updated><id>https://mallibone.com/feed.xml</id><title type="html">Mark&apos;s Blog</title><subtitle>Contents on this Site are considered to be mobile friendly.</subtitle><author><name>Mark Allibone</name></author><entry><title type="html">From developer art to good looking apps - an AI design skill for .NET MAUI</title><link href="https://mallibone.com/post/maui-ai-design-skill" rel="alternate" type="text/html" title="From developer art to good looking apps - an AI design skill for .NET MAUI" /><published>2026-07-15T00:03:00+00:00</published><updated>2026-07-15T00:03:00+00:00</updated><id>https://mallibone.com/post/maui-ai-design-skill</id><author><name>Mark Allibone</name></author><category term=".NET MAUI" /><category term="AI" /><category term="Claude Code" /><category term="GitHub Copilot" /><category term="UI" /><summary type="html"><![CDATA[I have a confession to make. Hand me a design and I will happily implement it in .NET MAUI. But ask me to come up with the design myself and things get dicey rather quickly. Since this post is part of MAUI UI July, I thought instead of implementing yet another control, I would go after the root of the problem this year. And teach my AI assistant to be the designer I never was. If you scroll through my older posts you will find the evidence. The layouts were fine, the functionality was there, but the colouring… My process for picking colours was scrolling through the Colors class and choosing whatever name sounded nice. CornflowerBlue has carried more of my side projects than I would like to admit. And when I felt really fancy, I kept the template purple and called it branding. The result is what a designer friend of mine once lovingly called developer art. Now here is the thing: the large language models we all have sitting in our editors these days actually know design. Colour theory, contrast rules, typography - it is all in there. If you ask nicely, you get a decent palette out of them. But I found myself typing the same three paragraphs of instructions into every new chat. And every chat gave me a slightly different answer. One day pure black backgrounds, the next day pastel everything. What I wanted was my design opinions, written down once, applied every time. That is exactly what a skill is. One skill, two assistants A skill is a folder with a SKILL.md file - markdown with a bit of YAML frontmatter that tells the agent what the skill does and when to use it. The agent only loads the file into context when it decides the skill is relevant. What makes this interesting right now: the Agent Skills format started out in the Claude world, but GitHub Copilot adopted it as well. So the same file works in Claude Code, in VS Code with Copilot, in the Copilot CLI and even in the Copilot coding agent on GitHub. The only difference is where the file lives: Claude Code: .claude/skills/maui-app-design/SKILL.md GitHub Copilot: .github/skills/maui-app-design/SKILL.md I keep one copy and symlink the other folder, so the two can never drift apart. The frontmatter is short and sweet: --- name: maui-app-design description: &gt; Create the initial UI design for a .NET MAUI app. Produces a colour palette based on colour theory, sets up an icon font and generates Colors.xaml, Styles.xaml and the first page scaffolds. model: haiku --- More on that model: haiku line in a bit - it is my favourite part. Writing down the colour theory The heart of the skill is a small dose of colour theory, written as rules the model can follow step by step. I will not repeat the whole file here (you can find the complete skill on GitHub), but these are the rules that made the biggest difference for my apps: The 60-30-10 rule. Roughly 60% of a screen should be neutral background and surfaces, 30% your primary colour, and only 10% the accent. The accent is the loudest colour on the screen - it marks the one action that matters. My old UIs happily used three loud colours at 33% each, which is why they screamed at you. Pick colours on the wheel, not from a list. The skill derives a harmony scheme from the mood of the app: analogous (neighbouring hues, ±30°) for calm and professional apps, triadic (±120°) for playful ones, complementary (+180°) when you want energy - but then the complement is only allowed as the accent. This alone replaces the CornflowerBlue lottery. No pure black, no pure white. #000000 on #FFFFFF looks harsh and cheap. The skill uses off-blacks and off-whites and tints the neutrals a few percent towards the primary hue, which makes the whole app feel like it belongs together. Dark mode is not just inverted. Saturated colours vibrate on dark backgrounds, so the dark theme variants get their lightness raised and their saturation lowered. I never knew this before a designer told me, and my dark themes never made sense before either. Contrast is not negotiable. Every text and background pair has to pass WCAG AA, so 4.5:1 for body text. The skill computes the ratios and adjusts the lightness until they pass. This is the kind of tedious checking that models are surprisingly happy to do and humans (well, me) surprisingly happy to skip. The output of this step is a palette table with contrast ratios and a ready to use Resources/Styles/Colors.xaml. All colours end up as resources, and the styles reference them via AppThemeBinding - no literal hex values sprinkled over the pages. Icons from a font The second half of the skill covers icons. I have been a fan of icon fonts since the Xamarin days - one file, every size, tintable with a simple colour property. Back then I used Font Awesome, these days the skill sets up FluentUI System Icons, which fits the platform look rather nicely. Registering the font is the usual one liner in MauiProgram.cs: fonts.AddFont("FluentSystemIcons-Regular.ttf", "FluentIcons"); And the glyphs go into a static class, so no magic strings in the XAML: public static class IconFont { public const string Home = "\uE9A9"; // code from FluentSystemIcons-Regular.json public const string Settings = "\uEB44"; // never let the model guess these } One rule in the skill turned out to be essential: never guess glyph code points. Ask a model for the unicode value of the settings icon and it will confidently make one up - and you end up with a beautiful row of □ □ □ in your tab bar. Ask me how I know. The skill instructs the model to look every code up in the FluentSystemIcons-Regular.json mapping file that ships next to the font, and to leave a TODO if it cannot verify one. The skill also carries a few small opinions: tab bar icons via FontImageSource at size 24, inline icons at 20, and pick either the regular or the filled variant - mixing both on one screen looks broken. Why Haiku? Now to that model: haiku line in the frontmatter. In Claude Code, a skill can pin the model it runs on. And this is the part I find genuinely neat: because the skill already contains all the thinking - the harmony rules, the contrast thresholds, the checklist - there is nothing left that needs a big model to reason about. Executing a well written skill is mostly following instructions, and that is exactly what the small, fast and cheap models are good at. So the initial design of an app now runs on Haiku, comes back in seconds, and costs next to nothing. If you use the skill through Copilot the model field is simply ignored, since it is not part of the shared standard (yet?) - Copilot just runs it on whatever model you have selected. Taking it for a spin With the skill in place, starting a new app looks like this: I type /maui-app-design in Claude Code (or ask Copilot in agent mode for an initial design), answer four short questions - what does the app do, two mood words, brand colour if any, light/dark or both - and get back a palette table with contrast ratios, Colors.xaml, Styles.xaml, the registered icon font and scaffolded pages that already look like someone with opinions made them. The sample I used to shake all this out is a small workout app - a start page with the usual AMRAP/EMOM/TABATA suspects - which picks up where my last MAUI UI July post left off. That one was built with MAUI Reactor, the code-first C# flavour of MAUI, and so is this one. Reactor doesn’t write its pages in XAML, but it happily consumes XAML resource dictionaries - so the Colors.xaml and Styles.xaml the skill spits out drop straight in and the C# pages just reference them. The skill stays framework-agnostic, and I get to keep writing my UI in C#. I gave it the mood words energetic and friendly, so it went for a warm orange with a complementary teal accent: Is it going to put designers out of work? Absolutely not - a good designer would find plenty to improve here. But it reliably lifts my apps from developer art to something I am not embarrassed to show in a demo. And because the rules are written down in a file that lives in the repo, every team member (and every AI assistant) applies the same design language. You can find the complete skill and the sample app on GitHub - feel free to steal it and swap in your own opinions. The developer-art version lives on the before branch if you want to see the full contrast. And be sure to check out the other MAUI UI July posts for more .NET MAUI eye candy. HTH]]></summary></entry><entry><title type="html">Developing your .NET app using virtual serial ports on macOS</title><link href="https://mallibone.com/post/dotnet-on-macos" rel="alternate" type="text/html" title="Developing your .NET app using virtual serial ports on macOS" /><published>2025-04-04T00:03:00+00:00</published><updated>2025-04-04T00:03:00+00:00</updated><id>https://mallibone.com/post/dotnet-on-macos</id><author><name>Mark Allibone</name></author><category term=".NET" /><category term="macOS" /><category term="socat" /><category term="dev life" /><category term="IoT" /><summary type="html"><![CDATA[It’s the year 2025, and I’m writing this blog post about serial ports—also known as RS232. While this might feel like a blast from the past for many, if you’re working in IoT or IIoT, you might find yourself writing a .NET app that communicates over a serial port. Using virtual serial ports allows you to test your app without needing any hardware connected to your development machine. So let’s go over the steps you need to take and a simple setup to verify that everything works as expected. Since we’re on a Mac (I’m using a MacBook Pro with an M2 Max), the first step is to install socat via the command line. Open your terminal and enter the following command: brew install socat This installs socat—a very handy tool for creating virtual serial (RS232) ports. In fact, socat does much more: it can relay all kinds of communication protocols such as TCP, UDP, and Unix sockets. So if you’re looking for a powerful routing tool on a Unix-like system, this might be your tool of choice. But back to our serial ports. With the following command, we’ll set up two virtual serial ports, where the TX of the first port is “wired” to the RX of the second one - and vice versa: socat -d -d pty,raw,echo=0 pty,raw,echo=0 After running this command, you should see something along the lines of: 2025/04/03 14:08:15 socat[68927] N PTY is /dev/ttys000 2025/04/03 14:08:15 socat[68927] N PTY is /dev/ttys001 2025/04/03 14:08:15 socat[68927] N starting data transfer loop with FDs [5,5] and [7,7] On my machine, the serial sockets were created under /dev/ttys000 and /dev/ttys001—but this may differ on your system, so make sure to check your terminal output rather than relying on the example above. With the sockets and wiring in place, let’s write a basic .NET app to verify that everything works. .NET doesn’t include built-in support for RS232, so you’ll need to install the System.IO.Ports NuGet package. dotnet add package System.IO.Ports Now, let’s take a look at a simple echo console app: using System.IO.Ports; var portName = "/dev/ttys001"; // change as needed using var serialPort = new SerialPort(portName, 9600); serialPort.Open(); Console.WriteLine($"Echo server on {portName}"); serialPort.DataReceived += (s, e) =&gt; { var sp = (SerialPort)s; string incoming = sp.ReadExisting(); Console.WriteLine("Received: " + incoming); sp.Write(incoming); // Echo back Console.WriteLine("Echo"); }; Console.ReadLine(); Note the portName and the baud rate (set to 9600)—these might be different for your setup. Other than that, this service will simply echo back whatever you send to it. If you like, you could extend it to simulate responses for specific commands, giving you a basic simulation environment for a device. But I’ll leave that up to you, in case you’re ready to go down that rabbit hole. 😉 We can now start the echo service, and it should simply connect and wait to receive data. For demonstration purposes, let’s write a small app that lets us enter messages to be sent to ttys000 - and we should receive them back immediately. using System.IO.Ports; using System.Text; Console.WriteLine("RS232 Terminal Application"); // Setup variables var serialPort = new SerialPort("/dev/ttys000", 9600); var running = true; try { // Open port and handle exit serialPort.Open(); // .. // Start read task _ = Task.Run(() =&gt; { var buffer = new byte[1024]; while (running) { // ... var bytesRead = serialPort.Read(buffer, 0, Math.Min(buffer.Length, serialPort.BytesToRead)); Console.Write($"Received: {Encoding.ASCII.GetString(buffer, 0, bytesRead)}"); // ... } }); // Main send loop Console.WriteLine("Type messages (Ctrl+C to exit):"); while (running) { var message = Console.ReadLine(); if (!string.IsNullOrEmpty(message) &amp;&amp; running) { try { serialPort.WriteLine(message); } catch (Exception ex) { Console.WriteLine($"Send error: {ex.Message}"); } } } } // ... With this app, you can now send messages and see them echoed back—just like that, you’ve got an easy way to develop and test COM port functionality without needing to attach any physical devices. To clean up the virtual ports created by socat, simply stop the process using Ctrl+C. The virtual ports will then be removed automatically. You can find the complete sample application from this blog post on GitHub. HTH]]></summary></entry><entry><title type="html">Getting fit with MAUI Reactor</title><link href="https://mallibone.com/post/maui-reactor-fit" rel="alternate" type="text/html" title="Getting fit with MAUI Reactor" /><published>2024-07-09T00:03:00+00:00</published><updated>2024-07-09T00:03:00+00:00</updated><id>https://mallibone.com/post/maui-reactor-fit</id><author><name>Mark Allibone</name></author><category term=".NET" /><category term=".NET MAUI" /><category term="MAUI Reactor" /><summary type="html"><![CDATA[MAUI Reactor is a code first, MVU style framework based on .NET MAUI that promises less ceremony when writing your apps. And I like the sound of that and since this post will be part of MAUI UI July I wanted to tackle a long hedged goal of mine which is to write a app that will keep me motivated during my home workouts. This app has been heavily inspired by the WODTimer app. If you are looking for a sound and complete app (for free) be sure to check it out. I have been working out at home for quite some time now. And while I have started to join again in some group training, I often find myself in my home/hotel/outdoor gym by myself. Finding motivation to workout can be tough, but time boxing the training is a great helper on my end. There are multiple forms on workout timers out there and I want to add a few of them into my app. These are for example: EMOM AMRAP For Time TABATA I will dive in later what these modes mean but for now, I want to have a general overview which will allow me to select the workout mode. Once on the page I will want to configure the workout timer. Depending on the option chosen I will need to set different options. Then I want to start the workout. In this first post we will focus on the start page and the AMRAP workout since it is the simplest and will let me show how you can work with setting up the pages, navigation (incl. parameters) and share styling accross pages. Setting things up MAUI Reactor builds on top of .NET MAUI, so first we will want to have .NET MAUI installed and properly configured. Next you can install MAUI Reactor, don’t forget to also install the tooling that enables hot reload, since it will greatly improve your productivity, when developing an app. As for the editor - you can choose whatever you want. I have been using it with Visual Studio on Windows; Rider and VS Code on macOS. I decided to use the Shell for navigation - especially since MAUI Reactor comes with a nice template. dotnet new maui-reactor-appshell -n AlohaFit The start page With everything setup, let’s have a look at the main page. The main thing I want to do is have a list of buttons - this works well for the limited amount of workout types in the app. The general setup of the page looks as follows: using AlohaFit.Types; using MauiReactor; namespace AlohaFit.Pages; class MainPage : Component { private async void NavigateToAmrap() =&gt; await Microsoft.Maui.Controls.Shell.Current.GoToAsync&lt;WorkoutParameters&gt;(NavigationRoutes.WorkoutPage, param =&gt; param.SelectedWorkoutMode = WorkoutModes.AMRAP); // Other workout navigation methods public override VisualNode Render() =&gt; ContentPage( ScrollView( VStack( Label("TIMER").Style("Headline").HCenter().Margin(0,0,0,32), Components.PrimaryButton("AMRAP", Colors.DarkOrange, NavigateToAmrap), // Other workout timer buttons ) .VCenter() .Spacing(25) .Padding(30, 0) ) ); } Since the buttons all look the same we can easily extract a component that will give us the functionality and consistent style. using System; using MauiReactor; namespace AlohaFit.Pages; public static class Components { public static Button PrimaryButton(string title, Color color, Action handleOnClicked) =&gt; new Button(title) .BackgroundColor(color) .CornerRadius(24) .HeightRequest(50) .WidthRequest(200) .OnClicked(handleOnClicked) .HCenter(); } Extracting components and reusing them is nothing new. It can be done in .NET MAUI, but using C# makes this approach a lot more terse. But let’s have a look at our options when it comes to creating reusable views and styling in general. Working with styles Especially since the properties we have are simply function parameters for smaller things. If we wanted to create an entire View - we can do that too, and we can then even provide properties that we can reuse - you can read more about that in the MAUI Reactor docs. When working on apps it is common to extract styles. When working with MAUI Reactor we can do exactly that too. If we look under Resources/Styles, we will find xaml files for the colour and the styles for our components. We can change the styles and the components used in MAUI Reactor will actually use those styles. Which is super neat, because this means you can reuse any existing mobile styles you might already use. But let’s get back to our workout timer. The workout timer Generally speaking all workout timers require configuring the timer. Once configured the user can start the timer. When running the timer will complete or the user can abort. For now I will focus on the AMRAP timer, but in the near future I will want to add the other timers. Since they all seem very similar in function I will try to create a workout page which will render itself based on the mode and the features each timer will provide. So when choosing a timer I will want to open the workout page and pass in as a parameter which workout has been chosen. The navigation Since we are using the Shell we can configure the navigation as follows: class AppShell : Component { protected override void OnMounted() { MauiControls.Application.Current.UserAppTheme = AppTheme.Dark; Routing.RegisterRoute&lt;WorkoutPage&gt;(nameof(WorkoutPage)); base.OnMounted(); } public override VisualNode Render() =&gt; Shell(ShellContent().Title("").RenderContent(() =&gt; new MainPage())); } And invoke the navigation by using the Shell in the MainPage. await Microsoft.Maui.Controls.Shell.Current.GoToAsync&lt;WorkoutParameters&gt;(NavigationRoutes.WorkoutPage, param =&gt; param.SelectedWorkoutMode = WorkoutModes.AMRAP); Note we are also passing a navigation parameter. MAUI Reactor uses a class as navigation parameter. I kind of like this, because it extends the Shell navigation but also provides type safety. The workout page defines the parameter as additional generic argument WorkoutParameters - which is the name of the class: class WorkoutPage : Component&lt;WorkoutState, WorkoutParameters&gt; { // ... } And we can access the parameters via the Props attribute in a Component in the code: Props.SelectedWorkoutMode I really like this approach. It gives me the typesafety of a language like C#, but also requires very little fuss when doing so. Be sure to check the MAUI Reactor docs to get the full picture of the Shell navigation in MAUI Reactor. Dealing with state With the navigation and the parameters out of the way. Let’s implement the timer. For this we will need some state. The standard MAUI app usually relies on the MVVM pattern for dealing with seperation of View ViewModel and Models. But in MAUI Reactor we use a Model View Update (MVU) based approach. The state of the page is stored in a seperate class (the Model of MVU). But before we dive into that I feel like owing you an explanation of what AMRAP stands for… AMRAP (As Many Rounds/Reps As Possible): If you have a given workout for example 5 Pullups, 10 Pushups and 20 Air Squats and your AMRAP is 15 minutes long. You will try to do as many rounds as possible. You should generally try to be able to keep on working out without any larger breaks. So your pace will become a factor once you have built the muscular endurance. With that out of the way - let’s look at what information we need when setting our timer: public class AmrapState { public DateTimeOffset StartTime { get; set; } public TimeSpan Duration { get; set; } public TimeSpan RoundDuration { get; set; } public int Rounds { get; set; } public bool IsRunning { get; set; } public IReadOnlyCollection&lt;DurationOption&gt; DurationOptions { get; set; } public int SelectedDurationIndex { get; set; } } The wiring of the view to the state is done with like with the navigation parameters via a generic attribute. Starting the workout With all this in place - we are left with starting the workout. So once the user hits the button the timer starts: private void StartWorkout() { SetState(s =&gt; s.IsRunning = true); SetState(s =&gt; s.StartTime = DateTimeOffset.Now); // ... _timer.Start(); } The wiring up is very straight forward. You can also see that choosing between the configuration or running sub-view is done by observing a boolean in the state. Further note that we do not have to implement any events, raise property changed or the like. It’s simply calling SetState and assigning the property. So less hassle, same effect.. I would call that neat. 😃 And that is my basic AMRAP timer using MAUI Reactor. Looking forward I hope this post could give you an overview of what Reactor MAUI is and how it extends .NET MAUI to provide a different approach in writing native apps. In future posts I will expand this app to provide different workout modes, expand the design. So stay tuned for future updates. Also be sure to check out Reactor MAUIs GitHub page and the docs. You can find the app to this post on here. HTH]]></summary></entry><entry><title type="html">Autostarting your (ASP.NET Core) websites on IIS</title><link href="https://mallibone.com/post/iis-autostart-website" rel="alternate" type="text/html" title="Autostarting your (ASP.NET Core) websites on IIS" /><published>2024-02-26T00:03:00+00:00</published><updated>2024-02-26T00:03:00+00:00</updated><id>https://mallibone.com/post/iis-autostart-website</id><author><name>Mark Allibone</name></author><category term="IIS" /><category term="ASP.NET Core" /><summary type="html"><![CDATA[You might have an ASP.NET Core app that you deploy to IIS (Internet Information Server) that, for example, listens in the background to incoming IoT Signals from a Message Queue, but it only does so after you have visited the server in the browser. Having an app that should do background work but does not start automatically can be cumbersome at best and lead to outages, aka angry support calls, at worst. Let’s look at how we can ensure an app is started automatically on startup. How can we change this to make your app appear automatically after deployment? You might ask yourself: “Okay, let’s see how we can configure a site to come up automatically. But why do I even have to do this in the first place?” - The answer is quite simple. IIS does this to save resources. Reducing resource usage allows hosting a plethora of sites on your server, and IIS will ensure that sites that are not in use will spin down, aka only get started once they are in need. While this is great if you have many small sites to run that only run once in a while, it’s a pain in the neck if you have a site that should always be up and running since it does more things than respond to HTTP requests. IIS Setup Usually, hosting your site on IIS will run on a Windows Server once you hit production. IIS is installed using the Server Manager and added under Roles and Features. The Application Initialization role service is not added by default when enabling IIS. Be sure it is enabled for auto-start to work. IIS app configuration Microsoft docs already cover deploying your app to IIS on a Windows server. After that, change the Start Mode to Always Running under Advanced Settings of the AppPool. Then go to the website and set Always Running to true in the Advanced Settings. Your app will automatically start running once the app pool starts up. Note that you will only want this for some apps you are developing. But whenever you have an app that also serves Requests over a different interface than HTTP - for instance, RabbitMQ, MQTT, SQL, file-based, etc., you will either have to remember always to visit the page or else it will not respond as you might intend. HTH]]></summary></entry><entry><title type="html">Accessing a private Azure DevOps NuGet feed when using Docker build containers</title><link href="https://mallibone.com/post/docker-build-private-nuget-feed" rel="alternate" type="text/html" title="Accessing a private Azure DevOps NuGet feed when using Docker build containers" /><published>2023-12-31T00:03:00+00:00</published><updated>2023-12-31T00:03:00+00:00</updated><id>https://mallibone.com/post/docker-build-private-nuget-feed</id><author><name>Mark Allibone</name></author><category term="Docker" /><category term="DevOps" /><category term="NuGet" /><category term="Azure DevOps" /><summary type="html"><![CDATA[Using Docker for your .NET builds gives you a reproducible way to execute your builds on your build server and developers’ devices. Setting up Docker to build your .NET applications is a straightforward process, but what if you use a private Azure DevOps NuGet feed that has to be accessed to build your app? Well, let’s find out. 🤓 In this example, Docker is used to build an ASP.NET Core application. Let’s start by looking at a Dockerfile defining the image we want to create for our build. Microsoft offers a few of the images we can use for this task. At the time of writing, .NET 8 was the latest image, so we will be using this for our app: FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build COPY src/. /source WORKDIR /source/Hello.AspNet RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \ dotnet publish --use-current-runtime --self-contained false -o /app # Rest of the file using the build output FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final WORKDIR /app # Copy everything needed to run the app from the "build" stage. COPY --from=build /app . # ... run app One advantage of using Docker to build your app is that it will create a reproducible build environment. We can ensure that even in a few years when .NET 8 is no longer the current standard, we can use the same image used during the app’s creation. It is not uncommon to extract standard functionality into a library. Many companies use private feeds to post libs for reuse in other closed-source applications. Azure DevOps provides a way to host said libraries. A private feed for NuGet, NPM, et al. can be created and used by publishing to Artefacts. But how can we enable our Docker to build a container to access that feed? First, we will have to inform NuGet what the package sources are. For this, we can define a nuget.config file: &lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;configuration&gt; &lt;packageSources&gt; &lt;!--To inherit the global NuGet package sources remove the &lt;clear/&gt; line below --&gt; &lt;!-- &lt;clear /&gt; --&gt; &lt;add key="Private Feed" value="https://pkgs.dev.azure.com/my-org/_packaging/my-private-feed/nuget/v3/index.json" /&gt; &lt;add key="nuget.org" value="https://api.nuget.org/v3/index.json" /&gt; &lt;/packageSources&gt; &lt;/configuration&gt; You can place the nuget.conifg at the root of the project. Alternatively, copy the file before the build starts: FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build COPY src/. /source COPY nuget.config /source/Hello.AspNet # ... unchanged from the docker file above The dotnet build will know where to look but will need authorization access. We can use a Personal Access Token, or PAT for short. The PAT will require read access enabled under the Packaging section. Creating a PAT is a simple process, and I will point you to the official docs to ensure that a future redesign will not be the source of any confusion. 😉 If you create a PAT, you can reuse it. However, since the generated code is only shown right after generating it, storing it in your password manager of choice is a good idea if you want to reuse the PAT for other build tasks. With the PATat hand, we now have different options for using it. The most straightforward way is to put it into the NuGet.config file: &lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;configuration&gt; &lt;packageSources&gt; &lt;!-- The package sources --&gt; &lt;/packageSources&gt; &lt;packageSourceCredentials&gt; &lt;keyName&gt; &lt;add key="Username" value="Will-be-ignored" /&gt; &lt;add key="ClearTextPassword" value="THE-PAT-GENERATED-BY-AZURE-DEVOPS" /&gt; &lt;/keyName&gt; &lt;/packageSourceCredentials&gt; &lt;/configuration&gt; While adding the PAT credentials directly to the file will fix the authorization issue. It will also mean that your PAT to source control. It could be better, but what are the alternatives? One way would be to set the PAT as an environment variable. So we leave the nuget.config as follows: &lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;configuration&gt; &lt;packageSources&gt; &lt;!--To inherit the global NuGet package sources remove the &lt;clear/&gt; line below --&gt; &lt;!-- &lt;clear /&gt; --&gt; &lt;add key="Private Feed" value="https://pkgs.dev.azure.com/my-org/_packaging/my-private-feed/nuget/v3/index.json" /&gt; &lt;add key="nuget.org" value="https://api.nuget.org/v3/index.json" /&gt; &lt;/packageSources&gt; &lt;packageSourceCredentials&gt; &lt;keyName&gt; &lt;add key="Username" value="Will-be-ignored" /&gt; &lt;add key="ClearTextPassword" value="%THE_PAT%" /&gt; &lt;/keyName&gt; &lt;/packageSourceCredentials&gt; &lt;/configuration&gt; And set the environment variable using Docker as follows: FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build COPY src/. /source WORKDIR /source/Hello.AspNet ARG FEED_PAT # ... build and run app If you are triggering the Docker container build via the Azure DevOps build pipeline, you can set variables or give your build run access via the NuGetAuthenticate build step. This will allow your build to access the private NuGet feed without the PAT showing up in any of your files, i.e. git repo. HTH]]></summary></entry><entry><title type="html">Setting up Tailscale on your QNAP NAS</title><link href="https://mallibone.com/post/tailscale-qnap" rel="alternate" type="text/html" title="Setting up Tailscale on your QNAP NAS" /><published>2023-10-31T12:03:00+00:00</published><updated>2023-10-31T12:03:00+00:00</updated><id>https://mallibone.com/post/tailscale-qnap</id><author><name>Mark Allibone</name></author><category term="VPN" /><category term="Home Automation" /><summary type="html"><![CDATA[By discovering Tailscale, I finally found a solution for my QNAP NAS as a VPN gateway into my home network. I have a couple of devices at home that do not talk to some cloud service but provide services when accessed through my home network. Whenever I am not at home, accessing said services is no longer possible. Let’s examine how Tailscale enables accessing devices on the same network as your QNAP NAS. Tailscale is a VPN (Virtual Private Network) service that enables devices to communicate securely with each other. You must set up an account by following the steps described here. You can install a client on your phone, tablet and/or laptop. Next, be sure to install Tailscale on your QNAP NAS. You can choose your NAS as your exit node. This is nice if you are abroad and want to access websites/movie services only available in your home country. Open the app on your QNAP and select advertise as exit node to enable this feature. But there is more. What if you want to access devices only available on your network at home. For this, you have to enable the local bridge mode. This will require you to access your QNAP system via SSH. You will have to ensure that SSH is enabled on your QNAP NAS. Next, an SSH client is required. If you are on macOS, use Terminal. On Windows, you can use the Windows Terminal to connect to your NAS: ssh your-nas-user@your-nas-ip-or-dns After successfully logging in, depending on your QNAP NAS, you must locate where your Tailscale app has been installed. You should find it in either one of these two locations: /share/CACHEDEV1_DATA/.qpkg/ /share/MD0_DATA/.qpkg/ You can check if the NAS exists using the ls THE-PATH command. If a list of directories is returned, you have found the right one. Next, invoke the following command - in my case, it was the second option, adjust to your NAS accordingly: /share/MD0_DATA/.qpkg/tailscale/tailscale -socket /tmp/tailscale/tailscaled.sock up --advertise-routes=\192.168.1.0/24 --advertise-exit-node --reset Note that you might have to change the IP according to the IP of your local network. The /24 is for the Subnet and says how many Bits are used to identify the network, e.g. shorthand for 255.255.255.0. Once this command runs through, open up the Tailscale app. You will be able to enable your NAS as a logical bridge. Once enabled and connected to your Tailscale VPN, you can see all your local network devices. HTH]]></summary></entry><entry><title type="html">Writing UI tests for .NET MAUI - POC</title><link href="https://mallibone.com/post/uitest-maui-101" rel="alternate" type="text/html" title="Writing UI tests for .NET MAUI - POC" /><published>2023-07-26T00:03:00+00:00</published><updated>2023-07-26T00:03:00+00:00</updated><id>https://mallibone.com/post/uitest-maui-101</id><author><name>Mark Allibone</name></author><category term=".NET MAUI" /><category term="UI Test" /><summary type="html"><![CDATA[This blog post is part of the .NET MAUI UI July - be sure to check out the other blog posts. 🙂 Automated UI tests allow you to write tests that execute your app as a user would. This can be a significant selling point compared to a unit, integration, and other non-UI automated tests. Non-technical users are no longer left with a chart of how many passed, failed, skipped, etc. - they actually get to see what the tests performed. At first glance, automated UI tests seem like the ultimate automated tests, but more later. If you have written Xamarin apps in the past, you might have heard about Xamarin UI Tests. The testing framework based on Calabash and NUnit allows you to write automated UI tests. Sadly for users of said framework, there never has been a .NET MAUI UI Test successor - or has there? This post will examine how to use the Xamarin UI Test framework to write .NET MAUI UI tests. Foreword Before we start, I want to be honest with you. A lot of the following steps may seem odd. While you will see how to write UI tests using the Xamarin UI Test framework, a few parts remain left to be migrated to certain foundations that .NET MAUI is built, such as using .NET. So if a few of the following details feel “hacky” - perhaps one could even say “yucky”. I would concur but also add that only if there is actual usage of a product will any further development be put behind it. So, in the hopes of UI tests living onwards, let’s look at how we can cobble together - erh, write UI tests for .NET MAUI. Writing Tests UI tests are currently supported for Android, iOS and MacCatalyst. Given the “Create new .NET MAUI Project” starting point, we first want to add the test project using your favourite .NET IDE. Therefore, we add a .NET Framework 4.8 library project. That is not a typo; we add a .Net Framework 4.8 library project. Next, open the newly added csproj and change it to the following lines: &lt;Project Sdk="Microsoft.NET.Sdk"&gt; &lt;PropertyGroup&gt; &lt;TargetFramework&gt;net481&lt;/TargetFramework&gt; &lt;IsPackable&gt;false&lt;/IsPackable&gt; &lt;IsTestProject&gt;true&lt;/IsTestProject&gt; &lt;/PropertyGroup&gt; &lt;ItemGroup&gt; &lt;PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /&gt; &lt;PackageReference Include="NUnit" Version="3.13.3" /&gt; &lt;PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /&gt; &lt;PackageReference Include="NUnit.Analyzers" Version="3.6.1" /&gt; &lt;PackageReference Include="coverlet.collector" Version="3.2.0" /&gt; &lt;PackageReference Include="SharpZipLib" Version="1.4.2" /&gt; &lt;PackageReference Include="Xamarin.UITest" Version="4.2.0" /&gt; &lt;/ItemGroup&gt; &lt;/Project&gt; With all this in place, we are ready to write our first UI test for Android. We will get back to the Apple platforms in a minute. UI Tests require more setup. We generally have to pass in the platform, which allows the test framework to translate your test code into actual UI actions. Since you will do this setup for every test class, it is often moved to a base class: using System; using NUnit.Framework; using Xamarin.UITest; namespace MauiTesting101.UiTests { [TestFixture(Platform.Android)] [TestFixture(Platform.iOS)] public class BaseTest { IApp _app; protected readonly Platform Platform; protected BaseTest(Platform platform) =&gt; Platform = platform; protected IApp App =&gt; _app ?? throw new NullReferenceException(); [SetUp] public virtual void BeforeEachTest() { _app = AppInitializer.StartApp(Platform); App.Screenshot("App Initialized"); } } } We can write our first test with the UI test setup out of the way. Since we are doing this for the .NET MAUI “Hello World” app, let’s check if the button changes its text accordingly after it gets selected: [Test] public void WhenCounterClicked_IncrementCount() { // Arrange: Wait until counter Button is present const string countButton = "CounterBtn"; App.WaitForElement(countButton); // Act: Tap the button App.Tap(countButton); // Assert: check label updated var buttonLabelValue = "Clicked 1 time"; Assert.DoesNotThrow(() =&gt; App.WaitForElement(x =&gt; x.Marked(buttonLabelValue)), "Button was not clicked"); // Take a screenshot App.Screenshot("Tapped 1 time"); } Now, let’s look at some of the things going on. The arrange, act, assert style of writing the test is optional, but I like the structure it gives the tests. One thing that you will quickly notice when writing UI tests is that everything takes more time. The test code must wait for the app to start before we tap the button. And this is what is happening on this line: [Test] public void WhenCounterClicked_IncrementCount() { // Arrange: Wait until counter Button is present ... App.WaitForElement(countButton); // Act ... } This line will either timeout (UI element not found) or complete without exception. The second one means the UI element is present. Once that is the case, we invoke the actual tapping of the button: [Test] public void WhenCounterClicked_IncrementCount() { // Arrange: Wait until counter Button is present ... // Act: Tap the button App.Tap(countButton); // Assert: check label updated ... } Then we wait again for the UI to update and verify the value has changed according to our expectations. Even with this relatively simple test, you notice a crucial point when writing UI tests. Before you invoke an action, you ensure that the expected UI is loaded by waiting for a UI element. You may have noticed that we invoke the button using an identifier string CounterBtn. This tag is set on in the XAML by setting the AutomationId property: &lt;?xml version="1.0" encoding="utf-8" ?&gt; &lt;ContentPage ...&gt; &lt;ScrollView&gt; &lt;VerticalStackLayout ...&gt; ... &lt;Button x:Name="CounterBtn" AutomationId="CounterBtn" ... /&gt; &lt;/VerticalStackLayout&gt; &lt;/ScrollView&gt; &lt;/ContentPage&gt; The AutomationId property is not mandatory but significantly reduces the time to write a UI test. It also significantly improves the quality of your tests since you will not have to rewrite code if, for example, the UI hierarchy changes. But it does require that during development (or when writing the UI test), the AutomationId Property is set. If we execute the test, we will see the following: Next up is iOS. For iOS, we must install the Xamarin.TestCloud.Agent NuGet package. This is required to invoke the UI commands. Since .NET MAUI uses a single project file, we must ensure that this NuGet package is only added when compiling against iOS. The same applies to macCatalyst. When adding the NuGet to the solution, this is automagically done for us: &lt;Project&gt; ... &lt;ItemGroup Condition="'$(TargetFramework)' == 'net7.0-ios'"&gt; &lt;PackageReference Include="Xamarin.TestCloud.Agent" Version="0.23.2" /&gt; &lt;/ItemGroup&gt; &lt;ItemGroup Condition="'$(TargetFramework)' == 'net7.0-maccatalyst'"&gt; &lt;PackageReference Include="Xamarin.TestCloud.Agent" Version="0.23.2" /&gt; &lt;/ItemGroup&gt; &lt;/Project&gt; I had some issues with this approach, so I changed the lines to the following: &lt;ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'"&gt; &lt;PackageReference ... /&gt; &lt;/ItemGroup&gt; Generally speaking, this ensures the NuGet package is only added when compiling against iOS or macCatalayst. We could further check that the package only gets used for Debug builds. This would ensure we don’t submit our app with this NuGet package to the store. It has been heard that the package can lead to rejection during the store evaluation process. But then again, sometimes you want to test against the release build, which could require dedicated build configurations for UI tests. And down the rabbit hole we go. But if you are in the situation, this would be an option. After installing the NuGet package, we must enable the testing agent by adding Xamarin.Calabash.Start() to the AppDelegate.cs file: using Foundation; namespace MauiTesting101; [Register("AppDelegate")] public class AppDelegate : MauiUIApplicationDelegate { protected override MauiApp CreateMauiApp() { #if ENABLE_TEST_CLOUD Xamarin.Calabash.Start(); #endif return MauiProgram.CreateMauiApp(); } } You can define the constant as follows in your .NET MAUI app project file: &lt;Project Sdk="Microsoft.NET.Sdk"&gt; &lt;PropertyGroup&gt; ... &lt;PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "&gt; &lt;DefineConstants Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios' OR $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst' "&gt;TRACE;DEBUG;NET;NET7_0;NETCOREAPP;ENABLE_TEST_CLOUD&lt;/DefineConstants&gt; &lt;DefineConstants Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'ios' AND $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'maccatalyst' "&gt;TRACE;DEBUG;NET;NET7_0;NETCOREAPP&lt;/DefineConstants&gt; ... &lt;/PropertyGroup&gt; ... &lt;/Project&gt; I had again some issues setting this up. Due to the single project file structure, I added two definitions depending on the platform. There might be a nighter way; let me know what I’m missing out on. 😆 With all this in place, we can now execute the same test for iOS. When should you write UI Tests? As I said in the beginning, UI tests are great at first glance. The end user sees what is being tested, and you write tests very closely to how a real user would interact with the application. However, there are some caveats when writing UI tests, so many do not even try to write them. So, let’s start with the three concerns I hear most: UI tests are slow UI tests are brittle UI tests require high maintenance Performance Right out of the gate, yes, UI tests are slow. If we compare the execution time of writing the above test as a unit test, we compare these two times to each other (run on a MacBook Pro M2Max): UI Test: 9086 ms Unit test: 7 ms There is one significant takeaway from the times you see above. Do not replace your unit tests using UI tests. Also, if all you are checking could be done in a unit test, then go with the unit test. UI tests are not meant to replace a unit test. They are intended to allow you to test the UI layer without having to go over a test script on every release (you do have a test script, right? 😉) One strategy to improve performance on your UI tests, apart from ensuring they add value by testing the UI layer, is to parallelize the execution of your tests. This will reduce the time it takes until you get the feedback from an automated build that includes UI tests. Flaky UI tests Most UI test setups test the entire code base. They are used to test the UI and whatever service is invoked by UI actions. It will execute said code chain from the UI, ViewModel, Service and sometimes even the backend. So, if anything goes wrong, it could be at any stage. A test that appears to be randomly failing or succeeding is terrible since you will lose confidence when it is read that there is a problem you should actually be investigating. So, have different tests to catch errors in separate layers. The UI test can be a great canary, but it often will not be able to pinpoint the source of the issue. Another reason UI tests can fail is that some action takes too long. There can be many reasons, from slow devices, slow network, overloaded test server, etc. - long story short, this can be the most unnerving because everything is correct, and it is really tough to figure out why the app is running so slow. One obvious solution is cranking up the default timeout. Still, it can also be a great starting place to think about improving your app’s performance to ensure the user experience does not take a hit due to a bad performance. UI tests require high maintenance Writing a good UI test takes more thought than your average unit/integration test. There are more moving parts, and anyone could fail. It is now up to you if that failure should indicate the app has failed or if there was some glitch that should be caught by the user/test code. Another reason UI Tests take longer is whenever the UI has some considerable change (redesign). It might require you to rewrite the UI tests. How the UI tests interact with your app also means you must execute the test to determine if it is still working. Is it worth the trouble As you can see, UI tests are no silver bullet. But they provide a unique opportunity to validate that your app is running as it should. I like the idea of an automated smoke test of an app. This test checks if the app starts up as expected. The next series of tests validate that every screen opens without issue in the app. These tests usually take little to write and maintain. The next step would be to test an app’s most common scenarios. This usually eliminates the check every screen test. Writing these tests is more complex and will require additional thought when creating them. Generally speaking, I would strive to test the happy path. The maintenance will be higher and should be considered when choosing which tests to perform using UI tests. Conclusion You can actually write UI tests for your .NET MAUI app. Yes, there are still some rough edges here and there to get it running. But it does run. The nice thing about the UI tests is that they follow the spirit of writing once and then having it run on multiple platforms. You do not have to implement tests for every platform your app runs on. Currently, the tests are limited to iOS, Mac Catatalyst and Android. As stated above, it is to be seen how this goes forward. When used appropriately, UI tests can be a great addition to your other tests and be a real time and money saver. The only alternative to automated UI tests is manual or exploratory field testing, usually a combination of bug reports by your users and (sound) log files to find the error and have an update ready as quickly as possible once a problematic bug has been discovered. Kudos to Gerald for creating the YouTube video that inspired this post. 🙂 HTH]]></summary></entry><entry><title type="html">Using Reactive UI in your .NET MAUI app</title><link href="https://mallibone.com/post/rxui-maui-101" rel="alternate" type="text/html" title="Using Reactive UI in your .NET MAUI app" /><published>2023-04-25T00:03:00+00:00</published><updated>2023-04-25T00:03:00+00:00</updated><id>https://mallibone.com/post/rxui-maui-101</id><author><name>Mark Allibone</name></author><category term="ReactiveUI" /><category term="Rx" /><category term=".NET MAUI" /><summary type="html"><![CDATA[One of my favourite MVVM framework libraries for C# is Reactive UI. But is it ready for prime time when developing a .NET MAUI app? Spoiler: It is. So let’s look at how to get Reactive UI setup in a .NET MAUI app and how the basics are implemented in Reactive UI. We are talking bindings, commands and wiring up the dependency injection. We will rewrite the Hello World .NET MAUI project using Reactive UI in this post. Even add a little timer, aka scheduler sample. I assume you are familiar with the basic MVVM concept. If not, there is an introduction by Microsoft here. Setup After creating a new .NET MAUI project. Add the following NuGet packages: Reactive UI .NET MAUI Reactive UI Fody Now we can start pushing the logic from the View into the ViewModel. Writing a ViewModel When using Reactive UI the View Model inherits from ReactiveObject. To move the basic functionality of the Hello World application, we will implement a property that we will bind to the button text. A counter value to keep track of the count. And a Command property to handle when the button is selected. Let’s start with the properties which afterwards can be used in the View via binding: public class MainViewModel : ReactiveObject, IActivatableViewModel { public MainViewModel() { // Configuration of CounterButtonText and ButtonClickedCommand } [Reactive] public int Count { get; set; } [ObservableAsProperty] public string CounterButtonText { get; } public ReactiveCommand&lt;Unit,Unit&gt; ButtonClickedCommand { get; } // ... } Reactive UI Fody provides the attributes [Reactive] and [ObservableAsProperty]. Which reduces the amount of typing required to define bindable properties. You can read more on the official docs. The Count is a property with a getter and setter and can be used like a normal Property in C#. The CounterButtonText is a bit special. When we increment the counter we want to change the button text. Since our Counter property raises an event everytime it gets changed. Which means we can write a Observable as Property and implement the button text changes: Reactive UI Fody provides the attributes [Reactive] and [ObservableAsProperty]. The attributes reduce the amount of typing required to define bindable properties. More info can be found in the official docs. The Count is a property with a getter and setter and can be used like a typical Property in C#. The CounterButtonText is special. Since it will never be set by the UI. A Read Only Property can be used. When the Count has a new value. The button text is changed. The Counter property raises an event every time it gets changed. This event can be observed by using Observable as Property; the text can be updated: this.WhenAnyValue(vm =&gt; vm.Count) .Select(c =&gt; c switch { 0 =&gt; "Click me", 1 =&gt; "Clicked 1 time", _ =&gt; $"Clicked {c} times" }) .ToPropertyEx(this, vm =&gt; vm.CounterButtonText); Implementing the (synchronous) Command is done with the ReactiveCommand helpers from ReactiveUI: ButtonClickedCommand = ReactiveCommand.Create(ButtonClicked); With all these pieces in place, we are feature complete with the original hello world sample using the MVVM architectural pattern. MVVM is a proven way to write extensible and maintainable front-end code. But using a library like Reactive UI provides additional features. Features are helpful in common front-end scenarios. For instance, having a timer that periodically checks/changes a value. The standard “Hello World” message could use some improvement. Let’s have it display a reactish Hello World message. Let’s start by adding a new property to hold the displayed text: [Reactive] public string Greeting { get; set; } With the property in place - the timer and the logic to change the text every second can be implemented: public class MainViewModel : ReactiveObject, IActivatableViewModel { public MainViewModel() { this.WhenActivated(disposables =&gt; { // ... Observable .Timer( TimeSpan.FromMilliseconds(100), // give the view time to activate TimeSpan.FromMilliseconds(1000), RxApp.MainThreadScheduler) .Do( t =&gt; { var newGreeting = $"Hello, {Traits[t % Traits.Length]} world !"; Console.WriteLine( $"[vm {Thread.CurrentThread.ManagedThreadId}]: " + $"Timer Observable -&gt; " + $"Setting greeting to: \"{newGreeting}\""); Greeting = newGreeting; }, () =&gt; Console.WriteLine( "Those are all the greetings, folks! " + "Feel free to close the window now...\n")) .Subscribe() .DisposeWith(disposables); // ... } [Reactive] public string Greeting { get; set; } // More Properties public ViewModelActivator Activator { get; } = new(); } The timer is placed in the WhenActivated extension method. This helper allows starting and disposing of Observables. The default usage is bound to the lifecycle of the View. When the View is being rendered WhenActivated is invoked. When the user navigates away from the View, all the items are disposed of by a CompositeDisposable. As per documentation, the ViewModel inherits from IActivatableViewModel. The View invokes the method by inheriting ReactiveContentPage&lt;T&gt;. For more details, be sure to read the docs. Dependency Injection With all the functionality in place, there is one last thing left to do. Wiring up the ViewModel to the View. While it is possible to do this all by hand, aka BindingContext = new FunkyViewModel(), this can get rather tedious. With .NET MAUI, there is a standard way to register and resolve dependencies automagically. In the MauiProgram.cs file, use the builder to register all our classes, including Views and ViewModels: public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); // Maui registration stuff builder.Services.AddTransient&lt;MainPage&gt;(); builder.Services.AddScoped&lt;MainViewModel&gt;(); // ... return builder.Build(); } } With this in place, we can change the constructor of our View to get the ViewModel injected: public partial class MainPage : ReactiveContentPage&lt;MainViewModel&gt; { public MainPage(MainViewModel viewModel) { ViewModel = viewModel; InitializeComponent(); this.WhenActivated(_ =&gt; { }); } } Check out the official docs to learn more about how Dependency Injection works in .NET MAUI. ## Conclusion .NET MAUI and Reactive UI allow you to write native applications for desktop and mobile using the well-known and proven patterns of reactive programming using C#. This post looked at many topics concerning integrating Reactive UI into your .NET MAUI application. You can find the complete sample on GitHub using the fundamental functions of Reactive UI, such as Bindings, Commands and timers. HTH]]></summary></entry><entry><title type="html">.NET MAUI Device Runner Unit Tests</title><link href="https://mallibone.com/post/maui-device-unit-testing" rel="alternate" type="text/html" title=".NET MAUI Device Runner Unit Tests" /><published>2023-03-28T00:03:00+00:00</published><updated>2023-03-28T00:03:00+00:00</updated><id>https://mallibone.com/post/maui-device-unit-testing</id><author><name>Mark Allibone</name></author><category term="Automated Testing" /><category term="XUnit" /><category term="Device Runner" /><category term=".NET MAUI" /><summary type="html"><![CDATA[Automated tests can be a great timesaver when developing any kind of application. Some pieces of code can be harder to test automatically since they require a dependency on an external ressource. When writing .NET MAUI apps this often means that a piece of code or library requires to run on a platform to perform it’s function. This is the time when running your code on a device or emulator becomes mandatory. So let’s see how this can be done. Before we start looking at how device running tests are implemented. Let’s quickly discuss when these tests can be of great value. Generally speaking it is often a good idea to write your app in a way that your business logic does not have any dependency on any external parts. In C# this is usually done via Dependency Inversion and Dependency Injection. But if you are writing the code that interacts with a platform, the very reason of existence for that code is to be dependent on a given platform SDK. In those cases using device running unit tests will be a great time saver. App setup The sample we will be using does not require any platform specifics, but it will highlight how we have to architect our app and how to setup the test project. As a test project we will use the Hello World .NET MAUI project and add a ViewModel which takes over the code that is implemented in the code behind. The ViewModel will be placed in a class library and the .NET MAUI app will reference said library. The test project will then reference said library. Giving us the following projects: The ViewModel implements the counter logic and uses the Community Toolkit MVVM library: public partial class MainViewModel : ObservableObject { [ObservableProperty] private int _count; [ObservableProperty] private string _text = "Click me"; [RelayCommand] public void CounterClicked() { Count++; if (Count == 1) Text = $"Clicked {Count} time"; else Text = $"Clicked {Count} times"; } } Setting up the Device Runner Testproject With the projects and code to test in place, lets move along and implement the test project. We start out by creating a new .NET MAUI application and deleting the following files App.xaml, App.xaml.cs, AppShell.xaml, AppShell.xaml.cs, MainPage.xaml, MainPage.xaml.cs . Next we will install the Shiny.Xunit.Runners.Maui NuGet package, created by Allan Ritchie. And change the Program.cs to look as follows. public static class MauiProgram { public static MauiApp CreateMauiApp() =&gt; MauiApp .CreateBuilder() .ConfigureTests(new TestOptions() { Assemblies = { typeof(MauiProgram).Assembly } }) .UseVisualRunner() .Build(); } With this our Device Runner Testsetup is complete and we can go ahead and implement a test: public class MainViewModelShould { [Fact] public void IncrementcountOnCounterClicked() { // Arrange var sut = new MainViewModel(); // Act sut.CounterClickedCommand.Execute(null); // Assert Assert.Equal(1, sut.Count); } } With that done all that is left is to execute the device runner project and tap the button to run the tests. Conclusion To run device tests requires some adaptation how a .NET MAUI project needs to be setup. By extracting the business logic we can reference it in our Device Runner MAUI project. The tests are based on the XUnit Testing framework. You might wonder. Should you run all your tests from now on using device runners? Short answer probably not. While running your tests on a device will also check your logic. It will further allow you to see how well code performs on a device. But even though you can get these additional insights, it takes longer to execute. Plus running the tests always involves waiting for the device/emulator to start and then tap a button. Therefore I would recommend to use device runners when testing code that requires running against a device sdk, or to explicitly test if your code is able to run on a specific test device. You can find the complete sample on GitHub. HTH]]></summary></entry><entry><title type="html">Writing Unit Tests for your .NET MAUI app</title><link href="https://mallibone.com/post/maui-unit-testing" rel="alternate" type="text/html" title="Writing Unit Tests for your .NET MAUI app" /><published>2023-03-14T00:03:00+00:00</published><updated>2023-03-14T00:03:00+00:00</updated><id>https://mallibone.com/post/maui-unit-testing</id><author><name>Mark Allibone</name></author><category term="Automated Testing" /><category term="XUnit" /><category term=".NET MAUI" /><summary type="html"><![CDATA[Writing mobile apps usually brings along its fair share of application logic run on the device. When writing code, it is paramount to test it. How else will you know if it is working as expected? The answer is testing. And since we are developers, we like to automate mundane tasks. So Unit Testing our logic can be a great way to test our logic. If you know how to set up unit tests, skip to the TL;DR; marker. 🙃 Unit Tests allow us to test the logic parts of our app. This will enable us to try most of our apps except the views. The most popular testing frameworks to the date of writing this post are XUnit, NUnit and MS Test. All three are solid choices and mostly have the same functionality. They mainly differ in how tests are written. For my Unit Tests, I usually like to go with XUnit. Ensure that the .NET MAUI and test projects use the same .NET version. In this example, it will be .NET 7. Given an existing .NET MAUI application, we can add a new testing project to the solution and choose XUnit as the testing framework. To have some code, we can test, I have rewritten the .NET MAUI hello world app to use a ViewModel (using the Community Toolkit MVVM framework). Doing so gives us the following ViewModel code. public partial class MainViewModel : ObservableObject { [ObservableProperty] public int _count; [ObservableProperty] public string _text = "Click me"; [RelayCommand] public void CounterClicked() { Count++; if (Count == 1) Text = $"Clicked {Count} time"; else Text = $"Clicked {Count} times"; } } We can test this logic by writing a Test that calls the command method in the well-known Arrange/Act/Assert pattern: public class MainViewModelShould { [Fact] public void IncrementcountOnCounterClicked() { // Arrange var sut = new MainViewModel(); // Act sut.CounterClickedCommand.Execute(null); // Assert Assert.Equal(1, sut.Count); } } This will require us to add a project reference from the test project to the .NET MAUI project. &lt;Project Sdk="Microsoft.NET.Sdk"&gt; &lt;!-- ... --&gt; &lt;ItemGroup&gt; &lt;ProjectReference Include="..\MauiTesting101\MauiTesting101.csproj" /&gt; &lt;/ItemGroup&gt; &lt;/Project&gt; TL;DR; Up to this point, we just followed the usual steps for setting up a Unit Test project. If you put your app logic in the .NET MAUI app project, you will get a weird error when compiling the test project. To make the compiler happy, we will have to change the .NET MAUI .csproj file as follows: &lt;OutputType Condition="'$(TargetFramework)' != 'net7.0'"&gt;Exe&lt;/OutputType&gt; Note that we have to add a condition to the &lt;OutputType&gt; or else you will get an error that there is no main method when executing the test. If you are using a different .NET version i.e. .NET 6 you would have to write the condition as follows &lt;OutputType Condition="'$(TargetFramework)' != 'net6.0'"&gt;. And for .NET 8 we would have to replace number with an 8 and so forth. Note that we have to add a condition to the &lt;OutputType&gt;, or else you will get an error that there is no primary method when executing the test. If you are using a different .NET version, i.e. .NET 6, you would have to write the condition as follows &lt;OutputType Condition="'$(TargetFramework)' != 'net6.0'"&gt;. And for .NET 8, we would have to replace the number with an 8. And for .NET 9, we … you get the point. 😉 With these changes, we can start running the tests from your IDE or via the command line using dotnet test . You can find the complete example on GitHub. HTH]]></summary></entry></feed>