File: HttpResponseJsonExtensionsTests.cs
Web Access
Project: src\src\Http\Http.Extensions\test\Microsoft.AspNetCore.Http.Extensions.Tests.csproj (Microsoft.AspNetCore.Http.Extensions.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.TestHost;
 
#nullable enable
 
namespace Microsoft.AspNetCore.Http.Extensions.Tests;
 
public class HttpResponseJsonExtensionsTests
{
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_SimpleValue_JsonResponse()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        await context.Response.WriteAsJsonAsync(1);
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        var data = body.ToArray();
        Assert.Collection(data, b => Assert.Equal((byte)'1', b));
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_NullValue_JsonResponse()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        await context.Response.WriteAsJsonAsync<Uri?>(value: null);
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
 
        var data = Encoding.UTF8.GetString(body.ToArray());
        Assert.Equal("null", data);
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_WithOptions_JsonResponse()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        var options = new JsonSerializerOptions();
        options.Converters.Add(new IntegerConverter());
        await context.Response.WriteAsJsonAsync(new int[] { 1, 2, 3 }, options);
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
 
        var data = Encoding.UTF8.GetString(body.ToArray());
        Assert.Equal("[false,true,false]", data);
    }
 
    private class IntegerConverter : JsonConverter<int>
    {
        public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            throw new NotImplementedException();
        }
 
        public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
        {
            writer.WriteBooleanValue(value % 2 == 0);
        }
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_CustomStatusCode_StatusCodeUnchanged()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        context.Response.StatusCode = StatusCodes.Status418ImATeapot;
        await context.Response.WriteAsJsonAsync(1);
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status418ImATeapot, context.Response.StatusCode);
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_WithContentType_JsonResponseWithCustomContentType()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        await context.Response.WriteAsJsonAsync(1, options: null, contentType: "application/custom-type");
 
        // Assert
        Assert.Equal("application/custom-type", context.Response.ContentType);
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_WithCancellationToken_CancellationRaised()
    {
        // Arrange
        var context = new DefaultHttpContext();
        context.Response.Body = new TestStream();
 
        var cts = new CancellationTokenSource();
 
        // Act
        var writeTask = context.Response.WriteAsJsonAsync(1, cts.Token);
        Assert.False(writeTask.IsCompleted);
 
        cts.Cancel();
 
        // Assert
        await Assert.ThrowsAsync<TaskCanceledException>(async () => await writeTask);
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_ObjectWithStrings_CamcelCaseAndNotEscaped()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
        var value = new TestObject
        {
            StringProperty = "激光這兩個字是甚麼意思"
        };
 
        // Act
        await context.Response.WriteAsJsonAsync(value);
 
        // Assert
        var data = Encoding.UTF8.GetString(body.ToArray());
        Assert.Equal(@"{""stringProperty"":""激光這兩個字是甚麼意思""}"{""stringProperty"":""激光這兩個字是甚麼意思""}", data);
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_SimpleValue_JsonResponse()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        await context.Response.WriteAsJsonAsync(1, typeof(int));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        var data = body.ToArray();
        Assert.Collection(data, b => Assert.Equal((byte)'1', b));
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_NullValue_JsonResponse()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        await context.Response.WriteAsJsonAsync(value: null, typeof(int?));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
 
        var data = Encoding.UTF8.GetString(body.ToArray());
        Assert.Equal("null", data);
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_NullType_ThrowsArgumentNullException()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentNullException>(async () => await context.Response.WriteAsJsonAsync(value: null, type: null!));
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_NullResponse_ThrowsArgumentNullException()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act & Assert
        await Assert.ThrowsAsync<ArgumentNullException>(async () => await HttpResponseJsonExtensions.WriteAsJsonAsync(response: null!, value: null, typeof(int?)));
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_ObjectWithStrings_CamcelCaseAndNotEscaped()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
        var value = new TestObject
        {
            StringProperty = "激光這兩個字是甚麼意思"
        };
 
        // Act
        await context.Response.WriteAsJsonAsync(value, typeof(TestObject));
 
        // Assert
        var data = Encoding.UTF8.GetString(body.ToArray());
        Assert.Equal(@"{""stringProperty"":""激光這兩個字是甚麼意思""}"{""stringProperty"":""激光這兩個字是甚麼意思""}", data);
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_CustomStatusCode_StatusCodeUnchanged()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        context.Response.StatusCode = StatusCodes.Status418ImATeapot;
        await context.Response.WriteAsJsonAsync(1, typeof(int));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status418ImATeapot, context.Response.StatusCode);
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        await context.Response.WriteAsJsonAsync(AsyncEnumerable());
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        Assert.Equal("[1,2]", Encoding.UTF8.GetString(body.ToArray()));
 
        async IAsyncEnumerable<int> AsyncEnumerable()
        {
            await Task.Yield();
            yield return 1;
            yield return 2;
        }
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_AsyncEnumerable()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        await context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable<int>));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        Assert.Equal("[1,2]", Encoding.UTF8.GetString(body.ToArray()));
 
        async IAsyncEnumerable<int> AsyncEnumerable()
        {
            await Task.Yield();
            yield return 1;
            yield return 2;
        }
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable_ClosedConnecton()
    {
        // Arrange
        var cts = new CancellationTokenSource();
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
        context.RequestAborted = cts.Token;
        var iterated = false;
 
        // Act
        await context.Response.WriteAsJsonAsync(AsyncEnumerable());
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        // System.Text.Json might write the '[' before cancellation is observed
        Assert.InRange(body.ToArray().Length, 0, 1);
        Assert.False(iterated);
 
        async IAsyncEnumerable<int> AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.Yield();
            cts.Cancel();
            for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++)
            {
                iterated = true;
                yield return i;
            }
        }
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_AsyncEnumerable_ClosedConnecton()
    {
        // Arrange
        var cts = new CancellationTokenSource();
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
        context.RequestAborted = cts.Token;
        var iterated = false;
 
        // Act
        await context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable<int>));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        // System.Text.Json might write the '[' before cancellation is observed
        Assert.InRange(body.ToArray().Length, 0, 1);
        Assert.False(iterated);
 
        async IAsyncEnumerable<int> AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.Yield();
            cts.Cancel();
            for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++)
            {
                iterated = true;
                yield return i;
            }
        }
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_AsyncEnumerable_UserPassedTokenThrows()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
        context.RequestAborted = new CancellationToken(canceled: true);
        var cts = new CancellationTokenSource();
        var iterated = false;
 
        // Act
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable<int>), cts.Token));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        // System.Text.Json might write the '[' before cancellation is observed
        Assert.InRange(body.ToArray().Length, 0, 1);
        Assert.False(iterated);
 
        async IAsyncEnumerable<int> AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.Yield();
            cts.Cancel();
            for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++)
            {
                iterated = true;
                yield return i;
            }
        }
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable_UserPassedTokenThrows()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
        context.RequestAborted = new CancellationToken(canceled: true);
        var cts = new CancellationTokenSource();
        var iterated = false;
 
        // Act
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => context.Response.WriteAsJsonAsync(AsyncEnumerable(), cts.Token));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
 
        // System.Text.Json might write the '[' before cancellation is observed
        Assert.InRange(body.ToArray().Length, 0, 1);
        Assert.False(iterated);
 
        async IAsyncEnumerable<int> AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.Yield();
            cts.Cancel();
            for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++)
            {
                iterated = true;
                yield return i;
            }
        }
    }
 
    [Fact]
    public async Task WriteAsJsonAsyncGeneric_WithJsonTypeInfo_JsonResponse()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        var options = new JsonSerializerOptions();
        options.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
 
        await context.Response.WriteAsJsonAsync(new int[] { 1, 2, 3 }, (JsonTypeInfo<int[]>)options.GetTypeInfo(typeof(int[])));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
 
        var data = Encoding.UTF8.GetString(body.ToArray());
        Assert.Equal("[1,2,3]", data);
    }
 
    [Fact]
    public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse()
    {
        // Arrange
        var body = new MemoryStream();
        var context = new DefaultHttpContext();
        context.Response.Body = body;
 
        // Act
        var options = new JsonSerializerOptions();
        options.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
 
        await context.Response.WriteAsJsonAsync(value : null, options.GetTypeInfo(typeof(Uri)));
 
        // Assert
        Assert.Equal(ContentTypeConstants.JsonContentTypeWithCharset, context.Response.ContentType);
 
        var data = Encoding.UTF8.GetString(body.ToArray());
        Assert.Equal("null", data);
    }
 
    // Regression test: https://github.com/dotnet/aspnetcore/issues/57895
    [Fact]
    public async Task AsyncEnumerableCanSetHeader()
    {
        var builder = WebApplication.CreateBuilder();
        builder.WebHost.UseTestServer();
 
        await using var app = builder.Build();
 
        app.MapGet("/", IAsyncEnumerable<int> (HttpContext httpContext) =>
        {
            return AsyncEnum();
 
            async IAsyncEnumerable<int> AsyncEnum()
            {
                await Task.Yield();
                httpContext.Response.Headers["Test"] = "t";
                yield return 1;
            }
        });
 
        await app.StartAsync();
 
        var client = app.GetTestClient();
 
        var result = await client.GetAsync("/");
        result.EnsureSuccessStatusCode();
        var headerValue = Assert.Single(result.Headers.GetValues("Test"));
        Assert.Equal("t", headerValue);
 
        await app.StopAsync();
    }
 
    // Regression test: https://github.com/dotnet/aspnetcore/issues/57895
    [Fact]
    public async Task EnumerableCanSetHeader()
    {
        var builder = WebApplication.CreateBuilder();
        builder.WebHost.UseTestServer();
 
        await using var app = builder.Build();
 
        app.MapGet("/", IEnumerable<int> (HttpContext httpContext) =>
        {
            return Enum();
 
            IEnumerable<int> Enum()
            {
                httpContext.Response.Headers["Test"] = "t";
                yield return 1;
            }
        });
 
        await app.StartAsync();
 
        var client = app.GetTestClient();
 
        var result = await client.GetAsync("/");
        result.EnsureSuccessStatusCode();
        var headerValue = Assert.Single(result.Headers.GetValues("Test"));
        Assert.Equal("t", headerValue);
 
        await app.StopAsync();
    }
 
    public class TestObject
    {
        public string? StringProperty { get; set; }
    }
 
    private class TestStream : Stream
    {
        public override bool CanRead { get; }
        public override bool CanSeek { get; }
        public override bool CanWrite { get; }
        public override long Length { get; }
        public override long Position { get; set; }
 
        public override void Flush() { }
 
        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }
 
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotImplementedException();
        }
 
        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }
 
        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }
 
        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
        {
            var tcs = new TaskCompletionSource<int>();
            cancellationToken.Register(s => ((TaskCompletionSource<int>)s!).SetCanceled(), tcs);
            return new ValueTask<int>(tcs.Task);
        }
 
        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
        {
            var tcs = new TaskCompletionSource();
            cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), tcs);
            return new ValueTask(tcs.Task);
        }
    }
}