From 3a1fe0a344e43274b00d3a697b8f752de82346e7 Mon Sep 17 00:00:00 2001 From: Dzmitry Safarau Date: Fri, 10 Aug 2018 13:47:42 +0300 Subject: [PATCH] Implemented improvements mentioned in #97 --- .../Formatter.cs | 71 +++---- .../MediaTypeHeaderValues.cs | 12 ++ .../Utf8Json.AspNetCoreMvcFormatter.csproj | 2 +- .../AspNetCoreFormatterTests.cs | 193 ++++++++++++++++++ tests/Utf8Json.Tests/Utf8Json.Tests.csproj | 1 + 5 files changed, 234 insertions(+), 45 deletions(-) create mode 100644 src/Utf8Json.AspNetCoreMvcFormatter/MediaTypeHeaderValues.cs create mode 100644 tests/Utf8Json.Tests/AspNetCoreFormatterTests.cs diff --git a/src/Utf8Json.AspNetCoreMvcFormatter/Formatter.cs b/src/Utf8Json.AspNetCoreMvcFormatter/Formatter.cs index a7a0fec..22a9d6a 100644 --- a/src/Utf8Json.AspNetCoreMvcFormatter/Formatter.cs +++ b/src/Utf8Json.AspNetCoreMvcFormatter/Formatter.cs @@ -1,13 +1,11 @@ -using Microsoft.AspNetCore.Mvc.Formatters; +using System.Text; +using Microsoft.AspNetCore.Mvc.Formatters; using System.Threading.Tasks; namespace Utf8Json.AspNetCoreMvcFormatter { - public class JsonOutputFormatter : IOutputFormatter //, IApiResponseTypeMetadataProvider + public class JsonOutputFormatter : TextOutputFormatter { - const string ContentType = "application/json"; - static readonly string[] SupportedContentTypes = new[] { ContentType }; - readonly IJsonFormatterResolver resolver; public JsonOutputFormatter() @@ -15,42 +13,31 @@ public JsonOutputFormatter() { } + public JsonOutputFormatter(IJsonFormatterResolver resolver) { this.resolver = resolver ?? JsonSerializer.DefaultResolver; + SupportedEncodings.Add(Encoding.UTF8); + SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson); + SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson); + SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); } - //public IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) - //{ - // return SupportedContentTypes; - //} - - public bool CanWriteResult(OutputFormatterCanWriteContext context) - { - return true; - } - - public Task WriteAsync(OutputFormatterWriteContext context) + public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { - context.HttpContext.Response.ContentType = ContentType; - - // when 'object' use the concrete type(object.GetType()) - if (context.ObjectType == typeof(object)) - { - return JsonSerializer.NonGeneric.SerializeAsync(context.HttpContext.Response.Body, context.Object, resolver); - } - else - { - return JsonSerializer.NonGeneric.SerializeAsync(context.ObjectType, context.HttpContext.Response.Body, context.Object, resolver); - } + return context.ObjectType == typeof(object) + ? JsonSerializer.NonGeneric.SerializeAsync(context.HttpContext.Response.Body, + context.Object, + resolver) + : JsonSerializer.NonGeneric.SerializeAsync(context.ObjectType, + context.HttpContext.Response.Body, + context.Object, + resolver); } } - public class JsonInputFormatter : IInputFormatter // , IApiRequestFormatMetadataProvider + public class JsonInputFormatter : TextInputFormatter { - const string ContentType = "application/json"; - static readonly string[] SupportedContentTypes = new[] { ContentType }; - readonly IJsonFormatterResolver resolver; public JsonInputFormatter() @@ -62,23 +49,19 @@ public JsonInputFormatter() public JsonInputFormatter(IJsonFormatterResolver resolver) { this.resolver = resolver ?? JsonSerializer.DefaultResolver; + SupportedEncodings.Add(UTF8EncodingWithoutBOM); + SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson); + SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson); + SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); } - //public IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) - //{ - // return SupportedContentTypes; - //} - - public bool CanRead(InputFormatterContext context) - { - return true; - } - - public Task ReadAsync(InputFormatterContext context) + public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) { var request = context.HttpContext.Request; - var result = JsonSerializer.NonGeneric.Deserialize(context.ModelType, request.Body, resolver); - return InputFormatterResult.SuccessAsync(result); + var result = await JsonSerializer.NonGeneric.DeserializeAsync(context.ModelType, + request.Body, + resolver).ConfigureAwait(false); + return InputFormatterResult.Success(result); } } } \ No newline at end of file diff --git a/src/Utf8Json.AspNetCoreMvcFormatter/MediaTypeHeaderValues.cs b/src/Utf8Json.AspNetCoreMvcFormatter/MediaTypeHeaderValues.cs new file mode 100644 index 0000000..fce9c47 --- /dev/null +++ b/src/Utf8Json.AspNetCoreMvcFormatter/MediaTypeHeaderValues.cs @@ -0,0 +1,12 @@ +using Microsoft.Net.Http.Headers; + +namespace Utf8Json.AspNetCoreMvcFormatter +{ + internal class MediaTypeHeaderValues + { + public static readonly MediaTypeHeaderValue ApplicationJson = MediaTypeHeaderValue.Parse("application/json").CopyAsReadOnly(); + public static readonly MediaTypeHeaderValue TextJson = MediaTypeHeaderValue.Parse("text/json").CopyAsReadOnly(); + public static readonly MediaTypeHeaderValue ApplicationJsonPatch = MediaTypeHeaderValue.Parse("application/json-patch+json").CopyAsReadOnly(); + public static readonly MediaTypeHeaderValue ApplicationAnyJsonSyntax = MediaTypeHeaderValue.Parse("application/*+json").CopyAsReadOnly(); + } +} \ No newline at end of file diff --git a/src/Utf8Json.AspNetCoreMvcFormatter/Utf8Json.AspNetCoreMvcFormatter.csproj b/src/Utf8Json.AspNetCoreMvcFormatter/Utf8Json.AspNetCoreMvcFormatter.csproj index 42eabb9..596709e 100644 --- a/src/Utf8Json.AspNetCoreMvcFormatter/Utf8Json.AspNetCoreMvcFormatter.csproj +++ b/src/Utf8Json.AspNetCoreMvcFormatter/Utf8Json.AspNetCoreMvcFormatter.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests/Utf8Json.Tests/AspNetCoreFormatterTests.cs b/tests/Utf8Json.Tests/AspNetCoreFormatterTests.cs new file mode 100644 index 0000000..07adbde --- /dev/null +++ b/tests/Utf8Json.Tests/AspNetCoreFormatterTests.cs @@ -0,0 +1,193 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Primitives; +using Utf8Json.AspNetCoreMvcFormatter; +using Utf8Json.Resolvers; +using Xunit; + +namespace Utf8Json.Tests +{ + public class AspNetCoreFormatterTests + { + private MemoryStream responseStream = new MemoryStream(); + private InputFormatter inputFormatter; + private OutputFormatter outputFormatter; + private HttpContext context; + private TestClass obj = new TestClass + { + A = 2 + }; + + public class TestClass + { + public int A { get; set; } + } + + public AspNetCoreFormatterTests() + { + context = new DefaultHttpContext(); + context.Response.Body = responseStream; + } + + [Fact] + public async Task OutputFormatterShouldFormatWithDefaultParameters() + { + outputFormatter = new JsonOutputFormatter(); + var outputContext = CreateOutputFormatterContext(obj, obj.GetType(), "application/json"); + + Assert.True(outputFormatter.CanWriteResult(outputContext)); + await outputFormatter.WriteAsync(outputContext); + + Assert.Equal(outputContext.ContentType, "application/json; charset=utf-8"); + Assert.Equal(200, outputContext.HttpContext.Response.StatusCode); + + var responseText = Encoding.UTF8.GetString(responseStream.ToArray()); + Assert.Equal("{\"A\":2}", responseText); + } + + [Fact] + public async Task OutputFormatterShouldRespectSerializerSettings() + { + outputFormatter = new JsonOutputFormatter(StandardResolver.CamelCase); + var outputContext = CreateOutputFormatterContext(obj, obj.GetType(), "application/json"); + + await outputFormatter.WriteAsync(outputContext); + + Assert.Equal(outputContext.ContentType, "application/json; charset=utf-8"); + Assert.Equal(200, outputContext.HttpContext.Response.StatusCode); + + var responseText = Encoding.UTF8.GetString(responseStream.ToArray()); + Assert.Equal("{\"a\":2}", responseText); + } + + [Fact] + public void OutputFormatterShouldNotWriteResultWithUnsupportedContentType() + { + outputFormatter = new JsonOutputFormatter(); + var outputContext = CreateOutputFormatterContext(obj, obj.GetType(), "application/xml"); + + Assert.False(outputFormatter.CanWriteResult(outputContext)); + } + + [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 OutputFormatterCanWriteReturnsExpectedValue(string mediaType, bool isServerDefined, string expectedResult) + { + var formatter = new JsonOutputFormatter(); + var outputFormatterContext = CreateOutputFormatterContext(new object(), typeof(object), mediaType, isServerDefined); + + var actualCanWriteValue = formatter.CanWriteResult(outputFormatterContext); + + // Assert + var expectedContentType = expectedResult ?? mediaType; + Assert.Equal(expectedResult != null, actualCanWriteValue); + Assert.Equal(new StringSegment(expectedContentType), outputFormatterContext.ContentType); + } + + [Fact] + public async Task InputFormatterShouldFormatWithDefaultParameters() + { + inputFormatter = new JsonInputFormatter(); + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"A\":2}")); + context.Request.ContentLength = context.Request.Body.Length; + context.Request.ContentType = "application/json"; + + var inputContext = CreateInputFormatterContext(typeof(TestClass)); + Assert.True(inputFormatter.CanRead(inputContext)); + var inputFormatterResult = await inputFormatter.ReadAsync(inputContext); + Assert.Equal(2, ((TestClass)inputFormatterResult.Model).A); + } + + [Fact] + public async Task InputFormatterShouldRespectSerializerSettings() + { + inputFormatter = new JsonInputFormatter(StandardResolver.CamelCase); + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"a\":2}")); + context.Request.ContentLength = context.Request.Body.Length; + context.Request.ContentType = "application/json"; + + var inputContext = CreateInputFormatterContext(typeof(TestClass)); + Assert.True(inputFormatter.CanRead(inputContext)); + var inputFormatterResult = await inputFormatter.ReadAsync(inputContext); + Assert.Equal(2, ((TestClass)inputFormatterResult.Model).A); + } + + [Theory] + [InlineData("application/json", true)] + [InlineData("application/*", false)] + [InlineData("*/*", false)] + [InlineData("text/json", true)] + [InlineData("text/*", false)] + [InlineData("text/xml", false)] + [InlineData("application/xml", false)] + [InlineData("application/some.entity+json", true)] + [InlineData("application/some.entity+json;v=2", true)] + [InlineData("application/some.entity+xml", false)] + [InlineData("application/some.entity+*", false)] + [InlineData("", false)] + [InlineData(null, false)] + [InlineData("invalid", false)] + public void InputFormatterCanReadAnySupportedContentType(string requestContentType, bool expectedCanRead) + { + context.Request.ContentType = requestContentType; + inputFormatter = new JsonInputFormatter(); + var formatterContext = CreateInputFormatterContext(typeof(string)); + + var result = inputFormatter.CanRead(formatterContext); + + Assert.Equal(expectedCanRead, result); + } + + + + + private OutputFormatterWriteContext CreateOutputFormatterContext( + object outputValue, + Type outputType, + string contentType = "application/xml; charset=utf-8", + bool contentTypeIsServerDefined = false) + { + return new OutputFormatterWriteContext( + context, + (stream, encoding) => new StreamWriter(stream), + outputType, + outputValue) + { + ContentType = new StringSegment(contentType), + ContentTypeIsServerDefined = contentTypeIsServerDefined + }; + } + + private InputFormatterContext CreateInputFormatterContext( + Type modelType, + string modelName = null) + { + var provider = new EmptyModelMetadataProvider(); + var metadata = provider.GetMetadataForType(modelType); + + return new InputFormatterContext( + context, + modelName ?? string.Empty, + new ModelStateDictionary(), + metadata, + (stream, encoding) => new StreamReader(stream)); + } + } +} \ No newline at end of file diff --git a/tests/Utf8Json.Tests/Utf8Json.Tests.csproj b/tests/Utf8Json.Tests/Utf8Json.Tests.csproj index feeade2..f88e2af 100644 --- a/tests/Utf8Json.Tests/Utf8Json.Tests.csproj +++ b/tests/Utf8Json.Tests/Utf8Json.Tests.csproj @@ -29,6 +29,7 @@ +