Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Blazor] Use JSON source generator during WebAssembly startup #54956

Merged
merged 20 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Components.Infrastructure;

Expand All @@ -21,8 +23,25 @@ public class ComponentStatePersistenceManager
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
/// </summary>
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
: this(DefaultJsonSerializerOptions.Instance, logger)
{
State = new PersistentComponentState(_currentState, _registeredCallbacks);
}

/// <summary>
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
/// </summary>
public ComponentStatePersistenceManager(
IOptions<JsonOptions> jsonOptions,
ILogger<ComponentStatePersistenceManager> logger)
: this(jsonOptions.Value.SerializerOptions, logger)
{
}

private ComponentStatePersistenceManager(
JsonSerializerOptions jsonSerializerOptions,
ILogger<ComponentStatePersistenceManager> logger)
{
State = new PersistentComponentState(jsonSerializerOptions, _currentState, _registeredCallbacks);
_logger = logger;
}

Expand Down
17 changes: 17 additions & 0 deletions src/Components/Components/src/JsonOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Options to configure JSON serialization settings for components.
/// </summary>
public sealed class JsonOptions
{
/// <summary>
/// Gets the <see cref="JsonSerializerOptions"/>.
/// </summary>
public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultJsonSerializerOptions.Instance);
}
24 changes: 24 additions & 0 deletions src/Components/Components/src/JsonServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods for configuring JSON options for components.
/// </summary>
public static class JsonServiceCollectionExtensions
{
/// <summary>
/// Configures options used for serializing JSON in components functionality.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure options on.</param>
/// <param name="configureOptions">The <see cref="Action{JsonOptions}"/> to configure the <see cref="JsonOptions"/>.</param>
/// <returns>The modified <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection ConfigureComponentsJsonOptions(this IServiceCollection services, Action<JsonOptions> configureOptions)
{
services.Configure(configureOptions);
return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

<ItemGroup>
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="RenderTree" />
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultJsonSerializerOptions.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\HotReloadManager.cs" LinkBase="HotReload" />
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)QueryStringEnumerable.cs" LinkBase="Shared" />
Expand Down
10 changes: 6 additions & 4 deletions src/Components/Components/src/PersistentComponentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ public class PersistentComponentState
{
private IDictionary<string, byte[]>? _existingState;
private readonly IDictionary<string, byte[]> _currentState;

private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly List<PersistComponentStateRegistration> _registeredCallbacks;

internal PersistentComponentState(
IDictionary<string , byte[]> currentState,
JsonSerializerOptions jsonSerializerOptions,
IDictionary<string, byte[]> currentState,
List<PersistComponentStateRegistration> pauseCallbacks)
{
_jsonSerializerOptions = jsonSerializerOptions;
_currentState = currentState;
_registeredCallbacks = pauseCallbacks;
}
Expand Down Expand Up @@ -84,7 +86,7 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
}

_currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
_currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, _jsonSerializerOptions));
}

/// <summary>
Expand All @@ -104,7 +106,7 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
if (TryTake(key, out var data))
{
var reader = new Utf8JsonReader(data);
instance = JsonSerializer.Deserialize<TValue>(ref reader, JsonSerializerOptionsProvider.Options)!;
instance = JsonSerializer.Deserialize<TValue>(ref reader, _jsonSerializerOptions)!;
return true;
}
else
Expand Down
6 changes: 6 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Components.JsonOptions!>! jsonOptions, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager!>! logger) -> void
Microsoft.AspNetCore.Components.JsonOptions
Microsoft.AspNetCore.Components.JsonOptions.JsonOptions() -> void
Microsoft.AspNetCore.Components.JsonOptions.SerializerOptions.get -> System.Text.Json.JsonSerializerOptions!
Microsoft.Extensions.DependencyInjection.JsonServiceCollectionExtensions
static Microsoft.Extensions.DependencyInjection.JsonServiceCollectionExtensions.ConfigureComponentsJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.Components.JsonOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class ComponentApplicationStateTest
public void InitializeExistingState_SetupsState()
{
// Arrange
var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());
var existingState = new Dictionary<string, byte[]>
{
["MyState"] = JsonSerializer.SerializeToUtf8Bytes(new byte[] { 1, 2, 3, 4 })
Expand All @@ -29,7 +29,7 @@ public void InitializeExistingState_SetupsState()
public void InitializeExistingState_ThrowsIfAlreadyInitialized()
{
// Arrange
var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());
var existingState = new Dictionary<string, byte[]>
{
["MyState"] = new byte[] { 1, 2, 3, 4 }
Expand All @@ -45,7 +45,7 @@ public void InitializeExistingState_ThrowsIfAlreadyInitialized()
public void TryRetrieveState_ReturnsStateWhenItExists()
{
// Arrange
var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());
var existingState = new Dictionary<string, byte[]>
{
["MyState"] = JsonSerializer.SerializeToUtf8Bytes(new byte[] { 1, 2, 3, 4 })
Expand All @@ -65,7 +65,7 @@ public void PersistState_SavesDataToTheStoreAsync()
{
// Arrange
var currentState = new Dictionary<string, byte[]>();
var applicationState = new PersistentComponentState(currentState, new List<PersistComponentStateRegistration>())
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List<PersistComponentStateRegistration>())
{
PersistingState = true
};
Expand All @@ -84,7 +84,7 @@ public void PersistState_ThrowsForDuplicateKeys()
{
// Arrange
var currentState = new Dictionary<string, byte[]>();
var applicationState = new PersistentComponentState(currentState, new List<PersistComponentStateRegistration>())
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List<PersistComponentStateRegistration>())
{
PersistingState = true
};
Expand All @@ -101,7 +101,7 @@ public void PersistAsJson_SerializesTheDataToJsonAsync()
{
// Arrange
var currentState = new Dictionary<string, byte[]>();
var applicationState = new PersistentComponentState(currentState, new List<PersistComponentStateRegistration>())
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List<PersistComponentStateRegistration>())
{
PersistingState = true
};
Expand All @@ -120,7 +120,7 @@ public void PersistAsJson_NullValueAsync()
{
// Arrange
var currentState = new Dictionary<string, byte[]>();
var applicationState = new PersistentComponentState(currentState, new List<PersistComponentStateRegistration>())
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, currentState, new List<PersistComponentStateRegistration>())
{
PersistingState = true
};
Expand All @@ -140,7 +140,7 @@ public void TryRetrieveFromJson_DeserializesTheDataFromJson()
var myState = new byte[] { 1, 2, 3, 4 };
var serialized = JsonSerializer.SerializeToUtf8Bytes(myState);
var existingState = new Dictionary<string, byte[]>() { ["MyState"] = serialized };
var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());

applicationState.InitializeExistingState(existingState);

Expand All @@ -158,7 +158,7 @@ public void TryRetrieveFromJson_NullValue()
// Arrange
var serialized = JsonSerializer.SerializeToUtf8Bytes<byte[]>(null);
var existingState = new Dictionary<string, byte[]>() { ["MyState"] = serialized };
var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());
var applicationState = new PersistentComponentState(DefaultJsonSerializerOptions.Instance, new Dictionary<string, byte[]>(), new List<PersistComponentStateRegistration>());

applicationState.InitializeExistingState(existingState);

Expand Down
8 changes: 8 additions & 0 deletions src/Components/Server/src/Circuits/RemoteJSRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal partial class RemoteJSRuntime : JSRuntime
public RemoteJSRuntime(
IOptions<CircuitOptions> circuitOptions,
IOptions<HubOptions<ComponentHub>> componentHubOptions,
IOptions<JsonOptions> jsonOptions,
ILogger<RemoteJSRuntime> logger)
{
_options = circuitOptions.Value;
Expand All @@ -48,6 +49,13 @@ public RemoteJSRuntime(
DefaultAsyncTimeout = _options.JSInteropDefaultCallTimeout;
ElementReferenceContext = new WebElementReferenceContext(this);
JsonSerializerOptions.Converters.Add(new ElementReferenceJsonConverter(ElementReferenceContext));

JsonSerializerOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver.Instance);

if (jsonOptions.Value.SerializerOptions is { TypeInfoResolver: { } typeInfoResolver })
{
JsonSerializerOptions.TypeInfoResolverChain.Add(typeInfoResolver);
}
MackinnonBuck marked this conversation as resolved.
Show resolved Hide resolved
}

public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Infrastructure;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -87,6 +88,10 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJavaScriptInitializersConfiguration>());

// Configure JSON serializer options
services.ConfigureComponentsWebJsonOptions();
services.ConfigureDefaultAntiforgeryJsonOptions();

if (configure != null)
{
services.Configure(configure);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,15 @@
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentOperationBatch.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentOperationType.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultJsonSerializerOptions.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultAntiforgeryStateProvider.cs" LinkBase="Forms" />
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultAntiforgeryJsonOptionsServiceCollectionExtensions.cs" LinkBase="DependencyInjection" />
<Compile Include="$(ComponentsSharedSourceRoot)src\WebRendererId.cs" />

<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerialization\JsonConverterFactoryTypeInfoResolver.cs" LinkBase="JsonSerialization" />
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerialization\JSRuntimeSerializerContext.cs" LinkBase="JsonSerialization" />

<Compile Include="..\..\Shared\src\BrowserNavigationManagerInterop.cs" />
<Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />
<Compile Include="..\..\Shared\src\WebEventData\*.cs" LinkBase="WebEventData" />

<Compile Include="$(RepoRoot)src\SignalR\common\Shared\BinaryMessageFormatter.cs" LinkBase="BlazorPack" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public abstract class ProtectedBrowserStorage
private readonly string _storeName;
private readonly IJSRuntime _jsRuntime;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly ConcurrentDictionary<string, IDataProtector> _cachedDataProtectorsByPurpose
= new ConcurrentDictionary<string, IDataProtector>(StringComparer.Ordinal);

Expand All @@ -25,7 +26,12 @@ private readonly ConcurrentDictionary<string, IDataProtector> _cachedDataProtect
/// <param name="storeName">The name of the store in which the data should be stored.</param>
/// <param name="jsRuntime">The <see cref="IJSRuntime"/>.</param>
/// <param name="dataProtectionProvider">The <see cref="IDataProtectionProvider"/>.</param>
private protected ProtectedBrowserStorage(string storeName, IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider)
/// <param name="jsonSerializerOptions">The <see cref="JsonSerializerOptions"/>.</param>
private protected ProtectedBrowserStorage(
string storeName,
IJSRuntime jsRuntime,
IDataProtectionProvider dataProtectionProvider,
JsonSerializerOptions? jsonSerializerOptions)
{
// Performing data protection on the client would give users a false sense of security, so we'll prevent this.
if (OperatingSystem.IsBrowser())
Expand All @@ -38,6 +44,7 @@ private protected ProtectedBrowserStorage(string storeName, IJSRuntime jsRuntime
_storeName = storeName;
_jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
_dataProtectionProvider = dataProtectionProvider ?? throw new ArgumentNullException(nameof(dataProtectionProvider));
_jsonSerializerOptions = jsonSerializerOptions ?? DefaultJsonSerializerOptions.Instance;
}

/// <summary>
Expand Down Expand Up @@ -122,7 +129,7 @@ public ValueTask DeleteAsync(string key)

private string Protect(string purpose, object value)
{
var json = JsonSerializer.Serialize(value, options: JsonSerializerOptionsProvider.Options);
var json = JsonSerializer.Serialize(value, options: _jsonSerializerOptions);
var protector = GetOrCreateCachedProtector(purpose);

return protector.Protect(json);
Expand All @@ -133,7 +140,7 @@ private TValue Unprotect<TValue>(string purpose, string protectedJson)
var protector = GetOrCreateCachedProtector(purpose);
var json = protector.Unprotect(protectedJson);

return JsonSerializer.Deserialize<TValue>(json, options: JsonSerializerOptionsProvider.Options)!;
return JsonSerializer.Deserialize<TValue>(json, options: _jsonSerializerOptions)!;
}

private ValueTask SetProtectedJsonAsync(string key, string protectedJson)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;

namespace Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
Expand All @@ -23,7 +25,29 @@ public sealed class ProtectedLocalStorage : ProtectedBrowserStorage
/// <param name="jsRuntime">The <see cref="IJSRuntime"/>.</param>
/// <param name="dataProtectionProvider">The <see cref="IDataProtectionProvider"/>.</param>
public ProtectedLocalStorage(IJSRuntime jsRuntime, IDataProtectionProvider dataProtectionProvider)
: base("localStorage", jsRuntime, dataProtectionProvider)
: this(jsRuntime, dataProtectionProvider, jsonSerializerOptions: null)
{
}

/// <summary>
/// Constructs an instance of <see cref="ProtectedLocalStorage"/>.
/// </summary>
/// <param name="jsRuntime">The <see cref="IJSRuntime"/>.</param>
/// <param name="dataProtectionProvider">The <see cref="IDataProtectionProvider"/>.</param>
/// <param name="jsonOptions">The <see cref="JsonOptions"/>.</param>
public ProtectedLocalStorage(
IJSRuntime jsRuntime,
IDataProtectionProvider dataProtectionProvider,
IOptions<JsonOptions> jsonOptions)
: this(jsRuntime, dataProtectionProvider, jsonOptions.Value.SerializerOptions)
{
}

private ProtectedLocalStorage(
IJSRuntime jsRuntime,
IDataProtectionProvider dataProtectionProvider,
JsonSerializerOptions? jsonSerializerOptions)
: base("localStorage", jsRuntime, dataProtectionProvider, jsonSerializerOptions)
{
}
}
Loading
Loading