diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs index 3eb0cf431f..9884e3c5c6 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs @@ -69,6 +69,14 @@ namespace Microsoft.AspNet.Mvc public virtual IOutputFormatter SelectFormatter(OutputFormatterContext formatterContext, IEnumerable formatters) { + if (ContentTypes.Count == 1) + { + // There is only one content type specified so we can skip looking at the accept headers. + return SelectFormatterUsingAnyAcceptableContentType(formatterContext, + formatters, + ContentTypes); + } + var incomingAcceptHeaderMediaTypes = formatterContext.ActionContext.HttpContext.Request.GetTypedHeaders().Accept ?? new MediaTypeHeaderValue[] { }; @@ -143,14 +151,6 @@ namespace Microsoft.AspNet.Mvc } } } - else if (ContentTypes.Count == 1) - { - // There is only one value that can be supported. - selectedFormatter = SelectFormatterUsingAnyAcceptableContentType( - formatterContext, - formatters, - ContentTypes); - } else { if (respectAcceptHeader) diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/StreamOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/StreamOutputFormatter.cs new file mode 100644 index 0000000000..39c4cf2154 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/StreamOutputFormatter.cs @@ -0,0 +1,72 @@ +// 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.IO; +using System.Threading.Tasks; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Always copies the stream to the response, regardless of requested content type. + /// + 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) + { + // Ignore the passed in content type, if the object is a Stream. + if (context.Object is Stream) + { + context.SelectedContentType = contentType; + return true; + } + + return false; + } + + /// + public async Task WriteAsync([NotNull] OutputFormatterContext context) + { + using (var valueAsStream = ((Stream)context.Object)) + { + var response = context.ActionContext.HttpContext.Response; + + if (context.SelectedContentType != null) + { + response.ContentType = context.SelectedContentType.ToString(); + } + + await valueAsStream.CopyToAsync(response.Body); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index dcd3f49bd4..fdba761395 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -44,6 +44,7 @@ namespace Microsoft.AspNet.Mvc // Set up default output formatters. options.OutputFormatters.Add(new HttpNoContentOutputFormatter()); options.OutputFormatters.Add(new StringOutputFormatter()); + options.OutputFormatters.Add(new StreamOutputFormatter()); options.OutputFormatters.Add(new JsonOutputFormatter()); // Set up default mapping for json extensions to content type diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs new file mode 100644 index 0000000000..032b787cf8 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs @@ -0,0 +1,89 @@ +// 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.IO; +using Microsoft.Net.Http.Headers; +using Xunit; + +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) + { + // Arrange + var formatter = new StreamOutputFormatter(); + var contentTypeHeader = contentType == null ? null : new MediaTypeHeaderValue(contentType); + + // Act + var contentTypes = formatter.GetSupportedContentTypes(declaredType, runtimeType, contentTypeHeader); + + // Assert + if (expected == null) + { + Assert.Null(contentTypes); + } + else + { + Assert.Equal(1, contentTypes.Count); + Assert.Equal(expected, contentTypes[0].ToString()); + } + } + + [Theory] + [InlineData(typeof(object))] + [InlineData(typeof(SimplePOCO))] + [InlineData(null)] + public void CanWriteResult_OnlyActsOnStreams(Type type) + { + // Arrange + var formatter = new StreamOutputFormatter(); + var context = new OutputFormatterContext(); + var contentType = new MediaTypeHeaderValue("text/plain"); + + context.Object = type != null ? Activator.CreateInstance(type) : null; + + // Act + var result = formatter.CanWriteResult(context, contentType); + + // Assert + Assert.False(result); + Assert.Null(context.SelectedContentType); + } + + [Fact] + public void CanWriteResult_SetsContentType() + { + // Arrange + var formatter = new StreamOutputFormatter(); + var contentType = new MediaTypeHeaderValue("text/plain"); + var context = new OutputFormatterContext(); + context.Object = new MemoryStream(); + + // Act + var result = formatter.CanWriteResult(context, contentType); + + // Assert + Assert.True(result); + Assert.Same(contentType, context.SelectedContentType); + } + + private class SimplePOCO + { + public int Id { get; set; } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/StreamOutputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/StreamOutputFormatterTest.cs new file mode 100644 index 0000000000..4614323dfb --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/StreamOutputFormatterTest.cs @@ -0,0 +1,40 @@ +// 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 FormatterWebSite; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class StreamOutputFormatterTest + { + private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(FormatterWebSite)); + private readonly Action _app = new Startup().Configure; + + [Theory] + [InlineData("SimpleMemoryStream", null)] + [InlineData("MemoryStreamWithContentType", "text/html")] + [InlineData("MemoryStreamWithContentTypeFromProduces", "text/plain")] + [InlineData("MemoryStreamWithContentTypeFromProducesWithMultipleValues", "text/html")] + [InlineData("MemoryStreamOverridesContentTypeWithProduces", "text/plain")] + public async Task StreamOutputFormatter_ReturnsAppropriateContentAndContentType(string actionName, string contentType) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/Stream/" + actionName); + var body = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(contentType, response.Content.Headers.ContentType?.ToString()); + + Assert.Equal("Sample text from a stream", body); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs similarity index 96% rename from test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs rename to test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs index 7e22edeb35..07f420c24d 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs @@ -10,7 +10,7 @@ using Xunit; namespace Microsoft.AspNet.Mvc { - public class MvcOptionSetupTest + public class MvcOptionsSetupTest { [Fact] public void Setup_SetsUpViewEngines() @@ -83,10 +83,11 @@ namespace Microsoft.AspNet.Mvc setup.Configure(mvcOptions); // Assert - Assert.Equal(3, mvcOptions.OutputFormatters.Count); + Assert.Equal(4, mvcOptions.OutputFormatters.Count); Assert.IsType(mvcOptions.OutputFormatters[0].Instance); Assert.IsType(mvcOptions.OutputFormatters[1].Instance); - Assert.IsType(mvcOptions.OutputFormatters[2].Instance); + Assert.IsType(mvcOptions.OutputFormatters[2].Instance); + Assert.IsType(mvcOptions.OutputFormatters[3].Instance); } [Fact] diff --git a/test/WebSites/FormatterWebSite/Controllers/StreamController.cs b/test/WebSites/FormatterWebSite/Controllers/StreamController.cs new file mode 100644 index 0000000000..e3de944274 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Controllers/StreamController.cs @@ -0,0 +1,60 @@ +// 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.IO; +using Microsoft.AspNet.Mvc; + +namespace FormatterWebSite +{ + public class StreamController : Controller + { + [HttpGet] + public Stream SimpleMemoryStream() + { + return CreateDefaultStream(); + } + + [HttpGet] + public Stream MemoryStreamWithContentType() + { + Response.ContentType = "text/html"; + return CreateDefaultStream(); + } + + [HttpGet] + [Produces("text/plain")] + public Stream MemoryStreamWithContentTypeFromProduces() + { + return CreateDefaultStream(); + } + + [HttpGet] + [Produces("text/html", "text/plain")] + public Stream MemoryStreamWithContentTypeFromProducesWithMultipleValues() + { + return CreateDefaultStream(); + } + + [HttpGet] + [Produces("text/plain")] + public Stream MemoryStreamOverridesContentTypeWithProduces() + { + // Produces will set a ContentType on the implicit ObjectResult and + // ContentType on response are overriden by content types from ObjectResult. + Response.ContentType = "text/html"; + + return CreateDefaultStream(); + } + + private static Stream CreateDefaultStream() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write("Sample text from a stream"); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + } +}