From 2d2400e7faf5c354c785b606e8a59afadbbb9c19 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 Jul 2019 15:21:28 -0700 Subject: [PATCH 1/3] Use JsonConverter for DotNetObjectRef \n\nCommit migrated from https://github.com/dotnet/extensions/commit/0389bf44d4833843801061c744599b26e32268d8 --- .../ref/Microsoft.JSInterop.netstandard2.0.cs | 6 +- .../src/DotNetDispatcher.cs | 4 +- .../src/DotNetObjectRef.cs | 3 +- .../DotNetObjectRefJsonConverterFactory.cs | 29 +++++++ .../src/DotNetObjectRefManager.cs | 6 +- .../src/DotNetObjectRefOfT.cs | 43 ++-------- .../src/DotNetObjectReferenceJsonConverter.cs | 51 ++++++++++++ .../src/IDotNetObjectRef.cs | 1 - .../Microsoft.JSInterop/test/Class1.cs | 27 +++++++ .../test/DotNetDispatcherTest.cs | 2 +- .../test/DotNetObjectRefTest.cs | 12 +-- .../DotNetObjectReferenceJsonConverterTest.cs | 78 +++++++++++++++++++ 12 files changed, 204 insertions(+), 58 deletions(-) create mode 100644 src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs create mode 100644 src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs create mode 100644 src/JSInterop/Microsoft.JSInterop/test/Class1.cs create mode 100644 src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs index 125a555c9d..654ae9d617 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -17,12 +17,8 @@ namespace Microsoft.JSInterop } public sealed partial class DotNetObjectRef : 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 diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs index 60b6ad9ab0..3de8f55882 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs @@ -17,7 +17,7 @@ namespace Microsoft.JSInterop /// public static class DotNetDispatcher { - internal const string DotNetObjectRefKey = nameof(DotNetObjectRef.__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> _cachedMethodsByAssembly @@ -238,7 +238,7 @@ namespace Microsoft.JSInterop // an incorrect use if there's a object that looks like { '__dotNetObject': }, // 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); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs index 1aabc5ad59..af790281e9 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs @@ -15,7 +15,8 @@ namespace Microsoft.JSInterop /// An instance of . public static DotNetObjectRef Create(TValue value) where TValue : class { - return new DotNetObjectRef(value); + var objectId = DotNetObjectRefManager.Current.TrackObject(value); + return new DotNetObjectRef(objectId, value); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs new file mode 100644 index 0000000000..3bc1f83c7e --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs @@ -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 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); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs index f263716f53..ad1469e38f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs @@ -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 _trackedRefsById = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _trackedRefsById = new ConcurrentDictionary(); 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)); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs index 8b7035e957..be6bf91663 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs @@ -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. /// /// The type of the value to wrap. + [JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))] public sealed class DotNetObjectRef : IDotNetObjectRef, IDisposable where TValue : class { - private long? _trackingId; - - /// - /// This API is for meant for JSON deserialization and should not be used by user code. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public DotNetObjectRef() - { - } - /// /// Initializes a new instance of . /// + /// The object Id. /// The value to pass by reference. - internal DotNetObjectRef(TValue value) + internal DotNetObjectRef(long objectId, TValue value) { + ObjectId = objectId; Value = value; - _trackingId = DotNetObjectRefManager.Current.TrackObject(this); } /// /// Gets the object instance represented by this wrapper. /// - [JsonIgnore] - public TValue Value { get; private set; } + public TValue Value { get; } - /// - /// This API is for meant for JSON serialization and should not be used by user code. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public long __dotNetObject - { - get => _trackingId.Value; - set - { - if (_trackingId != null) - { - throw new InvalidOperationException($"{nameof(DotNetObjectRef)} cannot be reinitialized."); - } - - _trackingId = value; - Value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(value); - } - } - - object IDotNetObjectRef.Value => Value; + internal long ObjectId { get; } /// /// Stops tracking this object reference, allowing it to be garbage collected @@ -70,7 +41,7 @@ namespace Microsoft.JSInterop /// public void Dispose() { - DotNetObjectRefManager.Current.ReleaseDotNetObject(_trackingId.Value); + DotNetObjectRefManager.Current.ReleaseDotNetObject(ObjectId); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs new file mode 100644 index 0000000000..d07735734e --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs @@ -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 : JsonConverter> where TValue : class + { + private static JsonEncodedText DotNetObjectRefKey => DotNetDispatcher.DotNetObjectRefKey; + + public override DotNetObjectRef 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(dotNetObjectId, value); + } + + public override void Write(Utf8JsonWriter writer, DotNetObjectRef value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber(DotNetObjectRefKey, value.ObjectId); + writer.WriteEndObject(); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs index 5f21808a9f..b082d0ce10 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs @@ -7,6 +7,5 @@ namespace Microsoft.JSInterop { internal interface IDotNetObjectRef : IDisposable { - public object Value { get; } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/Class1.cs b/src/JSInterop/Microsoft.JSInterop/test/Class1.cs new file mode 100644 index 0000000000..1e58139b36 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/Class1.cs @@ -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 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); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs index a4a0de5a3f..1c73cc63b2 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs @@ -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(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64())); Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(1299, resultDto2.IntVal); diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs index 77af280d94..f417985746 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs @@ -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(() => jsRuntime.ObjectRefManager.FindDotNetObject(trackingId)); + var ex = Assert.Throws(() => 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) diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs new file mode 100644 index 0000000000..c2dbd88895 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs @@ -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>(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>(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 + { + + } + } +} From 950b3873b138a7f27bc2d41200319785bda093df Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 Jul 2019 16:15:48 -0700 Subject: [PATCH 2/3] PR comments \n\nCommit migrated from https://github.com/dotnet/extensions/commit/791c96b143d3f4e1e0b507cd0283cdf09ac0adb6 --- .../DotNetObjectRefJsonConverterFactory.cs | 9 ++-- .../src/DotNetObjectReferenceJsonConverter.cs | 39 +++++++------- .../test/DotNetObjectRefTest.cs | 18 ------- .../DotNetObjectReferenceJsonConverterTest.cs | 52 +++++++++++++++++-- .../test/{Class1.cs => TestJSRuntime.cs} | 5 ++ 5 files changed, 76 insertions(+), 47 deletions(-) rename src/JSInterop/Microsoft.JSInterop/test/{Class1.cs => TestJSRuntime.cs} (78%) diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs index 3bc1f83c7e..5cfec5be9d 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs @@ -2,7 +2,7 @@ // 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; using System.Text.Json.Serialization; namespace Microsoft.JSInterop @@ -11,13 +11,10 @@ namespace Microsoft.JSInterop { public override bool CanConvert(Type typeToConvert) { - Debug.Assert( - typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectRef<>), - "We expect this to only be called for DotNetObjectRef instances."); - return true; + return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectRef<>); } - protected override JsonConverter CreateConverter(Type typeToConvert) + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions jsonSerializerOptions) { // System.Text.Json handles caching the converters per type on our behalf. No caching is required here. var instanceType = typeToConvert.GetGenericArguments()[0]; diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs index d07735734e..487b4c77b0 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs @@ -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.IO; using System.Text.Json; using System.Text.Json.Serialization; @@ -14,27 +13,31 @@ namespace Microsoft.JSInterop public override DotNetObjectRef Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (!reader.Read()) + long dotNetObjectId = 0; + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - throw new InvalidDataException("Invalid DotNetObjectRef JSON."); + if (reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.ValueTextEquals(DotNetObjectRefKey.EncodedUtf8Bytes)) + { + reader.Read(); + dotNetObjectId = reader.GetInt64(); + } + else + { + throw new JsonException($"Unexcepted JSON property {reader.GetString()}."); + } + } + else + { + throw new JsonException($"Unexcepted JSON Token {reader.TokenType}."); + } } - if (reader.TokenType != JsonTokenType.PropertyName || !reader.ValueTextEquals(DotNetObjectRefKey.EncodedUtf8Bytes)) + if (dotNetObjectId is 0) { - 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."); + throw new JsonException($"Required property {DotNetObjectRefKey} not found."); } var value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs index f417985746..112363869e 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs @@ -30,23 +30,5 @@ namespace Microsoft.JSInterop var ex = Assert.Throws(() => jsRuntime.ObjectRefManager.FindDotNetObject(objRef.ObjectId)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); }); - } - - protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) - { - throw new NotImplementedException(); - } - } - - async Task WithJSRuntime(Action 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); - } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs index c2dbd88895..be1d2bf975 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs @@ -10,12 +10,58 @@ namespace Microsoft.JSInterop.Tests { public class DotNetObjectReferenceJsonConverterTest { + [Fact] + public Task Read_Throws_IfJsonIsMissingDotNetObjectProperty() => WithJSRuntime(_ => + { + // Arrange + var dotNetObjectRef = DotNetObjectRef.Create(new TestModel()); + + var json = "{}"; + + // Act & Assert + var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json)); + Assert.Equal("Required property __dotNetObject not found.", ex.Message); + }); + + [Fact] + public Task Read_Throws_IfJsonContainsUnknownContent() => WithJSRuntime(_ => + { + // Arrange + var dotNetObjectRef = DotNetObjectRef.Create(new TestModel()); + + var json = "{\"foo\":2}"; + + // Act & Assert + var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json)); + Assert.Equal("Unexcepted JSON property foo.", ex.Message); + }); + [Fact] public Task Read_ReadsJson() => WithJSRuntime(_ => { // Arrange - // Throw-away value + var input = new TestModel(); + var dotNetObjectRef = DotNetObjectRef.Create(input); + var objectId = dotNetObjectRef.ObjectId; + + var json = $"{{\"__dotNetObject\":{objectId}}}"; + + // Act + var deserialized = JsonSerializer.Deserialize>(json); + + // Assert + Assert.Same(input, deserialized.Value); + Assert.Equal(objectId, deserialized.ObjectId); + }); + + [Fact] + public Task Read_ReturnsTheCorrectInstance() => WithJSRuntime(_ => + { + // Arrange + // Track a few instances and verify that the deserialized value returns the corect value. DotNetObjectRef.Create(new TestModel()); + DotNetObjectRef.Create(new TestModel()); + var input = new TestModel(); var dotNetObjectRef = DotNetObjectRef.Create(input); var objectId = dotNetObjectRef.ObjectId; @@ -34,8 +80,6 @@ namespace Microsoft.JSInterop.Tests 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; @@ -57,8 +101,6 @@ namespace Microsoft.JSInterop.Tests public Task WriteJsonTwice_KeepsObjectId() => WithJSRuntime(_ => { // Arrange - // Throw-away value - DotNetObjectRef.Create(new TestModel()); var dotNetObjectRef = DotNetObjectRef.Create(new TestModel()); // Act diff --git a/src/JSInterop/Microsoft.JSInterop/test/Class1.cs b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs similarity index 78% rename from src/JSInterop/Microsoft.JSInterop/test/Class1.cs rename to src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs index 1e58139b36..c4e6b05c5b 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Class1.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs @@ -13,6 +13,11 @@ namespace Microsoft.JSInterop throw new NotImplementedException(); } + protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) + { + throw new NotImplementedException(); + } + public static async Task WithJSRuntime(Action testCode) { // Since the tests rely on the asynclocal JSRuntime.Current, ensure we From 39c052b8bce2628a430697f4fb16d4207872ef80 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 23 Jul 2019 19:36:16 -0700 Subject: [PATCH 3/3] More \n\nCommit migrated from https://github.com/dotnet/extensions/commit/0cf3c01afc54980d5d49d31b233b01eafd60e104 --- .../src/DotNetObjectReferenceJsonConverter.cs | 2 +- .../DotNetObjectReferenceJsonConverterTest.cs | 51 +++++++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs index 487b4c77b0..eaabdbf9e6 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs @@ -19,7 +19,7 @@ namespace Microsoft.JSInterop { if (reader.TokenType == JsonTokenType.PropertyName) { - if (reader.ValueTextEquals(DotNetObjectRefKey.EncodedUtf8Bytes)) + if (dotNetObjectId == 0 && reader.ValueTextEquals(DotNetObjectRefKey.EncodedUtf8Bytes)) { reader.Read(); dotNetObjectId = reader.GetInt64(); diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs index be1d2bf975..18f3db55c1 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs @@ -36,6 +36,36 @@ namespace Microsoft.JSInterop.Tests Assert.Equal("Unexcepted JSON property foo.", ex.Message); }); + [Fact] + public Task Read_Throws_IfJsonIsIncomplete() => WithJSRuntime(_ => + { + // Arrange + var input = new TestModel(); + var dotNetObjectRef = DotNetObjectRef.Create(input); + var objectId = dotNetObjectRef.ObjectId; + + var json = $"{{\"__dotNetObject\":{objectId}"; + + // Act & Assert + var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); + Assert.IsAssignableFrom(ex); + }); + + [Fact] + public Task Read_Throws_IfDotNetObjectIdAppearsMultipleTimes() => WithJSRuntime(_ => + { + // Arrange + var input = new TestModel(); + var dotNetObjectRef = DotNetObjectRef.Create(input); + var objectId = dotNetObjectRef.ObjectId; + + var json = $"{{\"__dotNetObject\":{objectId},\"__dotNetObject\":{objectId}}}"; + + // Act & Assert + var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); + Assert.IsAssignableFrom(ex); + }); + [Fact] public Task Read_ReadsJson() => WithJSRuntime(_ => { @@ -54,26 +84,25 @@ namespace Microsoft.JSInterop.Tests Assert.Equal(objectId, deserialized.ObjectId); }); + [Fact] public Task Read_ReturnsTheCorrectInstance() => WithJSRuntime(_ => { // Arrange - // Track a few instances and verify that the deserialized value returns the corect value. - DotNetObjectRef.Create(new TestModel()); - DotNetObjectRef.Create(new TestModel()); + // Track a few instances and verify that the deserialized value returns the correct value. + var instance1 = new TestModel(); + var instance2 = new TestModel(); + var ref1 = DotNetObjectRef.Create(instance1); + var ref2 = DotNetObjectRef.Create(instance2); - var input = new TestModel(); - var dotNetObjectRef = DotNetObjectRef.Create(input); - var objectId = dotNetObjectRef.ObjectId; - - var json = $"{{\"__dotNetObject\":{objectId}}}"; + var json = $"[{{\"__dotNetObject\":{ref2.ObjectId}}},{{\"__dotNetObject\":{ref1.ObjectId}}}]"; // Act - var deserialized = JsonSerializer.Deserialize>(json); + var deserialized = JsonSerializer.Deserialize[]>(json); // Assert - Assert.Same(input, deserialized.Value); - Assert.Equal(objectId, deserialized.ObjectId); + Assert.Same(instance2, deserialized[0].Value); + Assert.Same(instance1, deserialized[1].Value); }); [Fact]