Add EndpointBuilder (#701)

This commit is contained in:
James Newton-King 2018-08-09 13:06:27 +12:00 committed by GitHub
parent 14a3a98f48
commit 95267a32e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 542 additions and 67 deletions

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;

View File

@ -0,0 +1,33 @@
// 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.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
public static class EndpointDataSourceBuilderExtensions
{
public static EndpointBuilder MapHello(this EndpointDataSourceBuilder builder, string template, string greeter)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
var pipeline = builder.CreateApplicationBuilder()
.UseHello(greeter)
.Build();
return builder.MapEndpoint(
(next) => pipeline,
template,
"Hello");
}
}
}

View File

@ -0,0 +1,25 @@
// 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 Microsoft.Extensions.Options;
using RoutingSample.Web.HelloExtension;
namespace Microsoft.AspNetCore.Builder
{
public static class HelloAppBuilderExtensions
{
public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<HelloMiddleware>(Options.Create(new HelloOptions
{
Greeter = greeter
}));
}
}
}

View File

@ -0,0 +1,45 @@
// 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.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace RoutingSample.Web.HelloExtension
{
public class HelloMiddleware
{
private readonly RequestDelegate _next;
private readonly HelloOptions _helloOptions;
private readonly byte[] _helloPayload;
public HelloMiddleware(RequestDelegate next, IOptions<HelloOptions> helloOptions)
{
_next = next;
_helloOptions = helloOptions.Value;
var payload = new List<byte>();
payload.AddRange(Encoding.UTF8.GetBytes("Hello"));
if (!string.IsNullOrEmpty(_helloOptions.Greeter))
{
payload.Add((byte)' ');
payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter));
}
_helloPayload = payload.ToArray();
}
public Task InvokeAsync(HttpContext context)
{
var response = context.Response;
var payloadLength = _helloPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloPayload, 0, payloadLength);
}
}
}

View File

@ -0,0 +1,10 @@
// 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.
namespace RoutingSample.Web.HelloExtension
{
public class HelloOptions
{
public string Greeter { get; set; }
}
}

View File

@ -3,20 +3,16 @@
using System;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace RoutingSample.Web
{
public class UseEndpointRoutingStartup
{
private static readonly byte[] _homePayload = Encoding.UTF8.GetBytes("Endpoint Routing sample endpoints:" + Environment.NewLine + "/plaintext");
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!");
public void ConfigureServices(IServiceCollection services)
{
@ -26,65 +22,59 @@ namespace RoutingSample.Web
{
options.ConstraintMap.Add("endsWith", typeof(EndsWithStringMatchProcessor));
});
var endpointDataSource = new DefaultEndpointDataSource(new[]
{
new MatcherEndpoint((next) => (httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _homePayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_homePayload, 0, payloadLength);
},
RoutePatternFactory.Parse("/"),
0,
EndpointMetadataCollection.Empty,
"Home"),
new MatcherEndpoint((next) => (httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _helloWorldPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
},
RoutePatternFactory.Parse("/plaintext"),
0,
EndpointMetadataCollection.Empty,
"Plaintext"),
new MatcherEndpoint((next) => (httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("WithConstraints");
},
RoutePatternFactory.Parse("/withconstraints/{id:endsWith(_001)}"),
0,
EndpointMetadataCollection.Empty,
"withconstraints"),
new MatcherEndpoint((next) => (httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("withoptionalconstraints");
},
RoutePatternFactory.Parse("/withoptionalconstraints/{id:endsWith(_001)?}"),
0,
EndpointMetadataCollection.Empty,
"withoptionalconstraints"),
});
services.TryAddEnumerable(ServiceDescriptor.Singleton<EndpointDataSource>(endpointDataSource));
}
public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app)
{
app.UseEndpointRouting();
app.UseEndpointRouting(builder =>
{
builder.MapHello("/helloworld", "World");
builder.MapEndpoint(
(next) => (httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _homePayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_homePayload, 0, payloadLength);
},
"/",
"Home");
builder.MapEndpoint(
(next) => (httpContext) =>
{
var response = httpContext.Response;
var payloadLength = _plainTextPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength);
},
"/plaintext",
"Plaintext");
builder.MapEndpoint(
(next) => (httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("WithConstraints");
},
"/withconstraints/{id:endsWith(_001)}",
"withconstraints");
builder.MapEndpoint(
(next) => (httpContext) =>
{
var response = httpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
return response.WriteAsync("withoptionalconstraints");
},
"/withoptionalconstraints/{id:endsWith(_001)?}",
"withoptionalconstraints");
});
// Imagine some more stuff here...

View File

@ -7,16 +7,28 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Internal
namespace Microsoft.AspNetCore.Builder
{
public static class EndpointRoutingApplicationBuilderExtensions
{
private const string EndpointRoutingRegisteredKey = "__EndpointRoutingMiddlewareRegistered";
public static IApplicationBuilder UseEndpointRouting(this IApplicationBuilder builder)
{
return builder.UseEndpointRouting(null);
}
public static IApplicationBuilder UseEndpointRouting(this IApplicationBuilder builder, Action<EndpointDataSourceBuilder> configure)
{
VerifyRoutingIsRegistered(builder);
if (configure != null)
{
var dataSourceBuilder = (DefaultEndpointDataSourceBuilder)builder.ApplicationServices.GetRequiredService<EndpointDataSourceBuilder>();
dataSourceBuilder.ApplicationBuilder = builder;
configure(dataSourceBuilder);
}
builder.Properties[EndpointRoutingRegisteredKey] = true;
return builder.UseMiddleware<EndpointRoutingMiddleware>();
@ -50,4 +62,4 @@ namespace Microsoft.AspNetCore.Internal
}
}
}
}
}

View File

@ -0,0 +1,69 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
public static class MapEndpointEndpointDataSourceBuilderExtensions
{
public static MatcherEndpointBuilder MapEndpoint(
this EndpointDataSourceBuilder builder,
Func<RequestDelegate, RequestDelegate> invoker,
string pattern,
string displayName)
{
return MapEndpoint(builder, invoker, pattern, displayName, metadata: null);
}
public static MatcherEndpointBuilder MapEndpoint(
this EndpointDataSourceBuilder builder,
Func<RequestDelegate, RequestDelegate> invoker,
RoutePattern pattern,
string displayName)
{
return MapEndpoint(builder, invoker, pattern, displayName, metadata: null);
}
public static MatcherEndpointBuilder MapEndpoint(
this EndpointDataSourceBuilder builder,
Func<RequestDelegate, RequestDelegate> invoker,
string pattern,
string displayName,
IList<object> metadata)
{
return MapEndpoint(builder, invoker, RoutePatternFactory.Parse(pattern), displayName, metadata);
}
public static MatcherEndpointBuilder MapEndpoint(
this EndpointDataSourceBuilder builder,
Func<RequestDelegate, RequestDelegate> invoker,
RoutePattern pattern,
string displayName,
IList<object> metadata)
{
const int defaultOrder = 0;
var endpointBuilder = new MatcherEndpointBuilder(
invoker,
pattern,
defaultOrder);
endpointBuilder.DisplayName = displayName;
if (metadata != null)
{
foreach (var item in metadata)
{
endpointBuilder.Metadata.Add(item);
}
}
builder.Endpoints.Add(endpointBuilder);
return endpointBuilder;
}
}
}

View File

@ -0,0 +1,33 @@
// 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.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing
{
internal class BuilderEndpointDataSource : EndpointDataSource
{
private readonly EndpointDataSourceBuilder _builder;
public BuilderEndpointDataSource(EndpointDataSourceBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
_builder = builder;
}
public override IChangeToken GetChangeToken()
{
return NullChangeToken.Singleton;
}
public override IReadOnlyList<Endpoint> Endpoints => _builder.Endpoints.Select(b => b.Build()).ToArray();
}
}

View File

@ -0,0 +1,17 @@
// 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 Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Routing
{
internal class DefaultEndpointDataSourceBuilder : EndpointDataSourceBuilder
{
public IApplicationBuilder ApplicationBuilder { get; set; }
public override ICollection<EndpointBuilder> Endpoints { get; } = new List<EndpointBuilder>();
public override IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New();
}
}

View File

@ -59,6 +59,12 @@ namespace Microsoft.Extensions.DependencyInjection
return new CompositeEndpointDataSource(options.Value.DataSources);
});
//
// Endpoint Infrastructure
//
services.TryAddSingleton<EndpointDataSource, BuilderEndpointDataSource>();
services.TryAddSingleton<EndpointDataSourceBuilder, DefaultEndpointDataSourceBuilder>();
//
// Default matcher implementation
//

View File

@ -0,0 +1,16 @@
// 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;
namespace Microsoft.AspNetCore.Routing
{
public abstract class EndpointBuilder
{
public string DisplayName { get; set; }
public IList<object> Metadata { get; } = new List<object>();
public abstract Endpoint Build();
}
}

View File

@ -0,0 +1,15 @@
// 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 Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Routing
{
public abstract class EndpointDataSourceBuilder
{
public abstract ICollection<EndpointBuilder> Endpoints { get; }
public abstract IApplicationBuilder CreateApplicationBuilder();
}
}

View File

@ -0,0 +1,41 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Routing.Matching
{
public sealed class MatcherEndpointBuilder : EndpointBuilder
{
public Func<RequestDelegate, RequestDelegate> Invoker { get; set; }
public RoutePattern RoutePattern { get; set; }
public int Order { get; set; }
public MatcherEndpointBuilder(
Func<RequestDelegate, RequestDelegate> invoker,
RoutePattern routePattern,
int order)
{
Invoker = invoker;
RoutePattern = routePattern;
Order = order;
}
public override Endpoint Build()
{
var matcherEndpoint = new MatcherEndpoint(
Invoker,
RoutePattern,
Order,
new EndpointMetadataCollection(Metadata),
DisplayName);
return matcherEndpoint;
}
}
}

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
{
// Arrange
var expectedContentType = "text/plain";
var expectedContent = "Hello, World!";
var expectedContent = "Plain text!";
// Act
var response = await _client.GetAsync("/plaintext");
@ -62,6 +62,25 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
Assert.Equal(expectedContent, actualContent);
}
[Fact]
public async Task MatchesHelloMiddleware_AndReturnsPlaintext()
{
// Arrange
var expectedContentType = "text/plain";
var expectedContent = "Hello World";
// Act
var response = await _client.GetAsync("/helloworld");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType);
var actualContent = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedContent, actualContent);
}
[Fact]
public async Task MatchesEndpoint_WithSuccessfulConstraintMatch()
{

View File

@ -5,7 +5,6 @@ using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
@ -13,7 +12,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Builder
{
public class EndpointRoutingBuilderExtensionsTest
public class EndpointRoutingApplicationBuilderExtensionsTest
{
[Fact]
public void UseEndpointRouting_ServicesNotRegistered_Throws()
@ -109,6 +108,26 @@ namespace Microsoft.AspNetCore.Builder
Assert.NotNull(httpContext.Features.Get<IEndpointFeature>());
}
[Fact]
public void UseEndpointRouting_CallWithBuilder_SetsEndpointBuilder()
{
// Arrange
var services = CreateServices();
var app = new ApplicationBuilder(services);
// Act
app.UseEndpointRouting(builder =>
{
builder.MapEndpoint(d => null, "/", "Test endpoint");
});
// Assert
var dataSourceBuilder = (DefaultEndpointDataSourceBuilder)services.GetRequiredService<EndpointDataSourceBuilder>();
var endpointBuilder = Assert.Single(dataSourceBuilder.Endpoints);
Assert.Equal("Test endpoint", endpointBuilder.DisplayName);
}
private IServiceProvider CreateServices()
{
var services = new ServiceCollection();

View File

@ -0,0 +1,89 @@
// 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.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Builder
{
public class MapEndpointEndpointDataSourceBuilderExtensionsTest
{
[Fact]
public void MapEndpoint_StringPattern_BuildsEndpoint()
{
// Arrange
var builder = new DefaultEndpointDataSourceBuilder();
Func<RequestDelegate, RequestDelegate> invoker = (d) => null;
// Act
var endpointBuilder = builder.MapEndpoint(invoker, "/", "Display name!");
// Assert
Assert.Equal(endpointBuilder, Assert.Single(builder.Endpoints));
Assert.Equal(invoker, endpointBuilder.Invoker);
Assert.Equal("Display name!", endpointBuilder.DisplayName);
Assert.Equal("/", endpointBuilder.RoutePattern.RawText);
}
[Fact]
public void MapEndpoint_TypedPattern_BuildsEndpoint()
{
// Arrange
var builder = new DefaultEndpointDataSourceBuilder();
Func<RequestDelegate, RequestDelegate> invoker = (d) => null;
// Act
var endpointBuilder = builder.MapEndpoint(invoker, RoutePatternFactory.Parse("/"), "Display name!");
// Assert
Assert.Equal(endpointBuilder, Assert.Single(builder.Endpoints));
Assert.Equal(invoker, endpointBuilder.Invoker);
Assert.Equal("Display name!", endpointBuilder.DisplayName);
Assert.Equal("/", endpointBuilder.RoutePattern.RawText);
}
[Fact]
public void MapEndpoint_StringPatternAndMetadata_BuildsEndpoint()
{
// Arrange
var metadata = new object();
var builder = new DefaultEndpointDataSourceBuilder();
Func<RequestDelegate, RequestDelegate> invoker = (d) => null;
// Act
var endpointBuilder = builder.MapEndpoint(invoker, "/", "Display name!", new[] { metadata });
// Assert
Assert.Equal(endpointBuilder, Assert.Single(builder.Endpoints));
Assert.Equal(invoker, endpointBuilder.Invoker);
Assert.Equal("Display name!", endpointBuilder.DisplayName);
Assert.Equal("/", endpointBuilder.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpointBuilder.Metadata));
}
[Fact]
public void MapEndpoint_TypedPatternAndMetadata_BuildsEndpoint()
{
// Arrange
var metadata = new object();
var builder = new DefaultEndpointDataSourceBuilder();
Func<RequestDelegate, RequestDelegate> invoker = (d) => null;
// Act
var endpointBuilder = builder.MapEndpoint(invoker, RoutePatternFactory.Parse("/"), "Display name!", new[] { metadata });
// Assert
Assert.Equal(endpointBuilder, Assert.Single(builder.Endpoints));
Assert.Equal(invoker, endpointBuilder.Invoker);
Assert.Equal("Display name!", endpointBuilder.DisplayName);
Assert.Equal("/", endpointBuilder.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpointBuilder.Metadata));
}
}
}

View File

@ -0,0 +1,36 @@
// 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.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
namespace Microsoft.AspNetCore.Routing.Matching
{
public class MatcherEndpointBuilderTest
{
[Fact]
public void Build_AllValuesSet_EndpointCreated()
{
const int defaultOrder = 0;
object metadata = new object();
Func<RequestDelegate, RequestDelegate> invoker = (d) => null;
var builder = new MatcherEndpointBuilder(invoker, RoutePatternFactory.Parse("/"), defaultOrder)
{
DisplayName = "Display name!",
Metadata = { metadata }
};
var endpoint = Assert.IsType<MatcherEndpoint>(builder.Build());
Assert.Equal("Display name!", endpoint.DisplayName);
Assert.Equal(defaultOrder, endpoint.Order);
Assert.Equal(invoker, endpoint.Invoker);
Assert.Equal("/", endpoint.RoutePattern.RawText);
Assert.Equal(metadata, Assert.Single(endpoint.Metadata));
}
}
}