Azure & Why You Shouldn’t Use Defaults

A purple-petaled flower growing amongst a field of yellow-petaled flowers.

The cover image for this post is by Dan Meyers

This blog post was written by Jamie.


TL;DR

As Abel Wang was proud of saying

Don’t accept the defaults!

- Abel Wang (RIP)

Click here for the actual solution to the problem I struggled with for three days.

Introduction

If you’ve been following along with our 2022 Apprentice Dev Blogs, you’ll know that Alex (our apprentice) has been working on an application for us. We started by having them build a WinForms app with one layer

i.e. everything was done in the UI

then introduced nTier architecture along with SOLID principles. From there, we built toward a four layer application:

  • UI (WinForms)
  • Business Logic
  • Generic Repository
  • Database

This made complete sense, as we would be able to separate the concerns of the application, program to interfaces, and apply the basics of Domain-Driven Design. And it lent itself to our ultimate goal of swapping WinForms for .NET Maui.

as a quick reminder, Alex started this project before .NET Maui saw a version one release; so they’ve been right on the bleeding edge of the technology throughout their apprenticeship with us

But we ran into some problems along the way. Some of these were problems of my own creation, and some were slight weirdness. In this blog post, I want to talk through some of these issues, because they might happen to you too.

Before we get to that: at the time of writing, Alex’s project is still in progress and they have another two weeks with us.

Just Copy Those Projects

Because we already had a set of projects which worked, when Alex added a non-functional .NET Maui UI (with just two screens) I told them to just copy the projects under the UI over to the .NET Maui solution and re-use them. After all, that was the plan right? Implement DI, bring over the interfaces and implementations, and it will all just work.

Except that it didn’t.

It turns out that there’s a new project type with .NET Maui: A .NET Maui class project. They look a little different from standard csproj files because they need to make your code cross-platform:

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

  <PropertyGroup>
    <TargetFrameworks>net6.0;net6.0-android;net6.0-ios;net6.0-maccatalyst</TargetFrameworks>
    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net6.0-windows10.0.19041.0</TargetFrameworks>
    <!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
    <!-- <TargetFrameworks>$(TargetFrameworks);net6.0-tizen</TargetFrameworks> -->
    <UseMaui>true</UseMaui>
    <SingleProject>true</SingleProject>
    <ImplicitUsings>enable</ImplicitUsings>

    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion>
    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
    <TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
  </PropertyGroup>

</Project>

They also bring a number of different files and directories and files with them; which looks like this:

A screenshot of Visual Studio Code showing the contents of a .NET Maui class project. The Android PlatformClass1cs file loaded into the file viewer, this file allows you to include any Android specific code.
Forgive the censoring here

The PlatformClass1 file is used to include code specifically for that platform. This is a neat idea that gets around littering the code base with #if preprocessors, making the code more readable and easily discoverable.

Without these files and the csproj properties, the solution will still build and run locally (even in the various device emulators); but it will refuse to publish. Which brings me on to…

Not Understanding .NET Maui

It’s safe to say that .NET Maui is a completely new technology. Even though it’s based on Xamarin, there are a tonne of potential pitfalls for someone who has managed to miss the entire XAML revolution (like me). Before this project, I’d heard of the MVVM pattern

my thanks go out to Jim Bennet for explaining it, when I interviewed him in 2019

and I knew the basic idea:

  • Put C# code in
  • Build a XAML-based UI
  • Magic
  • iOS and Android apps come out the other end

One of the things that I hadn’t thought about when designing this project for Alex was that the code bases for .NET Maui (and Xamarin) applications aren’t designed the same way as WinForms ones. Those who have experience with both .NET Maui and Xamarin would have baulked when I said that we would simply swap the UI for .NET Maui, as it simply doesn’t work like that.

Firstly, it’s not a good idea to include direct access to a remote database in an app which might end up on the various app stores. This a dreadful idea for security

my fellow security experts will likely be clawing at the screen when they read that we gave app users direct access to the database. But it’s an MVP, it’s allowed… right?

Also the .NET Maui tool chain does something really rather interesting when you build for iOS: it attempts to compress the binaries.

the following is correct as of July 5th, 2022

When you do a release build for iOS, the tooling requires that the binaries are compressed as part of the publish action. If this compression step fails, the publish will fail. You can disable this by adding a PublishTrimmed=false flag to the publish command. Like so:

dotnet build yourProject.csproj -c Release -f net6.0-ios --no-restore -p:PublishTrimmed=false

But that will immediately fail, because that compression step is required in order for a publish to iOS to complete.

Each time we tried to publish for iOS it would fail, because the tooling couldn’t compress the binaries.

This was for an app with two screens:

  1. A welcome screen
  2. A record display screen

There wasn’t anything flashy happening in the code, and there were no embedded resources. But we were including Entity Framework Core and a few layers of abstraction. So it couldn’t have been Alex’s code which was failing to compress, which meant that it must have been the libraries we were including.

So…

Hiding The Database Behind An API

I wanted to give Alex the option of publishing for a iOS device, regardless of whether they use one or not. After all, what’s the point of building a cross platform application if you can’t publish it for one of those platforms?

There was another problem too:

A screenshot of GitHub's artifact view for an Android build of the code base. It shows that the built app is 163 MB, which is way too big for two screens.
And that’s an app with two screens

We can’t really do a great deal about the size of the application, because it’ll be including a lot of the .NET runtime, our dependencies, and any translation code required in order to run it on the device. But we could make it smaller by reducing our dependencies. And we could do that by putting the database behind a WebApi.

as an update: after doing everything in this blog post, we’ve gotten the app down to ~100MB

Not only would this reduce our dependencies, but it would also make the code more secure because we could then add authentication and authorisation at the API side.


Side note:

We’re a 100% remote company. This meant that Alex was working from a number of different locations during their apprenticeship. Because of the rules in place by the T-Level course, Alex was not allowed to work from home, and was required to work from certified co-working spaces and their educational institution.

Because we had the database on an Azure SQL Server, I had to add new rules to (and remove old rules from) the IP allowlist for the database on a daily basis.

This is a task that takes around 90 seconds (with 80 seconds being spent navigating to the SQL Database firewall settings), but it was toil which would be fixed by putting the database behind an Azure WebApp. This was daily toil, and toil should always be reduced where possible

see this episode of Coding Blocks for a discussion on why reducing toil is a good idea


So I spent the weekend building a WebApi project to hide the database behind. But ran into a HUGE problem when attempting to deploy it:

But what do I mean by “huge issue when deploying”?

This was what I was presented with when attempting to access the WebApp, regardless of what I did:

A screenshot of GitHub's artifact view for an Android build of the code base. It shows that the built app is 163 MB, which is way too big for two screens.
And that’s an app with two screens

For those who don’t know, this is the “you have created a WebApp but not deployed anything to it” screen.

I spent almost the entire weekend doubting myself and wondering why the app wouldn’t start when it was deployed. I’d followed all of the steps in Azure for setting up a GitHub auto deploy, and even used the yml file that it generated for me.

It MUST be the code, right?

- Jamie

So I went back to the code and stripped everything back. I removed all of the documentation, I removed the .NET Boxed stuff that I’d added

pro tip: take a look at the .NET Boxed stuff, it’s great

I removed serilog (I wasn’t getting any logs anyway, because it wasn’t deploying correctly, so I didn’t need the logging code). But none of that was working.

There must be something janky with my Azure account. Right?

- Jamie

There were no logs in the Azure dashboard, so I loaded up the Kudu dashboard

for those who don’t know, if you head to "yoursite".scm.azurewebsites.net and are signed into the Azure Portal, you’ll get some extra stuff to help you debug your WebApps

I could see that the files where being deployed the the Web App perfectly fine, and it was definitely the compiled binaries which where being deployed. Martin Thwaites had a great suggestion:

The strangest thing was that none of the Azure web Apps that I spun up seemed to have the dotnet tooling installed, so running dotnet followed by the dll of the application would not work. I would later find that this was a problem completely unrelated to what I was seeing, and it was some weird jankyness with Azure that day.

It’s Always The Yaml

So I’d checked:

  • The code
  • My account
  • The WebApp

The only thing left to check was the deployment yml that Azure had generated. Here’s the original yml in all of it’s autogenerated glory:

# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy ASP.Net Core app to Azure Web App

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Set up .NET Core
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: '6.0.x'
          include-prerelease: true

      - name: Build with dotnet
        run: dotnet build --configuration Release

      - name: dotnet publish
        run: dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v2
        with:
          name: .net-app
          path: ${{env.DOTNET_ROOT}}/myapp

  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: 'Production'
      url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

    steps:
      - name: Download artifact from build job
        uses: actions/download-artifact@v2
        with:
          name: .net-app

      - name: Deploy to Azure Web App
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v2
        with:
          app-name: 'my-web-app-here'
          slot-name: 'Production'
          publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_A_LONG_GUID_HERE }}
          package: .

There’s nothing inherently wrong with this yml… if you only have one project. But this solution had a lot going on, with six projects (and two of them being test projects):

A screenshot of Visual Studio Code showing the project layout of the application in question. There are six projects, two of which are test projects.
Forgive the project name censoring

The thing about when you run dotnet build --configuration Release in a directory where there’s a solution file present

the .sln file in the screen shot above

is that the tooling will build any and all projects listed in the solution file, copying the resulting binaries to the relevant output directory - in this case bin/Release/net6.0, because it was building a Release configuration. This isn’t a big problem at all.

But because the next line (dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp) did a publish using the solution file the binaries for the test projects ended up in the output directory. This meant that when the project was deployed to the Web App, it included the dlls which exercised the code.

the following is entirely conjecture on my part

Since the startup project was called REDACTED.WebApi and one of the test projects was called REDACTED.WebApi.IntegrationTests, and since they were both present in the deployed code, I THINK that the runtime was getting confused and must have been running the REDACTED.WebApi.IntegrationTests dll instead of the REDACTED.WebApi dll. I think that the presence of the xunit dlls was confusing things, too

these are a dependency of my test projects as I prefer to use xunit over nunit

Meaning that every time I hit the WebApp’s URL, it was re-running the tests.

Azure has some handy metrics to show CPU usage and such for your resources, and I could see the CPU spiking at around 50-70% for two seconds after every one of my failed requests for the WebApp’s url. But after that it would drop to 0% until I put another request in.

The Solution

So I decided to explicitly reference the startup project in my yml file, commit and watch to see if it would fall over. The two lines that I changed from the above became:

- name: Build with dotnet
  run: dotnet build --configuration Release src/REDACTED.WebApi/REDACTED.WebApi.csproj

- name: dotnet publish
  run: dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp src/REDACTED.WebApi/REDACTED.WebApi.csproj

Instead of building all of the projects referenced in the solution file, these two lines tell the tooling to only build the REDACTED.WebApi.csproj project and it’s direct dependencies.

Lo and behold, when I pushed this yml change up to GitHub, it built, deployed the binaries, and everything started working… finally.

Conclusion

I intentionally haven’t included in this blog post everything that I tried to get this app to deploy correctly. I tried a number of other things including but not limited to:

  • Putting the app inside a docker container
  • Shouting rather loudly
  • Not sleeping
  • Stress eating some pizza
  • Scratching my head
  • Asking on Twitter

But at the end of the day, all I needed to do was follow the advice of Abel Wang:

Don’t accept the defaults!

- Abel Wang (RIP)

I’ve almost always explicitly referenced the csproj that I want to build and deploy with other solutions that I’ve worked on. Not out of any kind of arcane knowledge, but just because I’m only ever interested in the result of building one very specific project. And the one time that it was required, I forgot to do it.

So don’t accept the defaults, and always explicitly reference the project you want to build.