diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/NoContentFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/NoContentFormatter.cs new file mode 100644 index 0000000000..5234fcd4e8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/NoContentFormatter.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Sets the status code to 204 if the content is null. + /// + public class NoContentFormatter : IOutputFormatter + { + public bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType) + { + // ignore the contentType and just look at the content. + // This formatter will be selected if the content is null. + return context.Object == null; + } + + public Task WriteAsync(OutputFormatterContext context) + { + var response = context.ActionContext.HttpContext.Response; + response.ContentLength = 0; + + // Only set the status code if its not already set. + if (response.StatusCode == 0) + { + response.StatusCode = 204; + } + + return Task.FromResult(true); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index c68b0ee58d..c132206cfd 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -32,6 +32,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index cf4d6cd29b..c1f3ddf6c7 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -34,6 +34,7 @@ namespace Microsoft.AspNet.Mvc options.ModelBinders.Add(new ComplexModelDtoModelBinder()); // Set up default output formatters. + options.OutputFormatters.Add(new NoContentFormatter()); options.OutputFormatters.Add(new TextPlainFormatter()); options.OutputFormatters.Add(new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false)); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/NoContentFormatterTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/NoContentFormatterTests.cs new file mode 100644 index 0000000000..207080b33a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/NoContentFormatterTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.HeaderValueAbstractions; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Test +{ + public class NoContentFormatterTests + { + public static IEnumerable OutputFormatterContextValues_CanWriteType + { + get + { + // object value, bool useDeclaredTypeAsString, bool expectedCanwriteResult, bool useNonNullContentType + yield return new object[] { "valid value", true, false, true }; + yield return new object[] { "valid value", false, false, true }; + yield return new object[] { "", false, false, true }; + yield return new object[] { "", true, false, true }; + yield return new object[] { null, true, true, true }; + yield return new object[] { null, false, true, true }; + yield return new object[] { null, false, true, false }; + yield return new object[] { new object(), false, false, true }; + yield return new object[] { 1232, false, false, true }; + yield return new object[] { 1232, false, false, false }; + } + } + + [Theory] + [MemberData("OutputFormatterContextValues_CanWriteType")] + public void CanWriteResult_ReturnsTrueOnlyIfTheValueIsNull(object value, + bool declaredTypeAsString, + bool expectedCanwriteResult, + bool useNonNullContentType) + { + // Arrange + var typeToUse = declaredTypeAsString ? typeof(string) : typeof(object); + var formatterContext = new OutputFormatterContext() + { + Object = value, + DeclaredType = typeToUse, + ActionContext = null, + }; + var contetType = useNonNullContentType ? MediaTypeHeaderValue.Parse("text/plain") : null; + var formatter = new NoContentFormatter(); + + // Act + var actualCanWriteResult = formatter.CanWriteResult(formatterContext, contetType); + + // Assert + Assert.Equal(expectedCanwriteResult, actualCanWriteResult); + } + + [Fact] + public async Task WriteAsync_WritesTheStatusCode204_IfNotAlreadySet() + { + // Arrange + var defaultHttpContext = new DefaultHttpContext(); + + // Workaround for https://github.com/aspnet/HttpAbstractions/issues/114 + defaultHttpContext.Response.StatusCode = 0; + var formatterContext = new OutputFormatterContext() + { + Object = null, + ActionContext = new ActionContext(defaultHttpContext, new RouteData(), new ActionDescriptor()) + }; + + var formatter = new NoContentFormatter(); + + // Act + await formatter.WriteAsync(formatterContext); + + // Assert + Assert.Equal(204, defaultHttpContext.Response.StatusCode); + } + + [Fact] + public async Task WriteAsync_DoesnNotWriteTheStatusCode204_IfStatusCodeIsSetAlready() + { + // Arrange + var defaultHttpContext = new DefaultHttpContext(); + defaultHttpContext.Response.StatusCode = 201; + var formatterContext = new OutputFormatterContext() + { + Object = null, + ActionContext = new ActionContext(defaultHttpContext, new RouteData(), new ActionDescriptor()) + }; + + var formatter = new NoContentFormatter(); + + // Act + await formatter.WriteAsync(formatterContext); + + // Assert + Assert.Equal(201, defaultHttpContext.Response.StatusCode); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index f9e444cdaa..9dc3aa1386 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -30,8 +30,11 @@ + + + @@ -58,6 +61,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs index fe2fb85a78..14799760ef 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs @@ -189,65 +189,5 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); Assert.Equal(expectedBody, body); } - - - [InlineData("ReturnTaskOfString")] - [InlineData("ReturnTaskOfObject_StringValue")] - [InlineData("ReturnString")] - [InlineData("ReturnObject_StringValue")] - [InlineData("ReturnString_NullValue")] - public async Task TextPlainFormatter_ReturnsTextPlainContentType(string actionName) - { - // Arrange - var server = TestServer.Create(_provider, _app); - var client = server.Handler; - var expectedContentType = "text/plain;charset=utf-8"; - var expectedBody = actionName; - - // Act - var result = await client.GetAsync("http://localhost/TextPlain/" + actionName); - - // Assert - Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType); - var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); - Assert.Equal(expectedBody, body); - } - - [InlineData("ReturnTaskOfObject_ObjectValue")] - [InlineData("ReturnObject_ObjectValue")] - [InlineData("ReturnObject_NullValue")] - public async Task TextPlainFormatter_DoesNotSelectTextPlainFormatterForNonStringValue(string actionName) - { - // Arrange - var server = TestServer.Create(_provider, _app); - var client = server.Handler; - var expectedContentType = "application/json;charset=utf-8"; - var expectedBody = actionName; - - // Act - var result = await client.GetAsync("http://localhost/TextPlain/" + actionName); - - // Assert - Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType); - var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); - } - - [InlineData("ReturnString_NullValue")] - public async Task TextPlainFormatter_DoesNotWriteNullValue(string actionName) - { - // Arrange - var server = TestServer.Create(_provider, _app); - var client = server.Handler; - var expectedContentType = "text/plain;charset=utf-8"; - string expectedBody = null; - - // Act - var result = await client.GetAsync("http://localhost/TextPlain/" + actionName); - - // Assert - Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType); - var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); - Assert.Equal(expectedBody, body); - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj index 6cb7fc3c3c..ebcd3020a1 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj @@ -31,6 +31,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/OutputFormattterTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/OutputFormattterTests.cs new file mode 100644 index 0000000000..7d123ee0d1 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/OutputFormattterTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using ConnegWebsite; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class OutputFormatterTests + { + private readonly IServiceProvider _provider = TestHelper.CreateServices("ConnegWebsite"); + private readonly Action _app = new Startup().Configure; + + [Theory] + [InlineData("ReturnTaskOfString")] + [InlineData("ReturnTaskOfObject_StringValue")] + [InlineData("ReturnString")] + [InlineData("ReturnObject_StringValue")] + public async Task TextPlainFormatter_ForStringValues_GetsSelectedReturnsTextPlainContentType(string actionName) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + var expectedContentType = "text/plain;charset=utf-8"; + var expectedBody = actionName; + + // Act + var result = await client.GetAsync("http://localhost/TextPlain/" + actionName); + + // Assert + Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType); + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData("ReturnTaskOfObject_ObjectValue")] + [InlineData("ReturnObject_ObjectValue")] + public async Task JsonOutputFormatter_ForNonStringValue_GetsSelected(string actionName) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + var expectedContentType = "application/json;charset=utf-8"; + var expectedBody = actionName; + + // Act + var result = await client.GetAsync("http://localhost/TextPlain/" + actionName); + + // Assert + Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType); + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + } + + [Theory] + [InlineData("ReturnTaskOfString_NullValue")] + [InlineData("ReturnTaskOfObject_StringValue")] + [InlineData("ReturnTaskOfObject_NullValue")] + [InlineData("ReturnObject_NullValue")] + public async Task NoContentFormatter_ForNullValue_GetsSelectedAndWritesResponse(string actionName) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + string expectedContentType = null; + + // ReadBodyAsString returns empty string instead of null. + string expectedBody = ""; + + // Act + var result = await client.GetAsync("http://localhost/NoContent/" + actionName); + + // Assert + Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType); + var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); + Assert.Equal(expectedBody, body); + Assert.Equal(204, result.HttpContext.Response.StatusCode); + Assert.Equal(0, result.HttpContext.Response.ContentLength); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ConnegWebSite/ConnegWebsite.kproj b/test/WebSites/ConnegWebSite/ConnegWebsite.kproj index dd3b32c0d0..727e56bd55 100644 --- a/test/WebSites/ConnegWebSite/ConnegWebsite.kproj +++ b/test/WebSites/ConnegWebSite/ConnegWebsite.kproj @@ -31,6 +31,7 @@ + diff --git a/test/WebSites/ConnegWebSite/Controllers/NoContentController.cs b/test/WebSites/ConnegWebSite/Controllers/NoContentController.cs new file mode 100644 index 0000000000..6805cf195d --- /dev/null +++ b/test/WebSites/ConnegWebSite/Controllers/NoContentController.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; + +namespace ConnegWebsite +{ + public class NoContentController : Controller + { + public Task ReturnTaskOfString_NullValue() + { + return Task.FromResult(null); + } + + public Task ReturnTaskOfObject_NullValue() + { + return Task.FromResult(null); + } + + public string ReturnString_NullValue() + { + return null; + } + + public object ReturnObject_NullValue() + { + return null; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ConnegWebSite/Controllers/TextPlainController.cs b/test/WebSites/ConnegWebSite/Controllers/TextPlainController.cs index a3d17b7d70..1568dbafa8 100644 --- a/test/WebSites/ConnegWebSite/Controllers/TextPlainController.cs +++ b/test/WebSites/ConnegWebSite/Controllers/TextPlainController.cs @@ -30,7 +30,7 @@ namespace ConnegWebsite public object ReturnObject_StringValue() { - return ""; + return "ReturnObject_StringValue"; } public object ReturnObject_ObjectValue()