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