# White Labeling .NET MAUI Apps

# Introduction

*Have you ever come across two (or more) products that looked almost exactly the same and only differed in things like color and the company logo printed on them?* This is a common practice called ***white labeling*** where one company develops a product and then rebrands it for other companies to sell it under their own brand. A lot of products you can find in popular online shops are actually white-labeled.

So, let me rephrase my initial question: *Have you ever come across two (or more)* ***apps*** *that looked almost exactly the same and only differed in things like color and the company logo on them?* What you saw might indeed have been the same app just with a different icon, different images and different colors but based on exactly the same code base.

> This blog post is my contribution to Matt Goldman's [MAUI UI July](https://goforgoldman.com/posts/mauiuijuly-24/) 2024. Check it out, there are many great blog posts about .NET MAUI to be found there.

Today, I will show you how you can develop a single .NET MAUI app and easily rebrand it for different clients that share the same *(or very similar)* requirements. We'll see an example setup for two clients: *ClientA* and *ClientB*, as well as an unbranded "default" version of the app. We'll cover client-specific *logos*, *images*, *colors*, *styles*, *app names* and *identifiers*, as well as *fonts*, and also quickly touch on *custom behavior*.

The goal is achieve two or three differently styled apps with the same code base under the hood:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720768928477/e4231f02-272f-47cb-a12a-6d008567827d.png align="center")

> **Disclaimer:** This blog post can only serve as a starting point, because this topic is too large to cover all aspects in a single write-up. For example, customer-specific resource files (for strings and translations) are a topic that might be addressed in a follow-up post. I do not guarantee that the concepts presented here will work for every scenario. There are various different ways to achieve white labeling and this is just one of many approaches.

As always, there is a [companion repository](https://github.com/ewerspej/MauiWhiteLabelling) where you can find the full source code for this blog post *(and more)*. To quote a popular YouTuber: *"it is enough talking, so let's do it!"*

# Project Setup

First, let's look at the project structure that we'll work with. A new MAUI single project app created using one of the available templates will usually result in a project structure that looks similar to this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720692927218/963f37ff-65c1-44ff-923a-edaeaf5de0bb.png align="center")

Things like the app identifier, app name, app icon, images, fonts, etc. are all defined or referenced in the SDK-style project *(.csproj)* file of a MAUI app project. This is typically done between `<PropertyGroup>` and `<ItemGroup>` tags:

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <!-- Display name -->
    <ApplicationTitle>MauiSamples</ApplicationTitle>
    <!-- App Identifier -->
    <ApplicationId>com.companyname.mauisamples</ApplicationId>
  </PropertyGroup>
  <ItemGroup>
    <!-- App Icon -->
    <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/>
    <!-- Splash Screen -->
    <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128"/>
    <!-- Images -->
    <MauiImage Include="Resources\Images\*"/>
    <!-- Custom Fonts -->
    <MauiFont Include="Resources\Fonts\*"/>
    <!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
    <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
  </ItemGroup>
</Project>
```

> **Note:** To keep things simple, I'm only showing relevant bits of the *.csproj* file

This structure assumes that there's ever only a single source of truth when it comes to icons, images, styles and so on, which in this context usually is the *Resources* folder. For single-client apps, this is perfect, but for white labeling this let's us face a couple of problems. For example, how would we replace the app icons or fonts for each client?

We can work with this, but we need to make some changes, that I will describe in the following paragraphs.

## Resources Structure

For white labeling, I suggest using a slightly altered project structure with an additional level of subfolders right under *Resources* with the usual folders for icons, fonts, images and so on separated by client:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720598242306/97fcc8de-ac2d-4a47-9ef2-89f8613c4a1d.png align="center")

The main benefit of this structure is that we can keep all the client-specific assets separate without losing the logical grouping we're used to from other MAUI apps.

Now, this will not do anything on its own yet. In a MAUI single project app, the different asset types have their own specific build actions, as we've already seen above:

```xml
<!-- excerpt from .csproj file -->
<ItemGroup>
    <!-- App Icon -->
    <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/>
    <!-- Splash Screen -->
    <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128"/>
    <!-- Images -->
    <MauiImage Include="Resources\Images\*"/>
    <!-- Custom Fonts -->
    <MauiFont Include="Resources\Fonts\*"/>
    <!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
    <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>
```

Now, you might be asking yourself *"but, how do I get the client-specific resources in there?"*. Well, we could of course replace the paths to each resource subfolder each time we build the app for a different client. This, however, would quickly become a massive hassle and wouldn't work with automated build pipelines.

A common solution to this type of problem is to use different build configurations, two for each client (one *Debug* and one *Release* configuration) and then use build *conditions* to select the correct set of resources to be included in the build process. We'll have a look at that next before coming back to using the correct assets.

## Build Configurations

To add new build configurations, we need to select the drop down with the active build configuration:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720600037466/d929ed24-875b-4f9d-8ef4-d37eb615a012.png align="center")

In the drop down that appears, we then select "Configuration Manager", which opens the following dialog:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720600157889/24de12b1-004f-4d98-a4b4-3d2076a0da00.png align="center")

Now, we select the "Active solution configuration" drop down and then hit "New", which will open the "New Solution Configuration" dialog:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720600277630/3badab97-36d2-46d9-9396-732f3129fbaa.png align="center")

We can now provide a name to the new configuration and copy the settings from an existing one. We also need to create new project configurations *(make sure to tick the checkbox)*. When we're done, we hit "OK" and come back to the "Configuration Manager" dialog.

Now, we need to select the newly created solution configuration and select the companion project configuration that we created, so that it looks like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720600597077/45337367-f9ea-40b8-bcec-26fe38516257.png align="center")

We repeat these steps for every client and for both *Debug* and *Release* configurations. Provided that we use two clients *(ClientA, ClientB)* and a *default* configuration in this example, we will end up with the following six configurations:

* Debug
    
* Debug-ClientA
    
* Debug-ClientB
    
* Release
    
* Release-ClientA
    
* Release-ClientB
    

Note how these conditions also get added to your project's *.csproj* file:

```xml
<PropertyGroup>
    <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
    <!-- skipping other properties -->

    <!-- new configurations we just added in the Configuration Manager -->
    <Configurations>Debug;Release;Debug-ClientA;Debug-ClientB;Release-ClientA;Release-ClientB</Configurations>
</PropertyGroup>
```

> **Note:** The *.sln* file will also get, too, some new GUIDs and configurations are added in there, as well. ***Please do not meddle with this file, as it's very easy to break the entire solution this way.***

Now, we can use these configurations together with build conditions in order to decide which assets to include in the build process.

## Build Conditions and Properties

When working with a default MAUI app template, we will eventually come across some build conditions. If you have a little bit of experience with MAUI, you will have seen lines like this in the *.csproj* file of other apps plenty of times already:

```xml
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks>
```

The `Condition` in the above example will ensure that the *Windows* target is only used when the operating system on which the solution is compiled is *Windows*, because MAUI apps for Windows cannot be compiled on a Mac.

We can use this same mechanism also for our white labeling purposes, which is what we created the build configurations for in the previous step. We can filter the assets to be included using the active build configuration as follows:

```xml
<PropertyGroup Condition="'$(Configuration)' == 'Debug' OR '$(Configuration)' == 'Release'">
   <!-- define default properties -->
</PropertyGroup>

<PropertyGroup Condition="$(Configuration.EndsWith('ClientA'))">
   <!-- define ClientA properties -->
</PropertyGroup>

<PropertyGroup Condition="$(Configuration.EndsWith('ClientB'))">
   <!-- define ClientA properties -->
</PropertyGroup>
```

Above, we use the `Configuration` build property, which will have the value of one of the six build configurations we created earlier *(e.g. "Debug", "Debug-ClientA" or "Release-ClientB", ...)*. The value of this property can be evaluated. There are even methods that can be called, such as `EndsWith()` to check for a substring, e.g. `Condition="$(Configuration.EndsWith('ClientB'))"`.

Now, if we would define the properties and project items all in the *.csproj* file, things would get messy pretty quickly. Having a clean, manageable structure is key when it comes to rebranding. Therefore, instead of having all client-specific data in one massive *.csproj* file, we can split this into separate project property *(.props)* files, one for each of the clients, which for ClientA could look like this, for example:

```xml
<!-- ClientA.props file -->
<Project>
  <PropertyGroup>
    <!-- Assembly Name -->
    <ApplicationAssemblyName>SuperDuperApp</ApplicationAssemblyName>
    <!-- Display name -->
    <ApplicationTitle>Super Duper App</ApplicationTitle>
    <!-- App Identifier -->
    <ApplicationId>com.ClientA.SuperDuperApp</ApplicationId>
  </PropertyGroup>
  <ItemGroup>
    <!-- App Icon -->
    <MauiIcon Include="Resources\ClientA\AppIcon\appicon.svg" ForegroundFile="Resources\ClientA\AppIcon\appiconfg.svg" Color="#123456" ForegroundScale="0.65"/>
    <!-- Splash Screen -->
    <MauiSplashScreen Include="Resources\ClientA\Splash\splash.svg" Color="#123456" BaseSize="128,128"/>
    <!-- Images -->
    <MauiImage Include="Resources\ClientA\Images\*"/>
    <!-- Custom Fonts -->
    <MauiFont Include="Resources\ClientA\Fonts\*"/>
    <!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
    <MauiAsset Include="Resources\ClientA\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
    <!-- Privacy Manifest for iOS -->
    <BundleResource Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'" Include="Platforms\iOS\PrivacyInfo.xcprivacy" LogicalName="PrivacyInfo.xcprivacy"/>
  </ItemGroup>
</Project>
```

This file contains all the client-specific properties and items that should be included only when building the app for ClientA. It is referencing the files and folders from the *Resources/ClientA* subfolder for the app icon, splash screen, etc., and it also defines the client-specific app title and package identifier *(= ApplicationId)*.

If we do this for all clients, we can then include the correct *.props* file based on the selected configuration in our *.csproj* file using our build conditions:

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

  <Import Condition="'$(Configuration)' == 'Debug' OR '$(Configuration)' == 'Release'" Project="Resources\Default\default.props" />
  <Import Condition="$(Configuration.EndsWith('ClientA'))" Project="Resources\ClientA\ClientA.props" />
  <Import Condition="$(Configuration.EndsWith('ClientB'))" Project="Resources\ClientB\ClientB.props" />

  <!-- this is required to give the output files (.dll) a different assembly name per client -->
  <PropertyGroup>
    <AssemblyName>$(ApplicationAssemblyName)</AssemblyName>
  </PropertyGroup>

</Project>
```

This just looks so much cleaner than having one massive *.csproj* file with lots of client-specific stuff in it, don't you agree? It also makes the onboarding of new clients more straightforward.

## Intermediate Build

If we would build and deploy each of the three different client apps to a device now, we will see three different installed apps each with a unique app icon and name, all based on the same code base:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720605397731/d907e780-b42b-492f-a8f1-f069ac8017db.png align="center")

This is already pretty cool! However, we're not done yet, because if we would run each of the apps, apart from the app icon and app name, their content would still look identical. This is because we haven't made any other changes so far.

**Important:** For this to work, the *App.xaml* file needs to reference valid paths to the *Colors.xaml* and *Styles.xaml* resource dictionaries. However, in our setup above, these files have been moved to a different location and we haven't modified the *App.xaml* file. **However, I don't actually want to change the *App.xaml* file at all, it should remain exactly the way it is**. We will take a look at this next.

# Styles and Colors

In a MAUI app, the *App.xaml* file typically references the [resource dictionaries](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/resource-dictionaries) that contain definitions for the colors and styles like this:

```xml
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
			 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
			 x:Class="MauiWhiteLabelling.App">
	<Application.Resources>
		<ResourceDictionary>
			<ResourceDictionary.MergedDictionaries>
				<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
				<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
			</ResourceDictionary.MergedDictionaries>
		</ResourceDictionary>
	</Application.Resources>
</Application>
```

Note that there is a relative path to each of the XAML resource dictionaries. This path is problematic, because, unfortunately, XAML does not support conditional compilation. Therefore, we cannot provide different paths for different clients and unfortunately, MAUI also doesn't allow us to set the `Source` property from the code-behind, either.

So, how can we reference the correct colors and styles from our separate client asset subfolders? We shouldn't have to modify the *App.xaml* file every time we build an app for a specific client. That's too risky and only makes things complicated.

A common *Xamarin.Forms* way for this was to load these resource dictionaries *dynamically* during the app start. However, this introduces a new problem: The `StaticResource` markup will not be usable, because the styles are not known at the time of construction. Hence, we would have to resort to using the `DynamicResource` markup instead, which would introduce a performance penalty and we would lose the preview capabilities of the XAML editor *(at least when using Visual Studio on Windows)*.

💡 However, fret not, I found a solution for this, as well. Since we have the *.props* files for each client in place already, we can use this mechanism to also copy some assets like the XAML resource dictionaries to a shared location, more specifically the location where the *App.xaml* file expects those resources to be: the *Resources/Styles* subfolder.

All we need to do for this to work is to update the *.props* files and add the `InitialTargets` attribute to the `<Project>` tag that points to a named `Target` with an action that copies the required files to the correct location:

```xml
<Project InitialTargets="CopyResourceFiles">
  <!-- skipping PropertyGroups and ItemGroups -->
  <Target Name="CopyResourceFiles">
    <Copy SourceFiles="Resources\ClientA\Styles\Styles.xaml;Resources\ClientA\Styles\Colors.xaml" DestinationFolder="Resources\Styles" />
  </Target>
</Project>
```

Basically, with this approach, we replace the two resource dictionary files every time we select a different build configuration. This is fine, because it's very fast and any build targets included in the `InitialTargets` will run *before* compilation. This is what allows us to still use the `StaticResource` markup, thus avoiding sweeping changes and performance hits.

I recommend adding the paths of the shared *Styles.xaml* and *Colors.xaml* file locations under *Resources/Styles* to the *.gitignore* file of your repository, so that they don't get included in any commits, similar to any auto-generated files that also don't need to be included, either:

```plaintext
# Explicit file exclusions
MauiWhiteLabelling/Resources/Styles/Colors.xaml
MauiWhiteLabelling/Resources/Styles/Styles.xaml
```

Styles and colors: Check ✅.

Let's have a look at custom fonts before finally running the app for the first time.

# Fonts

In order to use custom fonts, we usually need to add the font files to the project and set the build action to `MauiFont`. Normally, this is already the case, as long as the font files are located in the directory specified for the `<MauiFont>` tag, e.g.:

```xml
<MauiFont Include="Resources\Fonts\*"/>
```

For white labeling to work, a simple trick to use different fonts per client is to add the font files using the *exact same name* for all clients inside the client-specific folders and then to register them using the *exact same alias:*

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720691150063/a289fa98-4fef-4ce3-a1de-6f874ee7dd4a.png align="center")

In the custom *.props* file we already specified the path to the client-specific font files:

```xml
<!-- ClientA.props -->
<MauiFont Include="Resources\ClientA\Fonts\*"/>
```

This way, we can reference a font directly by its shared alias and the correct client font file will always be used. We then don't have to specify which client we're registering which font for, we only need to modify the font registration in the *MauiProgram.cs* file as follows:

```csharp
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .UseMauiCommunityToolkit()
        .ConfigureFonts(fonts =>
        {
            // use the same font filename and alias for every client
            fonts.AddFont("FontRegular.ttf", "FontRegular");
            fonts.AddFont("FontSemibold.ttf", "FontSemibold");
        });

    return builder.Build();
}
```

As you can see, we don't use the actual font name here, we just use a shared naming convention for the font file and alias.

This approach is not mandatory, the `FontFamily` can also be specified in a style definition in the client-specific *Styles.xaml* resource dictionary using the actual names of the fonts. However the approach described here will make our lives *a lot easier*, since it allows us to use fonts by a shared naming convention everywhere in the app equally. We can simply specify the `FontFamily` like we normally would anywhere in our app and see the text in a different font depending on the selected client configuration:

```xml
<Label
    Text="Welcome!"
    FontFamily="FontSemibold"
    FontSize="36" />
```

In the next section we'll see what it all looks like when we actually run the app after quickly setting up a page that uses the client-specific resources we've supplied.

# Running the app

Let's put it all together with a simple *ContentPage* that has the client's app logo, a label and a button, with a custom font and a client-specific background color:

```xml
<ContentPage
  x:Class="MauiWhiteLabelling.Views.MainPage"
  xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  BackgroundColor="{StaticResource Primary}"
  Shell.BackgroundColor="{StaticResource Primary}"
  Shell.NavBarIsVisible="False">

  <Grid>
    <VerticalStackLayout
      Padding="20,40"
      Spacing="40"
      VerticalOptions="Start"
      HorizontalOptions="Center">
      <Image
        Source="logo.png"
        WidthRequest="80"
        HeightRequest="80"
        HorizontalOptions="Center"
        VerticalOptions="Center" />
      <Label
        Text="Welcome!"
        FontFamily="FontSemibold"
        FontSize="36"
        HorizontalOptions="Center"
        VerticalOptions="Center" />
      <Button Text="Press me" Clicked="Button_OnClicked" />
    </VerticalStackLayout>
  </Grid>

</ContentPage>
```

Running this app now will yield three different results for the different client configurations, which is exactly what we wanted:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720768955727/f1353dd1-d22f-434a-9f01-97d4cb702d3b.png align="center")

🎉 Isn't this awesome? With a simple setup and a few tricks, we managed to white-label an app for three different client configurations. Rock and roll 🤘!

However, sometimes, clients also want some unique or custom behavior. Now, that can be done just as well. Let me show you how in the next section.

# Custom Behavior

We can create custom behavior for every client, e.g. by using compile constants or feature toggles. Compile constants can be defined in the client *.props* file as follows:

```xml
<Project InitialTargets="CopyResourceFiles">
  <PropertyGroup>
    <!-- skipping other stuff for brevity -->
    <!-- Compilation Constants for Client A -->
    <DefineConstants>$(DefineConstants);CLIENT_A</DefineConstants>
  </PropertyGroup>
  <ItemGroup>
    <!-- skipping assets for brevity -->
  </ItemGroup>
</Project>
```

With this in place, we can then instruct the compiler to modify the behavior of a method or anything else (even enable or disable entire features altogether) for a specific client. To keep things simple, I'll only show a small example here. The following code shows an event handler for a button's `Clicked` event on the MainPage:

```csharp
public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
	}
    
    private async void Button_OnClicked(object? sender, EventArgs e)
    {
#if DEFAULT_APP
        await DisplayAlert("Default App", "This is the default app", "OK");
#elif CLIENT_A
        await DisplayAlert("Client A", "This is client A", "OK");
#elif CLIENT_B
        await DisplayAlert("Client B", "This is client B", "OK");
#endif
    }
}
```

When the button is clicked, it will show a different text message based on the selected client configuration:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720769212036/d6e77c69-7619-49c0-b7ee-4f847b2d0291.png align="center")

✨Whoohoo, we have a fully customized app!

# Summary and next steps

I'm super excited about how easy it was to white-label a .NET MAUI app once I figured out all the necessary steps. I've demonstrated that it's possible to rebrand an app for different clients while maintaining the same code base and without having to frequently modify files and configurations.

A next step would be to include client-specific translation resources using *.resx* files, which could either be achieved by also copying the resource files into a single, shared location using the `InitialTargets` step or by using some kind of lookup mechanism (e.g. with a custom markup extension). If I find the time, I might investigate how to achieve this in the most straightforward way I can think of.

> If you're wondering how I've set the client-specific status bar color on Android without having modified or included the *AndroidManifest.xml* file in my demonstration, check out the [*App.xaml.cs*](https://github.com/ewerspej/MauiWhiteLabelling/blob/main/MauiWhiteLabelling/App.xaml.cs) file in the repository that goes along with this post.

I hope this is useful to you. If you enjoyed this blog post, then you may follow me on [LinkedIn](https://www.linkedin.com/in/jewerspeters/), subscribe to this [blog](https://blog.ewers-peters.de/) and star the [GitHub repository](https://github.com/ewerspej/MauiWhiteLabelling) for this post as well as my [MAUI Samples](https://github.com/ewerspej/maui-samples/) repository, so you don't miss out on any future posts and developments.

Thanks for reading until here, and remember: sharing is caring!

## **Attributions**

*MauiWhiteLabelling* project created using Matt Lacey's [MAUI App Accelerator](https://marketplace.visualstudio.com/items?itemName=MattLaceyLtd.MauiAppAccelerator).

*Jar with while label* image generated using Microsoft Copilot/DALL-E.
