Blazor.SourceGenerators 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Blazor.SourceGenerators --version 1.0.0
NuGet\Install-Package Blazor.SourceGenerators -Version 1.0.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Blazor.SourceGenerators" Version="1.0.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Blazor.SourceGenerators --version 1.0.0
#r "nuget: Blazor.SourceGenerators, 1.0.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Blazor.SourceGenerators as a Cake Addin
#addin nuget:?package=Blazor.SourceGenerators&version=1.0.0

// Install Blazor.SourceGenerators as a Cake Tool
#tool nuget:?package=Blazor.SourceGenerators&version=1.0.0

Blazorators: Blazor C# Source Generators

build pull request

All Contributors

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:

[JSAutoInterop(
    TypeName = "Geolocation",
    PathFromWidow = "window.navigator.geolocation",
    Url = "https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API",
    OnlyGeneratePureJS = false)]
public static partial class GeolocationExtensions { }

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 GeolocationExtensions object:

using Microsoft.JSInterop;

namespace Microsoft.JSInterop.Extensions;

public static partial class GeolocationExtensions
{
    /// <summary>
    /// See <a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition"></a>.
    /// </summary>
    public static ValueTask GetCurrentPositionAsync<T>(
        this IJSRuntime jsRuntime,
        T dotnetObject,
        string successMethodName,
        string? errorMethodName = null,
        PositionOptions? options = null)
        where T : class
    {
        return jsRuntime.InvokeVoidAsync(
            "blazorator.getCurrentLocation",
            DotNetObjectReference.Create(dotnetObject),
            successMethodName,
            errorMethodName,
            options
        );
    }

    /// <summary>
    /// See <a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition"></a>
    /// </summary>
    public static ValueTask<double> WatchPositionAsync<T>(
        this IJSRuntime jsRuntime,
        T dotnetObject,
        string successMethodName,
        string? errorMethodName = null,
        PositionOptions? options = null)
        where T : class
    {
        return jsRuntime.InvokeAsync<double>(
            "blazorator.watchPosition",
            DotNetObjectReference.Create(dotnetObject),
            successMethodName,
            errorMethodName,
            options
        );
    }

    /// <summary>
    /// See <a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/clearWatch"></a>
    /// </summary>
    public ValueTask ClearWatchAsync(this IJSRuntime jsRuntime, double id)
    {
        return jsRuntime.InvokevoidAsync(
            "navigator.geolocation.clearWatch", id
        );
    }
}

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

  • PositionOptions
  • GeolocationCoordinates
  • GeolocationPosition
  • GeolocationPositionError
using System.Text.Json.Serialization;

namespace Microsoft.JSInterop.Extensions;

/// <summary>
/// See <a href="https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPosition"></a>
/// </summary>
public record GeolocationPosition(
    [property: JsonPropertyName("coords")] GeolocationCoordinates Coordinates,
    [property: JsonPropertyName("timestamp")] DOMTimeStamp TimeStamp
);

/// <summary>
/// See <a href="https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates"></a>
/// </summary>
public record GeolocationCoordinates(
    [property: JsonPropertyName("latitude")] double Latitude,
    [property: JsonPropertyName("longitude")] double Longitude,
    [property: JsonPropertyName("altitude")] double Altitude,
    [property: JsonPropertyName("altitudeAccuracy")] double? AltitudeAccuracy,
    [property: JsonPropertyName("heading")] double? Heading,
    [property: JsonPropertyName("speed")] double Speed
);

/// <summary>
/// See <a href="https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError"></a>
/// </summary>
public record GeolocationPositionError(
    [property: JsonPropertyName("code")] short Code,
    [property: JsonPropertyName("message")] string Message
);

// 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 JavaScriptInteropAttribute.
  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.

NuGet packages đŸ“Ļ

This repository will expose two NuGet packages:

  1. The source-generated IJSRuntime extension methods for a select few well-defined APIs.
  2. The source generator itself, as a consumable analyzer package.

References and resources 📑

Contributors ✨

Thanks goes to these wonderful people (emoji key):

<table> <tr> <td align="center"><a href="https://www.cnblogs.com/weihanli"><img src="https://avatars.githubusercontent.com/u/7604648?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Weihan Li</b></sub></a><br /><a href="https://github.com/IEvangelist/blazorators/commits?author=WeihanLi" title="Code">đŸ’ģ</a></td> <td align="center"><a href="https://www.microsoft.com"><img src="https://avatars.githubusercontent.com/u/7679720?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Pine</b></sub></a><br /><a href="https://github.com/IEvangelist/blazorators/commits?author=IEvangelist" title="Code">đŸ’ģ</a> <a href="#design-IEvangelist" title="Design">🎨</a> <a href="https://github.com/IEvangelist/blazorators/pulls?q=is%3Apr+reviewed-by%3AIEvangelist" title="Reviewed Pull Requests">👀</a> <a href="#ideas-IEvangelist" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/IEvangelist/blazorators/commits?author=IEvangelist" title="Tests">⚠ī¸</a></td> </tr> </table>

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

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
8.0.0 711 11/17/2023
8.0.0-rc.2.23480.2 59 10/13/2023
7.0.3 322 2/15/2023
7.0.2 242 2/7/2023
7.0.1 259 1/24/2023
7.0.0 268 1/11/2023
2.0.11 438 10/10/2022
2.0.10 398 5/19/2022
2.0.9 420 4/18/2022
2.0.8 416 4/14/2022
2.0.7 412 4/14/2022
2.0.6 445 4/7/2022
2.0.5 394 4/5/2022
2.0.4 419 4/5/2022
2.0.3 384 4/5/2022
2.0.2 455 4/4/2022
2.0.1 379 4/4/2022
1.5.0 387 3/29/2022
1.4.5 419 3/26/2022
1.4.3 380 3/25/2022
1.4.2 408 3/23/2022
1.4.0 399 3/22/2022
1.3.3 377 3/18/2022
1.3.1 440 3/16/2022
1.3.0 424 3/16/2022
1.2.0 385 3/13/2022
1.1.1 425 3/8/2022
1.1.0 412 3/5/2022
1.0.5 382 3/4/2022
1.0.4 620 3/3/2022
1.0.3 395 2/24/2022
1.0.2 415 2/22/2022
1.0.1 398 2/22/2022
1.0.0 394 2/22/2022