Use JsonConverter for DotNetObjectRef

\n\nCommit migrated from 0389bf44d4
This commit is contained in:
Pranav K 2019-07-16 15:21:28 -07:00
parent b4bcb1fd15
commit 2d2400e7fa
12 changed files with 204 additions and 58 deletions

View File

@ -17,12 +17,8 @@ namespace Microsoft.JSInterop
} }
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
{ {
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] internal DotNetObjectRef() { }
public DotNetObjectRef() { }
[System.Text.Json.Serialization.JsonIgnoreAttribute]
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } 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 void Dispose() { }
} }
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime

View File

@ -17,7 +17,7 @@ namespace Microsoft.JSInterop
/// </summary> /// </summary>
public static class DotNetDispatcher 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 Type[] EndInvokeParameterTypes = new Type[] { typeof(long), typeof(bool), typeof(JSAsyncCallResult) };
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly 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> }, // an incorrect use if there's a object that looks like { '__dotNetObject': <some number> },
// but we aren't assigning to DotNetObjectRef{T}. // but we aren't assigning to DotNetObjectRef{T}.
return item.ValueKind == JsonValueKind.Object && return item.ValueKind == JsonValueKind.Object &&
item.TryGetProperty(DotNetObjectRefKey, out _) && item.TryGetProperty(DotNetObjectRefKey.EncodedUtf8Bytes, out _) &&
!typeof(IDotNetObjectRef).IsAssignableFrom(parameterType); !typeof(IDotNetObjectRef).IsAssignableFrom(parameterType);
} }
} }

View File

@ -15,7 +15,8 @@ namespace Microsoft.JSInterop
/// <returns>An instance of <see cref="DotNetObjectRef{TValue}" />.</returns> /// <returns>An instance of <see cref="DotNetObjectRef{TValue}" />.</returns>
public static DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class 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);
} }
} }
} }

View File

@ -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);
}
}
}

View File

@ -10,7 +10,7 @@ namespace Microsoft.JSInterop
internal class DotNetObjectRefManager 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 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 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); var dotNetObjectId = Interlocked.Increment(ref _nextId);
_trackedRefsById[dotNetObjectId] = dotNetObjectRef; _trackedRefsById[dotNetObjectId] = dotNetObjectRef;
@ -36,7 +36,7 @@ namespace Microsoft.JSInterop
public object FindDotNetObject(long dotNetObjectId) public object FindDotNetObject(long dotNetObjectId)
{ {
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) 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)); : throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectRef instance was already disposed.", nameof(dotNetObjectId));
} }

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.ComponentModel;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Microsoft.JSInterop 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. /// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code.
/// </summary> /// </summary>
/// <typeparam name="TValue">The type of the value to wrap.</typeparam> /// <typeparam name="TValue">The type of the value to wrap.</typeparam>
[JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))]
public sealed class DotNetObjectRef<TValue> : IDotNetObjectRef, IDisposable where TValue : class 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> /// <summary>
/// Initializes a new instance of <see cref="DotNetObjectRef{TValue}" />. /// Initializes a new instance of <see cref="DotNetObjectRef{TValue}" />.
/// </summary> /// </summary>
/// <param name="objectId">The object Id.</param>
/// <param name="value">The value to pass by reference.</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; Value = value;
_trackingId = DotNetObjectRefManager.Current.TrackObject(this);
} }
/// <summary> /// <summary>
/// Gets the object instance represented by this wrapper. /// Gets the object instance represented by this wrapper.
/// </summary> /// </summary>
[JsonIgnore] public TValue Value { get; }
public TValue Value { get; private set; }
/// <summary> internal long ObjectId { get; }
/// 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;
/// <summary> /// <summary>
/// Stops tracking this object reference, allowing it to be garbage collected /// Stops tracking this object reference, allowing it to be garbage collected
@ -70,7 +41,7 @@ namespace Microsoft.JSInterop
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
DotNetObjectRefManager.Current.ReleaseDotNetObject(_trackingId.Value); DotNetObjectRefManager.Current.ReleaseDotNetObject(ObjectId);
} }
} }
} }

View File

@ -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();
}
}
}

View File

@ -7,6 +7,5 @@ namespace Microsoft.JSInterop
{ {
internal interface IDotNetObjectRef : IDisposable internal interface IDotNetObjectRef : IDisposable
{ {
public object Value { get; }
} }
} }

View File

@ -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);
}
}
}

View File

@ -142,7 +142,7 @@ namespace Microsoft.JSInterop.Tests
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.StringVal), out _)); Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.StringVal), out _));
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), 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())); var resultDto2 = Assert.IsType<TestDTO>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64()));
Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal("MY STRING", resultDto2.StringVal);
Assert.Equal(1299, resultDto2.IntVal); Assert.Equal(1299, resultDto2.IntVal);

View File

@ -4,8 +4,9 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
using static Microsoft.JSInterop.TestJSRuntime;
namespace Microsoft.JSInterop.Tests namespace Microsoft.JSInterop
{ {
public class DotNetObjectRefTest public class DotNetObjectRefTest
{ {
@ -21,21 +22,14 @@ namespace Microsoft.JSInterop.Tests
{ {
// Arrange // Arrange
var objRef = DotNetObjectRef.Create(new object()); var objRef = DotNetObjectRef.Create(new object());
var trackingId = objRef.__dotNetObject;
// Act // Act
objRef.Dispose(); objRef.Dispose();
// Assert // 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); 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) protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId)

View File

@ -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
{
}
}
}