SaveTempDataFilter handle write to body

This commit is contained in:
Ryan Brandenburg 2017-01-24 12:09:15 -08:00
parent 3e214e2399
commit 03cdd15e5c
4 changed files with 145 additions and 47 deletions

View File

@ -29,6 +29,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
/// <inheritdoc />
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (!context.HttpContext.Response.HasStarted)
{
context.HttpContext.Response.OnStarting((state) =>
{
var saveTempDataContext = (SaveTempDataContext)state;
// If temp data was already saved, skip trying to save again as the calls here would potentially fail
// because the session feature might not be available at this point.
// Example: An action returns NoContentResult and since NoContentResult does not write anything to
// the body of the response, this delegate would get executed way late in the pipeline at which point
// the session feature would have been removed.
object obj;
if (saveTempDataContext.HttpContext.Items.TryGetValue(TempDataSavedKey, out obj))
{
return TaskCache.CompletedTask;
}
SaveTempData(
result: null,
factory: saveTempDataContext.TempDataDictionaryFactory,
httpContext: saveTempDataContext.HttpContext);
return TaskCache.CompletedTask;
},
state: new SaveTempDataContext()
{
HttpContext = context.HttpContext,
TempDataDictionaryFactory = _factory
});
}
}
/// <inheritdoc />
@ -39,34 +69,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
/// <inheritdoc />
public void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.OnStarting((state) =>
{
var saveTempDataContext = (SaveTempDataContext)state;
// If temp data was already saved, skip trying to save again as the calls here would potentially fail
// because the session feature might not be available at this point.
// Example: An action returns NoContentResult and since NoContentResult does not write anything to
// the body of the response, this delegate would get executed way late in the pipeline at which point
// the session feature would have been removed.
object obj;
if (saveTempDataContext.HttpContext.Items.TryGetValue(TempDataSavedKey, out obj))
{
return TaskCache.CompletedTask;
}
SaveTempData(
saveTempDataContext.ActionResult,
saveTempDataContext.TempDataDictionaryFactory,
saveTempDataContext.HttpContext);
return TaskCache.CompletedTask;
},
state: new SaveTempDataContext()
{
HttpContext = context.HttpContext,
ActionResult = context.Result,
TempDataDictionaryFactory = _factory
});
}
/// <inheritdoc />
@ -78,7 +80,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
if (!context.HttpContext.Response.HasStarted)
{
SaveTempData(context.Result, _factory, context.HttpContext);
context.HttpContext.Items.Add(TempDataSavedKey, true);
// If SaveTempDataFilter got added twice this might already be in there.
if (!context.HttpContext.Items.ContainsKey(TempDataSavedKey))
{
context.HttpContext.Items.Add(TempDataSavedKey, true);
}
}
}
@ -94,7 +100,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
private class SaveTempDataContext
{
public HttpContext HttpContext { get; set; }
public IActionResult ActionResult { get; set; }
public ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; }
}
}

View File

@ -162,6 +162,20 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal($"Foo 10 3 10/10/2010 00:00:00 {testGuid.ToString()}", body);
}
[Fact]
public async Task ResponseWrite_DoesNotCrashSaveTempDataFilter()
{
// Arrange
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("Name", "Jordan"),
};
var content = new FormUrlEncodedContent(nameValueCollection);
// Act, checking it didn't throw
var response = await Client.GetAsync("/TempData/SetTempDataResponseWrite");
}
[Fact]
public async Task SetInActionResultExecution_AvailableForNextRequest()
{

View File

@ -2,11 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Moq;
using Xunit;
@ -28,23 +30,46 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
[Fact]
public void OnResultExecuting_RegistersOnStartingCallback()
public async Task OnResultExecuting_DoesntThrowIfResponseStarted()
{
// Arrange
var responseFeature = new TestResponseFeature(hasStarted: true);
var httpContext = GetHttpContext(responseFeature);
var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Loose);
tempDataFactory
.Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.Verifiable();
var filter = new SaveTempDataFilter(tempDataFactory.Object);
var context = GetResultExecutingContext(httpContext);
filter.OnResultExecuting(context);
// Act
// Checking it doesn't throw
await responseFeature.FireOnSendingHeadersAsync();
}
[Fact]
public void OnResourceExecuting_RegistersOnStartingCallback()
{
// Arrange
var responseFeature = new Mock<IHttpResponseFeature>(MockBehavior.Strict);
responseFeature
.Setup(rf => rf.OnStarting(It.IsAny<System.Func<object, Task>>(), It.IsAny<object>()))
.Setup(rf => rf.OnStarting(It.IsAny<Func<object, Task>>(), It.IsAny<object>()))
.Verifiable();
responseFeature
.SetupGet(rf => rf.HasStarted)
.Returns(false);
var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Strict);
tempDataFactory
.Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.Verifiable();
var filter = new SaveTempDataFilter(tempDataFactory.Object);
var httpContext = GetHttpContext(responseFeature.Object);
var context = GetResultExecutingContext(httpContext);
var context = GetResourceExecutingContext(httpContext);
// Act
filter.OnResultExecuting(context);
filter.OnResourceExecuting(context);
// Assert
responseFeature.Verify();
@ -52,7 +77,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
[Fact]
public async Task OnResultExecuting_DoesNotSaveTempData_WhenTempDataAlreadySaved()
public void OnResultExecuted_CanBeCalledTwice()
{
// Arrange
var responseFeature = new TestResponseFeature();
var httpContext = GetHttpContext(responseFeature);
var tempData = GetTempDataDictionary();
var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Strict);
tempDataFactory
.Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.Returns(tempData.Object)
.Verifiable();
var filter = new SaveTempDataFilter(tempDataFactory.Object);
var context = GetResultExecutedContext(httpContext);
// Act (No Assert)
filter.OnResultExecuted(context);
// Shouldn't have thrown
filter.OnResultExecuted(context);
}
[Fact]
public async Task OnResourceExecuting_DoesNotSaveTempData_WhenTempDataAlreadySaved()
{
// Arrange
var responseFeature = new TestResponseFeature();
@ -63,8 +109,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
.Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.Verifiable();
var filter = new SaveTempDataFilter(tempDataFactory.Object);
var context = GetResultExecutingContext(httpContext);
filter.OnResultExecuting(context); // registers callback
var context = GetResourceExecutingContext(httpContext);
filter.OnResourceExecuting(context); // registers callback
// Act
await responseFeature.FireOnSendingHeadersAsync();
@ -81,10 +127,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var tempDataDictionary = GetTempDataDictionary();
var filter = GetFilter(tempDataDictionary.Object);
var responseFeature = new TestResponseFeature();
var actionContext = GetActionContext(GetHttpContext(responseFeature));
var context = GetResultExecutingContext(actionContext, result);
filter.OnResultExecuting(context); // registers callback
var httpContext = GetHttpContext(responseFeature);
var resourceContext = GetResourceExecutingContext(httpContext);
var resultContext = GetResultExecutedContext(httpContext, result);
filter.OnResourceExecuting(resourceContext); // registers callback
filter.OnResultExecuted(resultContext);
// Act
await responseFeature.FireOnSendingHeadersAsync();
@ -93,15 +141,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
[Fact]
public async Task OnResultExecuting_KeepsTempData_ForIKeepTempDataResult()
public async Task OnResourceExecuting_KeepsTempData_ForIKeepTempDataResult()
{
// Arrange
var tempDataDictionary = GetTempDataDictionary();
var filter = GetFilter(tempDataDictionary.Object);
var responseFeature = new TestResponseFeature();
var actionContext = GetActionContext(GetHttpContext(responseFeature));
var context = GetResultExecutingContext(actionContext, new TestKeepTempDataActionResult());
filter.OnResultExecuting(context); // registers callback
var httpContext = GetHttpContext(responseFeature);
var resourceContext = GetResourceExecutingContext(httpContext);
var resultContext = GetResultExecutedContext(httpContext, new TestKeepTempDataActionResult());
filter.OnResourceExecuting(resourceContext); // registers callback
filter.OnResultExecuted(resultContext);
// Act
await responseFeature.FireOnSendingHeadersAsync();
@ -118,9 +168,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var tempDataDictionary = GetTempDataDictionary();
var filter = GetFilter(tempDataDictionary.Object);
var responseFeature = new TestResponseFeature();
var actionContext = GetActionContext(GetHttpContext(responseFeature));
var context = GetResultExecutingContext(actionContext, new TestActionResult());
filter.OnResultExecuting(context); // registers callback
var actionContext = GetHttpContext(responseFeature);
var context = GetResourceExecutingContext(actionContext);
filter.OnResourceExecuting(context); // registers callback
// Act
await responseFeature.FireOnSendingHeadersAsync();
@ -224,6 +274,21 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return tempDataDictionary;
}
private ResourceExecutingContext GetResourceExecutingContext(HttpContext httpContext)
{
if (httpContext == null)
{
httpContext = GetHttpContext();
}
var actionResult = new TestActionResult();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var filters = new IFilterMetadata[] { };
var valueProviderFactories = new IValueProviderFactory[] { };
return new ResourceExecutingContext(actionContext, filters, valueProviderFactories);
}
private ResultExecutedContext GetResultExecutedContext(HttpContext httpContext = null, IActionResult actionResult = null)
{
if (httpContext == null)
@ -331,6 +396,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
public override void OnStarting(Func<object, Task> callback, object state)
{
if (_hasStarted)
{
throw new ArgumentException();
}
var prior = _responseStartingAsync;
_responseStartingAsync = async () =>
{

View File

@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace BasicWebSite.Controllers
@ -59,6 +61,13 @@ namespace BasicWebSite.Controllers
return RedirectToAction("GetTempDataMultiple");
}
public async Task SetTempDataResponseWrite()
{
TempData["value1"] = "steve";
await Response.WriteAsync("Steve!");
}
public string GetTempDataMultiple()
{
var value1 = TempData["key1"].ToString();