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