Use MatcherPolicy for Consumes
This commit is contained in:
parent
44f5b54f5f
commit
2b289d2f2c
|
|
@ -175,6 +175,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Transient<IActionConstraintProvider, DefaultActionConstraintProvider>());
|
services.TryAddEnumerable(ServiceDescriptor.Transient<IActionConstraintProvider, DefaultActionConstraintProvider>());
|
||||||
|
|
||||||
// Policies for Endpoints
|
// Policies for Endpoints
|
||||||
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, ConsumesMatcherPolicy>());
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, ActionConstraintMatcherPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, ActionConstraintMatcherPolicy>());
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Matching;
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
using Microsoft.AspNetCore.Routing.Metadata;
|
using Microsoft.AspNetCore.Routing.Metadata;
|
||||||
|
|
@ -348,6 +349,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
metadata.Add(new HttpMethodMetadata(httpMethodActionConstraint.HttpMethods));
|
metadata.Add(new HttpMethodMetadata(httpMethodActionConstraint.HttpMethods));
|
||||||
}
|
}
|
||||||
|
else if (actionConstraint is ConsumesAttribute consumesAttribute &&
|
||||||
|
!metadata.OfType<ConsumesMetadata>().Any())
|
||||||
|
{
|
||||||
|
metadata.Add(new ConsumesMetadata(consumesAttribute.ContentTypes.ToArray()));
|
||||||
|
}
|
||||||
else if (!metadata.Contains(actionConstraint))
|
else if (!metadata.Contains(actionConstraint))
|
||||||
{
|
{
|
||||||
// The constraint might have been added earlier, e.g. it is also a filter descriptor
|
// The constraint might have been added earlier, e.g. it is also a filter descriptor
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
|
{
|
||||||
|
internal class ConsumesMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
|
||||||
|
{
|
||||||
|
internal const string Http415EndpointDisplayName = "415 HTTP Unsupported Media Type";
|
||||||
|
internal const string AnyContentType = "*/*";
|
||||||
|
|
||||||
|
// Run after HTTP methods, but before 'default'.
|
||||||
|
public override int Order { get; } = -100;
|
||||||
|
|
||||||
|
public IComparer<Endpoint> Comparer { get; } = new ConsumesMetadataEndpointComparer();
|
||||||
|
|
||||||
|
public bool AppliesToNode(IReadOnlyList<Endpoint> endpoints)
|
||||||
|
{
|
||||||
|
if (endpoints == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(endpoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints.Any(e => e.Metadata.GetMetadata<IConsumesMetadata>()?.ContentTypes.Count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
|
||||||
|
{
|
||||||
|
if (endpoints == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(endpoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The algorithm here is designed to be preserve the order of the endpoints
|
||||||
|
// while also being relatively simple. Preserving order is important.
|
||||||
|
|
||||||
|
// First, build a dictionary of all of the content-type patterns that are included
|
||||||
|
// at this node.
|
||||||
|
//
|
||||||
|
// For now we're just building up the set of keys. We don't add any endpoints
|
||||||
|
// to lists now because we don't want ordering problems.
|
||||||
|
var edges = new Dictionary<string, List<Endpoint>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (var i = 0; i < endpoints.Count; i++)
|
||||||
|
{
|
||||||
|
var endpoint = endpoints[i];
|
||||||
|
var contentTypes = endpoint.Metadata.GetMetadata<IConsumesMetadata>()?.ContentTypes;
|
||||||
|
if (contentTypes == null || contentTypes.Count == 0)
|
||||||
|
{
|
||||||
|
contentTypes = new string[] { AnyContentType, };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j < contentTypes.Count; j++)
|
||||||
|
{
|
||||||
|
var contentType = contentTypes[j];
|
||||||
|
|
||||||
|
if (!edges.ContainsKey(contentType))
|
||||||
|
{
|
||||||
|
edges.Add(contentType, new List<Endpoint>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now in a second loop, add endpoints to these lists. We've enumerated all of
|
||||||
|
// the states, so we want to see which states this endpoint matches.
|
||||||
|
for (var i = 0; i < endpoints.Count; i++)
|
||||||
|
{
|
||||||
|
var endpoint = endpoints[i];
|
||||||
|
var contentTypes = endpoint.Metadata.GetMetadata<IConsumesMetadata>()?.ContentTypes ?? Array.Empty<string>();
|
||||||
|
if (contentTypes.Count == 0)
|
||||||
|
{
|
||||||
|
// OK this means that this endpoint matches *all* content methods.
|
||||||
|
// So, loop and add it to all states.
|
||||||
|
foreach (var kvp in edges)
|
||||||
|
{
|
||||||
|
kvp.Value.Add(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// OK this endpoint matches specific content types -- we have to loop through edges here
|
||||||
|
// because content types could either be exact (like 'application/json') or they
|
||||||
|
// could have wildcards (like 'text/*'). We don't expect wildcards to be especially common
|
||||||
|
// with consumes, but we need to support it.
|
||||||
|
foreach (var kvp in edges)
|
||||||
|
{
|
||||||
|
// The edgeKey maps to a possible request header value
|
||||||
|
var edgeKey = new MediaType(kvp.Key);
|
||||||
|
|
||||||
|
for (var j = 0; j < contentTypes.Count; j++)
|
||||||
|
{
|
||||||
|
var contentType = contentTypes[j];
|
||||||
|
|
||||||
|
var mediaType = new MediaType(contentType);
|
||||||
|
|
||||||
|
// Example: 'application/json' is subset of 'application/*'
|
||||||
|
//
|
||||||
|
// This means that when the request has content-type 'application/json' an endpoint
|
||||||
|
// what consumes 'application/*' should match.
|
||||||
|
if (edgeKey.IsSubsetOf(mediaType))
|
||||||
|
{
|
||||||
|
kvp.Value.Add(endpoint);
|
||||||
|
|
||||||
|
// It's possible that a ConsumesMetadata defines overlapping wildcards. Don't add an endpoint
|
||||||
|
// to any edge twice
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If after we're done there isn't any endpoint that accepts */*, then we'll synthesize an
|
||||||
|
// endpoint that always returns a 415.
|
||||||
|
if (!edges.ContainsKey(AnyContentType))
|
||||||
|
{
|
||||||
|
edges.Add(AnyContentType, new List<Endpoint>()
|
||||||
|
{
|
||||||
|
CreateRejectionEndpoint(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges
|
||||||
|
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Endpoint CreateRejectionEndpoint()
|
||||||
|
{
|
||||||
|
return new MatcherEndpoint(
|
||||||
|
(next) => (context) =>
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
RoutePatternFactory.Parse("/"),
|
||||||
|
new RouteValueDictionary(),
|
||||||
|
0,
|
||||||
|
EndpointMetadataCollection.Empty,
|
||||||
|
Http415EndpointDisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
|
||||||
|
{
|
||||||
|
if (edges == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(edges));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they
|
||||||
|
// are then then execute them in linear order.
|
||||||
|
var ordered = edges
|
||||||
|
.Select(e => (mediaType: new MediaType((string)e.State), destination: e.Destination))
|
||||||
|
.OrderBy(e => GetScore(e.mediaType))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// If any edge matches all content types, then treat that as the 'exit'. This will
|
||||||
|
// always happen because we insert a 415 endpoint.
|
||||||
|
for (var i = 0; i < ordered.Length; i++)
|
||||||
|
{
|
||||||
|
if (ordered[i].mediaType.MatchesAllTypes)
|
||||||
|
{
|
||||||
|
exitDestination = ordered[i].destination;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ConsumesPolicyJumpTable(exitDestination, ordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetScore(MediaType mediaType)
|
||||||
|
{
|
||||||
|
// Higher score == lower priority - see comments on MediaType.
|
||||||
|
if (mediaType.MatchesAllTypes)
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
else if (mediaType.MatchesAllSubTypes)
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
else if (mediaType.MatchesAllSubTypesWithoutSuffix)
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConsumesMetadataEndpointComparer : EndpointMetadataComparer<IConsumesMetadata>
|
||||||
|
{
|
||||||
|
protected override int CompareMetadata(IConsumesMetadata x, IConsumesMetadata y)
|
||||||
|
{
|
||||||
|
// Ignore the metadata if it has an empty list of content types.
|
||||||
|
return base.CompareMetadata(
|
||||||
|
x?.ContentTypes.Count > 0 ? x : null,
|
||||||
|
y?.ContentTypes.Count > 0 ? y : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConsumesPolicyJumpTable : PolicyJumpTable
|
||||||
|
{
|
||||||
|
private (MediaType mediaType, int destination)[] _destinations;
|
||||||
|
private int _exitDestination;
|
||||||
|
|
||||||
|
public ConsumesPolicyJumpTable(int exitDestination, (MediaType mediaType, int destination)[] destinations)
|
||||||
|
{
|
||||||
|
_exitDestination = exitDestination;
|
||||||
|
_destinations = destinations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetDestination(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var contentType = httpContext.Request.ContentType;
|
||||||
|
if (string.IsNullOrEmpty(contentType))
|
||||||
|
{
|
||||||
|
return _exitDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestMediaType = new MediaType(contentType);
|
||||||
|
var destinations = _destinations;
|
||||||
|
for (var i = 0; i < destinations.Length; i++)
|
||||||
|
{
|
||||||
|
if (requestMediaType.IsSubsetOf(destinations[i].mediaType))
|
||||||
|
{
|
||||||
|
return destinations[i].destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _exitDestination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
|
{
|
||||||
|
internal class ConsumesMetadata : IConsumesMetadata
|
||||||
|
{
|
||||||
|
public ConsumesMetadata(string[] contentTypes)
|
||||||
|
{
|
||||||
|
if (contentTypes == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(contentTypes));
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentTypes = contentTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> ContentTypes { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -325,6 +325,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
typeof(MatcherPolicy),
|
typeof(MatcherPolicy),
|
||||||
new Type[]
|
new Type[]
|
||||||
{
|
{
|
||||||
|
typeof(ConsumesMatcherPolicy),
|
||||||
typeof(ActionConstraintMatcherPolicy),
|
typeof(ActionConstraintMatcherPolicy),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
|
{
|
||||||
|
public class ConsumesMatcherPolicyTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AppliesToNode_EndpointWithoutMetadata_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[] { CreateEndpoint("/", null), };
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = policy.AppliesToNode(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppliesToNode_EndpointWithoutContentTypes_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(Array.Empty<string>())),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = policy.AppliesToNode(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppliesToNode_EndpointHasContentTypes_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(Array.Empty<string>())),
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", })),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = policy.AppliesToNode(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetEdges_GroupsByContentType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||||
|
// this way so we can verify that ordering is preserved by GetEdges.
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", "application/*+json", })),
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(Array.Empty<string>())),
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(new[] { "application/xml", "application/*+xml", })),
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(new[] { "application/*", })),
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(new[]{ "*/*", })),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var edges = policy.GetEdges(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
edges.OrderBy(e => e.State),
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("*/*", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/*", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/*+json", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/*+xml", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/xml", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] // See explanation in GetEdges for how this case is different
|
||||||
|
public void GetEdges_GroupsByContentType_CreatesHttp405Endpoint()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||||
|
// this way so we can verify that ordering is preserved by GetEdges.
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(new[] { "application/json", "application/*+json", })),
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(new[] { "application/xml", "application/*+xml", })),
|
||||||
|
CreateEndpoint("/", new ConsumesMetadata(new[] { "application/*", })),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var edges = policy.GetEdges(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
edges.OrderBy(e => e.State),
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("*/*", e.State);
|
||||||
|
Assert.Equal(ConsumesMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName);
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/*", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/*+json", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[0], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/*+xml", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[0], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/xml", e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("image/png", 1)]
|
||||||
|
[InlineData("application/foo", 2)]
|
||||||
|
[InlineData("text/xml", 3)]
|
||||||
|
[InlineData("application/product+json", 6)] // application/json will match this
|
||||||
|
[InlineData("application/product+xml", 7)] // application/xml will match this
|
||||||
|
[InlineData("application/json", 6)]
|
||||||
|
[InlineData("application/xml", 7)]
|
||||||
|
public void BuildJumpTable_SortsEdgesByPriority(string contentType, int expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var edges = new PolicyJumpTableEdge[]
|
||||||
|
{
|
||||||
|
// In reverse order of how they should be processed
|
||||||
|
new PolicyJumpTableEdge("*/*", 1),
|
||||||
|
new PolicyJumpTableEdge("application/*", 2),
|
||||||
|
new PolicyJumpTableEdge("text/*", 3),
|
||||||
|
new PolicyJumpTableEdge("application/*+xml", 4),
|
||||||
|
new PolicyJumpTableEdge("application/*+json", 5),
|
||||||
|
new PolicyJumpTableEdge("application/json", 6),
|
||||||
|
new PolicyJumpTableEdge("application/xml", 7),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
var jumpTable = policy.BuildJumpTable(-1, edges);
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.ContentType = contentType;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = jumpTable.GetDestination(httpContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MatcherEndpoint CreateEndpoint(string template, ConsumesMetadata consumesMetadata)
|
||||||
|
{
|
||||||
|
var metadata = new List<object>();
|
||||||
|
if (consumesMetadata != null)
|
||||||
|
{
|
||||||
|
metadata.Add(consumesMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MatcherEndpoint(
|
||||||
|
(next) => null,
|
||||||
|
RoutePatternFactory.Parse(template),
|
||||||
|
new RouteValueDictionary(),
|
||||||
|
0,
|
||||||
|
new EndpointMetadataCollection(metadata),
|
||||||
|
$"test: {template} - {string.Join(", ", consumesMetadata?.ContentTypes ?? Array.Empty<string>())}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConsumesMatcherPolicy CreatePolicy()
|
||||||
|
{
|
||||||
|
return new ConsumesMatcherPolicy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -29,5 +30,22 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The endpoint routing version of this feature has fixed https://github.com/aspnet/Mvc/issues/8174
|
||||||
|
[Fact]
|
||||||
|
public override 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.UnsupportedMediaType, response.StatusCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
|
public virtual async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var request = new HttpRequestMessage(
|
var request = new HttpRequestMessage(
|
||||||
|
|
|
||||||
|
|
@ -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 System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue