CarterCommunity / Carter

Carter is framework that is a thin layer of extension methods and functionality over ASP.NET Core allowing code to be more explicit and most importantly more enjoyable.
MIT License
2.08k stars 175 forks source link

ASP.NET Core 3 signatures #207

Closed jchannon closed 4 years ago

jchannon commented 4 years ago

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

davidfowl commented 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();
            });
        }
    }
}
jchannon commented 4 years ago

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

jchannon commented 4 years ago

@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);
jchannon commented 4 years ago

I also can't see when CompositeConventionBuilder.Add would be called?

davidfowl commented 4 years ago

@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.

jchannon commented 4 years ago

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 .