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