From e51e0e1d52c06fd5f03363f98cf26c5c51bce728 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 14:52:04 -0700 Subject: [PATCH] Fix for issue #1279 - Add .Request property to ApiController This change adds a .Request property to the ApiController class that can be used to access an HttpRequestMessage wrapping the HttpContext. The HttpRequestMessage is stored in an http feature to make it accessible to model binders and other infrastructure. --- .../ApiController.cs | 22 +++ .../HttpRequestMessageFeature.cs | 70 +++++++++ ...HttpRequestMessageHttpContextExtensions.cs | 23 +++ .../IHttpRequestMessageFeature.cs | 12 ++ .../WebApiCompatShimBasicTest.cs | 23 +++ .../HttpRequestMessageFeatureTest.cs | 134 ++++++++++++++++++ .../HttpRequestMessageController.cs | 29 ++++ 7 files changed, 313 insertions(+) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageFeature.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageHttpContextExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/IHttpRequestMessageFeature.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageFeatureTest.cs create mode 100644 test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs index 45b8c0a1f1..3d9c2bfd0f 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -1,6 +1,7 @@ // 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; using System.Security.Principal; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; @@ -13,6 +14,8 @@ namespace System.Web.Http [UseWebApiOverloading] public abstract class ApiController : IDisposable { + private HttpRequestMessage _request; + /// Gets the action context. /// The setter is intended for unit testing purposes only. [Activate] @@ -40,6 +43,25 @@ namespace System.Web.Http } } + /// Gets or sets the HTTP request message. + /// The setter is intended for unit testing purposes only. + public HttpRequestMessage Request + { + get + { + if (_request == null && ActionContext != null) + { + _request = ActionContext.HttpContext.GetHttpRequestMessage(); + } + + return _request; + } + set + { + _request = value; + } + } + /// Gets a factory used to generate URLs to other APIs. /// The setter is intended for unit testing purposes only. [Activate] diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageFeature.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageFeature.cs new file mode 100644 index 0000000000..19f7d90591 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageFeature.cs @@ -0,0 +1,70 @@ +// 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.Diagnostics.Contracts; +using System.Net.Http; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class HttpRequestMessageFeature : IHttpRequestMessageFeature + { + private readonly HttpContext _httpContext; + private HttpRequestMessage _httpRequestMessage; + + public HttpRequestMessageFeature([NotNull] HttpContext httpContext) + { + _httpContext = httpContext; + } + + public HttpRequestMessage HttpRequestMessage + { + get + { + if (_httpRequestMessage == null) + { + _httpRequestMessage = CreateHttpRequestMessage(_httpContext); + } + + return _httpRequestMessage; + } + + set + { + _httpRequestMessage = value; + } + } + + private static HttpRequestMessage CreateHttpRequestMessage(HttpContext httpContext) + { + var httpRequest = httpContext.Request; + var uriString = + httpRequest.Scheme + "://" + + httpRequest.Host + + httpRequest.PathBase + + httpRequest.Path + + httpRequest.QueryString; + + var message = new HttpRequestMessage(new HttpMethod(httpRequest.Method), uriString); + + // This allows us to pass the message through APIs defined in legacy code and then + // operate on the HttpContext inside. + message.Properties[nameof(HttpContext)] = httpContext; + + message.Content = new StreamContent(httpRequest.Body); + + foreach (var header in httpRequest.Headers) + { + // Every header should be able to fit into one of the two header collections. + // Try message.Headers first since that accepts more of them. + if (!message.Headers.TryAddWithoutValidation(header.Key, header.Value)) + { + var added = message.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + Contract.Assert(added); + } + } + + return message; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageHttpContextExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageHttpContextExtensions.cs new file mode 100644 index 0000000000..4a96fd3859 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageHttpContextExtensions.cs @@ -0,0 +1,23 @@ +// 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; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public static class HttpRequestMessageHttpContextExtensions + { + public static HttpRequestMessage GetHttpRequestMessage(this HttpContext httpContext) + { + var feature = httpContext.GetFeature(); + if (feature == null) + { + feature = new HttpRequestMessageFeature(httpContext); + httpContext.SetFeature(feature); + } + + return feature.HttpRequestMessage; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/IHttpRequestMessageFeature.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/IHttpRequestMessageFeature.cs new file mode 100644 index 0000000000..9f6b19dc3b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/IHttpRequestMessageFeature.cs @@ -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.Net.Http; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public interface IHttpRequestMessageFeature + { + HttpRequestMessage HttpRequestMessage { get; set; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index c15ebb13f7..a53c0b695b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -4,6 +4,7 @@ #if ASPNET50 using System; using System.Net; +using System.Net.Http; using System.Net.Http.Formatting; using System.Threading.Tasks; using Microsoft.AspNet.Builder; @@ -78,6 +79,28 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expected, formatters); } + + [Fact] + public async Task ApiController_RequestProperty() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var expected = + "POST http://localhost/api/Blog/HttpRequestMessage/EchoProperty localhost " + + "13 Hello, world!"; + + // Act + var response = await client.PostAsync( + "http://localhost/api/Blog/HttpRequestMessage/EchoProperty", + new StringContent("Hello, world!")); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, content); + } } } #endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageFeatureTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageFeatureTest.cs new file mode 100644 index 0000000000..09912e7504 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageFeatureTest.cs @@ -0,0 +1,134 @@ +// 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.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.PipelineCore; +using Xunit; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class HttpRequestMessageFeatureTest + { + [Fact] + public void HttpRequestMessage_CombinesUri() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "GET"; + + context.Request.Scheme = "http"; + context.Request.Host = new HostString("contoso.com"); + context.Request.PathBase = new PathString("/app"); + context.Request.Path = new PathString("/api/Products"); + context.Request.QueryString = new QueryString("?orderId=3"); + + // Act + var request = feature.HttpRequestMessage; + + // Assert + Assert.Equal("http://contoso.com/app/api/Products?orderId=3", request.RequestUri.AbsoluteUri); + } + + [Fact] + public void HttpRequestMessage_CopiesRequestMethod() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "OPTIONS"; + + // Act + var request = feature.HttpRequestMessage; + + // Assert + Assert.Equal(new HttpMethod("OPTIONS"), request.Method); + } + + [Fact] + public void HttpRequestMessage_CopiesHeader() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "OPTIONS"; + + context.Request.Headers.Add("Host", new string[] { "contoso.com" }); + + // Act + var request = feature.HttpRequestMessage; + + // Assert + Assert.Equal("contoso.com", request.Headers.Host); + } + + [Fact] + public void HttpRequestMessage_CopiesContentHeader() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "OPTIONS"; + + context.Request.Headers.Add("Content-Type", new string[] { "text/plain" }); + + // Act + var request = feature.HttpRequestMessage; + + // Assert + Assert.Equal("text/plain", request.Content.Headers.ContentType.ToString()); + } + + [Fact] + public async Task HttpRequestMessage_WrapsBodyContent() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "OPTIONS"; + + var bytes = Encoding.UTF8.GetBytes("Hello, world!"); + context.Request.Body = new MemoryStream(bytes); + context.Request.Body.Seek(0, SeekOrigin.Begin); + + // Act + var request = feature.HttpRequestMessage; + + // Assert + var streamContent = Assert.IsType(request.Content); + var content = await request.Content.ReadAsStringAsync(); + Assert.Equal("Hello, world!", content); + } + + [Fact] + public void HttpRequestMessage_CachesMessage() + { + // Arrange + var context = new DefaultHttpContext(); + var feature = new HttpRequestMessageFeature(context); + + context.Request.Method = "GET"; + context.Request.Scheme = "http"; + context.Request.Host = new HostString("contoso.com"); + + // Act + var request1 = feature.HttpRequestMessage; + + context.Request.Path = new PathString("/api/Products"); + var request2 = feature.HttpRequestMessage; + + // Assert + Assert.Same(request1, request2); + Assert.Equal("/", request2.RequestUri.AbsolutePath); + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs new file mode 100644 index 0000000000..174b0d7fc6 --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs @@ -0,0 +1,29 @@ +// 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.Threading.Tasks; +using System.Web.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; + +namespace WebApiCompatShimWebSite +{ + public class HttpRequestMessageController : ApiController + { + public async Task EchoProperty() + { + var request = Request; + + var message = string.Format( + "{0} {1} {2} {3} {4}", + request.Method, + request.RequestUri.AbsoluteUri, + request.Headers.Host, + request.Content.Headers.ContentLength, + await request.Content.ReadAsStringAsync()); + + await Context.Response.WriteAsync(message); + return new EmptyResult(); + } + } +} \ No newline at end of file