File: src\Mvc\Mvc.Core\test\Formatters\JsonInputFormatterTestBase.cs
Web Access
Project: src\src\Mvc\Mvc.NewtonsoftJson\test\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj (Microsoft.AspNetCore.Mvc.NewtonsoftJson.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.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.WebUtilities;
using Newtonsoft.Json;
 
namespace Microsoft.AspNetCore.Mvc.Formatters;
 
public abstract class JsonInputFormatterTestBase : LoggedTest
{
    [Theory]
    [InlineData("application/json", true)]
    [InlineData("application/*", false)]
    [InlineData("*/*", false)]
    [InlineData("text/json", true)]
    [InlineData("text/*", false)]
    [InlineData("text/xml", false)]
    [InlineData("application/xml", false)]
    [InlineData("application/some.entity+json", true)]
    [InlineData("application/some.entity+json;v=2", true)]
    [InlineData("application/some.entity+xml", false)]
    [InlineData("application/some.entity+*", false)]
    [InlineData("text/some.entity+json", true)]
    [InlineData("", false)]
    [InlineData(null, false)]
    [InlineData("invalid", false)]
    public void CanRead_ReturnsTrueForAnySupportedContentType(string requestContentType, bool expectedCanRead)
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.UTF8.GetBytes("content");
        var httpContext = GetHttpContext(contentBytes, contentType: requestContentType);
 
        var formatterContext = CreateInputFormatterContext(typeof(string), httpContext);
 
        // Act
        var result = formatter.CanRead(formatterContext);
 
        // Assert
        Assert.Equal(expectedCanRead, result);
    }
 
    [Fact]
    public void DefaultMediaType_ReturnsApplicationJson()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        // Act
        var mediaType = formatter.SupportedMediaTypes[0];
 
        // Assert
        Assert.Equal("application/json", mediaType.ToString());
    }
 
    [Fact]
    public async Task JsonFormatterReadsIntValue()
    {
        // Arrange
        var content = "100";
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(int), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.False(result.HasError);
        var intValue = Assert.IsType<int>(result.Model);
        Assert.Equal(100, intValue);
    }
 
    [Fact]
    public async Task JsonFormatterReadsStringValue()
    {
        // Arrange
        var content = "\"abcd\"";
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(string), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.False(result.HasError);
        var stringValue = Assert.IsType<string>(result.Model);
        Assert.Equal("abcd", stringValue);
    }
 
    [Fact]
    public async Task JsonFormatterReadsNonUtf8Content()
    {
        // Arrange
        var content = "☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☮☯☰☱☲☳☴☵☶☷☸";
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.Unicode.GetBytes($"\"{content}\"");
        var httpContext = GetHttpContext(contentBytes, "application/json;charset=utf-16");
 
        var formatterContext = CreateInputFormatterContext(typeof(string), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.False(result.HasError);
        var stringValue = Assert.IsType<string>(result.Model);
        Assert.Equal(content, stringValue);
        Assert.True(httpContext.Request.Body.CanRead, "Verify that the request stream hasn't been disposed");
    }
 
    [Fact]
    public virtual async Task JsonFormatter_EscapedKeys()
    {
        var expectedKey = JsonFormatter_EscapedKeys_Expected;
 
        // Arrange
        var content = "[{\"It\\\"s a key\": 1234556}]"[{\"It\\\"s a key\": 1234556}]";
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(
            typeof(IEnumerable<IDictionary<string, short>>), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError);
        Assert.Collection(
            formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(expectedKey, kvp.Key);
            });
    }
 
    [Fact]
    public virtual async Task JsonFormatter_EscapedKeys_Bracket()
    {
        var expectedKey = JsonFormatter_EscapedKeys_Bracket_Expected;
 
        // Arrange
        var content = "[{\"It[s a key\":1234556}]"[{\"It[s a key\":1234556}]";
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(IEnumerable<IDictionary<string, short>>), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError);
        Assert.Collection(
            formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(expectedKey, kvp.Key);
            });
    }
 
    [Fact]
    public virtual async Task JsonFormatter_EscapedKeys_SingleQuote()
    {
        var expectedKey = JsonFormatter_EscapedKeys_SingleQuote_Expected;
 
        // Arrange
        var content = "[{\"It's a key\": 1234556}]"[{\"It's a key\": 1234556}]";
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(
            typeof(IEnumerable<IDictionary<string, short>>), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError);
        Assert.Collection(
            formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(expectedKey, kvp.Key);
            });
    }
 
    [Fact]
    public virtual async Task JsonFormatterReadsDateTimeValue()
    {
        // Arrange
        var expected = new DateTime(2012, 02, 01, 00, 45, 00);
        var content = $"\"{expected.ToString("O")}\"";
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(DateTime), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.False(result.HasError);
        var dateValue = Assert.IsType<DateTime>(result.Model);
        Assert.Equal(expected, dateValue);
    }
 
    [Fact]
    public async Task JsonFormatterReadsComplexTypes()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "{\"name\": \"Person Name\", \"age\": 30}"{\"name\": \"Person Name\", \"age\": 30}";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.False(result.HasError);
        var userModel = Assert.IsType<ComplexModel>(result.Model);
        Assert.Equal("Person Name", userModel.Name);
        Assert.Equal(30, userModel.Age);
    }
 
    [Fact]
    public async Task ReadAsync_ReadsValidArray()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "[0, 23, 300]";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(int[]), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.False(result.HasError);
        var integers = Assert.IsType<int[]>(result.Model);
        Assert.Equal(new int[] { 0, 23, 300 }, integers);
    }
 
    [Fact]
    public virtual Task ReadAsync_ReadsValidArray_AsListOfT() => ReadAsync_ReadsValidArray_AsList(typeof(List<int>));
 
    [Fact]
    public virtual Task ReadAsync_ReadsValidArray_AsIListOfT() => ReadAsync_ReadsValidArray_AsList(typeof(IList<int>));
 
    [Fact]
    public virtual Task ReadAsync_ReadsValidArray_AsCollectionOfT() => ReadAsync_ReadsValidArray_AsList(typeof(ICollection<int>));
 
    [Fact]
    public virtual Task ReadAsync_ReadsValidArray_AsEnumerableOfT() => ReadAsync_ReadsValidArray_AsList(typeof(IEnumerable<int>));
 
    protected async Task ReadAsync_ReadsValidArray_AsList(Type requestedType)
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "[0, 23, 300]";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(requestedType, httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.False(result.HasError);
        Assert.IsAssignableFrom(requestedType, result.Model);
        Assert.Equal(new int[] { 0, 23, 300 }, (IEnumerable<int>)result.Model);
    }
 
    [Fact]
    public virtual async Task ReadAsync_ArrayOfObjects_HasCorrectKey()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "[{\"Age\": 5}, {\"Age\": 3}, {\"Age\": \"Cheese\"} ]"[{\"Age\": 5}, {\"Age\": 3}, {\"Age\": \"Cheese\"} ]";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(List<ComplexModel>), httpContext);
 
        var expectedKey = ReadAsync_ArrayOfObjects_HasCorrectKey_Expected;
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError, "Model should have had an error!");
        Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(expectedKey, kvp.Key);
                Assert.Single(kvp.Value.Errors);
            });
    }
 
    [Fact]
    public virtual async Task ReadAsync_AddsModelValidationErrorsToModelState()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "{ \"Name\": \"Person Name\", \"Age\": \"not-an-age\" }"{ \"Name\": \"Person Name\", \"Age\": \"not-an-age\" }";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
        var expectedKey = ReadAsync_AddsModelValidationErrorsToModelState_Expected;
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError, "Model should have had an error!");
        Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(expectedKey, kvp.Key);
                Assert.Single(kvp.Value.Errors);
            });
    }
 
    [Fact]
    public virtual async Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "[0, 23, 33767]";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(short[]), httpContext);
 
        var expectedValue = ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected;
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError, "Model should have produced an error!");
        Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(expectedValue, kvp.Key);
            });
    }
 
    [Fact]
    public virtual async Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "[{ \"Name\": \"Name One\", \"Age\": 30}, { \"Name\": \"Name Two\", \"Small\": 300}]"[{ \"Name\": \"Name One\", \"Age\": 30}, { \"Name\": \"Name Two\", \"Small\": 300}]";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexModel[]), httpContext, modelName: "names");
        var expectedKey = ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected;
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError);
        Assert.Collection(
            formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(expectedKey, kvp.Key);
                Assert.Single(kvp.Value.Errors);
            });
    }
 
    [Fact]
    public virtual async Task ReadAsync_UsesTryAddModelValidationErrorsToModelState()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "{ \"Name\": \"Person Name\", \"Age\": \"not-an-age\"}"{ \"Name\": \"Person Name\", \"Age\": \"not-an-age\"}";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
        formatterContext.ModelState.MaxAllowedErrors = 3;
        formatterContext.ModelState.AddModelError("key1", "error1");
        formatterContext.ModelState.AddModelError("key2", "error2");
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError);
 
        Assert.False(formatterContext.ModelState.ContainsKey("age"));
        var error = Assert.Single(formatterContext.ModelState[""].Errors);
        Assert.IsType<TooManyModelErrorsException>(error.Exception);
    }
 
    [Theory]
    [InlineData("null", true, true)]
    [InlineData("null", false, false)]
    public async Task ReadAsync_WithInputThatDeserializesToNull_SetsModelOnlyIfAllowingEmptyInput(
        string content,
        bool treatEmptyInputAsDefaultValue,
        bool expectedIsModelSet)
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(
            typeof(string),
            httpContext,
            treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.False(result.HasError);
        Assert.Equal(expectedIsModelSet, result.IsModelSet);
        Assert.Null(result.Model);
    }
 
    [Fact]
    public async Task ReadAsync_ComplexPoco()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "{ \"Id\": 5, \"Person\": { \"Name\": \"name\", \"Numbers\": [3, 2, \"Hamburger\"]} }"{ \"Id\": 5, \"Person\": { \"Name\": \"name\", \"Numbers\": [3, 2, \"Hamburger\"]} }";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexPoco), httpContext);
 
        var expectedKey = ReadAsync_ComplexPoco_Expected;
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError, "Model should have had an error!");
        Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(expectedKey, kvp.Key);
                Assert.Single(kvp.Value.Errors);
            });
    }
 
    [Fact]
    public virtual async Task ReadAsync_NestedParseError()
    {
        // Arrange
        var formatter = GetInputFormatter();
        var content = @"{ ""b"": { ""c"": { ""d"": efg } } }";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(A), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError, "Model should have had an error!");
        Assert.Collection(
            formatterContext.ModelState.OrderBy(k => k.Key),
            kvp =>
            {
                Assert.Equal(ReadAsync_NestedParseError_Expected, kvp.Key);
            });
    }
 
    [Fact]
    public virtual async Task ReadAsync_RequiredAttribute()
    {
        // Arrange
        var formatter = GetInputFormatter();
        var content = "{ \"Id\": 5, \"Person\": {\"Numbers\": [3]} }"{ \"Id\": 5, \"Person\": {\"Numbers\": [3]} }";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexPoco), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        Assert.True(result.HasError, "Model should have had an error!");
        Assert.Single(formatterContext.ModelState["Person.Name"].Errors);
    }
 
    [Fact]
    public async Task ReadAsync_DoesNotDisposeBufferedReadStream()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "{\"name\": \"Test\"}"{\"name\": \"Test\"}";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
        var testBufferedReadStream = new VerifyDisposeFileBufferingReadStream(httpContext.Request.Body, 1024);
        httpContext.Request.Body = testBufferedReadStream;
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        var userModel = Assert.IsType<ComplexModel>(result.Model);
        Assert.Equal("Test", userModel.Name);
        Assert.False(testBufferedReadStream.Disposed);
    }
 
    [Fact]
    public async Task ReadAsync_WithEnableBufferingWorks()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "{\"name\": \"Test\"}"{\"name\": \"Test\"}";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
        httpContext.Request.EnableBuffering();
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        var userModel = Assert.IsType<ComplexModel>(result.Model);
        Assert.Equal("Test", userModel.Name);
        var requestBody = httpContext.Request.Body;
        requestBody.Position = 0;
        Assert.Equal(content, new StreamReader(requestBody).ReadToEnd());
    }
 
    [Fact]
    public async Task ReadAsync_WithEnableBufferingWorks_WithInputStreamAtOffset()
    {
        // Arrange
        var formatter = GetInputFormatter();
 
        var content = "abc{\"name\": \"Test\"}";
        var contentBytes = Encoding.UTF8.GetBytes(content);
        var httpContext = GetHttpContext(contentBytes);
        httpContext.Request.EnableBuffering();
        var requestBody = httpContext.Request.Body;
        requestBody.Position = 3;
 
        var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
 
        // Act
        var result = await formatter.ReadAsync(formatterContext);
 
        // Assert
        var userModel = Assert.IsType<ComplexModel>(result.Model);
        Assert.Equal("Test", userModel.Name);
        requestBody.Position = 0;
        Assert.Equal(content, new StreamReader(requestBody).ReadToEnd());
    }
 
    internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; }
 
    internal abstract string JsonFormatter_EscapedKeys_SingleQuote_Expected { get; }
 
    internal abstract string JsonFormatter_EscapedKeys_Expected { get; }
 
    internal abstract string ReadAsync_ArrayOfObjects_HasCorrectKey_Expected { get; }
 
    internal abstract string ReadAsync_AddsModelValidationErrorsToModelState_Expected { get; }
 
    internal abstract string ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected { get; }
 
    internal abstract string ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected { get; }
 
    internal abstract string ReadAsync_ComplexPoco_Expected { get; }
 
    internal abstract string ReadAsync_NestedParseError_Expected { get; }
 
    protected abstract TextInputFormatter GetInputFormatter(bool allowInputFormatterExceptionMessages = true);
 
    protected static HttpContext GetHttpContext(
        byte[] contentBytes,
        string contentType = "application/json")
    {
        return GetHttpContext(new MemoryStream(contentBytes), contentType);
    }
 
    protected static HttpContext GetHttpContext(
        Stream requestStream,
        string contentType = "application/json")
    {
        var httpContext = new DefaultHttpContext();
        httpContext.Request.Body = requestStream;
        httpContext.Request.ContentType = contentType;
        httpContext.Request.ContentLength = requestStream.Length;
 
        return httpContext;
    }
 
    protected static InputFormatterContext CreateInputFormatterContext(
        Type modelType,
        HttpContext httpContext,
        string modelName = null,
        bool treatEmptyInputAsDefaultValue = false)
    {
        var provider = new EmptyModelMetadataProvider();
        var metadata = provider.GetMetadataForType(modelType);
 
        return new InputFormatterContext(
            httpContext,
            modelName: modelName ?? string.Empty,
            modelState: new ModelStateDictionary(),
            metadata: metadata,
            readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader,
            treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
    }
 
    protected sealed class ComplexPoco
    {
        public int Id { get; set; }
        public Person Person { get; set; }
    }
 
    protected sealed class Person
    {
        [Required]
        [JsonProperty(Required = Required.Always)]
        public string Name { get; set; }
        public IEnumerable<int> Numbers { get; set; }
    }
 
    protected sealed class ComplexModel
    {
        public string Name { get; set; }
 
        public decimal Age { get; set; }
 
        public byte Small { get; set; }
    }
 
    class A
    {
        public B B { get; set; }
    }
 
    class B
    {
        public C C { get; set; }
    }
 
    class C
    {
        public string D { get; set; }
    }
 
    private class VerifyDisposeFileBufferingReadStream : FileBufferingReadStream
    {
        public bool Disposed { get; private set; }
        public VerifyDisposeFileBufferingReadStream(Stream inner, int memoryThreshold) : base(inner, memoryThreshold)
        {
        }
 
        protected override void Dispose(bool disposing)
        {
            Disposed = true;
            base.Dispose(disposing);
        }
 
        public override ValueTask DisposeAsync()
        {
            Disposed = true;
            return base.DisposeAsync();
        }
    }
}