diff --git a/Mvc.sln b/Mvc.sln
index 8f7bca4e31..d6c3adbc18 100644
--- a/Mvc.sln
+++ b/Mvc.sln
@@ -61,6 +61,12 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Header
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.HeaderValueAbstractions.Tests", "test\Microsoft.AspNet.Mvc.HeaderValueAbstractions.Test\Microsoft.AspNet.Mvc.HeaderValueAbstractions.Tests.kproj", "{E69FD235-2042-43A4-9970-59CB29955B4E}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FAD65E9C-3CF3-4F68-9757-C7358604030B}"
+ ProjectSection(SolutionItems) = preProject
+ global.json = global.json
+ EndProjectSection
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ConnegWebsite", "test\WebSites\ConnegWebSite\ConnegWebsite.kproj", "{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -321,6 +327,16 @@ Global
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|x86.ActiveCfg = Release|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -352,5 +368,6 @@ Global
{14F79E79-AE79-48FA-95DE-D794EF4EABB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{98335B23-E4B9-4CAD-9749-0DED32A659A1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{E69FD235-2042-43A4-9970-59CB29955B4E} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
+ {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
EndGlobal
diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs
index 0fce895466..2928cde9e4 100644
--- a/samples/MvcSample.Web/HomeController.cs
+++ b/samples/MvcSample.Web/HomeController.cs
@@ -97,6 +97,12 @@ namespace MvcSample.Web
Context.Response.WriteAsync("Hello World raw");
}
+ [Produces("application/json", "application/custom", "text/json", Type = typeof(User))]
+ public object ReturnUser()
+ {
+ return CreateUser();
+ }
+
public User CreateUser()
{
User user = new User()
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs
index 4e633b5c8e..0d54fb1fce 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs
@@ -71,9 +71,11 @@ namespace Microsoft.AspNet.Mvc
if (selectedFormatter == null)
{
+ var requestContentType = formatterContext.ActionContext.HttpContext.Request.ContentType;
+
// No formatter found based on accept headers, fall back on request contentType.
- var incomingContentType =
- MediaTypeHeaderValue.Parse(formatterContext.ActionContext.HttpContext.Request.ContentType);
+ MediaTypeHeaderValue incomingContentType = null;
+ MediaTypeHeaderValue.TryParse(requestContentType, out incomingContentType);
// In case the incomingContentType is null (as can be the case with get requests),
// we need to pick the first formatter which
diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs
new file mode 100644
index 0000000000..534a0345c9
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ProducesAttribute.cs
@@ -0,0 +1,52 @@
+// 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.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNet.Mvc.Core;
+using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Specifies the allowed content types and the type of the value returned by the action
+ /// which can be used to select a formatter while executing .
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class ProducesAttribute : ResultFilterAttribute, IProducesMetadataProvider
+ {
+ public ProducesAttribute(string contentType, params string[] additionalContentTypes)
+ {
+ ContentTypes = GetContentTypes(contentType, additionalContentTypes);
+ }
+
+ public Type Type { get; set; }
+
+ public IList ContentTypes { get; set; }
+
+ public override void OnResultExecuting([NotNull] ResultExecutingContext context)
+ {
+ base.OnResultExecuting(context);
+ var objectResult = context.Result as ObjectResult;
+
+ if (objectResult != null)
+ {
+ objectResult.ContentTypes = ContentTypes;
+ }
+ }
+
+ private List GetContentTypes(string firstArg, string[] args)
+ {
+ var contentTypes = new List();
+ contentTypes.Add(MediaTypeHeaderValue.Parse(firstArg));
+ foreach (var item in args)
+ {
+ var contentType = MediaTypeHeaderValue.Parse(item);
+ contentTypes.Add(contentType);
+ }
+
+ return contentTypes;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs
index 9bd41f033d..ff42bb55b8 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/OutputFormatter.cs
@@ -53,8 +53,9 @@ namespace Microsoft.AspNet.Mvc
if (encoding == null)
{
// Match based on request acceptHeader.
- var requestContentType = MediaTypeHeaderValue.Parse(request.ContentType);
- if (requestContentType != null && !string.IsNullOrEmpty(requestContentType.Charset))
+ MediaTypeHeaderValue requestContentType = null;
+ if (MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType) &&
+ !string.IsNullOrEmpty(requestContentType.Charset))
{
var requestCharset = requestContentType.Charset;
encoding = SupportedEncodings.FirstOrDefault(
diff --git a/src/Microsoft.AspNet.Mvc.Core/IProducesMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/IProducesMetadataProvider.cs
new file mode 100644
index 0000000000..2dc8f58fd6
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/IProducesMetadataProvider.cs
@@ -0,0 +1,25 @@
+// 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.Collections.Generic;
+using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Provides a return type and a set of possible content types returned by a successful execution of the action.
+ ///
+ public interface IProducesMetadataProvider
+ {
+ ///
+ /// Optimistic return type of the action.
+ ///
+ Type Type { get; set; }
+
+ ///
+ /// A collection of allowed content types which can be produced by the action.
+ ///
+ IList ContentTypes { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
index 2735d502b4..95345dd69e 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
+++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
@@ -35,28 +35,30 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeHeaderValue.cs b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeHeaderValue.cs
index cfbfac16d9..c8011b52a4 100644
--- a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeHeaderValue.cs
+++ b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/MediaTypeHeaderValue.cs
@@ -52,16 +52,28 @@ namespace Microsoft.AspNet.Mvc.HeaderValueAbstractions
public static MediaTypeHeaderValue Parse(string input)
{
+ MediaTypeHeaderValue headerValue = null;
+ if (!TryParse(input, out headerValue))
+ {
+ throw new ArgumentException(Resources.FormatInvalidContentType(input));
+ }
+
+ return headerValue;
+ }
+
+ public static bool TryParse(string input, out MediaTypeHeaderValue headerValue)
+ {
+ headerValue = null;
if (string.IsNullOrEmpty(input))
{
- return null;
+ return false;
}
var inputArray = input.Split(new[] { ';' }, 2);
var mediaTypeParts = inputArray[0].Split('/');
if (mediaTypeParts.Length != 2)
{
- return null;
+ return false;
}
// TODO: throw if the media type and subtypes are invalid.
@@ -85,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.HeaderValueAbstractions
parameters.TryGetValue("charset", out charset);
}
- var mediaTypeHeader = new MediaTypeHeaderValue()
+ headerValue = new MediaTypeHeaderValue()
{
MediaType = mediaType,
MediaSubType = mediaSubType,
@@ -94,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.HeaderValueAbstractions
Parameters = parameters ?? new Dictionary(StringComparer.OrdinalIgnoreCase),
};
- return mediaTypeHeader;
+ return true;
}
protected static Dictionary ParseParameters(string inputString)
diff --git a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Microsoft.AspNet.Mvc.HeaderValueAbstractions.kproj b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Microsoft.AspNet.Mvc.HeaderValueAbstractions.kproj
index d9ecfd883f..6fc2ae284f 100644
--- a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Microsoft.AspNet.Mvc.HeaderValueAbstractions.kproj
+++ b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Microsoft.AspNet.Mvc.HeaderValueAbstractions.kproj
@@ -21,10 +21,16 @@
+
+
+
+ ResXFileCodeGenerator
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..2299e4a0fd
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Properties/Resources.Designer.cs
@@ -0,0 +1,46 @@
+//
+namespace Microsoft.AspNet.Mvc.HeaderValueAbstractions
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNet.Mvc.HeaderValueAbstractions.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// Invalid Argument. Content type '{0}' could not be parsed.
+ ///
+ internal static string InvalidContentType
+ {
+ get { return GetString("InvalidContentType"); }
+ }
+
+ ///
+ /// Invalid Argument. Content type '{0}' could not be parsed.
+ ///
+ internal static string FormatInvalidContentType(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("InvalidContentType"), p0);
+ }
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Resources.resx b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Resources.resx
new file mode 100644
index 0000000000..d1a33eedd0
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/Resources.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Invalid Argument. Content type '{0}' could not be parsed.
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/project.json b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/project.json
index 0cbe5678d9..710b4d10bd 100644
--- a/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/project.json
+++ b/src/Microsoft.AspNet.Mvc.HeaderValueAbstractions/project.json
@@ -6,6 +6,8 @@
"k10": {
"dependencies": {
"System.Collections": "4.0.10.0",
+ "System.Diagnostics.Debug": "4.0.10.0",
+ "System.Resources.ResourceManager": "4.0.0.0",
"System.Runtime": "4.0.20.0",
"System.Runtime.Extensions": "4.0.10.0"
}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ProducesAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ProducesAttributeTests.cs
new file mode 100644
index 0000000000..ae0992cbeb
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ProducesAttributeTests.cs
@@ -0,0 +1,80 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Routing;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.Test
+{
+ public class ProducesAttributeTests
+ {
+ [Fact]
+ public async Task ProducesContentAttribute_SetsContentType()
+ {
+ // Arrange
+ var mediaType1 = MediaTypeHeaderValue.Parse("application/json");
+ var mediaType2 = MediaTypeHeaderValue.Parse("text/json;charset=utf-8");
+ var producesContentAttribute = new ProducesAttribute("application/json", "text/json;charset=utf-8");
+ var resultExecutingContext = CreateResultExecutingContext(producesContentAttribute);
+ var next = new ResultExecutionDelegate(
+ () => Task.FromResult(CreateResultExecutedContext(resultExecutingContext)));
+
+ // Act
+ await producesContentAttribute.OnResultExecutionAsync(resultExecutingContext, next);
+
+ // Assert
+ var objectResult = resultExecutingContext.Result as ObjectResult;
+ Assert.Equal(2, objectResult.ContentTypes.Count);
+ ValidateMediaType(mediaType1, objectResult.ContentTypes[0]);
+ ValidateMediaType(mediaType2, objectResult.ContentTypes[1]);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ [InlineData("invalid")]
+ public void ProducesAttribute_InvalidContentType_Throws(string content)
+ {
+ // Act & Assert
+ var ex = Assert.Throws(
+ () => new ProducesAttribute(content));
+ Assert.Equal("Invalid Argument. Content type '" + content + "' could not be parsed.",
+ ex.Message);
+ }
+
+ private static void ValidateMediaType(MediaTypeHeaderValue expectedMediaType, MediaTypeHeaderValue actualMediaType)
+ {
+ Assert.Equal(expectedMediaType.MediaType, actualMediaType.MediaType);
+ Assert.Equal(expectedMediaType.MediaSubType, actualMediaType.MediaSubType);
+ Assert.Equal(expectedMediaType.Charset, actualMediaType.Charset);
+ Assert.Equal(expectedMediaType.MediaTypeRange, actualMediaType.MediaTypeRange);
+ Assert.Equal(expectedMediaType.Parameters.Count, actualMediaType.Parameters.Count);
+ foreach (var item in expectedMediaType.Parameters)
+ {
+ Assert.Equal(item.Value, actualMediaType.Parameters[item.Key]);
+ }
+ }
+
+ private static ResultExecutedContext CreateResultExecutedContext(ResultExecutingContext context)
+ {
+ return new ResultExecutedContext(context, context.Filters, context.Result);
+ }
+
+ private static ResultExecutingContext CreateResultExecutingContext(IFilter filter)
+ {
+ return new ResultExecutingContext(
+ CreateActionContext(),
+ new IFilter[] { filter, },
+ new ObjectResult("Some Value"));
+ }
+
+ private static ActionContext CreateActionContext()
+ {
+ return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
index 9cc9c1569c..971a81b273 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
@@ -31,6 +31,8 @@
+
+
@@ -40,6 +42,7 @@
+
@@ -48,12 +51,10 @@
+
-
-
-
@@ -92,8 +93,8 @@
-
+
@@ -103,10 +104,10 @@
+
-
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/project.json b/test/Microsoft.AspNet.Mvc.Core.Test/project.json
index 28f4d47e51..d5d27f9f21 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/project.json
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/project.json
@@ -4,6 +4,7 @@
},
"dependencies": {
"Microsoft.AspNet.Http": "1.0.0-*",
+ "Microsoft.AspNet.PipelineCore": "1.0.0-*",
"Microsoft.AspNet.Mvc.HeaderValueAbstractions": "1.0.0-*",
"Microsoft.AspNet.Mvc" : "",
"Microsoft.AspNet.Mvc.Core" : "",
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs
new file mode 100644
index 0000000000..fe2fb85a78
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs
@@ -0,0 +1,253 @@
+// 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.Threading.Tasks;
+using ConnegWebsite;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.TestHost;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.FunctionalTests
+{
+ public class ConnegTests
+ {
+ private readonly IServiceProvider _provider = TestHelper.CreateServices("ConnegWebsite");
+ private readonly Action _app = new Startup().Configure;
+
+ [Fact]
+ public async Task ProducesContentAttribute_SingleContentType_PicksTheFirstSupportedFormatter()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+
+ // Selects custom even though it is last in the list.
+ var expectedContentType = "application/custom;charset=utf-8";
+ var expectedBody = "Written using custom format.";
+
+ // Act
+ var result = await client.GetAsync("http://localhost/Normal/WriteUserUsingCustomFormat");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ [Fact]
+ public async Task ProducesContentAttribute_MultipleContentTypes_RunsConnegToSelectFormatter()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "application/json;charset=utf-8";
+ var expectedBody = "{\r\n \"Name\": \"My name\",\r\n \"Address\": \"My address\"\r\n}";
+
+ // Act
+ var result = await client.GetAsync("http://localhost/Normal/MultipleAllowedContentTypes");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ [Fact]
+ public async Task NoProducesContentAttribute_ActionReturningString_RunsUsingTextFormatter()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "text/plain;charset=utf-8";
+ var expectedBody = "NormalController";
+
+ // Act
+ var result = await client.GetAsync("http://localhost/Normal/ReturnClassName");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ [Fact]
+ public async Task NoProducesContentAttribute_ActionReturningAnyObject_RunsUsingDefaultFormatters()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "application/json;charset=utf-8";
+ //var expectedBody = "\"NormalController\"";
+
+ // Act
+ var result = await client.GetAsync("http://localhost/Normal/ReturnUser");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ }
+
+ [Fact]
+ public async Task NoMatchingFormatter_ForTheGivenContentType_Returns406()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+
+ // Act
+ var result = await client.GetAsync("http://localhost/Normal/ReturnUser_NoMatchingFormatter");
+
+ // Assert
+ Assert.Equal(406, result.HttpContext.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ProducesContentAttribute_OnAction_OverridesTheValueOnClass()
+ {
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+
+ // Value on the class is application/json.
+ var expectedContentType = "application/custom_ProducesContentBaseController_Action;charset=utf-8";
+ var expectedBody = "ProducesContentBaseController";
+
+ // Act
+ var result = await client.GetAsync("http://localhost/ProducesContentBase/ReturnClassName");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ [Fact]
+ public async Task ProducesContentAttribute_OnDerivedClass_OverridesTheValueOnBaseClass()
+ {
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "application/custom_ProducesContentOnClassController;charset=utf-8";
+ var expectedBody = "ProducesContentOnClassController";
+
+ // Act
+ var result = await client.GetAsync(
+ "http://localhost/ProducesContentOnClass/ReturnClassNameWithNoContentTypeOnAction");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ [Fact]
+ public async Task ProducesContentAttribute_OnDerivedAction_OverridesTheValueOnBaseClass()
+ {
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "application/custom_NoProducesContentOnClassController_Action;charset=utf-8";
+ var expectedBody = "NoProducesContentOnClassController";
+
+ // Act
+ var result = await client.GetAsync("http://localhost/NoProducesContentOnClass/ReturnClassName");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ [Fact]
+ public async Task ProducesContentAttribute_OnDerivedAction_OverridesTheValueOnBaseAction()
+ {
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "application/custom_NoProducesContentOnClassController_Action;charset=utf-8";
+ var expectedBody = "NoProducesContentOnClassController";
+
+ // Act
+ var result = await client.GetAsync("http://localhost/NoProducesContentOnClass/ReturnClassName");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ [Fact]
+ public async Task ProducesContentAttribute_OnDerivedClassAndAction_OverridesTheValueOnBaseClass()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "application/custom_ProducesContentOnClassController_Action;charset=utf-8";
+ var expectedBody = "ProducesContentOnClassController";
+
+ // Act
+ var result = await client.GetAsync("http://localhost/ProducesContentOnClass/ReturnClassNameContentTypeOnDerivedAction");
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+
+ [InlineData("ReturnTaskOfString")]
+ [InlineData("ReturnTaskOfObject_StringValue")]
+ [InlineData("ReturnString")]
+ [InlineData("ReturnObject_StringValue")]
+ [InlineData("ReturnString_NullValue")]
+ public async Task TextPlainFormatter_ReturnsTextPlainContentType(string actionName)
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "text/plain;charset=utf-8";
+ var expectedBody = actionName;
+
+ // Act
+ var result = await client.GetAsync("http://localhost/TextPlain/" + actionName);
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ [InlineData("ReturnTaskOfObject_ObjectValue")]
+ [InlineData("ReturnObject_ObjectValue")]
+ [InlineData("ReturnObject_NullValue")]
+ public async Task TextPlainFormatter_DoesNotSelectTextPlainFormatterForNonStringValue(string actionName)
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "application/json;charset=utf-8";
+ var expectedBody = actionName;
+
+ // Act
+ var result = await client.GetAsync("http://localhost/TextPlain/" + actionName);
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ }
+
+ [InlineData("ReturnString_NullValue")]
+ public async Task TextPlainFormatter_DoesNotWriteNullValue(string actionName)
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.Handler;
+ var expectedContentType = "text/plain;charset=utf-8";
+ string expectedBody = null;
+
+ // Act
+ var result = await client.GetAsync("http://localhost/TextPlain/" + actionName);
+
+ // Assert
+ Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
+ var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj
index c60a012307..0002e0cf6e 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj
@@ -31,6 +31,7 @@
+
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
index e9a88b59dd..90a5f6a8b2 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
@@ -6,6 +6,7 @@
"ActivatorWebSite": "",
"BasicWebSite": "",
"CompositeViewEngine": "",
+ "ConnegWebsite": "",
"FormatterWebSite": "",
"InlineConstraintsWebSite": "",
"Microsoft.AspNet.TestHost": "1.0.0-*",
diff --git a/test/WebSites/ConnegWebSite/ConnegWebsite.kproj b/test/WebSites/ConnegWebSite/ConnegWebsite.kproj
new file mode 100644
index 0000000000..dd3b32c0d0
--- /dev/null
+++ b/test/WebSites/ConnegWebSite/ConnegWebsite.kproj
@@ -0,0 +1,41 @@
+
+
+
+ 12.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ c6e5affa-890a-448f-8de3-878b1d3c9fc7
+ Library
+
+
+ ConsoleDebugger
+
+
+ WebDebugger
+
+
+
+
+
+
+ 2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/WebSites/ConnegWebSite/ContentType.cs b/test/WebSites/ConnegWebSite/ContentType.cs
new file mode 100644
index 0000000000..d152773bed
--- /dev/null
+++ b/test/WebSites/ConnegWebSite/ContentType.cs
@@ -0,0 +1,45 @@
+// 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.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Mvc;
+using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
+
+namespace ConnegWebsite
+{
+ public class CustomFormatter : OutputFormatter
+ {
+ public string ContentType { get; private set; }
+
+ public CustomFormatter(string contentType)
+ {
+ ContentType = contentType;
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(contentType));
+ SupportedEncodings.Add(Encoding.GetEncoding("utf-8"));
+ }
+
+ public override bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
+ {
+ if (base.CanWriteResult(context, contentType))
+ {
+ var actionReturnString = context.Object as string;
+ if (actionReturnString != null)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public override async Task WriteResponseBodyAsync(OutputFormatterContext context)
+ {
+ var response = context.ActionContext.HttpContext.Response;
+ response.ContentType = ContentType + ";charset=utf-8";
+ await response.WriteAsync(context.Object as string);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ConnegWebSite/Controllers/HomeController.cs b/test/WebSites/ConnegWebSite/Controllers/HomeController.cs
new file mode 100644
index 0000000000..ec92463c3b
--- /dev/null
+++ b/test/WebSites/ConnegWebSite/Controllers/HomeController.cs
@@ -0,0 +1,15 @@
+// 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 ConnegWebsite
+{
+ public class HomeController : Controller
+ {
+ public IActionResult Index()
+ {
+ return new JsonResult("Index Method");
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ConnegWebSite/Controllers/NoProducesContentOnClassController.cs b/test/WebSites/ConnegWebSite/Controllers/NoProducesContentOnClassController.cs
new file mode 100644
index 0000000000..570a63a2b1
--- /dev/null
+++ b/test/WebSites/ConnegWebSite/Controllers/NoProducesContentOnClassController.cs
@@ -0,0 +1,28 @@
+// 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 ConnegWebsite
+{
+ public class NoProducesContentOnClassController : ProducesContentBaseController
+ {
+ public override void OnActionExecuted(ActionExecutedContext context)
+ {
+ var result = context.Result as ObjectResult;
+ if (result != null)
+ {
+ result.Formatters.Add(new CustomFormatter("application/custom_NoProducesContentOnClassController_Action"));
+ }
+
+ base.OnActionExecuted(context);
+ }
+
+ [Produces("application/custom_NoProducesContentOnClassController_Action")]
+ public override string ReturnClassName()
+ {
+ // should be written using the formatter provided by this action and not the base action.
+ return "NoProducesContentOnClassController";
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ConnegWebSite/Controllers/NormalController.cs b/test/WebSites/ConnegWebSite/Controllers/NormalController.cs
new file mode 100644
index 0000000000..04e6b589dd
--- /dev/null
+++ b/test/WebSites/ConnegWebSite/Controllers/NormalController.cs
@@ -0,0 +1,64 @@
+// 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 ConnegWebsite
+{
+ public class NormalController : Controller
+ {
+ public override void OnActionExecuted(ActionExecutedContext context)
+ {
+ var result = context.Result as ObjectResult;
+ if (result != null)
+ {
+ result.Formatters.Add(new PlainTextFormatter());
+ result.Formatters.Add(new CustomFormatter("application/custom"));
+ result.Formatters.Add(new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(),
+ indent: true));
+ }
+
+ base.OnActionExecuted(context);
+ }
+
+ public string ReturnClassName()
+ {
+ return "NormalController";
+ }
+
+ public User ReturnUser()
+ {
+ return CreateUser();
+ }
+
+ [Produces("application/NoFormatter")]
+ public User ReturnUser_NoMatchingFormatter()
+ {
+ return CreateUser();
+ }
+
+ [Produces("application/custom", "application/json", "text/json")]
+ public User MultipleAllowedContentTypes()
+ {
+ return CreateUser();
+ }
+
+ [Produces("application/custom")]
+ public string WriteUserUsingCustomFormat()
+ {
+ return "Written using custom format.";
+ }
+
+ [NonAction]
+ public User CreateUser()
+ {
+ User user = new User()
+ {
+ Name = "My name",
+ Address = "My address",
+ };
+
+ return user;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ConnegWebSite/Controllers/ProducesContentBaseController.cs b/test/WebSites/ConnegWebSite/Controllers/ProducesContentBaseController.cs
new file mode 100644
index 0000000000..f6cfa2db11
--- /dev/null
+++ b/test/WebSites/ConnegWebSite/Controllers/ProducesContentBaseController.cs
@@ -0,0 +1,42 @@
+// 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 ConnegWebsite
+{
+ [Produces("application/custom_ProducesContentBaseController")]
+ public class ProducesContentBaseController : Controller
+ {
+ public override void OnActionExecuted(ActionExecutedContext context)
+ {
+ var result = context.Result as ObjectResult;
+ if(result != null)
+ {
+ result.Formatters.Add(new PlainTextFormatter());
+ result.Formatters.Add(new CustomFormatter("application/custom_ProducesContentBaseController"));
+ result.Formatters.Add(new CustomFormatter("application/custom_ProducesContentBaseController_Action"));
+ }
+
+ base.OnActionExecuted(context);
+ }
+
+ [Produces("application/custom_ProducesContentBaseController_Action")]
+ public virtual string ReturnClassName()
+ {
+ // Should be written using the action's content type. Overriding the one at the class.
+ return "ProducesContentBaseController";
+ }
+
+ public virtual string ReturnClassNameWithNoContentTypeOnAction()
+ {
+ // Should be written using the action's content type. Overriding the one at the class.
+ return "ProducesContentBaseController";
+ }
+
+ public virtual string ReturnClassNameContentTypeOnDerivedAction()
+ {
+ return "ProducesContentBaseController";
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ConnegWebSite/Controllers/ProducesContentOnClassController.cs b/test/WebSites/ConnegWebSite/Controllers/ProducesContentOnClassController.cs
new file mode 100644
index 0000000000..a1a864f990
--- /dev/null
+++ b/test/WebSites/ConnegWebSite/Controllers/ProducesContentOnClassController.cs
@@ -0,0 +1,44 @@
+// 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 ConnegWebsite
+{
+ [Produces("application/custom_ProducesContentOnClassController")]
+ public class ProducesContentOnClassController : ProducesContentBaseController
+ {
+ public override void OnActionExecuted(ActionExecutedContext context)
+ {
+ var result = context.Result as ObjectResult;
+ if (result != null)
+ {
+ result.Formatters.Add(new CustomFormatter("application/custom_ProducesContentOnClassController"));
+ result.Formatters.Add(
+ new CustomFormatter("application/custom_ProducesContentOnClassController_Action"));
+ }
+
+ base.OnActionExecuted(context);
+ }
+
+ // No Content type defined by the derived class action.
+ public override string ReturnClassName()
+ {
+ // should be written using the content defined at base class's action.
+ return "ProducesContentOnClassController";
+ }
+
+ public override string ReturnClassNameWithNoContentTypeOnAction()
+ {
+ // should be written using the content defined at derived class's class.
+ return "ProducesContentOnClassController";
+ }
+
+ [Produces("application/custom_ProducesContentOnClassController_Action")]
+ public override string ReturnClassNameContentTypeOnDerivedAction()
+ {
+ // should be written using the content defined at derived class's class.
+ return "ProducesContentOnClassController";
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ConnegWebSite/Controllers/TextPlainController.cs b/test/WebSites/ConnegWebSite/Controllers/TextPlainController.cs
new file mode 100644
index 0000000000..a3d17b7d70
--- /dev/null
+++ b/test/WebSites/ConnegWebSite/Controllers/TextPlainController.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc;
+
+namespace ConnegWebsite
+{
+ public class TextPlainController : Controller
+ {
+ public Task ReturnTaskOfString()
+ {
+ return Task.FromResult("ReturnTaskOfString");
+ }
+
+ public Task