Consumes endpoint constraint (#8057)
This commit is contained in:
parent
c367e1d681
commit
a67d9363e2
|
|
@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
@ -24,7 +26,8 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
Attribute,
|
Attribute,
|
||||||
IResourceFilter,
|
IResourceFilter,
|
||||||
IConsumesActionConstraint,
|
IConsumesActionConstraint,
|
||||||
IApiRequestMetadataProvider
|
IApiRequestMetadataProvider,
|
||||||
|
IConsumesEndpointConstraint
|
||||||
{
|
{
|
||||||
public static readonly int ConsumesActionConstraintOrder = 200;
|
public static readonly int ConsumesActionConstraintOrder = 200;
|
||||||
|
|
||||||
|
|
@ -55,6 +58,11 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
int IActionConstraint.Order => ConsumesActionConstraintOrder;
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the supported request content types. Used to select an action when there would otherwise be
|
/// Gets or sets the supported request content types. Used to select an action when there would otherwise be
|
||||||
/// multiple matches.
|
/// multiple matches.
|
||||||
|
|
@ -184,6 +192,83 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
return true;
|
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)
|
private bool IsApplicable(ActionDescriptor actionDescriptor)
|
||||||
{
|
{
|
||||||
// If there are multiple IConsumeActionConstraints which are defined at the class and
|
// 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.
|
// closest to the action), we apply this constraint only if there is no IConsumeActionConstraint after this.
|
||||||
return actionDescriptor.FilterDescriptors.Last(
|
return actionDescriptor.FilterDescriptors.Last(
|
||||||
filter => filter.Filter is IConsumesActionConstraint).Filter == this;
|
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)
|
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.
|
// 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.Mvc.ActionConstraints;
|
||||||
|
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
|
|
@ -12,4 +13,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
public interface IConsumesActionConstraint : IActionConstraint
|
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.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||||
|
using Microsoft.AspNetCore.Routing.Matchers;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -80,7 +82,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
[InlineData("application/json")]
|
[InlineData("application/json")]
|
||||||
[InlineData("application/json;Parameter1=12")]
|
[InlineData("application/json;Parameter1=12")]
|
||||||
[InlineData("text/xml")]
|
[InlineData("text/xml")]
|
||||||
public void Accept_MatchesForMachingRequestContentType(string contentType)
|
public void ActionConstraint_Accept_MatchesForMachingRequestContentType(string contentType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var constraint = new ConsumesAttribute("application/json", "text/xml");
|
var constraint = new ConsumesAttribute("application/json", "text/xml");
|
||||||
|
|
@ -104,7 +106,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Accept_TheFirstCandidateReturnsFalse_IfALaterOneMatches()
|
public void ActionConstraint_Accept_TheFirstCandidateReturnsFalse_IfALaterOneMatches()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var constraint1 = new ConsumesAttribute("application/json", "text/xml");
|
var constraint1 = new ConsumesAttribute("application/json", "text/xml");
|
||||||
|
|
@ -114,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
|
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
|
||||||
};
|
};
|
||||||
|
|
||||||
var constraint2 = new Mock<ITestConsumeConstraint>();
|
var constraint2 = new Mock<ITestActionConsumeConstraint>();
|
||||||
var action2 = new ActionDescriptor()
|
var action2 = new ActionDescriptor()
|
||||||
{
|
{
|
||||||
FilterDescriptors =
|
FilterDescriptors =
|
||||||
|
|
@ -142,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
[InlineData("application/custom")]
|
[InlineData("application/custom")]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
[InlineData(null)]
|
[InlineData(null)]
|
||||||
public void Accept_ForNoMatchingCandidates_SelectsTheFirstCandidate(string contentType)
|
public void ActionConstraint_Accept_ForNoMatchingCandidates_SelectsTheFirstCandidate(string contentType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var constraint1 = new ConsumesAttribute("application/json", "text/xml");
|
var constraint1 = new ConsumesAttribute("application/json", "text/xml");
|
||||||
|
|
@ -152,7 +154,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
|
new List<FilterDescriptor>() { new FilterDescriptor(constraint1, FilterScope.Action) }
|
||||||
};
|
};
|
||||||
|
|
||||||
var constraint2 = new Mock<ITestConsumeConstraint>();
|
var constraint2 = new Mock<ITestActionConsumeConstraint>();
|
||||||
var action2 = new ActionDescriptor()
|
var action2 = new ActionDescriptor()
|
||||||
{
|
{
|
||||||
FilterDescriptors =
|
FilterDescriptors =
|
||||||
|
|
@ -179,7 +181,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
[InlineData(null)]
|
[InlineData(null)]
|
||||||
public void Accept_ForNoRequestType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
|
public void ActionConstraint_Accept_ForNoRequestType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var constraint1 = new ConsumesAttribute("application/json");
|
var constraint1 = new ConsumesAttribute("application/json");
|
||||||
|
|
@ -219,7 +221,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
[InlineData("application/xml")]
|
[InlineData("application/xml")]
|
||||||
[InlineData("application/custom")]
|
[InlineData("application/custom")]
|
||||||
[InlineData("invalid/invalid")]
|
[InlineData("invalid/invalid")]
|
||||||
public void Accept_UnrecognizedMediaType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
|
public void ActionConstraint_Accept_UnrecognizedMediaType_SelectsTheCandidateWithoutConstraintIfPresent(string contentType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionWithoutConstraint = new ActionDescriptor();
|
var actionWithoutConstraint = new ActionDescriptor();
|
||||||
|
|
@ -258,7 +260,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
[InlineData(null)]
|
[InlineData(null)]
|
||||||
public void Accept_ForNoRequestType_ReturnsTrueForAllConstraints(string contentType)
|
public void ActionConstraint_Accept_ForNoRequestType_ReturnsTrueForAllConstraints(string contentType)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var constraint1 = new ConsumesAttribute("application/json");
|
var constraint1 = new ConsumesAttribute("application/json");
|
||||||
|
|
@ -293,6 +295,193 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
Assert.True(constraint2.Accept(context));
|
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]
|
[Theory]
|
||||||
[InlineData("application/xml")]
|
[InlineData("application/xml")]
|
||||||
[InlineData("application/custom")]
|
[InlineData("application/custom")]
|
||||||
|
|
@ -404,11 +593,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
||||||
private static RouteContext CreateRouteContext(string contentType = null, object routeValues = null)
|
private static RouteContext CreateRouteContext(string contentType = null, object routeValues = null)
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = CreateHttpContext(contentType);
|
||||||
if (contentType != null)
|
|
||||||
{
|
|
||||||
httpContext.Request.ContentType = contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
var routeContext = new RouteContext(httpContext);
|
var routeContext = new RouteContext(httpContext);
|
||||||
routeContext.RouteData = new RouteData();
|
routeContext.RouteData = new RouteData();
|
||||||
|
|
@ -421,7 +606,22 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
return routeContext;
|
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.
|
// 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.
|
// 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
|
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
{
|
{
|
||||||
public class ConsumesAttributeTests : IClassFixture<MvcTestFixture<BasicWebSite.Startup>>
|
public class ConsumesAttributeTests : ConsumesAttributeTestsBase<BasicWebSite.Startup>
|
||||||
{
|
{
|
||||||
public ConsumesAttributeTests(MvcTestFixture<BasicWebSite.Startup> fixture)
|
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