diff --git a/Mvc.sln b/Mvc.sln
index 52f976e5b0..01b51cf2ce 100644
--- a/Mvc.sln
+++ b/Mvc.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
-VisualStudioVersion = 14.0.22416.0
+VisualStudioVersion = 14.0.22303.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@@ -116,6 +116,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LoggingWebSite", "test\WebS
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ErrorPageMiddlewareWebSite", "test\WebSites\ErrorPageMiddlewareWebSite\ErrorPageMiddlewareWebSite.kproj", "{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ActionConstraintsWebSite", "test\WebSites\ActionConstraintsWebSite\ActionConstraintsWebSite.kproj", "{AF210F69-9D31-43AF-AC3A-CD366E252218}"
+EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CustomRouteWebSite", "test\WebSites\CustomRouteWebSite\CustomRouteWebSite.kproj", "{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseCacheWebSite", "test\WebSites\ResponseCacheWebSite\ResponseCacheWebSite.kproj", "{BDEEBE09-C0C4-433C-B0B8-8478C9776996}"
@@ -644,6 +646,18 @@ Global
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|x86.ActiveCfg = Release|Any CPU
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|x86.Build.0 = Release|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Debug|x86.Build.0 = Debug|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|x86.ActiveCfg = Release|Any CPU
+ {AF210F69-9D31-43AF-AC3A-CD366E252218}.Release|x86.Build.0 = Release|Any CPU
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@@ -724,6 +738,7 @@ Global
{0A6BB4C0-48D3-4E7F-952B-B8917345E075} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{0AD78AB5-D67C-49BC-81B1-0C51BFA82B5E} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
+ {AF210F69-9D31-43AF-AC3A-CD366E252218} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{BDEEBE09-C0C4-433C-B0B8-8478C9776996} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionConstraints/ConsumesAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/ActionConstraints/ConsumesAttribute.cs
new file mode 100644
index 0000000000..6b61e23d38
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionConstraints/ConsumesAttribute.cs
@@ -0,0 +1,160 @@
+// 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.Net.Http.Headers;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Specifies the allowed content types which can be used to select the action based on request's content-type.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class ConsumesAttribute : Attribute, IResourceFilter, IConsumesActionConstraint
+ {
+ public static readonly int ConsumesActionConstraintOrder = 200;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ public ConsumesAttribute([NotNull] string contentType, params string[] otherContentTypes)
+ {
+ ContentTypes = GetContentTypes(contentType, otherContentTypes);
+ }
+
+ // The value used is a non default value so that it avoids getting mixed with other action constraints
+ // with default order.
+ ///
+ int IActionConstraint.Order { get; } = ConsumesActionConstraintOrder;
+
+ ///
+ public IList ContentTypes { get; set; }
+
+ ///
+ public void OnResourceExecuting([NotNull] ResourceExecutingContext context)
+ {
+ // Only execute if the current filter is the one which is closest to the action.
+ // Ignore all other filters. This is to ensure we have a overriding behavior.
+ if (IsApplicable(context.ActionDescriptor))
+ {
+ MediaTypeHeaderValue requestContentType = null;
+ MediaTypeHeaderValue.TryParse(context.HttpContext.Request.ContentType, out requestContentType);
+
+ // Only execute if this is the last filter before calling the action.
+ // This ensures that we only run the filter which is closest to the action.
+ if (requestContentType != null &&
+ !ContentTypes.Any(contentType => contentType.IsSubsetOf(requestContentType)))
+ {
+ context.Result = new UnsupportedMediaTypeResult();
+ }
+ }
+ }
+
+ ///
+ public void OnResourceExecuted([NotNull] ResourceExecutedContext context)
+ {
+ }
+
+ public bool Accept(ActionConstraintContext context)
+ {
+ // If this constraint is not closest to the action, it will be skipped.
+ if (!IsApplicable(context.CurrentCandidate.Action))
+ {
+ // Since the constraint is to be skipped, returning true here
+ // will let the current candidate ignore this constraint and will
+ // be selected based on other constraints for this action.
+ return true;
+ }
+
+ MediaTypeHeaderValue requestContentType = null;
+ MediaTypeHeaderValue.TryParse(context.RouteContext.HttpContext.Request.ContentType, out requestContentType);
+
+ // If the request content type is null we need to act like pass through.
+ // In case there is a single candidate with a constraint it should be selected.
+ // If there are multiple actions with consumes action constraints this should result in ambiguous exception
+ // unless there is another action without a consumes constraint.
+ if (requestContentType == null)
+ {
+ var isActionWithoutConsumeConstraintPresent = context.Candidates.Any(
+ candidate => candidate.Constraints == null ||
+ !candidate.Constraints.Any(constraint => constraint is IConsumesActionConstraint));
+
+ return !isActionWithoutConsumeConstraintPresent;
+ }
+
+ if (ContentTypes.Any(c => c.IsSubsetOf(requestContentType)))
+ {
+ return true;
+ }
+
+ var firstCandidate = context.Candidates[0];
+ if (firstCandidate != context.CurrentCandidate)
+ {
+ // If the current candidate is not same as the first candidate,
+ // we need not probe other candidates to see if they apply.
+ // Only the first candidate is allowed to probe other candidates and based on the result select itself.
+ return false;
+ }
+
+ // Run the matching logic for all IConsumesActionConstraints we can find, and see what matches.
+ // 1). If we have a unique best match, then only that constraint should return true.
+ // 2). If we have multiple matches, then all constraints that match will return true
+ // , resulting in ambiguity(maybe).
+ // 3). If we have no matches, then we choose the first constraint to return true.It will later return a 415
+ foreach (var candidate in context.Candidates)
+ {
+ if (candidate == firstCandidate)
+ {
+ continue;
+ }
+
+ var tempContext = new ActionConstraintContext()
+ {
+ Candidates = context.Candidates,
+ RouteContext = context.RouteContext,
+ CurrentCandidate = candidate
+ };
+
+ if (candidate.Constraints == null || candidate.Constraints.Count() == 0 ||
+ candidate.Constraints.Any(constraint => constraint is IConsumesActionConstraint &&
+ constraint.Accept(tempContext)))
+ {
+ // There is someone later in the chain which can handle the request.
+ // end the process here.
+ return false;
+ }
+ }
+
+ // There is no one later in the chain that can handle this content type return a false positive so that
+ // later we can detect and return a 415.
+ return true;
+ }
+
+ private bool IsApplicable(ActionDescriptor actionDescriptor)
+ {
+ // If there are multiple IConsumeActionConstraints which are defined at the class and
+ // at the action level, the one closest to the action overrides the others. To ensure this
+ // we take advantage of the fact that ConsumesAttribute is both an IActionFilter and an
+ // IConsumeActionConstraint. Since filterdescriptor collection is ordered (the last filter is the one
+ // closest to the action), we apply this constraint only if there is no IConsumeActionConstraint after this.
+ return actionDescriptor.FilterDescriptors.Last(
+ filter => filter.Filter is IConsumesActionConstraint).Filter == this;
+
+ }
+
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionConstraints/IConsumesActionConstraint.cs b/src/Microsoft.AspNet.Mvc.Core/ActionConstraints/IConsumesActionConstraint.cs
new file mode 100644
index 0000000000..5f5825b32f
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionConstraints/IConsumesActionConstraint.cs
@@ -0,0 +1,13 @@
+// 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
+{
+ ///
+ /// An constraint that identifies a type which can be used to select an action
+ /// based on incoming request.
+ ///
+ public interface IConsumesActionConstraint : IActionConstraint
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/UnsupportedMediaTypeResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/UnsupportedMediaTypeResult.cs
new file mode 100644
index 0000000000..e84fbc5379
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/UnsupportedMediaTypeResult.cs
@@ -0,0 +1,19 @@
+// 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
+{
+ ///
+ /// A that when
+ /// executed will produce a UnsupportedMediaType (415) response.
+ ///
+ public class UnsupportedMediaTypeResult : HttpStatusCodeResult
+ {
+ ///
+ /// Creates a new instance of .
+ ///
+ public UnsupportedMediaTypeResult() : base(415)
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ConsumesAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ConsumesAttributeTests.cs
new file mode 100644
index 0000000000..be302c3542
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ConsumesAttributeTests.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;
+using System.Collections.Generic;
+using Microsoft.AspNet.Http.Core;
+using Microsoft.AspNet.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public class ConsumesAttributeTests
+ {
+ [Theory]
+ [InlineData("application")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void Constructor_ForInvalidContentType_Throws(string contentType)
+ {
+ // Arrange
+ var expectedMessage = string.Format("Invalid value '{0}'.", contentType ?? "");
+
+ // Act & Assert
+ var exception = Assert.Throws(() => new ConsumesAttribute(contentType));
+ Assert.Equal(expectedMessage, exception.Message);
+ }
+
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("application/json;Parameter1=12")]
+ [InlineData("text/xml")]
+ public void Accept_MatchesForMachingRequestContentType(string contentType)
+ {
+ // Arrange
+ var constraint = new ConsumesAttribute("application/json", "text/xml");
+ var action = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint, FilterScope.Action) }
+ };
+
+ var context = new ActionConstraintContext();
+ context.Candidates = new List()
+ {
+ new ActionSelectorCandidate(action, new [] { constraint }),
+ };
+
+ context.CurrentCandidate = context.Candidates[0];
+ context.RouteContext = CreateRouteContext(contentType: contentType);
+
+ // Act & Assert
+ Assert.True(constraint.Accept(context));
+ }
+
+ [Fact]
+ public void Accept_TheFirstCandidateReturnsFalse_IfALaterOneMatches()
+ {
+ // Arrange
+ var constraint1 = new ConsumesAttribute("application/json", "text/xml");
+ var action1 = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint1, FilterScope.Action) }
+ };
+
+ var constraint2 = new Mock();
+ var action2 = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint2.Object, FilterScope.Action) }
+ };
+
+ constraint2.Setup(o => o.Accept(It.IsAny()))
+ .Returns(true);
+
+ var context = new ActionConstraintContext();
+ context.Candidates = new List()
+ {
+ new ActionSelectorCandidate(action1, new [] { constraint1 }),
+ new ActionSelectorCandidate(action2, new [] { constraint2.Object }),
+ };
+
+ context.CurrentCandidate = context.Candidates[0];
+ context.RouteContext = CreateRouteContext(contentType: "application/custom");
+
+ // Act & Assert
+ Assert.False(constraint1.Accept(context));
+ }
+
+ [Theory]
+ [InlineData("application/custom")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void Accept_ForNoMatchingCandidates_SelectsTheFirstCandidate(string contentType)
+ {
+ // Arrange
+ var constraint1 = new ConsumesAttribute("application/json", "text/xml");
+ var action1 = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint1, FilterScope.Action) }
+ };
+
+ var constraint2 = new Mock();
+ var action2 = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint2.Object, FilterScope.Action) }
+ };
+
+ constraint2.Setup(o => o.Accept(It.IsAny()))
+ .Returns(false);
+
+ var context = new ActionConstraintContext();
+ context.Candidates = new List()
+ {
+ new ActionSelectorCandidate(action1, new [] { constraint1 }),
+ new ActionSelectorCandidate(action2, new [] { constraint2.Object }),
+ };
+
+ context.CurrentCandidate = context.Candidates[0];
+ context.RouteContext = CreateRouteContext(contentType: contentType);
+
+ // Act & Assert
+ Assert.True(constraint1.Accept(context));
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ public void Accept_ForNoRequestType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
+ {
+ // Arrange
+ var constraint1 = new ConsumesAttribute("application/json");
+ var actionWithConstraint = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint1, FilterScope.Action) }
+ };
+
+ var constraint2 = new ConsumesAttribute("text/xml");
+ var actionWithConstraint2 = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint2, FilterScope.Action) }
+ };
+
+ var actionWithoutConstraint = new ActionDescriptor();
+
+ var context = new ActionConstraintContext();
+ context.Candidates = new List()
+ {
+ new ActionSelectorCandidate(actionWithConstraint, new [] { constraint1 }),
+ new ActionSelectorCandidate(actionWithConstraint2, new [] { constraint2 }),
+ new ActionSelectorCandidate(actionWithoutConstraint, new List()),
+ };
+
+ context.RouteContext = CreateRouteContext(contentType: contentType);
+
+ // Act & Assert
+ context.CurrentCandidate = context.Candidates[0];
+ Assert.False(constraint1.Accept(context));
+ context.CurrentCandidate = context.Candidates[1];
+ Assert.False(constraint2.Accept(context));
+ }
+
+ [Theory]
+ [InlineData("application/xml")]
+ [InlineData("application/custom")]
+ [InlineData("invalid/invalid")]
+ public void Accept_UnrecognizedMediaType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
+ {
+ // Arrange
+ var actionWithoutConstraint = new ActionDescriptor();
+ var constraint1 = new ConsumesAttribute("application/json");
+ var actionWithConstraint = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint1, FilterScope.Action) }
+ };
+
+ var constraint2 = new ConsumesAttribute("text/xml");
+ var actionWithConstraint2 = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint2, FilterScope.Action) }
+ };
+
+ var context = new ActionConstraintContext();
+ context.Candidates = new List()
+ {
+ new ActionSelectorCandidate(actionWithConstraint, new [] { constraint1 }),
+ new ActionSelectorCandidate(actionWithConstraint2, new [] { constraint2 }),
+ new ActionSelectorCandidate(actionWithoutConstraint, new List()),
+ };
+
+ context.RouteContext = CreateRouteContext(contentType: contentType);
+
+ // Act & Assert
+ context.CurrentCandidate = context.Candidates[0];
+ Assert.False(constraint1.Accept(context));
+
+ context.CurrentCandidate = context.Candidates[1];
+ Assert.False(constraint2.Accept(context));
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ public void Accept_ForNoRequestType_ReturnsTrueForAllConstraints(string contentType)
+ {
+ // Arrange
+ var constraint1 = new ConsumesAttribute("application/json");
+ var actionWithConstraint = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint1, FilterScope.Action) }
+ };
+
+ var constraint2 = new ConsumesAttribute("text/xml");
+ var actionWithConstraint2 = new ActionDescriptor()
+ {
+ FilterDescriptors =
+ new List() { new FilterDescriptor(constraint2, FilterScope.Action) }
+ };
+
+ var actionWithoutConstraint = new ActionDescriptor();
+
+ var context = new ActionConstraintContext();
+ context.Candidates = new List()
+ {
+ new ActionSelectorCandidate(actionWithConstraint, new [] { constraint1 }),
+ new ActionSelectorCandidate(actionWithConstraint2, new [] { constraint2 }),
+ };
+
+ context.RouteContext = CreateRouteContext(contentType: contentType);
+
+ // Act & Assert
+ context.CurrentCandidate = context.Candidates[0];
+ Assert.True(constraint1.Accept(context));
+ context.CurrentCandidate = context.Candidates[1];
+ Assert.True(constraint2.Accept(context));
+ }
+
+ [Theory]
+ [InlineData("application/xml")]
+ [InlineData("application/custom")]
+ public void OnResourceExecuting_ForNoContentTypeMatch_SetsUnsupportedMediaTypeResult(string contentType)
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.ContentType = contentType;
+ var consumesFilter = new ConsumesAttribute("application/json");
+ var actionWithConstraint = new ActionDescriptor()
+ {
+ ActionConstraints = new List() { consumesFilter },
+ FilterDescriptors =
+ new List() { new FilterDescriptor(consumesFilter, FilterScope.Action) }
+ };
+ var actionContext = new ActionContext(httpContext, new RouteData(), actionWithConstraint);
+
+ var resourceExecutingContext = new ResourceExecutingContext(actionContext, new[] { consumesFilter });
+
+ // Act
+ consumesFilter.OnResourceExecuting(resourceExecutingContext);
+
+ // Assert
+ Assert.NotNull(resourceExecutingContext.Result);
+ Assert.IsType(resourceExecutingContext.Result);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ public void OnResourceExecuting_NullOrEmptyRequestContentType_IsNoOp(string contentType)
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.ContentType = contentType;
+ var consumesFilter = new ConsumesAttribute("application/json");
+ var actionWithConstraint = new ActionDescriptor()
+ {
+ ActionConstraints = new List() { consumesFilter },
+ FilterDescriptors =
+ new List() { new FilterDescriptor(consumesFilter, FilterScope.Action) }
+ };
+ var actionContext = new ActionContext(httpContext, new RouteData(), actionWithConstraint);
+
+ var resourceExecutingContext = new ResourceExecutingContext(actionContext, new[] { consumesFilter });
+
+ // Act
+ consumesFilter.OnResourceExecuting(resourceExecutingContext);
+
+ // Assert
+ Assert.Null(resourceExecutingContext.Result);
+ }
+
+ [Theory]
+ [InlineData("application/xml")]
+ [InlineData("application/json")]
+ public void OnResourceExecuting_ForAContentTypeMatch_IsNoOp(string contentType)
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.ContentType = contentType;
+ var consumesFilter = new ConsumesAttribute("application/json", "application/xml");
+ var actionWithConstraint = new ActionDescriptor()
+ {
+ ActionConstraints = new List() { consumesFilter },
+ FilterDescriptors =
+ new List() { new FilterDescriptor(consumesFilter, FilterScope.Action) }
+ };
+ var actionContext = new ActionContext(httpContext, new RouteData(), actionWithConstraint);
+ var resourceExecutingContext = new ResourceExecutingContext(actionContext, new[] { consumesFilter });
+
+ // Act
+ consumesFilter.OnResourceExecuting(resourceExecutingContext);
+
+ // Assert
+ Assert.Null(resourceExecutingContext.Result);
+ }
+
+ private static RouteContext CreateRouteContext(string contentType = null, object routeValues = null)
+ {
+ var httpContext = new DefaultHttpContext();
+ if (contentType != null)
+ {
+ httpContext.Request.ContentType = contentType;
+ }
+
+ var routeContext = new RouteContext(httpContext);
+ routeContext.RouteData = new RouteData();
+
+ foreach (var kvp in new RouteValueDictionary(routeValues))
+ {
+ routeContext.RouteData.Values.Add(kvp.Key, kvp.Value);
+ }
+
+ return routeContext;
+ }
+
+ public interface ITestConsumeConstraint : IConsumesActionConstraint, IResourceFilter
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ConsumesAttributeTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConsumesAttributeTests.cs
new file mode 100644
index 0000000000..c1739bb4d7
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConsumesAttributeTests.cs
@@ -0,0 +1,161 @@
+// 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.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
+using ActionConstraintsWebSite;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.TestHost;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.FunctionalTests
+{
+ public class ConsumesAttributeTests
+ {
+ private readonly IServiceProvider _provider = TestHelper.CreateServices("ActionConstraintsWebSite");
+ private readonly Action _app = new Startup().Configure;
+
+ [Fact]
+ public async Task NoRequestContentType_SelectsActionWithoutConstraint()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_Company/CreateProduct");
+
+ // Act
+ var response = await client.SendAsync(request);
+ var product = JsonConvert.DeserializeObject(
+ await response.Content.ReadAsStringAsync());
+ // Assert
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ Assert.Null(product);
+ }
+
+ [Fact]
+ public async Task NoRequestContentType_Throws_IfMultipleActionsWithConstraints()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_AmbiguousActions/CreateProduct");
+
+ // Act
+ var response = await client.SendAsync(request);
+ var exception = response.GetServerException();
+
+ // Assert
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ Assert.Equal(typeof(AmbiguousActionException).FullName, exception.ExceptionType);
+ Assert.Equal(
+ "Multiple actions matched. The following actions matched route data and had all constraints "+
+ "satisfied:____ActionConstraintsWebSite.ConsumesAttribute_NoFallBackActionController."+
+ "CreateProduct__ActionConstraintsWebSite.ConsumesAttribute_NoFallBackActionController.CreateProduct",
+ exception.ExceptionMessage);
+ }
+
+ [Fact]
+ public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_PassThrough/CreateProduct");
+
+ // Act
+ var response = await client.SendAsync(request);
+ var product = JsonConvert.DeserializeObject(
+ await response.Content.ReadAsStringAsync());
+ // Assert
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ Assert.Null(product);
+ }
+
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("text/json")]
+ public async Task Selects_Action_BasedOnRequestContentType(string requestContentType)
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ var input = "{SampleString:\""+requestContentType+"\"}";
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_AmbiguousActions/CreateProduct");
+ request.Content = new StringContent(input, Encoding.UTF8, requestContentType);
+ // Act
+ var response = await client.SendAsync(request);
+ var product = JsonConvert.DeserializeObject(
+ await response.Content.ReadAsStringAsync());
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(requestContentType, product.SampleString);
+ }
+
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("text/json")]
+ public async Task ActionLevelAttribute_OveridesClassLevel(string requestContentType)
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ var input = "{SampleString:\"" + requestContentType + "\"}";
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_OverridesBase/CreateProduct");
+ request.Content = new StringContent(input, Encoding.UTF8, requestContentType);
+ var expectedString = "ConsumesAttribute_OverridesBaseController_" + requestContentType;
+
+ // Act
+ var response = await client.SendAsync(request);
+ var product = JsonConvert.DeserializeObject(
+ await response.Content.ReadAsStringAsync());
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(expectedString, product.SampleString);
+ }
+
+ [Fact]
+ public async Task DerivedClassLevelAttribute_OveridesBaseClassLevel()
+ {
+ // Arrange
+ var server = TestServer.Create(_provider, _app);
+ var client = server.CreateClient();
+
+ var input = "" +
+ "application/xml";
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_Overrides/CreateProduct");
+ request.Content = new StringContent(input, Encoding.UTF8, "application/xml");
+ var expectedString = "ConsumesAttribute_OverridesController_application/xml";
+
+ // Act
+ var response = await client.SendAsync(request);
+ var responseString = await response.Content.ReadAsStringAsync();
+ var product = JsonConvert.DeserializeObject(responseString);
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(expectedString, product.SampleString);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
index 502abe42d6..e29d0af3b5 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
@@ -3,6 +3,7 @@
"warningsAsErrors": "true"
},
"dependencies": {
+ "ActionConstraintsWebSite": "1.0.0",
"ActionResultsWebSite": "1.0.0",
"ActivatorWebSite": "1.0.0",
"AddServicesWebSite": "1.0.0",
diff --git a/test/WebSites/ActionConstraintsWebSite/ActionConstraintsWebSite.kproj b/test/WebSites/ActionConstraintsWebSite/ActionConstraintsWebSite.kproj
new file mode 100644
index 0000000000..f22c0494ee
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/ActionConstraintsWebSite.kproj
@@ -0,0 +1,20 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ af210f69-9d31-43af-ac3a-cd366e252218
+
+
+
+
+
+
+ 2.0
+ 41642
+
+
+
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_NoFallBackActionController.cs b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_NoFallBackActionController.cs
new file mode 100644
index 0000000000..c3fb5e1b7a
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_NoFallBackActionController.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNet.Mvc;
+
+namespace ActionConstraintsWebSite
+{
+ [Route("ConsumesAttribute_AmbiguousActions/[action]")]
+ public class ConsumesAttribute_NoFallBackActionController : Controller
+ {
+ [Consumes("application/json", "text/json")]
+ public Product CreateProduct([FromBody] Product_Json jsonInput)
+ {
+ return jsonInput;
+ }
+
+ [Consumes("application/xml")]
+ public Product CreateProduct([FromBody] Product_Xml xmlInput)
+ {
+ return xmlInput;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_OveridesBaseController.cs b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_OveridesBaseController.cs
new file mode 100644
index 0000000000..3bd66da5fc
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_OveridesBaseController.cs
@@ -0,0 +1,26 @@
+// 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 ActionConstraintsWebSite
+{
+ [Consumes("application/json")]
+ public class ConsumesAttribute_OverridesBaseController : Controller
+ {
+ [Consumes("text/json")]
+ public Product CreateProduct([FromBody] Product_Json product)
+ {
+ // should be picked if request content type is application/xml and not application/json.
+ product.SampleString = "ConsumesAttribute_OverridesBaseController_text/json";
+ return product;
+ }
+
+ public virtual IActionResult CreateProduct([FromBody] Product product)
+ {
+ // should be picked if request content type is application/json.
+ product.SampleString = "ConsumesAttribute_OverridesBaseController_application/json";
+ return new ObjectResult(product);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_OveridesController.cs b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_OveridesController.cs
new file mode 100644
index 0000000000..68bf431727
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_OveridesController.cs
@@ -0,0 +1,18 @@
+// 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 ActionConstraintsWebSite
+{
+ [Consumes("application/xml")]
+ public class ConsumesAttribute_OverridesController : ConsumesAttribute_OverridesBaseController
+ {
+ public override IActionResult CreateProduct([FromBody] Product product)
+ {
+ // should be picked if request content type is text/json.
+ product.SampleString = "ConsumesAttribute_OverridesController_application/xml";
+ return new JsonResult(product);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_PassThroughController.cs b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_PassThroughController.cs
new file mode 100644
index 0000000000..c800adc50c
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_PassThroughController.cs
@@ -0,0 +1,18 @@
+// 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 Microsoft.AspNet.Mvc;
+
+namespace ActionConstraintsWebSite
+{
+ [Route("ConsumesAttribute_PassThrough/[action]")]
+ public class ConsumesAttribute_PassThroughController : Controller
+ {
+ [Consumes("application/json")]
+ public Product CreateProduct([FromBody] Product_Json jsonInput)
+ {
+ return jsonInput;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_WithFallbackActionController.cs b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_WithFallbackActionController.cs
new file mode 100644
index 0000000000..454b597643
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Controllers/ConsumesAttribute_WithFallbackActionController.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.Mvc;
+
+namespace ActionConstraintsWebSite
+{
+ [Route("ConsumesAttribute_Company/[action]")]
+ public class ConsumesAttribute_WithFallbackActionController : Controller
+ {
+ [Consumes("application/json")]
+ public Product CreateProduct([FromBody] Product_Json jsonInput)
+ {
+ return jsonInput;
+ }
+
+ [Consumes("application/xml")]
+ public Product CreateProduct([FromBody] Product_Xml xmlInput)
+ {
+ return xmlInput;
+ }
+
+ public Product CreateProduct([FromBody] Product_Text defaultInput)
+ {
+ return defaultInput;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Models/Product.cs b/test/WebSites/ActionConstraintsWebSite/Models/Product.cs
new file mode 100644
index 0000000000..fa66f18ec7
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Models/Product.cs
@@ -0,0 +1,16 @@
+// 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.ComponentModel.DataAnnotations;
+
+namespace ActionConstraintsWebSite
+{
+ public class Product
+ {
+ [Range(10, 100)]
+ public int SampleInt { get; set; }
+
+ [MinLength(15)]
+ public string SampleString { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Models/Product_Json.cs b/test/WebSites/ActionConstraintsWebSite/Models/Product_Json.cs
new file mode 100644
index 0000000000..08c45c8cf2
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Models/Product_Json.cs
@@ -0,0 +1,9 @@
+// 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 ActionConstraintsWebSite
+{
+ public class Product_Json : Product
+ {
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Models/Product_Xml.cs b/test/WebSites/ActionConstraintsWebSite/Models/Product_Xml.cs
new file mode 100644
index 0000000000..88e6806888
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Models/Product_Xml.cs
@@ -0,0 +1,9 @@
+// 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 ActionConstraintsWebSite
+{
+ public class Product_Xml : Product
+ {
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Models/Product_text.cs b/test/WebSites/ActionConstraintsWebSite/Models/Product_text.cs
new file mode 100644
index 0000000000..33b4d0522b
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Models/Product_text.cs
@@ -0,0 +1,9 @@
+// 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 ActionConstraintsWebSite
+{
+ public class Product_Text : Product
+ {
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/Startup.cs b/test/WebSites/ActionConstraintsWebSite/Startup.cs
new file mode 100644
index 0000000000..f01b2f2974
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/Startup.cs
@@ -0,0 +1,36 @@
+// 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.Builder;
+using Microsoft.AspNet.Mvc;
+using Microsoft.AspNet.Routing;
+using Microsoft.Framework.DependencyInjection;
+
+namespace ActionConstraintsWebSite
+{
+ public class Startup
+ {
+ public void Configure(IApplicationBuilder app)
+ {
+ var configuration = app.GetTestConfiguration();
+
+ app.UseServices(services =>
+ {
+ services.AddMvc(configuration);
+ services.Configure(options =>
+ {
+ options.AddXmlDataContractSerializerFormatter();
+ });
+ });
+
+ app.UseErrorReporter();
+
+ app.UseMvc(routes =>
+ {
+ routes.MapRoute(
+ name: "default",
+ template: "{controller}/{action}/{id?}");
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/project.json b/test/WebSites/ActionConstraintsWebSite/project.json
new file mode 100644
index 0000000000..464f3bbc61
--- /dev/null
+++ b/test/WebSites/ActionConstraintsWebSite/project.json
@@ -0,0 +1,19 @@
+{
+ "commands": {
+ "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001",
+ "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000"
+ },
+ "dependencies": {
+ "Kestrel": "1.0.0-*",
+ "Microsoft.AspNet.Mvc": "6.0.0-*",
+ "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0",
+ "Microsoft.AspNet.Server.IIS": "1.0.0-*",
+ "Microsoft.AspNet.Server.WebListener": "1.0.0-*",
+ "Microsoft.AspNet.StaticFiles": "1.0.0-*"
+ },
+ "frameworks": {
+ "aspnet50": { },
+ "aspnetcore50": { }
+ },
+ "webroot": "wwwroot"
+}
\ No newline at end of file
diff --git a/test/WebSites/ActionConstraintsWebSite/wwwroot/readme.md b/test/WebSites/ActionConstraintsWebSite/wwwroot/readme.md
new file mode 100644
index 0000000000..1bd9fa1ec3
Binary files /dev/null and b/test/WebSites/ActionConstraintsWebSite/wwwroot/readme.md differ