[Fixes #2190] Remove GetSupportedContentTypes method from IOutputFormatter and move to a separate metadata interface

This commit is contained in:
Kiran Challa 2015-03-17 11:28:34 -07:00 committed by Kiran Challa
parent 52c1c20967
commit ee4ffea294
12 changed files with 163 additions and 138 deletions

View File

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

View File

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

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;
using System.Collections.Generic;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Provides metadata information about the response format to an <see cref="IApiDescriptionProvider"/>.
/// </summary>
/// <remarks>
/// An <see cref="IOutputFormatter"/> should implement this interface to expose metadata information
/// to an <see cref="IApiDescriptionProvider"/>.
/// </remarks>
public interface IApiResponseFormatMetadataProvider
{
/// <summary>
/// Gets a filtered list of content types which are supported by the <see cref="IOutputFormatter"/>
/// for the <paramref name="declaredType"/> and <paramref name="contentType"/>.
/// </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 the <see cref="IOutputFormatter"/>.</returns>
IReadOnlyList<MediaTypeHeaderValue> GetSupportedContentTypes(
Type declaredType,
Type runtimeType,
MediaTypeHeaderValue contentType);
}
}

View File

@ -33,14 +33,6 @@ namespace Microsoft.AspNet.Mvc
return TreatNullValueAsNoContent && context.Object == null;
}
public IReadOnlyList<MediaTypeHeaderValue> GetSupportedContentTypes(
Type declaredType,
Type runtimeType,
MediaTypeHeaderValue contentType)
{
return null;
}
public Task WriteAsync(OutputFormatterContext context)
{
var response = context.ActionContext.HttpContext.Response;

View File

@ -20,14 +20,6 @@ namespace Microsoft.AspNet.Mvc
return context.FailedContentNegotiation ?? false;
}
/// <inheritdoc />
public IReadOnlyList<MediaTypeHeaderValue> GetSupportedContentTypes(Type declaredType,
Type runtimeType,
MediaTypeHeaderValue contentType)
{
return null;
}
/// <inheritdoc />
public Task WriteAsync(OutputFormatterContext context)
{

View File

@ -13,31 +13,6 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
public interface IOutputFormatter
{
/// <summary>
/// Gets a filtered list of content types which are supported by this formatter
/// for the <paramref name="declaredType"/> and <paramref name="contentType"/>.
/// </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>
/// <remarks>
/// If the value of <paramref name="contentType"/> is <c>null</c>, 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 <paramref name="contentType"/> is not <c>null</c>, then the formatter should return
/// a list of all content types that it can produce which match the given data type and content type.
/// </remarks>
IReadOnlyList<MediaTypeHeaderValue> GetSupportedContentTypes(
Type declaredType,
Type runtimeType,
MediaTypeHeaderValue contentType);
/// <summary>
/// Determines whether this <see cref="IOutputFormatter"/> can serialize
/// an object of the specified type.

View File

@ -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
/// <summary>
/// Writes an object to the output stream.
/// </summary>
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<MediaTypeHeaderValue> _supportedMediaTypes;

View File

@ -15,32 +15,6 @@ namespace Microsoft.AspNet.Mvc
/// </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 &&
runtimeType != null &&
typeof(Stream).IsAssignableFrom(runtimeType))
{
return new[] { contentType };
}
return null;
}
/// <inheritdoc />
public bool CanWriteResult([NotNull] OutputFormatterContext context, MediaTypeHeaderValue contentType)
{

View File

@ -18,14 +18,6 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
return context.Object is HttpResponseMessage;
}
public IReadOnlyList<MediaTypeHeaderValue> GetSupportedContentTypes(
Type declaredType,
Type runtimeType,
MediaTypeHeaderValue contentType)
{
return null;
}
public async Task WriteAsync(OutputFormatterContext context)
{
var response = context.ActionContext.HttpContext.Response;

View File

@ -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<IOutputFormatter>();
@ -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<MediaTypeHeaderValue>(mockCountingSupportedContentType)))
.Returns(false);
It.Is<MediaTypeHeaderValue>(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<MediaTypeHeaderValue>()))
.Returns(new List<MediaTypeHeaderValue> { mockCountingSupportedContentType });
// Set more than one formatters. The test output formatter throws on write.
result.Formatters = new List<IOutputFormatter>
{
@ -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<MediaTypeHeaderValue>(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<HttpResponse>();
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<string>(), 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<HttpResponse>();
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<MediaTypeHeaderValue> GetSupportedContentTypes(Type declaredType,
Type runtimeType,
MediaTypeHeaderValue contentType)
{
return null;
}
public virtual Task WriteAsync(OutputFormatterContext context)
{
throw new NotImplementedException();

View File

@ -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]

View File

@ -61,13 +61,6 @@ namespace Microsoft.AspNet.Mvc.Core
throw new NotImplementedException();
}
public IReadOnlyList<MediaTypeHeaderValue> GetSupportedContentTypes(Type declaredType,
Type runtimeType,
MediaTypeHeaderValue contentType)
{
return null;
}
public Task WriteAsync(OutputFormatterContext context)
{
throw new NotImplementedException();