Add Consumes endpoint constraint (#8053)

This commit is contained in:
James Newton-King 2018-07-12 14:58:57 +12:00 committed by GitHub
parent a6987cc1cd
commit 3154979189
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 546 additions and 173 deletions

View File

@ -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)

View File

@ -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
{
}
}

View File

@ -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
{
}
}

View File

@ -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)
{
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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" });
});
}
}
}