Skip to content

Repository files navigation

Blazorators: Blazor JavaScript interop

Thank you for perusing my Blazor C# Source Generator repository. I'd really appreciate a ⭐ if you find this interesting.

build

All Contributors

Blazorators provides C# source generators and generated packages for strongly typed Blazor JavaScript interop, targeting either IJSInProcessRuntime or IJSRuntime. The repository supports two complementary generation paths:

  • Blazor.SourceGenerators for opt-in services generated from an attributed interface and TypeScript declarations.
  • Blazor.DOM and focused capability packages generated from a strict semantic model of the browser DOM.

The repository provides several NuGet packages:

Core libraries

NuGet package NuGet version Description
Blazor.SourceGenerators NuGet Core source generator library.
Blazor.Serialization NuGet Common serialization library, required in some scenarios when using generics.

WebAssembly libraries

NuGet package NuGet version Description
Blazor.LocalStorage.WebAssembly NuGet Blazor WebAssembly class library exposing DI-ready IStorageService type for the localStorage implementation (relies on IJSInProcessRuntime).
Blazor.SessionStorage.WebAssembly NuGet Blazor WebAssembly class library exposing DI-ready IStorageService type for the sessionStorage implementation (relies on IJSInProcessRuntime).
Blazor.Geolocation.WebAssembly NuGet Razor class library exposing DI-ready IGeolocationService type (and dependent callback types) for the geolocation implementation (relies on IJSInProcessRuntime).
Blazor.SpeechSynthesis.WebAssembly NuGet Razor class library exposing DI-ready ISpeechSynthesisService type for the speechSynthesis implementation (relies on IJSInProcessRuntime).
Blazor.SpeechRecognition.WebAssembly NuGet Razor class library exposing DI-ready ISpeechRecognitionService type for the webkitSpeechRecognition implementation (relies on IJSInProcessRuntime).
Blazor.Permissions.WebAssembly NuGet Razor class library exposing DI-ready IPermissionsService type for the permissions implementation (relies on IJSInProcessRuntime).

Targets the IJSInProcessRuntime type.

Server libraries

NuGet package NuGet version Description
Blazor.LocalStorage NuGet Blazor Server class library exposing DI-ready IStorageService type for the localStorage implementation (relies on IJSRuntime)
Blazor.SessionStorage NuGet Blazor Server class library exposing DI-ready IStorageService type for the sessionStorage implementation (relies on IJSRuntime)
Blazor.Geolocation NuGet Razor class library exposing DI-ready IGeolocationService type (and dependent callback types) for the geolocation implementation (relies on IJSRuntime).
Blazor.SpeechSynthesis NuGet Razor class library exposing DI-ready ISpeechSynthesisService type for the speechSynthesis implementation (relies on IJSRuntime).
Blazor.SpeechRecognition NuGet Razor class library exposing DI-ready ISpeechRecognitionService type for the webkitSpeechRecognition implementation (relies on IJSRuntime).

Targets the IJSRuntime type.

Legacy package note
The source-generator package pairs listed above ship separately for WebAssembly (IJSInProcessRuntime) and Server (IJSRuntime). Both expose asynchronous ValueTask-based APIs; the difference is which underlying JS runtime they dispatch through. The exhaustive DOM packages below use a newer host-specific runtime and projection model.

Exhaustive DOM interop

The exhaustive DOM packages are generated from exact-pinned TypeScript and Web IDL inputs rather than the legacy declaration parser. The semantic pipeline preserves merged declarations, inheritance, overloads, generics, event maps, constructors, global paths, documentation, deprecations, exposure metadata, and explicit transport semantics.

Generation runs once inside this repository's build graph and writes C# to intermediate obj output. Applications consuming the packages do not run Node.js, parse lib.dom.d.ts, or generate thousands of source files.

The current Window profile accounts for:

Surface Count
TypeScript symbols 2,183
Declarations 3,022
Members 12,217
Overloads 3,880
Parameters 6,522

Every in-profile symbol and member must be generated, explicitly excluded with a reviewed reason, or fail generation. The current exhaustive profile projects every symbol cleanly with no excluded, deferred, or generation-failed outcomes. Worker, Service Worker, Shared Worker, and Worklet exposure is retained in the model but intentionally deferred to separate future profiles.

Host packages

The host packages are mutually exclusive because they expose the same logical DOM surface with hosting-specific dispatch:

Package Hosting model Behavior
Blazor.DOM Blazor Server and hosting-neutral Asynchronous, cancellable ValueTask dispatch through IJSRuntime and IJSObjectReference.
Blazor.DOM.WebAssembly Blazor WebAssembly Synchronous non-Promise operations through in-process references; Promise and lifecycle operations remain asynchronous.

Both variants compile for net8.0, net9.0, and net10.0, with exact logical parity across 878 host symbols and 13,299 operations per host.

Register one package and inject the single IBrowser root:

// Blazor Server
builder.Services.AddBlazorDOM();

// Blazor WebAssembly
builder.Services.AddBlazorDOMWebAssembly();

A Blazor Server component can then resolve and use a live document proxy:

@inject IBrowser Browser

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }

        await using var document = await Browser.GetDocumentProxyAsync();
        var title = await document.GetTitleAsync();
    }
}

Generated browser interfaces are live JavaScript-reference proxies, not JSON snapshots. The runtime validates JSON dictionaries, strongly typed unions, binary values, streams, transferables, callbacks, and returned references before dispatch. Owned proxies, event subscriptions, callback-scoped borrowed references, and stream handles have explicit disposal rules.

Anonymous advanced shapes use deterministic semantic names derived from their declaring API slot and projected values, such as ClipboardItemDataStringOrBlobUnion. Structural fingerprints remain manifest-only; generation fails for an unresolved semantic collision rather than exposing hash suffixes or ordinal arm names in the public API.

Focused capability packages

Applications that do not need the complete DOM surface can reference a focused Server/WebAssembly pair generated from the same semantic model and runtime:

Capability Package pair
Permissions Blazor.Permissions / Blazor.Permissions.WebAssembly
Clipboard Blazor.Clipboard / Blazor.Clipboard.WebAssembly
Web Share Blazor.Share / Blazor.Share.WebAssembly
Wake Lock Blazor.WakeLock / Blazor.WakeLock.WebAssembly
Storage management Blazor.StorageManagement / Blazor.StorageManagement.WebAssembly
Screen APIs Blazor.Screen / Blazor.Screen.WebAssembly
Performance Blazor.Performance / Blazor.Performance.WebAssembly
Web Crypto Blazor.WebCrypto / Blazor.WebCrypto.WebAssembly
Credentials and WebAuthn Blazor.Credentials / Blazor.Credentials.WebAssembly
Offline storage Blazor.OfflineStorage / Blazor.OfflineStorage.WebAssembly
Browser coordination Blazor.BrowserCoordination / Blazor.BrowserCoordination.WebAssembly
Media devices Blazor.MediaDevices / Blazor.MediaDevices.WebAssembly
Notifications Blazor.Notifications / Blazor.Notifications.WebAssembly
File System Access Blazor.FileSystemAccess / Blazor.FileSystemAccess.WebAssembly

Together, these profiles account for 772 operations and 966 projected members. Each focused package exposes a generated capability facade, DI registration method, exact transitive type closure, host parity, feature metadata, and package-local static web assets. For example:

builder.Services.AddClipboardCapability();
@inject IClipboardCapability Capability

@code {
    async Task CopyAsync()
    {
        await using var clipboard = Capability.GetClipboard();
        await clipboard.WriteTextAsync("Generated DOM interop");
    }
}

The sample app includes an interactive DOM lab at /dom-e2e and a routed catalog of all 14 capabilities. Each page shows installation, registration, injection, generated contracts, a live browser workflow, operation facts, and the raw result envelope.

Building from source

Install the SDK selected by global.json, then build from the repository root:

dotnet build

The checked-in NuGet.config intentionally clears inherited machine-level package feeds and restores from NuGet.org. This keeps central package management deterministic across developer machines and CI. Command-line builds use MSBuild's static project graph so every project/framework combination has one artifact writer, including on Windows machines where antivirus scanning makes duplicate writes especially fragile. The shared DOM generation node is incremental; a clean build validates the pinned semantic inputs and generates all exhaustive and focused contracts under artifacts/obj.

Using the Blazor.SourceGenerators package 📦

As an example, the official Blazor.LocalStorage.WebAssembly package consumes the Blazor.SourceGenerators package. It exposes extension methods specific to Blazor WebAssembly and the localStorage Web API.

Consider the IStorageService.cs C# file:

// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.JSInterop;

[JSAutoGenericInterop(
    TypeName = "Storage",
    Implementation = "window.localStorage",
    Url = "https://developer.mozilla.org/docs/Web/API/Window/localStorage",
    GenericMethodDescriptors = new[]
    {
        "getItem",
        "setItem:value"
    })]
public partial interface IStorageService
{
}

This code designates itself into the Microsoft.JSInterop namespace, making the source generated implementation available to anyone consumer who uses types from this namespace. It uses the JSAutoGenericInterop to specify:

  • TypeName = "Storage": sets the type to Storage.
  • Implementation = "window.localStorage": expresses how to locate the implementation of the specified type from the globally scoped window object, this is the localStorage implementation.
  • Url: sets the URL for the implementation.
  • GenericMethodDescriptors: Defines the methods that should support generics as part of their source-generation. The localStorage.getItem is specified to return a generic TResult type, and the localStorage.setItem has its parameter with a name of value specified as a generic TArg type.

The generic method descriptors syntax is: "methodName" for generic return type and "methodName:parameterName" for generic parameter type.

The file needs to define an interface and it needs to be partial, for example; public partial interface. Decorating the class with the JSAutoInterop (or JSAutoGenericInterop) attribute will source generate the following C# code, as shown in the source generated IStorageService.g.cs:

using Blazor.Serialization.Extensions;
using System.Text.Json;

#nullable enable
namespace Microsoft.JSInterop;

/// <summary>
/// Source generated interface definition of the <c>Storage</c> type.
/// </summary>
public partial interface IStorageService
{
    /// <summary>
    /// Source generated implementation of <c>window.localStorage.length</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/length"></a>
    /// </summary>
    double Length
    {
        get;
    }

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.clear</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/clear"></a>
    /// </summary>
    void Clear();

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.getItem</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/getItem"></a>
    /// </summary>
    TValue? GetItem<TValue>(string key, JsonSerializerOptions? options = null);

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.key</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/key"></a>
    /// </summary>
    string? Key(double index);

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.removeItem</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/removeItem"></a>
    /// </summary>
    void RemoveItem(string key);

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.setItem</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/setItem"></a>
    /// </summary>
    void SetItem<TValue>(string key, TValue value, JsonSerializerOptions? options = null);
}

These internal extension methods rely on the IJSInProcessRuntime to perform JavaScript interop. From the given TypeName and corresponding Implementation, the following code is also generated:

  • IStorageService.g.cs: The interface for the corresponding Storage Web API surface area.
  • LocalStorgeService.g.cs: The internal implementation of the IStorageService interface.
  • LocalStorageServiceCollectionExtensions.g.cs: Extension methods to add the IStorageService service to the dependency injection IServiceCollection.

Here is the source generated LocalStorageService implementation:

// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License:
// https://github.com/IEvangelist/blazorators/blob/main/LICENSE
// Auto-generated by blazorators.

#nullable enable

using Blazor.Serialization.Extensions;
using Microsoft.JSInterop;
using System.Text.Json;

namespace Microsoft.JSInterop;

/// <inheritdoc />
internal sealed class LocalStorageService : IStorageService
{
    private readonly IJSInProcessRuntime _javaScript = null;

    /// <inheritdoc cref="P:Microsoft.JSInterop.IStorageService.Length" />
    double IStorageService.Length => _javaScript.Invoke<double>("eval", new object[1]
    {
        "window.localStorage.length"
    });

    public LocalStorageService(IJSInProcessRuntime javaScript)
    {
        _javaScript = javaScript;
    }

    /// <inheritdoc cref="M:Microsoft.JSInterop.IStorageService.Clear" />
    void IStorageService.Clear()
    {
        _javaScript.InvokeVoid("window.localStorage.clear");
    }

    /// <inheritdoc cref="M:Microsoft.JSInterop.IStorageService.GetItem``1(System.String,System.Text.Json.JsonSerializerOptions)" />
    TValue? IStorageService.GetItem<TValue>(string key, JsonSerializerOptions? options)
    {
        return _javaScript.Invoke<string>("window.localStorage.getItem", new object[1]
        {
            key
        }).FromJson<TValue>(options);
    }

    /// <inheritdoc cref="M:Microsoft.JSInterop.IStorageService.Key(System.Double)" />
    string? IStorageService.Key(double index)
    {
        return _javaScript.Invoke<string>("window.localStorage.key", new object[1]
        {
            index
        });
    }

    /// <inheritdoc cref="M:Microsoft.JSInterop.IStorageService.RemoveItem(System.String)" />
    void IStorageService.RemoveItem(string key)
    {
        _javaScript.InvokeVoid("window.localStorage.removeItem", key);
    }

    /// <inheritdoc cref="M:Microsoft.JSInterop.IStorageService.SetItem``1(System.String,``0,System.Text.Json.JsonSerializerOptions)" />
    void IStorageService.SetItem<TValue>(string key, TValue value, JsonSerializerOptions? options)
    {
        _javaScript.InvokeVoid("window.localStorage.setItem", key, value.ToJson<TValue>(options));
    }
}

Finally, here is the source generated service collection extension methods:

using Microsoft.JSInterop;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary></summary>
public static class LocalStorageServiceCollectionExtensions
{
    /// <summary>
    /// Adds the <see cref="IStorageService" /> service to the service collection.
    /// </summary>
    public static IServiceCollection AddLocalStorageServices(
        this IServiceCollection services) =>
        services.AddSingleton<IJSInProcessRuntime>(serviceProvider =>
            (IJSInProcessRuntime)serviceProvider.GetRequiredService<IJSRuntime>())
            .AddSingleton<IStorageService, LocalStorageService>();
}

Putting this all together, the Blazor.LocalStorage.WebAssembly NuGet package is actually less than 15 lines of code, and it generates full DI-ready services with JavaScript interop.

The Blazor.LocalStorage package, generates extensions on the IJSRuntime type.

// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.JSInterop;

[JSAutoInterop(
    TypeName = "Storage",
    Implementation = "window.localStorage",
    HostingModel = BlazorHostingModel.Server,
    OnlyGeneratePureJS = true,
    Url = "https://developer.mozilla.org/docs/Web/API/Window/localStorage")]
public partial interface IStorageService
{
}

Generates the following:

// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License:
// https://github.com/IEvangelist/blazorators/blob/main/LICENSE
// Auto-generated by blazorators.

using System.Threading.Tasks;

#nullable enable
namespace Microsoft.JSInterop;

public partial interface IStorageService
{
    /// <summary>
    /// Source generated implementation of <c>window.localStorage.length</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/length"></a>
    /// </summary>
    ValueTask<double> Length
    {
        get;
    }

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.clear</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/clear"></a>
    /// </summary>
    ValueTask ClearAsync();

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.getItem</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/getItem"></a>
    /// </summary>
    ValueTask<string?> GetItemAsync(string key);

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.key</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/key"></a>
    /// </summary>
    ValueTask<string?> KeyAsync(double index);

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.removeItem</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/removeItem"></a>
    /// </summary>
    ValueTask RemoveItemAsync(string key);

    /// <summary>
    /// Source generated implementation of <c>window.localStorage.setItem</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Storage/setItem"></a>
    /// </summary>
    ValueTask SetItemAsync(string key, string value);
}

Notice that, since this declaration does not include GenericMethodDescriptors, no generic overloads are produced. Both WebAssembly and Server hosting modes support generics by adding the GenericMethodDescriptors argument (or by using the JSAutoGenericInterop attribute); the example above is intentionally non-generic to show the simpler shape.

Diagnostics 🩺

The generator emits the following compile-time diagnostics so that misconfigured attribute usage surfaces in the IDE / build log rather than silently producing nothing:

ID Severity When it triggers
BR0001 Error The [JSAutoInterop] / [JSAutoGenericInterop] attribute is missing the required TypeName argument.
BR0002 Error The attribute is missing the required Implementation argument.
BR0005 Error The decorated interface isn't marked partial. The generator cannot extend it.
BR0006 Error The TypeName could not be found in the configured TypeScript declarations (lib.dom.d.ts or supplied TypeDeclarationSources).
BR0007 Error The generator threw while parsing the type declaration — typically a malformed ingestion edge case.

Bringing your own TypeScript declarations

By default the generator parses the bundled lib.dom.d.ts to resolve TypeName. Setting TypeDeclarationSources on the attribute switches the generator over to use TypeScript declaration files supplied via MSBuild <AdditionalFiles> instead:

<ItemGroup>
  <AdditionalFiles Include="declarations\my-api.d.ts" />
</ItemGroup>
[JSAutoInterop(
    TypeName = "MyCustomApi",
    Implementation = "window.myCustomApi",
    TypeDeclarationSources = new[] { "my-api.d.ts" })]
public partial interface IMyCustomApiService { }

TypeDeclarationSources entries are matched against AdditionalFiles by basename, full path, or trailing-segment suffix, so a bare filename in the attribute still resolves regardless of how MSBuild rewrites the path. When TypeDeclarationSources is set, the embedded lib.dom.d.ts is not consulted for that target; if none of the listed sources match an AdditionalFile, the generator surfaces BR0006.

Design goals 🎯

I was hoping to use the TypeScript lib.dom.d.ts bits as input. This input would be read, parsed, and cached within the generator. The generator code would be capable of generating extension methods on the IJSRuntime. Additionally, the generator will create object graphs from the well know web APIs.

Using the lib.dom.d.ts file, we could hypothetically parse various TypeScript type definitions. These definitions could then be converted to C# counterparts. While I realize that not all TypeScript is mappable to C#, there is a bit of room for interpretation.

Consider the following type definition:

/**
An object can programmatically obtain the position of the device.
It gives Web content access to the location of the device. This allows
a Web site or app to offer customized results based on the user's location.
*/
interface Geolocation {

    clearWatch(watchId: number): void;

    getCurrentPosition(
        successCallback: PositionCallback,
        errorCallback?: PositionErrorCallback | null,
        options?: PositionOptions): void;
    
    watchPosition(
        successCallback: PositionCallback,
        errorCallback?: PositionErrorCallback | null,
        options?: PositionOptions): number;
}

This is from the TypeScript repo, lib.dom.d.ts file lines 5,498-5,502.

Example consumption of source generator ✔️

Ideally, I would like to be able to define a C# class such as this:

// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.JSInterop;

[JSAutoInterop(
    TypeName = "Geolocation",
    Implementation = "window.navigator.geolocation",
    Url = "https://developer.mozilla.org/docs/Web/API/Geolocation")]
public partial interface IGeolocationService
{
}

The source generator will expose the JSAutoInteropAttribute, and consuming libraries will decorate their classes with it. The generator code will see this class, and use the TypeName from the attribute to find the corresponding type to implement. With the type name, the generator will generate the corresponding methods, and return types. The method implementations will be extensions of the IJSRuntime.

The following is an example resulting source generated IGeolocationService object:

namespace Microsoft.JSInterop;

public partial interface IGeolocationService
{
    /// <summary>
    /// Source generated implementation of <c>window.navigator.geolocation.clearWatch</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Geolocation/clearWatch"></a>
    /// </summary>
    void ClearWatch(double watchId);

    /// <summary>
    /// Source generated implementation of <c>window.navigator.geolocation.getCurrentPosition</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Geolocation/getCurrentPosition"></a>
    /// </summary>
    /// <param name="component">The calling Razor (or Blazor) component.</param>
    /// <param name="onSuccessCallbackMethodName">Expects the name of a <c>"JSInvokableAttribute"</c> C# method with the following <c>System.Action{GeolocationPosition}"</c>.</param>
    /// <param name="onErrorCallbackMethodName">Expects the name of a <c>"JSInvokableAttribute"</c> C# method with the following <c>System.Action{GeolocationPositionError}"</c>.</param>
    /// <param name="options">The <c>PositionOptions</c> value.</param>
    void GetCurrentPosition<TComponent>(
        TComponent component,
        string onSuccessCallbackMethodName,
        string? onErrorCallbackMethodName = null,
        PositionOptions? options = null)
        where TComponent : class;

    /// <summary>
    /// Source generated implementation of <c>window.navigator.geolocation.watchPosition</c>.
    /// <a href="https://developer.mozilla.org/docs/Web/API/Geolocation/watchPosition"></a>
    /// </summary>
    /// <param name="component">The calling Razor (or Blazor) component.</param>
    /// <param name="onSuccessCallbackMethodName">Expects the name of a <c>"JSInvokableAttribute"</c> C# method with the following <c>System.Action{GeolocationPosition}"</c>.</param>
    /// <param name="onErrorCallbackMethodName">Expects the name of a <c>"JSInvokableAttribute"</c> C# method with the following <c>System.Action{GeolocationPositionError}"</c>.</param>
    /// <param name="options">The <c>PositionOptions</c> value.</param>
    double WatchPosition<TComponent>(
        TComponent component, 
        string onSuccessCallbackMethodName, 
        string? onErrorCallbackMethodName = null, 
        PositionOptions? options = null) 
        where TComponent : class;
}

The generator will also produce the corresponding APIs object types. For example, the Geolocation API defines the following:

  • GeolocationService
  • PositionOptions
  • GeolocationCoordinates
  • GeolocationPosition
  • GeolocationPositionError
namespace Microsoft.JSInterop;

/// <inheritdoc />
internal sealed class GeolocationService : IGeolocationService
{
    private readonly IJSInProcessRuntime _javaScript = null;

    public GeolocationService(IJSInProcessRuntime javaScript)
    {
        _javaScript = javaScript;
    }

    /// <inheritdoc cref="M:Microsoft.JSInterop.IGeolocationService.ClearWatch(System.Double)" />
    void IGeolocationService.ClearWatch(double watchId)
    {
        _javaScript.InvokeVoid("window.navigator.geolocation.clearWatch", watchId);
    }

    /// <inheritdoc cref="M:Microsoft.JSInterop.IGeolocationService.GetCurrentPosition``1(``0,System.String,System.String,Microsoft.JSInterop.PositionOptions)" />
    void IGeolocationService.GetCurrentPosition<TComponent>(
        TComponent component, 
        string onSuccessCallbackMethodName, 
        string? onErrorCallbackMethodName, 
        PositionOptions? options)
    {
        _javaScript.InvokeVoid("blazorators.getCurrentPosition", DotNetObjectReference.Create<TComponent>(component), onSuccessCallbackMethodName, onErrorCallbackMethodName, options);
    }

    /// <inheritdoc cref="M:Microsoft.JSInterop.IGeolocationService.WatchPosition``1(``0,System.String,System.String,Microsoft.JSInterop.PositionOptions)" />
    double IGeolocationService.WatchPosition<TComponent>(
        TComponent component, 
        string onSuccessCallbackMethodName, 
        string? onErrorCallbackMethodName, 
        PositionOptions? options)
    {
        return _javaScript.Invoke<double>("blazorators.watchPosition", new object[4]
        {
            DotNetObjectReference.Create<TComponent>(component),
            onSuccessCallbackMethodName,
            onErrorCallbackMethodName,
            options
        });
    }
}
using System.Text.Json.Serialization;

namespace Microsoft.JSInterop;

/// <summary>
/// Source-generated object representing an ideally immutable <c>GeolocationPosition</c> value.
/// </summary>
public class GeolocationPosition
{
    /// <summary>
    /// Source-generated property representing the <c>GeolocationPosition.coords</c> value.
    /// </summary>
    [JsonPropertyName("coords")]
    public GeolocationCoordinates Coords
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationPosition.timestamp</c> value.
    /// </summary>
    [JsonPropertyName("timestamp")]
    public long Timestamp
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationPosition.timestamp</c> value, 
    /// converted as a <see cref="T:System.DateTime" /> in UTC.
    /// </summary>
    [JsonIgnore]
    public DateTime TimestampAsUtcDateTime => Timestamp.ToDateTimeFromUnix();
}

/// <summary>
/// Source-generated object representing an ideally immutable <c>GeolocationCoordinates</c> value.
/// </summary>
public class GeolocationCoordinates
{
    /// <summary>
    /// Source-generated property representing the <c>GeolocationCoordinates.accuracy</c> value.
    /// </summary>
    [JsonPropertyName("accuracy")]
    public double Accuracy
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationCoordinates.altitude</c> value.
    /// </summary>
    [JsonPropertyName("altitude")]
    public double? Altitude
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationCoordinates.altitudeAccuracy</c> value.
    /// </summary>
    [JsonPropertyName("altitudeAccuracy")]
    public double? AltitudeAccuracy
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationCoordinates.heading</c> value.
    /// </summary>
    [JsonPropertyName("heading")]
    public double? Heading
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationCoordinates.latitude</c> value.
    /// </summary>
    [JsonPropertyName("latitude")]
    public double Latitude
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationCoordinates.longitude</c> value.
    /// </summary>
    [JsonPropertyName("longitude")]
    public double Longitude
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationCoordinates.speed</c> value.
    /// </summary>
    [JsonPropertyName("speed")]
    public double? Speed
    {
        get;
        set;
    }
}

/// <summary>
/// Source-generated object representing an ideally immutable <c>GeolocationPositionError</c> value.
/// </summary>
public class GeolocationPositionError
{
    /// <summary>
    /// Source-generated property representing the <c>GeolocationPositionError.code</c> value.
    /// </summary>
    [JsonPropertyName("code")]
    public double Code
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationPositionError.message</c> value.
    /// </summary>
    [JsonPropertyName("message")]
    public string Message
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationPositionError.PERMISSION_DENIED</c> value.
    /// </summary>
    [JsonPropertyName("PERMISSION_DENIED")]
    public double PERMISSION_DENIED
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationPositionError.POSITION_UNAVAILABLE</c> value.
    /// </summary>
    [JsonPropertyName("POSITION_UNAVAILABLE")]
    public double POSITION_UNAVAILABLE
    {
        get;
        set;
    }

    /// <summary>
    /// Source-generated property representing the <c>GeolocationPositionError.TIMEOUT</c> value.
    /// </summary>
    [JsonPropertyName("TIMEOUT")]
    public double TIMEOUT
    {
        get;
        set;
    }
}

// Additional models omitted for brevity...

In addition to this GeolocationExtensions class being generated, the generator will also generate a bit of JavaScript. Some methods cannot be directly invoked as they define callbacks. The approach the generator takes is to delegate callback methods on a given T instance, with the JSInvokable attribute. Our generator should also warn when the corresponding T instance doesn't define a matching method name that is also JSInvokable.

const getCurrentLocation =
    (dotnetObj, successMethodName, errorMethodName, options) =>
    {
        if (navigator && navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(
                (position) => {
                    dotnetObj.invokeMethodAsync(
                        successMethodName, position);
                },
                (error) => {
                    dotnetObj.invokeMethodAsync(
                        errorMethodName, error);
                },
                options);
        }
    };

// Other implementations omitted for brevity...
// But we'd also define a "watchPosition" wrapper.
// The "clearWatch" is a straight pass-thru, no wrapper needed.

window.blazorator = {
    getCurrentLocation,
    watchPosition
};

The resulting JavaScript will have to be exposed to consuming projects. Additionally, consuming projects will need to adhere to extension method consumption semantics. When calling generated extension methods that require .NET object references of type T, the callback names should be marked with JSInvokable and the nameof operator should be used to ensure names are accurate. Consider the following example consuming Blazor component:

using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Microsoft.JSInterop.Extensions;

namespace Example.Components;

// This is the other half of ConsumingComponent.razor
public sealed partial class ConsumingComponent
{
    [Inject]
    public IJSRuntime JavaScript { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JavaScript.GetCurrentPositionAsync(
                this,
                nameof(OnCoordinatesPermitted),
                nameof(OnErrorRequestingCoordinates));
        }
    }

    [JSInvokable]
    public async Task OnCoordinatesPermitted(
        GeolocationPosition position)
    {
        // TODO: consume/handle position.

        await InvokeAsync(StateHasChanged);
    }

    [JSInvokable]
    public async Task OnErrorRequestingCoordinates(
        GeolocationPositionError error)
    {
        // TODO: consume/handle error.

        await InvokeAsync(StateHasChanged);
    }
}

Pseudocode and logical flow ➡️

  1. Consumer decorates a static partial class with the JSAutoInteropAttribute.
  2. Source generator is called:
    • JavaScriptInteropGenerator.Initialize
    • JavaScriptInteropGenerator.Execute
  3. The generator determines the TypeName from the attribute of the contextual class.
    1. The TypeName is used to look up the corresponding TypeScript type definition.
    2. If found, and a valid API - attempt source generation.

Future work

Known limitations ⚠️

At the time of writing, only pure JavaScript interop is supported. It is a stretch goal to add the following (currently missing) features:

  • Source generate corresponding (and supporting) JavaScript files.
    • We'd need to accept a desired output path from the consumer, JavaScriptOutputPath.
    • We would need to append all JavaScript into a single builder, and emit it collectively.
  • Allow for declarative and custom type mappings, for example; suppose the consumer wants the API to use generics instead of string.
    • We'd need to expose a TypeConverter parameter and allow for consumers to implement their own.
    • We'd provide a default one for standard JSON serialization, StringTypeConverter (maybe make this the default).

References and resources 📑

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Weihan Li
Weihan Li

💻
David Pine
David Pine

💻 🎨 👀 🤔 ⚠️
Robert McLaws
Robert McLaws

💻 🐛 🤔
Colin Dembovsky
Colin Dembovsky

🚇 📦
Tanay Parikh
Tanay Parikh

📖
Andreas Müller
Andreas Müller

🐛 💻
Mahmudul Hasan
Mahmudul Hasan

💻
fabiansanchez18
fabiansanchez18

🐛
Sean Feldman
Sean Feldman

🐛
daver77
daver77

🤔
Denny09310
Denny09310

💻 ⚠️ 🤔

This project follows the all-contributors specification. Contributions of any kind are welcome!

About

This project converts TypeScript type declarations into C# representations, and use C# source generators to expose automatic JavaScript interop functionality.

Topics

Resources

Code of conduct

Security policy

Stars

392 stars

Watchers

13 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages