From 2f8951e24424b9c113af87f8d59da2c59eae6903 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Thu, 5 Oct 2017 15:14:17 -0700 Subject: [PATCH] Productize HttpMethodEndpointSelector (#463) Addresses #452 --- samples/DispatcherSample/Startup.cs | 2 - .../HttpMethodEndpointSelector.cs | 13 ++- .../Dispatcher/TreeDispatcher.cs | 2 +- .../ApiAppStartup.cs | 20 +++- .../ApiAppTest.cs | 68 ++++++++++++ .../DispatcherTest.cs | 16 --- .../HttpMethodEndpointSelectorTest.cs | 101 ++++++++++++++++++ ...icrosoft.AspNetCore.Dispatcher.Test.csproj | 4 + 8 files changed, 198 insertions(+), 28 deletions(-) rename {samples/DispatcherSample => src/Microsoft.AspNetCore.Dispatcher}/HttpMethodEndpointSelector.cs (82%) delete mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/DispatcherTest.cs create mode 100644 test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs diff --git a/samples/DispatcherSample/Startup.cs b/samples/DispatcherSample/Startup.cs index 68f8467eaf..5d4edc7f69 100644 --- a/samples/DispatcherSample/Startup.cs +++ b/samples/DispatcherSample/Startup.cs @@ -7,11 +7,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Dispatcher; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Dispatcher; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace DispatcherSample { diff --git a/samples/DispatcherSample/HttpMethodEndpointSelector.cs b/src/Microsoft.AspNetCore.Dispatcher/HttpMethodEndpointSelector.cs similarity index 82% rename from samples/DispatcherSample/HttpMethodEndpointSelector.cs rename to src/Microsoft.AspNetCore.Dispatcher/HttpMethodEndpointSelector.cs index 3885eb3486..6045f1fb9a 100644 --- a/samples/DispatcherSample/HttpMethodEndpointSelector.cs +++ b/src/Microsoft.AspNetCore.Dispatcher/HttpMethodEndpointSelector.cs @@ -4,9 +4,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.AspNetCore.Dispatcher; -namespace DispatcherSample +namespace Microsoft.AspNetCore.Dispatcher { public class HttpMethodEndpointSelector : EndpointSelector { @@ -19,19 +18,19 @@ namespace DispatcherSample var snapshot = context.CreateSnapshot(); - var fallback = new List(); + var fallbackEndpoints = new List(); for (var i = context.Endpoints.Count - 1; i >= 0; i--) { var endpoint = context.Endpoints[i] as ITemplateEndpoint; if (endpoint == null || endpoint.HttpMethod == null) { // No metadata. - fallback.Add(context.Endpoints[i]); + fallbackEndpoints.Add(context.Endpoints[i]); context.Endpoints.RemoveAt(i); } 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 { @@ -50,9 +49,9 @@ namespace DispatcherSample context.RestoreSnapshot(snapshot); 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(); diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs index cdad000ff9..c11260b9e1 100644 --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs +++ b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeDispatcher.cs @@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Routing.Dispatcher trees.Add(new UrlMatchingTree(trees.Count)); } - var tree = trees[i]; + var tree = trees[entry.Order]; TreeRouteBuilder.AddEntryToTree(tree, entry); } diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs index 4f2cf54af3..89a34c8e86 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs @@ -62,15 +62,31 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest { 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); } + 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_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 { public string Name { get; set; } diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppTest.cs b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppTest.cs index 6a1eb005fe..36bd16e368 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppTest.cs +++ b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppTest.cs @@ -30,5 +30,73 @@ namespace Microsoft.AspNetCore.Dispatcher.FunctionalTest Assert.Equal(HttpStatusCode.OK, response.StatusCode); 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); + } } } diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/DispatcherTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/DispatcherTest.cs deleted file mode 100644 index 3c0b79e219..0000000000 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/DispatcherTest.cs +++ /dev/null @@ -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() - { - - } - } -} diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs new file mode 100644 index 0000000000..0136752df8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/HttpMethodEndpointSelectorTest.cs @@ -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() + { + 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(); + + // Assert + Assert.Collection( + templateEndpoints, + endpoint => Assert.Equal(httpMethod.ToUpperInvariant(), endpoint.HttpMethod)); + } + + [Fact] + public async Task RequestMethod_DoesNotMatch_AnyEndpointMethod() + { + // Arrange + var endpoints = new List() + { + 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() + { + 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(); + + // Assert + Assert.Collection( + templateEndpoints, + endpoint => Assert.Null(endpoint.HttpMethod)); + } + + private (EndpointSelectorContext, EndpointSelector) CreateContextAndSelector(string httpMethod, List endpoints) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = httpMethod; + var selector = new HttpMethodEndpointSelector(); + var selectors = new List() + { + 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"); + } +} diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Microsoft.AspNetCore.Dispatcher.Test.csproj b/test/Microsoft.AspNetCore.Dispatcher.Test/Microsoft.AspNetCore.Dispatcher.Test.csproj index 6f4ba1c03e..b75aac40fa 100644 --- a/test/Microsoft.AspNetCore.Dispatcher.Test/Microsoft.AspNetCore.Dispatcher.Test.csproj +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Microsoft.AspNetCore.Dispatcher.Test.csproj @@ -9,4 +9,8 @@ + + + +