Updated on

QRCoder generates QR codes in C# from a single NuGet package with no third-party dependencies. A QRCodeGenerator encodes our text into a QRCodeData grid, and a renderer turns that grid into a PNG, an SVG, or one of several other formats.

To download the source code for this article, you can visit our GitHub repository.

The renderer is the one choice that can break a deployment. PngByteQRCode and SvgQRCode write their own bytes and run anywhere .NET runs, Linux containers included, while the older QRCode class returns a System.Drawing.Bitmap and works only on Windows.

What Is a QR Code?

A QR code is a two-dimensional barcode. “QR” is short for Quick Response, and the name is the design goal: a scanner reads the whole grid at once instead of sweeping a line across it, and the code decodes quickly and from any angle.

The grid is built from modules, the individual dark and light squares. Large squares in three corners tell a scanner where the code is and which way up it is, and the rest mostly carries the data and its error correction.

Whatever a QR code holds, it usually holds as text. A URL, a phone number, a WiFi password and a payment request are all strings, and what makes a scanner act on one rather than just display it is the prefix it starts with, such as tel: for a phone number.

Two properties decide how much text fits. The version is the size of the grid, and the error correction level is how much of the code can be damaged and still read.

Denso Wave, the company that released the QR code in 1994, explains those corner squares on its What is a QR Code page: “QR Code accomplishes this task through position detection patterns located at the three corners of the symbol.” The task is reading the code from any direction.

How Do We Add QRCoder to an ASP.NET Core Project?

QRCoder is a single NuGet package with no third-party dependencies, so adding it is one dotnet add package QRCoder and one using QRCoder.

Generating a code then takes two objects. A QRCodeGenerator turns our text, or a payload object, into a QRCodeData. That is only the module matrix, with no image format or color attached yet. A renderer takes the matrix and produces something we can actually show.

In an ASP.NET Core MVC application the renderer we want is PngByteQRCode, which hands back a byte[] of PNG data and needs no drawing library underneath it.

From there we have two ways to get it onto the page. We can return the bytes from a controller action as a FileResult, or we can Base64-encode them into a data: URI and drop that straight into an img tag. This article takes the second route, because it keeps the whole example inside one action and one view.

First off, let’s create an ASP.NET Core Web Application (Model-View-Controller) called QRCodeGeneratorWebSample, and then add the QRCoder library to it:

dotnet new mvc -n QRCodeGeneratorWebSample
cd QRCodeGeneratorWebSample
dotnet add package QRCoder

Next, let’s add a new record in the Models folder:

namespace QRCodeGeneratorWebSample.Models;

public record HomeModel(Dictionary<string, string> QRCodes);

We will use this record to hold the encoded QR Codes, which we can then display in our view.

Let’s update the Home/Index view to display our QR codes:

@model HomeModel

<div class="text-center">
    @foreach (var qr in Model.QRCodes)
    {
        <hr/>
        <h2>@qr.Key</h2>
        <img width="250px" src="@qr.Value" />
    }
</div>

Finally, let’s modify the HomeController class, together with the using directives at the top of its file:

using Microsoft.AspNetCore.Mvc;
using QRCodeGeneratorWebSample.Models;
using QRCoder;
using static QRCoder.PayloadGenerator;

namespace QRCodeGeneratorWebSample.Controllers;

public class HomeController : Controller
{
    private readonly QRCodeGenerator qrGenerator = new();

    public IActionResult Index()
    {
        var qrCodes = new Dictionary<string, string>
        {
            { "Basic String", GenerateQRCodeString() }
        };

        var model = new HomeModel(qrCodes);

        return View(model);
    }

    private string GenerateQRCodeString()
    {
        var qrCodeData = qrGenerator.CreateQrCode("Hello CodeMaze readers :)", QRCodeGenerator.ECCLevel.Q);

        return GeneratePng(qrCodeData);
    }

    private static string GeneratePng(QRCodeData data)
    {
        using var qrCode = new PngByteQRCode(data);
        var qrCodeImage = qrCode.GetGraphic(20);

        return $"data:image/png;base64,{Convert.ToBase64String(qrCodeImage)}";
    }
}

Here we are creating a QR code that holds a simple string, and rendering it on the page as a PNG image. GeneratePng() gets the image into the view by Base64-encoding the PNG bytes into a data: URI.

Only the renderer sits in a using declaration. Disposing a QRCoder renderer also disposes the QRCodeData we passed to it, so the matrix needs no using of its own. The using static directive is there for later: it lets us write Url and PhoneNumber without the PayloadGenerator. prefix, because both are nested classes of PayloadGenerator.

For now, let’s ignore the parameters of the CreateQrCode() and the GetGraphic() methods, as we’ll cover those a bit later.

Now, let’s run our app and see our first QR Code displayed on the page:

Basic String QR Code

Which QRCoder Renderer Should We Use?

The QRCoder package ships nine renderers, with two more in companion packages, and the only choice that can actually break something is whether the one we pick needs Windows.

PngByteQRCode and SvgQRCode need nothing at all. They build the PNG and SVG bytes themselves, so they run unchanged in a Linux container, on macOS, and on either flavor of Azure App Service.

QRCode and ArtQRCode are the two that need Windows. Both return a System.Drawing.Bitmap, and System.Drawing.Common has been Windows-only since .NET 6. Both carry [SupportedOSPlatform("windows")], so calling them still compiles, with a CA1416 warning, and then throws at runtime on Linux.

Use PngByteQRCode when the output is an image on a page, in an email, or in an API response. Use SvgQRCode when the code will be printed or resized. Treat QRCode as a desktop class that a web application has no reason to call.

Here is every renderer QRCoder offers, with what it returns and what it needs:

RendererOutputNeeds Windows?Typical use
PngByteQRCodePNG byte[]NoWeb pages, emails, anything returned from an API
SvgQRCodeSVG stringNoPrint, and anywhere the code must scale
BitmapByteQRCodeBMP byte[]NoLegacy consumers that insist on BMP
PdfByteQRCodePDF byte[]NoA QR code as its own document
Base64QRCodeBase64 stringOnly for JPEG, GIF or an iconStraight into an <img src> data URI
AsciiQRCodeASCII art stringNoConsole output and tests
PostscriptQRCodePostScript / EPS stringNoPrepress and label printers
QRCodeSystem.Drawing.BitmapYesWindows desktop applications only
ArtQRCodeSystem.Drawing.Bitmap with dot-shaped modulesYesWindows desktop applications only
XamlQRCodeXAML DrawingImageWPFNeeds the separate QRCoder.Xaml package
UnityQRCodeUnity Texture2DUnityNeeds the separate QRCoder.Unity package

Only the two Windows rows carry the platform attribute on the class itself in the compiled QRCoder.dll, and the other seven renderers carry it, if at all, only on a few options built on System.Drawing.Common. On Linux, the failure the attribute warns about is the one Microsoft documents for System.Drawing.Common on non-Windows systems: a TypeInitializationException with a PlatformNotSupportedException inside it.

All of these renderers start from the same QRCodeData:

Diagram of the QRCoder pipeline: a payload or string goes into QRCodeGenerator, which produces a QRCodeData module matrix, which four renderers turn into PNG bytes, SVG text, console text, and a Windows-only Bitmap.

The encoding happens once, in QRCodeGenerator, and the renderer is a late choice we can swap without touching it.

To see that in code, let’s render our basic string a second time, as SVG:

private string GenerateQRCodeSvg()
{
    var qrCodeData = qrGenerator.CreateQrCode("Hello CodeMaze readers :)", QRCodeGenerator.ECCLevel.Q);
    using var qrCode = new SvgQRCode(qrCodeData);
    var svg = qrCode.GetGraphic(20);

    return $"data:image/svg+xml,{Uri.EscapeDataString(svg)}";
}

SvgQRCode.GetGraphic() returns the SVG markup as a string, so instead of Base64-encoding bytes, we percent-encode the text with Uri.EscapeDataString() into a data:image/svg+xml URI. The img tag in our view shows it like any other image.

Let’s add it to our Index() method:

public IActionResult Index()
{
    var qrCodes = new Dictionary<string, string>
    {
        { "Basic String", GenerateQRCodeString() },
        { "SVG", GenerateQRCodeSvg() }
    };

    var model = new HomeModel(qrCodes);

    return View(model);
}

When we run the app again, the SVG code shows the same modules as the PNG code above it, because both renderers draw the same matrix. The difference appears when we scale them: the SVG stays sharp at any size, which is why it is the better choice for print.

How Do QRCoder Payload Generators Work?

Every QR code in this article holds a string. A payload generator is a class that builds one of the specially formatted strings a scanner knows how to act on, so we do not have to remember the format ourselves.

They all live as nested classes of PayloadGenerator and they all derive from Payload, whose real job is to override ToString(). new Url("https://www.code-maze.com") produces a string a browser opens, and new PhoneNumber("+123456789") produces one a phone dials.

CreateQrCode() has an overload that takes a Payload directly, so the payload object never has to be converted by hand.

QRCoder ships more than twenty of these. They cover WiFi credentials, vCard contacts, calendar events, SEPA and Swiss payment codes, and the otpauth:// URIs that two-factor authenticator applications scan. Writing our own is the same small exercise: derive from Payload and override ToString(), which is exactly what the custom generator below does.

The OneTimePassword payload, for instance, builds the same kind of otpauth://totp/ URI that our article on two-factor authentication with Google Authenticator formats by hand.

Generating QR Codes for URLs

One of the most common use cases of QR Codes is navigating to a web page. For instance, if we wanted readers of our printed materials to fill out a survey, it would be more convenient to provide a QR code for scanning instead of a long URL that would have to be typed.

So, let’s add a new method to generate a URL:

private string GenerateQRCodeURL()
{
    var qrCodeData = qrGenerator.CreateQrCode(new Url("https://www.code-maze.com"));

    return GeneratePng(qrCodeData);
}

This Url class derives from the Payload class we mentioned earlier, and takes a URL as an argument.

Let’s update our Index() method:

public IActionResult Index()
{
    var qrCodes = new Dictionary<string, string>
    {
        { "Basic String", GenerateQRCodeString() },
        { "SVG", GenerateQRCodeSvg() },
        { "URL", GenerateQRCodeURL() }
    };

    var model = new HomeModel(qrCodes);

    return View(model);
}

Finally, let’s run our app and scan the QR Code for the expected URL:

URL QR Code

It’s worth noting that while the URL in the QR Code will be “static” (i.e. unchangeable), we can always dynamically redirect if needed on our webserver. For example, we can generate a URL that points to an endpoint on our web server (e.g. https://mysite.com/qr-code-campaign), but configure that endpoint to redirect to another resource.

Generating QR Codes for Phone Numbers

Phone numbers are another typical use case for QR Codes, especially when aimed at mobile devices. A classic example would be a business card with a QR code on it. Instead of typing the phone number, consumers can scan the code to be taken to their call app.

First, let’s add the GenerateQRCodePhoneNumber() method:

private string GenerateQRCodePhoneNumber()
{
    var qrCodeData = qrGenerator.CreateQrCode(new PhoneNumber("+123456789"));

    return GeneratePng(qrCodeData);
}

We use the PhoneNumber class which derives from the Payload class, and provide the phone number.

Let’s update our Index() method:

public IActionResult Index()
{
    var qrCodes = new Dictionary<string, string>
    {
        { "Basic String", GenerateQRCodeString() },
        { "SVG", GenerateQRCodeSvg() },
        { "URL", GenerateQRCodeURL() },
        { "Phone Number", GenerateQRCodePhoneNumber() }
    };

    var model = new HomeModel(qrCodes);

    return View(model);
}

Let’s now scan the code to reveal the embedded phone number:

Reading our phone number QR code

We can then attempt to call or save the number. This is how easy it is to set up these calls to action.

Building a Custom QR Code Generator

Creating a custom payload generator is straightforward. We just need to create a class derived from the Payload class, which we can place in HomeController.cs, below the controller, so it shares the file’s using directives:

public class CustomPayload(string name) : Payload
{
    private readonly string _name = name;

    public override string ToString()
    {
        return $"Hello {_name}, hope you're having a great time :)";
    }
}

Now, let’s create a method to generate our custom QR Code:

private string GenerateQRCodeCustom()
{
    var qrCodeData = qrGenerator.CreateQrCode(new CustomPayload("Reader"));

    return GeneratePng(qrCodeData);
}

Lastly, let’s add our GenerateQRCodeCustom() method to the Index() method:

public IActionResult Index()
{
    var qrCodes = new Dictionary<string, string>
    {
        { "Basic String", GenerateQRCodeString() },
        { "SVG", GenerateQRCodeSvg() },
        { "URL", GenerateQRCodeURL() },
        { "Phone Number", GenerateQRCodePhoneNumber() },
        { "Custom", GenerateQRCodeCustom() }
    };

    var model = new HomeModel(qrCodes);

    return View(model);
}

Now, let’s scan our code to see our embedded text:

Reading our custom quick-response code

There are a lot more built-in payload generators that QRCoder supplies. We can’t cover all of them here, so be sure to check out the extensive documentation at the QRCoder Wiki.

How Do We Store and Reuse a Generated QR Code?

Generating a QR code on every request is wasted work, because the same input always produces the same grid. The fix is to generate once and keep the result.

QRCodeData exposes GetRawData(), which serializes the module matrix into a byte[], and a constructor that takes those bytes straight back. So the matrix survives a round trip through a cache, a database column, or a file on disk.

Keeping the matrix is usually better than keeping the finished image. The matrix can still be rendered at any size, in any colors, as PNG or as SVG, without repeating the encoding work.

Storing the rendered image instead is perfectly defensible when the size and the colors are fixed and never change. What is not defensible is calling CreateQrCode() on every page load for a code whose contents have not moved since it was created.

In a real application, we would usually keep these bytes in an IMemoryCache or in a database. To keep the example small, we store them in a class of our own.

Writing QR Codes

To demonstrate this concept, let’s create a QrCodesDb class to hold the QR codes in memory:

using System.Collections.Concurrent;
using QRCoder;

namespace QRCodeGeneratorWebSample;

public class QrCodesDb
{
    private readonly ConcurrentDictionary<string, byte[]> _qrCodes = new();

    public void Add(string key, QRCodeData data)
    {
        _qrCodes.TryAdd(key, data.GetRawData(QRCodeData.Compression.Uncompressed));
    }

    public byte[]? Get(string key)
    {
        _qrCodes.TryGetValue(key, out var value);

        return value;
    }
}

The QRCodeData class has a method GetRawData() that provides us with access to the underlying raw data before rendering. Its argument picks Uncompressed, Deflate or GZip. For our custom payload, Deflate shrinks the 216 raw bytes to 189.

We register QrCodesDb as a singleton, so every request shares one instance. That is why it stores the bytes in a ConcurrentDictionary and adds them with TryAdd(): a plain Dictionary is not safe when two requests write to it at the same time.

Let’s register our QrCodesDb class in Program.cs, which also needs a using directive for its namespace:

using QRCodeGeneratorWebSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<QrCodesDb>();

Reading QR Codes

Our controller now needs the database, so let’s take it as a primary constructor parameter of HomeController:

public class HomeController(QrCodesDb db) : Controller

Now, let’s modify our GenerateQRCodeCustom() method:

private string GenerateQRCodeCustom()
{
    const string key = "Custom";

    var raw = db.Get(key);
    if (raw == null)
    {
        var qrCodeData = qrGenerator.CreateQrCode(new CustomPayload("Reader"));
        db.Add(key, qrCodeData);
        raw = qrCodeData.GetRawData(QRCodeData.Compression.Uncompressed);
    }

    return GeneratePng(new QRCodeData(raw, QRCodeData.Compression.Uncompressed));
}

Here, we are using the data from our in-memory database.

By adopting this approach instead of the previous on-the-fly code generation approach, we may generate data once, store it, and then render it as much as desired.

What Settings Control QR Code Size and Reliability?

Three settings change what comes out, and two of them change the size of the grid.

The error correction level decides how much damage a code survives. Level L restores 7 percent of the code, M 15 percent, Q 25 percent and H 30 percent. The cost is capacity: the same text at level H usually needs a bigger grid than at level L, because the redundancy has to go somewhere. In our test, the bigger grid took a little over a millisecond longer to encode.

The version is that grid size, numbered 1 to 40, with each step adding four modules per side. Left alone, QRCoder picks the smallest version that fits, which is almost always right. Ask for a version too small for the data and it throws rather than truncating.

Colors are a rendering setting, not an encoding one. A scanner has to tell the modules from the background, and Denso Wave’s advice is black on white.

That millisecond comes from encoding the same 300-character string 200 times per level: level L produced a version 11 grid at 2.71 ms per encode and level H a version 18 grid at 4.13 ms. A difference that small is not a reason to pick one level over another.

Error Correction

Starting with the error correction level, a parameter of the CreateQrCode() method:

QRCodeGenerator.ECCLevel.Q

What does ECCLevel.Q mean? ECC stands for Error Correction Code. QR Codes are meant to be robust, ensuring that they can be read by a range of devices even in the presence of physical damage. There is an underlying algorithm that goes into how this works, but we need to be aware that we can control this tolerance level:

  • Level L: 7% restoration
  • Level M: 15% restoration
  • Level Q: 25% restoration
  • Level H: 30% restoration

Each level corresponds to how much the algorithm can restore the QR code. For example, a level of “M” means that if 15% (or less) of the QR code is damaged, the QR code will still be readable. Therefore, we would use a higher level based on the damage we expect our code to sustain. If we are printing a business card, for instance, we would want to use level H as it can withstand damages that may occur in a wallet. However, for events where we don’t anticipate significant damage, we can choose a lower level.

Version

We can also specify a version of our quick-response code. There are forty versions of a standard QR code, numbered 1 to 40, each with a different module configuration. A module corresponds to a single black or white dot that makes up the QR code, and a module configuration specifies how many modules can exist. The higher the version we specify, the larger the number of modules that will be used, and the greater the data that is accommodated. The version we need depends on how much data we encode, its type, and the error correction level. In other words, the larger the data we need to encode, the more modules required, and therefore the higher version.

Choosing the right version is aided by understanding the type of data we want to save, how large the data is, and the necessary error correction. We can use the information on qrcode.com to help us make this decision.

Once we choose a version, we need to provide it in the requestedVersion parameter of the CreateQrCode() method:

var qrCodeData = qrGenerator.CreateQrCode("Hello CodeMaze readers :)", QRCodeGenerator.ECCLevel.Q, requestedVersion: 3);

QRCoder honors a version larger than the data needs and builds the bigger grid. A version too small for the data throws a DataTooLongException that names the byte limit for the chosen version and level.

Colors

So far, our QR codes have been black on white, which is the default, but we can change the colors. Let’s say our brand colors are red and dark blue. We can pass our colors as parameters into the GetGraphic() method in our GeneratePng() helper method:

var qrCodeImage = qrCode.GetGraphic(20, [255, 0, 0], [0, 0, 139]);

The 20 is each module’s size in pixels, the first array is the color of the dark modules, and the second is the background. In the sample repository, this call lives in its own GeneratePngBranded() method, so the other codes stay black on white.

Finally, let’s run our app to see the color change:

Controlling Colors

This pair puts the brighter color on the modules, which is the reverse of black on white. The QR Code FAQ from Denso Wave recommends black on a white background and warns that some color pairs cannot be read at all, so we should test a colored code with real scanners before we print it.

Conclusion

In this article, we’ve looked at generating QR Codes with the .NET library QRCoder. We have looked at how to set it up and go into the details of how to use payload generators, store the codes, and retrieve them later.

We’ve also looked at a few parameters we can set to customize our quick-response codes to make them more damage-tolerant, expand their data storage capacity, and make them more visually appealing.

Tested with .NET 10.0.10 and QRCoder 1.8.0.