From 5d105c387b2d0ae58ef12d47a664f574c6881d58 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Mon, 11 Jul 2022 10:24:03 -0700 Subject: [PATCH] Add HttpResponse.WriteFile/TransmitFile These APIs are essentially the same, except that they had different memory characteristics. TransmitFile did not buffer, while WriteFile did. On ASP.NET Core, these will just delegate to IHttpResponseBodyFeature.SendFileAsync. NOTE: This implementation will call Task.GetAwaiter().GetResult(). We could potentially expose an async API for this on the ASP.NET Core side as a stepping stone if we want. Part of #91 --- .../HttpResponse.cs | 42 ++++++++++++++++--- .../HttpResponseBase.cs | 2 +- .../HttpResponseWrapper.cs | 2 +- .../Ref.Standard.cs | 7 ++++ .../HttpResponseTests.cs | 40 ++++++++++++++++++ 5 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs index 917d29fe8..0e9e606c1 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs @@ -45,7 +45,9 @@ public int StatusCode set => _response.StatusCode = value; } - public string StatusDescription + public int SubStatusCode { get; set; } = -1; + + public string? StatusDescription { get => _response.HttpContext.Features.GetRequired().ReasonPhrase ?? ReasonPhrases.GetReasonPhrase(_response.StatusCode); set => _response.HttpContext.Features.GetRequired().ReasonPhrase = value; @@ -53,6 +55,18 @@ public string StatusDescription public NameValueCollection Headers => _headers ??= _response.Headers.ToNameValueCollection(); + public void ClearHeaders() + { + StatusCode = 200; + SubStatusCode = 0; + StatusDescription = null; + ContentType = "text/html"; + Charset = Encoding.UTF8.WebName; + + _response.Headers.Clear(); + _cookies?.Clear(); + } + public bool TrySkipIisCustomErrors { get => _response.HttpContext.Features.GetRequired().Enabled; @@ -61,10 +75,9 @@ public bool TrySkipIisCustomErrors public Stream OutputStream => _response.Body; - public HttpCookieCollection Cookies - { - get => _cookies ??= new(this); - } + public HttpCookieCollection Cookies => _cookies ??= new(this); + + public void AppendCookie(HttpCookie cookie) => Cookies.Add(cookie); public bool SuppressContent { @@ -177,6 +190,16 @@ public void AppendHeader(string name, string value) public void Write(object obj) => Output.Write(obj); + public void BinaryWrite(byte[] buffer) + { + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + OutputStream.Write(buffer, 0, buffer.Length); + } + public void Clear() { _response.Clear(); @@ -195,6 +218,15 @@ public void ClearContent() } } + public void WriteFile(string filename) + => TransmitFile(filename); + + public void TransmitFile(string filename) + => TransmitFile(filename, 0, -1); + + public void TransmitFile(string filename, long offset, long length) + => _response.SendFileAsync(filename, offset, length >= 0 ? length : null).GetAwaiter().GetResult(); + [return: NotNullIfNotNull("response")] public static implicit operator HttpResponse?(HttpResponseCore? response) => response?.GetAdapter(); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseBase.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseBase.cs index 7cfc1d692..c7f222f39 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseBase.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseBase.cs @@ -17,7 +17,7 @@ public virtual int StatusCode set => throw new NotImplementedException(); } - public virtual string StatusDescription + public virtual string? StatusDescription { get => throw new NotImplementedException(); set => throw new NotImplementedException(); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseWrapper.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseWrapper.cs index c593d9ecd..27b7e9bf6 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseWrapper.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseWrapper.cs @@ -63,7 +63,7 @@ public override int StatusCode set => _response.StatusCode = value; } - public override string StatusDescription + public override string? StatusDescription { get => _response.StatusDescription; set => _response.StatusDescription = value; diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs index c72b9aa5d..cdee7c821 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs @@ -221,17 +221,24 @@ internal HttpResponse() { } public System.IO.Stream OutputStream { get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} } public int StatusCode { get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} } public string StatusDescription { get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} } + public int SubStatusCode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} } public bool SuppressContent { get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} } public bool TrySkipIisCustomErrors { get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} } public void AddHeader(string name, string value) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} + public void AppendCookie(System.Web.HttpCookie cookie) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} public void AppendHeader(string name, string value) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} + public void BinaryWrite(byte[] buffer) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} public void Clear() { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} public void ClearContent() { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} + public void ClearHeaders() { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} public void End() { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} public void SetCookie(System.Web.HttpCookie cookie) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} + public void TransmitFile(string filename) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} + public void TransmitFile(string filename, long offset, long length) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} public void Write(char ch) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} public void Write(object obj) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} public void Write(string s) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} + public void WriteFile(string filename) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} } public partial class HttpResponseBase { diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpResponseTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpResponseTests.cs index c0c620ff4..8246be87f 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpResponseTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpResponseTests.cs @@ -378,4 +378,44 @@ public void Cookies() // Assert Assert.Same(cookies1, cookies2); } + + [Fact] + public void WriteFile() + => SendFileTest((response, file) => response.WriteFile(file)); + + [Fact] + public void TransmitFile() + => SendFileTest((response, file) => response.TransmitFile(file)); + + [Fact] + public void TransmitFileArgs() + => SendFileTest((response, file, offset, length) => response.TransmitFile(file, offset, length!.Value), 30, 3); + + private static void SendFileTest(Action action) + => SendFileTest((response, file, offset, length) => action(response, file), 0, default); + + private static void SendFileTest(Action action, long offset, long? length) + { + // Arrange + const string FileName = "somefile.txt"; + + var responsebody = new Mock(); + + var features = new Mock(); + features.Setup(f => f.Get()).Returns(responsebody.Object); + + var context = new Mock(); + context.Setup(c => c.Features).Returns(features.Object); + + var responseCore = new Mock(); + responseCore.Setup(r => r.HttpContext).Returns(context.Object); + + var response = new HttpResponse(responseCore.Object); + + // Act + action(response, FileName, offset, length); + + // Assert + responsebody.Verify(r => r.SendFileAsync(FileName, offset, length, default), Times.Once); + } }