File: ResponseFeature.cs
Web Access
Project: src\src\Hosting\TestHost\src\Microsoft.AspNetCore.TestHost.csproj (Microsoft.AspNetCore.TestHost)
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
 
namespace Microsoft.AspNetCore.TestHost;
 
internal sealed class ResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature
{
    private readonly HeaderDictionary _headers = new HeaderDictionary();
    private readonly Action<Exception> _abort;
 
    private Func<Task> _responseStartingAsync = () => Task.CompletedTask;
    private Func<Task> _responseCompletedAsync = () => Task.CompletedTask;
    private int _statusCode;
    private string? _reasonPhrase;
 
    public ResponseFeature(Action<Exception> abort)
    {
        Headers = _headers;
 
        // 200 is the default status code all the way down to the host, so we set it
        // here to be consistent with the rest of the hosts when writing tests.
        StatusCode = 200;
        _abort = abort;
    }
 
    public int StatusCode
    {
        get => _statusCode;
        set
        {
            if (HasStarted)
            {
                throw new InvalidOperationException("The status code cannot be set, the response has already started.");
            }
            if (value < 100)
            {
                throw new ArgumentOutOfRangeException(nameof(value), value, "The status code cannot be set to a value less than 100");
            }
 
            _statusCode = value;
        }
    }
 
    public string? ReasonPhrase
    {
        get => _reasonPhrase;
        set
        {
            if (HasStarted)
            {
                throw new InvalidOperationException("The reason phrase cannot be set, the response has already started.");
            }
 
            _reasonPhrase = value;
        }
    }
 
    public IHeaderDictionary Headers { get; set; }
 
    public Stream Body { get; set; } = default!;
 
    public Stream Stream => Body;
 
    internal PipeWriter BodyWriter { get; set; } = default!;
 
    public PipeWriter Writer => BodyWriter;
 
    public bool HasStarted { get; set; }
 
    public void OnStarting(Func<object, Task> callback, object state)
    {
        if (HasStarted)
        {
            throw new InvalidOperationException();
        }
 
        var prior = _responseStartingAsync;
        _responseStartingAsync = async () =>
        {
            await callback(state);
            await prior();
        };
    }
 
    public void OnCompleted(Func<object, Task> callback, object state)
    {
        var prior = _responseCompletedAsync;
        _responseCompletedAsync = async () =>
        {
            try
            {
                await callback(state);
            }
            finally
            {
                await prior();
            }
        };
    }
 
    public async Task FireOnSendingHeadersAsync()
    {
        if (!HasStarted)
        {
            try
            {
                await _responseStartingAsync();
            }
            finally
            {
                HasStarted = true;
                _headers.IsReadOnly = true;
            }
        }
    }
 
    public Task FireOnResponseCompletedAsync()
    {
        return _responseCompletedAsync();
    }
 
    public async Task StartAsync(CancellationToken token = default)
    {
        try
        {
            await FireOnSendingHeadersAsync();
        }
        catch (Exception ex)
        {
            _abort(ex);
            throw;
        }
    }
 
    public void DisableBuffering()
    {
    }
 
    public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
    {
        return SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellation);
    }
 
    public Task CompleteAsync()
    {
        return Writer.CompleteAsync().AsTask();
    }
}