From 3968df90e45deddaaf61e9831da3347a36714bf1 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 Oct 2014 15:01:02 -0700 Subject: [PATCH] Fix issue #1282 - Add Request.CreateResponse extension methods Adds the set of CreateResponse/CreateErrorResponse extension methods that return an HttpResponseMessage. For the overloads that perform content negotiation they will access the collection of MediaTypeFormatters through the shim options. Note that CreateResponse and friends use the OLD formatters. Also, HttpError and CreateErrorResponse assume ErrorDetail == false. Using the shim you will not get detailed error messages unless you construct the HttpError instance yourself. --- .../HttpError.cs | 265 ++++++++++++ .../HttpErrorKeys.cs | 56 +++ .../HttpRequestMessageExtensions.cs | 382 ++++++++++++++++++ .../Properties/Resources.Designer.cs | 80 ++++ .../Resources.resx | 15 + ...piCompatShimServiceCollectionExtensions.cs | 6 + .../WebApiCompatShimBasicTest.cs | 104 +++++ .../HttpErrorTest.cs | 285 +++++++++++++ .../HttpRequestMessageExtensionsTest.cs | 347 ++++++++++++++++ .../HttpRequestMessageController.cs | 43 +- .../WebApiCompatShimWebSite/Models/User.cs | 1 + 11 files changed, 1581 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpError.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpErrorKeys.cs create mode 100644 src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpError.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpError.cs new file mode 100644 index 0000000000..438facb22b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpError.cs @@ -0,0 +1,265 @@ +// 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.Linq; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; +using ShimResources = Microsoft.AspNet.Mvc.WebApiCompatShim.Resources; + +namespace System.Web.Http +{ + /// + /// Defines a serializable container for storing error information. This information is stored + /// as key/value pairs. The dictionary keys to look up standard error information are available + /// on the type. + /// + [XmlRoot("Error")] + public sealed class HttpError : Dictionary, IXmlSerializable + { + /// + /// Initializes a new instance of the class. + /// + public HttpError() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + /// + /// Initializes a new instance of the class containing error message . + /// + /// The error message to associate with this instance. + public HttpError([NotNull] string message) + : this() + { + Message = message; + } + + /// + /// Initializes a new instance of the class for . + /// + /// The exception to use for error information. + /// true to include the exception information in the error; false otherwise + public HttpError([NotNull] Exception exception, bool includeErrorDetail) + : this() + { + Message = ShimResources.HttpError_GenericError; + + if (includeErrorDetail) + { + Add(HttpErrorKeys.ExceptionMessageKey, exception.Message); + Add(HttpErrorKeys.ExceptionTypeKey, exception.GetType().FullName); + Add(HttpErrorKeys.StackTraceKey, exception.StackTrace); + if (exception.InnerException != null) + { + Add(HttpErrorKeys.InnerExceptionKey, new HttpError(exception.InnerException, includeErrorDetail)); + } + } + } + + /// + /// Initializes a new instance of the class for . + /// + /// The invalid model state to use for error information. + /// true to include exception messages in the error; false otherwise + public HttpError([NotNull] ModelStateDictionary modelState, bool includeErrorDetail) + : this() + { + if (modelState.IsValid) + { + throw new ArgumentException(ShimResources.HttpError_ValidModelState, nameof(modelState)); + } + + Message = ShimResources.HttpError_BadRequest; + + var modelStateError = new HttpError(); + foreach (KeyValuePair keyModelStatePair in modelState) + { + var key = keyModelStatePair.Key; + var errors = keyModelStatePair.Value.Errors; + if (errors != null && errors.Count > 0) + { + var errorMessages = errors.Select(error => + { + if (includeErrorDetail && error.Exception != null) + { + return error.Exception.Message; + } + else + { + return + string.IsNullOrEmpty(error.ErrorMessage) ? + ShimResources.HttpError_GenericError : + error.ErrorMessage; + } + }).ToArray(); + modelStateError.Add(key, errorMessages); + } + } + + Add(HttpErrorKeys.ModelStateKey, modelStateError); + } + + /// + /// The high-level, user-visible message explaining the cause of the error. Information carried in this field + /// should be considered public in that it will go over the wire regardless of the value of error detail policy. + /// As a result care should be taken not to disclose sensitive information about the server or the application. + /// + public string Message + { + get { return GetPropertyValue(HttpErrorKeys.MessageKey); } + set { this[HttpErrorKeys.MessageKey] = value; } + } + + /// + /// The containing information about the errors that occurred during model binding. + /// + /// + /// The inclusion of information carried in the is + /// controlled by the error detail policy. All other information in the + /// should be considered public in that it will go over the wire. As a result care should be taken not to + /// disclose sensitive information about the server or the application. + /// + public HttpError ModelState + { + get { return GetPropertyValue(HttpErrorKeys.ModelStateKey); } + } + + /// + /// A detailed description of the error intended for the developer to understand exactly what failed. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public string MessageDetail + { + get { return GetPropertyValue(HttpErrorKeys.MessageDetailKey); } + set { this[HttpErrorKeys.MessageDetailKey] = value; } + } + + /// + /// The message of the if available. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public string ExceptionMessage + { + get { return GetPropertyValue(HttpErrorKeys.ExceptionMessageKey); } + set { this[HttpErrorKeys.ExceptionMessageKey] = value; } + } + + /// + /// The type of the if available. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public string ExceptionType + { + get { return GetPropertyValue(HttpErrorKeys.ExceptionTypeKey); } + set { this[HttpErrorKeys.ExceptionTypeKey] = value; } + } + + /// + /// The stack trace information associated with this instance if available. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public string StackTrace + { + get { return GetPropertyValue(HttpErrorKeys.StackTraceKey); } + set { this[HttpErrorKeys.StackTraceKey] = value; } + } + + /// + /// The inner associated with this instance if available. + /// + /// + /// The inclusion of this field is controlled by the error detail policy. The + /// field is expected to contain information about the server or the application that should not + /// be disclosed broadly. + /// + public HttpError InnerException + { + get { return GetPropertyValue(HttpErrorKeys.InnerExceptionKey); } + } + + /// + /// Gets a particular property value from this error instance. + /// + /// The type of the property. + /// The name of the error property. + /// The value of the error property. + public TValue GetPropertyValue(string key) + { + object value; + if (TryGetValue(key, out value) && value is TValue) + { + return (TValue)value; + } + + return default(TValue); + } + + XmlSchema IXmlSerializable.GetSchema() + { + return null; + } + + void IXmlSerializable.ReadXml(XmlReader reader) + { + if (reader.IsEmptyElement) + { + reader.Read(); + return; + } + + reader.ReadStartElement(); + while (reader.NodeType != System.Xml.XmlNodeType.EndElement) + { + var key = XmlConvert.DecodeName(reader.LocalName); + var value = reader.ReadInnerXml(); + + Add(key, value); + reader.MoveToContent(); + } + reader.ReadEndElement(); + } + + void IXmlSerializable.WriteXml(XmlWriter writer) + { + foreach (var keyValuePair in this) + { + var key = keyValuePair.Key; + var value = keyValuePair.Value; + writer.WriteStartElement(XmlConvert.EncodeLocalName(key)); + if (value != null) + { + var innerError = value as HttpError; + if (innerError == null) + { + writer.WriteValue(value); + } + else + { + ((IXmlSerializable)innerError).WriteXml(writer); + } + } + writer.WriteEndElement(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpErrorKeys.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpErrorKeys.cs new file mode 100644 index 0000000000..7d079b94ba --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpErrorKeys.cs @@ -0,0 +1,56 @@ +// 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. + +namespace System.Web.Http +{ + /// + /// Provides keys to look up error information stored in the dictionary. + /// + public static class HttpErrorKeys + { + /// + /// Provides a key for the Message. + /// + public static readonly string MessageKey = "Message"; + + /// + /// Provides a key for the MessageDetail. + /// + public static readonly string MessageDetailKey = "MessageDetail"; + + /// + /// Provides a key for the ModelState. + /// + public static readonly string ModelStateKey = "ModelState"; + + /// + /// Provides a key for the ExceptionMessage. + /// + public static readonly string ExceptionMessageKey = "ExceptionMessage"; + + /// + /// Provides a key for the ExceptionType. + /// + public static readonly string ExceptionTypeKey = "ExceptionType"; + + /// + /// Provides a key for the StackTrace. + /// + public static readonly string StackTraceKey = "StackTrace"; + + /// + /// Provides a key for the InnerException. + /// + public static readonly string InnerExceptionKey = "InnerException"; + + /// + /// Provides a key for the MessageLanguage. + /// + public static readonly string MessageLanguageKey = "MessageLanguage"; + + /// + /// Provides a key for the ErrorCode. + /// + public static readonly string ErrorCodeKey = "ErrorCode"; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs new file mode 100644 index 0000000000..a2555f9c75 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageExtensions.cs @@ -0,0 +1,382 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Web.Http; +using System.Net.Http.Formatting; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.Framework.DependencyInjection; + +using ShimResources = Microsoft.AspNet.Mvc.WebApiCompatShim.Resources; +using Microsoft.Framework.OptionsModel; + +namespace System.Net.Http +{ + /// + /// Provides extension methods for the class. + /// + public static class HttpRequestMessageExtensions + { + /// + /// Helper method for creating an message with a "416 (Requested Range Not Satisfiable)" status code. + /// This response can be used in combination with the to indicate that the requested range or + /// ranges do not overlap with the current resource. The response contains a "Content-Range" header indicating the valid upper and lower + /// bounds for requested ranges. + /// + /// The request. + /// An instance, typically thrown by a + /// instance. + /// An 416 (Requested Range Not Satisfiable) error response with a Content-Range header indicating the valid range. + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + [NotNull] InvalidByteRangeException invalidByteRangeException) + { + var rangeNotSatisfiableResponse = request.CreateErrorResponse( + HttpStatusCode.RequestedRangeNotSatisfiable, + invalidByteRangeException); + rangeNotSatisfiableResponse.Content.Headers.ContentRange = invalidByteRangeException.ContentRange; + return rangeNotSatisfiableResponse; + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping an with message . + /// If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The error message. + /// An error response with error message and status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] string message) + { + return request.CreateErrorResponse(statusCode, new HttpError(message)); + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping an with error message + /// for exception . If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The error message. + /// The exception. + /// An error response for with error message + /// and status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] string message, + [NotNull] Exception exception) + { + var error = new HttpError(exception, includeErrorDetail: false) { Message = message }; + return request.CreateErrorResponse(statusCode, error); + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping an for exception . + /// If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The exception. + /// An error response for with status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] Exception exception) + { + return request.CreateErrorResponse(statusCode, new HttpError(exception, includeErrorDetail: false)); + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping an for model state . + /// If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The model state. + /// An error response for with status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] ModelStateDictionary modelState) + { + return request.CreateErrorResponse(statusCode, new HttpError(modelState, includeErrorDetail: false)); + } + + /// + /// Helper method that performs content negotiation and creates a representing an error + /// with an instance of wrapping as the content. If no formatter + /// is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The request. + /// The status code of the created response. + /// The error to wrap. + /// An error response wrapping with status code . + public static HttpResponseMessage CreateErrorResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] HttpError error) + { + return request.CreateResponse(statusCode, error); + } + + /// + /// Helper method that performs content negotiation and creates a with an instance + /// of as the content and as the status code + /// if a formatter can be found. If no formatter is found, this method returns a response with status 406 NotAcceptable. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The type of the value. + /// The request. + /// The value to wrap. Can be null. + /// A response wrapping with status code. + public static HttpResponseMessage CreateResponse([NotNull] this HttpRequestMessage request, T value) + { + return request.CreateResponse(HttpStatusCode.OK, value, formatters: null); + } + + /// + /// Helper method that performs content negotiation and creates a with an instance + /// of as the content if a formatter can be found. If no formatter is found, this + /// method returns a response with status 406 NotAcceptable. + /// configuration. + /// + /// + /// This method requires that has been associated with an instance of + /// . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value) + { + return request.CreateResponse(statusCode, value, formatters: null); + } + + /// + /// Helper method that performs content negotiation and creates a with an instance + /// of as the content if a formatter can be found. If no formatter is found, this + /// method returns a response with status 406 NotAcceptable. + /// + /// + /// This method will use the provided or it will get the + /// instance associated with . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The configuration to use. Can be null. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + T value, + IEnumerable formatters) + { + var context = GetHttpContext(request); + + if (formatters == null) + { + // Get the default formatters from options + var options = context.RequestServices.GetService>(); + formatters = options.Options.Formatters; + } + + var contentNegotiator = context.RequestServices.GetService(); + + var result = contentNegotiator.Negotiate(typeof(T), request, formatters); + if (result?.Formatter == null) + { + // Return a 406 when we're actually performing conneg and it fails to find a formatter. + return request.CreateResponse(HttpStatusCode.NotAcceptable); + } + else + { + return request.CreateResponse(statusCode, value, result.Formatter, result.MediaType); + } + } + + /// + /// Helper method that creates a with an instance containing the provided + /// . The given is used to find an instance of . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The media type used to look up an instance of . + /// A response wrapping with . + public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value, string mediaType) + { + return request.CreateResponse(statusCode, value, new MediaTypeHeaderValue(mediaType)); + } + + /// + /// Helper method that creates a with an instance containing the provided + /// . The given is used to find an instance of . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The media type used to look up an instance of . + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] T value, + [NotNull] MediaTypeHeaderValue mediaType) + { + var context = GetHttpContext(request); + + // Get the default formatters from options + var options = context.RequestServices.GetService>(); + var formatters = options.Options.Formatters; + + var formatter = formatters.FindWriter(typeof(T), mediaType); + if (formatter == null) + { + var message = ShimResources.FormatHttpRequestMessage_CouldNotFindMatchingFormatter( + mediaType.ToString(), + value.GetType()); + throw new InvalidOperationException(message); + } + + return request.CreateResponse(statusCode, value, formatter, mediaType); + } + + /// + /// Helper method that creates a with an instance containing the provided + /// and the given . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The formatter to use. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] T value, + [NotNull] MediaTypeFormatter formatter) + { + return request.CreateResponse(statusCode, value, formatter, (MediaTypeHeaderValue)null); + } + + /// + /// Helper method that creates a with an instance containing the provided + /// and the given . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The formatter to use. + /// The media type override to set on the response's content. Can be null. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + [NotNull] T value, + [NotNull] MediaTypeFormatter formatter, + string mediaType) + { + var mediaTypeHeader = mediaType != null ? new MediaTypeHeaderValue(mediaType) : null; + return request.CreateResponse(statusCode, value, formatter, mediaTypeHeader); + } + + /// + /// Helper method that creates a with an instance containing the provided + /// and the given . + /// + /// The type of the value. + /// The request. + /// The status code of the created response. + /// The value to wrap. Can be null. + /// The formatter to use. + /// The media type override to set on the response's content. Can be null. + /// A response wrapping with . + public static HttpResponseMessage CreateResponse( + [NotNull] this HttpRequestMessage request, + HttpStatusCode statusCode, + T value, + [NotNull] MediaTypeFormatter formatter, + MediaTypeHeaderValue mediaType) + { + var response = new HttpResponseMessage(statusCode) + { + RequestMessage = request, + }; + + response.Content = new ObjectContent(value, formatter, mediaType); + + return response; + } + + private static HttpContext GetHttpContext(HttpRequestMessage request) + { + var context = request.GetProperty(nameof(HttpContext)); + if (context == null) + { + var message = ShimResources.FormatHttpRequestMessage_MustHaveHttpContext( + nameof(HttpRequestMessage), + "HttpRequestMessageHttpContextExtensions.GetHttpRequestMessage"); + throw new InvalidOperationException(message); + } + + return context; + } + + private static T GetProperty(this HttpRequestMessage request, string key) + { + object value; + request.Properties.TryGetValue(key, out value); + + if (value is T) + { + return (T)value; + } + else + { + return default(T); + } + } + } +} \ 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 cb10994d90..480fda8e65 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs @@ -10,6 +10,86 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.Mvc.WebApiCompatShim.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// The request is invalid. + /// + internal static string HttpError_BadRequest + { + get { return GetString("HttpError_BadRequest"); } + } + + /// + /// The request is invalid. + /// + internal static string FormatHttpError_BadRequest() + { + return GetString("HttpError_BadRequest"); + } + + /// + /// An error has occurred. + /// + internal static string HttpError_GenericError + { + get { return GetString("HttpError_GenericError"); } + } + + /// + /// An error has occurred. + /// + internal static string FormatHttpError_GenericError() + { + return GetString("HttpError_GenericError"); + } + + /// + /// The model state is valid. + /// + internal static string HttpError_ValidModelState + { + get { return GetString("HttpError_ValidModelState"); } + } + + /// + /// The model state is valid. + /// + internal static string FormatHttpError_ValidModelState() + { + return GetString("HttpError_ValidModelState"); + } + + /// + /// Could not find a formatter matching the media type '{0}' that can write an instance of '{1}'. + /// + internal static string HttpRequestMessage_CouldNotFindMatchingFormatter + { + get { return GetString("HttpRequestMessage_CouldNotFindMatchingFormatter"); } + } + + /// + /// Could not find a formatter matching the media type '{0}' that can write an instance of '{1}'. + /// + internal static string FormatHttpRequestMessage_CouldNotFindMatchingFormatter(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HttpRequestMessage_CouldNotFindMatchingFormatter"), p0, p1); + } + + /// + /// The {0} instance is not properly initialized. Use {1} to create an {0} for the current request. + /// + internal static string HttpRequestMessage_MustHaveHttpContext + { + get { return GetString("HttpRequestMessage_MustHaveHttpContext"); } + } + + /// + /// The {0} instance is not properly initialized. Use {1} to create an {0} for the current request. + /// + internal static string FormatHttpRequestMessage_MustHaveHttpContext(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HttpRequestMessage_MustHaveHttpContext"), p0, p1); + } + /// /// The {0} only supports writing objects of type {1}. /// diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx index a86ebf7b9a..e62964b509 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx @@ -117,6 +117,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The request is invalid. + + + An error has occurred. + + + The model state is valid. + + + Could not find a formatter matching the media type '{0}' that can write an instance of '{1}'. + + + The {0} instance is not properly initialized. Use {1} to create an {0} for the current request. + The {0} only supports writing objects of type {1}. diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs index 8ff52fb81a..c821107215 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimServiceCollectionExtensions.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.Formatting; using Microsoft.AspNet.Mvc.WebApiCompatShim; namespace Microsoft.Framework.DependencyInjection @@ -10,6 +11,11 @@ namespace Microsoft.Framework.DependencyInjection public static IServiceCollection AddWebApiConventions(this IServiceCollection services) { services.AddOptionsAction(); + + // The constructors on DefaultContentNegotiator aren't DI friendly, so just + // new it up. + services.AddInstance(new DefaultContentNegotiator()); + return services; } } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index b37a95c9c1..c9e2ff955b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -7,7 +7,9 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; +using System.Net.Http.Headers; using System.Threading.Tasks; +using System.Web.Http; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; using Newtonsoft.Json; @@ -223,6 +225,108 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(new string[] { "Hello!" }, values); Assert.Equal(true, response.Headers.TransferEncodingChunked); } + + [Theory] + [InlineData("application/json", "application/json")] + [InlineData("text/xml", "text/xml")] + [InlineData("text/plain, text/xml; q=0.5", "text/xml")] + [InlineData("application/*", "application/json")] + public async Task ApiController_CreateResponse_Conneg(string accept, string mediaType) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/api/Blog/HttpRequestMessage/GetUser"); + + request.Headers.Accept.ParseAdd(accept); + + // Act + var response = await client.SendAsync(request); + var user = await response.Content.ReadAsAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Test User", user.Name); + Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType); + } + + [Theory] + [InlineData("application/json")] + [InlineData("text/xml")] + public async Task ApiController_CreateResponse_HardcodedMediaType(string mediaType) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/api/Blog/HttpRequestMessage/GetUser?mediaType=" + mediaType); + + // Act + var response = await client.SendAsync(request); + var user = await response.Content.ReadAsAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Test User", user.Name); + Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType); + } + + [Theory] + [InlineData("application/json", "application/json")] + [InlineData("text/xml", "text/xml")] + [InlineData("text/plain, text/xml; q=0.5", "text/xml")] + [InlineData("application/*", "application/json")] + public async Task ApiController_CreateResponse_Conneg_Error(string accept, string mediaType) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/api/Blog/HttpRequestMessage/Fail"); + + request.Headers.Accept.ParseAdd(accept); + + // Act + var response = await client.SendAsync(request); + var error = await response.Content.ReadAsAsync(); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("It failed.", error.Message); + Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType); + } + + + [Fact] + public async Task ApiController_CreateResponse_HardcodedFormatter() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/api/Blog/HttpRequestMessage/GetUserJson"); + + // Accept header will be ignored + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml")); + + // Act + var response = await client.SendAsync(request); + var user = await response.Content.ReadAsAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Test User", user.Name); + Assert.Equal("text/json", response.Content.Headers.ContentType.MediaType); + } } } #endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs new file mode 100644 index 0000000000..3a30640193 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpErrorTest.cs @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Net.Http.Formatting; +using Newtonsoft.Json.Linq; +using Xunit; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace System.Web.Http.Dispatcher +{ + public class HttpErrorTest + { + public static IEnumerable ErrorKeyValue + { + get + { + var httpError = new HttpError(); + yield return new object[] { httpError, (Func)(() => httpError.Message), "Message", "Message_Value" }; + yield return new object[] { httpError, (Func)(() => httpError.MessageDetail), "MessageDetail", "MessageDetail_Value" }; + yield return new object[] { httpError, (Func)(() => httpError.ExceptionMessage), "ExceptionMessage", "ExceptionMessage_Value" }; + yield return new object[] { httpError, (Func)(() => httpError.ExceptionType), "ExceptionType", "ExceptionType_Value" }; + yield return new object[] { httpError, (Func)(() => httpError.StackTrace), "StackTrace", "StackTrace_Value" }; + } + } + + public static IEnumerable HttpErrors + { + get + { + yield return new[] { new HttpError() }; + yield return new[] { new HttpError("error") }; + yield return new[] { new HttpError(new NotImplementedException(), true) }; + yield return new[] { new HttpError(new ModelStateDictionary() { { "key", new ModelState() { Errors = { new ModelError("error") } } } }, true) }; + } + } + + [Fact] + public void StringConstructor_AddsCorrectDictionaryItems() + { + HttpError error = new HttpError("something bad happened"); + + Assert.Contains(new KeyValuePair("Message", "something bad happened"), error); + } + + [Fact] + public void ExceptionConstructorWithDetail_AddsCorrectDictionaryItems() + { + HttpError error = new HttpError(new ArgumentException("error", new Exception()), true); + + Assert.Contains(new KeyValuePair("Message", "An error has occurred."), error); + Assert.Contains(new KeyValuePair("ExceptionMessage", "error"), error); + Assert.Contains(new KeyValuePair("ExceptionType", "System.ArgumentException"), error); + Assert.True(error.ContainsKey("StackTrace")); + Assert.True(error.ContainsKey("InnerException")); + Assert.IsType(error["InnerException"]); + } + + [Fact] + public void ModelStateConstructorWithDetail_AddsCorrectDictionaryItems() + { + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("[0].Name", "error1"); + modelState.AddModelError("[0].Name", "error2"); + modelState.AddModelError("[0].Address", "error"); + modelState.AddModelError("[2].Name", new Exception("OH NO")); + + HttpError error = new HttpError(modelState, true); + HttpError modelStateError = error["ModelState"] as HttpError; + + Assert.Contains(new KeyValuePair("Message", "The request is invalid."), error); + Assert.Contains("error1", modelStateError["[0].Name"] as IEnumerable); + Assert.Contains("error2", modelStateError["[0].Name"] as IEnumerable); + Assert.Contains("error", modelStateError["[0].Address"] as IEnumerable); + Assert.True(modelStateError.ContainsKey("[2].Name")); + Assert.Contains("OH NO", modelStateError["[2].Name"] as IEnumerable); + } + + [Fact] + public void ExceptionConstructorWithoutDetail_AddsCorrectDictionaryItems() + { + HttpError error = new HttpError(new ArgumentException("error", new Exception()), false); + + Assert.Contains(new KeyValuePair("Message", "An error has occurred."), error); + Assert.False(error.ContainsKey("ExceptionMessage")); + Assert.False(error.ContainsKey("ExceptionType")); + Assert.False(error.ContainsKey("StackTrace")); + Assert.False(error.ContainsKey("InnerException")); + } + + [Fact] + public void ModelStateConstructorWithoutDetail_AddsCorrectDictionaryItems() + { + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("[0].Name", "error1"); + modelState.AddModelError("[0].Name", "error2"); + modelState.AddModelError("[0].Address", "error"); + modelState.AddModelError("[2].Name", new Exception("OH NO")); + + HttpError error = new HttpError(modelState, false); + HttpError modelStateError = error["ModelState"] as HttpError; + + Assert.Contains(new KeyValuePair("Message", "The request is invalid."), error); + Assert.Contains("error1", modelStateError["[0].Name"] as IEnumerable); + Assert.Contains("error2", modelStateError["[0].Name"] as IEnumerable); + Assert.Contains("error", modelStateError["[0].Address"] as IEnumerable); + Assert.True(modelStateError.ContainsKey("[2].Name")); + Assert.DoesNotContain("OH NO", modelStateError["[2].Name"] as IEnumerable); + } + + [Fact] + public void HttpError_Roundtrips_WithJsonFormatter() + { + HttpError error = new HttpError("error") { { "ErrorCode", 42 }, { "Data", new[] { "a", "b", "c" } } }; + MediaTypeFormatter formatter = new JsonMediaTypeFormatter(); + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + HttpError roundtrippedError = formatter.ReadFromStreamAsync(typeof(HttpError), stream, content: null, formatterLogger: null).Result as HttpError; + + Assert.NotNull(roundtrippedError); + Assert.Equal("error", roundtrippedError.Message); + Assert.Equal(42L, roundtrippedError["ErrorCode"]); + JArray data = roundtrippedError["Data"] as JArray; + Assert.Equal(3, data.Count); + Assert.Contains("a", data); + Assert.Contains("b", data); + Assert.Contains("c", data); + } + + [Fact] + public void HttpError_Roundtrips_WithXmlFormatter() + { + HttpError error = new HttpError("error") { { "ErrorCode", 42 }, { "Data", new[] { "a", "b", "c" } } }; + MediaTypeFormatter formatter = new XmlMediaTypeFormatter(); + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + HttpError roundtrippedError = formatter.ReadFromStreamAsync(typeof(HttpError), stream, content: null, formatterLogger: null).Result as HttpError; + + Assert.NotNull(roundtrippedError); + Assert.Equal("error", roundtrippedError.Message); + Assert.Equal("42", roundtrippedError["ErrorCode"]); + Assert.Equal("a b c", roundtrippedError["Data"]); + } + + [Fact] + public void HttpErrorWithWhitespace_Roundtrips_WithXmlFormatter() + { + string message = " foo\n bar \n "; + HttpError error = new HttpError(message); + MediaTypeFormatter formatter = new XmlMediaTypeFormatter(); + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + HttpError roundtrippedError = formatter.ReadFromStreamAsync(typeof(HttpError), stream, content: null, formatterLogger: null).Result as HttpError; + + Assert.NotNull(roundtrippedError); + Assert.Equal(message, roundtrippedError.Message); + } + + [Fact] + public void HttpError_Roundtrips_WithXmlSerializer() + { + HttpError error = new HttpError("error") { { "ErrorCode", 42 }, { "Data", new[] { "a", "b", "c" } } }; + MediaTypeFormatter formatter = new XmlMediaTypeFormatter() { UseXmlSerializer = true }; + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + HttpError roundtrippedError = formatter.ReadFromStreamAsync(typeof(HttpError), stream, content: null, formatterLogger: null).Result as HttpError; + + Assert.NotNull(roundtrippedError); + Assert.Equal("error", roundtrippedError.Message); + Assert.Equal("42", roundtrippedError["ErrorCode"]); + Assert.Equal("a b c", roundtrippedError["Data"]); + } + + [Fact] + public void HttpErrorForInnerException_Serializes_WithXmlSerializer() + { + HttpError error = new HttpError(new ArgumentException("error", new Exception("innerError")), includeErrorDetail: true); + MediaTypeFormatter formatter = new XmlMediaTypeFormatter() { UseXmlSerializer = true }; + MemoryStream stream = new MemoryStream(); + + formatter.WriteToStreamAsync(typeof(HttpError), error, stream, content: null, transportContext: null).Wait(); + stream.Position = 0; + string serializedError = new StreamReader(stream).ReadToEnd(); + + Assert.NotNull(serializedError); + Assert.Equal( + "An error has occurred.errorSystem.ArgumentExceptionAn error has occurred.innerErrorSystem.Exception", + serializedError); + } + + [Fact] + public void GetPropertyValue_GetsValue_IfTypeMatches() + { + HttpError error = new HttpError(); + error["key"] = "x"; + + Assert.Equal("x", error.GetPropertyValue("key")); + Assert.Equal("x", error.GetPropertyValue("key")); + } + + [Fact] + public void GetPropertyValue_GetsDefault_IfTypeDoesNotMatch() + { + HttpError error = new HttpError(); + error["key"] = "x"; + + Assert.Null(error.GetPropertyValue("key")); + Assert.Equal(0, error.GetPropertyValue("key")); + } + + [Fact] + public void GetPropertyValue_GetsDefault_IfPropertyMissing() + { + HttpError error = new HttpError(); + + Assert.Null(error.GetPropertyValue("key")); + Assert.Equal(0, error.GetPropertyValue("key")); + } + + [Theory] + [MemberData("ErrorKeyValue")] + public void HttpErrorStringProperties_UseCorrectHttpErrorKey(HttpError httpError, Func productUnderTest, string key, string actualValue) + { + // Arrange + httpError[key] = actualValue; + + // Act + string expectedValue = productUnderTest.Invoke(); + + // Assert + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void HttpErrorProperty_InnerException_UsesCorrectHttpErrorKey() + { + // Arrange + HttpError error = new HttpError(new ArgumentException("error", new Exception()), true); + + // Act + HttpError innerException = error.InnerException; + + // Assert + Assert.Same(error["InnerException"], innerException); + } + + [Fact] + public void HttpErrorProperty_ModelState_UsesCorrectHttpErrorKey() + { + // Arrange + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("[0].Name", "error1"); + HttpError error = new HttpError(modelState, true); + + // Act + HttpError actualModelStateError = error.ModelState; + + // Assert + Assert.Same(error["ModelState"], actualModelStateError); + } + + [Theory] + [MemberData("HttpErrors")] + public void HttpErrors_UseCaseInsensitiveComparer(HttpError httpError) + { + // Arrange + var lowercaseKey = "abcd"; + var uppercaseKey = "ABCD"; + + httpError[lowercaseKey] = "error"; + + // Act & Assert + Assert.True(httpError.ContainsKey(lowercaseKey)); + Assert.True(httpError.ContainsKey(uppercaseKey)); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs new file mode 100644 index 0000000000..6996b3245a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageExtensionsTest.cs @@ -0,0 +1,347 @@ +// 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.Http.Formatting; +using System.Net.Http.Headers; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.AspNet.PipelineCore; +using Microsoft.Framework.OptionsModel; +using Moq; +using Xunit; + +namespace System.Net.Http +{ + public class HttpRequestMessageExtensionsTest + { + [Fact] + public void CreateResponse_DoingConneg_OnlyContent_RetrievesContentNegotiatorFromServices() + { + // Arrange + var context = new DefaultHttpContext(); + + var services = new Mock(); + services + .Setup(s => s.GetService(typeof(IContentNegotiator))) + .Returns(Mock.Of()) + .Verifiable(); + + var options = new WebApiCompatShimOptions(); + options.Formatters.AddRange(new MediaTypeFormatterCollection()); + + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + services + .Setup(s => s.GetService(typeof(IOptionsAccessor))) + .Returns(optionsAccessor.Object); + + context.RequestServices = services.Object; + + var request = CreateRequest(context); + + // Act + request.CreateResponse(CreateValue()); + + // Assert + services.Verify(); + } + + [Fact] + public void CreateResponse_DoingConneg_RetrievesContentNegotiatorFromServices() + { + // Arrange + var context = new DefaultHttpContext(); + + var services = new Mock(); + services + .Setup(s => s.GetService(typeof(IContentNegotiator))) + .Returns(Mock.Of()) + .Verifiable(); + + var options = new WebApiCompatShimOptions(); + options.Formatters.AddRange(new MediaTypeFormatterCollection()); + + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + services + .Setup(s => s.GetService(typeof(IOptionsAccessor))) + .Returns(optionsAccessor.Object); + + context.RequestServices = services.Object; + + var request = CreateRequest(context); + + // Act + request.CreateResponse(HttpStatusCode.OK, CreateValue()); + + // Assert + services.Verify(); + } + + [Fact] + public void CreateResponse_DoingConneg_PerformsContentNegotiationAndCreatesContentUsingResults() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new XmlMediaTypeFormatter(); + + var contentNegotiator = new Mock(); + contentNegotiator + .Setup(c => c.Negotiate(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(new ContentNegotiationResult(formatter, mediaType: null)); + + context.RequestServices = CreateServices(contentNegotiator.Object, formatter); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse(HttpStatusCode.NoContent, "42"); + + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Same(request, response.RequestMessage); + + var objectContent = Assert.IsType>(response.Content); + Assert.Equal("42", objectContent.Value); + Assert.Same(formatter, objectContent.Formatter); + } + + + [Fact] + public void CreateResponse_MatchingMediaType_WhenMediaTypeStringIsInvalidFormat_Throws() + { + HttpRequestMessage request = CreateRequest(new DefaultHttpContext()); + + var ex = Assert.Throws( + () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), "foo/bar; param=value")); + + Assert.Equal("The format of value 'foo/bar; param=value' is invalid.", ex.Message); + } + + [Fact] + public void CreateResponse_MatchingMediaType_WhenRequestDoesNotHaveHttpContextThrows() + { + HttpRequestMessage request = CreateRequest(null); + + // Arrange + + // Act + var ex = Assert.Throws( + () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), mediaType: "foo/bar")); + + Assert.Equal( + "The HttpRequestMessage instance is not properly initialized. " + + "Use HttpRequestMessageHttpContextExtensions.GetHttpRequestMessage to create an HttpRequestMessage " + + "for the current request.", + ex.Message); + } + + [Fact] + public void CreateResponse_MatchingMediaType_WhenMediaTypeDoesNotMatch_Throws() + { + // Arrange + var context = new DefaultHttpContext(); + context.RequestServices = CreateServices(new DefaultContentNegotiator()); + + var request = CreateRequest(context); + + // Act + var ex = Assert.Throws( + () => request.CreateResponse(HttpStatusCode.OK, CreateValue(), mediaType: "foo/bar")); + Assert.Equal( + "Could not find a formatter matching the media type 'foo/bar' that can write an instance of 'System.Object'.", + ex.Message); + } + + [Fact] + public void CreateResponse_MatchingMediaType_FindsMatchingFormatterAndCreatesResponse() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new Mock { CallBase = true }; + formatter.Setup(f => f.CanWriteType(typeof(object))).Returns(true).Verifiable(); + formatter.Object.SupportedMediaTypes.Add(new MediaTypeHeaderValue("foo/bar")); + + context.RequestServices = CreateServices(new DefaultContentNegotiator(), formatter.Object); + + var expectedValue = CreateValue(); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse(HttpStatusCode.Gone, expectedValue, mediaType: "foo/bar"); + + // Assert + Assert.Equal(HttpStatusCode.Gone, response.StatusCode); + var content = Assert.IsType>(response.Content); + Assert.Same(expectedValue, content.Value); + Assert.Same(formatter.Object, content.Formatter); + Assert.Equal("foo/bar", content.Headers.ContentType.MediaType); + formatter.Verify(); + } + + [Fact] + public void CreateResponse_AcceptingFormatter_CreatesResponseWithDefaultMediaType() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new Mock() { CallBase = true }; + formatter + .Setup(f => f.CanWriteType(typeof(object))) + .Returns(true) + .Verifiable(); + formatter + .Setup(f => f.SetDefaultContentHeaders(typeof(object), It.IsAny(), It.IsAny())) + .Callback(SetMediaType) + .Verifiable(); + + formatter.Object.SupportedMediaTypes.Add(new MediaTypeHeaderValue("foo/bar")); + + var expectedValue = CreateValue(); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse( + HttpStatusCode.MultipleChoices, + expectedValue, + formatter.Object, + mediaType: (string)null); + + // Assert + Assert.Equal(HttpStatusCode.MultipleChoices, response.StatusCode); + var content = Assert.IsType>(response.Content); + Assert.Same(expectedValue, content.Value); + Assert.Same(formatter.Object, content.Formatter); + Assert.Equal("foo/bar", content.Headers.ContentType.MediaType); + + formatter.Verify(); + } + + private static void SetMediaType(Type type, HttpContentHeaders headers, MediaTypeHeaderValue value) + { + headers.ContentType = new MediaTypeHeaderValue("foo/bar"); + } + + [Fact] + public void CreateResponse_AcceptingFormatter_WithOverridenMediaTypeString_CreatesResponse() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new Mock { CallBase = true }; + formatter.Setup(f => f.CanWriteType(typeof(object))).Returns(true).Verifiable(); + formatter.Object.SupportedMediaTypes.Add(new MediaTypeHeaderValue("foo/bar")); + + var expectedValue = CreateValue(); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse( + HttpStatusCode.MultipleChoices, + CreateValue(), + formatter.Object, + mediaType: "bin/baz"); + + // Assert + Assert.Equal("bin/baz", response.Content.Headers.ContentType.MediaType); + } + + [Fact] + public void CreateResponse_AcceptingFormatter_WithOverridenMediaTypeHeader_CreatesResponse() + { + // Arrange + var context = new DefaultHttpContext(); + + var formatter = new Mock { CallBase = true }; + formatter.Setup(f => f.CanWriteType(typeof(object))).Returns(true).Verifiable(); + formatter.Object.SupportedMediaTypes.Add(new MediaTypeHeaderValue("foo/bar")); + + var expectedValue = CreateValue(); + + var request = CreateRequest(context); + + // Act + var response = request.CreateResponse( + HttpStatusCode.MultipleChoices, + CreateValue(), + formatter.Object, + mediaType: new MediaTypeHeaderValue("bin/baz")); + + // Assert + Assert.Equal("bin/baz", response.Content.Headers.ContentType.MediaType); + } + + [Fact] + public void CreateErrorResponseRangeNotSatisfiable_SetsCorrectStatusCodeAndContentRangeHeader() + { + // Arrange + var context = new DefaultHttpContext(); + context.RequestServices = CreateServices(new DefaultContentNegotiator()); + + var request = CreateRequest(context); + + var expectedContentRange = new ContentRangeHeaderValue(length: 128); + var invalidByteRangeException = new InvalidByteRangeException(expectedContentRange); + + // Act + var response = request.CreateErrorResponse(invalidByteRangeException); + + // Assert + Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode); + Assert.Same(expectedContentRange, response.Content.Headers.ContentRange); + } + + private static HttpRequestMessage CreateRequest(HttpContext context) + { + var request = new HttpRequestMessage(); + request.Properties.Add(nameof(HttpContext), context); + return request; + } + + private static object CreateValue() + { + return new object(); + } + + private static IServiceProvider CreateServices( + IContentNegotiator contentNegotiator = null, + MediaTypeFormatter formatter = null) + { + var options = new WebApiCompatShimOptions(); + + if (formatter == null) + { + options.Formatters.AddRange(new MediaTypeFormatterCollection()); + } + else + { + options.Formatters.Add(formatter); + } + + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + var services = new Mock(MockBehavior.Strict); + services + .Setup(s => s.GetService(typeof(IOptionsAccessor))) + .Returns(optionsAccessor.Object); + + if (contentNegotiator != null) + { + services + .Setup(s => s.GetService(typeof(IContentNegotiator))) + .Returns(contentNegotiator); + } + + return services.Object; + } + } +} diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs index c4b654c5ee..a46eca249c 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/HttpRequestMessage/HttpRequestMessageController.cs @@ -8,6 +8,7 @@ using System.Web.Http; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; using System.Net; +using System.Net.Http.Formatting; namespace WebApiCompatShimWebSite { @@ -16,7 +17,7 @@ namespace WebApiCompatShimWebSite public async Task EchoProperty() { await Echo(Request); - return new EmptyResult(); + return new EmptyResult(); } public async Task EchoParameter(HttpRequestMessage request) @@ -33,8 +34,8 @@ namespace WebApiCompatShimWebSite public async Task EchoWithResponseMessage(HttpRequestMessage request) { var message = string.Format( - "{0} {1}", - request.Method.ToString(), + "{0} {1}", + request.Method.ToString(), await request.Content.ReadAsStringAsync()); var response = request.CreateResponse(HttpStatusCode.OK); @@ -57,6 +58,42 @@ namespace WebApiCompatShimWebSite return response; } + public HttpResponseMessage GetUser(string mediaType = null) + { + var user = new User() + { + Name = "Test User", + }; + + if (mediaType == null) + { + // This will perform content negotation + return Request.CreateResponse(HttpStatusCode.OK, user); + } + else + { + // This will use the provided media type + return Request.CreateResponse(HttpStatusCode.OK, user, mediaType); + } + } + + public HttpResponseMessage GetUserJson() + { + var user = new User() + { + Name = "Test User", + }; + + return Request.CreateResponse(HttpStatusCode.OK, user, new JsonMediaTypeFormatter(), "text/json"); + } + + [HttpGet] + public HttpResponseMessage Fail() + { + // This will perform content negotation + return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "It failed."); + } + private async Task Echo(HttpRequestMessage request) { var message = string.Format( diff --git a/test/WebSites/WebApiCompatShimWebSite/Models/User.cs b/test/WebSites/WebApiCompatShimWebSite/Models/User.cs index 50df5ddc90..7e6db5217d 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Models/User.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Models/User.cs @@ -7,5 +7,6 @@ namespace WebApiCompatShimWebSite { public class User { + public string Name { get; set; } } } \ No newline at end of file