Build a Blazor WebAssembly Barcode and QR Code Scanner with Dynamsoft

Blazor is a powerful framework by Microsoft that enables developers to build interactive web applications using C# instead of JavaScript, bringing the full power of .NET to the browser. While Blazor allows you to write most of your web app in C#, calling existing JavaScript APIs remains an inevitable part of enhancing Blazor functionalities, especially when integrating with powerful libraries like the Dynamsoft JavaScript Barcode SDK. In this tutorial, we will show you how to build a .NET Blazor application for scanning 1D/2D barcodes by leveraging the interop between C# and JavaScript.

What you’ll build: A Blazor WebAssembly application that reads barcodes and QR codes from both static images and a live device camera stream, using Dynamsoft Capture Vision and C#–JavaScript interop.

Key Takeaways

  • Blazor WebAssembly integrates the Dynamsoft JavaScript Barcode SDK via IJSRuntime, enabling 1D/2D barcode decoding without leaving the .NET ecosystem.
  • The CaptureVisionRouter API provides a unified capture() interface for both image-file and live-camera barcode scanning.
  • JS-to-C# callbacks are wired through DotNetObjectReference, so real-time barcode results update Blazor component state directly.
  • This architecture runs in any modern browser without native plugins, making it suitable for inventory, logistics, and ID-scanning web apps.

Common Developer Questions

  • How do I build a Blazor WebAssembly barcode scanner using JavaScript interop?
  • How do I decode QR codes and barcodes from a webcam stream in a .NET Blazor app?
  • Why is the Dynamsoft WASM module slow to load on first use in Blazor WebAssembly?

See It in Action

Try the Live Demo Online

https://yushulx.me/blazor-barcode-mrz-document-scanner/

Prerequisites

  • Dynamsoft Capture Vision Bundle: This bundle includes all Dynamsoft JavaScript imaging SDKs, designed to work seamlessly together. It is available on npm.

    Dynamsoft Capture Vision Bundle

  • Dynamsoft Capture Vision Trial License: A trial license supports all features (including Barcode, MRZ, Document recognition, and more) of the Dynamsoft Capture Vision Bundle for 30 days. You can apply for a trial license here.

Step 1: Integrate Dynamsoft Capture Vision Bundle into a Blazor WebAssembly Project

  1. Create a Blazor WebAssembly Project: Start a new Blazor WebAssembly project in Visual Studio or Visual Studio Code.
  2. Add the Capture Vision Bundle Script: In the wwwroot/index.html file, include the Dynamsoft Capture Vision Bundle script:
     <script src="https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-bundle@3.0.3001/dist/dcv.bundle.min.js"></script>
    
  3. Create a JavaScript File for Interop: Create a file named jsInterop.js in the wwwroot folder and include it in the index.html:
     <script src="jsInterop.js"></script>
    
  4. Configure JavaScript to Set the License: In jsInterop.js, define a setLicense function that specifies the resource paths for the dependencies and sets the license key:
     window.jsFunctions = {
         setLicense: async function setLicense(license) {
             if (isInitialized) return true;
        
             try {
                 Dynamsoft.Core.CoreModule.engineResourcePaths.rootDirectory = "https://cdn.jsdelivr.net/npm";                
                 Dynamsoft.License.LicenseManager.initLicense(license, true);
        
                 await Dynamsoft.Core.CoreModule.loadWasm(["dbr"]);
        
                 ...
             } catch (e) {
                 alert(e);
                 return false;
             }
        
             return true;
         }
     };
    

    The loadWasm method preloads the WebAssembly module for the Dynamsoft Barcode Reader SDK. If you don’t load it ahead of time, the SDK will load it on-demand, which may cause a delay in the first barcode scanning.

  5. Update the Blazor Page to Activate the SDK: Modify the Pages/Home.razor file to include HTML and C# code that allows users to activate the SDK with a valid license key:

     @page "/"
     @inject IJSRuntime JSRuntime
        
     <PageTitle>Home</PageTitle>
        
     <p>Click <a href="https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform" target="_blank">here</a> to obtain a Dynamsoft Capture Vision Trial License.</p>
        
     <EditForm Model="@this">
         <InputText @bind-Value="LicenseKey" placeholder="Enter your license key" />
         <button type="button" class="btn btn-primary" @onclick="SetLicenseKey">Activate the SDK</button>
     </EditForm>
        
     @code {
         Boolean initialized = false;
        
         private string LicenseKey = "LICENSE-KEY";
        
         private async Task SetLicenseKey()
         {
             initialized = await JSRuntime.InvokeAsync<Boolean>("jsFunctions.setLicense", LicenseKey);
             StateHasChanged();
         }
     }
    

Step 2: Implement a Barcode Reader Razor Page

  1. Create the Barcode Reader Component: Create a new Razor component called Reader.razor in the Pages folder and add it to your navigation menu in NavMenu.razor:
     <div class="nav-item px-3">
         <NavLink class="nav-link" href="barcodereader">
             <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Barcode Reader
         </NavLink>
     </div>
    

    Ensure that the href attribute value (barcodereader) matches the path specified in the Razor component.

  2. Implement the Barcode Reader in Reader.razor: Add the following code in Reader.razor to set up the barcode reader functionality:

     @page "/barcodereader"
     @inject IJSRuntime JSRuntime
        
     <button @onclick="ReadBarcodes">Select an image</button>
     <p class="p-result">@result</p>
        
     <div id="imageview">
         <img id="@imageId" />
         <canvas id="@overlayId"></canvas>
     </div>
        
     @code {
         private String result = "";
         private DotNetObjectReference<Reader>? objRef;
         private String imageId = "image";
         private String overlayId = "overlay";
        
         protected override void OnInitialized()
         {
             objRef = DotNetObjectReference.Create(this);
         }
        
         protected override async Task OnAfterRenderAsync(bool firstRender)
         {
             if (firstRender)
             {
                 // Call the JavaScript function when the page is loaded
                 await JSRuntime.InvokeVoidAsync("jsFunctions.initReader");
             }
         }
        
        
         public async Task ReadBarcodes()
         {
             await JSRuntime.InvokeVoidAsync(
             "jsFunctions.selectFile", objRef, overlayId, imageId);
         }
        
         [JSInvokable]
         public void ReturnBarcodeResultsAsync(String text)
         {
             result = text;
             StateHasChanged();
         }
        
         public void Dispose()
         {
             objRef?.Dispose();
         }
     }
    

    Explanation

    • @page "/barcodereader" sets the route for the Razor component.
    • OnInitialized() creates a DotNetObjectReference, enabling JavaScript to call C# methods.
    • OnAfterRenderAsync() triggers the JavaScript function initReader when the page loads.
    • ReadBarcodes() invokes the JavaScript function selectFile, which opens a file dialog to select an image for barcode scanning.
    • ReturnBarcodeResultsAsync() is a C# method that JavaScript calls to return barcode results to the Blazor component.
  3. Add JavaScript Functions in jsInterop.js: Add the corresponding JavaScript functions in jsInterop.js:
     function showResults(result, dotnetRef) {
         clearOverlay();
        
         let txts = [];
         try {
             let localization;
             let items = result.items
             if (items.length > 0) {
                 for (var i = 0; i < items.length; ++i) {
        
                     if (items[i].type !== Dynamsoft.Core.EnumCapturedResultItemType.CRIT_BARCODE) {
                         continue;
                     }
        
                     let item = items[i];
        
                     txts.push(item.text);
                     localization = item.location;
        
                     drawOverlay(
                         localization,
                         item.text
                     );
                 }
        
        
             }
         } catch (e) {
             alert(e);
         }
        
         let barcoderesults = txts.join(', ');
         if (txts.length == 0) {
             barcoderesults = 'No barcode found';
         }
        
         if (dotnetRef) {
             dotnetRef.invokeMethodAsync('ReturnBarcodeResultsAsync', barcoderesults);
         }
     }
    
     function decodeImage(dotnetRef, url, data) {
         const img = new Image()
         img.onload = () => {
             updateOverlay(img.width, img.height);
             if (cvr) {
                 cvr.capture(url, 'ReadBarcodes_Balance').then((result) => {
                     showResults(result, dotnetRef);
                 });
        
             }
         }
         img.src = url
     }
    
     window.jsFunctions = {
         ...
         initReader: async function () {
             try {
                 dispose();
                 cvr = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
        
             } catch (e) {
                 console.log(e);
             }
         },
        
         selectFile: async function (dotnetRef, overlayId, imageId) {
             if (cameraEnhancer) {
                 cameraEnhancer.dispose();
                 cameraEnhancer = null;
             }
             initOverlay(document.getElementById(overlayId));
             if (cvr) {
                 let input = document.createElement("input");
                 input.type = "file";
                 input.onchange = async function () {
                     try {
                         let file = input.files[0];
                         var fr = new FileReader();
                         fr.onload = function () {
                             let image = document.getElementById(imageId);
                             image.src = fr.result;
                             image.style.display = 'block';
        
                             decodeImage(dotnetRef, fr.result, file);
                         }
                         fr.readAsDataURL(file);
        
                     } catch (ex) {
                         alert(ex.message);
                         throw ex;
                     }
                 };
                 input.click();
             } else {
                 alert("The barcode reader is still initializing.");
             }
         },
     };
    

    Explanation

    • initReader() initializes the Capture Vision Router.
    • decodeImage() decodes barcodes from the selected image.
    • showResults() displays the decoded barcode results.
    • selectFile() allows users to select an image file, which is then processed for barcode scanning.

Step 3: Implement a Barcode Scanner Razor Page

  1. Create the Barcode Scanner Component: Create a new Razor component called Scanner.razor in the Pages folder and add it to the navigation menu in NavMenu.razor:
     <div class="nav-item px-3">
         <NavLink class="nav-link" href="barcodescanner">
             <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Barcode Scanner
         </NavLink>
     </div>
    
  2. Implement the Barcode Scanner in Scanner.razor: Add the following code in Scanner.razor to set up the barcode scanner functionality:
     @page "/barcodescanner"
    
     @inject IJSRuntime JSRuntime
        
     <div class="select">
         <button @onclick="GetCameras">Get Cameras</button>
         <label for="videoSource"></label>
         <select id="@videoSourceId"></select>
         <button @onclick="StartCamera">Open Camera</button>
         <button @onclick="StopCamera">Stop Camera</button>
     </div>
        
     <div id="videoview">
         <div class="dce-video-container" id="@videoContainerId"></div>
         <canvas id="@overlayId"></canvas>
     </div>
        
     @code {
         private String result = "";
         private DotNetObjectReference<Scanner>? objRef;
         private string videoSourceId = "videoSource";
         private string overlayId = "overlay";
         private string videoContainerId = "videoContainer";
        
         protected override async Task OnAfterRenderAsync(bool firstRender)
         {
             if (firstRender)
             {
                 objRef = DotNetObjectReference.Create(this);
                 await JSRuntime.InvokeAsync<Boolean>("jsFunctions.initScanner", objRef, videoContainerId, videoSourceId, overlayId);
             }
         }
        
        
         [JSInvokable]
         public void ReturnBarcodeResultsAsync(String text)
         {
             result = text;
             StateHasChanged();
         }
        
         public void Dispose()
         {
             objRef?.Dispose();
         }
        
         public async Task GetCameras()
         {
             await JSRuntime.InvokeVoidAsync("jsFunctions.getCameras");
         }
        
         public async Task StartCamera()
         {
             await JSRuntime.InvokeVoidAsync("jsFunctions.startCamera");
         }
        
         public async Task StopCamera()
         {
             await JSRuntime.InvokeVoidAsync("jsFunctions.stopCamera");
         }
     }
        
    

    Explanation

    • @page "/barcodescanner" defines the route for the Razor component.
    • OnAfterRenderAsync() initializes the barcode scanner by calling the JavaScript function initScanner once the page is loaded.
    • GetCameras(), StartCamera(), and StopCamera() are methods that interact with JavaScript functions to manage the camera and barcode scanning operations.
  3. Add JavaScript Functions in jsInterop.js: Add the corresponding JavaScript functions in your jsInterop.js file:

     async function openCamera() {
         clearOverlay();
        
         try {
             let deviceId = videoSelect.value;
             if (cameraEnhancer && deviceId !== "") {
                 await cameraEnhancer.selectCamera(deviceId);
                 await cameraEnhancer.open();
                 cvr.startCapturing('ReadSingleBarcode');
             }
         }
         catch(e) {
             console.log(e);
         }
     }
    
     window.jsFunctions = {
         ...
         initScanner: async function (dotnetRef, videoId, selectId, overlayId) {
             let canvas = document.getElementById(overlayId);
             initOverlay(canvas);
             videoSelect = document.getElementById(selectId);
             videoSelect.onchange = openCamera;
            
             try {
                 dispose();
            
                 let cameraView = await Dynamsoft.DCE.CameraView.createInstance();
                 cameraEnhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance(cameraView);
            
                 let uiElement = document.getElementById(videoId);
                 uiElement.append(cameraView.getUIElement());
            
                 cameraView.getUIElement().shadowRoot?.querySelector('.dce-sel-camera')?.setAttribute('style', 'display: none');
                 cameraView.getUIElement().shadowRoot?.querySelector('.dce-sel-resolution')?.setAttribute('style', 'display: none');
            
                    
            
                 cvr = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
                 cvr.setInput(cameraEnhancer);
            
                 cvr.addResultReceiver({
                     onCapturedResultReceived: (result) => {
                         showResults(result, dotnetRef);
                     },
                 });
            
                 cvr.addResultReceiver({
                     onDecodedBarcodesReceived: (result) => {
                         if (!result.barcodeResultItems.length) return;
            
                     },
                 });
            
                 cameraEnhancer.on('played', () => {
                     updateResolution();
                 });
            
             } catch (e) {
                 console.log(e);
                 result = false;
             }
             return true;
         },
         getCameras: async function () {
             if (cameraEnhancer) {
                 let cameras = await cameraEnhancer.getAllCameras();
                 listCameras(cameras);
             }
         },
         startCamera: async function() {
             openCamera();
         },
         stopCamera: async function () {
             try {
                 if (cameraEnhancer) {
                     cameraEnhancer.pause();
                 }
             }
             catch (e) {
                 console.log(e);
             }
         }
     };
    

    Explanation

    • initScanner() initializes the camera view, camera enhancer, and Capture Vision Router, and registers callback functions to handle barcode results.
    • getCameras() retrieves the list of available cameras.
    • startCamera() opens the selected camera and starts barcode scanning.
    • stopCamera() stops the camera preview, halting any ongoing image processing.

Step 4: Run the Blazor Barcode Reader and Scanner Application

To launch your Blazor application, press F5 in Visual Studio or use your preferred method to start debugging. Once the application is running, you can access the barcode reader and scanner pages to scan barcodes and QR codes effortlessly.

  • Barcode Reader: Use the barcode reader page to select image files for barcode detection.

    Blazor WebAssembly barcode and QR code reader

  • Barcode Scanner: The barcode scanner page allows for real-time scanning using your device’s camera, making it easy to detect barcodes and QR codes.

    Blazor WebAssembly barcode and QR code scanner

Common Issues & Edge Cases

  • WASM module loads slowly on first scan: Calling CoreModule.loadWasm(["dbr"]) in the setLicense function preloads the WebAssembly binary ahead of time. Skipping this causes the SDK to download on-demand, adding a 3–5 second delay on the first barcode capture.
  • Camera blocked on non-HTTPS origins: CameraEnhancer requires a secure context (https:// or localhost). Deploying to plain HTTP will prevent camera access. Always develop on localhost and serve over HTTPS in production.
  • No results from blurry or low-resolution images: Switch from ReadSingleBarcode to ReadBarcodes_Balance in the cvr.capture() call to enable multi-pass decoding, which handles low-resolution and slightly blurry inputs more reliably.

Source Code

https://github.com/yushulx/blazor-barcode-mrz-document-scanner