534 lines
20 KiB
C#
534 lines
20 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Buffers;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
|
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Primitives;
|
|
using Microsoft.Net.Http.Headers;
|
|
using Moq;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using Newtonsoft.Json.Serialization;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
|
{
|
|
public class NewtonsoftJsonOutputFormatterTest
|
|
{
|
|
[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 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);
|
|
|
|
// 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}" },
|
|
{ new DefaultNamingStrategy(), "{\"FullName\":\"John\",\"age\":35}" },
|
|
{ new SnakeCaseNamingStrategy(), "{\"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}";
|
|
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\"}";
|
|
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\"}";
|
|
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);
|
|
var before = new JValue(beforeMessage);
|
|
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);
|
|
}
|
|
|
|
public static TheoryData<string, string, bool> WriteCorrectCharacterEncoding
|
|
{
|
|
get
|
|
{
|
|
var data = new TheoryData<string, string, bool>
|
|
{
|
|
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-8", "utf-8", true },
|
|
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-16", "utf-16", true },
|
|
{ "This is a test 激光這兩個字是甚麼意思 string written using utf-32", "utf-32", false },
|
|
{ "This is a test æøå string written using iso-8859-1", "iso-8859-1", false },
|
|
};
|
|
|
|
return data;
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(WriteCorrectCharacterEncoding))]
|
|
public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding(
|
|
string content,
|
|
string encodingAsString,
|
|
bool isDefaultEncoding)
|
|
{
|
|
// Arrange
|
|
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
|
|
var formattedContent = "\"" + content + "\"";
|
|
var mediaType = MediaTypeHeaderValue.Parse(string.Format("application/json; charset={0}", encodingAsString));
|
|
var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding);
|
|
var expectedData = encoding.GetBytes(formattedContent);
|
|
|
|
|
|
var body = new MemoryStream();
|
|
var actionContext = GetActionContext(mediaType, body);
|
|
|
|
var outputFormatterContext = new OutputFormatterWriteContext(
|
|
actionContext.HttpContext,
|
|
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
|
typeof(string),
|
|
content)
|
|
{
|
|
ContentType = new StringSegment(mediaType.ToString()),
|
|
};
|
|
|
|
// Act
|
|
await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString));
|
|
|
|
// Assert
|
|
var actualData = body.ToArray();
|
|
Assert.Equal(expectedData, actualData);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ErrorDuringSerialization_DoesNotCloseTheBrackets()
|
|
{
|
|
// Arrange
|
|
var expectedOutput = "{\"name\":\"Robert\"";
|
|
var outputFormatterContext = GetOutputFormatterContext(
|
|
new ModelWithSerializationError(),
|
|
typeof(ModelWithSerializationError));
|
|
var serializerSettings = JsonSerializerSettingsProvider.CreateSerializerSettings();
|
|
var jsonFormatter = new NewtonsoftJsonOutputFormatter(serializerSettings, ArrayPool<char>.Shared);
|
|
|
|
// Act
|
|
try
|
|
{
|
|
await jsonFormatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.UTF8);
|
|
}
|
|
catch (JsonSerializationException serializerException)
|
|
{
|
|
var expectedException = Assert.IsType<NotImplementedException>(serializerException.InnerException);
|
|
Assert.Equal("Property Age has not been implemented", expectedException.Message);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("application/json", false, "application/json")]
|
|
[InlineData("application/json", true, "application/json")]
|
|
[InlineData("application/xml", false, null)]
|
|
[InlineData("application/xml", true, null)]
|
|
[InlineData("application/*", false, "application/json")]
|
|
[InlineData("text/*", false, "text/json")]
|
|
[InlineData("custom/*", false, null)]
|
|
[InlineData("application/json;v=2", false, null)]
|
|
[InlineData("application/json;v=2", true, null)]
|
|
[InlineData("application/some.entity+json", false, null)]
|
|
[InlineData("application/some.entity+json", true, "application/some.entity+json")]
|
|
[InlineData("application/some.entity+json;v=2", true, "application/some.entity+json;v=2")]
|
|
[InlineData("application/some.entity+xml", true, null)]
|
|
public void CanWriteResult_ReturnsExpectedValueForMediaType(
|
|
string mediaType,
|
|
bool isServerDefined,
|
|
string expectedResult)
|
|
{
|
|
// Arrange
|
|
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared);
|
|
|
|
var body = new MemoryStream();
|
|
var actionContext = GetActionContext(MediaTypeHeaderValue.Parse(mediaType), body);
|
|
var outputFormatterContext = new OutputFormatterWriteContext(
|
|
actionContext.HttpContext,
|
|
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
|
typeof(string),
|
|
new object())
|
|
{
|
|
ContentType = new StringSegment(mediaType),
|
|
ContentTypeIsServerDefined = isServerDefined,
|
|
};
|
|
|
|
// Act
|
|
var actualCanWriteValue = formatter.CanWriteResult(outputFormatterContext);
|
|
|
|
// Assert
|
|
var expectedContentType = expectedResult ?? mediaType;
|
|
Assert.Equal(expectedResult != null, actualCanWriteValue);
|
|
Assert.Equal(new StringSegment(expectedContentType), outputFormatterContext.ContentType);
|
|
}
|
|
|
|
private static Encoding CreateOrGetSupportedEncoding(
|
|
NewtonsoftJsonOutputFormatter formatter,
|
|
string encodingAsString,
|
|
bool isDefaultEncoding)
|
|
{
|
|
Encoding encoding = null;
|
|
if (isDefaultEncoding)
|
|
{
|
|
encoding = formatter.SupportedEncodings
|
|
.First((e) => e.WebName.Equals(encodingAsString, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
else
|
|
{
|
|
encoding = Encoding.GetEncoding(encodingAsString);
|
|
formatter.SupportedEncodings.Add(encoding);
|
|
}
|
|
|
|
return encoding;
|
|
}
|
|
|
|
private static ILogger GetLogger()
|
|
{
|
|
return NullLogger.Instance;
|
|
}
|
|
|
|
private static OutputFormatterWriteContext GetOutputFormatterContext(
|
|
object outputValue,
|
|
Type outputType,
|
|
string contentType = "application/xml; charset=utf-8",
|
|
MemoryStream responseStream = null)
|
|
{
|
|
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);
|
|
|
|
var actionContext = GetActionContext(mediaTypeHeaderValue, responseStream);
|
|
return new OutputFormatterWriteContext(
|
|
actionContext.HttpContext,
|
|
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
|
outputType,
|
|
outputValue)
|
|
{
|
|
ContentType = new StringSegment(contentType),
|
|
};
|
|
}
|
|
|
|
private static ActionContext GetActionContext(
|
|
MediaTypeHeaderValue contentType,
|
|
MemoryStream responseStream = null)
|
|
{
|
|
var context = new DefaultHttpContext();
|
|
context.Request.ContentType = contentType.ToString();
|
|
context.Request.Headers[HeaderNames.AcceptCharset] = contentType.Charset.ToString();
|
|
context.Response.Body = responseStream ?? new MemoryStream();
|
|
|
|
return new ActionContext(context, new RouteData(), new ActionDescriptor());
|
|
}
|
|
|
|
private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter
|
|
{
|
|
public TestableJsonOutputFormatter(JsonSerializerSettings serializerSettings)
|
|
: base(serializerSettings, ArrayPool<char>.Shared)
|
|
{
|
|
}
|
|
|
|
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; }
|
|
}
|
|
|
|
private class ModelWithSerializationError
|
|
{
|
|
public string Name { get; } = "Robert";
|
|
public int Age
|
|
{
|
|
get
|
|
{
|
|
throw new NotImplementedException($"Property {nameof(Age)} has not been implemented");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |