[Fixes #1331] Register object for dispose in ObjectResult and

FileStreamResult
This commit is contained in:
Ajay Bhargav Baaskaran 2015-03-10 11:21:44 -07:00
parent 1c00cfe7aa
commit fc9e1caf43
7 changed files with 317 additions and 7 deletions

View File

@ -376,6 +376,12 @@ namespace Microsoft.AspNet.Mvc
[NonAction] [NonAction]
public virtual JsonResult Json(object data) public virtual JsonResult Json(object data)
{ {
var disposableValue = data as IDisposable;
if (disposableValue != null)
{
Response.OnResponseCompleted(_ => disposableValue.Dispose(), state: null);
}
return new JsonResult(data); return new JsonResult(data);
} }
@ -659,6 +665,11 @@ namespace Microsoft.AspNet.Mvc
[NonAction] [NonAction]
public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName) public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName)
{ {
if (fileStream != null)
{
Response.OnResponseCompleted(_ => fileStream.Dispose(), state: null);
}
return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName }; return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
} }
@ -717,6 +728,12 @@ namespace Microsoft.AspNet.Mvc
[NonAction] [NonAction]
public virtual HttpNotFoundObjectResult HttpNotFound(object value) public virtual HttpNotFoundObjectResult HttpNotFound(object value)
{ {
var disposableValue = value as IDisposable;
if (disposableValue != null)
{
Response.OnResponseCompleted(_ => disposableValue.Dispose(), state: null);
}
return new HttpNotFoundObjectResult(value); return new HttpNotFoundObjectResult(value);
} }
@ -737,6 +754,12 @@ namespace Microsoft.AspNet.Mvc
[NonAction] [NonAction]
public virtual BadRequestObjectResult HttpBadRequest(object error) public virtual BadRequestObjectResult HttpBadRequest(object error)
{ {
var disposableValue = error as IDisposable;
if (disposableValue != null)
{
Response.OnResponseCompleted(_ => disposableValue.Dispose(), state: null);
}
return new BadRequestObjectResult(error); return new BadRequestObjectResult(error);
} }
@ -759,6 +782,12 @@ namespace Microsoft.AspNet.Mvc
[NonAction] [NonAction]
public virtual CreatedResult Created([NotNull] string uri, object value) public virtual CreatedResult Created([NotNull] string uri, object value)
{ {
var disposableValue = value as IDisposable;
if (disposableValue != null)
{
Response.OnResponseCompleted(_ => disposableValue.Dispose(), state: null);
}
return new CreatedResult(uri, value); return new CreatedResult(uri, value);
} }
@ -780,7 +809,8 @@ namespace Microsoft.AspNet.Mvc
{ {
location = uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); location = uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
} }
return new CreatedResult(location, value);
return Created(location, value);
} }
/// <summary> /// <summary>
@ -822,6 +852,12 @@ namespace Microsoft.AspNet.Mvc
object routeValues, object routeValues,
object value) object value)
{ {
var disposableValue = value as IDisposable;
if (disposableValue != null)
{
Response.OnResponseCompleted(_ => disposableValue.Dispose(), state: null);
}
return new CreatedAtActionResult(actionName, controllerName, routeValues, value); return new CreatedAtActionResult(actionName, controllerName, routeValues, value);
} }
@ -859,6 +895,12 @@ namespace Microsoft.AspNet.Mvc
[NonAction] [NonAction]
public virtual CreatedAtRouteResult CreatedAtRoute(string routeName, object routeValues, object value) public virtual CreatedAtRouteResult CreatedAtRoute(string routeName, object routeValues, object value)
{ {
var disposableValue = value as IDisposable;
if (disposableValue != null)
{
Response.OnResponseCompleted(_ => disposableValue.Dispose(), state: null);
}
return new CreatedAtRouteResult(routeName, routeValues, value); return new CreatedAtRouteResult(routeName, routeValues, value);
} }

View File

@ -423,6 +423,33 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(uri.OriginalString, result.Location); Assert.Equal(uri.OriginalString, result.Location);
} }
[Fact]
public void Created_IDisposableObject_RegistersForDispose()
{
// Arrange
var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var uri = new Uri("/test/url", UriKind.Relative);
var controller = new TestableController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
var input = new DisposableObject();
// Act
var result = controller.Created(uri, input);
// Assert
Assert.IsType<CreatedResult>(result);
Assert.Equal(StatusCodes.Status201Created, result.StatusCode);
Assert.Equal(uri.OriginalString, result.Location);
Assert.Same(input, result.Value);
mockHttpContext.Verify(
x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()),
Times.Once());
}
[Fact] [Fact]
public void CreatedAtAction_WithParameterActionName_SetsResultActionName() public void CreatedAtAction_WithParameterActionName_SetsResultActionName()
{ {
@ -483,6 +510,32 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(expected, result.RouteValues); Assert.Equal(expected, result.RouteValues);
} }
[Fact]
public void CreatedAtAction_IDisposableObject_RegistersForDispose()
{
// Arrange
var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var controller = new TestableController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
var input = new DisposableObject();
// Act
var result = controller.CreatedAtAction("SampleAction", input);
// Assert
Assert.IsType<CreatedAtActionResult>(result);
Assert.Equal(StatusCodes.Status201Created, result.StatusCode);
Assert.Equal("SampleAction", result.ActionName);
Assert.Same(input, result.Value);
mockHttpContext.Verify(
x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()),
Times.Once());
}
[Fact] [Fact]
public void CreatedAtRoute_WithParameterRouteName_SetsResultSameRouteName() public void CreatedAtRoute_WithParameterRouteName_SetsResultSameRouteName()
{ {
@ -540,6 +593,32 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(expected, result.RouteValues); Assert.Equal(expected, result.RouteValues);
} }
[Fact]
public void CreatedAtRoute_IDisposableObject_RegistersForDispose()
{
// Arrange
var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var controller = new TestableController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
var input = new DisposableObject();
// Act
var result = controller.CreatedAtRoute("SampleRoute", input);
// Assert
Assert.IsType<CreatedAtRouteResult>(result);
Assert.Equal(StatusCodes.Status201Created, result.StatusCode);
Assert.Equal("SampleRoute", result.RouteName);
Assert.Same(input, result.Value);
mockHttpContext.Verify(
x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()),
Times.Once());
}
[Fact] [Fact]
public void File_WithContents() public void File_WithContents()
{ {
@ -612,7 +691,12 @@ namespace Microsoft.AspNet.Mvc.Test
public void File_WithStream() public void File_WithStream()
{ {
// Arrange // Arrange
var controller = new TestableController(); var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var controller = new TestableController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
var fileStream = Stream.Null; var fileStream = Stream.Null;
// Act // Act
@ -629,7 +713,13 @@ namespace Microsoft.AspNet.Mvc.Test
public void File_WithStreamAndFileDownloadName() public void File_WithStreamAndFileDownloadName()
{ {
// Arrange // Arrange
var controller = new TestableController(); var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var controller = new TestableController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
var fileStream = Stream.Null; var fileStream = Stream.Null;
// Act // Act
@ -640,6 +730,9 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Same(fileStream, result.FileStream); Assert.Same(fileStream, result.FileStream);
Assert.Equal("someContentType", result.ContentType); Assert.Equal("someContentType", result.ContentType);
Assert.Equal("someDownloadName", result.FileDownloadName); Assert.Equal("someDownloadName", result.FileDownloadName);
mockHttpContext.Verify(
x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()),
Times.Once());
} }
[Fact] [Fact]
@ -685,6 +778,31 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal("Test Content", result.Value); Assert.Equal("Test Content", result.Value);
} }
[Fact]
public void HttpNotFound_IDisposableObject_RegistersForDispose()
{
// Arrange
var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var controller = new TestableController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
var input = new DisposableObject();
// Act
var result = controller.HttpNotFound(input);
// Assert
Assert.IsType<HttpNotFoundObjectResult>(result);
Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode);
Assert.Same(input, result.Value);
mockHttpContext.Verify(
x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()),
Times.Once());
}
[Fact] [Fact]
public void BadRequest_SetsStatusCode() public void BadRequest_SetsStatusCode()
{ {
@ -715,6 +833,31 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(obj, result.Value); Assert.Equal(obj, result.Value);
} }
[Fact]
public void BadRequest_IDisposableObject_RegistersForDispose()
{
// Arrange
var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var controller = new TestableController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
var input = new DisposableObject();
// Act
var result = controller.HttpBadRequest(input);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
Assert.Same(input, result.Value);
mockHttpContext.Verify(
x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()),
Times.Once());
}
[Fact] [Fact]
public void BadRequest_SetsStatusCodeAndValue_ModelState() public void BadRequest_SetsStatusCodeAndValue_ModelState()
{ {
@ -888,6 +1031,30 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Same(data, actualJsonResult.Value); Assert.Same(data, actualJsonResult.Value);
} }
[Fact]
public void Controller_Json_IDisposableObject_RegistersForDispose()
{
// Arrange
var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var controller = new TestableController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
var input = new DisposableObject();
// Act
var result = controller.Json(input);
// Assert
Assert.IsType<JsonResult>(result);
Assert.Same(input, result.Value);
mockHttpContext.Verify(
x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()),
Times.Once());
}
public static IEnumerable<object[]> RedirectTestData public static IEnumerable<object[]> RedirectTestData
{ {
get get
@ -1402,7 +1569,7 @@ namespace Microsoft.AspNet.Mvc.Test
{ {
// Arrange // Arrange
var model = new TryValidateModelModel(); var model = new TryValidateModelModel();
var validationResult = new [] var validationResult = new[]
{ {
new ModelValidationResult(string.Empty, "Out of range!") new ModelValidationResult(string.Empty, "Out of range!")
}; };
@ -1569,5 +1736,13 @@ namespace Microsoft.AspNet.Mvc.Test
{ {
} }
private class DisposableObject : IDisposable
{
public void Dispose()
{
throw new NotImplementedException();
}
}
} }
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Core;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.WebUtilities; using Microsoft.AspNet.WebUtilities;
@ -144,7 +145,12 @@ namespace Microsoft.AspNet.Mvc
public void ControllerFileStream_InvokedInUnitTests(string content, string contentType, string fileName) public void ControllerFileStream_InvokedInUnitTests(string content, string contentType, string fileName)
{ {
// Arrange // Arrange
var controller = new TestabilityController(); var mockHttpContext = new Mock<DefaultHttpContext>();
mockHttpContext.Setup(x => x.Response.OnResponseCompleted(It.IsAny<Action<object>>(), It.IsAny<object>()));
var controller = new TestabilityController()
{
ActionContext = new ActionContext(mockHttpContext.Object, new RouteData(), new ActionDescriptor())
};
// Act // Act
var result = controller.FileStream_Action(content, contentType, fileName); var result = controller.FileStream_Action(content, contentType, fileName);

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@ -307,12 +308,10 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Arrange // Arrange
var server = TestHelper.CreateServer(_app, SiteName); var server = TestHelper.CreateServer(_app, SiteName);
var client = server.CreateClient(); var client = server.CreateClient();
var input = "{\"SampleInt\":10}";
var request = new HttpRequestMessage( var request = new HttpRequestMessage(
HttpMethod.Post, HttpMethod.Post,
"http://localhost/ActionResultsVerification/GetNotFoundObjectResultWithContent"); "http://localhost/ActionResultsVerification/GetNotFoundObjectResultWithContent");
request.Content = new StringContent(input, Encoding.UTF8, "application/json");
// Act // Act
var response = await client.SendAsync(request); var response = await client.SendAsync(request);
@ -321,5 +320,37 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("{\"SampleInt\":10,\"SampleString\":\"Foo\"}", await response.Content.ReadAsStringAsync()); Assert.Equal("{\"SampleInt\":10,\"SampleString\":\"Foo\"}", await response.Content.ReadAsStringAsync());
} }
[Fact]
public async Task HttpNotFoundObjectResult_WithDisposableObject()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName);
var client = server.CreateClient();
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("guid", Guid.NewGuid().ToString()),
};
// Act
var response1 = await client.PostAsync(
"/ActionResultsVerification/GetDisposeCalled",
new FormUrlEncodedContent(nameValueCollection));
await client.PostAsync(
"/ActionResultsVerification/GetNotFoundObjectResultWithDisposableObject",
new FormUrlEncodedContent(nameValueCollection));
var response2 = await client.PostAsync(
"/ActionResultsVerification/GetDisposeCalled",
new FormUrlEncodedContent(nameValueCollection));
// Assert
var isDisposed = Convert.ToBoolean(await response1.Content.ReadAsStringAsync());
Assert.False(isDisposed);
isDisposed = Convert.ToBoolean(await response2.Content.ReadAsStringAsync());
Assert.True(isDisposed);
}
} }
} }

View File

@ -10,6 +10,10 @@ namespace ActionResultsWebSite
{ {
public class ActionResultsVerificationController : Controller public class ActionResultsVerificationController : Controller
{ {
[FromServices]
public GuidLookupService Service { get; set; }
public IActionResult Index([FromBody]DummyClass test) public IActionResult Index([FromBody]DummyClass test)
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
@ -95,6 +99,22 @@ namespace ActionResultsWebSite
return HttpNotFound(CreateDummy()); return HttpNotFound(CreateDummy());
} }
public IActionResult GetNotFoundObjectResultWithDisposableObject(string guid)
{
return HttpNotFound(CreateDisposableType(guid));
}
public bool GetDisposeCalled(string guid)
{
bool value;
if (Service.IsDisposed.TryGetValue(guid, out value))
{
return value;
}
return false;
}
public DummyClass GetDummy(int id) public DummyClass GetDummy(int id)
{ {
return CreateDummy(); return CreateDummy();
@ -108,5 +128,28 @@ namespace ActionResultsWebSite
SampleString = "Foo" SampleString = "Foo"
}; };
} }
private DisposableType CreateDisposableType(string guid)
{
return new DisposableType(Service, guid);
}
private class DisposableType : IDisposable
{
private GuidLookupService _service;
private string _guid;
public DisposableType(GuidLookupService service, string guid)
{
_service = service;
_guid = guid;
_service.IsDisposed[_guid] = false;
}
public void Dispose()
{
_service.IsDisposed[_guid] = true;
}
}
} }
} }

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace ActionResultsWebSite
{
public class GuidLookupService
{
public Dictionary<string, bool> IsDisposed { get; } = new Dictionary<string, bool>();
}
}

View File

@ -16,6 +16,7 @@ namespace ActionResultsWebSite
app.UseServices(services => app.UseServices(services =>
{ {
services.AddMvc(); services.AddMvc();
services.AddInstance(new GuidLookupService());
services.Configure<MvcOptions>(options => services.Configure<MvcOptions>(options =>
{ {