ResponseBuffering middleware initial checkin.
Restrict buffer to reset. Add sample. Cleanup.
This commit is contained in:
parent
c88e7f5a44
commit
c91cc89ee3
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.23018.0
|
||||
VisualStudioVersion = 14.0.23107.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.HttpOverrides", "src\Microsoft.AspNet.HttpOverrides\Microsoft.AspNet.HttpOverrides.xproj", "{517308C3-B477-4B01-B461-CAB9C10B6928}"
|
||||
EndProject
|
||||
|
|
@ -16,6 +16,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
global.json = global.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Buffering", "src\Microsoft.AspNet.Buffering\Microsoft.AspNet.Buffering.xproj", "{2363D0DD-A3BF-437E-9B64-B33AE132D875}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Buffering.Tests", "test\Microsoft.AspNet.Buffering.Tests\Microsoft.AspNet.Buffering.Tests.xproj", "{F5F1D123-9C81-4A9E-8644-AA46B8E578FB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9587FE9F-5A17-42C4-8021-E87F59CECB98}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseBufferingSample", "samples\ResponseBufferingSample\ResponseBufferingSample.xproj", "{E5C55B80-7827-40EB-B661-32B0E0E431CA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -30,6 +38,18 @@ Global
|
|||
{D6341B92-3416-4F11-8DF4-CB274296175F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D6341B92-3416-4F11-8DF4-CB274296175F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D6341B92-3416-4F11-8DF4-CB274296175F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2363D0DD-A3BF-437E-9B64-B33AE132D875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2363D0DD-A3BF-437E-9B64-B33AE132D875}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2363D0DD-A3BF-437E-9B64-B33AE132D875}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2363D0DD-A3BF-437E-9B64-B33AE132D875}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F5F1D123-9C81-4A9E-8644-AA46B8E578FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F5F1D123-9C81-4A9E-8644-AA46B8E578FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F5F1D123-9C81-4A9E-8644-AA46B8E578FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F5F1D123-9C81-4A9E-8644-AA46B8E578FB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E5C55B80-7827-40EB-B661-32B0E0E431CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E5C55B80-7827-40EB-B661-32B0E0E431CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5C55B80-7827-40EB-B661-32B0E0E431CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E5C55B80-7827-40EB-B661-32B0E0E431CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -37,5 +57,8 @@ Global
|
|||
GlobalSection(NestedProjects) = preSolution
|
||||
{517308C3-B477-4B01-B461-CAB9C10B6928} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
|
||||
{D6341B92-3416-4F11-8DF4-CB274296175F} = {8437B0F3-3894-4828-A945-A9187F37631D}
|
||||
{2363D0DD-A3BF-437E-9B64-B33AE132D875} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
|
||||
{F5F1D123-9C81-4A9E-8644-AA46B8E578FB} = {8437B0F3-3894-4828-A945-A9187F37631D}
|
||||
{E5C55B80-7827-40EB-B661-32B0E0E431CA} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNET_ENV": "Development"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"commandName": "web",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "http://localhost:5000/"
|
||||
},
|
||||
"kestrel": {
|
||||
"commandName": "kestrel",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "http://localhost:5001/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>e5c55b80-7827-40eb-b661-32b0e0e431ca</ProjectGuid>
|
||||
<RootNamespace>ResponseBufferingSample</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<DevelopmentServerPort>46823</DevelopmentServerPort>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
||||
namespace ResponseBufferingSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async (context) =>
|
||||
{
|
||||
// Write some stuff
|
||||
context.Response.ContentType = "text/other";
|
||||
await context.Response.WriteAsync("Hello World!");
|
||||
|
||||
// ... more work ...
|
||||
|
||||
// Something went wrong and we want to replace the response
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.Headers.Clear();
|
||||
context.Response.Body.SetLength(0);
|
||||
|
||||
// Try again
|
||||
context.Response.ContentType = "text/plain";
|
||||
await context.Response.WriteAsync("Hi Bob!");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"webroot": "wwwroot",
|
||||
"version": "1.0.0-*",
|
||||
"dependencies": {
|
||||
"Kestrel": "1.0.0-*",
|
||||
"Microsoft.AspNet.Buffering": "1.0.0-*",
|
||||
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
|
||||
"Microsoft.AspNet.Server.WebListener": "1.0.0-*"
|
||||
},
|
||||
"commands": {
|
||||
"web": "Microsoft.AspNet.Server.WebListener --server.urls=http://localhost:5000",
|
||||
"kestrel": "Kestrel --server.urls=http://localhost:5001"
|
||||
},
|
||||
"frameworks": {
|
||||
"dnx451": { },
|
||||
"dnxcore50": { }
|
||||
},
|
||||
"publishExclude": [
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"**.xproj",
|
||||
"**.user",
|
||||
"**.vspscc"
|
||||
],
|
||||
"exclude": [
|
||||
"wwwroot",
|
||||
"node_modules",
|
||||
"bower_components"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Buffering
|
||||
{
|
||||
internal class BufferingWriteStream : Stream
|
||||
{
|
||||
private readonly Stream _innerStream;
|
||||
private readonly MemoryStream _buffer = new MemoryStream();
|
||||
private bool _isBuffering = true;
|
||||
|
||||
public BufferingWriteStream(Stream innerStream)
|
||||
{
|
||||
_innerStream = innerStream;
|
||||
}
|
||||
|
||||
public override bool CanRead
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override bool CanSeek
|
||||
{
|
||||
get { return _isBuffering; }
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
{
|
||||
get { return _innerStream.CanWrite; }
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
return _buffer.Length;
|
||||
}
|
||||
// May throw
|
||||
return _innerStream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear/Reset the buffer by setting Position, Seek, or SetLength to 0. Random access is not supported.
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
return _buffer.Position;
|
||||
}
|
||||
// May throw
|
||||
return _innerStream.Position;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
if (value != 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, nameof(Position) + " can only be set to 0.");
|
||||
}
|
||||
_buffer.Position = value;
|
||||
_buffer.SetLength(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// May throw
|
||||
_innerStream.Position = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear/Reset the buffer by setting Position, Seek, or SetLength to 0. Random access is not supported.
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
if (value != 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, nameof(Length) + " can only be set to 0.");
|
||||
}
|
||||
_buffer.Position = value;
|
||||
_buffer.SetLength(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// May throw
|
||||
_innerStream.SetLength(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear/Reset the buffer by setting Position, Seek, or SetLength to 0. Random access is not supported.
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
if (origin != SeekOrigin.Begin)
|
||||
{
|
||||
throw new ArgumentException(nameof(origin), nameof(Seek) + " can only be set to " + nameof(SeekOrigin.Begin) + ".");
|
||||
}
|
||||
if (offset != 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset), offset, nameof(Seek) + " can only be set to 0.");
|
||||
}
|
||||
_buffer.SetLength(offset);
|
||||
return _buffer.Seek(offset, origin);
|
||||
}
|
||||
// Try the inner stream instead, but this will usually fail.
|
||||
return _innerStream.Seek(offset, origin);
|
||||
}
|
||||
|
||||
internal void DisableBuffering()
|
||||
{
|
||||
_isBuffering = false;
|
||||
if (_buffer.Length > 0)
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
}
|
||||
|
||||
internal Task DisableBufferingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_isBuffering = false;
|
||||
if (_buffer.Length > 0)
|
||||
{
|
||||
return FlushAsync(cancellationToken);
|
||||
}
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
_buffer.Write(buffer, offset, count);
|
||||
}
|
||||
else
|
||||
{
|
||||
_innerStream.Write(buffer, offset, count);
|
||||
}
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
return _buffer.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
}
|
||||
#if !DNXCORE50
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
return _buffer.BeginWrite(buffer, offset, count, callback, state);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _innerStream.BeginWrite(buffer, offset, count, callback, state);
|
||||
}
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
if (_isBuffering)
|
||||
{
|
||||
_buffer.EndWrite(asyncResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_innerStream.EndWrite(asyncResult);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
public override void Flush()
|
||||
{
|
||||
_isBuffering = false;
|
||||
if (_buffer.Length > 0)
|
||||
{
|
||||
_buffer.Seek(0, SeekOrigin.Begin);
|
||||
_buffer.CopyTo(_innerStream);
|
||||
_buffer.Seek(0, SeekOrigin.Begin);
|
||||
_buffer.SetLength(0);
|
||||
}
|
||||
_innerStream.Flush();
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_isBuffering = false;
|
||||
if (_buffer.Length > 0)
|
||||
{
|
||||
_buffer.Seek(0, SeekOrigin.Begin);
|
||||
await _buffer.CopyToAsync(_innerStream, 1024 * 16, cancellationToken);
|
||||
_buffer.Seek(0, SeekOrigin.Begin);
|
||||
_buffer.SetLength(0);
|
||||
}
|
||||
await _innerStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException("This Stream only supports Write operations.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNet.Buffering
|
||||
{
|
||||
internal class HttpBufferingFeature : IHttpBufferingFeature
|
||||
{
|
||||
private readonly BufferingWriteStream _buffer;
|
||||
private readonly IHttpBufferingFeature _innerFeature;
|
||||
|
||||
internal HttpBufferingFeature(BufferingWriteStream buffer, IHttpBufferingFeature innerFeature)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_innerFeature = innerFeature;
|
||||
}
|
||||
|
||||
public void DisableRequestBuffering()
|
||||
{
|
||||
_innerFeature?.DisableRequestBuffering();
|
||||
}
|
||||
|
||||
public void DisableResponseBuffering()
|
||||
{
|
||||
_buffer.DisableBuffering();
|
||||
_innerFeature?.DisableResponseBuffering();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>2363d0dd-a3bf-437e-9b64-b33ae132d875</ProjectGuid>
|
||||
<RootNamespace>Microsoft.AspNet.Buffering</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNet.Buffering
|
||||
{
|
||||
public class ResponseBufferingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public ResponseBufferingMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
var originalResponseBody = httpContext.Response.Body;
|
||||
|
||||
// no-op if buffering is already available.
|
||||
if (originalResponseBody.CanSeek)
|
||||
{
|
||||
await _next(httpContext);
|
||||
return;
|
||||
}
|
||||
|
||||
var originalBufferingFeature = httpContext.GetFeature<IHttpBufferingFeature>();
|
||||
var originalSendFileFeature = httpContext.GetFeature<IHttpSendFileFeature>();
|
||||
try
|
||||
{
|
||||
// Shim the response stream
|
||||
var bufferStream = new BufferingWriteStream(originalResponseBody);
|
||||
httpContext.Response.Body = bufferStream;
|
||||
httpContext.SetFeature<IHttpBufferingFeature>(new HttpBufferingFeature(bufferStream, originalBufferingFeature));
|
||||
if (originalSendFileFeature != null)
|
||||
{
|
||||
httpContext.SetFeature<IHttpSendFileFeature>(new SendFileFeatureWrapper(originalSendFileFeature, bufferStream));
|
||||
}
|
||||
|
||||
await _next(httpContext);
|
||||
|
||||
// If we're still buffered, set the content-length header and flush the buffer.
|
||||
// Only if the content-length header is not already set, and some content was buffered.
|
||||
if (!httpContext.Response.HasStarted && bufferStream.CanSeek && bufferStream.Length > 0)
|
||||
{
|
||||
if (!httpContext.Response.ContentLength.HasValue)
|
||||
{
|
||||
httpContext.Response.ContentLength = bufferStream.Length;
|
||||
}
|
||||
await bufferStream.FlushAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// undo everything
|
||||
httpContext.SetFeature(originalBufferingFeature);
|
||||
httpContext.SetFeature(originalSendFileFeature);
|
||||
httpContext.Response.Body = originalResponseBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Buffering;
|
||||
|
||||
namespace Microsoft.AspNet.Builder
|
||||
{
|
||||
public static class ResponseBufferingMiddlewareExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables full buffering of response bodies. This can be disabled on a per request basis using IHttpBufferingFeature.
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseResponseBuffering(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<ResponseBufferingMiddleware>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNet.Buffering
|
||||
{
|
||||
internal class SendFileFeatureWrapper : IHttpSendFileFeature
|
||||
{
|
||||
private readonly IHttpSendFileFeature _originalSendFileFeature;
|
||||
private readonly BufferingWriteStream _bufferStream;
|
||||
|
||||
public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, BufferingWriteStream bufferStream)
|
||||
{
|
||||
_originalSendFileFeature = originalSendFileFeature;
|
||||
_bufferStream = bufferStream;
|
||||
}
|
||||
|
||||
// Flush and disable the buffer if anyone tries to call the SendFile feature.
|
||||
public async Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
|
||||
{
|
||||
await _bufferStream.DisableBufferingAsync(cancellation);
|
||||
await _originalSendFileFeature.SendFileAsync(path, offset, length, cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"description": "ASP.NET middleware for buffering response bodies.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/aspnet/basicmiddleware"
|
||||
},
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Http.Abstractions": "1.0.0-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"dnx451": { },
|
||||
"dnxcore50": {
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>f5f1d123-9c81-4a9e-8644-aa46b8e578fb</ProjectGuid>
|
||||
<RootNamespace>Microsoft.AspNet.Buffering.Tests</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Http.Features;
|
||||
using Microsoft.AspNet.TestHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Buffering.Tests
|
||||
{
|
||||
public class ResponseBufferingMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task BufferResponse_SetsContentLength()
|
||||
{
|
||||
var server = TestServer.Create(app =>
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async context =>
|
||||
{
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
await context.Response.WriteAsync("Hello World");
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
});
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync("");
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
||||
|
||||
// Set automatically by buffer
|
||||
IEnumerable<string> values;
|
||||
Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values));
|
||||
Assert.Equal("11", values.FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BufferResponseWithManualContentLength_NotReplaced()
|
||||
{
|
||||
var server = TestServer.Create(app =>
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.ContentLength = 12;
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
await context.Response.WriteAsync("Hello World");
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
});
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync("");
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
||||
|
||||
IEnumerable<string> values;
|
||||
Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values));
|
||||
Assert.Equal("12", values.FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Seek_AllowsResttingBuffer()
|
||||
{
|
||||
var server = TestServer.Create(app =>
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async context =>
|
||||
{
|
||||
var body = context.Response.Body;
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(body.CanSeek);
|
||||
Assert.Equal(0, body.Position);
|
||||
Assert.Equal(0, body.Length);
|
||||
|
||||
await context.Response.WriteAsync("Hello World");
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
Assert.Equal(11, body.Position);
|
||||
Assert.Equal(11, body.Length);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => body.Seek(1, SeekOrigin.Begin));
|
||||
Assert.Throws<ArgumentException>(() => body.Seek(0, SeekOrigin.Current));
|
||||
Assert.Throws<ArgumentException>(() => body.Seek(0, SeekOrigin.End));
|
||||
|
||||
Assert.Equal(0, body.Seek(0, SeekOrigin.Begin));
|
||||
Assert.Equal(0, body.Position);
|
||||
Assert.Equal(0, body.Length);
|
||||
|
||||
await context.Response.WriteAsync("12345");
|
||||
Assert.Equal(5, body.Position);
|
||||
Assert.Equal(5, body.Length);
|
||||
});
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync("");
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal("12345", await response.Content.ReadAsStringAsync());
|
||||
|
||||
// Set automatically by buffer
|
||||
IEnumerable<string> values;
|
||||
Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values));
|
||||
Assert.Equal("5", values.FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetPosition_AllowsResttingBuffer()
|
||||
{
|
||||
var server = TestServer.Create(app =>
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async context =>
|
||||
{
|
||||
var body = context.Response.Body;
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(body.CanSeek);
|
||||
Assert.Equal(0, body.Position);
|
||||
Assert.Equal(0, body.Length);
|
||||
|
||||
await context.Response.WriteAsync("Hello World");
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
Assert.Equal(11, body.Position);
|
||||
Assert.Equal(11, body.Length);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => body.Position = 1);
|
||||
|
||||
body.Position = 0;
|
||||
Assert.Equal(0, body.Position);
|
||||
Assert.Equal(0, body.Length);
|
||||
|
||||
await context.Response.WriteAsync("12345");
|
||||
Assert.Equal(5, body.Position);
|
||||
Assert.Equal(5, body.Length);
|
||||
});
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync("");
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal("12345", await response.Content.ReadAsStringAsync());
|
||||
|
||||
// Set automatically by buffer
|
||||
IEnumerable<string> values;
|
||||
Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values));
|
||||
Assert.Equal("5", values.FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetLength_AllowsResttingBuffer()
|
||||
{
|
||||
var server = TestServer.Create(app =>
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async context =>
|
||||
{
|
||||
var body = context.Response.Body;
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(body.CanSeek);
|
||||
Assert.Equal(0, body.Position);
|
||||
Assert.Equal(0, body.Length);
|
||||
|
||||
await context.Response.WriteAsync("Hello World");
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
Assert.Equal(11, body.Position);
|
||||
Assert.Equal(11, body.Length);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => body.SetLength(1));
|
||||
|
||||
body.SetLength(0);
|
||||
Assert.Equal(0, body.Position);
|
||||
Assert.Equal(0, body.Length);
|
||||
|
||||
await context.Response.WriteAsync("12345");
|
||||
Assert.Equal(5, body.Position);
|
||||
Assert.Equal(5, body.Length);
|
||||
});
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync("");
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal("12345", await response.Content.ReadAsStringAsync());
|
||||
|
||||
// Set automatically by buffer
|
||||
IEnumerable<string> values;
|
||||
Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values));
|
||||
Assert.Equal("5", values.FirstOrDefault());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableBufferingViaFeature()
|
||||
{
|
||||
var server = TestServer.Create(app =>
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async context =>
|
||||
{
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
|
||||
var bufferingFeature = context.GetFeature<IHttpBufferingFeature>();
|
||||
Assert.NotNull(bufferingFeature);
|
||||
bufferingFeature.DisableResponseBuffering();
|
||||
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.False(context.Response.Body.CanSeek);
|
||||
|
||||
await context.Response.WriteAsync("Hello World");
|
||||
|
||||
Assert.True(context.Response.HasStarted);
|
||||
Assert.False(context.Response.Body.CanSeek);
|
||||
});
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync("");
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
||||
IEnumerable<string> values;
|
||||
Assert.False(response.Content.Headers.TryGetValues("Content-Length", out values));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableBufferingViaFeatureAfterFirstWrite_Flushes()
|
||||
{
|
||||
var server = TestServer.Create(app =>
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async context =>
|
||||
{
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
|
||||
await context.Response.WriteAsync("Hello");
|
||||
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
|
||||
var bufferingFeature = context.GetFeature<IHttpBufferingFeature>();
|
||||
Assert.NotNull(bufferingFeature);
|
||||
bufferingFeature.DisableResponseBuffering();
|
||||
|
||||
Assert.True(context.Response.HasStarted);
|
||||
Assert.False(context.Response.Body.CanSeek);
|
||||
|
||||
await context.Response.WriteAsync(" World");
|
||||
|
||||
Assert.True(context.Response.HasStarted);
|
||||
Assert.False(context.Response.Body.CanSeek);
|
||||
});
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync("");
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
||||
IEnumerable<string> values;
|
||||
Assert.False(response.Content.Headers.TryGetValues("Content-Length", out values));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FlushDisablesBuffering()
|
||||
{
|
||||
var server = TestServer.Create(app =>
|
||||
{
|
||||
app.UseResponseBuffering();
|
||||
app.Run(async context =>
|
||||
{
|
||||
Assert.False(context.Response.HasStarted);
|
||||
Assert.True(context.Response.Body.CanSeek);
|
||||
|
||||
context.Response.Body.Flush();
|
||||
|
||||
Assert.True(context.Response.HasStarted);
|
||||
Assert.False(context.Response.Body.CanSeek);
|
||||
|
||||
await context.Response.WriteAsync("Hello World");
|
||||
|
||||
Assert.True(context.Response.HasStarted);
|
||||
Assert.False(context.Response.Body.CanSeek);
|
||||
});
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync("");
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
||||
IEnumerable<string> values;
|
||||
Assert.False(response.Content.Headers.TryGetValues("Content-Length", out values));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Buffering": "1.0.0-*",
|
||||
"Microsoft.AspNet.TestHost": "1.0.0-*",
|
||||
"xunit.runner.aspnet": "2.0.0-aspnet-*"
|
||||
},
|
||||
"commands": {
|
||||
"test": "xunit.runner.aspnet"
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"dnx451": { },
|
||||
"dnxcore50": {
|
||||
"dependencies": {
|
||||
"System.Collections": "4.0.10-*",
|
||||
"System.Linq": "4.0.0-*",
|
||||
"System.Threading": "4.0.10-*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue