From 9f71e602837b164e6f525ae33821d1d95ea29f6b Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 28 Jan 2019 20:38:49 +0000 Subject: [PATCH] Hoist method attributes from the request delegate as metadata. (#6911) - This should allow a more declarative approach to declaring endpoint metadata using the default methods. - Attributes are applied first and can be overridden imperatively --- .../Builder/EndpointRouteBuilderExtensions.cs | 18 ++++-- ...EndpointDataSourceBuilderExtensionsTest.cs | 58 ++++++++++++++++++- .../UseEndpointRoutingStartup.cs | 33 +++++++++++ 3 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 27b048debc..6fa6d4db1c 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; @@ -21,7 +22,6 @@ namespace Microsoft.AspNetCore.Builder private static readonly string[] PutVerb = new[] { "PUT" }; private static readonly string[] DeleteVerb = new[] { "DELETE" }; - #region MapVerbs /// /// Adds a to the that matches HTTP GET requests /// for the specified pattern. @@ -227,9 +227,7 @@ namespace Microsoft.AspNetCore.Builder return Map(builder, pattern, displayName ?? $"{pattern} HTTP: {string.Join(", ", httpMethods)}", requestDelegate, metadata: resolvedMetadata.ToArray()); } - #endregion - #region Map /// /// Adds a to the that matches HTTP requests /// for the specified pattern. @@ -323,8 +321,17 @@ namespace Microsoft.AspNetCore.Builder var routeEndpointBuilder = new RouteEndpointBuilder( requestDelegate, pattern, - defaultOrder); - routeEndpointBuilder.DisplayName = displayName; + defaultOrder) + { + DisplayName = displayName + }; + + // Add delegate attributes as metadata + foreach (var attribute in requestDelegate.Method.GetCustomAttributes()) + { + routeEndpointBuilder.Metadata.Add(attribute); + } + if (metadata != null) { foreach (var item in metadata) @@ -343,6 +350,5 @@ namespace Microsoft.AspNetCore.Builder return modelEndpointDataSource.AddEndpointBuilder(routeEndpointBuilder); } - #endregion } } diff --git a/src/Http/Routing/test/UnitTests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs index 1ac87b888c..ae53ec0427 100644 --- a/src/Http/Routing/test/UnitTests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/MapEndpointEndpointDataSourceBuilderExtensionsTest.cs @@ -2,11 +2,9 @@ // 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.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; @@ -97,5 +95,59 @@ namespace Microsoft.AspNetCore.Builder Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); Assert.Equal(metadata, Assert.Single(endpointBuilder1.Metadata)); } + + [Fact] + public void MapEndpoint_AttributesCollectedAsMetadata() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(); + + // Act + var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), "Display name!", Handle); + + // Assert + var endpointBuilder1 = GetRouteEndpointBuilder(builder); + Assert.Equal("Display name!", endpointBuilder1.DisplayName); + Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); + Assert.Equal(2, endpointBuilder1.Metadata.Count); + Assert.IsType(endpointBuilder1.Metadata[0]); + Assert.IsType(endpointBuilder1.Metadata[1]); + } + + [Fact] + public void MapEndpoint_ExplicitMetadataAddedAfterAttributeMetadata() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(); + + // Act + var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), "Display name!", Handle, new Metadata()); + + // Assert + var endpointBuilder1 = GetRouteEndpointBuilder(builder); + Assert.Equal("Display name!", endpointBuilder1.DisplayName); + Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); + Assert.Equal(3, endpointBuilder1.Metadata.Count); + Assert.IsType(endpointBuilder1.Metadata[0]); + Assert.IsType(endpointBuilder1.Metadata[1]); + Assert.IsType(endpointBuilder1.Metadata[2]); + } + + [Attribute1] + [Attribute2] + private static Task Handle(HttpContext context) => Task.CompletedTask; + + private class Attribute1 : Attribute + { + } + + private class Attribute2 : Attribute + { + } + + private class Metadata + { + + } } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/UseEndpointRoutingStartup.cs b/src/Http/Routing/test/testassets/RoutingSandbox/UseEndpointRoutingStartup.cs index a82dcd5052..681eae941d 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/UseEndpointRoutingStartup.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/UseEndpointRoutingStartup.cs @@ -2,6 +2,7 @@ // 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.IO; using System.Linq; using System.Text; @@ -46,6 +47,10 @@ namespace RoutingSandbox foreach (var endpoint in dataSource.Endpoints.OfType().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase)) { sb.AppendLine($"- {endpoint.RoutePattern.RawText}"); + foreach (var metadata in endpoint.Metadata) + { + sb.AppendLine(" " + metadata); + } } var response = httpContext.Response; @@ -79,6 +84,10 @@ namespace RoutingSandbox return Task.CompletedTask; }); + builder.MapGet("/attributes", HandlerWithAttributes); + + builder.Map("/getwithattributes", Handler); + builder.MapFramework(frameworkBuilder => { frameworkBuilder.AddPattern("/transform/{hub:slugify=TestHub}/{method:slugify=TestMethod}"); @@ -92,5 +101,29 @@ namespace RoutingSandbox app.UseStaticFiles(); } + + [Authorize] + private Task HandlerWithAttributes(HttpContext context) + { + return context.Response.WriteAsync("I have ann authorize attribute"); + } + + [HttpGet] + private Task Handler(HttpContext context) + { + return context.Response.WriteAsync("I have a method metadata attribute"); + } + + private class AuthorizeAttribute : Attribute + { + + } + + private class HttpGetAttribute : Attribute, IHttpMethodMetadata + { + public bool AcceptCorsPreflight => false; + + public IReadOnlyList HttpMethods { get; } = new List { "GET" }; + } } }