Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Implement StreamOutputFormatter
Browse files Browse the repository at this point in the history
Include Functional tests and unit tests

Resolves #1653
  • Loading branch information
Yishai Galatzer committed Feb 3, 2015
1 parent 2c5ae68 commit b9c5019
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 9 deletions.
16 changes: 8 additions & 8 deletions src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public override async Task ExecuteResultAsync(ActionContext context)
public virtual IOutputFormatter SelectFormatter(OutputFormatterContext formatterContext,
IEnumerable<IOutputFormatter> 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[] { };

Expand Down Expand Up @@ -143,14 +151,6 @@ public override async Task ExecuteResultAsync(ActionContext context)
}
}
}
else if (ContentTypes.Count == 1)
{
// There is only one value that can be supported.
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
formatterContext,
formatters,
ContentTypes);
}
else
{
if (respectAcceptHeader)
Expand Down
76 changes: 76 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/Formatters/StreamOutputFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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
{
/// <summary>
/// Always copies the stream to the response, regardless of requested content type.
/// </summary>
public class StreamOutputFormatter : IOutputFormatter
{

/// <summary>
/// Echos the <paramref name="contentType"/> if the <paramref name="runtimeType"/> implements
/// <see cref="Stream"/> and <paramref name="contentType"/> is not <c>null</c>.
/// </summary>
/// <param name="declaredType">The declared type for which the supported content types are desired.</param>
/// <param name="runtimeType">The runtime type for which the supported content types are desired.</param>
/// <param name="contentType">
/// The content type for which the supported content types are desired, or <c>null</c> if any content
/// type can be used.
/// </param>
/// <returns>Content types which are supported by this formatter.</returns>
public IReadOnlyList<MediaTypeHeaderValue> GetSupportedContentTypes(
Type declaredType,
Type runtimeType,
MediaTypeHeaderValue contentType)
{
if (contentType == null)
{
return null;
}

if (runtimeType != null && typeof(Stream).IsAssignableFrom(runtimeType))
{
return new[] { contentType };
}

return null;
}

/// <inheritdoc />
public bool CanWriteResult([NotNull] OutputFormatterContext context, MediaTypeHeaderValue contentType)
{
// Ignore the passed in content type, if the object is a Stream.
// always return it as a text/plain format.
if (context.Object is Stream)
{
context.SelectedContentType = contentType;
return true;
}

return false;
}

/// <inheritdoc />
public async Task WriteAsync([NotNull] OutputFormatterContext context)
{
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);
}
}
}
1 change: 1 addition & 0 deletions src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static void ConfigureMvc(MvcOptions options)
// 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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.AspNet.Mvc;
using Microsoft.Net.Http.Headers;
using Xunit;

namespace Microsoft.AspNet.Mvc
{
public class StreamOutputFormatterTest
{
[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)]
[Theory]
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());
}
}

[InlineData(typeof(object))]
[InlineData(typeof(SimplePOCO))]
[InlineData(null)]
[Theory]
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; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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.Net;
using System.Net.Http.Headers;
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<IApplicationBuilder> _app = new Startup().Configure;

[InlineData("SimpleMemoryStream", null)]
[InlineData("MemoryStreamWithContentType", "text/html")]
[InlineData("MemoryStreamWithContentTypeFromProduces", "text/plain")]
[InlineData("MemoryStreamWithContentTypeFromProducesWithMultipleValues", "text/html")]
[InlineData("MemoryStreamOverridesContentTypeWithProduces", "text/plain")]
[Theory]
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);
}
}
}
3 changes: 2 additions & 1 deletion test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ public void Setup_SetsUpOutputFormatters()
setup.Configure(mvcOptions);

// Assert
Assert.Equal(3, mvcOptions.OutputFormatters.Count);
Assert.Equal(4, mvcOptions.OutputFormatters.Count);
Assert.IsType<HttpNoContentOutputFormatter>(mvcOptions.OutputFormatters[0].Instance);
Assert.IsType<StringOutputFormatter>(mvcOptions.OutputFormatters[1].Instance);
Assert.IsType<StreamOutputFormatter>(mvcOptions.OutputFormatters[1].Instance);
Assert.IsType<JsonOutputFormatter>(mvcOptions.OutputFormatters[2].Instance);
}

Expand Down
61 changes: 61 additions & 0 deletions test/WebSites/FormatterWebSite/Controllers/StreamController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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;
using Microsoft.Net.Http.Headers;

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 ObjecResult 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;
}
}
}

0 comments on commit b9c5019

Please sign in to comment.