diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpResponseException.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpResponseException.cs
new file mode 100644
index 0000000000..fa70323f8e
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpResponseException.cs
@@ -0,0 +1,36 @@
+// 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;
+using System.Net;
+using System.Net.Http;
+
+namespace Microsoft.AspNet.Mvc.WebApiCompatShim
+{
+ public class HttpResponseException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The status code of the response.
+ public HttpResponseException(HttpStatusCode statusCode)
+ : this(new HttpResponseMessage(statusCode))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The response message.
+ public HttpResponseException([NotNull] HttpResponseMessage response)
+ : base(Resources.HttpResponseExceptionMessage)
+ {
+ Response = response;
+ }
+
+ ///
+ /// Gets the to return to the client.
+ ///
+ public HttpResponseMessage Response { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpResponseExceptionActionFilter.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpResponseExceptionActionFilter.cs
new file mode 100644
index 0000000000..1ab0817042
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpResponseExceptionActionFilter.cs
@@ -0,0 +1,48 @@
+// 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.Net.Http;
+
+namespace Microsoft.AspNet.Mvc.WebApiCompatShim
+{
+ ///
+ /// An action filter which sets to an
+ /// if the exception type is .
+ /// This filter runs immediately after the action.
+ ///
+ public class HttpResponseExceptionActionFilter : IActionFilter, IOrderedFilter
+ {
+ // Return a high number by default so that it runs closest to the action.
+ public int Order { get; set; } = int.MaxValue - 10;
+
+ public void OnActionExecuting([NotNull] ActionExecutingContext context)
+ {
+ }
+
+ public void OnActionExecuted([NotNull] ActionExecutedContext context)
+ {
+ var httpResponseException = context.Exception as HttpResponseException;
+ if (httpResponseException != null)
+ {
+ var request = context.HttpContext.GetHttpRequestMessage();
+ var response = httpResponseException.Response;
+
+ if (response != null && response.RequestMessage == null)
+ {
+ response.RequestMessage = request;
+ }
+
+ var objectResult = new ObjectResult(response)
+ {
+ DeclaredType = typeof(HttpResponseMessage)
+ };
+
+ context.Result = objectResult;
+
+ // Its marked as handled as in webapi because an HttpResponseException
+ // was considered as a 'success' response.
+ context.ExceptionHandled = true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs
index 480fda8e65..c77f24fddc 100644
--- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs
@@ -138,6 +138,22 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
return string.Format(CultureInfo.CurrentCulture, GetString("MaxHttpCollectionKeyLimitReached"), p0, p1);
}
+ ///
+ /// Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the 'Response' property of this exception for details.
+ ///
+ internal static string HttpResponseExceptionMessage
+ {
+ get { return GetString("HttpResponseExceptionMessage"); }
+ }
+
+ ///
+ /// Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the 'Response' property of this exception for details.
+ ///
+ internal static string FormatHttpResponseExceptionMessage()
+ {
+ return GetString("HttpResponseExceptionMessage");
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx
index e62964b509..1c433adac4 100644
--- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx
@@ -141,4 +141,7 @@
The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class.
+
+ Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the 'Response' property of this exception for details.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs
index 8f5a061336..3aa6159b52 100644
--- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs
+++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs
@@ -25,6 +25,9 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention());
options.ApplicationModelConventions.Add(new WebApiRoutesGlobalModelConvention(area: DefaultAreaName));
+ // Add an action filter for handling the HttpResponseException.
+ options.Filters.Add(new HttpResponseExceptionActionFilter());
+
// Add a model binder to be able to bind HttpRequestMessage
options.ModelBinders.Insert(0, new HttpRequestMessageModelBinder());
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs
index 77b819b3b4..0b43a30624 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs
@@ -83,6 +83,78 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expected, formatters);
}
+ [Fact]
+ public async Task ActionThrowsHttpResponseException_WithStatusCode()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync(
+ "http://localhost/api/Blog/HttpResponseException/ThrowsHttpResponseExceptionWithHttpStatusCode");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal(string.Empty, content);
+ }
+
+ [Fact]
+ public async Task ActionThrowsHttpResponseException_WithResponse()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync(
+ "http://localhost/api/Blog/HttpResponseException"+
+ "/ThrowsHttpResponseExceptionWithHttpResponseMessage?message=send some message");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal("send some message", content);
+ }
+
+ [Fact]
+ public async Task ActionThrowsHttpResponseException_EnsureGlobalHttpresponseExceptionActionFilter_IsInvoked()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync(
+ "http://localhost/api/Blog/HttpResponseException/ThrowsHttpResponseExceptionEnsureGlobalFilterRunsLast");
+
+ // Assert
+ // Ensure we do not get a no content result.
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal(string.Empty, content);
+ }
+
+ [Fact]
+ public async Task ActionThrowsHttpResponseException_EnsureGlobalFilterConvention_IsApplied()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync(
+ "http://localhost/api/Blog/"+
+ "HttpResponseException/ThrowsHttpResponseExceptionInjectAFilterToHandleHttpResponseException");
+
+ // Assert
+ // Ensure we do get a no content result.
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal(string.Empty, content);
+ }
+
[Fact]
public async Task ApiController_CanValidateCustomObjectWithPrefix_Fails()
{
diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionActionFilterTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionActionFilterTest.cs
new file mode 100644
index 0000000000..b03db244ef
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionActionFilterTest.cs
@@ -0,0 +1,75 @@
+// 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;
+using System.Net;
+using System.Net.Http;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.WebApiCompatShim
+{
+ public class HttpResponseExceptionActionFilterTest
+ {
+ [Fact]
+ public void OnActionExecuting_IsNoOp()
+ {
+ // Arrange
+ var filter = new HttpResponseExceptionActionFilter();
+ var context = new ActionExecutingContext(new ActionContext(
+ new DefaultHttpContext(),
+ new RouteData(),
+ actionDescriptor: Mock.Of()),
+ filters: Mock.Of>(),
+ actionArguments: new Dictionary());
+
+ // Act
+ filter.OnActionExecuting(context);
+
+ // Assert
+ Assert.Null(context.Result);
+ }
+
+ [Fact]
+ public void OrderIsSetToMaxValue()
+ {
+ // Arrange
+ var filter = new HttpResponseExceptionActionFilter();
+ var expectedFilterOrder = int.MaxValue - 10;
+
+ // Act & Assert
+ Assert.Equal(expectedFilterOrder, filter.Order);
+ }
+
+ [Fact]
+ public void OnActionExecuted_HandlesExceptionAndReturnsObjectResult()
+ {
+ // Arrange
+ var filter = new HttpResponseExceptionActionFilter();
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = "GET";
+
+ var context = new ActionExecutedContext(
+ new ActionContext(
+ httpContext,
+ new RouteData(),
+ actionDescriptor: Mock.Of()),
+ filters: null);
+ context.Exception = new HttpResponseException(HttpStatusCode.BadRequest);
+
+ // Act
+ filter.OnActionExecuted(context);
+
+ // Assert
+ Assert.True(context.ExceptionHandled);
+ var result = Assert.IsType(context.Result);
+ Assert.Equal(typeof(HttpResponseMessage), result.DeclaredType);
+ var response = Assert.IsType(result.Value);
+ Assert.NotNull(response.RequestMessage);
+ Assert.Equal(context.HttpContext.GetHttpRequestMessage(), response.RequestMessage);
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionTest.cs
new file mode 100644
index 0000000000..0289f2c960
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpResponseExceptionTest.cs
@@ -0,0 +1,44 @@
+// 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.Net;
+using System.Net.Http;
+using Microsoft.AspNet.Testing;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.WebApiCompatShim
+{
+ public class HttpResponseExceptionTest
+ {
+ [Fact]
+ [ReplaceCulture]
+ public void Constructor_SetsResponseProperty()
+ {
+ // Arrange and Act
+ var response = new HttpResponseMessage();
+ var exception = new HttpResponseException(response);
+
+ // Assert
+ Assert.Same(response, exception.Response);
+ Assert.Equal("Processing of the HTTP request resulted in an exception."+
+ " Please see the HTTP response returned by the 'Response' "+
+ "property of this exception for details.",
+ exception.Message);
+ }
+
+ [Fact]
+ [ReplaceCulture]
+ public void Constructor_SetsResponsePropertyWithGivenStatusCode()
+ {
+ // Arrange and Act
+ var exception = new HttpResponseException(HttpStatusCode.BadGateway);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadGateway, exception.Response.StatusCode);
+ Assert.Equal("Processing of the HTTP request resulted in an exception."+
+ " Please see the HTTP response returned by the 'Response' "+
+ "property of this exception for details.",
+ exception.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json
index 1ca380fda6..e92790191d 100644
--- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json
+++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/project.json
@@ -5,6 +5,7 @@
"dependencies": {
"Microsoft.AspNet.Mvc": "6.0.0-*",
"Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-*",
+ "Microsoft.AspNet.Testing": "1.0.0-*",
"Moq": "4.2.1312.1622",
"Xunit.KRunner": "1.0.0-*"
},
diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpResponseExceptionController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpResponseExceptionController.cs
new file mode 100644
index 0000000000..946c8a068c
--- /dev/null
+++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpResponseExceptionController.cs
@@ -0,0 +1,62 @@
+// 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.Net;
+using System.Net.Http;
+using System.Web.Http;
+using Microsoft.AspNet.Mvc;
+using Microsoft.AspNet.Mvc.WebApiCompatShim;
+
+namespace WebApiCompatShimWebSite
+{
+ public class HttpResponseExceptionController : ApiController
+ {
+ [HttpGet]
+ public object ThrowsHttpResponseExceptionWithHttpStatusCode()
+ {
+ throw new HttpResponseException(HttpStatusCode.BadRequest);
+ }
+
+ [HttpGet]
+ public object ThrowsHttpResponseExceptionWithHttpResponseMessage(string message)
+ {
+ var httpResponse = new HttpResponseMessage();
+ httpResponse.Content = new StringContent(message);
+ throw new HttpResponseException(httpResponse);
+ }
+
+ [TestActionFilter]
+ [HttpGet]
+ public object ThrowsHttpResponseExceptionEnsureGlobalFilterRunsLast()
+ {
+ throw new HttpResponseException(HttpStatusCode.BadRequest);
+ }
+
+ // Runs before the HttpResponseExceptionActionFilter's OnActionExecuted.
+ [TestActionFilter(Order = int.MaxValue)]
+ [HttpGet]
+ public object ThrowsHttpResponseExceptionInjectAFilterToHandleHttpResponseException()
+ {
+ throw new HttpResponseException(HttpStatusCode.BadRequest);
+ }
+ }
+
+ public class TestActionFilterAttribute : ActionFilterAttribute
+ {
+ public override void OnActionExecuted(ActionExecutedContext context)
+ {
+ if (!context.ExceptionHandled)
+ {
+ var httpResponseException = context.Exception as HttpResponseException;
+ if (httpResponseException != null)
+ {
+ context.Result = new NoContentResult();
+ context.ExceptionHandled = true;
+
+ // Null it out so that next filter do not handle it.
+ context.Exception = null;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/WebApiCompatShimWebSite/Startup.cs b/test/WebSites/WebApiCompatShimWebSite/Startup.cs
index 180c028745..8be05a3231 100644
--- a/test/WebSites/WebApiCompatShimWebSite/Startup.cs
+++ b/test/WebSites/WebApiCompatShimWebSite/Startup.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
@@ -16,10 +17,9 @@ namespace WebApiCompatShimWebSite
app.UseServices(services =>
{
services.AddMvc(configuration);
-
services.AddWebApiConventions();
});
-
+
app.UseMvc(routes =>
{
// This route can't access any of our webapi controllers