Skip to content

Commit

Permalink
[Blazor] Use JSON source generator during WebAssembly startup (#54956)
Browse files Browse the repository at this point in the history
  • Loading branch information
MackinnonBuck authored Apr 19, 2024
1 parent 23afddf commit c9af79a
Show file tree
Hide file tree
Showing 19 changed files with 238 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Microsoft.AspNetCore.Components;

// For custom converters that don't rely on serializing an object graph,
// we can resolve the incoming type's JsonTypeInfo directly from the converter.
// This skips extra work to collect metadata for the type that won't be used.
internal sealed class JsonConverterFactoryTypeInfoResolver<T> : IJsonTypeInfoResolver
{
public static readonly JsonConverterFactoryTypeInfoResolver<T> Instance = new();

private JsonConverterFactoryTypeInfoResolver()
{
}

public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
{
if (type != typeof(T))
{
return null;
}

foreach (var converter in options.Converters)
{
if (converter is not JsonConverterFactory factory || !factory.CanConvert(type))
{
continue;
}

if (factory.CreateConverter(type, options) is not { } converterToUse)
{
continue;
}

return JsonMetadataServices.CreateValueInfo<T>(options, converterToUse);
}

return null;
}
}
20 changes: 20 additions & 0 deletions src/Components/Web/src/Internal/IInternalWebJSInProcessRuntime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;

namespace Microsoft.AspNetCore.Components.Web.Internal;

/// <summary>
/// For internal framework use only.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IInternalWebJSInProcessRuntime
{
/// <summary>
/// For internal framework use only.
/// </summary>
string InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Compile Include="$(ComponentsSharedSourceRoot)src\AttributeUtilities.cs" LinkBase="Forms" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ExpressionFormatting\**\*.cs" LinkBase="Forms\ExpressionFommatting" />
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultAntiforgeryStateProvider.cs" LinkBase="Forms" />
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerialization\JsonConverterFactoryTypeInfoResolver.cs" LinkBase="JsonSerialization" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime
Microsoft.AspNetCore.Components.Web.Internal.IInternalWebJSInProcessRuntime.InvokeJS(string! identifier, string? argsJson, Microsoft.JSInterop.JSCallResultType resultType, long targetInstanceId) -> string!
Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.get -> bool
Microsoft.AspNetCore.Components.Web.KeyboardEventArgs.IsComposing.set -> void
55 changes: 49 additions & 6 deletions src/Components/Web/src/WebRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Infrastructure;
using Microsoft.AspNetCore.Components.Web.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
Expand Down Expand Up @@ -41,12 +43,7 @@ public WebRenderer(
// Supply a DotNetObjectReference to JS that it can use to call us back for events etc.
jsComponentInterop.AttachToRenderer(this);
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
jsRuntime.InvokeVoidAsync(
"Blazor._internal.attachWebRendererInterop",
_rendererId,
_interopMethodsReference,
jsComponentInterop.Configuration.JSComponentParametersByIdentifier,
jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer).Preserve();
AttachWebRendererInterop(jsRuntime, jsonOptions, jsComponentInterop);
}

/// <summary>
Expand Down Expand Up @@ -103,6 +100,44 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}

[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
private void AttachWebRendererInterop(IJSRuntime jsRuntime, JsonSerializerOptions jsonOptions, JSComponentInterop jsComponentInterop)
{
const string JSMethodIdentifier = "Blazor._internal.attachWebRendererInterop";

// These arguments should be kept in sync with WebRendererSerializerContext
object[] args = [
_rendererId,
_interopMethodsReference,
jsComponentInterop.Configuration.JSComponentParametersByIdentifier,
jsComponentInterop.Configuration.JSComponentIdentifiersByInitializer,
];

if (jsRuntime is IInternalWebJSInProcessRuntime inProcessRuntime)
{
// Fast path for WebAssembly: Rather than using the JSRuntime to serialize
// parameters, we utilize the source-generated WebRendererSerializerContext
// for a faster JsonTypeInfo resolution.

// We resolve a JsonTypeInfo for DotNetObjectReference<WebRendererInteropMethods> from
// the JS runtime's JsonConverters. This is because adding DotNetObjectReference<T> as
// a supported type in the JsonSerializerContext generates unnecessary code to produce
// JsonTypeInfo for all the types referenced by both DotNetObjectReference<T> and its
// generic type argument.

var newJsonOptions = new JsonSerializerOptions(jsonOptions);
newJsonOptions.TypeInfoResolverChain.Clear();
newJsonOptions.TypeInfoResolverChain.Add(WebRendererSerializerContext.Default);
newJsonOptions.TypeInfoResolverChain.Add(JsonConverterFactoryTypeInfoResolver<DotNetObjectReference<WebRendererInteropMethods>>.Instance);
var argsJson = JsonSerializer.Serialize(args, newJsonOptions);
inProcessRuntime.InvokeJS(JSMethodIdentifier, argsJson, JSCallResultType.JSVoidResult, 0);
}
else
{
jsRuntime.InvokeVoidAsync(JSMethodIdentifier, args).Preserve();
}
}
/// <summary>
/// A collection of JS invokable methods that the JS-side code can use when it needs to
/// make calls in the context of a particular renderer. This object is never exposed to
Expand Down Expand Up @@ -145,3 +180,11 @@ public void RemoveRootComponent(int componentId)
=> _jsComponentInterop.RemoveRootComponent(componentId);
}
}

// This should be kept in sync with the argument types in the call to
// 'Blazor._internal.attachWebRendererInterop'
[JsonSerializable(typeof(object[]))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(Dictionary<string, JSComponentConfigurationStore.JSComponentParameter[]>))]
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
internal sealed partial class WebRendererSerializerContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
{
private static readonly JsonSerializerOptions JsonOptions = new();

[Fact]
public void CanResolve_AccessTokenProvider()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
builder.Services.AddApiAuthorization();
var host = builder.Build();

Expand All @@ -27,7 +25,7 @@ public void CanResolve_AccessTokenProvider()
[Fact]
public void CanResolve_IRemoteAuthenticationService()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
builder.Services.AddApiAuthorization();
var host = builder.Build();

Expand All @@ -37,7 +35,7 @@ public void CanResolve_IRemoteAuthenticationService()
[Fact]
public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
builder.Services.AddApiAuthorization();
var host = builder.Build();

Expand Down Expand Up @@ -71,7 +69,7 @@ public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied()
[Fact]
public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
var calls = 0;
builder.Services.AddApiAuthorization(options =>
{
Expand All @@ -98,7 +96,7 @@ public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce()
[Fact]
public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
var calls = 0;
builder.Services.AddApiAuthorization<TestAuthenticationState>(options => calls++);

Expand All @@ -124,7 +122,7 @@ public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration()
[Fact]
public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
builder.Services.AddApiAuthorization<TestAuthenticationState>();

var host = builder.Build();
Expand All @@ -147,7 +145,7 @@ public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfigurati
[Fact]
public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
var calls = 0;
builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>(options => calls++);

Expand All @@ -173,7 +171,7 @@ public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfigurat
[Fact]
public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>();

var host = builder.Build();
Expand All @@ -196,7 +194,7 @@ public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpC
[Fact]
public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
builder.Services.AddApiAuthorization(options =>
{
options.AuthenticationPaths.LogInPath = "a";
Expand Down Expand Up @@ -247,7 +245,7 @@ public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
[Fact]
public void OidcOptions_ConfigurationDefaultsGetApplied()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
builder.Services.Replace(ServiceDescriptor.Singleton<NavigationManager, TestNavigationManager>());
builder.Services.AddOidcAuthentication(options => { });
var host = builder.Build();
Expand Down Expand Up @@ -286,7 +284,7 @@ public void OidcOptions_ConfigurationDefaultsGetApplied()
[Fact]
public void OidcOptions_DefaultsCanBeOverriden()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
builder.Services.AddOidcAuthentication(options =>
{
options.AuthenticationPaths.LogInPath = "a";
Expand Down Expand Up @@ -348,7 +346,7 @@ public void OidcOptions_DefaultsCanBeOverriden()
[Fact]
public void AddOidc_ConfigurationGetsCalledOnce()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
var calls = 0;

builder.Services.AddOidcAuthentication(options => calls++);
Expand All @@ -365,7 +363,7 @@ public void AddOidc_ConfigurationGetsCalledOnce()
[Fact]
public void AddOidc_CustomState_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
var calls = 0;

builder.Services.AddOidcAuthentication<TestAuthenticationState>(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture));
Expand All @@ -387,7 +385,7 @@ public void AddOidc_CustomState_SetsUpConfiguration()
[Fact]
public void AddOidc_CustomStateAndAccount_SetsUpConfiguration()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
var calls = 0;

builder.Services.AddOidcAuthentication<TestAuthenticationState, TestAccount>(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture));
Expand All @@ -409,7 +407,7 @@ public void AddOidc_CustomStateAndAccount_SetsUpConfiguration()
[Fact]
public void OidcProviderOptionsAndDependencies_NotResolvedFromRootScope()
{
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());

var calls = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,6 @@ private static void AddWebRootComponents(WebAssemblyRenderer renderer, RootCompo
operation.Descriptor!.Parameters));
}

WebAssemblyRenderer.NotifyEndUpdateRootComponents(operationBatch.BatchId);
renderer.NotifyEndUpdateRootComponents(operationBatch.BatchId);
}
}
Loading

0 comments on commit c9af79a

Please sign in to comment.