File: Infrastructure\DefaultOutputFormatterSelectorTest.cs
Web Access
Project: src\src\Mvc\Mvc.Core\test\Microsoft.AspNetCore.Mvc.Core.Test.csproj (Microsoft.AspNetCore.Mvc.Core.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
 
namespace Microsoft.AspNetCore.Mvc.Infrastructure;
 
public class DefaultObjectResultExecutorTest
{
    [Fact]
    public void SelectFormatter_UsesPassedInFormatters_IgnoresOptionsFormatters()
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new TestXmlOutputFormatter(),
                new TestJsonOutputFormatter(), // This will be chosen based on the content type
            };
        var selector = CreateSelector(new IOutputFormatter[] { });
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        context.HttpContext.Request.Headers.Accept = "application/xml"; // This will not be used
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            formatters,
            new MediaTypeCollection { "application/json" });
 
        // Assert
        Assert.Same(formatters[1], formatter);
        Assert.Equal(new StringSegment("application/json"), context.ContentType);
    }
 
    [Fact]
    public void SelectFormatter_WithOneProvidedContentType_IgnoresAcceptHeader()
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new TestXmlOutputFormatter(),
                new TestJsonOutputFormatter(), // This will be chosen based on the content type
            };
        var selector = CreateSelector(formatters);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        context.HttpContext.Request.Headers.Accept = "application/xml"; // This will not be used
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            new MediaTypeCollection { "application/json" });
 
        // Assert
        Assert.Same(formatters[1], formatter);
        Assert.Equal(new StringSegment("application/json"), context.ContentType);
    }
 
    [Fact]
    public void SelectFormatter_WithOneProvidedContentType_NoFallback()
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new TestXmlOutputFormatter(),
            };
        var selector = CreateSelector(formatters);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        context.HttpContext.Request.Headers.Accept = "application/xml"; // This will not be used
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            new MediaTypeCollection { "application/json" });
 
        // Assert
        Assert.Null(formatter);
    }
 
    // ObjectResult.ContentTypes, Accept header, expected content type
    public static TheoryData<MediaTypeCollection, string, string> ContentTypes
    {
        get
        {
            var contentTypes = new MediaTypeCollection
                {
                    "text/plain",
                    "text/xml",
                    "application/json",
                };
 
            return new TheoryData<MediaTypeCollection, string, string>()
                {
                    // Empty accept header, should select based on ObjectResult.ContentTypes.
                    { contentTypes, "", "application/json" },
 
                    // null accept header, should select based on ObjectResult.ContentTypes.
                    { contentTypes, null, "application/json" },
 
                    // The accept header does not match anything in ObjectResult.ContentTypes.
                    // The first formatter that can write the result gets to choose the content type.
                    { contentTypes, "text/custom", "application/json" },
 
                    // Accept header matches ObjectResult.ContentTypes, but no formatter supports the accept header.
                    // The first formatter that can write the result gets to choose the content type.
                    { contentTypes, "text/xml", "application/json" },
 
                    // Filters out Accept headers with 0 quality and selects the one with highest quality.
                    {
                        contentTypes,
                        "text/plain;q=0.3, text/json;q=0, text/cusotm;q=0.0, application/json;q=0.4",
                        "application/json"
                    },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(ContentTypes))]
    public void SelectFormatter_WithMultipleProvidedContentTypes_DoesConneg(
        MediaTypeCollection contentTypes,
        string acceptHeader,
        string expectedContentType)
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new CannotWriteFormatter(),
                new TestJsonOutputFormatter(),
            };
        var selector = CreateSelector(formatters);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        context.HttpContext.Request.Headers.Accept = acceptHeader;
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            contentTypes);
 
        // Assert
        Assert.Same(formatters[1], formatter);
        Assert.Equal(new StringSegment(expectedContentType), context.ContentType);
    }
 
    [Fact]
    public void SelectFormatter_NoProvidedContentTypesAndNoAcceptHeader_ChoosesFirstFormatterThatCanWrite()
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new CannotWriteFormatter(),
                new TestJsonOutputFormatter(),
                new TestXmlOutputFormatter(),
            };
        var selector = CreateSelector(formatters);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            new MediaTypeCollection());
 
        // Assert
        Assert.Same(formatters[1], formatter);
        Assert.Equal(new StringSegment("application/json"), context.ContentType);
    }
 
    [Fact]
    public void SelectFormatter_WithAcceptHeader_UsesFallback()
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new TestXmlOutputFormatter(),
                new TestJsonOutputFormatter(),
            };
        var selector = CreateSelector(formatters);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        context.HttpContext.Request.Headers.Accept = "text/custom,application/custom";
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            new MediaTypeCollection());
 
        // Assert
        Assert.Same(formatters[0], formatter);
        Assert.Equal(new StringSegment("application/xml"), context.ContentType);
    }
 
    [Fact]
    public void SelectFormatter_WithAcceptHeaderAndReturnHttpNotAcceptable_DoesNotUseFallback()
    {
        // Arrange
        var options = new MvcOptions()
        {
            ReturnHttpNotAcceptable = true,
            OutputFormatters =
                {
                    new TestXmlOutputFormatter(),
                    new TestJsonOutputFormatter(),
                },
        };
 
        var selector = CreateSelector(options);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        context.HttpContext.Request.Headers.Accept = "text/custom,application/custom";
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            new MediaTypeCollection());
 
        // Assert
        Assert.Null(formatter);
    }
 
    [Fact]
    public void SelectFormatter_WithAcceptHeaderOnly_SetsContentTypeIsServerDefinedToFalse()
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new ServerContentTypeOnlyFormatter()
            };
 
        var selector = CreateSelector(formatters);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        context.HttpContext.Request.Headers.Accept = "text/custom";
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            new MediaTypeCollection());
 
        // Assert
        Assert.Null(formatter);
    }
 
    [Fact]
    public void SelectFormatter_WithAcceptHeaderAndContentTypes_SetsContentTypeIsServerDefinedWhenExpected()
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new ServerContentTypeOnlyFormatter()
            };
 
        var selector = CreateSelector(formatters);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        context.HttpContext.Request.Headers.Accept = "text/custom, text/custom2";
 
        var serverDefinedContentTypes = new MediaTypeCollection();
        serverDefinedContentTypes.Add("text/other");
        serverDefinedContentTypes.Add("text/custom2");
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            serverDefinedContentTypes);
 
        // Assert
        Assert.Same(formatters[0], formatter);
        Assert.Equal(new StringSegment("text/custom2"), context.ContentType);
    }
 
    [Fact]
    public void SelectFormatter_WithContentTypesOnly_SetsContentTypeIsServerDefinedToTrue()
    {
        // Arrange
        var formatters = new List<IOutputFormatter>
            {
                new ServerContentTypeOnlyFormatter()
            };
 
        var selector = CreateSelector(formatters);
 
        var context = new OutputFormatterWriteContext(
            new DefaultHttpContext(),
            new TestHttpResponseStreamWriterFactory().CreateWriter,
            objectType: null,
            @object: null);
 
        var serverDefinedContentTypes = new MediaTypeCollection();
        serverDefinedContentTypes.Add("text/custom");
 
        // Act
        var formatter = selector.SelectFormatter(
            context,
            Array.Empty<IOutputFormatter>(),
            serverDefinedContentTypes);
 
        // Assert
        Assert.Same(formatters[0], formatter);
        Assert.Equal(new StringSegment("text/custom"), context.ContentType);
    }
 
    private static DefaultOutputFormatterSelector CreateSelector(IEnumerable<IOutputFormatter> formatters)
    {
        var options = new MvcOptions();
        foreach (var formatter in formatters)
        {
            options.OutputFormatters.Add(formatter);
        }
 
        return CreateSelector(options);
    }
 
    private static DefaultOutputFormatterSelector CreateSelector(MvcOptions options)
    {
        return new DefaultOutputFormatterSelector(Options.Create(options), NullLoggerFactory.Instance);
    }
 
    private class CannotWriteFormatter : IOutputFormatter
    {
        public virtual bool CanWriteResult(OutputFormatterCanWriteContext context)
        {
            return false;
        }
 
        public virtual Task WriteAsync(OutputFormatterWriteContext context)
        {
            throw new NotImplementedException();
        }
    }
 
    private class TestJsonOutputFormatter : TextOutputFormatter
    {
        public TestJsonOutputFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
 
            SupportedEncodings.Add(Encoding.UTF8);
        }
 
        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
        {
            return Task.FromResult(0);
        }
    }
 
    private class TestXmlOutputFormatter : TextOutputFormatter
    {
        public TestXmlOutputFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml"));
 
            SupportedEncodings.Add(Encoding.UTF8);
        }
 
        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
        {
            return Task.FromResult(0);
        }
    }
 
    private class TestStringOutputFormatter : TextOutputFormatter
    {
        public TestStringOutputFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
 
            SupportedEncodings.Add(Encoding.UTF8);
        }
 
        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
        {
            return Task.FromResult(0);
        }
    }
 
    private class ServerContentTypeOnlyFormatter : OutputFormatter
    {
        public override bool CanWriteResult(OutputFormatterCanWriteContext context)
        {
            // This test formatter matches if and only if the content type is specified
            // as "server defined". This lets tests identify what value the ObjectResultExecutor
            // passed for that flag.
            return context.ContentTypeIsServerDefined;
        }
 
        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
        {
            return Task.FromResult(0);
        }
    }
}