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

Content for Minimal APIs scaffolding with .NET 7 features #1937

Closed
jcjiang opened this issue Jun 15, 2022 · 12 comments
Closed

Content for Minimal APIs scaffolding with .NET 7 features #1937

jcjiang opened this issue Jun 15, 2022 · 12 comments

Comments

@jcjiang
Copy link

jcjiang commented Jun 15, 2022

The team will be rolling out a number of features for .NET 7, some of which we want to include in our scaffolding to make the new functionalities more easily adopted.

Existing scaffolded items include:

  • Minimal API with read/write endpoints
  • Minimal API with read/write endpoints (with OpenAPI)
  • Minimal API with endpoints, using Entity Framework
  • Minimal API with endpoints, using Entity Framework (with OpenAPI)

List of our .NET 7 features:

  • Route groups: allows devs to relate their endpoints by putting them into the same group which can be decorated with metadata
  • Endpoint filters: allows users to implement parameter validation or custom response processing
  • Results API improvements: easier testing and interacting
  • Improved OpenAPI integration: support for endpoint metadata providers

Ideas for scaffolded items:

  • Minimal API with a route group
  • Minimal API with a route group (with OpenAPI)
  • Minimal API with endpoints, using filters
  • Minimal API with endpoints, using filters (with OpenAPI)
  • Minimal API with endpoints, using Result API
  • Minimal API with endpoints, using Result API (with OpenAPI)
  • Minimal API with endpoints (with OpenAPI using an endpoint metadata provider)
  • ideas from EF team, C# folks, etc.

From the previous minimal API issue:

When OpenAPI is opted-in to, the required packages and startup configuration code should be applied to the project.

For the items using Entity Framework, just like the existing Web API scaffolders, the user can select to have a DbContext created or select an existing DbContext. The appropriate packages and startup configuration code should be applied to project for Entity Framework based on the user's selection.

In lieu of selecting a controller name, the user will select a class name that will contain the generated endpoint mapping code. If the class doesn't exist, a new static class will be created and the extension method added to it. If the class already exists, it must be static, and the extension method will be added to it. The name of the class will default to [ModelClass]Endpoints, e.g. given model class Widget the class will be named WidgetEndpoints. The static method will be named Map[ModelClass]Endpoints, e.g. given model class Widget the method will be names MapWidgetEndpoints.

The application startup code should be updated by the scaffolder to call the generated extension method that maps the endpoints.

@DamianEdwards
Copy link
Member

We should figure out if we want to just switch the scaffolders to using the new TypedResults and Results<TResult1, TResultN> types or whether that's an option. We should likely be conservative WRT to options as they can add significant cost to authoring, testing, and maintaining the scaffolders.

@jcjiang
Copy link
Author

jcjiang commented Jun 16, 2022

Spoke to Kathleen Dollard about new C# and Roslyn features we may want to accommodate. An updated list can be found here:
https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md

Sounds like there shouldn't be any features that we will need to support or add. Main C#/Roslyn updates are:

  • Parsing strings that contain UTF-8 information - improvements have been made to improve the process
  • Defaults for structs - i.e. when x is created as an integer, it is automatically assigned 0
  • improvements to JSON that's being created by hand - I don't think we do that now, so shouldn't be a problem
  • attributes on top-level statements - IIRC we're already using top-level statements so we are not affected

@DamianEdwards
Copy link
Member

Strawman proposal based on what we did for .NET 6 plus new Minimal API features:

Sample generated code updated for .NET 7

Given the existing model class Widget defined as follows:

namespace WebApplication126;

public class Widget
{
    public int Id { get; set; }

    public string? Name { get; set; }
}

Basic CRUD without OpenAPI

The following class will be generated for the basic CRUD case without OpenAPI:

namespace WebApplication126;

public static class WidgetEndpoints
{
    public static void MapWidgetEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/widgets");

        group.MapGet("/", () =>
        {
            return new [] { new Widget() };
        })
        .WithName("GetAllWidgets");

        group.MapGet("/{id}", (int id) =>
        {
            return new Widget { Id = id };
        })
        .WithName("GetWidgetById");

        group.MapPut("/{id}", (int id, Widget input) =>
        {
            return Results.NoContent();
        })
        .WithName("UpdateWidget");

        group.MapPost("/", (Widget widget) =>
        {
            return Results.Created($"{group.RoutePrefix}/{widget.Id}", widget);
        })
        .WithName("CreateWidget");

        group.MapDelete("/{id}", (int id) =>
        {
            return Results.Ok(new Widget { Id = id });
        })
        .WithName("DeleteWidget");
    }
}

Basic CRUD with OpenAPI

The following class will be generated for the basic CRUD case without OpenAPI:

namespace WebApplication126;

public static class WidgetEndpoints
{
    public static void MapWidgetEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/widgets");
        group.WithOpenApi();

        group.MapGet("/", () =>
        {
            return new [] { new Widget() };
        })
        .WithName("GetAllWidgets");

        group.MapGet("/{id}", (int id) =>
        {
            return new Widget { Id = id };
        })
        .WithName("GetWidgetById");

        group.MapPut("/{id}", (int id, Widget input) =>
        {
            return TypedResults.NoContent();
        })
        .WithName("UpdateWidget");

        group.MapPost("/", (Widget widget) =>
        {
            return TypedResults.Created($"{group.RoutePrefix}/{widget.Id}", widget);
        })
        .WithName("CreateWidget");

        group.MapDelete("/{id}", (int id) =>
        {
            return TypedResults.Ok(new Widget { Id = id });
        })
        .WithName("DeleteWidget");
    }
}

Entity Framework without OpenAPI

The following class will be generated for the Entity Framework case without OpenAPI:

using Microsoft.EntityFrameworkCore;

namespace WebApplication126;

public static class WidgetEndpoints
{
    public static void MapWidgetEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/widgets");

        group.MapGet("/", async (AppDbContext db) =>
        {
            return await db.Widgets.ToListAsync();
        })
        .WithName("GetAllWidgets");

        group.MapGet("/{id}", async (int id, AppDbContext db) =>
        {
            return await db.Widgets.FindAsync(id)
                is Widget widget
                    ? Results.Ok(widget)
                    : Results.NotFound();
        })
        .WithName("GetWidgetById");

        group.MapPut("/{id}", async (int id, Widget input, AppDbContext db) =>
        {
            var widget = await db.Widgets.FindAsync(id);

            if (widget is null)
            {
                return Results.NotFound();
            }

            widget.Name = input.Name;

            await db.SaveChangesAsync();

            return Results.NoContent();
        })
        .WithName("UpdateWidget");

        group.MapPost("/", async (Widget widget, AppDbContext db) =>
        {
            db.Widgets.Add(widget);
            await db.SaveChangesAsync();

            return Results.Created($"{group.RoutePrefix}/{widget.Id}", widget);
        })
        .WithName("CreateWidget");

        group.MapDelete("/{id}", async (int id, AppDbContext db) =>
        {
            if (await db.Widgets.FindAsync(id) is Widget widget)
            {
                db.Widgets.Remove(widget);
                await db.SaveChangesAsync();
                return Results.Ok(widget);
            }

            return Results.NotFound();
        })
        .WithName("DeleteWidget");
    }
}

Entity Framework with OpenAPI

The following class will be generated for the Entity Framework case with OpenAPI:

using Microsoft.EntityFrameworkCore;

namespace WebApplication126;

public static class WidgetEndpoints
{
    public static void MapWidgetEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/widgets");
        group.WithOpenApi();

        group.MapGet("/", async (AppDbContext db) =>
        {
            return await db.Widgets.ToListAsync();
        })
        .WithName("GetAllWidgets");

        group.MapGet("/{id}", async Task<Results<Ok<Widget>, NotFound>> (int id, AppDbContext db) =>
        {
            return await db.Widgets.FindAsync(id)
                is Widget widget
                    ? TypedResults.Ok(widget)
                    : TypedResults.NotFound();
        })
        .WithName("GetWidgetById");

        group.MapPut("/{id}", async Task<Results<NotFound, NoContent>> (int id, Widget input, AppDbContext db) =>
        {
            var widget = await db.Widgets.FindAsync(id);

            if (widget is null)
            {
                return TypedResults.NotFound();
            }

            widget.Name = input.Name;

            await db.SaveChangesAsync();

            return TypedResults.NoContent();
        })
        .WithName("UpdateWidget");

        group.MapPost("/", async (Widget widget, AppDbContext db) =>
        {
            db.Widgets.Add(widget);
            await db.SaveChangesAsync();

            return TypedResults.Created($"/widgets/{widget.Id}", widget);
        })
        .WithName("CreateWidget");

        group.MapDelete("/{id}", async Task<Results<Ok<Widget>, NotFound>> (int id, AppDbContext db) =>
        {
            if (await db.Widgets.FindAsync(id) is Widget widget)
            {
                db.Widgets.Remove(widget);
                await db.SaveChangesAsync();
                return TypedResults.Ok(widget);
            }

            return TypedResults.NotFound();
        })
        .WithName("DeleteWidget");
    }
}

@dotnet dotnet deleted a comment from jcjiang Jun 17, 2022
@deepchoudhery
Copy link
Member

Apologies for the delay, PR is up here

@captainsafia
Copy link
Member

captainsafia commented Aug 29, 2022

Does it make sense to make the return type for these a RouteGroupBuilder or IEndpointRouteBuilder so that users can continue to modify the output? It seems to align more cleanly to me with the other builder patterns we have.

public static void MapWidgetEndpoints(this IEndpointRouteBuilder routes)

WithOpenApi integration in these looks good. One thing to note is that it'll depend on RC1 since we did some work to fix up route groups + WithOpenApi in RC1.

@DamianEdwards
Copy link
Member

Does it make sense to make the return type for these a RouteGroupBuilder or IEndpointRouteBuilder so that users can continue to modify the output? It seems to align more cleanly to me with the other builder patterns we have.

Yes I think the static method should return IEndpointRouteBuilder for the reasons you cite. I wouldn't return RouteGroupBuilder as in my mind that's an implementation detail of the method that shouldn't "leak" out.

Do folks think we should add a scaffolder option RE the use of TypedResults. and Results<TResult1, ..> vs. Results. and the inferred IResult along with the Produces extension method in the case of Open API?

@deepchoudhery
Copy link
Member

@DamianEdwards @captainsafia Should I make this change in the current PR?

@vijayrkn
Copy link
Contributor

vijayrkn commented Oct 4, 2022

Closing this since the base support is checked in. Will use follow-up issues for specific open items.

@vijayrkn vijayrkn closed this as completed Oct 4, 2022
@pstricks-fans
Copy link

The following

group.MapPost("/", (Widget widget) =>
{
     return Results.Created($"{group.RoutePrefix}/{widget.Id}", widget);
})
.WithName("CreateWidget");

generates

'RouteGroupBuilder' does not contain a definition for 'RoutePrefix' and no accessible extension method 'RoutePrefix' accepting a first argument of type ...

Where is RoutePrefix defined?

@captainsafia
Copy link
Member

@pstricks-fans RoutePrefix isn't defined on the RouteGroupBuilder type. Is the code that you pasted above generated by the scaffold or something you've written? Based on this codeblock, this doesn't look like the generated code.

@pstricks-fans
Copy link

@captainsafia I wrote the code based on Damien Edwards' code in the section Basic CRUD without OpenAPI. It was not generated by scaffolding. Thank you.

@DamianEdwards
Copy link
Member

I think the speclet there was based on an earlier design of route groups. Whatever the scaffolding produces now is what we'd recommend you use as a starting point.

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

No branches or pull requests

6 participants