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