File: NewtonsoftJsonOutputFormatter.cs
Web Access
Project: src\src\Mvc\Mvc.NewtonsoftJson\src\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj (Microsoft.AspNetCore.Mvc.NewtonsoftJson)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Text;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
 
namespace Microsoft.AspNetCore.Mvc.Formatters;
 
/// <summary>
/// A <see cref="TextOutputFormatter"/> for JSON content.
/// </summary>
public partial class NewtonsoftJsonOutputFormatter : TextOutputFormatter
{
    private readonly IArrayPool<char> _charPool;
    private readonly MvcOptions _mvcOptions;
    private MvcNewtonsoftJsonOptions? _jsonOptions;
    private readonly AsyncEnumerableReader _asyncEnumerableReaderFactory;
    private JsonSerializerSettings? _serializerSettings;
 
    /// <summary>
    /// Initializes a new <see cref="NewtonsoftJsonOutputFormatter"/> instance.
    /// </summary>
    /// <param name="serializerSettings">
    /// The <see cref="JsonSerializerSettings"/>. Should be either the application-wide settings
    /// (<see cref="MvcNewtonsoftJsonOptions.SerializerSettings"/>) or an instance
    /// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
    /// </param>
    /// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
    /// <param name="mvcOptions">The <see cref="MvcOptions"/>.</param>
    [Obsolete("This constructor is obsolete and will be removed in a future version.")]
    public NewtonsoftJsonOutputFormatter(
        JsonSerializerSettings serializerSettings,
        ArrayPool<char> charPool,
        MvcOptions mvcOptions) : this(serializerSettings, charPool, mvcOptions, jsonOptions: null)
    {
    }
 
    /// <summary>
    /// Initializes a new <see cref="NewtonsoftJsonOutputFormatter"/> instance.
    /// </summary>
    /// <param name="serializerSettings">
    /// The <see cref="JsonSerializerSettings"/>. Should be either the application-wide settings
    /// (<see cref="MvcNewtonsoftJsonOptions.SerializerSettings"/>) or an instance
    /// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
    /// </param>
    /// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
    /// <param name="mvcOptions">The <see cref="MvcOptions"/>.</param>
    /// <param name="jsonOptions">The <see cref="MvcNewtonsoftJsonOptions"/>.</param>
    public NewtonsoftJsonOutputFormatter(
        JsonSerializerSettings serializerSettings,
        ArrayPool<char> charPool,
        MvcOptions mvcOptions,
        MvcNewtonsoftJsonOptions? jsonOptions)
    {
        ArgumentNullException.ThrowIfNull(serializerSettings);
        ArgumentNullException.ThrowIfNull(charPool);
 
        SerializerSettings = serializerSettings;
        _charPool = new JsonArrayPool<char>(charPool);
        _mvcOptions = mvcOptions ?? throw new ArgumentNullException(nameof(mvcOptions));
        _jsonOptions = jsonOptions;
 
        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
        SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
        SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
        SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
 
        _asyncEnumerableReaderFactory = new AsyncEnumerableReader(_mvcOptions);
    }
 
    /// <summary>
    /// Gets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
    /// </summary>
    /// <remarks>
    /// Any modifications to the <see cref="JsonSerializerSettings"/> object after this
    /// <see cref="NewtonsoftJsonOutputFormatter"/> has been used will have no effect.
    /// </remarks>
    protected JsonSerializerSettings SerializerSettings { get; }
 
    /// <summary>
    /// Called during serialization to create the <see cref="JsonWriter"/>.
    /// </summary>
    /// <param name="writer">The <see cref="TextWriter"/> used to write.</param>
    /// <returns>The <see cref="JsonWriter"/> used during serialization.</returns>
    protected virtual JsonWriter CreateJsonWriter(TextWriter writer)
    {
        ArgumentNullException.ThrowIfNull(writer);
 
        var jsonWriter = new JsonTextWriter(writer)
        {
            ArrayPool = _charPool,
            CloseOutput = false,
            AutoCompleteOnClose = false
        };
 
        return jsonWriter;
    }
 
    /// <summary>
    /// Called during serialization to create the <see cref="JsonSerializer"/>.The formatter context
    /// that is passed gives an ability to create serializer specific to the context.
    /// </summary>
    /// <returns>The <see cref="JsonSerializer"/> used during serialization and deserialization.</returns>
    protected virtual JsonSerializer CreateJsonSerializer()
    {
        if (_serializerSettings == null)
        {
            // Lock the serializer settings once the first serialization has been initiated.
            _serializerSettings = ShallowCopy(SerializerSettings);
        }
 
        return JsonSerializer.Create(_serializerSettings);
    }
 
    /// <summary>
    /// Called during serialization to create the <see cref="JsonSerializer"/>.The formatter context
    /// that is passed gives an ability to create serializer specific to the context.
    /// </summary>
    /// <param name="context">A context object for <see cref="IOutputFormatter.WriteAsync(OutputFormatterWriteContext)"/>.</param>
    /// <returns>The <see cref="JsonSerializer"/> used during serialization and deserialization.</returns>
    protected virtual JsonSerializer CreateJsonSerializer(OutputFormatterWriteContext context)
    {
        return CreateJsonSerializer();
    }
 
    /// <inheritdoc />
    public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(selectedEncoding);
 
        // Compat mode for derived options
        _jsonOptions ??= context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value;
 
        var response = context.HttpContext.Response;
 
        var responseStream = response.Body;
        FileBufferingWriteStream? fileBufferingWriteStream = null;
        if (!_mvcOptions.SuppressOutputFormatterBuffering)
        {
            fileBufferingWriteStream = new FileBufferingWriteStream(_jsonOptions.OutputFormatterMemoryBufferThreshold);
            responseStream = fileBufferingWriteStream;
        }
 
        var value = context.Object;
        if (value is not null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
        {
            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<NewtonsoftJsonOutputFormatter>>();
            Log.BufferingAsyncEnumerable(logger, value);
            try
            {
                value = await reader(value, context.HttpContext.RequestAborted);
            }
            catch (OperationCanceledException) when (context.HttpContext.RequestAborted.IsCancellationRequested) { }
 
            if (context.HttpContext.RequestAborted.IsCancellationRequested)
            {
                return;
            }
        }
 
        try
        {
            await using (var writer = context.WriterFactory(responseStream, selectedEncoding))
            {
                using var jsonWriter = CreateJsonWriter(writer);
                var jsonSerializer = CreateJsonSerializer(context);
                jsonSerializer.Serialize(jsonWriter, value);
            }
 
            if (fileBufferingWriteStream != null)
            {
                response.ContentLength = fileBufferingWriteStream.Length;
                await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter);
            }
        }
        finally
        {
            if (fileBufferingWriteStream != null)
            {
                await fileBufferingWriteStream.DisposeAsync();
            }
        }
    }
 
    private static JsonSerializerSettings ShallowCopy(JsonSerializerSettings settings)
    {
        var copiedSettings = new JsonSerializerSettings
        {
            FloatParseHandling = settings.FloatParseHandling,
            FloatFormatHandling = settings.FloatFormatHandling,
            DateParseHandling = settings.DateParseHandling,
            DateTimeZoneHandling = settings.DateTimeZoneHandling,
            DateFormatHandling = settings.DateFormatHandling,
            Formatting = settings.Formatting,
            MaxDepth = settings.MaxDepth,
            DateFormatString = settings.DateFormatString,
            Context = settings.Context,
            Error = settings.Error,
            SerializationBinder = settings.SerializationBinder,
            TraceWriter = settings.TraceWriter,
            Culture = settings.Culture,
            ReferenceResolverProvider = settings.ReferenceResolverProvider,
            EqualityComparer = settings.EqualityComparer,
            ContractResolver = settings.ContractResolver,
            ConstructorHandling = settings.ConstructorHandling,
            TypeNameAssemblyFormatHandling = settings.TypeNameAssemblyFormatHandling,
            MetadataPropertyHandling = settings.MetadataPropertyHandling,
            TypeNameHandling = settings.TypeNameHandling,
            PreserveReferencesHandling = settings.PreserveReferencesHandling,
            Converters = settings.Converters,
            DefaultValueHandling = settings.DefaultValueHandling,
            NullValueHandling = settings.NullValueHandling,
            ObjectCreationHandling = settings.ObjectCreationHandling,
            MissingMemberHandling = settings.MissingMemberHandling,
            ReferenceLoopHandling = settings.ReferenceLoopHandling,
            CheckAdditionalContent = settings.CheckAdditionalContent,
            StringEscapeHandling = settings.StringEscapeHandling,
        };
 
        return copiedSettings;
    }
 
    private static partial class Log
    {
        [LoggerMessage(1, LogLevel.Debug, "Buffering IAsyncEnumerable instance of type '{Type}'.", EventName = "BufferingAsyncEnumerable", SkipEnabledCheck = true)]
        private static partial void BufferingAsyncEnumerable(ILogger logger, string? type);
 
        public static void BufferingAsyncEnumerable(ILogger logger, object asyncEnumerable)
        {
            if (logger.IsEnabled(LogLevel.Debug))
            {
                BufferingAsyncEnumerable(logger, asyncEnumerable.GetType().FullName);
            }
        }
    }
}