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()