File: NewtonsoftJsonOutputFormatterTest.cs
Web Access
Project: src\src\Mvc\Mvc.NewtonsoftJson\test\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj (Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
 
namespace Microsoft.AspNetCore.Mvc.Formatters;
 
public class NewtonsoftJsonOutputFormatterTest : JsonOutputFormatterTestBase
{
    protected override TextOutputFormatter GetOutputFormatter()
    {
        return new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions(), new MvcNewtonsoftJsonOptions());
    }
 
    [Fact]
    public void Creates_SerializerSettings_ByDefault()
    {
        // Arrange & Act
        var jsonFormatter = new TestableJsonOutputFormatter(new JsonSerializerSettings());
 
        // Assert
        Assert.NotNull(jsonFormatter.SerializerSettings);
    }
 
    [Fact]
    public void Constructor_UsesSerializerSettings()
    {
        // Arrange
        // Act
        var serializerSettings = new JsonSerializerSettings();
        var jsonFormatter = new TestableJsonOutputFormatter(serializerSettings);
 
        // Assert
        Assert.Same(serializerSettings, jsonFormatter.SerializerSettings);
    }
 
    [Fact]
    public async Task MvcJsonOptionsAreUsedToSetBufferThresholdFromServices()
    {
        // Arrange
        var person = new User() { FullName = "John", age = 35 };
        Stream writeStream = null;
        var outputFormatterContext = GetOutputFormatterContext(person, typeof(User), writerFactory: (stream, encoding) =>
        {
            writeStream = stream;
            return StreamWriter.Null;
        });
 
        var services = new ServiceCollection()
                .AddOptions()
                .Configure<MvcNewtonsoftJsonOptions>(o =>
                {
                    o.OutputFormatterMemoryBufferThreshold = 1;
                })
                .BuildServiceProvider();
 
        outputFormatterContext.HttpContext.RequestServices = services;
 
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            Formatting = Formatting.Indented,
        };
        var expectedOutput = JsonConvert.SerializeObject(person, settings);
#pragma warning disable CS0618 // Type or member is obsolete
        var jsonFormatter = new NewtonsoftJsonOutputFormatter(settings, ArrayPool<char>.Shared, new MvcOptions());
#pragma warning restore CS0618 // Type or member is obsolete
 
        // Act
        await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
 
        // Assert
        Assert.IsType<FileBufferingWriteStream>(writeStream);
 
        Assert.Equal(1, ((FileBufferingWriteStream)writeStream).MemoryThreshold);
    }
 
    [Fact]
    public async Task MvcJsonOptionsAreUsedToSetBufferThreshold()
    {
        // Arrange
        var person = new User() { FullName = "John", age = 35 };
        Stream writeStream = null;
        var outputFormatterContext = GetOutputFormatterContext(person, typeof(User), writerFactory: (stream, encoding) =>
        {
            writeStream = stream;
            return StreamWriter.Null;
        });
 
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            Formatting = Formatting.Indented,
        };
        var expectedOutput = JsonConvert.SerializeObject(person, settings);
        var jsonFormatter = new NewtonsoftJsonOutputFormatter(settings, ArrayPool<char>.Shared, new MvcOptions(), new MvcNewtonsoftJsonOptions()
        {
            OutputFormatterMemoryBufferThreshold = 2
        });
 
        // Act
        await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
 
        // Assert
        Assert.IsType<FileBufferingWriteStream>(writeStream);
 
        Assert.Equal(2, ((FileBufferingWriteStream)writeStream).MemoryThreshold);
    }
 
    [Fact]
    public async Task ChangesTo_SerializerSettings_AffectSerialization()
    {
        // Arrange
        var person = new User() { FullName = "John", age = 35 };
        var outputFormatterContext = GetOutputFormatterContext(person, typeof(User));
 
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            Formatting = Formatting.Indented,
        };
        var expectedOutput = JsonConvert.SerializeObject(person, settings);
        var jsonFormatter = new NewtonsoftJsonOutputFormatter(settings, ArrayPool<char>.Shared, new MvcOptions(), new MvcNewtonsoftJsonOptions());
 
        // Act
        await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
 
        // Assert
        var body = outputFormatterContext.HttpContext.Response.Body;
 
        Assert.NotNull(body);
        body.Position = 0;
 
        var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
        Assert.Equal(expectedOutput, content);
    }
 
    [Fact]
    public async Task ChangesTo_SerializerSettings_AfterSerialization_DoNotAffectSerialization()
    {
        // Arrange
        var person = new User() { FullName = "John", age = 35 };
        var expectedOutput = JsonConvert.SerializeObject(person, new JsonSerializerSettings());
 
        var jsonFormatter = new TestableJsonOutputFormatter(new JsonSerializerSettings());
 
        // This will create a serializer - which gets cached.
        var outputFormatterContext1 = GetOutputFormatterContext(person, typeof(User));
        await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext1, Encoding.UTF8);
 
        // These changes should have no effect.
        jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        jsonFormatter.SerializerSettings.Formatting = Formatting.Indented;
 
        var outputFormatterContext2 = GetOutputFormatterContext(person, typeof(User));
 
        // Act
        await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext2, Encoding.UTF8);
 
        // Assert
        var body = outputFormatterContext2.HttpContext.Response.Body;
 
        Assert.NotNull(body);
        body.Position = 0;
 
        var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
        Assert.Equal(expectedOutput, content);
    }
 
    public static TheoryData<NamingStrategy, string> NamingStrategy_AffectsSerializationData
    {
        get
        {
            return new TheoryData<NamingStrategy, string>
                {
                    { new CamelCaseNamingStrategy(), "{\"fullName\":\"John\",\"age\":35}"{\"fullName\":\"John\",\"age\":35}" },
                    { new DefaultNamingStrategy(), "{\"FullName\":\"John\",\"age\":35}"{\"FullName\":\"John\",\"age\":35}" },
                    { new SnakeCaseNamingStrategy(), "{\"full_name\":\"John\",\"age\":35}"{\"full_name\":\"John\",\"age\":35}" },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(NamingStrategy_AffectsSerializationData))]
    public async Task NamingStrategy_AffectsSerialization(NamingStrategy strategy, string expected)
    {
        // Arrange
        var user = new User { FullName = "John", age = 35 };
        var context = GetOutputFormatterContext(user, typeof(User));
 
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new DefaultContractResolver
            {
                NamingStrategy = strategy,
            },
        };
        var formatter = new TestableJsonOutputFormatter(settings);
 
        // Act
        await formatter.WriteResponseBodyAsync(context, Encoding.UTF8);
 
        // Assert
        var body = context.HttpContext.Response.Body;
 
        Assert.NotNull(body);
        body.Position = 0;
 
        var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
        Assert.Equal(expected, content);
    }
 
    public static TheoryData<NamingStrategy> NamingStrategy_DoesNotAffectSerializationData
    {
        get
        {
            return new TheoryData<NamingStrategy>
                {
                    { new CamelCaseNamingStrategy() },
                    { new DefaultNamingStrategy() },
                    { new SnakeCaseNamingStrategy() },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(NamingStrategy_DoesNotAffectSerializationData))]
    public async Task NamingStrategy_DoesNotAffectDictionarySerialization(NamingStrategy strategy)
    {
        // Arrange
        var dictionary = new Dictionary<string, int>(StringComparer.Ordinal)
            {
                { "id", 12 },
                { "Id", 12 },
                { "fullName", 12 },
                { "full-name", 12 },
                { "FullName", 12 },
                { "full_Name", 12 },
            };
        var expected = "{\"id\":12,\"Id\":12,\"fullName\":12,\"full-name\":12,\"FullName\":12,\"full_Name\":12}"{\"id\":12,\"Id\":12,\"fullName\":12,\"full-name\":12,\"FullName\":12,\"full_Name\":12}";
        var context = GetOutputFormatterContext(dictionary, typeof(Dictionary<string, int>));
 
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new DefaultContractResolver
            {
                NamingStrategy = strategy,
            },
        };
        var formatter = new TestableJsonOutputFormatter(settings);
 
        // Act
        await formatter.WriteResponseBodyAsync(context, Encoding.UTF8);
 
        // Assert
        var body = context.HttpContext.Response.Body;
 
        Assert.NotNull(body);
        body.Position = 0;
 
        var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
        Assert.Equal(expected, content);
    }
 
    [Theory]
    [MemberData(nameof(NamingStrategy_DoesNotAffectSerializationData))]
    public async Task NamingStrategy_DoesNotAffectSerialization_WithJsonProperty(NamingStrategy strategy)
    {
        // Arrange
        var user = new UserWithJsonProperty
        {
            Name = "Joe",
            AnotherName = "Joe",
            ThirdName = "Joe",
        };
        var expected = "{\"ThisIsTheFullName\":\"Joe\",\"another_name\":\"Joe\",\"ThisIsTheThirdName\":\"Joe\"}"{\"ThisIsTheFullName\":\"Joe\",\"another_name\":\"Joe\",\"ThisIsTheThirdName\":\"Joe\"}";
        var context = GetOutputFormatterContext(user, typeof(UserWithJsonProperty));
 
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new DefaultContractResolver
            {
                NamingStrategy = strategy,
            },
        };
        var formatter = new TestableJsonOutputFormatter(settings);
 
        // Act
        await formatter.WriteResponseBodyAsync(context, Encoding.UTF8);
 
        // Assert
        var body = context.HttpContext.Response.Body;
 
        Assert.NotNull(body);
        body.Position = 0;
 
        var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
        Assert.Equal(expected, content);
    }
 
    [Theory]
    [MemberData(nameof(NamingStrategy_DoesNotAffectSerializationData))]
    public async Task NamingStrategy_DoesNotAffectSerialization_WithJsonObject(NamingStrategy strategy)
    {
        // Arrange
        var user = new UserWithJsonObject
        {
            age = 35,
            FullName = "John",
        };
        var expected = "{\"age\":35,\"full_name\":\"John\"}"{\"age\":35,\"full_name\":\"John\"}";
        var context = GetOutputFormatterContext(user, typeof(UserWithJsonProperty));
 
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new DefaultContractResolver
            {
                NamingStrategy = strategy,
            },
        };
        var formatter = new TestableJsonOutputFormatter(settings);
 
        // Act
        await formatter.WriteResponseBodyAsync(context, Encoding.UTF8);
 
        // Assert
        var body = context.HttpContext.Response.Body;
 
        Assert.NotNull(body);
        body.Position = 0;
 
        var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
        Assert.Equal(expected, content);
    }
 
    [Fact]
    public async Task WriteToStreamAsync_RoundTripsJToken()
    {
        // Arrange
        var beforeMessage = "Hello World";
        var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions(), new MvcNewtonsoftJsonOptions());
        var memStream = new MemoryStream();
        var outputFormatterContext = GetOutputFormatterContext(
            beforeMessage,
            typeof(string),
            "application/json; charset=utf-8",
            memStream);
 
        // Act
        await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
 
        // Assert
        memStream.Position = 0;
        var after = JToken.Load(new JsonTextReader(new StreamReader(memStream)));
        var afterMessage = after.ToObject<string>();
 
        Assert.Equal(beforeMessage, afterMessage);
    }
 
    [Fact]
    public async Task WriteToStreamAsync_LargePayload_DoesNotPerformSynchronousWrites()
    {
        // Arrange
        var model = Enumerable.Range(0, 1000).Select(p => new User { FullName = new string('a', 5000) });
 
        var stream = new Mock<Stream> { CallBase = true };
        stream.Setup(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
            .Returns(Task.CompletedTask);
        stream.Setup(v => v.FlushAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
        stream.SetupGet(s => s.CanWrite).Returns(true);
 
        var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions(), new MvcNewtonsoftJsonOptions());
        var outputFormatterContext = GetOutputFormatterContext(
            model,
            typeof(string),
            "application/json; charset=utf-8",
            stream.Object);
 
        // Act
        await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
 
        // Assert
        stream.Verify(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.AtLeastOnce());
 
        stream.Verify(v => v.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
        stream.Verify(v => v.Flush(), Times.Never());
        Assert.NotNull(outputFormatterContext.HttpContext.Response.ContentLength);
    }
 
    [Fact]
    public async Task SerializingWithPreserveReferenceHandling()
    {
        // Arrange
        var expected = "{\"$id\":\"1\",\"fullName\":\"John\",\"age\":35}"{\"$id\":\"1\",\"fullName\":\"John\",\"age\":35}";
        var user = new User { FullName = "John", age = 35 };
 
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new DefaultContractResolver
            {
                NamingStrategy = new CamelCaseNamingStrategy(),
            },
            PreserveReferencesHandling = PreserveReferencesHandling.All,
        };
        var formatter = new TestableJsonOutputFormatter(settings);
 
        for (var i = 0; i < 3; i++)
        {
            // Act
            var context = GetOutputFormatterContext(user, typeof(User));
            await formatter.WriteResponseBodyAsync(context, Encoding.UTF8);
 
            // Assert
            var body = context.HttpContext.Response.Body;
 
            Assert.NotNull(body);
            body.Position = 0;
 
            var content = new StreamReader(body, Encoding.UTF8).ReadToEnd();
            Assert.Equal(expected, content);
        }
    }
 
    [Fact]
    public async Task WriteResponseBodyAsync_AsyncEnumerableConnectionCloses()
    {
        // Arrange
        var formatter = GetOutputFormatter();
        var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
 
        var body = new MemoryStream();
        var actionContext = GetActionContext(mediaType, body);
        var cts = new CancellationTokenSource();
        actionContext.HttpContext.RequestAborted = cts.Token;
        actionContext.HttpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider();
 
        var asyncEnumerable = AsyncEnumerableClosedConnection();
        var outputFormatterContext = new OutputFormatterWriteContext(
            actionContext.HttpContext,
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            asyncEnumerable.GetType(),
            asyncEnumerable)
        {
            ContentType = new StringSegment(mediaType.ToString()),
        };
        var iterated = false;
 
        // Act
        await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8"));
 
        // Assert
        Assert.Empty(body.ToArray());
        Assert.False(iterated);
 
        async IAsyncEnumerable<int> AsyncEnumerableClosedConnection([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.Yield();
            cts.Cancel();
            // MvcOptions.MaxIAsyncEnumerableBufferLimit is 8192. Pick some value larger than that.
            foreach (var i in Enumerable.Range(0, 9000))
            {
                cancellationToken.ThrowIfCancellationRequested();
                iterated = true;
                yield return i;
            }
        }
    }
 
    [Fact]
    public async Task WriteResponseBodyAsync_AsyncEnumerableThrowsCustomOCE()
    {
        // Arrange
        var formatter = GetOutputFormatter();
        var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
 
        var body = new MemoryStream();
        var actionContext = GetActionContext(mediaType, body);
        var cts = new CancellationTokenSource();
        actionContext.HttpContext.RequestAborted = cts.Token;
        actionContext.HttpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider();
 
        var asyncEnumerable = AsyncEnumerableThrows();
        var outputFormatterContext = new OutputFormatterWriteContext(
            actionContext.HttpContext,
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            asyncEnumerable.GetType(),
            asyncEnumerable)
        {
            ContentType = new StringSegment(mediaType.ToString()),
        };
 
        // Act
        await Assert.ThrowsAsync<OperationCanceledException>(() => formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8")));
 
        async IAsyncEnumerable<int> AsyncEnumerableThrows([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.Yield();
            yield return 1;
            throw new OperationCanceledException();
        }
    }
 
    [Fact]
    public async Task WriteResponseBodyAsync_AsyncEnumerableThrowsConnectionAbortedOCE()
    {
        // Arrange
        var formatter = GetOutputFormatter();
        var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
 
        var body = new MemoryStream();
        var actionContext = GetActionContext(mediaType, body);
        var cts = new CancellationTokenSource();
        actionContext.HttpContext.RequestAborted = cts.Token;
        actionContext.HttpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider();
 
        var asyncEnumerable = AsyncEnumerableThrows();
        var outputFormatterContext = new OutputFormatterWriteContext(
            actionContext.HttpContext,
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            asyncEnumerable.GetType(),
            asyncEnumerable)
        {
            ContentType = new StringSegment(mediaType.ToString()),
        };
 
        // Act
        // Act
        await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8"));
 
        // Assert
        Assert.Empty(body.ToArray());
 
        async IAsyncEnumerable<int> AsyncEnumerableThrows([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.Yield();
            cts.Cancel();
            cancellationToken.ThrowIfCancellationRequested();
            yield return 1;
        }
    }
 
    private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter
    {
        public TestableJsonOutputFormatter(JsonSerializerSettings serializerSettings)
            : base(serializerSettings, ArrayPool<char>.Shared, new MvcOptions(), new MvcNewtonsoftJsonOptions())
        {
        }
 
        public new JsonSerializerSettings SerializerSettings => base.SerializerSettings;
    }
 
    private sealed class User
    {
        public string FullName { get; set; }
 
        public int age { get; set; }
    }
 
    private class UserWithJsonProperty
    {
        [JsonProperty("ThisIsTheFullName")]
        public string Name { get; set; }
 
        [JsonProperty(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
        public string AnotherName { get; set; }
 
        // NamingStrategyType should be ignored with an explicit name.
        [JsonProperty("ThisIsTheThirdName", NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
        public string ThirdName { get; set; }
    }
 
    [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
    private class UserWithJsonObject
    {
        public int age { get; set; }
 
        public string FullName { get; set; }
    }
}