Blazor.LocalStorage.WebAssembly
1.1.0
See the version list below for details.
dotnet add package Blazor.LocalStorage.WebAssembly --version 1.1.0
NuGet\Install-Package Blazor.LocalStorage.WebAssembly -Version 1.1.0
<PackageReference Include="Blazor.LocalStorage.WebAssembly" Version="1.1.0" />
paket add Blazor.LocalStorage.WebAssembly --version 1.1.0
#r "nuget: Blazor.LocalStorage.WebAssembly, 1.1.0"
// Install Blazor.LocalStorage.WebAssembly as a Cake Addin #addin nuget:?package=Blazor.LocalStorage.WebAssembly&version=1.1.0 // Install Blazor.LocalStorage.WebAssembly as a Cake Tool #tool nuget:?package=Blazor.LocalStorage.WebAssembly&version=1.1.0
Blazorators: Blazor C# Source Generators
Thank you for perusing my Blazor C# Source Generators repository. I'd really appreciate a ⭐ if you find this interesting.
A C# source generator that creates extensions methods on the Blazor WebAssembly JavaScript implementation of the IJSInProcessRuntime
type. This library provides several NuGet packages:
NuGet package | NuGet version |
---|---|
Blazor.SourceGenerators |
|
Blazor.LocalStorage.WebAssembly |
|
Blazor.LocalStorage.Server |
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 LocalStorageExtensions.cs C# file:
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.JSInterop;
/// <summary>
/// Source generated extension methods on the <see cref="IJSInProcessRuntime"/> implementation.
/// </summary>
[JSAutoGenericInterop(
TypeName = "Storage",
PathFromWindow = "window.localStorage",
HostingModel = BlazorHostingModel.WebAssembly,
OnlyGeneratePureJS = true,
Url = "https://developer.mozilla.org/docs/Web/API/Window/localStorage",
GenericMethodDescriptors = new[]
{
"getItem",
"setItem:value"
})]
public static partial class SynchronousLocalStorageExtensions
{
}
This code designates itself into the Microsoft.JSInterop
namespace, making all of the source generated extensions available to anyone consumer who uses types from this namespace. It uses the JSAutoInterop
to specify:
TypeName = "Storage"
: sets the type toStorage
.PathFromWindow = "window.localStorage"
: expresses how to locate the implementation of the specified type from the globally scopedwindow
object, this is thelocalStorage
implementation.HostingModel = BlazorHostingModel.WebAssembly
: tells the generator to create synchronous extension methods on theIJSInProcessRuntime
type (default), use.Server
forIJSRuntime
and Task-based asynchronous methods instead.OnlyGeneratePureJS = true
: configures the source generator to emit only C#, whenfalse
will emit JavaScript.Url
: sets the URL for the implementation.GenericMethodDescriptors
: Defines the methods that should support generics as part of their source-generation. ThelocalStorage.getItem
is specified to return a genericTResult
type, and thelocalStorage.setItem
has its parameter with a name ofvalue
specified as a genericTArg
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 extension class and needs to be partial
, for example; public static partial class
. Decorating the class with the JSAutoInterop
attribute will source generate the following C# code:
// 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 Blazor.Serialization.Extensions;
using System.Text.Json;
#nullable enable
namespace Microsoft.JSInterop;
public static partial class SynchronousLocalStorageExtensions
{
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.clear</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/clear"></a>
/// </summary>
public static void Clear(
this IJSInProcessRuntime javaScript) =>
javaScript.InvokeVoid("window.localStorage.clear");
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.getItem</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/getItem"></a>
/// </summary>
public static TResult? GetItem<TResult>(
this IJSInProcessRuntime javaScript,
string key,
JsonSerializerOptions? options = null) =>
javaScript.Invoke<string?>(
"window.localStorage.getItem",
key)
.FromJson<TResult>(options);
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.key</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/key"></a>
/// </summary>
public static string? Key(
this IJSInProcessRuntime javaScript,
double index) =>
javaScript.Invoke<string?>(
"window.localStorage.key",
index);
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.removeItem</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/removeItem"></a>
/// </summary>
public static void RemoveItem(
this IJSInProcessRuntime javaScript,
string key) =>
javaScript.InvokeVoid(
"window.localStorage.removeItem",
key);
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.setItem</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/setItem"></a>
/// </summary>
public static void SetItem<TArg>(
this IJSInProcessRuntime javaScript,
string key,
TArg value,
JsonSerializerOptions? options = null) =>
javaScript.InvokeVoid(
"window.localStorage.setItem",
key,
value.ToJson(options));
}
The Blazor.LocalStorage.Server
package, generates extensions on the IJSRuntime
type.
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.JSInterop;
/// <summary>
/// Source generated extension methods on the <see cref="IJSRuntime"/> implementation.
/// </summary>
[JSAutoInterop(
TypeName = "Storage",
PathFromWindow = "window.localStorage",
HostingModel = BlazorHostingModel.Server,
OnlyGeneratePureJS = true,
Url = "https://developer.mozilla.org/docs/Web/API/Window/localStorage")]
public static partial class AsynchronousLocalStorageExtensions
{
}
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 static partial class AsynchronousLocalStorageExtensions
{
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.clear</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/clear"></a>
/// </summary>
public static ValueTask ClearAsync(
this IJSRuntime javaScript) =>
javaScript.InvokeVoidAsync("window.localStorage.clear");
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.getItem</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/getItem"></a>
/// </summary>
public static ValueTask<string?> GetItemAsync(
this IJSRuntime javaScript,
string key) =>
javaScript.InvokeAsync<string?>(
"window.localStorage.getItem",
key);
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.key</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/key"></a>
/// </summary>
public static ValueTask<string?> KeyAsync(
this IJSRuntime javaScript,
double index) =>
javaScript.InvokeAsync<string?>(
"window.localStorage.key",
index);
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.removeItem</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/removeItem"></a>
/// </summary>
public static ValueTask RemoveItemAsync(
this IJSRuntime javaScript,
string key) =>
javaScript.InvokeVoidAsync(
"window.localStorage.removeItem",
key);
/// <summary>
/// Source generated extension method implementation of <c>window.localStorage.setItem</c>.
/// <a href="https://developer.mozilla.org/docs/Web/API/Storage/setItem"></a>
/// </summary>
public static ValueTask SetItemAsync(
this IJSRuntime javaScript,
string key,
string value) =>
javaScript.InvokeVoidAsync(
"window.localStorage.setItem",
key,
value);
}
Notice, that since the generic method descriptors are not added generics are not supported. This is not yet implemented as I've been focusing on WebAssembly scenarios.
Using the Blazor.LocalStorage.WebAssembly
package 📦
The Blazor.LocalStorage.WebAssembly
package is a WebAssembly specific implementation of the localStorage
Web API that has been source generated. The example above is the result of this published package.
This package exposes a convenience extension method on the IServiceCollection
type, named AddWebAssemblyLocalStorage
. Calling this will expose the IJSInProcessRuntime
as a dependency injection service type as a scoped lifetime. Consider the following Program.cs C# file for an example Blazor WebAssembly template project:
using Blazor.ExampleConsumer;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(
sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
// Adds the IJSInProcessRuntime type to DI.
builder.Services.AddWebAssemblyLocalStorage();
await builder.Build().RunAsync();
Then, in your components you can consume this as you would any other service type. Consider the Counter.razor file:
@page "/counter"
@inject IJSInProcessRuntime JavaScript
<PageTitle>Counter (@_currentCount)</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @_currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Increment</button>
@code {
private int _currentCount = 0;
private void IncrementCount() => JavaScript.SetItem("CounterValue", (++ _currentCount).ToString());
protected override void OnInitialized()
{
base.OnInitialized();
if (JavaScript.GetItem("CounterValue") is { } count && int.TryParse(count, out var currentCount))
{
_currentCount = currentCount;
}
}
}
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 ➡️
- Consumer decorates a
static partial class
with theJSAutoInteropAttribute
. - Source generator is called:
JavaScriptInteropGenerator.Initialize
JavaScriptInteropGenerator.Execute
- The generator determines the
TypeName
from the attribute of the contextual class.- The
TypeName
is used to look up the corresponding TypeScript type definition. - If found, and a valid API - attempt source generation.
- The
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.
- We'd need to accept a desired output path from the consumer,
- 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).
- We'd need to expose a
References and resources 📑
- MDN Web Docs: Web APIs
- TypeScript DOM lib generator
- ASP.NET Core Docs: Blazor JavaScript interop
- Jared Parsons - GitHub Channel 9 Source Generators
- .NET Docs: C# Source Generators
- Source Generators Cookbook
- Source Generators: Design Document
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!
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- Blazor.Serialization (>= 1.1.0)
- Microsoft.Extensions.DependencyInjection (>= 6.0.0)
- Microsoft.Extensions.Primitives (>= 6.0.0)
- Microsoft.JSInterop (>= 6.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories (3)
Showing the top 3 popular GitHub repositories that depend on Blazor.LocalStorage.WebAssembly:
Repository | Stars |
---|---|
Azure-Samples/azure-search-openai-demo-csharp
A sample app for the Retrieval-Augmented Generation pattern running in Azure, using Azure Cognitive Search for retrieval and Azure OpenAI large language models to power ChatGPT-style and Q&A experiences.
|
|
IEvangelist/signalr-chat
A chat app built with Blazor WebAssembly, hosted on ASP.NET Core, with the latest C# and SignalR -- need I say more?
|
|
IEvangelist/learning-blazor
The application for the "Learning Blazor: Build Single Page Apps with WebAssembly and C#" O'Reilly Media book by David Pine.
|
Version | Downloads | Last updated |
---|---|---|
9.0.0 | 49 | 11/22/2024 |
8.0.0 | 43,829 | 11/17/2023 |
8.0.0-rc.2.23480.2 | 3,295 | 10/13/2023 |
7.0.3 | 24,251 | 2/15/2023 |
7.0.2 | 297 | 2/7/2023 |
7.0.1 | 360 | 1/24/2023 |
7.0.0 | 6,488 | 1/11/2023 |
2.0.11 | 3,612 | 10/10/2022 |
2.0.10 | 6,730 | 5/19/2022 |
2.0.9 | 589 | 4/18/2022 |
2.0.8 | 440 | 4/14/2022 |
2.0.7 | 431 | 4/14/2022 |
2.0.6 | 461 | 4/7/2022 |
2.0.5 | 853 | 4/5/2022 |
2.0.3 | 436 | 4/5/2022 |
2.0.2 | 432 | 4/4/2022 |
2.0.1 | 434 | 4/4/2022 |
1.5.0 | 451 | 3/29/2022 |
1.4.5 | 476 | 3/26/2022 |
1.4.3 | 428 | 3/25/2022 |
1.4.2 | 457 | 3/23/2022 |
1.4.0 | 442 | 3/22/2022 |
1.3.3 | 448 | 3/18/2022 |
1.3.1 | 428 | 3/16/2022 |
1.3.0 | 441 | 3/16/2022 |
1.2.0 | 537 | 3/13/2022 |
1.1.1 | 556 | 3/8/2022 |
1.1.0 | 438 | 3/5/2022 |
1.0.5 | 446 | 3/4/2022 |
1.0.4 | 458 | 3/3/2022 |
1.0.3 | 481 | 2/24/2022 |
1.0.2 | 458 | 2/22/2022 |
1.0.1 | 444 | 2/22/2022 |
1.0.0 | 680 | 2/22/2022 |