[Fixes #2190] Remove GetSupportedContentTypes method from IOutputFormatter and move to a separate metadata interface
This commit is contained in:
parent
52c1c20967
commit
ee4ffea294
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue