An essential part of building web applications is understanding what elements may look like in an evolving user interface. For most users, the web is a visual medium with complex components that include many images. This trend has accelerated since the introduction of broadband in more regions worldwide. Who doesn’t love a pretty-looking picture on a website? That said, when you first start building your applications, image assets might be far and few between and may not be essential to the structure of your web application.

In this short post, we’ll see how to use ImageSharp.Web and ASP.NET Core to build a local placeholder image service to help you prototype an expanding UI component library.

Placeholder Images and Why Not A Service?

Placeholder images help you fill out the structure of an HTML component. If you’ve seen any HTML component library, such as Bootstrap, you’re likely familiar with some of the components requiring an image element. The most notable is the card component, where an image is at the top of the component, followed by a title, subtitle, and content.

bootstrap card

These components don’t look right until you have an image placed in them, so what is one to do? Well, in most cases, you’d likely reach for an image placeholder service. Here are a few:

Easy enough, right? A few drawbacks should make you cautious about using these third-party services.

  1. These services can have intermittent issues that cause your layout to break.
  2. Slow transfer speeds of placeholder images can make your app seem slow to clients. In some scenarios, slow images can pause the rendering of your page until the client has fully downloaded the image.
  3. You could be sending cookie data to an unknown third party.

Issue 2 is particularly problematic if you’re doing automated testing or have performance alerts for page rendering times. These warnings can lead you to spend hours looking for performance bottlenecks in your code when the issue is with these placeholder services.

Let’s Write A Local Image Placeholder Service

Luckily for you, there’s an effortless way of creating a local placeholder image service with ASP.NET Core and ImageSharp.Web. But before we get to code, you’ll need an abstract image that can be stretched and resized without affecting the image’s content. In my case, I’ve created one in my favorite image editor. Feel free to use it.

placeholder image

Ok, let’s get to some code.

In a new ASP.NET Core project, you’ll want to add the following NuGet dependency of SixLabors.ImageSharp.Web.

dotnet add package SixLabors.ImageSharp.Web

Once you’ve installed the package, you’ll want to set up the ASP.NET Core pipeline. First, you’ll register the ImageSharp services, then add the middleware to your application’s request pipeline.

using SixLabors.ImageSharp.Web.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddImageSharp(options =>
    options.BrowserMaxAge = TimeSpan.FromDays(7);
    options.CacheMaxAge = TimeSpan.FromDays(365);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see


// Important, have the ImageSharp
// middleware before the static files

It’s important that you register ImageSharp middleware before the static file middleware, or else ASP.NET Core won’t process images from your static folders properly. Next, let’s create a specialized tag helper to make adding placeholder images more straightforward in our Razor views.

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace Bespoke;

[HtmlTargetElement("placeholder", TagStructure = TagStructure.WithoutEndTag)] 
public class PlaceholderTagHelper : TagHelper
    public int Width { get; set; } = 256;
    public int? Height { get; set; }
    public override void Process(TagHelperContext context, TagHelperOutput output)
        output.TagName = "img";
        var src = $"/img/placeholder.png?width={Width}&height={Height ?? Width}&rmode=stretch";

        output.Attributes.SetAttribute("src", src);

At this point, you should create a folder named img under your wwwroot folder and add the placeholder image there.

Finally, let’s add our tag helper to our _ViewImports.cshtml so we can reference the new tag in all of our Razor views. Be sure to adjust the namespace to match your particular project’s namespace.

@using Bespoke
@namespace Bespoke.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Bespoke

Let’s use the placeholder tag in some Razor views.

<div class="card" style="width: 18rem;">
    <placeholder class="card-img-top" width="286" height="180" alt="Card image cap"/>
    <div class="card-body">
        <h5 class="card-title">Card title</h5>
        <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
        <a href="#" class="btn btn-primary">Go somewhere</a>

Cool! Let’s also see what it looks like rendered.

final image with aspnet core placeholder tag

If you look at the developer tools at the bottom of the screenshot, you’ll notice that each image is rendered precisely for the use case it is applied within. You also have the added benefit of caching each image, so your app stays more performant as more images are processed. Finally, all images are processed and served locally, so you can accurately represent the performance of your application profile.

Adding The Dimensions To The Image

While not completely necessary, adding the dimensions of your placeholder image can help debug layout issues faster than without them. Luckily, ImageSharp.Web has an extensible command pipeline that you can use to your advantage.

The first step is to create a DimensionsWatermarkProcessor class. This will give us an opportunity to add additional context to our original placeholder image. Before pasting this class into your project you’ll need a few additional packages in your project: SixLabors.Fonts and SixLabors.ImageSHarp.Drawing.

<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta18" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta15" />

Now, you’ll need to create a DimensionsWatermarkProcessor class. Thanks to Adam Russel for the watermark blog post. You can modify the below code to load custom fonts, change sizes of the dimensions, and the location. For this use case, I felt having the values in the bottom-right were fine.

using System.Globalization;
using Microsoft.Extensions.Options;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Web;
using SixLabors.ImageSharp.Web.Commands;
using SixLabors.ImageSharp.Web.Middleware;
using SixLabors.ImageSharp.Web.Processors;

namespace Bespoke;

public class DimensionsWatermarkProcessor : IImageWebProcessor
    public const string Dimensions = "dim";

    private static readonly IEnumerable<string> DimensionsCommands
        = new[] { Dimensions };

    private readonly ImageSharpMiddlewareOptions options;

    public FormattedImage Process(FormattedImage image, ILogger logger, CommandCollection commands, CommandParser parser,
        CultureInfo culture)
        var extension = commands.GetValueOrDefault(Dimensions);

        if (!string.IsNullOrEmpty(extension))
            var text = $"{image.Image.Width}x{image.Image.Height}";

            // substitute your own font here if you like
            var font = SystemFonts.Families.First().CreateFont(18f, FontStyle.Regular);
            var textOptions = new TextOptions(font) { Dpi = 72, KerningMode = KerningMode.Normal };
            var rect = TextMeasurer.Measure(text, textOptions);

            // add watermark
            image.Image.Mutate(x => x.DrawText(
                $"{image.Image.Width} x {image.Image.Height}",
                new Color(Rgba32.ParseHex("#FFFFFF")),
                new PointF(image.Image.Width - rect.Width - 18f,
                    image.Image.Height - rect.Height - 5f)

        return image;

    public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture)
        => false;

    public IEnumerable<string> Commands { get; } = DimensionsCommands;

    public DimensionsWatermarkProcessor(IOptions<ImageSharpMiddlewareOptions> options)
        => this.options = options.Value;

Once you have the watermark processor in your project, you’ll need to add the processor to the ImageSharp middleware pipeline. Back in your Program.cs file, you’ll need to call AddProcessor.

builder.Services.AddImageSharp(options =>
    options.BrowserMaxAge = TimeSpan.FromDays(7);
    options.CacheMaxAge = TimeSpan.FromDays(365);

Finally, let’s update the PlaceholderTagHelper implementation to include the additional dim command that the DimensionsWatermarkProcessor expects. Note, you could modify the value of dim to allow for configurability of the watermark.

[HtmlTargetElement("placeholder", TagStructure = TagStructure.WithoutEndTag)] 
public class PlaceholderTagHelper : TagHelper
    public int Width { get; set; } = 256;
    public int? Height { get; set; }
    public override void Process(TagHelperContext context, TagHelperOutput output)
        output.TagName = "img";
        var src = $"/img/placeholder.png?width={Width}&height={Height ?? Width}&rmode=stretch&dim=true";

        output.Attributes.SetAttribute("src", src);

Let’s see what the updated placeholders look like now.

final image with aspnet core placeholder tag


I hope you enjoyed this short post. As you can see, with a few bits of code, you can create a local placeholder service designed specifically for your needs. Suppose you want to go the extra mile. In that case, you can explore the ImageSharp APIs and develop processors to generate custom images and watermarks to help uniquely identify image placeholders.

Again, thank you for reading, and I hope you share this post with friends and colleagues. Also, remember to follow me on Twitter at @buhakmeh for more #dotnet news and discussions.