Closed jchannon closed 4 years ago
UseCarter
should hang off IEndpointConventionBuilder
instead of hanging off IApplicationBuilder
(and would be renamed to MapCarter).
Middleware can act on selected endpoints when chosen so things like authorization/cors can be moved outside of the framework into middleware. As part of route registration users should have a way to add metadata to their route.
public class ProductsModule : CaterModule
{
public MyModule() : base("/products")
{
this.Get("/", ctx => ctx.WriteAsync("hi")).RequireAuthorization();
}
}
The CarterModule
would be a mini DSL over routing.
Quickly looking at the code I'm not sure how you could expose the IEndpointConventionBuilder
from the route registration methods without a breaking change. Maybe it could be an argument instead?
Here's a strawman:
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace CarterDemo
{
public class HelloModule : CarterModule
{
public HelloModule()
{
Get("/", context => context.Response.WriteAsync("Hello World")).RequireAuthorization();
}
}
public class CarterModule
{
internal readonly Dictionary<(string verb, string path), (RequestDelegate handler, RouteConventions conventions)> Routes = new Dictionary<(string verb, string path), (RequestDelegate handler, RouteConventions conventions)>();
public IEndpointConventionBuilder Get(string path, RequestDelegate handler)
{
var conventions= new RouteConventions();
Routes.Add((HttpMethods.Get, path), (handler, conventions));
return conventions;
}
internal class RouteConventions : IEndpointConventionBuilder
{
private readonly List<Action<EndpointBuilder>> _actions = new List<Action<EndpointBuilder>>();
public void Add(Action<EndpointBuilder> convention)
{
_actions.Add(convention);
}
public void Apply(IEndpointConventionBuilder builder)
{
foreach (var a in _actions)
{
builder.Add(a);
}
}
}
}
public static class CarterExtensions
{
public static IEndpointConventionBuilder MapCarter(this IEndpointRouteBuilder builder)
{
var builders = new List<IEndpointConventionBuilder>();
using var scope = builder.ServiceProvider.CreateScope();
foreach (var module in scope.ServiceProvider.GetServices<CarterModule>())
{
foreach (var route in module.Routes)
{
var conventionBuilder = builder.MapMethods(route.Key.path, new[] { route.Key.verb }, route.Value.handler);
route.Value.conventions.Apply(conventionBuilder);
builders.Add(conventionBuilder);
}
}
// Allow the user to apply conventions to all modules
return new CompositeConventionBuilder(builders);
}
private class CompositeConventionBuilder : IEndpointConventionBuilder
{
private readonly List<IEndpointConventionBuilder> _builders;
public CompositeConventionBuilder(List<IEndpointConventionBuilder> builders)
{
_builders = builders;
}
public void Add(Action<EndpointBuilder> convention)
{
foreach (var builder in _builders)
{
builder.Add(convention);
}
}
}
}
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization();
services.AddScoped<CarterModule, HelloModule>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapCarter();
});
}
}
}
Awesome, will take a look.
I'm fine with a breaking change so if there's anything else you think which might be more appropriate I'm all ears!
Thanks again
@davidfowl Is there a difference in using your IEndpointConventionBuilder
examples and EndpointDataSource
like below
.UseEndpoints(builder =>
{
var dataSource = new DefaultEndpointDataSource(
new RouteEndpointBuilder(
context =>
context.Response.WriteAsync(context.Request.Method.ToString()),
RoutePatternFactory.Parse("/test"),
0)
.Build()
);
builder.DataSources.Add(dataSource);
I also can't see when CompositeConventionBuilder.Add would be called?
@davidfowl Is there a difference in using your IEndpointConventionBuilder examples and EndpointDataSource like below
Not sure what you mean? It's typical for extension methods to return IEndpointConventionBuilder
so callers can add metadata to routes.
I also can't see when CompositeConventionBuilder.Add would be called?
CompositeConventionBuilder
implements IEndpointConventionBuilder
which is where Add comes from. When you call something like RequireAuthorization
it calls Add on the builder https://github.com/aspnet/AspNetCore/blob/16a47948f80fede807fabe3c291d793590e8fd17/src/Security/Authorization/Policy/src/AuthorizationEndpointConventionBuilderExtensions.cs#L82. It's how all conventions work.
Yup, I see, got it. Got onion layers going on...
Thanks
On Fri, 11 Oct 2019 at 04:06, David Fowler notifications@github.com wrote:
@davidfowl https://github.com/davidfowl Is there a difference in using your IEndpointConventionBuilder examples and EndpointDataSource like below
Not sure what you mean? It's typical for extension methods to return IEndpointConventionBuilder so callers can add metadata to routes.
I also can't see when CompositeConventionBuilder.Add would be called?
CompositeConventionBuilder implements IEndpointConventionBuilder which is where Add comes from. When you call something like RequireAuthorization it calls Add on the builder https://github.com/aspnet/AspNetCore/blob/16a47948f80fede807fabe3c291d793590e8fd17/src/Security/Authorization/Policy/src/AuthorizationEndpointConventionBuilderExtensions.cs#L82. It's how all conventions work.
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/CarterCommunity/Carter/issues/207?email_source=notifications&email_token=AAAZVJV76NCYIR3RZ4F7K5DQN7UR7A5CNFSM4I3XQTA2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEA6TAYA#issuecomment-540880992, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAZVJVO6MAWQ4OCTSQB37TQN7UR7ANCNFSM4I3XQTAQ .
I have been thinking about what we have in #112 and what we can do there. I have been thinking Carter could expose the ASP.NET Core APIs yet still offer its own features. This means we could offer the same as ASP.NET Core APIs such as
this.Get("/", ctx => ctx.WriteAsync("hi").RequiresAuth().RequiresHost()
however I am currently unsure how to wire this up.We currently do this for ASPNET3 https://github.com/CarterCommunity/Carter/blob/aspnetcore3/src/CarterExtensions.cs#L58 but I'm not sure if CarterModule should expose a list of
IEndpointConventionBuilder
potentially and then wire that up to the endpoint routing middleware.Hoping @davidfowl can help point in the right direction