Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File-based routing #66

Open
Atulin opened this issue Jul 26, 2024 · 2 comments
Open

File-based routing #66

Atulin opened this issue Jul 26, 2024 · 2 comments

Comments

@Atulin
Copy link

Atulin commented Jul 26, 2024

Having to prefix all endpoints of a given type with a part of a path can be tedious and error-prone. What is not tedious or error-prone, is using the filesystem to drive routing.

Current state:

To have an endpoint at POST: /api/public/products we need:

// location: Api/Public/Products/CreateProduct.cs

[Handler]
[HttpPost("api/public/porducts")]
public static partial CreateProduct
{
    // ...
}

Can you spot the typo?

Proposed

To have an endpoint at POST: /api/public/products is as simple as:

// location: Api/Public/Products/CreateProduct.cs

[Handler]
[HttpPost]
public static partial CreateProduct
{
    // ...
}

Considerations

Opt-in methods

It does not have to be the default behavior. It could be controlled globally with some [assembly: FilesystemRouting] attribute, or perhaps on the individual endpoint basis with a similar attribute, or some magic like [HttpPost("[file]")] or [HttpPost(FilesystemRouting = true)]

Naming policies

What happens when a path contains a space? Should PascalCase directories be translated to lowercase, kebab-case, or something else?

The easiest way would be to go with what is the default for Microsoft, or at least seems to be: whitespace and special characters get removed, everything gets translated into PascalCase.

The best way would be to provide a choice, for example via an enum:

public enum NamingPolicy
{
    PascalCase,
    CamelCase,
    LowerCase,
    KebabCase,
    SnakeCase
}

to be used with attributes discussed prior, for example

[assembly: FileSystemRouting(
    NamingPolicy = NamingPolicy.LowerCase
)]

Base path

Endpoints don't always have to be placed in a directory at the root of the project. For various reasons, they can be placed elsewhere. A way should be provided to define the base directory. For example:

// location: Api/Handlers/Product/Create.cs

[assembly: FileSystemRouting(
    RootDirectory = "Api/Handlers"
)]

[Handler]
[MapPost] // maps it to POST: /product
public static partial class CreateProduct
{
    // ...
}

Source generator limitations

File location might not be available to a source generator. In such a case, should the namespace be used as a close approximation?

@Atulin
Copy link
Author

Atulin commented Aug 26, 2024

A couple more things that came to my head as I was moving all my APIs to IA:

Areas

My project uses areas, particularly for admin-only endpoints. Would be nice to support areas somehow.

MyProject
|— Api
|  |— One.cs
|  \— Two.cs
|— Areas
|  |— Admin
|  |  \— Three.cs
|  |— Unga
|  |  |— Bunga
|  |  |  \— Four.cs

The above should produce endpoints:

  • /one
  • /two
  • /admin/three
  • /unga/bunga/four

Whether it's achieved by convention like ASP does it by default, that is treating each directory in Area as a root for the purposes of routing, or with some kind of config... not sure.

Versioning

Similarly to the above, API versioning is often handled with separate directories as well:

MyProject
|— Api
|  |— V1
|  |  \— One.cs
|  |— V2
|  |  \— Two.cs

which should work out of the box, seeing how they are directories after all, but perhaps something more needs to be done to make it work with NSwag, Swashbuckle, and the bunch.

Route Patterns...?

Both of the above, as well as any potential future advanced uses could maybe be handled by a route pattern. It could use RegEx, and the route would consist of consecutive capture groups

[assembly: FileSystemRouting(
    NamingPolicy = NamingPolicy.LowerCase,
    RoutePattern = "App\.(?:Areas\.)?(\w+).(.+)"
)]

(regex101)

The above would produce

Namespace + File Route
App.Areas.Admin.GetThing,cs /admin/getthing
App.Api.GetStuff.cs /api/getstuff
App.Api.V1.MakeDo.cs /api/v1/makedo
App.Areas.Unga.Bunga.DeleteTing.cs /unga/bunga/deleteting

Additionally — and I'm probably getting into the area of way overcomplicating what started as a simple proposal — it could support named capture groups and a template using those names:

[assembly: FileSystemRouting(
    NamingPolicy = NamingPolicy.LowerCase,
    RoutePattern = "App\.(?:Areas\.)?(?<root>\w+)\.?(?<version>V\d)?\.(.+)",
    RouteTemplate = "[root]/[version?]/[...]"
)]

(regex101)

which, with the following placeholder meanings

  • [name] replace with the contents of capture group name
  • [name?] replace with the contents of capture group name or leave empty and remove the resulting double-slash if the group does not exist
  • [...] the rest of the path, coming from the last unnamed capture group

would produce the same.

@Atulin
Copy link
Author

Atulin commented Aug 30, 2024

Another thought... What if it could make use of MapGroup...?

Api
|— Foo
|  |— GetFoo.cs
|  |— MakeFoo.cs
|  \— FooGroup.cs
\— ApiGroup.cs
// ApiGroup.cs
[MapGroup("api")]
public static partial class ApiGroup;
// FooGroup.cs
[MapGroup("foo")]
public static partial class FooGroup
{
    private static partial RouteGroupBuilder Configure(RouteGroupBuilder group) => group
        .WithTags("foo")
        .RequireAuthorization()
        .WhateverOtherConfig();
}

which would produce code akin to

FooGroup.Configure(builder
    .MapGroup("api")
    .MapGroup("foo"))
        .MapGet("/", GetFoo.Handler)
        .MapPost("/", MakeFoo.Handler);

or whatever Minimal APIs would require.

Groups could be either resolved automagically, based on the namespaces, or if that's not feasible, an attribute like [Group(name)] could be used. For example:

[MapGroup("api")]
public static partial class ApiGroup;

[MapGroup("foo")]
[Group(nameof(ApiGroup))]
public static partial class FooGroup;

[MapGet]
[Handler]
[Group(nameof(FooGroup))]
public static partial class WhateverHandler;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant