Alter content negotiation algorithm so that it can be configured (via
MvcOptions) to always respect an explicit Accept header. Fixes #4612.
This commit is contained in:
parent
2e2784aa3d
commit
5a3875ea72
|
|
@ -22,12 +22,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// </remarks>
|
||||
public virtual StringSegment ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating that content-negotiation could not find a formatter based on the
|
||||
/// information on the <see cref="Http.HttpRequest"/>.
|
||||
/// </summary>
|
||||
public virtual bool? FailedContentNegotiation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the object to write to the response.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
/// <summary>
|
||||
/// A formatter which selects itself when content-negotiation has failed and writes a 406 Not Acceptable response.
|
||||
/// </summary>
|
||||
public class HttpNotAcceptableOutputFormatter : IOutputFormatter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool CanWriteResult(OutputFormatterCanWriteContext context)
|
||||
{
|
||||
return context.FailedContentNegotiation ?? false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
OptionsFormatters = options.Value.OutputFormatters;
|
||||
RespectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader;
|
||||
ReturnHttpNotAcceptable = options.Value.ReturnHttpNotAcceptable;
|
||||
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
|
||||
WriterFactory = writerFactory.CreateWriter;
|
||||
}
|
||||
|
|
@ -64,6 +65,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// </summary>
|
||||
protected bool RespectBrowserAcceptHeader { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of <see cref="MvcOptions.ReturnHttpNotAcceptable"/>.
|
||||
/// </summary>
|
||||
protected bool ReturnHttpNotAcceptable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the writer factory delegate.
|
||||
/// </summary>
|
||||
|
|
@ -130,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
{
|
||||
// No formatter supports this.
|
||||
Logger.NoFormatter(formatterContext);
|
||||
|
||||
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
|
|
@ -175,71 +181,57 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
throw new ArgumentNullException(nameof(formatters));
|
||||
}
|
||||
|
||||
// Check if any content-type was explicitly set (for example, via ProducesAttribute
|
||||
// or URL path extension mapping). If yes, then ignore content-negotiation and use this content-type.
|
||||
if (contentTypes.Count == 1)
|
||||
{
|
||||
Logger.SkippedContentNegotiation(contentTypes[0]);
|
||||
|
||||
return SelectFormatterUsingAnyAcceptableContentType(formatterContext, formatters, contentTypes);
|
||||
}
|
||||
|
||||
var request = formatterContext.HttpContext.Request;
|
||||
|
||||
var mediaTypes = GetMediaTypes(contentTypes, request);
|
||||
var acceptableMediaTypes = GetAcceptableMediaTypes(contentTypes, request);
|
||||
var selectFormatterWithoutRegardingAcceptHeader = false;
|
||||
IOutputFormatter selectedFormatter = null;
|
||||
if (contentTypes.Count == 0)
|
||||
|
||||
if (acceptableMediaTypes.Count == 0)
|
||||
{
|
||||
// Check if we have enough information to do content-negotiation, otherwise get the first formatter
|
||||
// which can write the type. Let the formatter choose the Content-Type.
|
||||
if (!(mediaTypes.Count > 0))
|
||||
{
|
||||
Logger.NoAcceptForNegotiation();
|
||||
// There is either no Accept header value, or it contained */* and we
|
||||
// are not currently respecting the 'browser accept header'.
|
||||
Logger.NoAcceptForNegotiation();
|
||||
|
||||
return SelectFormatterNotUsingAcceptHeaders(formatterContext, formatters);
|
||||
}
|
||||
|
||||
//
|
||||
// Content-Negotiation starts from this point on.
|
||||
//
|
||||
|
||||
// 1. Select based on sorted accept headers.
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||
formatterContext,
|
||||
formatters,
|
||||
mediaTypes);
|
||||
|
||||
// 2. No formatter was found based on Accept header. Fallback to the first formatter which can write
|
||||
// the type. Let the formatter choose the Content-Type.
|
||||
if (selectedFormatter == null)
|
||||
{
|
||||
Logger.NoFormatterFromNegotiation(mediaTypes);
|
||||
|
||||
// Set this flag to indicate that content-negotiation has failed to let formatters decide
|
||||
// if they want to write the response or not.
|
||||
formatterContext.FailedContentNegotiation = true;
|
||||
|
||||
return SelectFormatterNotUsingAcceptHeaders(formatterContext, formatters);
|
||||
}
|
||||
selectFormatterWithoutRegardingAcceptHeader = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mediaTypes.Count > 0)
|
||||
if (contentTypes.Count == 0)
|
||||
{
|
||||
// Use whatever formatter can meet the client's request
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
|
||||
formatterContext,
|
||||
formatters,
|
||||
mediaTypes);
|
||||
acceptableMediaTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Verify that a content type from the context is compatible with the client's request
|
||||
selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
|
||||
formatterContext,
|
||||
formatters,
|
||||
acceptableMediaTypes,
|
||||
contentTypes);
|
||||
}
|
||||
|
||||
if (selectedFormatter == null)
|
||||
if (selectedFormatter == null && !ReturnHttpNotAcceptable)
|
||||
{
|
||||
Logger.NoFormatterFromNegotiation(acceptableMediaTypes);
|
||||
|
||||
selectFormatterWithoutRegardingAcceptHeader = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectFormatterWithoutRegardingAcceptHeader)
|
||||
{
|
||||
if (contentTypes.Count == 0)
|
||||
{
|
||||
selectedFormatter = SelectFormatterNotUsingContentType(
|
||||
formatterContext,
|
||||
formatters);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Either there were no acceptHeaders that were present OR
|
||||
// There were no accept headers which matched OR
|
||||
// There were acceptHeaders which matched but there was no formatter
|
||||
// which supported any of them.
|
||||
// In any of these cases, if the user has specified content types,
|
||||
// do a last effort to find a formatter which can write any of the user specified content type.
|
||||
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
|
||||
formatterContext,
|
||||
formatters,
|
||||
|
|
@ -250,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
return selectedFormatter;
|
||||
}
|
||||
|
||||
private List<MediaTypeSegmentWithQuality> GetMediaTypes(
|
||||
private List<MediaTypeSegmentWithQuality> GetAcceptableMediaTypes(
|
||||
MediaTypeCollection contentTypes,
|
||||
HttpRequest request)
|
||||
{
|
||||
|
|
@ -264,11 +256,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
result.Clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!InAcceptableMediaTypes(result[i].MediaType, contentTypes))
|
||||
{
|
||||
result.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
result.Sort((left, right) => left.Quality > right.Quality ? -1 : (left.Quality == right.Quality ? 0 : 1));
|
||||
|
|
@ -276,26 +263,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
return result;
|
||||
}
|
||||
|
||||
private static bool InAcceptableMediaTypes(StringSegment mediaType, MediaTypeCollection acceptableMediaTypes)
|
||||
{
|
||||
if (acceptableMediaTypes.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var parsedMediaType = new MediaType(mediaType);
|
||||
for (int i = 0; i < acceptableMediaTypes.Count; i++)
|
||||
{
|
||||
var acceptableMediaType = new MediaType(acceptableMediaTypes[i]);
|
||||
if (acceptableMediaType.IsSubsetOf(parsedMediaType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="IOutputFormatter"/> to write the response. The first formatter which
|
||||
/// can write the response should be chosen without any consideration for content type.
|
||||
|
|
@ -307,7 +274,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// <returns>
|
||||
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
|
||||
/// </returns>
|
||||
protected virtual IOutputFormatter SelectFormatterNotUsingAcceptHeaders(
|
||||
protected virtual IOutputFormatter SelectFormatterNotUsingContentType(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters)
|
||||
{
|
||||
|
|
@ -432,6 +399,53 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="IOutputFormatter"/> to write the response based on the content type values
|
||||
/// present in <paramref name="sortedAcceptableContentTypes"/> and <paramref name="possibleOutputContentTypes"/>.
|
||||
/// </summary>
|
||||
/// <param name="formatterContext">The <see cref="OutputFormatterWriteContext"/>.</param>
|
||||
/// <param name="formatters">
|
||||
/// The list of <see cref="IOutputFormatter"/> instances to consider.
|
||||
/// </param>
|
||||
/// <param name="sortedAcceptableContentTypes">
|
||||
/// The ordered content types from the <c>Accept</c> header, sorted by descending q-value.
|
||||
/// </param>
|
||||
/// <param name="possibleOutputContentTypes">
|
||||
/// The ordered content types from <see cref="ObjectResult.ContentTypes"/> in descending priority order.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
|
||||
/// </returns>
|
||||
protected virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
|
||||
OutputFormatterWriteContext formatterContext,
|
||||
IList<IOutputFormatter> formatters,
|
||||
IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
|
||||
MediaTypeCollection possibleOutputContentTypes)
|
||||
{
|
||||
for (var i = 0; i < sortedAcceptableContentTypes.Count; i++)
|
||||
{
|
||||
var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType);
|
||||
for (var j = 0; j < possibleOutputContentTypes.Count; j++)
|
||||
{
|
||||
var candidateContentType = new MediaType(possibleOutputContentTypes[j]);
|
||||
if (candidateContentType.IsSubsetOf(acceptableContentType))
|
||||
{
|
||||
for (var k = 0; k < formatters.Count; k++)
|
||||
{
|
||||
var formatter = formatters[k];
|
||||
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
|
||||
if (formatter.CanWriteResult(formatterContext))
|
||||
{
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ValidateContentTypes(MediaTypeCollection contentTypes)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -123,6 +123,13 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
public bool RespectBrowserAcceptHeader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flag which decides whether an HTTP 406 Not Acceptable response
|
||||
/// will be returned if no formatter has been selected to format the response.
|
||||
/// <see langword="false"/> by default.
|
||||
/// </summary>
|
||||
public bool ReturnHttpNotAcceptable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of <see cref="IValueProviderFactory"/> used by this application.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||
{
|
||||
public class HttpNotAcceptableOutputFormatterTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(null)]
|
||||
public void CanWriteResult_ReturnsFalse_WhenConnegHasntFailed(bool? failedContentNegotiation)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new HttpNotAcceptableOutputFormatter();
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null)
|
||||
{
|
||||
FailedContentNegotiation = failedContentNegotiation,
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = formatter.CanWriteResult(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanWriteResult_ReturnsTrue_WhenConnegHasFailed()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new HttpNotAcceptableOutputFormatter();
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null)
|
||||
{
|
||||
FailedContentNegotiation = true,
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = formatter.CanWriteResult(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_Sets406NotAcceptable()
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new HttpNotAcceptableOutputFormatter();
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
// Act
|
||||
await formatter.WriteAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(StatusCodes.Status406NotAcceptable, context.HttpContext.Response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -249,7 +249,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_NoProvidedContentTypesAndNoAcceptHeader_ChoosesFirstFormattterThatCanWrite()
|
||||
public void SelectFormatter_NoProvidedContentTypesAndNoAcceptHeader_ChoosesFirstFormatterThatCanWrite()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
|
@ -276,11 +276,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
// Assert
|
||||
Assert.Same(formatters[1], formatter);
|
||||
Assert.Equal(new StringSegment("application/json"), context.ContentType);
|
||||
Assert.Null(context.FailedContentNegotiation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeader_ConnegFails()
|
||||
public void SelectFormatter_WithAcceptHeader_UsesFallback()
|
||||
{
|
||||
// Arrange
|
||||
var executor = CreateExecutor();
|
||||
|
|
@ -308,7 +307,39 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
// Assert
|
||||
Assert.Same(formatters[0], formatter);
|
||||
Assert.Equal(new StringSegment("application/xml"), context.ContentType);
|
||||
Assert.True(context.FailedContentNegotiation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectFormatter_WithAcceptHeaderAndReturnHttpNotAcceptable_DoesNotUseFallback()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TestOptionsManager<MvcOptions>();
|
||||
options.Value.ReturnHttpNotAcceptable = true;
|
||||
|
||||
var executor = CreateExecutor(options);
|
||||
|
||||
var formatters = new List<IOutputFormatter>
|
||||
{
|
||||
new TestXmlOutputFormatter(),
|
||||
new TestJsonOutputFormatter(),
|
||||
};
|
||||
|
||||
var context = new OutputFormatterWriteContext(
|
||||
new DefaultHttpContext(),
|
||||
new TestHttpResponseStreamWriterFactory().CreateWriter,
|
||||
objectType: null,
|
||||
@object: null);
|
||||
|
||||
context.HttpContext.Request.Headers[HeaderNames.Accept] = "text/custom,application/custom";
|
||||
|
||||
// Act
|
||||
var formatter = executor.SelectFormatter(
|
||||
context,
|
||||
new MediaTypeCollection { },
|
||||
formatters);
|
||||
|
||||
// Assert
|
||||
Assert.Null(formatter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -324,29 +324,6 @@ END:VCARD
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("UseTheFallback_WithDefaultFormatters")]
|
||||
[InlineData("UseTheFallback_UsingCustomFormatters")]
|
||||
public async Task NoMatchOn_RequestContentType_FallsBackOnTypeBasedMatch_MatchFound(string actionName)
|
||||
{
|
||||
// Arrange
|
||||
var expectedContentType = MediaTypeHeaderValue.Parse("application/json;charset=utf-8");
|
||||
var expectedBody = "1234";
|
||||
var targetUri = "http://localhost/FallbackOnTypeBasedMatch/" + actionName + "/?input=1234";
|
||||
var content = new StringContent("1234", Encoding.UTF8, "application/custom");
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, targetUri);
|
||||
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/custom1"));
|
||||
request.Content = content;
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContentType, response.Content.Headers.ContentType);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedBody, body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
|
|
@ -388,25 +365,6 @@ END:VCARD
|
|||
Assert.Equal("Hello World!", actualBody);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("OverrideTheFallback_WithDefaultFormatters")]
|
||||
[InlineData("OverrideTheFallback_UsingCustomFormatters")]
|
||||
public async Task NoMatchOn_RequestContentType_SkipTypeMatchByAddingACustomFormatter(string actionName)
|
||||
{
|
||||
// Arrange
|
||||
var targetUri = "http://localhost/FallbackOnTypeBasedMatch/" + actionName + "/?input=1234";
|
||||
var content = new StringContent("1234", Encoding.UTF8, "application/custom");
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, targetUri);
|
||||
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/custom1"));
|
||||
request.Content = content;
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoMatchOn_RequestContentType_FallsBackOnTypeBasedMatch_NoMatchFound_Returns406()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ namespace BasicWebSite.Controllers.ContentNegotiation
|
|||
public IActionResult OverrideTheFallback_UsingCustomFormatters(int input)
|
||||
{
|
||||
var objectResult = new ObjectResult(input);
|
||||
objectResult.Formatters.Add(new HttpNotAcceptableOutputFormatter());
|
||||
objectResult.Formatters.Add(new PlainTextFormatter());
|
||||
objectResult.Formatters.Add(_jsonOutputFormatter);
|
||||
|
||||
|
|
@ -63,7 +62,6 @@ namespace BasicWebSite.Controllers.ContentNegotiation
|
|||
public IActionResult OverrideTheFallback_WithDefaultFormatters(int input)
|
||||
{
|
||||
var objectResult = new ObjectResult(input);
|
||||
objectResult.Formatters.Add(new HttpNotAcceptableOutputFormatter());
|
||||
foreach (var formatter in _mvcOptions.Value.OutputFormatters)
|
||||
{
|
||||
objectResult.Formatters.Add(formatter);
|
||||
|
|
@ -73,14 +71,9 @@ namespace BasicWebSite.Controllers.ContentNegotiation
|
|||
}
|
||||
|
||||
public IActionResult ReturnString(
|
||||
bool matchFormatterOnObjectType,
|
||||
[FromServices] IOptions<MvcOptions> optionsAccessor)
|
||||
{
|
||||
var objectResult = new ObjectResult("Hello World!");
|
||||
if (matchFormatterOnObjectType)
|
||||
{
|
||||
objectResult.Formatters.Add(new HttpNotAcceptableOutputFormatter());
|
||||
}
|
||||
|
||||
foreach (var formatter in optionsAccessor.Value.OutputFormatters)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue