Add Consumes endpoint constraint (#8053)
This commit is contained in:
parent
a6987cc1cd
commit
3154979189
|
|
@ -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
|
|||
/// <inheritdoc />
|
||||
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.
|
||||
/// <inheritdoc />
|
||||
int IEndpointConstraint.Order => ConsumesActionConstraintOrder;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IEndpointConstraint"/> constraint that identifies a type which can be used to select an action
|
||||
/// based on incoming request.
|
||||
/// </summary>
|
||||
public interface IConsumesEndpointConstraint : IEndpointConstraint
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
|
||||
};
|
||||
|
||||
var constraint2 = new Mock<ITestConsumeConstraint>();
|
||||
var constraint2 = new Mock<ITestActionConsumeConstraint>();
|
||||
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<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
|
||||
};
|
||||
|
||||
var constraint2 = new Mock<ITestConsumeConstraint>();
|
||||
var constraint2 = new Mock<ITestActionConsumeConstraint>();
|
||||
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<EndpointSelectorCandidate>()
|
||||
{
|
||||
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<ITestEndpointConsumeConstraint>();
|
||||
var endpoint2 = CreateEndpoint(constraint2.Object);
|
||||
|
||||
constraint2.Setup(o => o.Accept(It.IsAny<EndpointConstraintContext>()))
|
||||
.Returns(true);
|
||||
|
||||
var context = new EndpointConstraintContext();
|
||||
context.Candidates = new List<EndpointSelectorCandidate>()
|
||||
{
|
||||
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<ITestEndpointConsumeConstraint>();
|
||||
var endpoint2 = CreateEndpoint(constraint2.Object);
|
||||
|
||||
constraint2.Setup(o => o.Accept(It.IsAny<EndpointConstraintContext>()))
|
||||
.Returns(false);
|
||||
|
||||
var context = new EndpointConstraintContext();
|
||||
context.Candidates = new List<EndpointSelectorCandidate>()
|
||||
{
|
||||
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<EndpointSelectorCandidate>()
|
||||
{
|
||||
new EndpointSelectorCandidate(endpointWithConstraint, new [] { constraint1 }),
|
||||
new EndpointSelectorCandidate(endpointWithConstraint2, new [] { constraint2 }),
|
||||
new EndpointSelectorCandidate(endpointWithoutConstraint, new List<IEndpointConstraint>()),
|
||||
};
|
||||
|
||||
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<EndpointSelectorCandidate>()
|
||||
{
|
||||
new EndpointSelectorCandidate(endpointWithConstraint, new [] { constraint1 }),
|
||||
new EndpointSelectorCandidate(endpointWithConstraint2, new [] { constraint2 }),
|
||||
new EndpointSelectorCandidate(endpointWithoutConstraint, new List<IEndpointConstraint>()),
|
||||
};
|
||||
|
||||
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<EndpointSelectorCandidate>()
|
||||
{
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BasicWebSite.StartupWithDispatching>
|
||||
{
|
||||
public ConsumesAttributeDispatchingTests(MvcTestFixture<BasicWebSite.StartupWithDispatching> fixture)
|
||||
: base(fixture)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MvcTestFixture<BasicWebSite.Startup>>
|
||||
public class ConsumesAttributeTests : ConsumesAttributeTestsBase<BasicWebSite.Startup>
|
||||
{
|
||||
public ConsumesAttributeTests(MvcTestFixture<BasicWebSite.Startup> 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<Product>(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<Product>(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 = "<Product xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" " +
|
||||
"xmlns=\"http://schemas.datacontract.org/2004/07/BasicWebSite.Models\">" +
|
||||
"<SampleString>application/xml</SampleString></Product>";
|
||||
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<Product>(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<Product>(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 = "<Product xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" " +
|
||||
"xmlns=\"http://schemas.datacontract.org/2004/07/BasicWebSite.Models\">" +
|
||||
"<SampleString>some input</SampleString></Product>";
|
||||
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<Product>(await response.Content.ReadAsStringAsync());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Read from XML: some input", product.SampleString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TStartup> : IClassFixture<MvcTestFixture<TStartup>> where TStartup : class
|
||||
{
|
||||
protected ConsumesAttributeTestsBase(MvcTestFixture<TStartup> fixture)
|
||||
{
|
||||
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
|
||||
Client = factory.CreateDefaultClient();
|
||||
}
|
||||
|
||||
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
|
||||
builder.UseStartup<TStartup>();
|
||||
|
||||
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<Product>(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<Product>(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 = "<Product xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" " +
|
||||
"xmlns=\"http://schemas.datacontract.org/2004/07/BasicWebSite.Models\">" +
|
||||
"<SampleString>application/xml</SampleString></Product>";
|
||||
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<Product>(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<Product>(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 = "<Product xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\" " +
|
||||
"xmlns=\"http://schemas.datacontract.org/2004/07/BasicWebSite.Models\">" +
|
||||
"<SampleString>some input</SampleString></Product>";
|
||||
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<Product>(await response.Content.ReadAsStringAsync());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("Read from XML: some input", product.SampleString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue