Resolving the .NET MAUI “VersionCode 1.0 is invalid. It must be an integer value.” error when updating Visual Studio 2022 Preview

I installed the Visual Studio 2022 Preview 6 this evening. I had been using Preview 4. I installed Preview 5, but didn’t have a chance to play with it. I have a simple demo app that I have working with, a basic stopwatch type of app. That app had been created with Preview 4 and it more or less worked fine (unless you counted Mac Catalyst and Windows). After I installed Preview 6, I tried to run the project on Android. It failed to compile with the following error message:

Severity	Code	Description	Project	File	Line	Suppression State
Error	XA0003	VersionCode 1.0 is invalid. It must be an integer value.
Parameter name: VersionCode	StopwatchMaui	....\StopwatchMaui\obj\Debug\net6.0-android\android\AndroidManifest.xml

As a test, I created a new .NET MAUI app from Preview 6. It compiled and ran just fine.

So who now, what now? When I first saw the error, I didn’t pay too much attention to the full path, just the file name. With .NET MAUI, there is an AndroidManifest.xml in the android platform folder.

And we take a look at the file, it’s pretty standard, pretty boring AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
	<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
	<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

No versionCode there. What’s going on? So I went back and actually read the error message and it was complaining about a version of AndroidManifest.xml located in obj\Debug\net6.0-android. That little fellow looks like this:

<?xml version="1.0" encoding="utf-8"?>
<!--
    This code was generated by a tool.
    It was generated from ....\StopwatchMaui\Platforms\Android\AndroidManifest.xml
    Changes to this file may cause incorrect behavior and will be lost if
    the contents are regenerated.
    -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1.0" package="com.companyname.StopwatchMaui" android:versionName="1.0.0">
  <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true" android:name="crc64c1104ba8f6ea44b3.MainApplication" android:label="StopwatchMaui" android:debuggable="true" android:extractNativeLibs="true">
    <activity android:configChanges="orientation|smallestScreenSize|screenLayout|screenSize|uiMode" android:theme="@style/Maui.SplashTheme" android:name="crc64c1104ba8f6ea44b3.MainActivity" android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <receiver android:enabled="true" android:exported="false" android:label="Essentials Battery Broadcast Receiver" android:name="crc64192d9de59b079c6d.BatteryBroadcastReceiver" />
    <receiver android:enabled="true" android:exported="false" android:label="Essentials Energy Saver Broadcast Receiver" android:name="crc64192d9de59b079c6d.EnergySaverBroadcastReceiver" />
    <receiver android:enabled="true" android:exported="false" android:label="Essentials Connectivity Broadcast Receiver" android:name="crc64192d9de59b079c6d.ConnectivityBroadcastReceiver" />
    <activity android:configChanges="orientation|screenSize" android:name="crc64192d9de59b079c6d.IntermediateActivity" />
    <provider android:authorities="com.companyname.StopwatchMaui.fileProvider" android:exported="false" android:grantUriPermissions="true" android:name="xamarin.essentials.fileProvider">
      <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/xamarin_essentials_fileprovider_file_paths" />
    </provider>
    <activity android:configChanges="orientation|screenSize" android:name="crc64192d9de59b079c6d.WebAuthenticatorIntermediateActivity" />
    <service android:name="crc64396a3fe5f8138e3f.KeepAliveService" />
    <provider android:name="mono.MonoRuntimeProvider" android:exported="false" android:initOrder="1999999999" android:authorities="com.companyname.StopwatchMaui.mono.MonoRuntimeProvider.__mono_init__" />
  </application>
</manifest>

If we look at line 8, we see the culprit

<manifest 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:versionCode="1.0" 
    package="com.companyname.StopwatchMaui" 
    android:versionName="1.0.0">

In the new app that was created in Preview 6, the same file had the following line:

<manifest 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:versionCode="1" 
    package="com.companyname.StopwatchMaui" 
    android:versionName="1.0.0">

So why is the first one bad and the second one good? In the wacky world of Android, android:versionCode has to have an integer value. This is documented here. So now we know what is the actual error, the next question is why that error occurred.

We can’t just edit the obj\Debug\net6.0-android\AndroidManifest.xml file and call it a day. The next time you rebuild the app, that file gets generated from Platforms\Android\AndroidManifest.xml. And apparently it pulls in information from somewhere else as well.

So I took a look at the .csproj files for the working and non-working apps. In the .csproj file generated by Preview 4, the version information was defined with the following two lines

<!-- Versions -->
<ApplicationVersion>1.0</ApplicationVersion>
<AndroidVersionCode>1</AndroidVersionCode>

With the new project freshly generated by Release 6, the same two lines were now a single line, with the ApplicationVersion now set with an integer value.

<!-- Versions -->
<ApplicationVersion>1</ApplicationVersion>

When I changed the “Versions” lines .csproj to match the single line used in the new .csproj Preview 6, the app compiled and deployed to Android. My best guess is that AndroidVersionCode was being used in Preview 4 and sometime after that, they made the breaking change to ApplicationVersion and jettisoned the AndroidVersionCode setting. It’s a preview release of Visual Studio and they are still baking .NET MAUI. This kind of stuff happens and the end result is a better product.

Controlling a WSL installation of redis-server from the Windows command line

If you like using redis for web site caching and you are writing and testing code locally from Windows, you’ll want to figure out how to run a local instance of redis-server. You have a few options. You can run it from another machine that’s running something vaguely Linux-like or MacOS. You can run it from a Docker container under Windows. Or you can run it directly from Windows SubSystem (WSL) for Linux.

For the last few years, Windows (10, 11, Server 2019) comes with a compatibility layer that lets you run Linux binary executables. The current version is WSL 2, but I’m just going to refer to it as WSL.  If you don’t have installed already, just run the following command from an elevated shell (run as administrator)

wsl --install
Installing WSl via command line
Installing WSl via command line

It will install the bits that you need and then ask you to reboot your machine. For more information about installing and configuring WSL, Microsoft has some really good documentation at Install WSL.

The default Linux distribution for WSL is Ubuntu. After you reboot, you may see a Ubuntu shell for a while as Windows installs the bits that you need. It will churn for a bit and then ask you for a username and password. That password will be your sudo (user root) password. One that is done, you should see something like this.

Final step of installing WSL is configuring the default Linux distribution
Final step of installing WSL is configuring the default Linux distribution

The next thing to do is to install redis-server. We can install redis vis apt-get, but before we do that, we need to update apt-get and remove some of the new install shininess off.

sudo apt-get update
sudo apt-get upgrade

After updating apt-get, you’ll want to upgrade it. Confusing? Sort of. Update is updating the instance of apt-get, upgrade is updating all of the packages that were installed via apt-get. After that has finished, you can install redis-server

sudo apt-get install redis-server

There will be some churn and finally, it be installed. Out of the box, redis will not be running. My personal preference is to only run redis when I’m actually using it for development. So I Iike to start and stop it from the command line. Here are commands that you need to know:

sudo service redis-server status
sudo service redis-server start
sudo service redis-server stop

I’ll start up redis with the service start command, and it will come back with a message that redis is starting. And you can use the service status command to verify that it’s running. The acid test is to connect to redis and see if it’s working. You can use the redis-cli tool to set and get a cache value. You should see something like this.

Running redis-cli from the Ubuntu shell
Running redis-cli from the Ubuntu shell

So now redis-server is running. If you close the shell and open up a new one, it will still be running. If you restart WSL or Windows, then it wont be running. Now you can always pop open an Ubuntu shell and start redis-server, but you can do it from a Windows command line

wsl sudo service redis-server status
Running redis commands from PowerShell
Running redis commands from PowerShell

From the screenshot, you can see that WSL passed along the sudo service command to Ubunto. And because I used sudo, I was prompted for the root password. And it returned the same message that I would see from the Ubuntu shell. Since I only have Ubunto installed, that was the default Linux that received the command. If you have multiple distributions installed, you would use “wsl -d DistributionName”. You can get the names of the installed distributions with the “wsl -l” command. Unlike from the Linux shell, each time I invoke “sudo”, I’m prompted for the password. In the Linux shell, you are prompted just the first time you call sudo in a terminal session.

Having to use the root password over and over again can be tedious on a development box. There is a way around that. You can add a file to the /etc/sudoers.d folder in the Linux distribution and remove the root password requirement for the redis-server service. From the Linux shell do the following

cd /etc/sudoers.d
sudo sh
echo "%sudo ALL=(ALL) NOPASSWD: /usr/sbin/service redis-server *" >> allowed-services
sudo chmod 0440 allowed-services
exit

The first thing we do is make /etc/sudoers.d the current folder. Then we use “sudo sh” to gain root access. You need root access to work with this folder. The echo line basically says that you can invoke sudo with no password for redis-server and writes that setting to a file named allowed-services. The file name allowed-services is arbitrary, I picked it because it made sense to me. There is a file in the sudoers.d folder named README, it will explain what the file name restrictions are.

The chmod 0440 command sets the permissions to read-only for the root account and is required for sudoers.d. This allows us to remove the sudo password requirement for redis-server and only for redis-server. Now we can go back to the Windows shell and run the wsl commands without being prompted.

You can even run the redis-cli tool from powershell…

Redis from PowerShell, no password
Redis from PowerShell, no password

The WIndows Subsystem for Linux is one of the hidden gems for developers. And this gem sparkles…

Bonus Round!

Because I’m lazy, I created shortcuts in my PowerShell profile. I added the following functions:

function redstat {wsl sudo service redis-server status}
function redstart {wsl sudo service redis-server start}

And now I can just check the redis-server status via “redstat”