Title image of How to White Label ASP .NET Core with Middleware

How to White Label ASP .NET Core with Middleware

10 March 2023

·
C#
AspNet

There are a few ways an ASP .NET app can be white-labelled. A good solution needs to be easy to add and have little disruption for future development work. The last thing we need is for our app to be tangled up in white-labelling logic so much that adding new features becomes painful. An elegant self-contained solution is needed for keeping the developers of the app happy.

For these reasons I started looking into using middleware for white labelling. The idea is to set up the app to be served from multiple URLs: our original URL and any resellers URLs. The middleware then changes the look of the app when it’s being accessed from the reseller URL.

💡

White Labelling is the process of rebranding a company’s product so that it can be resold by other companies. It usually involves replacing the original logo and colour scheme with that of the reseller company. Read more on white labelling.

The code in this blog post is going to be based on the default ASP .NET Core template using .Net 6.0. To test changes Kestrel is set to serve the app over two localhost ports. To make that happen add a second URL into the lauchSettings.json file:

"applicationUrl": "https://localhost:7108;https://localhost:7109"

Asp .Net Core Middleware

First, it’s worth getting a clear understanding of how middleware works in ASP .NET.

Middleware allows us to execute code before and after a request gets to any controller or static file. In fact, the code that directs requests to controllers or static files is middleware itself!

The common word used for describing middleware is a pipeline. It’s called a pipeline because the middleware gets executed in the order that they’re registered. This makes it very important to take care when registering middleware. Middleware that serves sensitive information shouldn’t be registered before the middleware that authenticates requests. Otherwise unauthenticated requests would have access to the information.

AspNet Core middleware pipeline

To get started this is what a blank middleware type looks like:

namespace WebApplication1
{
    public class WhitelabelMiddleware
    {
        private readonly RequestDelegate _next;

        public WhitelabelMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            await _next(context);
        }
    }
}

And here’s how we add it to the middleware pipeline in the Program.cs

app.UseHttpsRedirection();

app.UseMiddleware<WhitelabelMiddleware>();

app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();

It’s important to register WhitelabelMiddleware before the other middleware so it can intercept and modify the responses of the others.

How to modify a response in Middleware

Modifying the response of our app is necessary for white labelling our product. We need to be able to replace any of our company branding with the reseller's branding.

Modifying responses is actually quite tricky in middleware. The response is stored in a stream that we can’t modify directly. Streams are so annoying… However, with some creativity we can get around this.

Replacing the original stream with a memory stream before the next middleware allows the response to be edited. But this memory stream can’t be returned. The contents then needs to be copied back into the original stream which can be returned.

Confusing I know…. here’s the middleware code to make it happen:

var whiteLabel = context.Request.Host.Value == "localhost:7109";

if (whiteLabel)
{
    // Replace response stream with our own to be able to modify it
    var originBody = context.Response.Body;
    context.Response.Body = new MemoryStream();

    // Call the next middleware
    await _next(context);

    context.Response.Body.Seek(0, SeekOrigin.Begin);

    using (var reader = new StreamReader(context.Response.Body))
    {
        var body = await reader.ReadToEndAsync();

        // Modify body here

        var requestContent = new StringContent(body, Encoding.UTF8, "text/css");                    
        context.Response.Body = await requestContent.ReadAsStreamAsync();
        context.Response.ContentLength = context.Response.Body.Length;
        context.Response.Body.Seek(0, SeekOrigin.Begin);

        // Return the original stream containing our modified response
        await context.Response.Body.CopyToAsync(originBody);
        context.Response.Body = originBody;
    }
}
else
{
    await _next(context);
}

Colours

Replacing the colour scheme makes a huge difference to the look of the app. A basic example of this is to use a simple string replace on the CSS:

var whiteLabel = context.Request.Host.Value == "localhost:7109";
bool modifyCss = whiteLabel && context.Request.Path.Value
	.Contains("site.css", System.StringComparison.OrdinalIgnoreCase);

if (whiteLabel && modifyCss)
{
		// Stream stuff
		
		body = body.Replace("#1b294c", "#ff0000", System.StringComparison.OrdinalIgnoreCase);
		
		// More Stream stuff			    
}
else
{
    await _next(context);
}

This code will replace all uses of the colour #1b294c with a new colour: #1c1f38. Doing it this way is rather crude and basic but it gets the job done. It’s fine to start this way but it gets messy when replacing lots of colours or whitelabelling your app for multiple clients from multiple URLs. An improvement would be to pull the colours from a configuration store.

Logos

We’re going to use a different technique to replace the logos of the site. We don’t need to edit the response like we did for the colours. Instead, the middleware is going to rewrite the request URL to the different logo.

White-labelling middleware redirecting logo requests

Making the file names the same means the rewriting code can be simple. Here’s the file structure of logos if we’re white-labelling the app for a company called ExampleCompany:

Our logos are in the root folder of ‘logos’. Requests to the white-labeled version of the app are redirected to the ‘ExampleCompany’ folder:

// Replace Logos
if (whiteLabel)
{
    if (context.Request.Path.Value.Contains("images/Logos", System.StringComparison.OrdinalIgnoreCase))
    {
        context.Request.Path = context.Request.Path.Value
            .Replace("images/Logos", "images/Logos/ExampleCompany", System.StringComparison.OrdinalIgnoreCase);
    }
}

Hiding or showing elements

Sometimes it’s not good enough to just change a couple of colours. Whole sections of an app might need to be hidden from the white-labelled version. Keeping the logic tidy for this is very important. Having lots of rendering conditions everywhere becomes hard to maintain.

Our approach is going to use a single boolean. The boolean is set in the middleware with the other white labelling logic. It’s then passed to the views of the app using HttpContext items.

Inside the middleware:

context.Items["whitelabel"] = whiteLabel;

Helper extension method for checking if the value is true:

public static bool IsWhitelabelled(this HttpContext context)
{
    if(context.Items.TryGetValue("whitelabel", out var whitelabel))
    {
        var whiteLabelBool = whitelabel as bool?;

        if(whiteLabelBool != null)
        {
            return whiteLabelBool.Value;
        }
    }

    return false;
}

And showing/hiding elements inside the view:

<div class="text-center">    
    @if (HttpContext.IsWhitelabelled())
    {
        <h1 class="display-4">Welcome</h1>
    }
    else
    {
        <h1 class="display-4">Hey Friends</h1>
    }
</div>

Final code

Here’s everything combined together!

using System.Text;

namespace WebApplication1
{
    public class WhitelabelMiddleware
    {
        private readonly RequestDelegate _next;

        public WhitelabelMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var whiteLabel = context.Request.Host.Value == "localhost:57109";
            bool modifyCss = whiteLabel && context.Request.Path.Value
                .Contains("site.css", System.StringComparison.OrdinalIgnoreCase);

            // Set whitelabel boolean for downstream views
            context.Items["whitelabel"] = whiteLabel;

            // Replace Logos
            if (whiteLabel)
            {
                if (context.Request.Path.Value.Contains("images/Logos", System.StringComparison.OrdinalIgnoreCase))
                {
                    context.Request.Path = context.Request.Path.Value
                        .Replace("images/Logos", "images/Logos/ExampleCompany", System.StringComparison.OrdinalIgnoreCase);
                }
            }

            if (whiteLabel && modifyCss)
            {
                // Replace response stream with our own to be able to modify it
                var originBody = context.Response.Body;
                context.Response.Body = new MemoryStream();

                // Call the next middleware
                await _next(context);

                context.Response.Body.Seek(0, SeekOrigin.Begin);

                using (var reader = new StreamReader(context.Response.Body))
                {
                    var body = await reader.ReadToEndAsync();

                    // Modify body here

                    body = body.Replace("#1b294c", "#ff0000", System.StringComparison.OrdinalIgnoreCase);

                    var requestContent = new StringContent(body, Encoding.UTF8, "text/css");                    
                    context.Response.Body = await requestContent.ReadAsStreamAsync();
                    context.Response.ContentLength = context.Response.Body.Length;
                    context.Response.Body.Seek(0, SeekOrigin.Begin);

                    // Return the original stream containing our modified response
                    await context.Response.Body.CopyToAsync(originBody);
                    context.Response.Body = originBody;
                }
            }
            else
            {
                await _next(context);
            }
        }
    }
}

I hope you found this interesting 🙂