diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index 20098d157a..305b556be5 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -50,6 +50,8 @@ namespace MvcSample.Web services.Configure(options => { options.Filters.Add(typeof(PassThroughAttribute), order: 17); + + options.AddXmlDataContractSerializerFormatter(); }); services.Configure(options => { @@ -90,6 +92,7 @@ namespace MvcSample.Web services.Configure(options => { options.Filters.Add(typeof(PassThroughAttribute), order: 17); + options.AddXmlDataContractSerializerFormatter(); }); }); } diff --git a/samples/TagHelperSample.Web/Startup.cs b/samples/TagHelperSample.Web/Startup.cs index 0b26939810..0742f29a6f 100644 --- a/samples/TagHelperSample.Web/Startup.cs +++ b/samples/TagHelperSample.Web/Startup.cs @@ -14,10 +14,15 @@ namespace TagHelperSample.Web app.UseServices(services => { services.AddMvc(); - + // Setup services with a test AssemblyProvider so that only the sample's assemblies are loaded. This // prevents loading controllers from other assemblies when the sample is used in functional tests. services.AddTransient>(); + + services.Configure(options => + { + options.AddXmlDataContractSerializerFormatter(); + }); }); app.UseMvc(); } diff --git a/samples/TagHelperSample.Web/TagHelperSample.Web.kproj b/samples/TagHelperSample.Web/TagHelperSample.Web.kproj index a3afa289a4..6fcc162ebf 100644 --- a/samples/TagHelperSample.Web/TagHelperSample.Web.kproj +++ b/samples/TagHelperSample.Web/TagHelperSample.Web.kproj @@ -15,4 +15,9 @@ 30540 + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs index 86736f97c9..1af7787ca6 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Mvc @@ -63,20 +64,43 @@ namespace Microsoft.AspNet.Mvc public virtual IOutputFormatter SelectFormatter(OutputFormatterContext formatterContext, IEnumerable formatters) { - var incomingAcceptHeader = formatterContext.ActionContext.HttpContext.Request.GetTypedHeaders().Accept; - var sortedAcceptHeaders = SortMediaTypeHeaderValues(incomingAcceptHeader) - .Where(header => header.Quality != HeaderQuality.NoMatch) - .ToArray(); + var incomingAcceptHeaderMediaTypes = formatterContext.ActionContext.HttpContext.Request.GetTypedHeaders().Accept ?? + new MediaTypeHeaderValue[] { }; + + // By default we want to ignore considering accept headers for content negotiation when + // they have a media type like */* in them. Browsers typically have these media types. + // In these cases we would want the first formatter in the list of output formatters to + // write the response. This default behavior can be changed through options, so checking here. + var options = formatterContext.ActionContext.HttpContext + .RequestServices + .GetRequiredService>() + .Options; + + var respectAcceptHeader = true; + if (options.RespectBrowserAcceptHeader == false + && incomingAcceptHeaderMediaTypes.Any(mediaType => mediaType.MatchesAllTypes)) + { + respectAcceptHeader = false; + } + + IEnumerable sortedAcceptHeaderMediaTypes = null; + if (respectAcceptHeader) + { + sortedAcceptHeaderMediaTypes = SortMediaTypeHeaderValues(incomingAcceptHeaderMediaTypes) + .Where(header => header.Quality != HeaderQuality.NoMatch); + } IOutputFormatter selectedFormatter = null; - if (ContentTypes == null || ContentTypes.Count == 0) { - // Select based on sorted accept headers. - selectedFormatter = SelectFormatterUsingSortedAcceptHeaders( - formatterContext, - formatters, - sortedAcceptHeaders); + if (respectAcceptHeader) + { + // Select based on sorted accept headers. + selectedFormatter = SelectFormatterUsingSortedAcceptHeaders( + formatterContext, + formatters, + sortedAcceptHeaderMediaTypes); + } if (selectedFormatter == null) { @@ -124,16 +148,15 @@ namespace Microsoft.AspNet.Mvc } else { - // Filter and remove accept headers which cannot support any of the user specified content types. - var filteredAndSortedAcceptHeaders = sortedAcceptHeaders - .Where(acceptHeader => - ContentTypes - .Any(contentType => - contentType.IsSubsetOf(acceptHeader))) - .ToArray(); - - if (filteredAndSortedAcceptHeaders.Length > 0) + if (respectAcceptHeader) { + // Filter and remove accept headers which cannot support any of the user specified content types. + var filteredAndSortedAcceptHeaders = sortedAcceptHeaderMediaTypes + .Where(acceptHeader => + ContentTypes + .Any(contentType => + contentType.IsSubsetOf(acceptHeader))); + selectedFormatter = SelectFormatterUsingSortedAcceptHeaders( formatterContext, formatters, @@ -194,20 +217,14 @@ namespace Microsoft.AspNet.Mvc return selectedFormatter; } - private static MediaTypeHeaderValue[] SortMediaTypeHeaderValues - (IEnumerable headerValues) + private static IEnumerable SortMediaTypeHeaderValues( + IEnumerable headerValues) { - if (headerValues == null) - { - return new MediaTypeHeaderValue[] { }; - } - // Use OrderBy() instead of Array.Sort() as it performs fewer comparisons. In this case the comparisons // are quite expensive so OrderBy() performs better. return headerValues.OrderByDescending(headerValue => headerValue, - MediaTypeHeaderValueComparer.QualityComparer) - .ToArray(); + MediaTypeHeaderValueComparer.QualityComparer); } private IEnumerable GetDefaultFormatters(ActionContext context) diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs index ab5e271cd8..cd5b652f87 100644 --- a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs @@ -125,5 +125,11 @@ namespace Microsoft.AspNet.Mvc /// the when discovering actions. /// public List ApplicationModelConventions { get; private set; } + + /// + /// Gets or sets the flag which causes content negotiation to ignore Accept header + /// when it contains the media type */*. by default. + /// + public bool RespectBrowserAcceptHeader { get; set; } = false; } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptionsExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptionsExtensions.cs new file mode 100644 index 0000000000..fdeb84115c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptionsExtensions.cs @@ -0,0 +1,22 @@ +// 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 Microsoft.AspNet.Mvc +{ + public static class MvcOptionsExtensions + { + /// + /// Adds and + /// to the input and output formatter + /// collections respectively. + /// + /// The MvcOptions + public static void AddXmlDataContractSerializerFormatter([NotNull] this MvcOptions options) + { + options.OutputFormatters.Add( + new XmlDataContractSerializerOutputFormatter(XmlOutputFormatter.GetDefaultXmlWriterSettings())); + + options.InputFormatters.Add(new XmlDataContractSerializerInputFormatter()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index 886870402e..ff0fb02ec9 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -44,12 +44,9 @@ namespace Microsoft.AspNet.Mvc options.OutputFormatters.Add(new HttpNoContentOutputFormatter()); options.OutputFormatters.Add(new TextPlainFormatter()); options.OutputFormatters.Add(new JsonOutputFormatter()); - options.OutputFormatters.Add( - new XmlDataContractSerializerOutputFormatter(XmlOutputFormatter.GetDefaultXmlWriterSettings())); // Set up default input formatters. options.InputFormatters.Add(new JsonInputFormatter()); - options.InputFormatters.Add(new XmlDataContractSerializerInputFormatter()); // Set up ValueProviders options.ValueProviderFactories.Add(new RouteValueValueProviderFactory()); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedAtActionResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedAtActionResultTests.cs index ab24e30437..bed768483f 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedAtActionResultTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedAtActionResultTests.cs @@ -10,6 +10,7 @@ using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Routing; using Microsoft.AspNet.Testing; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -92,6 +93,12 @@ namespace Microsoft.AspNet.Mvc var services = new Mock(); services.Setup(p => p.GetService(typeof(IOutputFormattersProvider))).Returns(new TestOutputFormatterProvider()); httpContext.RequestServices = services.Object; + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + services.Setup(p => p.GetService(typeof(IOptions))) + .Returns(options.Object); + return httpContext; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedAtRouteResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedAtRouteResultTests.cs index c88019c240..4345727611 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedAtRouteResultTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedAtRouteResultTests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Routing; using Microsoft.AspNet.Testing; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -97,6 +98,11 @@ namespace Microsoft.AspNet.Mvc httpContext.Setup(o => o.Request) .Returns(request); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions))) + .Returns(options.Object); httpContext.Setup(o => o.Response) .Returns(response); httpContext.Setup(o => o.RequestServices.GetService(typeof(IOutputFormattersProvider))) diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedResultTests.cs index 271ada0dfa..b70b8027eb 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedResultTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/CreatedResultTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Routing; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -69,7 +70,11 @@ namespace Microsoft.AspNet.Mvc .Returns(response); httpContext.Setup(o => o.RequestServices.GetService(typeof(IOutputFormattersProvider))) .Returns(new TestOutputFormatterProvider()); - + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions))) + .Returns(options.Object); return httpContext.Object; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectResultTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectResultTests.cs index 1abccd2380..13cdea35bb 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectResultTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ObjectResultTests.cs @@ -72,6 +72,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults httpResponse.SetupGet(r => r.Body).Returns(stream); var actionContext = CreateMockActionContext(httpResponse.Object, acceptHeader); + var result = new ObjectResult(input); // Set the content type property explicitly. @@ -467,10 +468,148 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults Assert.Equal(tempStream.ToArray(), ((MemoryStream)actionContext.HttpContext.Response.Body).ToArray()); } - private static ActionContext CreateMockActionContext(HttpResponse response = null, + [Theory] + [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "application/json; charset=utf-8")] //Chrome + [InlineData("text/html, application/xhtml+xml, */*", + "application/json; charset=utf-8")] //IE + [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "application/json; charset=utf-8")] //Firefox + [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "application/json; charset=utf-8")] //Safari + [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "application/json; charset=utf-8")] //Opera + [InlineData("*/*", @"application/json; charset=utf-8")] + [InlineData("text/html,*/*;q=0.8,application/xml;q=0.9", + "application/json; charset=utf-8")] + public async Task ObjectResult_SelectDefaultFormatter_OnAllMediaRangeAcceptHeaderMediaType( + string acceptHeader, + string expectedResponseContentType) + { + // Arrange + var objectResult = new ObjectResult(new Person() { Name = "John" }); + var outputFormatters = new IOutputFormatter[] { + new HttpNoContentOutputFormatter(), + new TextPlainFormatter(), + new JsonOutputFormatter(), + new XmlDataContractSerializerOutputFormatter(XmlSerializerOutputFormatter.GetDefaultXmlWriterSettings()) + }; + var response = GetMockHttpResponse(); + + var actionContext = CreateMockActionContext( + outputFormatters, + response.Object, + requestAcceptHeader: acceptHeader); + + // Act + await objectResult.ExecuteResultAsync(actionContext); + + // Assert + response.VerifySet(resp => resp.ContentType = expectedResponseContentType); + } + + [Theory] + [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "application/xml; charset=utf-8")] //Chrome + [InlineData("text/html, application/xhtml+xml, */*", + "application/json; charset=utf-8")] //IE + [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "application/xml; charset=utf-8")] //Firefox + [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "application/xml; charset=utf-8")] //Safari + [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "application/xml; charset=utf-8")] //Opera + [InlineData("*/*", + "application/json; charset=utf-8")] + [InlineData("text/html,*/*;q=0.8,application/xml;q=0.9", + "application/xml; charset=utf-8")] + public async Task ObjectResult_PerformsContentNegotiation_OnAllMediaRangeAcceptHeaderMediaType( + string acceptHeader, + string expectedResponseContentType) + { + // Arrange + var objectResult = new ObjectResult(new Person() { Name = "John" }); + var outputFormatters = new IOutputFormatter[] { + new HttpNoContentOutputFormatter(), + new TextPlainFormatter(), + new JsonOutputFormatter(), + new XmlDataContractSerializerOutputFormatter(XmlSerializerOutputFormatter.GetDefaultXmlWriterSettings()) + }; + var response = GetMockHttpResponse(); + + var actionContext = CreateMockActionContext( + outputFormatters, + response.Object, + requestAcceptHeader: acceptHeader, + respectBrowserAcceptHeader: true); + + // Act + await objectResult.ExecuteResultAsync(actionContext); + + // Assert + response.VerifySet(resp => resp.ContentType = expectedResponseContentType); + } + + [Theory] + [InlineData("application/xml;q=0.9,text/plain;q=0.5", "application/xml; charset=utf-8", false)] + [InlineData("application/xml;q=0.9,*/*;q=0.5", "application/json; charset=utf-8", false)] + [InlineData("application/xml;q=0.9,text/plain;q=0.5", "application/xml; charset=utf-8", true)] + [InlineData("application/xml;q=0.9,*/*;q=0.5", "application/xml; charset=utf-8", true)] + public async Task ObjectResult_WildcardAcceptMediaType_AndExplicitResponseContentType( + string acceptHeader, + string expectedResponseContentType, + bool respectBrowserAcceptHeader) + { + // Arrange + var objectResult = new ObjectResult(new Person() { Name = "John" }); + objectResult.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/xml")); + objectResult.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/json")); + var outputFormatters = new IOutputFormatter[] { + new HttpNoContentOutputFormatter(), + new TextPlainFormatter(), + new JsonOutputFormatter(), + new XmlDataContractSerializerOutputFormatter(XmlSerializerOutputFormatter.GetDefaultXmlWriterSettings()) + }; + var response = GetMockHttpResponse(); + + var actionContext = CreateMockActionContext( + outputFormatters, + response.Object, + acceptHeader, + respectBrowserAcceptHeader: respectBrowserAcceptHeader); + + // Act + await objectResult.ExecuteResultAsync(actionContext); + + // Assert + response.VerifySet(resp => resp.ContentType = expectedResponseContentType); + } + + private static ActionContext CreateMockActionContext( + HttpResponse response = null, string requestAcceptHeader = "application/*", string requestContentType = "application/json", - string requestAcceptCharsetHeader = "") + string requestAcceptCharsetHeader = "", + bool respectBrowserAcceptHeader = false) + { + var formatters = new IOutputFormatter[] { new TextPlainFormatter(), new JsonOutputFormatter() }; + + return CreateMockActionContext( + formatters, + response: response, + requestAcceptHeader: requestAcceptHeader, + requestContentType: requestContentType, + requestAcceptCharsetHeader: requestAcceptCharsetHeader, + respectBrowserAcceptHeader: respectBrowserAcceptHeader); + } + + private static ActionContext CreateMockActionContext( + IEnumerable outputFormatters, + HttpResponse response = null, + string requestAcceptHeader = "application/*", + string requestContentType = "application/json", + string requestAcceptCharsetHeader = "", + bool respectBrowserAcceptHeader = false) { var httpContext = new Mock(); if (response != null) @@ -490,7 +629,17 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults httpContext.Setup(o => o.Request).Returns(request); httpContext.Setup(o => o.RequestServices).Returns(GetServiceProvider()); httpContext.Setup(o => o.RequestServices.GetService(typeof(IOutputFormattersProvider))) - .Returns(new TestOutputFormatterProvider()); + .Returns(new TestOutputFormatterProvider(outputFormatters)); + + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions() + { + RespectBrowserAcceptHeader = respectBrowserAcceptHeader + }); + httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions))) + .Returns(options.Object); + return new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); } @@ -551,17 +700,25 @@ namespace Microsoft.AspNet.Mvc.Core.Test.ActionResults private class TestOutputFormatterProvider : IOutputFormattersProvider { + private readonly IEnumerable _formatters; + + public TestOutputFormatterProvider(IEnumerable formatters) + { + _formatters = formatters; + } + public IReadOnlyList OutputFormatters { get { - return new List() - { - new TextPlainFormatter(), - new JsonOutputFormatter() - }; + return _formatters.ToList(); } } } + + public class Person + { + public string Name { get; set; } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionInvokerTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionInvokerTest.cs index d40d158b22..3277056e17 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerActionInvokerTest.cs @@ -13,6 +13,7 @@ using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Routing; using Microsoft.AspNet.Testing; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -1957,6 +1958,12 @@ namespace Microsoft.AspNet.Mvc .Returns(mockFormattersProvider.Object); httpResponse.Body = new MemoryStream(); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions))) + .Returns(options.Object); + var actionContext = new ActionContext( httpContext: httpContext.Object, routeData: new RouteData(), diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs index 0741b68fca..509cbe8ff3 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Routing; +using Microsoft.Framework.OptionsModel; using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -192,6 +193,12 @@ namespace Microsoft.AspNet.Mvc .Setup(s => s.GetService(typeof(IOutputFormattersProvider))) .Returns(mockFormattersProvider.Object); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + services.Setup(s => s.GetService(typeof(IOptions))) + .Returns(options.Object); + // This is the ultimate fallback, it will be used if none of the formatters from options // work. if (enableFallback) diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs index 7b24cf6bba..4ef10959a4 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs @@ -279,11 +279,11 @@ namespace Microsoft.AspNet.Mvc if (optionsAccessor == null) { - var mockOptionsAccessor = new Mock>(); - mockOptionsAccessor.SetupGet(o => o.Options) + var options = new Mock>(); + options.SetupGet(o => o.Options) .Returns(new MvcOptions()); - optionsAccessor = mockOptionsAccessor.Object; + optionsAccessor = options.Object; } var httpContext = new Mock(); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RespectBrowserAcceptHeaderTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RespectBrowserAcceptHeaderTests.cs new file mode 100644 index 0000000000..b2a81e6c86 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RespectBrowserAcceptHeaderTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class RespectBrowserAcceptHeaderTests + { + private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(FormatterWebSite)); + private readonly Action _app = new FormatterWebSite.Startup().Configure; + + [Theory] + [InlineData("application/xml,*/*;0.2")] + [InlineData("application/xml,*/*")] + public async Task AllMediaRangeAcceptHeader_FirstFormatterInListWritesResponse(string acceptHeader) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("Accept", acceptHeader); + + // Act + var response = await client.GetAsync("http://localhost/RespectBrowserAcceptHeader/EmployeeInfo"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString()); + var responseData = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"Id\":10,\"Name\":\"John\"}", responseData); + } + + [Theory] + [InlineData("application/xml,*/*;0.2")] + [InlineData("application/xml,*/*")] + public async Task AllMediaRangeAcceptHeader_ProducesAttributeIsHonored(string acceptHeader) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("Accept", acceptHeader); + var expectedResponseData = "20Mike" + + ""; + + // Act + var response = await client.GetAsync("http://localhost/RespectBrowserAcceptHeader/EmployeeInfoWithProduces"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("application/xml; charset=utf-8", response.Content.Headers.ContentType.ToString()); + var responseData = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponseData, responseData); + } + + [Theory] + [InlineData("application/xml,*/*;0.2")] + [InlineData("application/xml,*/*")] + public async Task AllMediaRangeAcceptHeader_WithContentTypeHeader_ContentTypeIsHonored(string acceptHeader) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("Accept", acceptHeader); + var requestData = "35Jimmy" + + ""; + + // Act + var response = await client.PostAsync("http://localhost/RespectBrowserAcceptHeader/CreateEmployee", + new StringContent(requestData, Encoding.UTF8, "application/xml")); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("application/xml; charset=utf-8", response.Content.Headers.ContentType.ToString()); + var responseData = await response.Content.ReadAsStringAsync(); + Assert.Equal(requestData, responseData); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj index 9e842ca455..9b102c6387 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs index f57b928e44..d946be0beb 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs @@ -83,11 +83,10 @@ namespace Microsoft.AspNet.Mvc setup.Configure(mvcOptions); // Assert - Assert.Equal(4, mvcOptions.OutputFormatters.Count); + Assert.Equal(3, mvcOptions.OutputFormatters.Count); Assert.IsType(mvcOptions.OutputFormatters[0].Instance); Assert.IsType(mvcOptions.OutputFormatters[1].Instance); Assert.IsType(mvcOptions.OutputFormatters[2].Instance); - Assert.IsType(mvcOptions.OutputFormatters[3].Instance); } [Fact] @@ -101,9 +100,8 @@ namespace Microsoft.AspNet.Mvc setup.Configure(mvcOptions); // Assert - Assert.Equal(2, mvcOptions.InputFormatters.Count); + Assert.Equal(1, mvcOptions.InputFormatters.Count); Assert.IsType(mvcOptions.InputFormatters[0].Instance); - Assert.IsType(mvcOptions.InputFormatters[1].Instance); } [Fact] @@ -122,6 +120,20 @@ namespace Microsoft.AspNet.Mvc Assert.IsType(mvcOptions.ModelValidatorProviders[1].Instance); } + [Fact] + public void Setup_IgnoresAcceptHeaderHavingWildCardMediaAndSubMediaTypes() + { + // Arrange + var mvcOptions = new MvcOptions(); + var setup = new MvcOptionsSetup(); + + // Act + setup.Configure(mvcOptions); + + // Assert + Assert.False(mvcOptions.RespectBrowserAcceptHeader); + } + [Fact] public void Setup_SetsUpExcludeFromValidationDelegates() { diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/BadRequestErrorMessageResultTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/BadRequestErrorMessageResultTest.cs index d66607af91..cb4cee8672 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/BadRequestErrorMessageResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/BadRequestErrorMessageResultTest.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -73,6 +74,13 @@ namespace System.Web.Http .Setup(s => s.GetService(typeof(IOutputFormattersProvider))) .Returns(formatters.Object); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + + services.Setup(s => s.GetService(typeof(IOptions))) + .Returns(options.Object); + return services.Object; } } diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/ExceptionResultTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/ExceptionResultTest.cs index a8dda6a917..a394eb1943 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/ExceptionResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/ExceptionResultTest.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -73,6 +74,13 @@ namespace System.Web.Http .Setup(s => s.GetService(typeof(IOutputFormattersProvider))) .Returns(formatters.Object); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + + services.Setup(s => s.GetService(typeof(IOptions))) + .Returns(options.Object); + return services.Object; } } diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/InvalidModelStateResultTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/InvalidModelStateResultTest.cs index 2c28dc0543..6f4425bd89 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/InvalidModelStateResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/InvalidModelStateResultTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Routing; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -86,6 +87,13 @@ namespace System.Web.Http .Setup(s => s.GetService(typeof(IOutputFormattersProvider))) .Returns(formatters.Object); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + + services.Setup(s => s.GetService(typeof(IOptions))) + .Returns(options.Object); + return services.Object; } } diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/NegotiatedContentResultTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/NegotiatedContentResultTest.cs index bf3199b80f..4074de2317 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/NegotiatedContentResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/NegotiatedContentResultTest.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -75,6 +76,13 @@ namespace System.Web.Http .Setup(s => s.GetService(typeof(IOutputFormattersProvider))) .Returns(formatters.Object); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + + services.Setup(s => s.GetService(typeof(IOptions))) + .Returns(options.Object); + return services.Object; } diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/OkNegotiatedContentResultTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/OkNegotiatedContentResultTest.cs index 06b9148a2a..c04eb12f1f 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/OkNegotiatedContentResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ActionResults/OkNegotiatedContentResultTest.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -48,6 +49,13 @@ namespace System.Web.Http .Setup(s => s.GetService(typeof(IOutputFormattersProvider))) .Returns(formatters.Object); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions()); + + services.Setup(s => s.GetService(typeof(IOptions))) + .Returns(options.Object); + return services.Object; } diff --git a/test/WebSites/ActionResultsWebSite/Startup.cs b/test/WebSites/ActionResultsWebSite/Startup.cs index 3b7e8f4020..d8c6760cf9 100644 --- a/test/WebSites/ActionResultsWebSite/Startup.cs +++ b/test/WebSites/ActionResultsWebSite/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; using Microsoft.Framework.DependencyInjection; @@ -16,6 +17,11 @@ namespace ActionResultsWebSite app.UseServices(services => { services.AddMvc(configuration); + + services.Configure(options => + { + options.AddXmlDataContractSerializerFormatter(); + }); }); app.UseMvc(routes => diff --git a/test/WebSites/FormatterWebSite/Controllers/RespectBrowserAcceptHeaderController.cs b/test/WebSites/FormatterWebSite/Controllers/RespectBrowserAcceptHeaderController.cs new file mode 100644 index 0000000000..0724ff9ac7 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Controllers/RespectBrowserAcceptHeaderController.cs @@ -0,0 +1,49 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace FormatterWebSite.Controllers +{ + public class RespectBrowserAcceptHeaderController : Controller + { + [HttpGet] + public Employee EmployeeInfo() + { + return new Employee() + { + Id = 10, + Name = "John" + }; + } + + [HttpGet] + [Produces("application/xml")] + public Employee EmployeeInfoWithProduces() + { + return new Employee() + { + Id = 20, + Name = "Mike" + }; + } + + [HttpPost] + public IActionResult CreateEmployee([FromBody]Employee employee) + { + if(!ModelState.IsValid) + { + return HttpBadRequest(ModelState); + } + + return new ObjectResult(employee); + } + + public class Employee + { + public int Id { get; set; } + + public string Name { get; set; } + } + } +} diff --git a/test/WebSites/FormatterWebSite/FormatterWebSite.kproj b/test/WebSites/FormatterWebSite/FormatterWebSite.kproj index 5ac17019bb..970df2e1a6 100644 --- a/test/WebSites/FormatterWebSite/FormatterWebSite.kproj +++ b/test/WebSites/FormatterWebSite/FormatterWebSite.kproj @@ -15,4 +15,9 @@ 49634 + + + + + \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Startup.cs b/test/WebSites/FormatterWebSite/Startup.cs index 389e4c4ccf..07ebad5684 100644 --- a/test/WebSites/FormatterWebSite/Startup.cs +++ b/test/WebSites/FormatterWebSite/Startup.cs @@ -24,6 +24,8 @@ namespace FormatterWebSite { options.ValidationExcludeFilters.Add(typeof(Developer)); options.ValidationExcludeFilters.Add(typeof(Supplier)); + + options.AddXmlDataContractSerializerFormatter(); }); }); diff --git a/test/WebSites/ModelBindingWebSite/Startup.cs b/test/WebSites/ModelBindingWebSite/Startup.cs index 6a4e37e113..ede76f9cb0 100644 --- a/test/WebSites/ModelBindingWebSite/Startup.cs +++ b/test/WebSites/ModelBindingWebSite/Startup.cs @@ -23,6 +23,8 @@ namespace ModelBindingWebSite { m.MaxModelValidationErrors = 8; m.ModelBinders.Insert(0, typeof(TestMetadataAwareBinder)); + + m.AddXmlDataContractSerializerFormatter(); }); services.AddSingleton();