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

User learning arc for new minimal/focused ASP.NET Core API experience #30580

Closed
DamianEdwards opened this issue Mar 3, 2021 · 12 comments
Closed
Assignees
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels feature-minimal-actions Controller-like actions for endpoint routing User Story A single user-facing feature. Can be grouped under an epic.
Milestone

Comments

@DamianEdwards
Copy link
Member

DamianEdwards commented Mar 3, 2021

This code sample is intended to represent the coming together of the various efforts associated with "simplification" and "scaling-down" of C# HTTP API apps into a progressive narrative example of how one might learn or be taught the concepts with reasonably scaled steps during the grow-up journey.

This example uses a number of features (some existing, some committed, some proposed) including:

  • C# top level statements
  • C# global usings
  • C# using static directive
  • C# implicit MethodGroup -> Delegate cast
  • C# type deconstruction, e.g. var (todo, isValid) = inputTodo; // Validated<Todo> inputTodo
  • C# pattern matching, e.g. return await db.Todos.FindAsync(id) is Todo todo ? Ok(todo) : NotFound();
  • single-file apps (i.e. runnable single file projects, e.g. dotnet run server.cs)
  • simplified ASP.NET Core app host (aka FeatherHTTP, aka "Direct Hosting")
  • lightweight programming model for APIs (aka MapAction)
  • route definition instances, e.g. var myRoute = HttpGet("todo/{id}"); myRoute.Url(todo.Id);
  • higher level service registration methods, e.g. builder.AddSqlite<EntityType>(string connectionString)
  • non-DI scenarios

Not all concepts are necessarily required for an MVP (e.g. in an early preview) and we should capture details of how we think about that (TODO), e.g. #! support & global usings aren't required for this to be valuable but they naturally build on the MVP in a way that adds further value.

We'll update this sample in-place as it evolves. In-progress iterations can be captured in gists and linked to in comments, etc.

#!/usr/bin/env dotnet run

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
var connString = config["connectionString"] ?? "Data Source=todos.db";

builder.AddDbContext<TodoDb>(options => options.UseSqlite(connString));
builder.AddSqlite<Todo>(connString) // Higher level API perhaps?

var app = builder.Build();

// Step 0: Request delegate
app.MapGet("/", ctx => ctx.Respsone.WriteAsync("Hello, World!"));

// Step 1: Return string 
app.MapGet("/hello", () => "Hello, World!");

// Step 2: Return custom type
app.MapGet("/todo", () => new Todo("Do the thing"));

// Step 3a: Use a DB (no DI)
app.MapGet("/todos/{id}", async (int id) =>
{
    using var db = new TodoDb(connString);
    return await db.Todos.FindAsync(id) is Todo todo
        ? Ok(todo) : NotFound();
});

// Step 3b: Use a DB (with DI)
app.MapGet("/todos/{id}", async (int id, TodoDb db) =>
{
    return await db.Todos.FindAsync(id) is Todo todo
        ? Ok(todo) : NotFound();
});

app.MapPost("/todos", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await Todos.SaveChangesAsync();

   return CreatedAt($"/todo-db/{todo.Id}", todo);
};

app.MapPut("/todos", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return NotFound();

    todo.Title = inputTodo.Title;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChanges();

    return NoContent();
});

app.MapDelete("/todos", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChanges();
        return OK(todo);
    }

    return NotFound();
});

// Step 4: Referenced routes
var routes = new {
    GetTodo = HttpGet("/todos/{id}")
};
app.MapGet(routes.GetTodo, async (int id, TodoDb db) =>
{
    return await db.Todos.FindAsync(id) is Todo todo
        ? Ok(todo) : NotFound();
});
app.MapPost("/todos", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await Todos.SaveChangesAsync();

   return CreatedAt(routes.GetTodo.Url(todo.Id), todo);
};

// Step 5: Input validation
app.MapPost("/todos", async (Validated<Todo> inputTodo, TodoDb db) =>
{
    var (todo, isValid) = inputTodo;
    if (!isValid) return Problem(inputTodo);

    db.Todos.Add(todo);
    await Todos.SaveChangesAsync();

    return CreatedAt(routes.GetTodo.Url(todo.Id), todo);
};


// Run app
await app.RunAsync();

// Data types
record Todo(string Title)
{
    public bool IsComplete { get; set; }
}

// DI scenario
class TodoDb : DbContext
{
    // Rough edge here potentially with this constructor signature being quite verbose
    // Can we improve this somehow? Ideally this constructor wouldn't be required by default at all
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options)
    {

    }

    public DbSet<Todo> Todos { get; set; }
}

// Non-DI use scenario, this is pretty rough right now, explore improvements?
class TodoDb : DbContext
{
    private readonly string _cs;

    public TodoDb(string connectionString) : base() => _cs = connectionString; 

    protected override OnConfiguring(DbContextOptionsBuilder optionsBuilder) // Non-DI use scenario
    {
        optionsBuilder.UseSqlite(_cs);
    }

    public DbSet<Todo> Todos { get; set; }
}
@DamianEdwards DamianEdwards added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates User Story A single user-facing feature. Can be grouped under an epic. labels Mar 3, 2021
@DamianEdwards DamianEdwards added this to the 6.0.0 milestone Mar 3, 2021
@Pilchie
Copy link
Member

Pilchie commented Mar 15, 2021

👀

@mkArtakMSFT mkArtakMSFT added the feature-minimal-actions Controller-like actions for endpoint routing label Mar 24, 2021
@halter73 halter73 added area-servers and removed area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels Mar 30, 2021
@omariom
Copy link

omariom commented Apr 21, 2021

db.Todos.FindAsync(id) is Todo todo ? Ok(todo) : NotFound();

Might be an issue if after refactoring FindAsync starts returning non-Todo.

May be db.Todos.FindAsync(id) is {} todo ? Ok(todo) : NotFound();?

@davidfowl
Copy link
Member

I don't think that's a realistic concern? If you rename the type Todo, you'll likely rename the DbContext property

@tungphuong
Copy link

Hello, I am trying on minimal API; could you please let me know that "how to apply a custom filter?". Currently, I implemented one HttpGlobalExceptionFilter and registered it in the controller, but it does not work

builder.Services
    .AddEndpointsApiExplorer()
    .AddControllers(o =>
    {
        o.Filters.Add<HttpGlobalExceptionFilter>();
    })
    .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);

@davidfowl
Copy link
Member

There are no filters in minimal APIs, you should be using middleware for something like HttpGlobalExceptionFilter

@tungphuong
Copy link

Many thanks, do you have the document or article to share the limitation or features that will not be included in minimal APIs?

@davidfowl
Copy link
Member

Many thanks, do you have the document or article to share the limitation or features that will not be included in minimal APIs?

It'll be the opposite. We'll have a list of what you CAN do 😄

@tungphuong
Copy link

Can you share its link?

@davidfowl
Copy link
Member

Once it exists, yes 😄

@DamianEdwards
Copy link
Member Author

In the meantime there a few examples of the kinds of things you can do here.

@trontronicent
Copy link

one word: "sexy"!

@rafikiassumani-msft rafikiassumani-msft added the area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Oct 7, 2021
@rafikiassumani-msft
Copy link
Contributor

Closing as some of the remaining items in this issue will be considered for .NET planning. See following issue: #36770

@ghost ghost locked as resolved and limited conversation to collaborators Nov 7, 2021
@amcasey amcasey added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Jun 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels feature-minimal-actions Controller-like actions for endpoint routing User Story A single user-facing feature. Can be grouped under an epic.
Projects
None yet
Development

No branches or pull requests