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 CRUD/EF scaffolding of Minimal APIs #1787

Closed
DamianEdwards opened this issue Jan 7, 2022 · 2 comments · Fixed by #1848
Closed

Content for CRUD/EF scaffolding of Minimal APIs #1787

DamianEdwards opened this issue Jan 7, 2022 · 2 comments · Fixed by #1848
Assignees
Milestone

Comments

@DamianEdwards
Copy link
Member

This issue covers delivering scaffolders for building web APIs using the new Minimal APIs style of endpoints in .NET 6+ that are basically equivalent to the existing scaffolders for Web API that exist today.

Starting with an existing model class, e.g. Widget, there are two scaffold items to select from:

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

Each of these items supports an OpenAPI/Swagger option, making for a total of four different outputs:

  • 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)

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.

Sample generated code

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)
    {
        routes.MapGet("/api/widgets", () =>
        {
            return new [] { new Widget() };
        })
        .WithName("GetAllWidgets");

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

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

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

        routes.MapDelete("/api/widgets/{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)
    {
        routes.MapGet("/api/widgets", () =>
        {
            return new [] { new Widget() };
        })
        .WithName("GetAllWidgets")
        .Produces<Widget[]>(StatusCodes.Status200OK);

        routes.MapGet("/api/widgets/{id}", (int id) =>
        {
            return new Widget { Id = id };
        })
        .WithName("GetWidgetById")
        .Produces<Widget>(StatusCodes.Status200OK);

        routes.MapPut("/api/widgets/{id}", (int id, Widget input) =>
        {
            return Results.NoContent();
        })
        .WithName("UpdateWidget")
        .Produces(StatusCodes.Status204NoContent);

        routes.MapPost("/api/widgets/", (Widget widget) =>
        {
            return Results.Created($"/widgets/{widget.Id}", widget);
        })
        .WithName("CreateWidget")
        .Produces<Widget>(StatusCodes.Status201Created);

        routes.MapDelete("/api/widgets/{id}", (int id) =>
        {
            return Results.Ok(new Widget { Id = id });
        })
        .WithName("DeleteWidget")
        .Produces<Widget>(StatusCodes.Status200OK);
    }
}

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)
    {
        routes.MapGet("/api/widgets", async (AppDbContext db) =>
        {
            return await db.Widgets.ToListAsync();
        })
        .WithName("GetAllWidgets");

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

        routes.MapPut("/api/widgets/{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");

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

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

        routes.MapDelete("/api/widgets/{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)
    {
        routes.MapGet("/api/widgets", async (AppDbContext db) =>
        {
            return await db.Widgets.ToListAsync();
        })
        .WithName("GetAllWidgets")
        .Produces<List<Widget>>(StatusCodes.Status200OK);

        routes.MapGet("/api/widgets/{id}", async (int id, AppDbContext db) =>
        {
            return await db.Widgets.FindAsync(id)
                is Widget widget
                    ? Results.Ok(widget)
                    : Results.NotFound();
        })
        .WithName("GetWidgetById")
        .Produces<Widget>(StatusCodes.Status200OK)
        .Produces(StatusCodes.Status404NotFound);

        routes.MapPut("/api/widgets/{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")
        .Produces(StatusCodes.Status404NotFound)
        .Produces(StatusCodes.Status204NoContent);

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

            return Results.Created($"/widgets/{widget.Id}", widget);
        })
        .WithName("CreateWidget")
        .Produces<Widget>(StatusCodes.Status201Created);

        routes.MapDelete("/api/widgets/{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")
        .Produces<Widget>(StatusCodes.Status200OK)
        .Produces(StatusCodes.Status404NotFound);
    }
}
@ajcvickers
Copy link
Contributor

/cc @JeremyLikness

@vijayrkn vijayrkn added this to the .NET 7.0 milestone Jan 31, 2022
@mviegas
Copy link

mviegas commented Feb 11, 2022

This looks promising folks. It'd be a great addition for quick boilerplate setups! :)

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

Successfully merging a pull request may close this issue.

5 participants