diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs
index c3c240e63e..7d63adcabf 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs
@@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc
@@ -24,7 +26,8 @@ namespace Microsoft.AspNetCore.Mvc
Attribute,
IResourceFilter,
IConsumesActionConstraint,
- IApiRequestMetadataProvider
+ IApiRequestMetadataProvider,
+ IConsumesEndpointConstraint
{
public static readonly int ConsumesActionConstraintOrder = 200;
@@ -55,6 +58,11 @@ namespace Microsoft.AspNetCore.Mvc
///
int IActionConstraint.Order => ConsumesActionConstraintOrder;
+ // The value used is a non default value so that it avoids getting mixed with other endpoint constraints
+ // with default order.
+ ///
+ int IEndpointConstraint.Order => ConsumesActionConstraintOrder;
+
///
/// Gets or sets the supported request content types. Used to select an action when there would otherwise be
/// multiple matches.
@@ -184,6 +192,83 @@ namespace Microsoft.AspNetCore.Mvc
return true;
}
+ ///
+ public bool Accept(EndpointConstraintContext context)
+ {
+ // If this constraint is not closest to the endpoint, it will be skipped.
+ if (!IsApplicable(context.CurrentCandidate.Endpoint))
+ {
+ // 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 endpoint.
+ return true;
+ }
+
+ var requestContentType = context.HttpContext.Request.ContentType;
+
+ // 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 endpoints with consumes endpoint constraints this should result in ambiguous exception
+ // unless there is another endpoint without a consumes constraint.
+ if (requestContentType == null)
+ {
+ var isEndpointWithoutConsumeConstraintPresent = context.Candidates.Any(
+ candidate => candidate.Constraints == null ||
+ !candidate.Constraints.Any(constraint => constraint is IConsumesEndpointConstraint));
+
+ return !isEndpointWithoutConsumeConstraintPresent;
+ }
+
+ // Confirm the request's content type is more specific than (a media type this endpoint supports e.g. OK
+ // if client sent "text/plain" data and this endpoint supports "text/*".
+ if (IsSubsetOfAnyContentType(requestContentType))
+ {
+ return true;
+ }
+
+ var firstCandidate = context.Candidates[0];
+ if (firstCandidate.Endpoint != context.CurrentCandidate.Endpoint)
+ {
+ // 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 IConsumesEndpointConstraints 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.Endpoint == firstCandidate.Endpoint)
+ {
+ continue;
+ }
+
+ var tempContext = new EndpointConstraintContext()
+ {
+ Candidates = context.Candidates,
+ HttpContext = context.HttpContext,
+ CurrentCandidate = candidate
+ };
+
+ if (candidate.Constraints == null || candidate.Constraints.Count == 0 ||
+ candidate.Constraints.Any(constraint => constraint is IConsumesEndpointConstraint &&
+ 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
@@ -193,7 +278,17 @@ namespace Microsoft.AspNetCore.Mvc
// 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 bool IsApplicable(Endpoint endpoint)
+ {
+ // 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 endpoint.Metadata.Last(
+ metadata => metadata is IConsumesEndpointConstraint) == this;
}
private MediaTypeCollection GetContentTypes(string firstArg, string[] args)
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/IConsumesActionConstraint.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/IConsumesActionConstraint.cs
index b004cbdcf3..2806bd496d 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/IConsumesActionConstraint.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/IConsumesActionConstraint.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.AspNetCore.Mvc.ActionConstraints;
+using Microsoft.AspNetCore.Routing.EndpointConstraints;
namespace Microsoft.AspNetCore.Mvc.Internal
{
@@ -12,4 +13,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public interface IConsumesActionConstraint : IActionConstraint
{
}
+
+ ///
+ /// An constraint that identifies a type which can be used to select an action
+ /// based on incoming request.
+ ///
+ public interface IConsumesEndpointConstraint : IEndpointConstraint
+ {
+ }
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs
index a2738ed9a2..83bae7b4b0 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs
@@ -12,6 +12,8 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.EndpointConstraints;
+using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@@ -80,7 +82,7 @@ namespace Microsoft.AspNetCore.Mvc
[InlineData("application/json")]
[InlineData("application/json;Parameter1=12")]
[InlineData("text/xml")]
- public void Accept_MatchesForMachingRequestContentType(string contentType)
+ public void ActionConstraint_Accept_MatchesForMachingRequestContentType(string contentType)
{
// Arrange
var constraint = new ConsumesAttribute("application/json", "text/xml");
@@ -104,7 +106,7 @@ namespace Microsoft.AspNetCore.Mvc
}
[Fact]
- public void Accept_TheFirstCandidateReturnsFalse_IfALaterOneMatches()
+ public void ActionConstraint_Accept_TheFirstCandidateReturnsFalse_IfALaterOneMatches()
{
// Arrange
var constraint1 = new ConsumesAttribute("application/json", "text/xml");
@@ -114,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc
new List() { new FilterDescriptor(constraint1, FilterScope.Action) }
};
- var constraint2 = new Mock();
+ var constraint2 = new Mock();
var action2 = new ActionDescriptor()
{
FilterDescriptors =
@@ -142,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc
[InlineData("application/custom")]
[InlineData("")]
[InlineData(null)]
- public void Accept_ForNoMatchingCandidates_SelectsTheFirstCandidate(string contentType)
+ public void ActionConstraint_Accept_ForNoMatchingCandidates_SelectsTheFirstCandidate(string contentType)
{
// Arrange
var constraint1 = new ConsumesAttribute("application/json", "text/xml");
@@ -152,7 +154,7 @@ namespace Microsoft.AspNetCore.Mvc
new List() { new FilterDescriptor(constraint1, FilterScope.Action) }
};
- var constraint2 = new Mock();
+ var constraint2 = new Mock();
var action2 = new ActionDescriptor()
{
FilterDescriptors =
@@ -179,7 +181,7 @@ namespace Microsoft.AspNetCore.Mvc
[Theory]
[InlineData("")]
[InlineData(null)]
- public void Accept_ForNoRequestType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
+ public void ActionConstraint_Accept_ForNoRequestType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
{
// Arrange
var constraint1 = new ConsumesAttribute("application/json");
@@ -219,7 +221,7 @@ namespace Microsoft.AspNetCore.Mvc
[InlineData("application/xml")]
[InlineData("application/custom")]
[InlineData("invalid/invalid")]
- public void Accept_UnrecognizedMediaType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
+ public void ActionConstraint_Accept_UnrecognizedMediaType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
{
// Arrange
var actionWithoutConstraint = new ActionDescriptor();
@@ -258,7 +260,7 @@ namespace Microsoft.AspNetCore.Mvc
[Theory]
[InlineData("")]
[InlineData(null)]
- public void Accept_ForNoRequestType_ReturnsTrueForAllConstraints(string contentType)
+ public void ActionConstraint_Accept_ForNoRequestType_ReturnsTrueForAllConstraints(string contentType)
{
// Arrange
var constraint1 = new ConsumesAttribute("application/json");
@@ -293,6 +295,193 @@ namespace Microsoft.AspNetCore.Mvc
Assert.True(constraint2.Accept(context));
}
+ private MatcherEndpoint CreateEndpoint(params IEndpointConstraint[] constraints)
+ {
+ EndpointMetadataCollection endpointMetadata = new EndpointMetadataCollection(constraints);
+
+ return new MatcherEndpoint(
+ (r) => null,
+ "",
+ new RouteValueDictionary(),
+ new RouteValueDictionary(),
+ 0,
+ endpointMetadata,
+ "");
+ }
+
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("application/json;Parameter1=12")]
+ [InlineData("text/xml")]
+ public void EndpointConstraint_Accept_MatchesForMachingRequestContentType(string contentType)
+ {
+ // Arrange
+ var constraint = new ConsumesAttribute("application/json", "text/xml");
+ var endpoint = CreateEndpoint(constraint);
+
+ var context = new EndpointConstraintContext();
+ context.Candidates = new List()
+ {
+ new EndpointSelectorCandidate(endpoint, new [] { constraint }),
+ };
+
+ context.CurrentCandidate = context.Candidates[0];
+ context.HttpContext = CreateHttpContext(contentType: contentType);
+
+ // Act & Assert
+ Assert.True(constraint.Accept(context));
+ }
+
+ [Fact]
+ public void EndpointConstraint_Accept_TheFirstCandidateReturnsFalse_IfALaterOneMatches()
+ {
+ // Arrange
+ var constraint1 = new ConsumesAttribute("application/json", "text/xml");
+ var endpoint1 = CreateEndpoint(constraint1);
+
+ var constraint2 = new Mock();
+ var endpoint2 = CreateEndpoint(constraint2.Object);
+
+ constraint2.Setup(o => o.Accept(It.IsAny()))
+ .Returns(true);
+
+ var context = new EndpointConstraintContext();
+ context.Candidates = new List()
+ {
+ new EndpointSelectorCandidate(endpoint1, new [] { constraint1 }),
+ new EndpointSelectorCandidate(endpoint2, new [] { constraint2.Object }),
+ };
+
+ context.CurrentCandidate = context.Candidates[0];
+ context.HttpContext = CreateHttpContext(contentType: "application/custom");
+
+ // Act & Assert
+ Assert.False(constraint1.Accept(context));
+ }
+
+ [Theory]
+ [InlineData("application/custom")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void EndpointConstraint_Accept_ForNoMatchingCandidates_SelectsTheFirstCandidate(string contentType)
+ {
+ // Arrange
+ var constraint1 = new ConsumesAttribute("application/json", "text/xml");
+ var endpoint1 = CreateEndpoint(constraint1);
+
+ var constraint2 = new Mock();
+ var endpoint2 = CreateEndpoint(constraint2.Object);
+
+ constraint2.Setup(o => o.Accept(It.IsAny()))
+ .Returns(false);
+
+ var context = new EndpointConstraintContext();
+ context.Candidates = new List()
+ {
+ new EndpointSelectorCandidate(endpoint1, new [] { constraint1 }),
+ new EndpointSelectorCandidate(endpoint2, new [] { constraint2.Object }),
+ };
+
+ context.CurrentCandidate = context.Candidates[0];
+ context.HttpContext = CreateHttpContext(contentType: contentType);
+
+ // Act & Assert
+ Assert.True(constraint1.Accept(context));
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ public void EndpointConstraint_Accept_ForNoRequestType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
+ {
+ // Arrange
+ var constraint1 = new ConsumesAttribute("application/json");
+ var endpointWithConstraint = CreateEndpoint(constraint1);
+
+ var constraint2 = new ConsumesAttribute("text/xml");
+ var endpointWithConstraint2 = CreateEndpoint(constraint2);
+
+ var endpointWithoutConstraint = CreateEndpoint();
+
+ var context = new EndpointConstraintContext();
+ context.Candidates = new List()
+ {
+ new EndpointSelectorCandidate(endpointWithConstraint, new [] { constraint1 }),
+ new EndpointSelectorCandidate(endpointWithConstraint2, new [] { constraint2 }),
+ new EndpointSelectorCandidate(endpointWithoutConstraint, new List()),
+ };
+
+ context.HttpContext = CreateHttpContext(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 EndpointConstraint_Accept_UnrecognizedMediaType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
+ {
+ // Arrange
+ var endpointWithoutConstraint = CreateEndpoint();
+ var constraint1 = new ConsumesAttribute("application/json");
+ var endpointWithConstraint = CreateEndpoint(constraint1);
+
+ var constraint2 = new ConsumesAttribute("text/xml");
+ var endpointWithConstraint2 = CreateEndpoint(constraint2);
+
+ var context = new EndpointConstraintContext();
+ context.Candidates = new List()
+ {
+ new EndpointSelectorCandidate(endpointWithConstraint, new [] { constraint1 }),
+ new EndpointSelectorCandidate(endpointWithConstraint2, new [] { constraint2 }),
+ new EndpointSelectorCandidate(endpointWithoutConstraint, new List()),
+ };
+
+ context.HttpContext = CreateHttpContext(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 EndpointConstraint_Accept_ForNoRequestType_ReturnsTrueForAllConstraints(string contentType)
+ {
+ // Arrange
+ var constraint1 = new ConsumesAttribute("application/json");
+ var endpointWithConstraint = CreateEndpoint(constraint1);
+
+ var constraint2 = new ConsumesAttribute("text/xml");
+ var endpointWithConstraint2 = CreateEndpoint(constraint2);
+
+ var endpointWithoutConstraint = CreateEndpoint();
+
+ var context = new EndpointConstraintContext();
+ context.Candidates = new List()
+ {
+ new EndpointSelectorCandidate(endpointWithConstraint, new [] { constraint1 }),
+ new EndpointSelectorCandidate(endpointWithConstraint2, new [] { constraint2 }),
+ };
+
+ context.HttpContext = CreateHttpContext(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")]
@@ -404,11 +593,7 @@ namespace Microsoft.AspNetCore.Mvc
private static RouteContext CreateRouteContext(string contentType = null, object routeValues = null)
{
- var httpContext = new DefaultHttpContext();
- if (contentType != null)
- {
- httpContext.Request.ContentType = contentType;
- }
+ var httpContext = CreateHttpContext(contentType);
var routeContext = new RouteContext(httpContext);
routeContext.RouteData = new RouteData();
@@ -421,7 +606,22 @@ namespace Microsoft.AspNetCore.Mvc
return routeContext;
}
- public interface ITestConsumeConstraint : IConsumesActionConstraint, IResourceFilter
+ private static HttpContext CreateHttpContext(string contentType = null, object routeValues = null)
+ {
+ var httpContext = new DefaultHttpContext();
+ if (contentType != null)
+ {
+ httpContext.Request.ContentType = contentType;
+ }
+
+ return httpContext;
+ }
+
+ public interface ITestActionConsumeConstraint : IConsumesActionConstraint, IResourceFilter
+ {
+ }
+
+ public interface ITestEndpointConsumeConstraint : IConsumesEndpointConstraint, IResourceFilter
{
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeDispatchingTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeDispatchingTests.cs
new file mode 100644
index 0000000000..ced707f674
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeDispatchingTests.cs
@@ -0,0 +1,13 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Mvc.FunctionalTests
+{
+ public class ConsumesAttributeDispatchingTests : ConsumesAttributeTestsBase
+ {
+ public ConsumesAttributeDispatchingTests(MvcTestFixture fixture)
+ : base(fixture)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs
index 181094b715..a8db21d806 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs
@@ -1,169 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System.Net;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-using BasicWebSite.Models;
-using Microsoft.AspNetCore.Mvc.Infrastructure;
-using Microsoft.AspNetCore.Testing.xunit;
-using Newtonsoft.Json;
-using Xunit;
-
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
- public class ConsumesAttributeTests : IClassFixture>
+ public class ConsumesAttributeTests : ConsumesAttributeTestsBase
{
public ConsumesAttributeTests(MvcTestFixture fixture)
+ : base(fixture)
{
- Client = fixture.CreateDefaultClient();
- }
-
- public HttpClient Client { get; }
-
- [Fact]
- public async Task NoRequestContentType_SelectsActionWithoutConstraint()
- {
- // Arrange
- var request = new HttpRequestMessage(
- HttpMethod.Post,
- "http://localhost/ConsumesAttribute_Company/CreateProduct");
-
- // Act
- var response = await Client.SendAsync(request);
- var body = await response.Content.ReadAsStringAsync();
-
- // Assert
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- Assert.Equal("CreateProduct_Product_Text", body);
- }
-
- [Fact]
- public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
- {
- // Arrange
- var request = new HttpRequestMessage(
- HttpMethod.Post,
- "http://localhost/ConsumesAttribute_PassThrough/CreateProduct");
-
- // Act
- var response = await Client.SendAsync(request);
- var body = await response.Content.ReadAsStringAsync();
-
- // Assert
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- Assert.Equal("ConsumesAttribute_PassThrough_Product_Json", body);
- }
-
- [Theory]
- [InlineData("application/json")]
- [InlineData("text/json")]
- public async Task Selects_Action_BasedOnRequestContentType(string requestContentType)
- {
- // Arrange
- 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 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);
- }
-
- [ConditionalFact]
- // Mono issue - https://github.com/aspnet/External/issues/18
- [FrameworkSkipCondition(RuntimeFrameworks.Mono)]
- public async Task DerivedClassLevelAttribute_OveridesBaseClassLevel()
- {
- // Arrange
- 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);
- }
-
- [Fact]
- public async Task JsonSyntaxSuffix_SelectsActionConsumingJson()
- {
- // Arrange
- var input = "{SampleString:\"some input\"}";
- var request = new HttpRequestMessage(
- HttpMethod.Post,
- "http://localhost/ConsumesAttribute_MediaTypeSuffix/CreateProduct");
- request.Content = new StringContent(input, Encoding.UTF8, "application/vnd.example+json");
-
- // Act
- var response = await Client.SendAsync(request);
- var product = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
-
- // Assert
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- Assert.Equal("Read from JSON: some input", product.SampleString);
- }
-
- [ConditionalFact]
- // Mono issue - https://github.com/aspnet/External/issues/18
- [FrameworkSkipCondition(RuntimeFrameworks.Mono)]
- public async Task XmlSyntaxSuffix_SelectsActionConsumingXml()
- {
- // Arrange
- var input = "" +
- "some input";
- var request = new HttpRequestMessage(
- HttpMethod.Post,
- "http://localhost/ConsumesAttribute_MediaTypeSuffix/CreateProduct");
- request.Content = new StringContent(input, Encoding.UTF8, "application/vnd.example+xml");
-
- // Act
- var response = await Client.SendAsync(request);
- var product = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
-
- // Assert
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- Assert.Equal("Read from XML: some input", product.SampleString);
}
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs
new file mode 100644
index 0000000000..9a414488b1
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs
@@ -0,0 +1,175 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using BasicWebSite.Models;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Testing.xunit;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.FunctionalTests
+{
+ public abstract class ConsumesAttributeTestsBase : IClassFixture> where TStartup : class
+ {
+ protected ConsumesAttributeTestsBase(MvcTestFixture fixture)
+ {
+ var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
+ Client = factory.CreateDefaultClient();
+ }
+
+ private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
+ builder.UseStartup();
+
+ public HttpClient Client { get; }
+
+ [Fact]
+ public async Task NoRequestContentType_SelectsActionWithoutConstraint()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_Company/CreateProduct");
+
+ // Act
+ var response = await Client.SendAsync(request);
+ var body = await response.Content.ReadAsStringAsync();
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("CreateProduct_Product_Text", body);
+ }
+
+ [Fact]
+ public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_PassThrough/CreateProduct");
+
+ // Act
+ var response = await Client.SendAsync(request);
+ var body = await response.Content.ReadAsStringAsync();
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("ConsumesAttribute_PassThrough_Product_Json", body);
+ }
+
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("text/json")]
+ public async Task Selects_Action_BasedOnRequestContentType(string requestContentType)
+ {
+ // Arrange
+ 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 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);
+ }
+
+ [ConditionalFact]
+ // Mono issue - https://github.com/aspnet/External/issues/18
+ [FrameworkSkipCondition(RuntimeFrameworks.Mono)]
+ public async Task DerivedClassLevelAttribute_OveridesBaseClassLevel()
+ {
+ // Arrange
+ 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);
+ }
+
+ [Fact]
+ public async Task JsonSyntaxSuffix_SelectsActionConsumingJson()
+ {
+ // Arrange
+ var input = "{SampleString:\"some input\"}";
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_MediaTypeSuffix/CreateProduct");
+ request.Content = new StringContent(input, Encoding.UTF8, "application/vnd.example+json");
+
+ // Act
+ var response = await Client.SendAsync(request);
+ var product = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Read from JSON: some input", product.SampleString);
+ }
+
+ [ConditionalFact]
+ // Mono issue - https://github.com/aspnet/External/issues/18
+ [FrameworkSkipCondition(RuntimeFrameworks.Mono)]
+ public async Task XmlSyntaxSuffix_SelectsActionConsumingXml()
+ {
+ // Arrange
+ var input = "" +
+ "some input";
+ var request = new HttpRequestMessage(
+ HttpMethod.Post,
+ "http://localhost/ConsumesAttribute_MediaTypeSuffix/CreateProduct");
+ request.Content = new StringContent(input, Encoding.UTF8, "application/vnd.example+xml");
+
+ // Act
+ var response = await Client.SendAsync(request);
+ var product = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Read from XML: some input", product.SampleString);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/BasicWebSite/StartupWithDispatching.cs b/test/WebSites/BasicWebSite/StartupWithDispatching.cs
new file mode 100644
index 0000000000..4c5ed0dc1e
--- /dev/null
+++ b/test/WebSites/BasicWebSite/StartupWithDispatching.cs
@@ -0,0 +1,37 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BasicWebSite
+{
+ public class StartupWithDispatching
+ {
+ // Set up application services
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddDispatcher();
+
+ services.AddMvc()
+ .SetCompatibilityVersion(CompatibilityVersion.Latest)
+ .AddXmlDataContractSerializerFormatters();
+
+ services.ConfigureBaseWebSiteAuthPolicies();
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseDispatcher();
+
+ app.UseMvcWithEndpoint(routes =>
+ {
+ routes.MapEndpoint(
+ "ActionAsMethod",
+ "{controller}/{action}",
+ defaults: new { controller = "Home", action = "Index" });
+ });
+ }
+ }
+}
\ No newline at end of file