Browse Source

Make System.Text.Json the default for SignalR and remove Newtonsoft from shared framework (#9476)

BrennanConroy 6 years ago
parent
commit
c84e37f30d
28 changed files with 240 additions and 158 deletions
  1. 0 1
      eng/SharedFramework.External.props
  2. 0 1
      eng/SharedFramework.Local.props
  3. 0 2
      eng/targets/ResolveReferences.targets
  4. 2 2
      src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.csproj
  5. 1 1
      src/SignalR/clients/csharp/Client.Core/src/HubConnectionBuilder.cs
  6. 2 2
      src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj
  7. 1 1
      src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs
  8. 0 1
      src/SignalR/clients/ts/FunctionalTests/ComplexObject.cs
  9. 0 6
      src/SignalR/clients/ts/FunctionalTests/Startup.cs
  10. 0 1
      src/SignalR/clients/ts/FunctionalTests/TestHub.cs
  11. 2 4
      src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts
  12. 12 0
      src/SignalR/common/Protocols.Json/ref/Microsoft.AspNetCore.SignalR.Protocols.Json.netcoreapp3.0.cs
  13. 12 0
      src/SignalR/common/Protocols.Json/ref/Microsoft.AspNetCore.SignalR.Protocols.Json.netstandard2.0.cs
  14. 25 0
      src/SignalR/common/Protocols.Json/src/JsonHubProtocolOptions.cs
  15. 14 0
      src/SignalR/common/Protocols.Json/src/JsonProtocolDependencyInjectionExtensions.cs
  16. 1 1
      src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj
  17. 35 17
      src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs
  18. 0 2
      src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj
  19. 21 7
      src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs
  20. 49 39
      src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs
  21. 42 50
      src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs
  22. 1 1
      src/SignalR/common/testassets/Tests.Utils/HubConnectionContextUtils.cs
  23. 1 1
      src/SignalR/server/Core/ref/Microsoft.AspNetCore.SignalR.Core.csproj
  24. 2 2
      src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj
  25. 1 1
      src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs
  26. 13 13
      src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs
  27. 2 1
      src/SignalR/server/Specification.Tests/src/Microsoft.AspNetCore.SignalR.Specification.Tests.csproj
  28. 1 1
      src/SignalR/server/StackExchangeRedis/src/Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj

+ 0 - 1
eng/SharedFramework.External.props

@@ -68,7 +68,6 @@
 
       If these are needed as direct dependencies, it is okay to change them to ExternalAspNetCoreAppReference and move up into sections above.
     -->
-    <_TransitiveExternalAspNetCoreAppReference Include="Newtonsoft.Json"                                    Version="$(NewtonsoftJsonPackageVersion)" />
     <_TransitiveExternalAspNetCoreAppReference Include="System.Security.Cryptography.Pkcs"                  Version="$(SystemSecurityCryptographyPkcsPackageVersion)" />
     <_TransitiveExternalAspNetCoreAppReference Include="System.Security.Permissions"                        Version="$(SystemSecurityPermissionsPackageVersion)" />
   </ItemGroup>

+ 0 - 1
eng/SharedFramework.Local.props

@@ -12,7 +12,6 @@
     <AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Connections.Abstractions" />
     <AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Http.Connections.Common" />
     <AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.SignalR.Protocols.Json" />
-    <AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" />
     <AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.SignalR.Common" />
     <AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Components.Browser" />
     <AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Components" />

+ 0 - 2
eng/targets/ResolveReferences.targets

@@ -79,13 +79,11 @@
     <_InvalidReferenceToNonSharedFxAssembly Condition="'$(IsAspNetCoreApp)' == 'true'"
       Include="@(Reference)"
       Exclude="
-        Newtonsoft.Json;
         @(AspNetCoreAppReference);
         @(AspNetCoreAppReferenceAndPackage);
         @(ExternalAspNetCoreAppReference);
         @(_CompilationOnlyReference);
         @(Reference->WithMetadataValue('IsSharedSource', 'true'))" />
-    <!-- TODO: remove Newtonsoft.Json from this list once https://github.com/aspnet/AspNetCore/issues/4260 is resolved -->
     <_OriginalReferences Include="@(Reference)" />
     <!--
       Turn Reference items into a ProjectReference when UseProjectReferences is true.

+ 2 - 2
src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.csproj

@@ -6,7 +6,7 @@
   <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
     <Compile Include="Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs" />
     <Reference Include="Microsoft.AspNetCore.SignalR.Common"  />
-    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson"  />
+    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.Json"  />
     <Reference Include="Microsoft.Extensions.DependencyInjection"  />
     <Reference Include="Microsoft.Extensions.Logging"  />
     <Reference Include="System.Threading.Channels"  />
@@ -14,7 +14,7 @@
 <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
     <Compile Include="Microsoft.AspNetCore.SignalR.Client.Core.netcoreapp3.0.cs" />
     <Reference Include="Microsoft.AspNetCore.SignalR.Common"  />
-    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson"  />
+    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.Json"  />
     <Reference Include="Microsoft.Extensions.DependencyInjection"  />
     <Reference Include="Microsoft.Extensions.Logging"  />
     <Reference Include="System.Threading.Channels"  />

+ 1 - 1
src/SignalR/clients/csharp/Client.Core/src/HubConnectionBuilder.cs

@@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
             Services = new ServiceCollection();
             Services.AddSingleton<HubConnection>();
             Services.AddLogging();
-            this.AddNewtonsoftJsonProtocol();
+            this.AddJsonProtocol();
         }
 
         /// <inheritdoc />

+ 2 - 2
src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <Description>Client for ASP.NET Core SignalR</Description>
@@ -22,7 +22,7 @@
 
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore.SignalR.Common" />
-    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" />
+    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" />
     <Reference Include="Microsoft.Extensions.DependencyInjection" />
     <Reference Include="Microsoft.Extensions.Logging" />
     <Reference Include="System.Threading.Channels" />

+ 1 - 1
src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs

@@ -1014,7 +1014,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
             }
         }
 
-        [Theory]
+        [Theory(Skip = "Will be fixed by https://github.com/dotnet/corefx/issues/36901")]
         [MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
         public async Task ServerThrowsHubExceptionOnHubMethodArgumentTypeMismatch(string hubProtocolName, HttpTransportType transportType, string hubPath)
         {

+ 0 - 1
src/SignalR/clients/ts/FunctionalTests/ComplexObject.cs

@@ -10,7 +10,6 @@ namespace FunctionalTests
         public string String { get; set; }
         public int[] IntArray { get; set; }
         public byte[] ByteArray { get; set; }
-        public Guid GUID { get; set; }
         public DateTime DateTime { get;set; }
     }
 }

+ 0 - 6
src/SignalR/clients/ts/FunctionalTests/Startup.cs

@@ -41,12 +41,6 @@ namespace FunctionalTests
             {
                 options.EnableDetailedErrors = true;
             })
-            .AddNewtonsoftJsonProtocol(options =>
-            {
-                // we are running the same tests with JSON and MsgPack protocols and having
-                // consistent casing makes it cleaner to verify results
-                options.PayloadSerializerSettings.ContractResolver = new DefaultContractResolver();
-            })
             .AddMessagePackProtocol();
 
             services.AddCors();

+ 0 - 1
src/SignalR/clients/ts/FunctionalTests/TestHub.cs

@@ -125,7 +125,6 @@ namespace FunctionalTests
             {
                 ByteArray = new byte[] { 0x1, 0x2, 0x3 },
                 DateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc),
-                GUID = new Guid("00010203-0405-0607-0706-050403020100"),
                 IntArray = new int[] { 1, 2, 3 },
                 String = "hello world",
             };

+ 2 - 4
src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts

@@ -457,12 +457,11 @@ describe("hubConnection", () => {
 
                 const complexObject = {
                     ByteArray: protocol.name === "json"
-                        ? "aGVsbG8="
+                        ? new Array(0x68, 0x65, 0x6c, 0x6c, 0x6f)
                         : new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]),
                     DateTime: protocol.name === "json"
                         ? "2002-04-01T10:20:15Z"
                         : new Date(Date.UTC(2002, 3, 1, 10, 20, 15)), // Apr 1, 2002, 10:20:15am UTC
-                    GUID: "00010203-0405-0607-0706-050403020100",
                     IntArray: [0x01, 0x02, 0x03, 0xff],
                     String: "Hello, World!",
                 };
@@ -500,12 +499,11 @@ describe("hubConnection", () => {
 
                 const complexObject = {
                     ByteArray: protocol.name === "json"
-                        ? "AQID"
+                        ? new Array(0x1, 0x2, 0x3)
                         : new Uint8Array([0x1, 0x2, 0x3]),
                     DateTime: protocol.name === "json"
                         ? "2000-01-01T00:00:00Z"
                         : new Date(Date.UTC(2000, 0, 1)),
-                    GUID: "00010203-0405-0607-0706-050403020100",
                     IntArray: [0x01, 0x02, 0x03],
                     String: "hello world",
                 };

+ 12 - 0
src/SignalR/common/Protocols.Json/ref/Microsoft.AspNetCore.SignalR.Protocols.Json.netcoreapp3.0.cs

@@ -1,11 +1,22 @@
 // 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.
 
+namespace Microsoft.AspNetCore.SignalR
+{
+    public partial class JsonHubProtocolOptions
+    {
+        public JsonHubProtocolOptions() { }
+        public bool AllowTrailingCommas { get { throw null; } set { } }
+        public bool IgnoreNullValues { get { throw null; } set { } }
+        public bool WriteIndented { get { throw null; } set { } }
+    }
+}
 namespace Microsoft.AspNetCore.SignalR.Protocol
 {
     public sealed partial class JsonHubProtocol : Microsoft.AspNetCore.SignalR.Protocol.IHubProtocol
     {
         public JsonHubProtocol() { }
+        public JsonHubProtocol(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.SignalR.JsonHubProtocolOptions> options) { }
         public int MinorVersion { get { throw null; } }
         public string Name { get { throw null; } }
         public Microsoft.AspNetCore.Connections.TransferFormat TransferFormat { get { throw null; } }
@@ -21,5 +32,6 @@ namespace Microsoft.Extensions.DependencyInjection
     public static partial class JsonProtocolDependencyInjectionExtensions
     {
         public static TBuilder AddJsonProtocol<TBuilder>(this TBuilder builder) where TBuilder : Microsoft.AspNetCore.SignalR.ISignalRBuilder { throw null; }
+        public static TBuilder AddJsonProtocol<TBuilder>(this TBuilder builder, System.Action<Microsoft.AspNetCore.SignalR.JsonHubProtocolOptions> configure) where TBuilder : Microsoft.AspNetCore.SignalR.ISignalRBuilder { throw null; }
     }
 }

+ 12 - 0
src/SignalR/common/Protocols.Json/ref/Microsoft.AspNetCore.SignalR.Protocols.Json.netstandard2.0.cs

@@ -1,11 +1,22 @@
 // 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.
 
+namespace Microsoft.AspNetCore.SignalR
+{
+    public partial class JsonHubProtocolOptions
+    {
+        public JsonHubProtocolOptions() { }
+        public bool AllowTrailingCommas { get { throw null; } set { } }
+        public bool IgnoreNullValues { get { throw null; } set { } }
+        public bool WriteIndented { get { throw null; } set { } }
+    }
+}
 namespace Microsoft.AspNetCore.SignalR.Protocol
 {
     public sealed partial class JsonHubProtocol : Microsoft.AspNetCore.SignalR.Protocol.IHubProtocol
     {
         public JsonHubProtocol() { }
+        public JsonHubProtocol(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.SignalR.JsonHubProtocolOptions> options) { }
         public int MinorVersion { get { throw null; } }
         public string Name { get { throw null; } }
         public Microsoft.AspNetCore.Connections.TransferFormat TransferFormat { get { throw null; } }
@@ -21,5 +32,6 @@ namespace Microsoft.Extensions.DependencyInjection
     public static partial class JsonProtocolDependencyInjectionExtensions
     {
         public static TBuilder AddJsonProtocol<TBuilder>(this TBuilder builder) where TBuilder : Microsoft.AspNetCore.SignalR.ISignalRBuilder { throw null; }
+        public static TBuilder AddJsonProtocol<TBuilder>(this TBuilder builder, System.Action<Microsoft.AspNetCore.SignalR.JsonHubProtocolOptions> configure) where TBuilder : Microsoft.AspNetCore.SignalR.ISignalRBuilder { throw null; }
     }
 }

+ 25 - 0
src/SignalR/common/Protocols.Json/src/JsonHubProtocolOptions.cs

@@ -0,0 +1,25 @@
+// 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.Serialization;
+using Microsoft.AspNetCore.SignalR.Protocol;
+
+namespace Microsoft.AspNetCore.SignalR
+{
+    /// <summary>
+    /// Options used to configure a <see cref="JsonHubProtocolOptions"/> instance.
+    /// </summary>
+    public class JsonHubProtocolOptions
+    {
+        internal readonly JsonSerializerOptions _serializerOptions;
+
+        public JsonHubProtocolOptions()
+        {
+            _serializerOptions = JsonHubProtocol.CreateDefaultSerializerSettings();
+        }
+
+        public bool IgnoreNullValues { get => _serializerOptions.IgnoreNullValues; set => _serializerOptions.IgnoreNullValues = value; }
+        public bool WriteIndented { get => _serializerOptions.WriteIndented; set => _serializerOptions.WriteIndented = value; }
+        public bool AllowTrailingCommas { get => _serializerOptions.AllowTrailingCommas; set => _serializerOptions.AllowTrailingCommas = value; }
+    }
+}

+ 14 - 0
src/SignalR/common/Protocols.Json/src/JsonProtocolDependencyInjectionExtensions.cs

@@ -1,6 +1,7 @@
 // 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 Microsoft.AspNetCore.SignalR;
 using Microsoft.AspNetCore.SignalR.Protocol;
 using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -21,8 +22,21 @@ namespace Microsoft.Extensions.DependencyInjection
         /// <param name="builder">The <see cref="ISignalRBuilder"/> representing the SignalR server to add JSON protocol support to.</param>
         /// <returns>The value of <paramref name="builder"/></returns>
         public static TBuilder AddJsonProtocol<TBuilder>(this TBuilder builder) where TBuilder : ISignalRBuilder
+            => AddJsonProtocol(builder, _ => { });
+
+        /// <summary>
+        /// Enables the JSON protocol for SignalR and allows options for the JSON protocol to be configured.
+        /// </summary>
+        /// <remarks>
+        /// Any options configured here will be applied, even if the JSON protocol has already been registered with the server.
+        /// </remarks>
+        /// <param name="builder">The <see cref="ISignalRBuilder"/> representing the SignalR server to add JSON protocol support to.</param>
+        /// <param name="configure">A delegate that can be used to configure the <see cref="JsonHubProtocolOptions"/></param>
+        /// <returns>The value of <paramref name="builder"/></returns>
+        public static TBuilder AddJsonProtocol<TBuilder>(this TBuilder builder, Action<JsonHubProtocolOptions> configure) where TBuilder : ISignalRBuilder
         {
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, JsonHubProtocol>());
+            builder.Services.Configure(configure);
             return builder;
         }
     }

+ 1 - 1
src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj

@@ -21,7 +21,7 @@
     <Reference Include="Microsoft.AspNetCore.SignalR.Common" />
   </ItemGroup>
 
-  <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'" >
+  <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
     <Reference Include="Microsoft.Bcl.Json.Sources" />
     <Reference Include="System.Buffers" />
     <Reference Include="System.Runtime.CompilerServices.Unsafe" />

+ 35 - 17
src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs

@@ -10,6 +10,7 @@ using System.Text.Json;
 using System.Text.Json.Serialization;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.Options;
 
 namespace Microsoft.AspNetCore.SignalR.Protocol
 {
@@ -42,11 +43,25 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
         private static readonly int ProtocolVersion = 1;
         private static readonly int ProtocolMinorVersion = 0;
 
+        /// <summary>
+        /// Gets the serializer used to serialize invocation arguments and return values.
+        /// </summary>
+        private readonly JsonSerializerOptions _payloadSerializerOptions;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="JsonHubProtocol"/> class.
+        /// </summary>
+        public JsonHubProtocol() : this(Options.Create(new JsonHubProtocolOptions()))
+        {
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="JsonHubProtocol"/> class.
         /// </summary>
-        public JsonHubProtocol()
+        /// <param name="options">The options used to initialize the protocol.</param>
+        public JsonHubProtocol(IOptions<JsonHubProtocolOptions> options)
         {
+            _payloadSerializerOptions = options.Value._serializerOptions;
         }
 
         /// <inheritdoc />
@@ -188,11 +203,8 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
                                 {
                                     // If we have an invocation id already we can parse the end result
                                     var returnType = binder.GetReturnType(invocationId);
-                                    if (reader.TokenType != JsonTokenType.Null)
-                                    {
-                                        using var token = JsonDocument.ParseValue(ref reader);
-                                        result = BindType(token.RootElement, returnType);
-                                    }
+                                    using var token = JsonDocument.ParseValue(ref reader);
+                                    result = BindType(token.RootElement, returnType);
                                 }
                             }
                             else if (reader.TextEquals(ItemPropertyNameBytes))
@@ -216,11 +228,8 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
                                 try
                                 {
                                     var itemType = binder.GetStreamItemType(id);
-                                    if (reader.TokenType != JsonTokenType.Null)
-                                    {
-                                        using var token = JsonDocument.ParseValue(ref reader);
-                                        item = BindType(token.RootElement, itemType);
-                                    }
+                                    using var token = JsonDocument.ParseValue(ref reader);
+                                    item = BindType(token.RootElement, itemType);
                                 }
                                 catch (Exception ex)
                                 {
@@ -571,7 +580,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
 
         private JsonDocument GetParsedObject(object obj, Type type)
         {
-            var bytes = JsonSerializer.ToBytes(obj, type);
+            var bytes = JsonSerializer.ToBytes(obj, type, _payloadSerializerOptions);
             var token = JsonDocument.Parse(bytes);
             return token;
         }
@@ -695,11 +704,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
                 return jsonObject.GetDateTimeOffset();
             }
 
-            if (jsonObject.Type == JsonValueType.Null)
-            {
-                return null;
-            }
-            return JsonSerializer.Parse(jsonObject.GetRawText(), type);
+            return JsonSerializer.Parse(jsonObject.GetRawText(), type, _payloadSerializerOptions);
         }
 
         private object[] BindTypes(JsonElement jsonArray, IReadOnlyList<Type> paramTypes)
@@ -756,5 +761,18 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
 
             return message;
         }
+
+        internal static JsonSerializerOptions CreateDefaultSerializerSettings()
+        {
+            var options = new JsonSerializerOptions();
+            options.WriteIndented = false;
+            options.ReadCommentHandling = JsonCommentHandling.Disallow;
+            options.AllowTrailingCommas = false;
+            options.IgnoreNullValues = false;
+            options.IgnoreReadOnlyProperties = false;
+            // TODO: camelCase
+
+            return options;
+        }
     }
 }

+ 0 - 2
src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj

@@ -3,8 +3,6 @@
   <PropertyGroup>
     <Description>Implements the SignalR Hub Protocol using Newtonsoft.Json.</Description>
     <TargetFramework>netstandard2.0</TargetFramework>
-    <!-- TODO: remove this from the shared framework once we have a JSON protocol implementation which doesn't use Newtonsoft.Json. -->
-    <IsAspNetCoreApp>true</IsAspNetCoreApp>
     <RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <IsShippingPackage>true</IsShippingPackage>

+ 21 - 7
src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs

@@ -9,6 +9,7 @@ using System.Linq;
 using System.Text;
 using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.SignalR.Protocol;
+using Microsoft.Extensions.Options;
 using Xunit;
 
 namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
@@ -17,6 +18,17 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
     {
         protected override IHubProtocol JsonHubProtocol => new JsonHubProtocol();
 
+        protected override IHubProtocol GetProtocolWithOptions(bool useCamelCase, bool ignoreNullValues)
+        {
+            var protocolOptions = new JsonHubProtocolOptions()
+            {
+                IgnoreNullValues = ignoreNullValues,
+                //TODO: camelCase
+            };
+
+            return new JsonHubProtocol(Options.Create(protocolOptions));
+        }
+
         [Theory]
         [InlineData("", "Error reading JSON.")]
         [InlineData("42", "Unexpected JSON Token Type 'Number'. Expected a JSON Object.")]
@@ -42,7 +54,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
             var writer = MemoryBufferWriter.Get();
             try
             {
-                JsonHubProtocol.WriteMessage(testData.Message, writer);
+                var protocol = GetProtocolWithOptions(testData.UseCamelCase, testData.IgnoreNullValues);
+                protocol.WriteMessage(testData.Message, writer);
                 var json = Encoding.UTF8.GetString(writer.ToArray());
 
                 Assert.Equal(expectedOutput, json);
@@ -63,7 +76,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
 
             var binder = new TestBinder(testData.Message);
             var data = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(input));
-            JsonHubProtocol.TryParseMessage(ref data, binder, out var message);
+            var protocol = GetProtocolWithOptions(testData.UseCamelCase, testData.IgnoreNullValues);
+            protocol.TryParseMessage(ref data, binder, out var message);
 
             Assert.Equal(testData.Message, message, TestHubMessageEqualityComparer.Instance);
         }
@@ -83,11 +97,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
 
         public static IDictionary<string, JsonProtocolTestData> CustomProtocolTestData => new[]
         {
-            new JsonProtocolTestData("InvocationMessage_HasFloatArgument", new InvocationMessage(null, "Target", new object[] { 1, "Foo", 2.0f }), "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"),
-            new JsonProtocolTestData("StreamItemMessage_HasFloatItem", new StreamItemMessage("123", 2.0f), "{\"type\":2,\"invocationId\":\"123\",\"item\":2}"),
-            new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), "{\"type\":3,\"invocationId\":\"123\",\"result\":2}"),
-            new JsonProtocolTestData("StreamInvocationMessage_HasFloatArgument", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f }), "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"),
-            new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20\\u002b12:34\"]}"),
+            new JsonProtocolTestData("InvocationMessage_HasFloatArgument", new InvocationMessage(null, "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"),
+            new JsonProtocolTestData("StreamItemMessage_HasFloatItem", new StreamItemMessage("123", 2.0f), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":2}"),
+            new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":2}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasFloatArgument", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"),
+            new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20\\u002b12:34\"]}"),
         }.ToDictionary(t => t.Name);
 
         public static IEnumerable<object[]> CustomProtocolTestDataNames => CustomProtocolTestData.Keys.Select(name => new object[] { name });

+ 49 - 39
src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs

@@ -19,6 +19,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
     {
         protected abstract IHubProtocol JsonHubProtocol { get; }
 
+        protected abstract IHubProtocol GetProtocolWithOptions(bool useCamelCase, bool ignoreNullValues);
+
         public static readonly IDictionary<string, string> TestHeaders = new Dictionary<string, string>
         {
             { "Foo", "Bar" },
@@ -31,18 +33,18 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
 
         public static IDictionary<string, JsonProtocolTestData> ProtocolTestData => new[]
         {
-            new JsonProtocolTestData("InvocationMessage_HasInvocationId", new InvocationMessage("123", "Target", new object[] { 1, "Foo" }), "{\"type\":1,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\"]}"),
-            new JsonProtocolTestData("InvocationMessage_HasBoolArgument", new InvocationMessage(null, "Target", new object[] { true }), "{\"type\":1,\"target\":\"Target\",\"arguments\":[true]}"),
-            new JsonProtocolTestData("InvocationMessage_HasNullArgument", new InvocationMessage(null, "Target", new object[] { null }), "{\"type\":1,\"target\":\"Target\",\"arguments\":[null]}"),
-            new JsonProtocolTestData("InvocationMessage_HasStreamArgument", new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__" }), "{\"type\":1,\"target\":\"Target\",\"arguments\":[],\"streamIds\":[\"__test_id__\"]}"),
-            new JsonProtocolTestData("InvocationMessage_HasStreamAndNormalArgument", new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }), "{\"type\":1,\"target\":\"Target\",\"arguments\":[42],\"streamIds\":[\"__test_id__\"]}"),
-            new JsonProtocolTestData("InvocationMessage_HasMultipleStreams", new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }), "{\"type\":1,\"target\":\"Target\",\"arguments\":[],\"streamIds\":[\"__test_id__\",\"__test_id2__\"]}"),
-            new JsonProtocolTestData("InvocationMessage_DateTimeOffsetArgument", new InvocationMessage("Method", new object[] { DateTimeOffset.Parse("2016-05-10T13:51:20+12:34") }), "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"),
-
-            new JsonProtocolTestData("StreamItemMessage_HasIntegerItem", new StreamItemMessage("123", 1), "{\"type\":2,\"invocationId\":\"123\",\"item\":1}"),
-            new JsonProtocolTestData("StreamItemMessage_HasStringItem", new StreamItemMessage("123", "Foo"), "{\"type\":2,\"invocationId\":\"123\",\"item\":\"Foo\"}"),
-            new JsonProtocolTestData("StreamItemMessage_HasBoolItem", new StreamItemMessage("123", true), "{\"type\":2,\"invocationId\":\"123\",\"item\":true}"),
-            new JsonProtocolTestData("StreamItemMessage_HasNullItem", new StreamItemMessage("123", null), "{\"type\":2,\"invocationId\":\"123\",\"item\":null}"),
+            new JsonProtocolTestData("InvocationMessage_HasInvocationId", new InvocationMessage("123", "Target", new object[] { 1, "Foo" }), true, true, "{\"type\":1,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\"]}"),
+            new JsonProtocolTestData("InvocationMessage_HasBoolArgument", new InvocationMessage(null, "Target", new object[] { true }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[true]}"),
+            new JsonProtocolTestData("InvocationMessage_HasNullArgument", new InvocationMessage(null, "Target", new object[] { null }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[null]}"),
+            new JsonProtocolTestData("InvocationMessage_HasStreamArgument", new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__" }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[],\"streamIds\":[\"__test_id__\"]}"),
+            new JsonProtocolTestData("InvocationMessage_HasStreamAndNormalArgument", new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[42],\"streamIds\":[\"__test_id__\"]}"),
+            new JsonProtocolTestData("InvocationMessage_HasMultipleStreams", new InvocationMessage(null, "Target", Array.Empty<object>(), new string[] { "__test_id__", "__test_id2__" }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[],\"streamIds\":[\"__test_id__\",\"__test_id2__\"]}"),
+            new JsonProtocolTestData("InvocationMessage_DateTimeOffsetArgument", new InvocationMessage("Method", new object[] { DateTimeOffset.Parse("2016-05-10T13:51:20+12:34") }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"),
+
+            new JsonProtocolTestData("StreamItemMessage_HasIntegerItem", new StreamItemMessage("123", 1), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":1}"),
+            new JsonProtocolTestData("StreamItemMessage_HasStringItem", new StreamItemMessage("123", "Foo"), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":\"Foo\"}"),
+            new JsonProtocolTestData("StreamItemMessage_HasBoolItem", new StreamItemMessage("123", true), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":true}"),
+            new JsonProtocolTestData("StreamItemMessage_HasNullItem", new StreamItemMessage("123", null), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":null}"),
 
             // Dictionary not supported yet
             //new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":[1,2,3]}}"),
@@ -50,25 +52,26 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
             //new JsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() })), "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":[1,2,3]}]}"),
             //new JsonProtocolTestData("CancelInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new CancelInvocationMessage("123")), "{\"type\":5," + SerializedHeaders + ",\"invocationId\":\"123\"}"),
 
-            new JsonProtocolTestData("CompletionMessage_HasIntegerResult", CompletionMessage.WithResult("123", 1), "{\"type\":3,\"invocationId\":\"123\",\"result\":1}"),
-            new JsonProtocolTestData("CompletionMessage_HasStringResult", CompletionMessage.WithResult("123", "Foo"), "{\"type\":3,\"invocationId\":\"123\",\"result\":\"Foo\"}"),
-            new JsonProtocolTestData("CompletionMessage_HasBoolResult", CompletionMessage.WithResult("123", true), "{\"type\":3,\"invocationId\":\"123\",\"result\":true}"),
-            new JsonProtocolTestData("CompletionMessage_HasNullResult", CompletionMessage.WithResult("123", null), "{\"type\":3,\"invocationId\":\"123\",\"result\":null}"),
-            new JsonProtocolTestData("CompletionMessage_HasError", CompletionMessage.WithError("123", "Whoops!"), "{\"type\":3,\"invocationId\":\"123\",\"error\":\"Whoops!\"}"),
-            new JsonProtocolTestData("CompletionMessage_HasErrorAndHeaders", AddHeaders(TestHeaders, CompletionMessage.WithError("123", "Whoops!")), "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"error\":\"Whoops!\"}"),
+            new JsonProtocolTestData("CompletionMessage_HasIntegerResult", CompletionMessage.WithResult("123", 1), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":1}"),
+            new JsonProtocolTestData("CompletionMessage_HasStringResult", CompletionMessage.WithResult("123", "Foo"), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":\"Foo\"}"),
+            new JsonProtocolTestData("CompletionMessage_HasBoolResult", CompletionMessage.WithResult("123", true), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":true}"),
+            new JsonProtocolTestData("CompletionMessage_HasNullResult", CompletionMessage.WithResult("123", null), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":null}"),
+            new JsonProtocolTestData("CompletionMessage_HasError", CompletionMessage.WithError("123", "Whoops!"), true, true, "{\"type\":3,\"invocationId\":\"123\",\"error\":\"Whoops!\"}"),
+            new JsonProtocolTestData("CompletionMessage_HasErrorAndHeaders", AddHeaders(TestHeaders, CompletionMessage.WithError("123", "Whoops!")), true, true, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"error\":\"Whoops!\"}"),
 
-            new JsonProtocolTestData("StreamInvocationMessage_HasInvocationId", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo" }), "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\"]}"),
-            new JsonProtocolTestData("StreamInvocationMessage_HasBoolArgument", new StreamInvocationMessage("123", "Target", new object[] { true }), "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[true]}"),
-            new JsonProtocolTestData("StreamInvocationMessage_HasNullArgument", new StreamInvocationMessage("123", "Target", new object[] { null }), "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[null]}"),
-            new JsonProtocolTestData("StreamInvocationMessage_HasStreamArgument", new StreamInvocationMessage("123", "Target", Array.Empty<object>(), new string[] { "__test_id__" }), "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[],\"streamIds\":[\"__test_id__\"]}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasInvocationId", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo" }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\"]}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasBoolArgument", new StreamInvocationMessage("123", "Target", new object[] { true }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[true]}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasNullArgument", new StreamInvocationMessage("123", "Target", new object[] { null }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[null]}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasStreamArgument", new StreamInvocationMessage("123", "Target", Array.Empty<object>(), new string[] { "__test_id__" }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[],\"streamIds\":[\"__test_id__\"]}"),
 
-            new JsonProtocolTestData("CancelInvocationMessage_HasInvocationId", new CancelInvocationMessage("123"), "{\"type\":5,\"invocationId\":\"123\"}"),
+            new JsonProtocolTestData("CancelInvocationMessage_HasInvocationId", new CancelInvocationMessage("123"), true, true, "{\"type\":5,\"invocationId\":\"123\"}"),
 
-            new JsonProtocolTestData("PingMessage", PingMessage.Instance, "{\"type\":6}"),
+            new JsonProtocolTestData("PingMessage", PingMessage.Instance, true, true, "{\"type\":6}"),
 
-            new JsonProtocolTestData("CloseMessage", CloseMessage.Empty, "{\"type\":7}"),
-            new JsonProtocolTestData("CloseMessage_HasError", new CloseMessage("Error!"), "{\"type\":7,\"error\":\"Error!\"}"),
-            new JsonProtocolTestData("CloseMessage_HasErrorEmptyString", new CloseMessage(""), "{\"type\":7,\"error\":\"\"}"),
+            new JsonProtocolTestData("CloseMessage", CloseMessage.Empty, false, true, "{\"type\":7}"),
+            new JsonProtocolTestData("CloseMessage_HasError", new CloseMessage("Error!"), false, true, "{\"type\":7,\"error\":\"Error!\"}"),
+            new JsonProtocolTestData("CloseMessage_HasErrorEmptyString", new CloseMessage(""), false, true, "{\"type\":7,\"error\":\"\"}"),
+            new JsonProtocolTestData("CloseMessage_HasErrorWithCamelCase", new CloseMessage("Error!"), true, true, "{\"type\":7,\"error\":\"Error!\"}"),
 
         }.ToDictionary(t => t.Name);
 
@@ -76,13 +79,12 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
 
         public static IDictionary<string, JsonProtocolTestData> OutOfOrderJsonTestData => new[]
         {
-            new JsonProtocolTestData("InvocationMessage_StringIsoDateArgumentFirst", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), "{ \"arguments\": [\"2016-05-10T13:51:20+12:34\"], \"type\":1, \"target\": \"Method\" }"),
-            //new JsonProtocolTestData("InvocationMessage_StringIsoDateArgumentFirst", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), false, "{ \"arguments\": [\"2016-05-10T13:51:20+12:34\"], \"type\":1, \"target\": \"Method\" }"),
-            //new JsonProtocolTestData("InvocationMessage_DateTimeOffsetArgumentFirst", new InvocationMessage("Method", new object[] { DateTimeOffset.Parse("2016-05-10T13:51:20+12:34") }), false, "{ \"arguments\": [\"2016-05-10T13:51:20+12:34\"], \"type\":1, \"target\": \"Method\" }"),
-            new JsonProtocolTestData("InvocationMessage_IntegerArrayArgumentFirst", new InvocationMessage("Method", new object[] { 1, 2 }), "{ \"arguments\": [1,2], \"type\":1, \"target\": \"Method\" }"),
-            new JsonProtocolTestData("StreamInvocationMessage_IntegerArrayArgumentFirst", new StreamInvocationMessage("3", "Method", new object[] { 1, 2 }), "{ \"type\":4, \"arguments\": [1,2], \"target\": \"Method\", \"invocationId\": \"3\" }"),
-            new JsonProtocolTestData("CompletionMessage_ResultFirst", new CompletionMessage("15", null, 10, hasResult: true), "{ \"type\":3, \"result\": 10, \"invocationId\": \"15\" }"),
-            new JsonProtocolTestData("StreamItemMessage_ItemFirst", new StreamItemMessage("1a", "foo"), "{ \"item\": \"foo\", \"invocationId\": \"1a\", \"type\":2 }")
+            new JsonProtocolTestData("InvocationMessage_StringIsoDateArgumentFirst", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), false, true, "{ \"arguments\": [\"2016-05-10T13:51:20+12:34\"], \"type\":1, \"target\": \"Method\" }"),
+            new JsonProtocolTestData("InvocationMessage_DateTimeOffsetArgumentFirst", new InvocationMessage("Method", new object[] { DateTimeOffset.Parse("2016-05-10T13:51:20+12:34") }), false, true, "{ \"arguments\": [\"2016-05-10T13:51:20+12:34\"], \"type\":1, \"target\": \"Method\" }"),
+            new JsonProtocolTestData("InvocationMessage_IntegerArrayArgumentFirst", new InvocationMessage("Method", new object[] { 1, 2 }), false, true, "{ \"arguments\": [1,2], \"type\":1, \"target\": \"Method\" }"),
+            new JsonProtocolTestData("StreamInvocationMessage_IntegerArrayArgumentFirst", new StreamInvocationMessage("3", "Method", new object[] { 1, 2 }), false, true, "{ \"type\":4, \"arguments\": [1,2], \"target\": \"Method\", \"invocationId\": \"3\" }"),
+            new JsonProtocolTestData("CompletionMessage_ResultFirst", new CompletionMessage("15", null, 10, hasResult: true), false, true, "{ \"type\":3, \"result\": 10, \"invocationId\": \"15\" }"),
+            new JsonProtocolTestData("StreamItemMessage_ItemFirst", new StreamItemMessage("1a", "foo"), false, true, "{ \"item\": \"foo\", \"invocationId\": \"1a\", \"type\":2 }")
 
         }.ToDictionary(t => t.Name);
 
@@ -99,7 +101,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
             var writer = MemoryBufferWriter.Get();
             try
             {
-                JsonHubProtocol.WriteMessage(testData.Message, writer);
+                var protocol = GetProtocolWithOptions(testData.UseCamelCase, testData.IgnoreNullValues);
+                protocol.WriteMessage(testData.Message, writer);
                 var json = Encoding.UTF8.GetString(writer.ToArray());
 
                 Assert.Equal(expectedOutput, json);
@@ -120,7 +123,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
 
             var binder = new TestBinder(testData.Message);
             var data = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(input));
-            JsonHubProtocol.TryParseMessage(ref data, binder, out var message);
+            var protocol = GetProtocolWithOptions(testData.UseCamelCase, testData.IgnoreNullValues);
+            protocol.TryParseMessage(ref data, binder, out var message);
 
             Assert.Equal(testData.Message, message, TestHubMessageEqualityComparer.Instance);
         }
@@ -179,7 +183,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
 
             var binder = new TestBinder(testData.Message);
             var data = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(input));
-            JsonHubProtocol.TryParseMessage(ref data, binder, out var message);
+            var protocol = GetProtocolWithOptions(testData.UseCamelCase, testData.IgnoreNullValues);
+            protocol.TryParseMessage(ref data, binder, out var message);
 
             Assert.Equal(testData.Message, message, TestHubMessageEqualityComparer.Instance);
         }
@@ -205,7 +210,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
         [InlineData("{\"type\":4,\"invocationId\":\"42\",\"target\":\"foo\",\"arguments\":[ \"abc\", \"xyz\"]}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
         [InlineData("{\"type\":1,\"invocationId\":\"42\",\"target\":\"foo\",\"arguments\":[1,\"\",{\"1\":1,\"2\":2}]}", "Invocation provides 3 argument(s) but target expects 2.")]
         [InlineData("{\"type\":1,\"arguments\":[1,\"\",{\"1\":1,\"2\":2}]},\"invocationId\":\"42\",\"target\":\"foo\"", "Invocation provides 3 argument(s) but target expects 2.")]
-        [InlineData("{\"type\":1,\"invocationId\":\"42\",\"target\":\"foo\",\"arguments\":[1,[1]]}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
+        // Both of these should be fixed by https://github.com/dotnet/corefx/issues/36901
+        // [InlineData("{\"type\":1,\"invocationId\":\"42\",\"target\":\"foo\",\"arguments\":[1,[1]]}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
         // [InlineData("{\"type\":1,\"invocationId\":\"42\",\"target\":\"foo\",\"arguments\":[1,[]]}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")]
         public void ArgumentBindingErrors(string input, string expectedMessage)
         {
@@ -277,12 +283,16 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
             public string Name { get; }
             public HubMessage Message { get; }
             public string Json { get; }
+            public bool UseCamelCase { get; }
+            public bool IgnoreNullValues { get; }
 
-            public JsonProtocolTestData(string name, HubMessage message, string json)
+            public JsonProtocolTestData(string name, HubMessage message, bool useCamelCase, bool ignoreNullValues, string json)
             {
                 Name = name;
                 Message = message;
                 Json = json;
+                UseCamelCase = useCamelCase;
+                IgnoreNullValues = ignoreNullValues;
             }
 
             public override string ToString() => Name;

+ 42 - 50
src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs

@@ -22,6 +22,20 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
     {
         protected override IHubProtocol JsonHubProtocol => new NewtonsoftJsonHubProtocol();
 
+        protected override IHubProtocol GetProtocolWithOptions(bool useCamelCase, bool ignoreNullValues)
+        {
+            var protocolOptions = new NewtonsoftJsonHubProtocolOptions
+            {
+                PayloadSerializerSettings = new JsonSerializerSettings()
+                {
+                    NullValueHandling = ignoreNullValues ? NullValueHandling.Ignore : NullValueHandling.Include,
+                    ContractResolver = useCamelCase ? new CamelCasePropertyNamesContractResolver() : new DefaultContractResolver()
+                }
+            };
+
+            return new NewtonsoftJsonHubProtocol(Options.Create(protocolOptions));
+        }
+
         [Theory]
         [InlineData("", "Unexpected end when reading JSON.")]
         [InlineData("42", "Unexpected JSON Token Type 'Integer'. Expected a JSON Object.")]
@@ -44,16 +58,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
 
             var expectedOutput = Frame(testData.Json);
 
-            var protocolOptions = new NewtonsoftJsonHubProtocolOptions
-            {
-                PayloadSerializerSettings = new JsonSerializerSettings()
-                {
-                    NullValueHandling = testData.NullValueHandling,
-                    ContractResolver = testData.CamelCase ? new CamelCasePropertyNamesContractResolver() : new DefaultContractResolver()
-                }
-            };
-
-            var protocol = new NewtonsoftJsonHubProtocol(Options.Create(protocolOptions));
+            var protocol = GetProtocolWithOptions(testData.UseCamelCase, testData.IgnoreNullValues);
 
             var writer = MemoryBufferWriter.Get();
             try
@@ -69,49 +74,36 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
             }
         }
 
-        public static IDictionary<string, NewtonsoftJsonProtocolTestData> CustomProtocolTestData => new[]
+        public static IDictionary<string, JsonProtocolTestData> CustomProtocolTestData => new[]
         {
-            new NewtonsoftJsonProtocolTestData("InvocationMessage_HasFloatArgument", new InvocationMessage(null, "Target", new object[] { 1, "Foo", 2.0f }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"),
-            new NewtonsoftJsonProtocolTestData("StreamItemMessage_HasFloatItem", new StreamItemMessage("123", 2.0f), true, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":2.0}"),
-            new NewtonsoftJsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":2.0}"),
-            new NewtonsoftJsonProtocolTestData("StreamInvocationMessage_HasFloatArgument", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f }), true, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"),
-            new NewtonsoftJsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), false, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"),
-            new NewtonsoftJsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, NullValueHandling.Include, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, NullValueHandling.Include, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("StreamItemMessage_HasCustomItemWithNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnore", new StreamItemMessage("123", new CustomObject()), true, NullValueHandling.Ignore, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnoreAndNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, NullValueHandling.Include, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new CustomObject()), true, NullValueHandling.Include, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, NullValueHandling.Include, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("CompletionMessage_HasCustomResultWithNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIgnore", CompletionMessage.WithResult("123", new CustomObject()), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIncludeAndNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, NullValueHandling.Include, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueInclude", CompletionMessage.WithResult("123", new CustomObject()), true, NullValueHandling.Include, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("CompletionMessage_HasTestHeadersAndCustomItemResult", AddHeaders(TestHeaders, CompletionMessage.WithResult("123", new CustomObject())), true, NullValueHandling.Include, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
-            new NewtonsoftJsonProtocolTestData("CompletionMessage_HasErrorAndCamelCase", CompletionMessage.Empty("123"), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\"}"),
-            new NewtonsoftJsonProtocolTestData("CompletionMessage_HasErrorAndHeadersAndCamelCase", AddHeaders(TestHeaders, CompletionMessage.Empty("123")), true, NullValueHandling.Ignore, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\"}"),
-            new NewtonsoftJsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnore", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, NullValueHandling.Ignore, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, NullValueHandling.Include, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueInclude", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, NullValueHandling.Include, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() })), true, NullValueHandling.Include, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
-            new NewtonsoftJsonProtocolTestData("CloseMessage_HasErrorWithCamelCase", new CloseMessage("Error!"), true, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"Error!\"}"),
+            new JsonProtocolTestData("InvocationMessage_HasFloatArgument", new InvocationMessage(null, "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"),
+            new JsonProtocolTestData("StreamItemMessage_HasFloatItem", new StreamItemMessage("123", 2.0f), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":2.0}"),
+            new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":2.0}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasFloatArgument", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"),
+            new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), false, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"),
+            new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
+            new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
+            new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
+            new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
+            new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnore", new StreamItemMessage("123", new CustomObject()), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnoreAndNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new CustomObject()), true, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, false, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIgnore", CompletionMessage.WithResult("123", new CustomObject()), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIncludeAndNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueInclude", CompletionMessage.WithResult("123", new CustomObject()), true, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("CompletionMessage_HasTestHeadersAndCustomItemResult", AddHeaders(TestHeaders, CompletionMessage.WithResult("123", new CustomObject())), true, false, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
+            new JsonProtocolTestData("CompletionMessage_HasErrorAndCamelCase", CompletionMessage.Empty("123"), true, true, "{\"type\":3,\"invocationId\":\"123\"}"),
+            new JsonProtocolTestData("CompletionMessage_HasErrorAndHeadersAndCamelCase", AddHeaders(TestHeaders, CompletionMessage.Empty("123")), true, true, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\"}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnore", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueInclude", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
+            new JsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() })), true, false, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
         }.ToDictionary(t => t.Name);
 
         public static IEnumerable<object[]> CustomProtocolTestDataNames => CustomProtocolTestData.Keys.Select(name => new object[] { name });
-
-        public class NewtonsoftJsonProtocolTestData : JsonProtocolTestData
-        {
-            public NewtonsoftJsonProtocolTestData(string name, HubMessage message, bool camelCase, NullValueHandling nullValueHandling, string json) : base(name, message, json)
-            {
-                CamelCase = camelCase;
-                NullValueHandling = nullValueHandling;
-            }
-
-            public bool CamelCase { get; }
-            public NullValueHandling NullValueHandling { get; }
-        }
     }
 }

+ 1 - 1
src/SignalR/common/testassets/Tests.Utils/HubConnectionContextUtils.cs

@@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
         {
             return new HubConnectionContext(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance)
             {
-                Protocol = protocol ?? new NewtonsoftJsonHubProtocol(),
+                Protocol = protocol ?? new JsonHubProtocol(),
                 UserIdentifier = userIdentifier,
             };
         }

+ 1 - 1
src/SignalR/server/Core/ref/Microsoft.AspNetCore.SignalR.Core.csproj

@@ -7,7 +7,7 @@
     <Compile Include="Microsoft.AspNetCore.SignalR.Core.netcoreapp3.0.cs" />
     <Reference Include="Microsoft.AspNetCore.Authorization"  />
     <Reference Include="Microsoft.AspNetCore.SignalR.Common"  />
-    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson"  />
+    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.Json"  />
     <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions"  />
     <Reference Include="Microsoft.Extensions.Logging.Abstractions"  />
     <Reference Include="System.Threading.Channels"  />

+ 2 - 2
src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <Description>Real-time communication framework for ASP.NET Core.</Description>
@@ -17,7 +17,7 @@
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore.Authorization" />
     <Reference Include="Microsoft.AspNetCore.SignalR.Common" />
-    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" />
+    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" />
     <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
     <Reference Include="Microsoft.Extensions.Logging.Abstractions" />
     <Reference Include="System.Threading.Channels" />

+ 1 - 1
src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs

@@ -32,7 +32,7 @@ namespace Microsoft.Extensions.DependencyInjection
             services.AddAuthorization();
 
             var builder = new SignalRServerBuilder(services);
-            builder.AddNewtonsoftJsonProtocol();
+            builder.AddJsonProtocol();
             return builder;
         }
     }

+ 13 - 13
src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs

@@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.User(userId).SendAsync("Send", message);
         }
 
-        public Task SendToMultipleUsers(List<string> userIds, string message)
+        public Task SendToMultipleUsers(IReadOnlyList<string> userIds, string message)
         {
             return Clients.Users(userIds).SendAsync("Send", message);
         }
@@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.Client(connectionId).SendAsync("Send", message);
         }
 
-        public Task SendToMultipleClients(string message, List<string> connectionIds)
+        public Task SendToMultipleClients(string message, IReadOnlyList<string> connectionIds)
         {
             return Clients.Clients(connectionIds).SendAsync("Send", message);
         }
@@ -48,12 +48,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.Group(groupName).SendAsync("Send", message);
         }
 
-        public Task GroupExceptSendMethod(string groupName, string message, List<string> excludedConnectionIds)
+        public Task GroupExceptSendMethod(string groupName, string message, IReadOnlyList<string> excludedConnectionIds)
         {
             return Clients.GroupExcept(groupName, excludedConnectionIds).SendAsync("Send", message);
         }
 
-        public Task SendToMultipleGroups(string message, List<string> groupNames)
+        public Task SendToMultipleGroups(string message, IReadOnlyList<string> groupNames)
         {
             return Clients.Groups(groupNames).SendAsync("Send", message);
         }
@@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
         {
         }
 
-        public Task SendToAllExcept(string message, List<string> excludedConnectionIds)
+        public Task SendToAllExcept(string message, IReadOnlyList<string> excludedConnectionIds)
         {
             return Clients.AllExcept(excludedConnectionIds).SendAsync("Send", message);
         }
@@ -313,7 +313,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.Client(connectionId).Send(message);
         }
 
-        public Task SendToMultipleClients(string message, List<string> connectionIds)
+        public Task SendToMultipleClients(string message, IReadOnlyList<string> connectionIds)
         {
             return Clients.Clients(connectionIds).Send(message);
         }
@@ -328,7 +328,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.Group(groupName).Send(message);
         }
 
-        public Task GroupExceptSendMethod(string groupName, string message, List<string> excludedConnectionIds)
+        public Task GroupExceptSendMethod(string groupName, string message, IReadOnlyList<string> excludedConnectionIds)
         {
             return Clients.GroupExcept(groupName, excludedConnectionIds).Send(message);
         }
@@ -338,7 +338,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.OthersInGroup(groupName).Send(message);
         }
 
-        public Task SendToMultipleGroups(string message, List<string> groupNames)
+        public Task SendToMultipleGroups(string message, IReadOnlyList<string> groupNames)
         {
             return Clients.Groups(groupNames).Send(message);
         }
@@ -348,7 +348,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.All.Broadcast(message);
         }
 
-        public Task SendToAllExcept(string message, List<string> excludedConnectionIds)
+        public Task SendToAllExcept(string message, IReadOnlyList<string> excludedConnectionIds)
         {
             return Clients.AllExcept(excludedConnectionIds).Send(message);
         }
@@ -393,7 +393,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.Client(connectionId).Send(message);
         }
 
-        public Task SendToMultipleClients(string message, List<string> connectionIds)
+        public Task SendToMultipleClients(string message, IReadOnlyList<string> connectionIds)
         {
             return Clients.Clients(connectionIds).Send(message);
         }
@@ -414,12 +414,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.Group(groupName).Send(message);
         }
 
-        public Task GroupExceptSendMethod(string groupName, string message, List<string> excludedConnectionIds)
+        public Task GroupExceptSendMethod(string groupName, string message, IReadOnlyList<string> excludedConnectionIds)
         {
             return Clients.GroupExcept(groupName, excludedConnectionIds).Send(message);
         }
 
-        public Task SendToMultipleGroups(string message, List<string> groupNames)
+        public Task SendToMultipleGroups(string message, IReadOnlyList<string> groupNames)
         {
             return Clients.Groups(groupNames).Send(message);
         }
@@ -434,7 +434,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
             return Clients.All.Broadcast(message);
         }
 
-        public Task SendToAllExcept(string message, List<string> excludedConnectionIds)
+        public Task SendToAllExcept(string message, IReadOnlyList<string> excludedConnectionIds)
         {
             return Clients.AllExcept(excludedConnectionIds).Send(message);
         }

+ 2 - 1
src/SignalR/server/Specification.Tests/src/Microsoft.AspNetCore.SignalR.Specification.Tests.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <Description>Tests for users to verify their own implementations of SignalR types</Description>
@@ -19,6 +19,7 @@
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore.SignalR.Common" />
     <Reference Include="Microsoft.AspNetCore.SignalR.Core" />
+    <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" />
     <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" />
     <Reference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" />
     <Reference Include="xunit.assert" />

+ 1 - 1
src/SignalR/server/StackExchangeRedis/src/Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj

@@ -7,7 +7,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <Compile Include="$(SignalRSharedSourceRoot)JsonUtils.cs" Link="Internal\JsonUtils.cs" />
+    <Compile Include="$(SignalRSharedSourceRoot)SystemTextJsonExtensions.cs" Link="Internal\SystemTextJsonExtensions.cs" />
     <Compile Include="$(SignalRSharedSourceRoot)MemoryBufferWriter.cs" Link="Internal\MemoryBufferWriter.cs" />
   </ItemGroup>