diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs
index 4126b035c4..fabce23584 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs
@@ -170,12 +170,7 @@ namespace Microsoft.AspNet.Mvc
{
foreach (var formatter in formatters)
{
- var supportedContentTypes = formatter.GetSupportedContentTypes(
- formatterContext.DeclaredType,
- formatterContext.Object?.GetType(),
- contentType: null);
-
- if (formatter.CanWriteResult(formatterContext, supportedContentTypes?.FirstOrDefault()))
+ if (formatter.CanWriteResult(formatterContext, contentType: null))
{
return formatter;
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs
index 9d0c6fbbd4..911a4728e8 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs
@@ -296,16 +296,24 @@ namespace Microsoft.AspNet.Mvc.Description
{
foreach (var formatter in formatters)
{
- var supportedTypes = formatter.GetSupportedContentTypes(declaredType, runtimeType, contentType);
- if (supportedTypes != null)
+ var responseFormatMetadataProvider = formatter as IApiResponseFormatMetadataProvider;
+ if (responseFormatMetadataProvider != null)
{
- foreach (var supportedType in supportedTypes)
+ var supportedTypes = responseFormatMetadataProvider.GetSupportedContentTypes(
+ declaredType,
+ runtimeType,
+ contentType);
+
+ if (supportedTypes != null)
{
- results.Add(new ApiResponseFormat()
+ foreach (var supportedType in supportedTypes)
{
- Formatter = formatter,
- MediaType = supportedType,
- });
+ results.Add(new ApiResponseFormat()
+ {
+ Formatter = formatter,
+ MediaType = supportedType,
+ });
+ }
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/IApiResponseFormatMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Description/IApiResponseFormatMetadataProvider.cs
new file mode 100644
index 0000000000..20b2610c2d
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Description/IApiResponseFormatMetadataProvider.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;
+using System.Collections.Generic;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNet.Mvc.Description
+{
+ ///
+ /// Provides metadata information about the response format to an .
+ ///
+ ///
+ /// An should implement this interface to expose metadata information
+ /// to an .
+ ///
+ public interface IApiResponseFormatMetadataProvider
+ {
+ ///
+ /// Gets a filtered list of content types which are supported by the
+ /// for the and .
+ ///
+ /// The declared type for which the supported content types are desired.
+ /// The runtime type for which the supported content types are desired.
+ ///
+ /// The content type for which the supported content types are desired, or null if any content
+ /// type can be used.
+ ///
+ /// Content types which are supported by the .
+ IReadOnlyList GetSupportedContentTypes(
+ Type declaredType,
+ Type runtimeType,
+ MediaTypeHeaderValue contentType);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNoContentOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNoContentOutputFormatter.cs
index 56707b2128..4b9f752f2a 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNoContentOutputFormatter.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNoContentOutputFormatter.cs
@@ -33,14 +33,6 @@ namespace Microsoft.AspNet.Mvc
return TreatNullValueAsNoContent && context.Object == null;
}
- public IReadOnlyList GetSupportedContentTypes(
- Type declaredType,
- Type runtimeType,
- MediaTypeHeaderValue contentType)
- {
- return null;
- }
-
public Task WriteAsync(OutputFormatterContext context)
{
var response = context.ActionContext.HttpContext.Response;
diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNotAcceptableOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNotAcceptableOutputFormatter.cs
index 149fcaf086..298f6f2516 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNotAcceptableOutputFormatter.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/HttpNotAcceptableOutputFormatter.cs
@@ -20,14 +20,6 @@ namespace Microsoft.AspNet.Mvc
return context.FailedContentNegotiation ?? false;
}
- ///
- public IReadOnlyList GetSupportedContentTypes(Type declaredType,
- Type runtimeType,
- MediaTypeHeaderValue contentType)
- {
- return null;
- }
-
///
public Task WriteAsync(OutputFormatterContext context)
{
diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormatter.cs
index f56e75eb50..b1e77e7a52 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormatter.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/IOutputFormatter.cs
@@ -13,31 +13,6 @@ namespace Microsoft.AspNet.Mvc
///
public interface IOutputFormatter
{
- ///
- /// Gets a filtered list of content types which are supported by this formatter
- /// for the and .
- ///
- /// The declared type for which the supported content types are desired.
- /// The runtime type for which the supported content types are desired.
- ///
- /// The content type for which the supported content types are desired, or null if any content
- /// type can be used.
- ///
- /// Content types which are supported by this formatter.
- ///
- /// If the value of is null, then the formatter should return a list
- /// of all content types that it can produce for the given data type. This may occur during content
- /// negotiation when the HTTP Accept header is not specified, or when no match for the value in the Accept
- /// header can be found.
- ///
- /// If the value of is not null, then the formatter should return
- /// a list of all content types that it can produce which match the given data type and content type.
- ///
- IReadOnlyList GetSupportedContentTypes(
- Type declaredType,
- Type runtimeType,
- MediaTypeHeaderValue contentType);
-
///
/// Determines whether this can serialize
/// an object of the specified type.
diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs
index ed50ac0ae8..6fa41b6647 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
+using Microsoft.AspNet.Mvc.Description;
using Microsoft.Framework.Internal;
using Microsoft.Net.Http.Headers;
@@ -16,7 +17,7 @@ namespace Microsoft.AspNet.Mvc
///
/// Writes an object to the output stream.
///
- public abstract class OutputFormatter : IOutputFormatter
+ public abstract class OutputFormatter : IOutputFormatter, IApiResponseFormatMetadataProvider
{
// using a field so we can return it as both IList and IReadOnlyList
private readonly List _supportedMediaTypes;
diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/StreamOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/StreamOutputFormatter.cs
index 0a019f5b11..f4298ce457 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Formatters/StreamOutputFormatter.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/StreamOutputFormatter.cs
@@ -15,32 +15,6 @@ namespace Microsoft.AspNet.Mvc
///
public class StreamOutputFormatter : IOutputFormatter
{
- ///
- /// Echos the if the implements
- /// and is not null.
- ///
- /// The declared type for which the supported content types are desired.
- /// The runtime type for which the supported content types are desired.
- ///
- /// The content type for which the supported content types are desired, or null if any content
- /// type can be used.
- ///
- /// Content types which are supported by this formatter.
- public IReadOnlyList GetSupportedContentTypes(
- Type declaredType,
- Type runtimeType,
- MediaTypeHeaderValue contentType)
- {
- if (contentType != null &&
- runtimeType != null &&
- typeof(Stream).IsAssignableFrom(runtimeType))
- {
- return new[] { contentType };
- }
-
- return null;
- }
-
///
public bool CanWriteResult([NotNull] OutputFormatterContext context, MediaTypeHeaderValue contentType)
{
diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs
index 30fe78acd4..1998074b36 100644
--- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs
+++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Formatters/HttpResponseMessageOutputFormatter.cs
@@ -18,14 +18,6 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
return context.Object is HttpResponseMessage;
}
- public IReadOnlyList GetSupportedContentTypes(
- Type declaredType,
- Type runtimeType,
- MediaTypeHeaderValue contentType)
- {
- return null;
- }
-
public async Task WriteAsync(OutputFormatterContext context)
{
var response = context.ActionContext.HttpContext.Response;
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectResultTests.cs
index b34ceab306..ef886a8e37 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectResultTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectResultTests.cs
@@ -319,7 +319,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
(string acceptHeader)
{
// For no accept headers,
- // can write is called twice once for the request media type and once for the type match pass.
+ // can write is called twice once for the request Content-Type and once for the type match pass.
// For each additional accept header, it is called once.
// Arrange
var acceptHeaderCollection = string.IsNullOrEmpty(acceptHeader) ?
@@ -332,7 +332,6 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
var actionContext = CreateMockActionContext(httpResponse.Object,
requestAcceptHeader: acceptHeader,
requestContentType: "application/xml");
- var requestContentType = MediaTypeHeaderValue.Parse("application/xml");
var input = "testInput";
var result = new ObjectResult(input);
var mockCountingFormatter = new Mock();
@@ -345,16 +344,11 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
};
var mockCountingSupportedContentType = MediaTypeHeaderValue.Parse("application/text");
mockCountingFormatter.Setup(o => o.CanWriteResult(context,
- It.IsNotIn(mockCountingSupportedContentType)))
- .Returns(false);
+ It.Is(mth => mth == null)))
+ .Returns(true);
mockCountingFormatter.Setup(o => o.CanWriteResult(context, mockCountingSupportedContentType))
.Returns(true);
- mockCountingFormatter.Setup(o => o.GetSupportedContentTypes(context.DeclaredType,
- input.GetType(),
- It.IsAny()))
- .Returns(new List { mockCountingSupportedContentType });
-
// Set more than one formatters. The test output formatter throws on write.
result.Formatters = new List
{
@@ -367,10 +361,13 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
// Assert
Assert.Equal(mockCountingFormatter.Object, formatter);
- mockCountingFormatter.Verify(v => v.CanWriteResult(context,
- mockCountingSupportedContentType),
- Times.Once());
- var callCount = (acceptHeaderCollection == null ? 0 : acceptHeaderCollection.Count()) + 1;
+ mockCountingFormatter.Verify(v => v.CanWriteResult(context, null), Times.Once());
+
+ // CanWriteResult is invoked for the following cases:
+ // 1. For each accept header present
+ // 2. Request Content-Type
+ // 3. Type based match
+ var callCount = (acceptHeaderCollection == null ? 0 : acceptHeaderCollection.Count()) + 2;
mockCountingFormatter.Verify(v => v.CanWriteResult(context,
It.IsNotIn(mockCountingSupportedContentType)),
Times.Exactly(callCount));
@@ -770,7 +767,70 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
var actual = new StreamReader(responseStream).ReadToEnd();
Assert.Equal(expectedData, actual);
}
-
+
+ [Fact]
+ public async Task ObjectResult_WithStream_DoesNotSetContentType_IfNotProvided()
+ {
+ // Arrange
+ var objectResult = new ObjectResult(new MemoryStream(Encoding.UTF8.GetBytes("Name=James")));
+ var outputFormatters = new IOutputFormatter[]
+ {
+ new StreamOutputFormatter(),
+ new JsonOutputFormatter()
+ };
+ var response = new Mock();
+ var responseStream = new MemoryStream();
+ response.SetupGet(r => r.Body).Returns(responseStream);
+ var expectedData = "Name=James";
+
+ var actionContext = CreateMockActionContext(
+ outputFormatters,
+ response.Object,
+ requestAcceptHeader: null,
+ requestContentType: null);
+
+ // Act
+ await objectResult.ExecuteResultAsync(actionContext);
+
+ // Assert
+ response.VerifySet(r => r.ContentType = It.IsAny(), Times.Never());
+ responseStream.Position = 0;
+ var actual = new StreamReader(responseStream).ReadToEnd();
+ Assert.Equal(expectedData, actual);
+ }
+
+ [Fact]
+ public async Task ObjectResult_WithStream_SetsExplicitContentType()
+ {
+ // Arrange
+ var objectResult = new ObjectResult(new MemoryStream(Encoding.UTF8.GetBytes("Name=James")));
+ objectResult.ContentTypes.Add(new MediaTypeHeaderValue("application/foo"));
+ var outputFormatters = new IOutputFormatter[]
+ {
+ new StreamOutputFormatter(),
+ new JsonOutputFormatter()
+ };
+ var response = new Mock();
+ var responseStream = new MemoryStream();
+ response.SetupGet(r => r.Body).Returns(responseStream);
+ var expectedData = "Name=James";
+
+ var actionContext = CreateMockActionContext(
+ outputFormatters,
+ response.Object,
+ requestAcceptHeader: "application/json",
+ requestContentType: null);
+
+ // Act
+ await objectResult.ExecuteResultAsync(actionContext);
+
+ // Assert
+ response.VerifySet(r => r.ContentType = "application/foo");
+ responseStream.Position = 0;
+ var actual = new StreamReader(responseStream).ReadToEnd();
+ Assert.Equal(expectedData, actual);
+ }
+
private static ActionContext CreateMockActionContext(
HttpResponse response = null,
string requestAcceptHeader = "application/*",
@@ -871,13 +931,6 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults
return false;
}
- public IReadOnlyList GetSupportedContentTypes(Type declaredType,
- Type runtimeType,
- MediaTypeHeaderValue contentType)
- {
- return null;
- }
-
public virtual Task WriteAsync(OutputFormatterContext context)
{
throw new NotImplementedException();
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs
index 032b787cf8..6a004eedb1 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs
@@ -11,36 +11,51 @@ namespace Microsoft.AspNet.Mvc
public class StreamOutputFormatterTest
{
[Theory]
- [InlineData(typeof(Stream), typeof(FileStream), "text/plain", "text/plain")]
- [InlineData(typeof(object), typeof(FileStream), "text/plain", "text/plain")]
- [InlineData(typeof(object), typeof(MemoryStream), "text/plain", "text/plain")]
- [InlineData(typeof(object), typeof(object), "text/plain", null)]
- [InlineData(typeof(object), typeof(string), "text/plain", null)]
- [InlineData(typeof(object), null, "text/plain", null)]
- [InlineData(typeof(IActionResult), null, "text/plain", null)]
- [InlineData(typeof(IActionResult), typeof(IActionResult), "text/plain", null)]
- public void GetSupportedContentTypes_ReturnsAppropriateValues(Type declaredType,
- Type runtimeType,
- string contentType,
- string expected)
+ [InlineData(typeof(Stream), "text/plain")]
+ [InlineData(typeof(Stream), null)]
+ [InlineData(typeof(object), "text/plain")]
+ [InlineData(typeof(object), null)]
+ [InlineData(typeof(IActionResult), "text/plain")]
+ [InlineData(typeof(IActionResult), null)]
+ public void CanWriteResult_ReturnsTrue_ForStreams(Type declaredType, string contentType)
{
// Arrange
var formatter = new StreamOutputFormatter();
var contentTypeHeader = contentType == null ? null : new MediaTypeHeaderValue(contentType);
+ var formatterContext = new OutputFormatterContext()
+ {
+ DeclaredType = declaredType,
+ Object = new MemoryStream()
+ };
// Act
- var contentTypes = formatter.GetSupportedContentTypes(declaredType, runtimeType, contentTypeHeader);
+ var canWrite = formatter.CanWriteResult(formatterContext, contentTypeHeader);
// Assert
- if (expected == null)
+ Assert.True(canWrite);
+ }
+
+ [Theory]
+ [InlineData(typeof(object), "text/plain")]
+ [InlineData(typeof(object), null)]
+ [InlineData(typeof(SimplePOCO), "text/plain")]
+ [InlineData(typeof(SimplePOCO), null)]
+ public void CanWriteResult_OnlyActsOnStreams_IgnoringContentType(Type declaredType, string contentType)
+ {
+ // Arrange
+ var formatter = new StreamOutputFormatter();
+ var contentTypeHeader = contentType == null ? null : new MediaTypeHeaderValue(contentType);
+ var formatterContext = new OutputFormatterContext()
{
- Assert.Null(contentTypes);
- }
- else
- {
- Assert.Equal(1, contentTypes.Count);
- Assert.Equal(expected, contentTypes[0].ToString());
- }
+ DeclaredType = declaredType,
+ Object = new SimplePOCO()
+ };
+
+ // Act
+ var canWrite = formatter.CanWriteResult(formatterContext, contentTypeHeader);
+
+ // Assert
+ Assert.False(canWrite);
}
[Theory]
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorTest.cs
index 8a94813df8..2f45a80f55 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/OptionDescriptors/OutputFormatterDescriptorTest.cs
@@ -61,13 +61,6 @@ namespace Microsoft.AspNet.Mvc.Core
throw new NotImplementedException();
}
- public IReadOnlyList GetSupportedContentTypes(Type declaredType,
- Type runtimeType,
- MediaTypeHeaderValue contentType)
- {
- return null;
- }
-
public Task WriteAsync(OutputFormatterContext context)
{
throw new NotImplementedException();