Use JsonConverter for DotNetObjectRef
\n\nCommit migrated from 0389bf44d4
This commit is contained in:
parent
b4bcb1fd15
commit
2d2400e7fa
|
|
@ -17,12 +17,8 @@ namespace Microsoft.JSInterop
|
|||
}
|
||||
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
|
||||
{
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public DotNetObjectRef() { }
|
||||
[System.Text.Json.Serialization.JsonIgnoreAttribute]
|
||||
internal DotNetObjectRef() { }
|
||||
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public long __dotNetObject { get { throw null; } set { } }
|
||||
public void Dispose() { }
|
||||
}
|
||||
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ namespace Microsoft.JSInterop
|
|||
/// </summary>
|
||||
public static class DotNetDispatcher
|
||||
{
|
||||
internal const string DotNetObjectRefKey = nameof(DotNetObjectRef<object>.__dotNetObject);
|
||||
internal static readonly JsonEncodedText DotNetObjectRefKey = JsonEncodedText.Encode("__dotNetObject");
|
||||
private static readonly Type[] EndInvokeParameterTypes = new Type[] { typeof(long), typeof(bool), typeof(JSAsyncCallResult) };
|
||||
|
||||
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
|
||||
|
|
@ -238,7 +238,7 @@ namespace Microsoft.JSInterop
|
|||
// an incorrect use if there's a object that looks like { '__dotNetObject': <some number> },
|
||||
// but we aren't assigning to DotNetObjectRef{T}.
|
||||
return item.ValueKind == JsonValueKind.Object &&
|
||||
item.TryGetProperty(DotNetObjectRefKey, out _) &&
|
||||
item.TryGetProperty(DotNetObjectRefKey.EncodedUtf8Bytes, out _) &&
|
||||
!typeof(IDotNetObjectRef).IsAssignableFrom(parameterType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ namespace Microsoft.JSInterop
|
|||
/// <returns>An instance of <see cref="DotNetObjectRef{TValue}" />.</returns>
|
||||
public static DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class
|
||||
{
|
||||
return new DotNetObjectRef<TValue>(value);
|
||||
var objectId = DotNetObjectRefManager.Current.TrackObject(value);
|
||||
return new DotNetObjectRef<TValue>(objectId, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
internal sealed class DotNetObjectReferenceJsonConverterFactory : JsonConverterFactory
|
||||
{
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
Debug.Assert(
|
||||
typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectRef<>),
|
||||
"We expect this to only be called for DotNetObjectRef<T> instances.");
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override JsonConverter CreateConverter(Type typeToConvert)
|
||||
{
|
||||
// System.Text.Json handles caching the converters per type on our behalf. No caching is required here.
|
||||
var instanceType = typeToConvert.GetGenericArguments()[0];
|
||||
var converterType = typeof(DotNetObjectReferenceJsonConverter<>).MakeGenericType(instanceType);
|
||||
|
||||
return (JsonConverter)Activator.CreateInstance(converterType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ namespace Microsoft.JSInterop
|
|||
internal class DotNetObjectRefManager
|
||||
{
|
||||
private long _nextId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1
|
||||
private readonly ConcurrentDictionary<long, IDotNetObjectRef> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectRef>();
|
||||
private readonly ConcurrentDictionary<long, object> _trackedRefsById = new ConcurrentDictionary<long, object>();
|
||||
|
||||
public static DotNetObjectRefManager Current
|
||||
{
|
||||
|
|
@ -25,7 +25,7 @@ namespace Microsoft.JSInterop
|
|||
}
|
||||
}
|
||||
|
||||
public long TrackObject(IDotNetObjectRef dotNetObjectRef)
|
||||
public long TrackObject(object dotNetObjectRef)
|
||||
{
|
||||
var dotNetObjectId = Interlocked.Increment(ref _nextId);
|
||||
_trackedRefsById[dotNetObjectId] = dotNetObjectRef;
|
||||
|
|
@ -36,7 +36,7 @@ namespace Microsoft.JSInterop
|
|||
public object FindDotNetObject(long dotNetObjectId)
|
||||
{
|
||||
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
|
||||
? dotNetObjectRef.Value
|
||||
? dotNetObjectRef
|
||||
: throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectRef instance was already disposed.", nameof(dotNetObjectId));
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
|
|
@ -14,54 +13,26 @@ namespace Microsoft.JSInterop
|
|||
/// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the value to wrap.</typeparam>
|
||||
[JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))]
|
||||
public sealed class DotNetObjectRef<TValue> : IDotNetObjectRef, IDisposable where TValue : class
|
||||
{
|
||||
private long? _trackingId;
|
||||
|
||||
/// <summary>
|
||||
/// This API is for meant for JSON deserialization and should not be used by user code.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public DotNetObjectRef()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="DotNetObjectRef{TValue}" />.
|
||||
/// </summary>
|
||||
/// <param name="objectId">The object Id.</param>
|
||||
/// <param name="value">The value to pass by reference.</param>
|
||||
internal DotNetObjectRef(TValue value)
|
||||
internal DotNetObjectRef(long objectId, TValue value)
|
||||
{
|
||||
ObjectId = objectId;
|
||||
Value = value;
|
||||
_trackingId = DotNetObjectRefManager.Current.TrackObject(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object instance represented by this wrapper.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public TValue Value { get; private set; }
|
||||
public TValue Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This API is for meant for JSON serialization and should not be used by user code.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public long __dotNetObject
|
||||
{
|
||||
get => _trackingId.Value;
|
||||
set
|
||||
{
|
||||
if (_trackingId != null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(DotNetObjectRef<TValue>)} cannot be reinitialized.");
|
||||
}
|
||||
|
||||
_trackingId = value;
|
||||
Value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(value);
|
||||
}
|
||||
}
|
||||
|
||||
object IDotNetObjectRef.Value => Value;
|
||||
internal long ObjectId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stops tracking this object reference, allowing it to be garbage collected
|
||||
|
|
@ -70,7 +41,7 @@ namespace Microsoft.JSInterop
|
|||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
DotNetObjectRefManager.Current.ReleaseDotNetObject(_trackingId.Value);
|
||||
DotNetObjectRefManager.Current.ReleaseDotNetObject(ObjectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
internal sealed class DotNetObjectReferenceJsonConverter<TValue> : JsonConverter<DotNetObjectRef<TValue>> where TValue : class
|
||||
{
|
||||
private static JsonEncodedText DotNetObjectRefKey => DotNetDispatcher.DotNetObjectRefKey;
|
||||
|
||||
public override DotNetObjectRef<TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (!reader.Read())
|
||||
{
|
||||
throw new InvalidDataException("Invalid DotNetObjectRef JSON.");
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.PropertyName || !reader.ValueTextEquals(DotNetObjectRefKey.EncodedUtf8Bytes))
|
||||
{
|
||||
throw new InvalidDataException("Invalid DotNetObjectRef JSON.");
|
||||
}
|
||||
|
||||
if (!reader.Read())
|
||||
{
|
||||
throw new InvalidDataException("Invalid DotNetObjectRef JSON.");
|
||||
}
|
||||
|
||||
var dotNetObjectId = reader.GetInt64();
|
||||
|
||||
if (!reader.Read())
|
||||
{
|
||||
// We need to read all the data that was given to us.
|
||||
throw new InvalidDataException("Invalid DotNetObjectRef JSON.");
|
||||
}
|
||||
|
||||
var value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
|
||||
return new DotNetObjectRef<TValue>(dotNetObjectId, value);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DotNetObjectRef<TValue> value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteNumber(DotNetObjectRefKey, value.ObjectId);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,5 @@ namespace Microsoft.JSInterop
|
|||
{
|
||||
internal interface IDotNetObjectRef : IDisposable
|
||||
{
|
||||
public object Value { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
internal class TestJSRuntime : JSRuntimeBase
|
||||
{
|
||||
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static async Task WithJSRuntime(Action<JSRuntimeBase> testCode)
|
||||
{
|
||||
// Since the tests rely on the asynclocal JSRuntime.Current, ensure we
|
||||
// are on a distinct async context with a non-null JSRuntime.Current
|
||||
await Task.Yield();
|
||||
|
||||
var runtime = new TestJSRuntime();
|
||||
JSRuntime.SetCurrentJSRuntime(runtime);
|
||||
testCode(runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@ namespace Microsoft.JSInterop.Tests
|
|||
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.StringVal), out _));
|
||||
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _));
|
||||
|
||||
Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey, out var property));
|
||||
Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property));
|
||||
var resultDto2 = Assert.IsType<TestDTO>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64()));
|
||||
Assert.Equal("MY STRING", resultDto2.StringVal);
|
||||
Assert.Equal(1299, resultDto2.IntVal);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using static Microsoft.JSInterop.TestJSRuntime;
|
||||
|
||||
namespace Microsoft.JSInterop.Tests
|
||||
namespace Microsoft.JSInterop
|
||||
{
|
||||
public class DotNetObjectRefTest
|
||||
{
|
||||
|
|
@ -21,21 +22,14 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
// Arrange
|
||||
var objRef = DotNetObjectRef.Create(new object());
|
||||
var trackingId = objRef.__dotNetObject;
|
||||
|
||||
// Act
|
||||
objRef.Dispose();
|
||||
|
||||
// Assert
|
||||
var ex = Assert.Throws<ArgumentException>(() => jsRuntime.ObjectRefManager.FindDotNetObject(trackingId));
|
||||
var ex = Assert.Throws<ArgumentException>(() => jsRuntime.ObjectRefManager.FindDotNetObject(objRef.ObjectId));
|
||||
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
|
||||
});
|
||||
|
||||
class TestJSRuntime : JSRuntimeBase
|
||||
{
|
||||
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using static Microsoft.JSInterop.TestJSRuntime;
|
||||
|
||||
namespace Microsoft.JSInterop.Tests
|
||||
{
|
||||
public class DotNetObjectReferenceJsonConverterTest
|
||||
{
|
||||
[Fact]
|
||||
public Task Read_ReadsJson() => WithJSRuntime(_ =>
|
||||
{
|
||||
// Arrange
|
||||
// Throw-away value
|
||||
DotNetObjectRef.Create(new TestModel());
|
||||
var input = new TestModel();
|
||||
var dotNetObjectRef = DotNetObjectRef.Create(input);
|
||||
var objectId = dotNetObjectRef.ObjectId;
|
||||
|
||||
var json = $"{{\"__dotNetObject\":{objectId}}}";
|
||||
|
||||
// Act
|
||||
var deserialized = JsonSerializer.Deserialize<DotNetObjectRef<TestModel>>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Same(input, deserialized.Value);
|
||||
Assert.Equal(objectId, deserialized.ObjectId);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task Read_ReadsJson_WithFormatting() => WithJSRuntime(_ =>
|
||||
{
|
||||
// Arrange
|
||||
// Throw-away value
|
||||
DotNetObjectRef.Create(new TestModel());
|
||||
var input = new TestModel();
|
||||
var dotNetObjectRef = DotNetObjectRef.Create(input);
|
||||
var objectId = dotNetObjectRef.ObjectId;
|
||||
|
||||
var json =
|
||||
@$"{{
|
||||
""__dotNetObject"": {objectId}
|
||||
}}";
|
||||
|
||||
// Act
|
||||
var deserialized = JsonSerializer.Deserialize<DotNetObjectRef<TestModel>>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Same(input, deserialized.Value);
|
||||
Assert.Equal(objectId, deserialized.ObjectId);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task WriteJsonTwice_KeepsObjectId() => WithJSRuntime(_ =>
|
||||
{
|
||||
// Arrange
|
||||
// Throw-away value
|
||||
DotNetObjectRef.Create(new TestModel());
|
||||
var dotNetObjectRef = DotNetObjectRef.Create(new TestModel());
|
||||
|
||||
// Act
|
||||
var json1 = JsonSerializer.Serialize(dotNetObjectRef);
|
||||
var json2 = JsonSerializer.Serialize(dotNetObjectRef);
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"{{\"__dotNetObject\":{dotNetObjectRef.ObjectId}}}", json1);
|
||||
Assert.Equal(json1, json2);
|
||||
});
|
||||
|
||||
private class TestModel
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue