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
This commit is contained in:
David Fowler 2019-01-28 20:38:49 +00:00 committed by GitHub
parent 14d8e33a93
commit 9f71e60283
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 9 deletions

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Patterns;
@ -21,7 +22,6 @@ namespace Microsoft.AspNetCore.Builder
private static readonly string[] PutVerb = new[] { "PUT" }; private static readonly string[] PutVerb = new[] { "PUT" };
private static readonly string[] DeleteVerb = new[] { "DELETE" }; private static readonly string[] DeleteVerb = new[] { "DELETE" };
#region MapVerbs
/// <summary> /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests
/// for the specified pattern. /// 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()); return Map(builder, pattern, displayName ?? $"{pattern} HTTP: {string.Join(", ", httpMethods)}", requestDelegate, metadata: resolvedMetadata.ToArray());
} }
#endregion
#region Map
/// <summary> /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern. /// for the specified pattern.
@ -323,8 +321,17 @@ namespace Microsoft.AspNetCore.Builder
var routeEndpointBuilder = new RouteEndpointBuilder( var routeEndpointBuilder = new RouteEndpointBuilder(
requestDelegate, requestDelegate,
pattern, pattern,
defaultOrder); defaultOrder)
routeEndpointBuilder.DisplayName = displayName; {
DisplayName = displayName
};
// Add delegate attributes as metadata
foreach (var attribute in requestDelegate.Method.GetCustomAttributes())
{
routeEndpointBuilder.Metadata.Add(attribute);
}
if (metadata != null) if (metadata != null)
{ {
foreach (var item in metadata) foreach (var item in metadata)
@ -343,6 +350,5 @@ namespace Microsoft.AspNetCore.Builder
return modelEndpointDataSource.AddEndpointBuilder(routeEndpointBuilder); return modelEndpointDataSource.AddEndpointBuilder(routeEndpointBuilder);
} }
#endregion
} }
} }

View File

@ -2,11 +2,9 @@
// 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; using System;
using System.Collections.Generic; using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Patterns;
using Xunit; using Xunit;
@ -97,5 +95,59 @@ namespace Microsoft.AspNetCore.Builder
Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); Assert.Equal("/", endpointBuilder1.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpointBuilder1.Metadata)); 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<Attribute1>(endpointBuilder1.Metadata[0]);
Assert.IsType<Attribute2>(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<Attribute1>(endpointBuilder1.Metadata[0]);
Assert.IsType<Attribute2>(endpointBuilder1.Metadata[1]);
Assert.IsType<Metadata>(endpointBuilder1.Metadata[2]);
}
[Attribute1]
[Attribute2]
private static Task Handle(HttpContext context) => Task.CompletedTask;
private class Attribute1 : Attribute
{
}
private class Attribute2 : Attribute
{
}
private class Metadata
{
}
} }
} }

View File

@ -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; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -46,6 +47,10 @@ namespace RoutingSandbox
foreach (var endpoint in dataSource.Endpoints.OfType<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase)) foreach (var endpoint in dataSource.Endpoints.OfType<RouteEndpoint>().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase))
{ {
sb.AppendLine($"- {endpoint.RoutePattern.RawText}"); sb.AppendLine($"- {endpoint.RoutePattern.RawText}");
foreach (var metadata in endpoint.Metadata)
{
sb.AppendLine(" " + metadata);
}
} }
var response = httpContext.Response; var response = httpContext.Response;
@ -79,6 +84,10 @@ namespace RoutingSandbox
return Task.CompletedTask; return Task.CompletedTask;
}); });
builder.MapGet("/attributes", HandlerWithAttributes);
builder.Map("/getwithattributes", Handler);
builder.MapFramework(frameworkBuilder => builder.MapFramework(frameworkBuilder =>
{ {
frameworkBuilder.AddPattern("/transform/{hub:slugify=TestHub}/{method:slugify=TestMethod}"); frameworkBuilder.AddPattern("/transform/{hub:slugify=TestHub}/{method:slugify=TestMethod}");
@ -92,5 +101,29 @@ namespace RoutingSandbox
app.UseStaticFiles(); 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<string> HttpMethods { get; } = new List<string> { "GET" };
}
} }
} }