Intermediate

JavaScript Interop in ASP.NET Core Blazor

Call JavaScript from C# and C# from JavaScript, with error handling and advanced interop patterns.

Technology: ASP.NET Core 8, Blazor Web App, .NET 8 Demo Application: WeatherApp (OpenWeatherMap API integration) Level: Intermediate | Estimated Duration: ~2h30


Table of Contents

  1. Introduction to JavaScript Interop
  2. Calling JavaScript from C#
  3. Calling C# from JavaScript
  4. Error Handling and Debugging
  5. Complete Application — WeatherApp
  6. Advanced Patterns
  7. Best Practices and Common Pitfalls
  8. Practical Exercises
  9. Glossary

1. Introduction to JavaScript Interop

1.1 Why JavaScript Interop?

Blazor allows you to build web applications with C#, but the browser remains a fundamentally JavaScript environment. Some features are only available via JavaScript:

flowchart LR
    subgraph CSharp["What C# can do alone"]
        C1[Business logic]
        C2[HTTP API calls]
        C3[Validation]
        C4[Model manipulation]
    end
    
    subgraph JavaScript["What requires JavaScript Interop"]
        J1["Browser APIs\n(Geolocation, Camera, Clipboard)"]
        J2["Existing JS libraries\n(Chart.js, Leaflet, Three.js)"]
        J3["Direct DOM manipulation\n(Focus, complex animations)"]
        J4["Local storage\n(localStorage, sessionStorage)"]
        J5["Recent browser APIs\n(Notifications, WebRTC, etc.)"]
    end
    
    Bridge[JavaScript\nInterop\nIJSRuntime] --> CSharp
    Bridge --> JavaScript

1.2 How JavaScript Interop Works

sequenceDiagram
    participant Blazor as Blazor Component (C#)
    participant JSRuntime as IJSRuntime
    participant JS as JavaScript

    Note over Blazor,JS: C# → JavaScript call
    Blazor->>JSRuntime: InvokeVoidAsync("functionName", args)
    JSRuntime->>JS: Call via interop channel
    JS->>JS: Execute function
    JS-->>JSRuntime: Result (optional)
    JSRuntime-->>Blazor: Returned value

    Note over Blazor,JS: JavaScript → C# call
    JS->>JS: DotNet.invokeMethodAsync("Assembly", "MethodName")
    JS-->>Blazor: Call to [JSInvokable] method
    Blazor->>Blazor: Execute C# method
    Blazor-->>JS: Result

1.3 Key Limitations

LimitationDescriptionWorkaround
TimingThe DOM must be rendered before calling JSUse OnAfterRenderAsync(firstRender)
CouplingMixing C# + JS = difficult to maintain codeCentralize JS calls in services
SecurityExternal JS code can be maliciousValidate sources, CSP headers
DebuggingErrors crossing both languagesUse debugging tools in both environments
PerformanceEach call = one interop round-tripMinimize calls, use modules

2. Calling JavaScript from C#

2.1 IJSRuntime — Fundamentals

IJSRuntime is the Blazor service for invoking JavaScript from C#. It is injected like any other service:

@inject IJSRuntime JSRuntime

@* Or in code-behind: *@
@code {
    [Inject] private IJSRuntime JSRuntime { get; set; } = default!;
}

Main methods:

MethodReturnUse case
InvokeVoidAsync(name, args)ValueTaskCall with no return value
InvokeAsync<T>(name, args)ValueTask<T>Call with return value
InvokeVoid(name, args)voidSynchronous (not recommended)
Invoke<T>(name, args)TSynchronous with return (not recommended)

Recommendation: Always use the async versions to avoid blocking the render thread.

2.2 Global JavaScript Functions

JavaScript exposes many global functions accessible directly:

// ─── ALERTS AND DIALOGS ──────────────────────────────────────
// alert()
await JSRuntime.InvokeVoidAsync("alert", "Message to the user");

// confirm() — returns bool
var confirmed = await JSRuntime.InvokeAsync<bool>(
    "confirm", "Are you sure you want to delete?");
if (confirmed) { /* ... */ }

// prompt() — returns string?
var userInput = await JSRuntime.InvokeAsync<string?>(
    "prompt", "Enter your name:", "Default value");

// ─── CONSOLE ─────────────────────────────────────────────────
await JSRuntime.InvokeVoidAsync("console.log", "Debug from C#");
await JSRuntime.InvokeVoidAsync("console.error", "Error from C#");
await JSRuntime.InvokeVoidAsync("console.warn", "Warning message");

// ─── NAVIGATION ──────────────────────────────────────────────
// Redirect
await JSRuntime.InvokeVoidAsync("window.location.replace", "https://example.com");

// Open in a new tab
await JSRuntime.InvokeVoidAsync("window.open", 
    "https://example.com", "_blank");

// ─── CLIPBOARD ───────────────────────────────────────────────
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", 
    "Text copied to clipboard");

// ─── LOCALSTORAGE ────────────────────────────────────────────
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "theme", "dark");
var theme = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", "theme");
await JSRuntime.InvokeVoidAsync("localStorage.removeItem", "theme");

// ─── FOCUS ───────────────────────────────────────────────────
// Focus an element by ID
await JSRuntime.InvokeVoidAsync("document.getElementById('myInput')?.focus");
// Or using ElementReference (recommended)
await JSRuntime.InvokeVoidAsync("eval", 
    "document.getElementById('searchInput').focus()");

// ─── SCROLL ──────────────────────────────────────────────────
await JSRuntime.InvokeVoidAsync("window.scrollTo", 0, 0); // Top of page

2.3 Custom JavaScript Functions

Recommended structure:

wwwroot/
├── js/
│   ├── weather.js          ← ES6 module for weather
│   ├── notifications.js    ← ES6 module for notifications
│   └── mapUtils.js         ← ES6 module for maps
└── index.html

Two approaches:

Option 1 — Global functions (window.xxx):

// wwwroot/js/custom.js — Referenced in index.html
// <script src="js/custom.js"></script>

window.checkWeather = function(cityName) {
    // JavaScript logic
    return `Weather for ${cityName}`;
};

window.showToast = function(message, type = 'info') {
    // Create a Bootstrap toast
    const toast = document.createElement('div');
    toast.className = `toast bg-${type}`;
    toast.textContent = message;
    document.getElementById('toastContainer').appendChild(toast);
    new bootstrap.Toast(toast).show();
};
// Call from C#
var result = await JSRuntime.InvokeAsync<string>("checkWeather", "London");
await JSRuntime.InvokeVoidAsync("showToast", "Operation successful!", "success");

Option 2 — ES6 Modules (recommended):

// wwwroot/js/weather.js — ES6 module (no window.xxx)

export async function checkWeather(cityName, apiKey) {
    const url = `https://api.openweathermap.org/data/2.5/weather` +
                `?q=${cityName}&appid=${apiKey}&units=metric&lang=en`;
    
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error(`HTTP error ${response.status}`);
    }
    return await response.json();
}

export function showNotification(title, message, type = 'info') {
    // Create notification via browser API
    if ('Notification' in window && Notification.permission === 'granted') {
        new Notification(title, { body: message });
    }
}
// Load and use an ES6 module in C#
private IJSObjectReference? _weatherModule;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        // Import the ES6 module (relative to wwwroot)
        _weatherModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
            "import", "./js/weather.js");
    }
}

private async Task<WeatherData?> GetWeatherAsync(string city)
{
    if (_weatherModule is null) return null;
    
    return await _weatherModule.InvokeAsync<WeatherData>("checkWeather", city, _apiKey);
}

// Always release modules
public async ValueTask DisposeAsync()
{
    if (_weatherModule is not null)
        await _weatherModule.DisposeAsync();
}

2.4 ElementReference — Reference to a DOM Element

@* Access a DOM element directly *@
<input @ref="_searchInput" type="text" @bind="_searchTerm" class="form-control" />
<button @onclick="FocusSearch">Focus search</button>

@code {
    private ElementReference _searchInput;
    private string _searchTerm = string.Empty;
    
    private async Task FocusSearch()
    {
        // Give focus to the DOM element
        await _searchInput.FocusAsync();
        // Or via JSRuntime for more control:
        await JSRuntime.InvokeVoidAsync("focusElement", _searchInput);
    }
}
// JS function receiving an ElementReference
window.focusElement = function(element) {
    element.focus();
    element.select(); // Also select the text
};

2.5 Asynchronous Calls with Return Values

// Retrieve data from a third-party API via JavaScript
private WeatherResponse? _weatherData;

private async Task LoadWeatherAsync(string cityName)
{
    _isLoading = true;
    _error = null;
    
    try
    {
        // JavaScript call returning a complex object
        // The JSON is automatically deserialized to the requested C# type
        _weatherData = await JSRuntime.InvokeAsync<WeatherResponse>(
            "checkWeather", cityName);
    }
    catch (JSException jsEx)
    {
        // JavaScript error
        _error = $"JavaScript error: {jsEx.Message}";
    }
    catch (Exception ex)
    {
        _error = $"Unexpected error: {ex.Message}";
    }
    finally
    {
        _isLoading = false;
    }
}

DTOs for the JavaScript response:

// DTOs/WeatherResponse.cs — Matches the JSON structure of the API
public class WeatherResponse
{
    [JsonPropertyName("name")]
    public string CityName { get; set; } = string.Empty;
    
    [JsonPropertyName("coord")]
    public Coordinates? Coordinates { get; set; }
    
    [JsonPropertyName("weather")]
    public WeatherInfo[]? Weather { get; set; }
    
    [JsonPropertyName("main")]
    public MainMetrics? Main { get; set; }
    
    [JsonPropertyName("sys")]
    public SystemInfo? Sys { get; set; }
}

public class WeatherInfo
{
    [JsonPropertyName("description")]
    public string Description { get; set; } = string.Empty;
    
    [JsonPropertyName("icon")]
    public string Icon { get; set; } = string.Empty;
}

public class MainMetrics
{
    [JsonPropertyName("temp")]
    public double Temperature { get; set; }
    
    [JsonPropertyName("feels_like")]
    public double FeelsLike { get; set; }
    
    [JsonPropertyName("humidity")]
    public int Humidity { get; set; }
}

3. Calling C# from JavaScript

3.1 Static Methods with [JSInvokable]

To call a C# method from JavaScript, two conditions must be met:

  1. The method must be decorated with [JSInvokable]
  2. Use DotNet.invokeMethodAsync("AssemblyName", "MethodName", ...args) in JavaScript
// In a Blazor component
[JSInvokable]
public static async Task ShowFavoriteButton()
{
    // This method can be called from JavaScript
    // ⚠️ Must be STATIC for invocation without an instance
    _isShowFavorite = true;
    // Note: StateHasChanged() does not work in static methods
}

[JSInvokable]
public static async Task UpdateCityName(string cityName)
{
    _selectedCity = cityName;
}
// JavaScript — Call to a static C# method
async function afterWeatherLoaded(cityName) {
    // DotNet.invokeMethodAsync("AssemblyName", "MethodName", ...args)
    await DotNet.invokeMethodAsync("WeatherApp", "ShowFavoriteButton");
    await DotNet.invokeMethodAsync("WeatherApp", "UpdateCityName", cityName);
}

// Synchronous call (less recommended)
DotNet.invokeMethod("WeatherApp", "ShowFavoriteButton");

3.2 Instance Methods with DotNetObjectReference

To call non-static methods on a specific instance:

// Components/Pages/Home.razor.cs
public partial class Home : IAsyncDisposable
{
    [Inject] private IJSRuntime JSRuntime { get; set; } = default!;
    
    // Reference to pass to JavaScript
    private DotNetObjectReference<Home>? _dotNetRef;
    
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Create the reference
            _dotNetRef = DotNetObjectReference.Create(this);
            
            // Pass the reference to JavaScript
            await JSRuntime.InvokeVoidAsync("setupInterop", _dotNetRef);
        }
    }
    
    // Method callable from JavaScript (non-static)
    [JSInvokable]
    public async Task FavoriteCity(string cityName)
    {
        // Save to localStorage
        await JSRuntime.InvokeVoidAsync("localStorage.setItem", 
            $"favorite_{cityName}", cityName);
        
        // Update Blazor UI
        _favoriteCities.Add(cityName);
        StateHasChanged(); // ← Possible because it's an instance method
    }
    
    [JSInvokable]
    public void OnLocationReceived(double latitude, double longitude)
    {
        _userLocation = (latitude, longitude);
        StateHasChanged();
    }
    
    // IMPORTANT: Release the reference
    public async ValueTask DisposeAsync()
    {
        _dotNetRef?.Dispose();
    }
}
// wwwroot/js/app.js

let dotNetRef = null;

// Receives the reference from C#
window.setupInterop = function(ref) {
    dotNetRef = ref;
    
    // Configure event listeners that call C#
    document.getElementById('favoriteBtn')?.addEventListener('click', async () => {
        const city = document.getElementById('cityInput').value;
        await dotNetRef.invokeMethodAsync('FavoriteCity', city);
    });
};

// Geolocation — calls C# when position is known
window.requestUserLocation = function() {
    if (!navigator.geolocation) {
        console.warn("Geolocation not available");
        return;
    }
    
    navigator.geolocation.getCurrentPosition(
        async (position) => {
            await dotNetRef?.invokeMethodAsync(
                'OnLocationReceived',
                position.coords.latitude,
                position.coords.longitude
            );
        },
        (error) => console.error("Geolocation error:", error)
    );
};

4. Error Handling and Debugging

4.1 Detailed Errors in Development

// Program.cs — Configuration by environment
if (app.Environment.IsDevelopment())
{
    // Detailed error page with stack trace
    app.UseDeveloperExceptionPage();
    app.UseWebAssemblyDebugging();
}
else
{
    // Generic error page in production
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

4.2 Error Boundaries — Isolating Errors

@* Wrapping a component that might crash *@
<ErrorBoundary>
    <ChildContent>
        @* If this component throws an exception, only this area is affected *@
        <WeatherWidget City="@_city" ApiKey="@_apiKey" />
    </ChildContent>
    <ErrorContent Context="ex">
        @* Content displayed on error *@
        <div class="alert alert-warning">
            <strong>⚠️ Weather error</strong> 
            Unable to load data.
            @if (IsDevelopment)
            {
                @* In development, show details *@
                <details class="mt-2">
                    <summary>Technical details</summary>
                    <pre class="text-danger small">@ex.Message</pre>
                </details>
            }
            <button class="btn btn-sm btn-outline-secondary mt-2" @onclick="Retry">
                Retry
            </button>
        </div>
    </ErrorContent>
</ErrorBoundary>
@* ErrorBoundary with ErrorBoundaryBase for more control *@
@inherits ErrorBoundaryBase

@if (CurrentException is null)
{
    @ChildContent
}
else
{
    <div class="error-container">
        <h4>An error occurred</h4>
        <button @onclick="Recover">Recover</button>
    </div>
}

@code {
    protected override Task OnErrorAsync(Exception exception)
    {
        // Log the error
        Logger.LogError(exception, "Error in ErrorBoundary");
        return base.OnErrorAsync(exception);
    }
}

4.3 Global Exception Handler

// Helpers/GlobalExceptionHandler.cs
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
    
    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
        => _logger = logger;
    
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);
        
        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "An internal error occurred",
            Detail = httpContext.RequestServices
                .GetRequiredService<IWebHostEnvironment>().IsDevelopment()
                ? exception.ToString()
                : null
        };
        
        httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
        
        return true; // Exception handled
    }
}

// Program.cs — Registration
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

// In the pipeline:
app.UseExceptionHandler();

4.4 Breakpoints and Debugging Types

Types of breakpoints in Visual Studio:

TypeDescriptionUsage
Line breakpointPause at a specific lineVariable inspection
Conditional breakpointPause only if condition is trueEx: cityId == 1
Exception breakpointPause when an exception is thrownFind error source
Hit count breakpointPause after N executionsBugs in loops
Data breakpointPause when a variable changesTrack state changes

Conditional breakpoint example:

// Set a breakpoint at this line
// Then right-click → Conditions → cityName == "London"
var weatherData = await _weatherModule!.InvokeAsync<WeatherResponse>(
    "checkWeather", cityName); // ← Conditional breakpoint here

4.5 Logging with Serilog

// Install packages:
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.File
// dotnet add package Serilog.Sinks.Console

// Program.cs — Serilog configuration
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .WriteTo.File(
        path: "logs/weather-app-.txt",
        rollingInterval: RollingInterval.Day,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

builder.Host.UseSerilog();
// Usage in a component
public partial class WeatherPage
{
    [Inject] private ILogger<WeatherPage> Logger { get; set; } = default!;
    
    private async Task LoadWeather(string city)
    {
        Logger.LogInformation("Loading weather for {City}", city);
        
        try
        {
            var data = await WeatherService.GetAsync(city);
            Logger.LogDebug("Weather received: {Temp}°C in {City}", data.Temperature, city);
        }
        catch (HttpRequestException ex)
        {
            Logger.LogError(ex, "Network error loading weather for {City}", city);
            _error = "Unable to load weather. Check your connection.";
        }
    }
}

5. Complete Application — WeatherApp

5.1 Application Architecture

flowchart TB
    subgraph Blazor["Blazor WeatherApp"]
        Home[Home.razor\nMain page]
        Favorites[Favorites.razor\nFavorite cities]
        NavMenu[NavMenu.razor]
    end
    
    subgraph JS["JavaScript (wwwroot/js/)"]
        Custom[custom.js\nmain functions]
        Storage[storage.js\nlocalStorage helpers]
    end
    
    subgraph External["External APIs"]
        OWM[OpenWeatherMap API\nhttps://api.openweathermap.org]
    end
    
    Home -->|"IJSRuntime"| Custom
    Custom -->|"fetch()"| OWM
    OWM -->|"JSON response"| Custom
    Custom -->|"WeatherResponse"| Home
    Favorites -->|"localStorage"| Storage

5.2 Complete Implementation

@* Components/Pages/Home.razor *@
@page "/weather"
@rendermode InteractiveServer
@inject IJSRuntime JSRuntime
@inject ILogger<Home> Logger
@implements IAsyncDisposable

<PageTitle>Weather — WeatherApp</PageTitle>

<div class="weather-container">
    <div class="search-section">
        <h1>🌤️ Real-Time Weather</h1>
        
        <div class="input-group mb-3">
            <input type="text" 
                   class="form-control form-control-lg" 
                   placeholder="Enter a city name..."
                   @bind="_cityName"
                   @bind:event="oninput"
                   @onkeyup="@(e => { if (e.Key == "Enter") _ = LoadWeatherAsync(); })" />
            <button class="btn btn-primary btn-lg" @onclick="LoadWeatherAsync" 
                    disabled="@(_isLoading || string.IsNullOrWhiteSpace(_cityName))">
                @if (_isLoading)
                {
                    <span class="spinner-border spinner-border-sm me-1"></span>
                }
                Search
            </button>
        </div>
    </div>
    
    @if (_error is not null)
    {
        <div class="alert alert-danger">@_error</div>
    }
    
    @if (_weatherData is not null)
    {
        <div class="weather-card card shadow">
            <div class="card-body">
                <div class="d-flex justify-content-between align-items-start">
                    <div>
                        <h2>@_weatherData.CityName, @_weatherData.Sys?.Country</h2>
                        <p class="text-muted">@_weatherData.Weather?[0].Description</p>
                    </div>
                    @if (_weatherData.Weather?[0].Icon is not null)
                    {
                        <img src="https://openweathermap.org/img/wn/@(_weatherData.Weather[0].Icon)@@2x.png"
                             alt="Weather icon" width="80" />
                    }
                </div>
                
                <div class="row g-3 mt-2">
                    <div class="col-md-3 text-center">
                        <div class="fs-1 fw-bold text-primary">
                            @(_weatherData.Main?.Temperature.ToString("F1"))°C
                        </div>
                        <small>Temperature</small>
                    </div>
                    <div class="col-md-3 text-center">
                        <div class="fs-3">@(_weatherData.Main?.FeelsLike.ToString("F1"))°C</div>
                        <small>Feels like</small>
                    </div>
                    <div class="col-md-3 text-center">
                        <div class="fs-3">@_weatherData.Main?.Humidity%</div>
                        <small>Humidity</small>
                    </div>
                </div>
                
                @if (_isShowFavorite)
                {
                    <button class="btn btn-outline-warning mt-3" @onclick="FavoriteCurrentCity">
                        ⭐ Favorite — @_cityName
                    </button>
                }
            </div>
        </div>
    }
</div>
// Components/Pages/Home.razor.cs
public partial class Home : IAsyncDisposable
{
    [Inject] private IJSRuntime JSRuntime { get; set; } = default!;
    [Inject] private ILogger<Home> Logger { get; set; } = default!;
    
    private IJSObjectReference? _weatherModule;
    private DotNetObjectReference<Home>? _dotNetRef;
    
    private string _cityName = string.Empty;
    private WeatherResponse? _weatherData;
    private bool _isLoading;
    private bool _isShowFavorite;
    private string? _error;
    
    private const string ApiKey = "your_openweathermap_api_key";
    
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _weatherModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
                "import", "./js/weather.js");
            
            _dotNetRef = DotNetObjectReference.Create(this);
            await JSRuntime.InvokeVoidAsync("setupWeatherCallbacks", _dotNetRef);
        }
    }
    
    private async Task LoadWeatherAsync()
    {
        if (string.IsNullOrWhiteSpace(_cityName)) return;
        
        _isLoading = true;
        _error = null;
        _isShowFavorite = false;
        
        try
        {
            Logger.LogInformation("Loading weather for {City}", _cityName);
            
            _weatherData = await _weatherModule!.InvokeAsync<WeatherResponse>(
                "checkWeather", _cityName, ApiKey);
            
            _isShowFavorite = _weatherData is not null;
            Logger.LogDebug("Weather loaded: {Temp}°C in {City}", 
                _weatherData?.Main?.Temperature, _cityName);
        }
        catch (JSException ex) when (ex.Message.Contains("404"))
        {
            _error = $"City '{_cityName}' not found. Check the spelling.";
        }
        catch (JSException ex)
        {
            _error = $"API error: {ex.Message}";
            Logger.LogError(ex, "JS error loading weather for {City}", _cityName);
        }
        catch (Exception ex)
        {
            _error = "An unexpected error occurred.";
            Logger.LogError(ex, "Unexpected error for {City}", _cityName);
        }
        finally
        {
            _isLoading = false;
        }
    }
    
    private async Task FavoriteCurrentCity()
    {
        if (string.IsNullOrWhiteSpace(_cityName)) return;
        await JSRuntime.InvokeVoidAsync("localStorage.setItem", 
            $"fav_{_cityName}", _cityName);
        await JSRuntime.InvokeVoidAsync("alert", $"'{_cityName}' added to favorites!");
    }
    
    [JSInvokable]
    public static void ShowFavoriteButton() => _isShowFavStatic = true;
    private static bool _isShowFavStatic;
    
    public async ValueTask DisposeAsync()
    {
        if (_weatherModule is not null)
            await _weatherModule.DisposeAsync();
        _dotNetRef?.Dispose();
    }
}

6. Advanced Patterns

6.1 Abstraction Service for JavaScript

Encapsulate JS calls in a dedicated service:

// Services/IStorageService.cs
public interface IStorageService
{
    Task<T?> GetItemAsync<T>(string key);
    Task SetItemAsync<T>(string key, T value);
    Task RemoveItemAsync(string key);
    Task ClearAsync();
}

// Services/LocalStorageService.cs
public class LocalStorageService : IStorageService
{
    private readonly IJSRuntime _jsRuntime;
    
    public LocalStorageService(IJSRuntime jsRuntime) => _jsRuntime = jsRuntime;
    
    public async Task<T?> GetItemAsync<T>(string key)
    {
        var json = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
        return json is null ? default : JsonSerializer.Deserialize<T>(json);
    }
    
    public async Task SetItemAsync<T>(string key, T value)
    {
        var json = JsonSerializer.Serialize(value);
        await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, json);
    }
    
    public async Task RemoveItemAsync(string key)
        => await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
    
    public async Task ClearAsync()
        => await _jsRuntime.InvokeVoidAsync("localStorage.clear");
}

// Registration in Program.cs
builder.Services.AddScoped<IStorageService, LocalStorageService>();

6.2 Geolocation

// Services/IGeolocationService.cs
public interface IGeolocationService
{
    Task<(double Latitude, double Longitude)?> GetCurrentPositionAsync();
}

// Services/GeolocationService.cs
public class GeolocationService : IGeolocationService
{
    private readonly IJSRuntime _jsRuntime;
    
    public GeolocationService(IJSRuntime jsRuntime) => _jsRuntime = jsRuntime;
    
    public async Task<(double Latitude, double Longitude)?> GetCurrentPositionAsync()
    {
        try
        {
            // JavaScript call for geolocation
            var position = await _jsRuntime.InvokeAsync<GeolocationPosition?>(
                "getCurrentPosition");
            
            return position is null 
                ? null 
                : (position.Latitude, position.Longitude);
        }
        catch
        {
            return null; // User denied or geolocation not available
        }
    }
}

public class GeolocationPosition
{
    [JsonPropertyName("latitude")]
    public double Latitude { get; set; }
    
    [JsonPropertyName("longitude")]
    public double Longitude { get; set; }
}
// wwwroot/js/geolocation.js
window.getCurrentPosition = function() {
    return new Promise((resolve, reject) => {
        if (!navigator.geolocation) {
            reject(new Error("Geolocation not available"));
            return;
        }
        
        navigator.geolocation.getCurrentPosition(
            position => resolve({
                latitude: position.coords.latitude,
                longitude: position.coords.longitude
            }),
            error => reject(error),
            { timeout: 10000, maximumAge: 60000 }
        );
    });
};

7. Best Practices and Common Pitfalls

7.1 Timing — OnAfterRenderAsync vs OnInitializedAsync

// ❌ BAD — DOM not yet rendered during OnInitialized
protected override async Task OnInitializedAsync()
{
    // The DOM does not exist yet!
    await JSRuntime.InvokeVoidAsync("initMap", "mapContainer"); // FAILS
}

// ✅ GOOD — DOM available after first render
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        // The DOM is now rendered, JS can access it
        await JSRuntime.InvokeVoidAsync("initMap", "mapContainer"); // OK
    }
}

7.2 Releasing Resources — IAsyncDisposable

// ❌ BAD — Memory leak!
public class MyComponent
{
    private IJSObjectReference? _module;
    
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        _module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/app.js");
    }
    // No Dispose → the JS module stays in memory!
}

// ✅ GOOD — Proper cleanup
public class MyComponent : IAsyncDisposable
{
    private IJSObjectReference? _module;
    private DotNetObjectReference<MyComponent>? _dotNetRef;
    
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/app.js");
            _dotNetRef = DotNetObjectReference.Create(this);
            await _module.InvokeVoidAsync("setup", _dotNetRef);
        }
    }
    
    public async ValueTask DisposeAsync()
    {
        if (_module is not null)
        {
            await _module.InvokeVoidAsync("cleanup"); // JS cleanup
            await _module.DisposeAsync();
        }
        _dotNetRef?.Dispose();
    }
}

7.3 Comparison of Approaches

ApproachAdvantagesDisadvantagesRecommended for
window.xxx (global)Simple, fastPollutes global namespaceSmall scripts, prototypes
ES6 ModuleEncapsulated, modernRequires import setupProduction, libraries
Inline scriptNo dependenciesHard to debugSimple emergency cases
NuGet Blazor packageNative integrationLicensing costsReusable components

8. Practical Exercises

Exercise 1 — LocalStorage (Beginner, ~45min)

Goal: Save user preferences (dark/light theme) in localStorage.

Starter code:

@inject IJSRuntime JSRuntime
@rendermode InteractiveServer

<button @onclick="ToggleTheme">Mode: @(_isDark ? "Dark" : "Light")</button>

@code {
    private bool _isDark = false;
    
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // TODO: Read theme from localStorage
        }
    }
    
    private async Task ToggleTheme()
    {
        _isDark = !_isDark;
        // TODO: Save to localStorage
        // TODO: Apply CSS class to body via JS
    }
}

Exercise 2 — Third-Party API with ES6 Module (Intermediate, ~2h)

Goal: Create a weather page using the weather.js module.

Steps:

  1. Create wwwroot/js/weather.js with the checkWeather() function
  2. Create the WeatherResponse DTO
  3. Load the module in OnAfterRenderAsync
  4. Call the function and display the data
  5. Handle errors (unknown city, no network)

Exercise 3 — Geolocation (Advanced, ~2h)

Goal: Get the user’s GPS position and automatically display the weather.

Steps:

  1. Create wwwroot/js/geolocation.js with getCurrentPosition()
  2. Create IGeolocationService and its implementation
  3. At page load, request the user’s position
  4. Call the weather API with the coordinates
  5. Display the weather for the current location

9. Glossary

TermDefinition
DotNet (JS)JavaScript object exposed by Blazor to call C# code
DotNetObjectReference.NET reference passed to JavaScript to invoke instance methods
ElementReferenceC# reference to a Blazor DOM element (@ref)
Error BoundaryBlazor component that catches exceptions in its child components
fetch()Native JavaScript API for making HTTP requests
GlobalExceptionHandlerASP.NET Core class handling all unhandled exceptions
IAsyncDisposable.NET interface for releasing asynchronous resources
IJSRuntimeBlazor injection service for invoking JavaScript
import()Dynamic loading of an ES6 module
InvokeAsyncJavaScript call returning a value of type T
InvokeVoidAsyncJavaScript call with no return value
JSExceptionException thrown when a JavaScript error occurs
JSInvokableAttribute marking a C# method callable from JavaScript
localStoragePersistent browser storage (key/value)
ES6 ModuleModern JavaScript module format with import/export
OnAfterRenderAsyncBlazor method called after the component has been rendered in the DOM
openweathermap.orgWeather API used in the demo application
ProblemDetailsRFC 7807 format for HTTP error responses
SerilogPopular .NET structured logging library
sessionStorageBrowser storage cleared when the tab is closed
ValueTaskAsync return type optimized for operations that are often synchronous
window.xxxConvention for global JavaScript functions

Additional Resources


Summary of Patterns

┌─────────────────────────────────────────────────────────────────┐
│                   JAVASCRIPT INTEROP PATTERNS                   │
│                                                                 │
│  C# → JS:                                                       │
│  ─────────                                                      │
│  InvokeVoidAsync("fn")           → No return value             │
│  InvokeAsync<T>("fn", args...)   → Typed return value          │
│  Wait for OnAfterRenderAsync     → DOM available               │
│                                                                 │
│  JS → C#:                                                       │
│  ─────────                                                      │
│  [JSInvokable] static            → DotNet.invokeMethodAsync    │
│  [JSInvokable] instance          → DotNetObjectReference       │
│                                                                 │
│  Errors:                                                        │
│  ────────                                                       │
│  ErrorBoundary                   → UI error isolation          │
│  IExceptionHandler               → Global handling             │
│  IsDevelopment()                 → Details vs generic messages │
└─────────────────────────────────────────────────────────────────┘

Search Terms

asp.net · javascript · interop · core · blazor · web · ui · c# · .net · development · application · calling · debugging · error · errors · functions · geolocation · global · methods · patterns · resources

Interested in this course?

Contact us to book it or get a custom training plan for your team.