Skip to content

Commit 4dc663a

Browse files
authored
Blazor Byte Array Interop Support (#33015)
* Blazorserver Byte Array Interop Support * Byte Array Interop in WASM (#33104) * E2E Tests * PR Feedback
1 parent ef40046 commit 4dc663a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+864
-76
lines changed

src/Components/Server/src/BlazorPack/BlazorPackHubProtocolWorker.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Buffers;
56
using System.IO;
67
using MessagePack;
78
using Microsoft.AspNetCore.SignalR.Protocol;
@@ -34,6 +35,20 @@ protected override object DeserializeObject(ref MessagePackReader reader, Type t
3435
{
3536
return reader.ReadSingle();
3637
}
38+
else if (type == typeof(byte[]))
39+
{
40+
var bytes = reader.ReadBytes();
41+
if (!bytes.HasValue)
42+
{
43+
return null;
44+
}
45+
else if (bytes.Value.Length == 0)
46+
{
47+
return Array.Empty<byte>();
48+
}
49+
50+
return bytes.Value.ToArray();
51+
}
3752
}
3853
catch (Exception ex)
3954
{
@@ -75,6 +90,10 @@ protected override void Serialize(ref MessagePackWriter writer, Type type, objec
7590
writer.Write(bytes);
7691
break;
7792

93+
case byte[] byteArray:
94+
writer.Write(byteArray);
95+
break;
96+
7897
default:
7998
throw new FormatException($"Unsupported argument type {type}");
8099
}

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,31 @@ await Renderer.Dispatcher.InvokeAsync(() =>
388388
}
389389
}
390390

391+
// ReceiveByteArray is used in a fire-and-forget context, so it's responsible for its own
392+
// error handling.
393+
internal async Task ReceiveByteArray(int id, byte[] data)
394+
{
395+
AssertInitialized();
396+
AssertNotDisposed();
397+
398+
try
399+
{
400+
await Renderer.Dispatcher.InvokeAsync(() =>
401+
{
402+
Log.ReceiveByteArraySuccess(_logger, id);
403+
DotNetDispatcher.ReceiveByteArray(JSRuntime, id, data);
404+
});
405+
}
406+
catch (Exception ex)
407+
{
408+
// An error completing JS interop means that the user sent invalid data, a well-behaved
409+
// client won't do this.
410+
Log.ReceiveByteArrayException(_logger, id, ex);
411+
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Invalid byte array."));
412+
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
413+
}
414+
}
415+
391416
// DispatchEvent is used in a fire-and-forget context, so it's responsible for its own
392417
// error handling.
393418
public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson)
@@ -603,6 +628,8 @@ private static class Log
603628
private static readonly Action<ILogger, Exception> _endInvokeDispatchException;
604629
private static readonly Action<ILogger, long, string, Exception> _endInvokeJSFailed;
605630
private static readonly Action<ILogger, long, Exception> _endInvokeJSSucceeded;
631+
private static readonly Action<ILogger, long, Exception> _receiveByteArraySuccess;
632+
private static readonly Action<ILogger, long, Exception> _receiveByteArrayException;
606633
private static readonly Action<ILogger, Exception> _dispatchEventFailedToParseEventData;
607634
private static readonly Action<ILogger, string, Exception> _dispatchEventFailedToDispatchEvent;
608635
private static readonly Action<ILogger, string, CircuitId, Exception> _locationChange;
@@ -645,6 +672,8 @@ private static class EventIds
645672
public static readonly EventId LocationChangeFailed = new EventId(210, "LocationChangeFailed");
646673
public static readonly EventId LocationChangeFailedInCircuit = new EventId(211, "LocationChangeFailedInCircuit");
647674
public static readonly EventId OnRenderCompletedFailed = new EventId(212, "OnRenderCompletedFailed");
675+
public static readonly EventId ReceiveByteArraySucceeded = new EventId(213, "ReceiveByteArraySucceeded");
676+
public static readonly EventId ReceiveByteArrayException = new EventId(214, "ReceiveByteArrayException");
648677
}
649678

650679
static Log()
@@ -764,6 +793,16 @@ static Log()
764793
EventIds.EndInvokeJSSucceeded,
765794
"The JS interop call with callback id '{AsyncCall}' succeeded.");
766795

796+
_receiveByteArraySuccess = LoggerMessage.Define<long>(
797+
LogLevel.Debug,
798+
EventIds.ReceiveByteArraySucceeded,
799+
"The ReceiveByteArray call with id '{id}' succeeded.");
800+
801+
_receiveByteArrayException = LoggerMessage.Define<long>(
802+
LogLevel.Debug,
803+
EventIds.ReceiveByteArrayException,
804+
"The ReceiveByteArray call with id '{id}' failed.");
805+
767806
_dispatchEventFailedToParseEventData = LoggerMessage.Define(
768807
LogLevel.Debug,
769808
EventIds.DispatchEventFailedToParseEventData,
@@ -826,6 +865,8 @@ public static void CircuitHandlerFailed(ILogger logger, CircuitHandler handler,
826865
public static void EndInvokeDispatchException(ILogger logger, Exception ex) => _endInvokeDispatchException(logger, ex);
827866
public static void EndInvokeJSFailed(ILogger logger, long asyncHandle, string arguments) => _endInvokeJSFailed(logger, asyncHandle, arguments, null);
828867
public static void EndInvokeJSSucceeded(ILogger logger, long asyncCall) => _endInvokeJSSucceeded(logger, asyncCall, null);
868+
internal static void ReceiveByteArraySuccess(ILogger logger, long id) => _receiveByteArraySuccess(logger, id, null);
869+
internal static void ReceiveByteArrayException(ILogger logger, long id, Exception ex) => _receiveByteArrayException(logger, id, ex);
829870
public static void DispatchEventFailedToParseEventData(ILogger logger, Exception ex) => _dispatchEventFailedToParseEventData(logger, ex);
830871
public static void DispatchEventFailedToDispatchEvent(ILogger logger, string eventHandlerId, Exception ex) => _dispatchEventFailedToDispatchEvent(logger, eventHandlerId ?? "", ex);
831872

src/Components/Server/src/Circuits/RemoteJSRuntime.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,22 @@ internal class RemoteJSRuntime : JSRuntime
1717
private readonly ILogger<RemoteJSRuntime> _logger;
1818
private CircuitClientProxy _clientProxy;
1919
private bool _permanentlyDisconnected;
20+
private readonly long _maximumIncomingBytes;
21+
private int _byteArraysToBeRevivedTotalBytes;
2022

2123
public ElementReferenceContext ElementReferenceContext { get; }
2224

2325
public bool IsInitialized => _clientProxy is not null;
2426

25-
public RemoteJSRuntime(IOptions<CircuitOptions> options, ILogger<RemoteJSRuntime> logger)
27+
public RemoteJSRuntime(
28+
IOptions<CircuitOptions> circuitOptions,
29+
IOptions<HubOptions> hubOptions,
30+
ILogger<RemoteJSRuntime> logger)
2631
{
27-
_options = options.Value;
32+
_options = circuitOptions.Value;
33+
_maximumIncomingBytes = hubOptions.Value.MaximumReceiveMessageSize is null
34+
? long.MaxValue
35+
: hubOptions.Value.MaximumReceiveMessageSize.Value;
2836
_logger = logger;
2937
DefaultAsyncTimeout = _options.JSInteropDefaultCallTimeout;
3038
ElementReferenceContext = new WebElementReferenceContext(this);
@@ -75,6 +83,11 @@ protected override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in
7583
}
7684
}
7785

86+
protected override void SendByteArray(int id, byte[] data)
87+
{
88+
_clientProxy.SendAsync("JS.ReceiveByteArray", id, data);
89+
}
90+
7891
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
7992
{
8093
if (_clientProxy is null)
@@ -99,6 +112,28 @@ protected override void BeginInvokeJS(long asyncHandle, string identifier, strin
99112
_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson, (int)resultType, targetInstanceId);
100113
}
101114

115+
protected override void ReceiveByteArray(int id, byte[] data)
116+
{
117+
if (id == 0)
118+
{
119+
// Starting a new transfer, clear out number of bytes read.
120+
_byteArraysToBeRevivedTotalBytes = 0;
121+
}
122+
123+
if (_maximumIncomingBytes - data.Length < _byteArraysToBeRevivedTotalBytes)
124+
{
125+
throw new ArgumentOutOfRangeException("Exceeded the maximum byte array transfer limit for a call.");
126+
}
127+
128+
// We also store the total number of bytes seen so far to compare against
129+
// the MaximumIncomingBytes limit.
130+
// We take the larger of the size of the array or 4, to ensure we're not inundated
131+
// with small/empty arrays.
132+
_byteArraysToBeRevivedTotalBytes += Math.Max(4, data.Length);
133+
134+
base.ReceiveByteArray(id, data);
135+
}
136+
102137
public void MarkPermanentlyDisconnected()
103138
{
104139
_permanentlyDisconnected = true;

src/Components/Server/src/ComponentHub.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,17 @@ public async ValueTask EndInvokeJSFromDotNet(long asyncHandle, bool succeeded, s
209209
_ = circuitHost.EndInvokeJSFromDotNet(asyncHandle, succeeded, arguments);
210210
}
211211

212+
public async ValueTask ReceiveByteArray(int id, byte[] data)
213+
{
214+
var circuitHost = await GetActiveCircuitAsync();
215+
if (circuitHost == null)
216+
{
217+
return;
218+
}
219+
220+
await circuitHost.ReceiveByteArray(id, data);
221+
}
222+
212223
public async ValueTask DispatchBrowserEvent(string eventDescriptor, string eventArgs)
213224
{
214225
var circuitHost = await GetActiveCircuitAsync();

src/Components/Server/test/Circuits/CircuitHostTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Reflection;
88
using System.Threading;
99
using System.Threading.Tasks;
10-
using Microsoft.AspNetCore.Components.Web.Rendering;
1110
using Microsoft.AspNetCore.DataProtection;
1211
using Microsoft.AspNetCore.SignalR;
1312
using Microsoft.Extensions.DependencyInjection;

src/Components/Server/test/Circuits/TestCircuitHost.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using Microsoft.AspNetCore.Components.Web.Rendering;
76
using Microsoft.AspNetCore.SignalR;
87
using Microsoft.Extensions.DependencyInjection;
98
using Microsoft.Extensions.Logging;
@@ -29,7 +28,7 @@ public static CircuitHost Create(
2928
{
3029
serviceScope = serviceScope ?? new AsyncServiceScope(Mock.Of<IServiceScope>());
3130
clientProxy = clientProxy ?? new CircuitClientProxy(Mock.Of<IClientProxy>(), Guid.NewGuid().ToString());
32-
var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()), Mock.Of<ILogger<RemoteJSRuntime>>());
31+
var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of<ILogger<RemoteJSRuntime>>());
3332

3433
if (remoteRenderer == null)
3534
{
@@ -42,7 +41,7 @@ public static CircuitHost Create(
4241
null);
4342
}
4443

45-
handlers = handlers ?? Array.Empty<CircuitHandler>();
44+
handlers ??= Array.Empty<CircuitHandler>();
4645
return new TestCircuitHost(
4746
circuitId is null ? new CircuitId(Guid.NewGuid().ToString(), Guid.NewGuid().ToString()) : circuitId.Value,
4847
serviceScope.Value,

src/Components/Shared/src/ArrayBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace Ignitor
1414
namespace Microsoft.AspNetCore.Components.WebView
1515
#elif COMPONENTS_SERVER
1616
namespace Microsoft.AspNetCore.Components.Server.Circuits
17+
#elif JS_INTEROP
18+
namespace Microsoft.JSInterop.Infrastructure
1719
#else
1820
namespace Microsoft.AspNetCore.Components.RenderTree
1921
#endif

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webview.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
105105
connection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId));
106106
connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
107107
connection.on('JS.EndInvokeDotNet', DotNet.jsCallDispatcher.endInvokeDotNetFromJS);
108+
connection.on('JS.ReceiveByteArray', DotNet.jsCallDispatcher.receiveByteArray);
108109

109110
const renderQueue = RenderQueue.getOrCreate(logger);
110111
connection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => {
@@ -134,6 +135,9 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
134135
endInvokeJSFromDotNet: (asyncHandle, succeeded, argsJson): void => {
135136
connection.send('EndInvokeJSFromDotNet', asyncHandle, succeeded, argsJson);
136137
},
138+
sendByteArray: (id: number, data: Uint8Array): void => {
139+
connection.send('ReceiveByteArray', id, data);
140+
},
137141
});
138142

139143
return connection;

0 commit comments

Comments
 (0)