Productize HttpMethodEndpointSelector (#463)

Addresses #452
This commit is contained in:
Jass Bagga 2017-10-05 15:14:17 -07:00 committed by GitHub
parent f49cbd1b25
commit 2f8951e244
8 changed files with 198 additions and 28 deletions

View File

@ -7,11 +7,9 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Dispatcher; using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Dispatcher; using Microsoft.AspNetCore.Routing.Dispatcher;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace DispatcherSample namespace DispatcherSample
{ {

View File

@ -4,9 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Dispatcher;
namespace DispatcherSample namespace Microsoft.AspNetCore.Dispatcher
{ {
public class HttpMethodEndpointSelector : EndpointSelector public class HttpMethodEndpointSelector : EndpointSelector
{ {
@ -19,19 +18,19 @@ namespace DispatcherSample
var snapshot = context.CreateSnapshot(); var snapshot = context.CreateSnapshot();
var fallback = new List<Endpoint>(); var fallbackEndpoints = new List<Endpoint>();
for (var i = context.Endpoints.Count - 1; i >= 0; i--) for (var i = context.Endpoints.Count - 1; i >= 0; i--)
{ {
var endpoint = context.Endpoints[i] as ITemplateEndpoint; var endpoint = context.Endpoints[i] as ITemplateEndpoint;
if (endpoint == null || endpoint.HttpMethod == null) if (endpoint == null || endpoint.HttpMethod == null)
{ {
// No metadata. // No metadata.
fallback.Add(context.Endpoints[i]); fallbackEndpoints.Add(context.Endpoints[i]);
context.Endpoints.RemoveAt(i); context.Endpoints.RemoveAt(i);
} }
else if (string.Equals(endpoint.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) else if (string.Equals(endpoint.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
{ {
// This one matches. // The request method matches the endpoint's HTTP method.
} }
else else
{ {
@ -50,9 +49,9 @@ namespace DispatcherSample
context.RestoreSnapshot(snapshot); context.RestoreSnapshot(snapshot);
context.Endpoints.Clear(); context.Endpoints.Clear();
for (var i = 0; i < fallback.Count; i++) for (var i = 0; i < fallbackEndpoints.Count; i++)
{ {
context.Endpoints.Add(fallback[i]); context.Endpoints.Add(fallbackEndpoints[i]);
} }
await context.InvokeNextAsync(); await context.InvokeNextAsync();

View File

@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher
trees.Add(new UrlMatchingTree(trees.Count)); trees.Add(new UrlMatchingTree(trees.Count));
} }
var tree = trees[i]; var tree = trees[entry.Order];
TreeRouteBuilder.AddEntryToTree(tree, entry); TreeRouteBuilder.AddEntryToTree(tree, entry);
} }

View File

@ -62,15 +62,31 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest
{ {
Endpoints = Endpoints =
{ {
new TemplateEndpoint("api/products", Products_Get), new TemplateEndpoint("api/products", Products_Fallback),
new TemplateEndpoint("api/products", new { controller = "Products", action = "Get", }, "GET", Products_Get),
new TemplateEndpoint("api/products/{id}", new { controller = "Products", action = "Get", }, "GET", Products_GetWithId),
new TemplateEndpoint("api/products", new { controller = "Products", action = "Post", }, "POST", Products_Post),
new TemplateEndpoint("api/products/{id}", new { controller = "Products", action = "Put", }, "PUT", Products_Put),
}, },
});
Selectors =
{
new HttpMethodEndpointSelector(),
}
});
options.HandlerFactories.Add(endpoint => (endpoint as TemplateEndpoint)?.HandlerFactory); options.HandlerFactories.Add(endpoint => (endpoint as TemplateEndpoint)?.HandlerFactory);
} }
private Task Products_Fallback(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Fallback");
private Task Products_Get(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Get"); private Task Products_Get(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Get");
private Task Products_GetWithId(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_GetWithId");
private Task Products_Post(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Post");
private Task Products_Put(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Put");
private class CorsPolicyMetadata private class CorsPolicyMetadata
{ {
public string Name { get; set; } public string Name { get; set; }

View File

@ -30,5 +30,73 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello, Products_Get", await response.Content.ReadAsStringAsync()); Assert.Equal("Hello, Products_Get", await response.Content.ReadAsStringAsync());
} }
[Fact]
public async Task ApiApp_RoutesTo_EndpointWithMatchingHttpMethod()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Post, "/api/products");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello, Products_Post", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task ApiApp_RoutesTo_EndpointWithMatchingHttpMethod_AndMatchingRoute()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Get, "/api/products/3");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello, Products_GetWithId", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task ApiApp_RoutesTo_EndpointWithMatchingHttpMethod_DoesNotMatchExpectedRoute()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Put, "/api/services/2");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task ApiApp_NoEndpointWithMatchingHttpMethod_FallbackEndpointSelected()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Delete, "/api/products");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello, Products_Fallback", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task ApiApp_NoEndpointWithMatchingHttpMethod_NoFallbackEndpointMatched()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Delete, "/api/products/4");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
} }
} }

View File

@ -1,16 +0,0 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Dispatcher.Abstractions.Test
{
public class DispatcherTest
{
[Fact]
public void Test()
{
}
}
}

View File

@ -0,0 +1,101 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Microsoft.AspNetCore.Dispatcher
{
public class HttpMethodEndpointSelectorTest
{
[Theory]
[InlineData("get")]
[InlineData("Get")]
[InlineData("GET")]
public async Task RequestMethod_MatchesEndpointMethod_IgnoresCase(string httpMethod)
{
// Arrange
var endpoints = new List<Endpoint>()
{
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Products", action = "Get", }, "GET", Products_Get, "Products:Get()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Products", action = "Create", }, "POST", Products_Post, "Products:Post()"),
};
var (context, selector) = CreateContextAndSelector(httpMethod, endpoints);
// Act
await selector.SelectAsync(context);
var templateEndpoints = context.Endpoints.Cast<TemplateEndpoint>();
// Assert
Assert.Collection(
templateEndpoints,
endpoint => Assert.Equal(httpMethod.ToUpperInvariant(), endpoint.HttpMethod));
}
[Fact]
public async Task RequestMethod_DoesNotMatch_AnyEndpointMethod()
{
// Arrange
var endpoints = new List<Endpoint>()
{
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Products", action = "Get", }, "GET", Products_Get, "Products:Get()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Products", action = "Create", }, "POST", Products_Post, "Products:Post()"),
};
var (context, selector) = CreateContextAndSelector("PUT", endpoints);
// Act
await selector.SelectAsync(context);
// Assert
Assert.Equal(0, context.Endpoints.Count);
}
[Theory]
[InlineData("PUT")]
[InlineData(null)]
public async Task RequestMethod_NotSpecifiedOrNotFound_ReturnsFallbackEndpointMethod(string httpMethod)
{
// Arrange
var endpoints = new List<Endpoint>()
{
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Products", action = "Get", }, "GET", Products_Get, "Products:Get()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Products", action = "Create", }, "POST", Products_Post, "Products:Post()"),
new TemplateEndpoint("{controller=Home}/{action=Index}/{id?}", new { controller = "Products", action = "Get", }, Products_Get),
};
var (context, selector) = CreateContextAndSelector(httpMethod, endpoints);
// Act
await selector.SelectAsync(context);
var templateEndpoints = context.Endpoints.Cast<TemplateEndpoint>();
// Assert
Assert.Collection(
templateEndpoints,
endpoint => Assert.Null(endpoint.HttpMethod));
}
private (EndpointSelectorContext, EndpointSelector) CreateContextAndSelector(string httpMethod, List<Endpoint> endpoints)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = httpMethod;
var selector = new HttpMethodEndpointSelector();
var selectors = new List<EndpointSelector>()
{
selector
};
var selectorContext = new EndpointSelectorContext(httpContext, endpoints, selectors);
return (selectorContext, selector);
}
private Task Products_Get(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Get");
private Task Products_Post(HttpContext httpContext) => httpContext.Response.WriteAsync("Hello, Products_Post");
}
}

View File

@ -9,4 +9,8 @@
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Dispatcher\Microsoft.AspNetCore.Dispatcher.csproj" /> <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Dispatcher\Microsoft.AspNetCore.Dispatcher.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" />
</ItemGroup>
</Project> </Project>