NoContentFormatter: Writes 204 to the response status code if the value returned is null.

This commit is contained in:
harshgMSFT 2014-07-30 12:47:18 -07:00
parent 538e589894
commit 1684d1d322
11 changed files with 267 additions and 61 deletions

View File

@ -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
{
/// <summary>
/// Sets the status code to 204 if the content is null.
/// </summary>
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<bool>(true);
}
}
}

View File

@ -32,6 +32,7 @@
<Compile Include="ActionResults\HttpNotFoundResult.cs" />
<Compile Include="Formatters\TextPlainFormatter.cs" />
<Compile Include="HttpMethodAttribute.cs" />
<Compile Include="Formatters\NoContentFormatter.cs" />
<Compile Include="Internal\DecisionTree\DecisionCriterionValue.cs" />
<Compile Include="Internal\DecisionTree\DecisionCriterionValueEqualityComparer.cs" />
<Compile Include="Logging\AttributeRouteRouteAsyncValues.cs" />

View File

@ -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));

View File

@ -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<object[]> 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);
}
}
}

View File

@ -30,8 +30,11 @@
<Compile Include="ActionResults\RedirectResultTest.cs" />
<Compile Include="ActionResults\RedirectToActionResultTest.cs" />
<Compile Include="ActionResults\RedirectToRouteResultTest.cs" />
<Compile Include="ActionResults\RedirectResultTest.cs" />
<Compile Include="DefaultActionDiscoveryConventionsActionSelectionTests.cs" />
<Compile Include="AntiXsrf\AntiForgeryOptionsTests.cs" />
<Compile Include="Filters\ProducesAttributeTests.cs" />
<Compile Include="Formatters\NoContentFormatterTests.cs" />
<Compile Include="Formatters\OutputFormatterTests.cs" />
<Compile Include="Formatters\TextPlainFormatterTests.cs" />
<Compile Include="DefaultViewComponentActivatorTests.cs" />
@ -58,6 +61,7 @@
<Compile Include="OptionDescriptors\ValueProviderFactoryDescriptorTest.cs" />
<Compile Include="OptionDescriptors\ViewEngineDescriptorTest.cs" />
<Compile Include="OptionDescriptors\ViewEngineDscriptorExtensionsTest.cs" />
<Compile Include="MediaTypeWithQualityHeaderValueTests.cs" />
<Compile Include="ParameterBinding\ModelBindingHelperTest.cs" />
<Compile Include="AntiXsrf\AntiForgeryTokenSerializerTest.cs" />
<Compile Include="AntiXsrf\ITokenProvider.cs" />

View File

@ -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);
}
}
}

View File

@ -31,6 +31,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="BasicTests.cs" />
<Compile Include="OutputFormattterTests.cs" />
<Compile Include="ConnegTests.cs" />
<Compile Include="AntiForgeryTests.cs" />
<Compile Include="ModelBindingTests.cs" />

View File

@ -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<IBuilder> _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);
}
}
}

View File

@ -31,6 +31,7 @@
<Compile Include="Controllers\ProducesContentBaseController.cs" />
<Compile Include="Controllers\ProducesContentOnClassController.cs" />
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Controllers\NoContentController.cs" />
<Compile Include="Controllers\TextPlainController.cs" />
<Compile Include="Models\User.cs" />
<Compile Include="ContentType.cs" />

View File

@ -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<string> ReturnTaskOfString_NullValue()
{
return Task.FromResult<string>(null);
}
public Task<object> ReturnTaskOfObject_NullValue()
{
return Task.FromResult<object>(null);
}
public string ReturnString_NullValue()
{
return null;
}
public object ReturnObject_NullValue()
{
return null;
}
}
}

View File

@ -30,7 +30,7 @@ namespace ConnegWebsite
public object ReturnObject_StringValue()
{
return "";
return "ReturnObject_StringValue";
}
public object ReturnObject_ObjectValue()