diff --git a/aspnetcore/tutorials/min-web-api.md b/aspnetcore/tutorials/min-web-api.md index 911f8086bf3c..6991bcb3a982 100644 --- a/aspnetcore/tutorials/min-web-api.md +++ b/aspnetcore/tutorials/min-web-api.md @@ -1,10 +1,11 @@ --- title: "Tutorial: Create a Minimal API with ASP.NET Core" author: wadepickett -description: Learn how to build a Minimal API with ASP.NET Core. +description: Learn how to build a minimal API with ASP.NET Core. +ai-usage: ai-assisted ms.author: wpickett -ms.date: 07/29/2024 ms.custom: engagement-fy24 +ms.date: 02/12/2026 monikerRange: '>= aspnetcore-6.0' uid: tutorials/min-web-api --- @@ -33,6 +34,7 @@ This tutorial creates the following API: | `GET /todoitems/{id}` | Get an item by ID | None | To-do item | | `POST /todoitems` | Add a new item | To-do item | To-do item | | `PUT /todoitems/{id}` | Update an existing item   | To-do item | None | +| `PATCH /todoitems/{id}` | Partially update an item  | Partial to-do item | None | | `DELETE /todoitems/{id}`     | Delete an item     | None | None | ## Prerequisites @@ -513,6 +515,81 @@ Use Swagger to send a PUT request: --- +## Examine the PATCH endpoint + +Create a file named `TodoPatchDto.cs` with the following code: + +:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todo/TodoPatchDto.cs"::: + +The `TodoPatchDto` class uses nullable properties (`string?` and `bool?`) to distinguish between a field that wasn't provided in the request versus a field explicitly set to a value. + +The sample app implements a single PATCH endpoint using `MapPatch`: + +[!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo/Program.cs?name=snippet_patch)] + +This method is similar to the `MapPut` method, except it uses HTTP PATCH and only updates the fields provided in the request. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PATCH request enables partial updates, allowing clients to send only the fields that need to be changed. + +The PATCH endpoint uses a `TodoPatchDto` class with nullable properties to properly handle partial updates. Using nullable properties allows the endpoint to distinguish between a field that wasn't provided (null) versus a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool would default to false, potentially overwriting an existing true value when that field wasn't included in the request. + +> [!NOTE] +> PATCH operations allow partial updates to resources. For more advanced partial updates using JSON Patch documents, see . + +## Test the PATCH endpoint + +This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PATCH call. Call GET to ensure there's an item in the database before making a PATCH call. + +Update only the `name` property of the to-do item that has `Id = 1` and set its name to `"run errands"`. + +# [Visual Studio](#tab/visual-studio) + +* In **Endpoints Explorer**, right-click the **PATCH** endpoint, and select **Generate request**. + + The following content is added to the `TodoApi.http` file: + + ```http + PATCH {{TodoApi_HostAddress}}/todoitems/{id} + + ### + ``` + +* In the PATCH request line, replace `{id}` with `1`. + +* Add the following lines immediately after the PATCH request line: + + ```http + Content-Type: application/json + + { + "name": "run errands" + } + ``` + + The preceding code adds a Content-Type header and a JSON request body with only the field to update. + +* Select the **Send request** link that is above the new PATCH request line. + + The PATCH request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204. + +# [Visual Studio Code](#tab/visual-studio-code) + +Use Swagger to send a PATCH request: + +* Select **Patch /todoitems/{id}** > **Try it out**. + +* Set the **id** field to `1`. + +* Set the request body to the following JSON: + + ```json + { + "name": "run errands" + } + ``` + +* Select **Execute**. + +--- + ## Examine and test the DELETE endpoint The sample app implements a single DELETE endpoint using `MapDelete`: diff --git a/aspnetcore/tutorials/min-web-api/includes/min-web-api6-7.md b/aspnetcore/tutorials/min-web-api/includes/min-web-api6-7.md index d92866593e91..e6a5fd95fcea 100644 --- a/aspnetcore/tutorials/min-web-api/includes/min-web-api6-7.md +++ b/aspnetcore/tutorials/min-web-api/includes/min-web-api6-7.md @@ -15,6 +15,7 @@ This tutorial creates the following API: | `GET /todoitems/{id}` | Get an item by ID | None | To-do item | | `POST /todoitems` | Add a new item | To-do item | To-do item | | `PUT /todoitems/{id}` | Update an existing item   | To-do item | None | +| `PATCH /todoitems/{id}` | Partially update an item  | Partial to-do item | None | | `DELETE /todoitems/{id}`     | Delete an item     | None | None | ## Prerequisites @@ -320,6 +321,47 @@ Use Swagger to send a PUT request: * Select **Execute**. +## Examine the PATCH endpoint + +Create a file named `TodoPatchDto.cs` with the following code: + +:::code language="csharp" source="~/tutorials/min-web-api/samples/7.x/todo/TodoPatchDto.cs"::: + +The `TodoPatchDto` class uses nullable properties (`string?` and `bool?`) to distinguish between a field that wasn't provided in the request versus a field explicitly set to a value. + +The sample app implements a single PATCH endpoint using `MapPatch`: + +[!code-csharp[](~/tutorials/min-web-api/samples/7.x/todo/Program.cs?name=snippet_patch)] + +This method is similar to the `MapPut` method, except it uses HTTP PATCH and only updates the fields provided in the request. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PATCH request enables partial updates, allowing clients to send only the fields that need to be changed. + +The PATCH endpoint uses a `TodoPatchDto` class with nullable properties to properly handle partial updates. Using nullable properties allows the endpoint to distinguish between a field that wasn't provided (null) versus a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool would default to false, potentially overwriting an existing true value when that field wasn't included in the request. + +> [!NOTE] +> PATCH operations allow partial updates to resources. For more advanced partial updates using JSON Patch documents, see . + +## Test the PATCH endpoint + +This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PATCH call. Call GET to ensure there's an item in the database before making a PATCH call. + +Update only the `name` property of the to-do item that has `Id = 1` and set its name to `"run errands"`. + +Use Swagger to send a PATCH request: + +* Select **Patch /todoitems/{id}** > **Try it out**. + +* Set the **id** field to `1`. + +* Set the request body to the following JSON: + + ```json + { + "name": "run errands" + } + ``` + +* Select **Execute**. + ## Examine and test the DELETE endpoint The sample app implements a single DELETE endpoint using `MapDelete`: diff --git a/aspnetcore/tutorials/min-web-api/includes/min-web-api8.md b/aspnetcore/tutorials/min-web-api/includes/min-web-api8.md index 7ec7a397cd4c..fbf2ba1217c4 100644 --- a/aspnetcore/tutorials/min-web-api/includes/min-web-api8.md +++ b/aspnetcore/tutorials/min-web-api/includes/min-web-api8.md @@ -15,6 +15,7 @@ This tutorial creates the following API: | `GET /todoitems/{id}` | Get an item by ID | None | To-do item | | `POST /todoitems` | Add a new item | To-do item | To-do item | | `PUT /todoitems/{id}` | Update an existing item   | To-do item | None | +| `PATCH /todoitems/{id}` | Partially update an item  | Partial to-do item | None | | `DELETE /todoitems/{id}`     | Delete an item     | None | None | ## Prerequisites @@ -491,6 +492,81 @@ Use Swagger to send a PUT request: --- +## Examine the PATCH endpoint + +Create a file named `TodoPatchDto.cs` with the following code: + +:::code language="csharp" source="~/tutorials/min-web-api/samples/8.x/todo/TodoPatchDto.cs"::: + +The `TodoPatchDto` class uses nullable properties (`string?` and `bool?`) to distinguish between a field that wasn't provided in the request versus a field explicitly set to a value. + +The sample app implements a single PATCH endpoint using `MapPatch`: + +[!code-csharp[](~/tutorials/min-web-api/samples/8.x/todo/Program.cs?name=snippet_patch)] + +This method is similar to the `MapPut` method, except it uses HTTP PATCH and only updates the fields provided in the request. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PATCH request enables partial updates, allowing clients to send only the fields that need to be changed. + +The PATCH endpoint uses a `TodoPatchDto` class with nullable properties to properly handle partial updates. Using nullable properties allows the endpoint to distinguish between a field that wasn't provided (null) versus a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool would default to false, potentially overwriting an existing true value when that field wasn't included in the request. + +> [!NOTE] +> PATCH operations allow partial updates to resources. For more advanced partial updates using JSON Patch documents, see . + +## Test the PATCH endpoint + +This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PATCH call. Call GET to ensure there's an item in the database before making a PATCH call. + +Update only the `name` property of the to-do item that has `Id = 1` and set its name to `"run errands"`. + +# [Visual Studio](#tab/visual-studio) + +* In **Endpoints Explorer**, right-click the **PATCH** endpoint, and select **Generate request**. + + The following content is added to the `TodoApi.http` file: + + ```http + PATCH {{TodoApi_HostAddress}}/todoitems/{id} + + ### + ``` + +* In the PATCH request line, replace `{id}` with `1`. + +* Add the following lines immediately after the PATCH request line: + + ```http + Content-Type: application/json + + { + "name": "run errands" + } + ``` + + The preceding code adds a Content-Type header and a JSON request body with only the field to update. + +* Select the **Send request** link that is above the new PATCH request line. + + The PATCH request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204. + +# [Visual Studio Code](#tab/visual-studio-code) + +Use Swagger to send a PATCH request: + +* Select **Patch /todoitems/{id}** > **Try it out**. + +* Set the **id** field to `1`. + +* Set the request body to the following JSON: + + ```json + { + "name": "run errands" + } + ``` + +* Select **Execute**. + +--- + ## Examine and test the DELETE endpoint The sample app implements a single DELETE endpoint using `MapDelete`: diff --git a/aspnetcore/tutorials/min-web-api/samples/7.x/todo/Program.cs b/aspnetcore/tutorials/min-web-api/samples/7.x/todo/Program.cs index 54881cf93541..0cd1540ced1f 100644 --- a/aspnetcore/tutorials/min-web-api/samples/7.x/todo/Program.cs +++ b/aspnetcore/tutorials/min-web-api/samples/7.x/todo/Program.cs @@ -1,4 +1,4 @@ -#define FINAL // MINIMAL FINAL TYPEDR +#define FINAL // MINIMAL FINAL WITHPATCH TYPEDR #if MINIMAL // var builder = WebApplication.CreateBuilder(args); @@ -77,6 +77,77 @@ is Todo todo app.Run(); // +#elif WITHPATCH +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); +var app = builder.Build(); + +app.MapGet("/todoitems", async (TodoDb db) => + await db.Todos.ToListAsync()); + +app.MapGet("/todoitems/complete", async (TodoDb db) => + await db.Todos.Where(t => t.IsComplete).ToListAsync()); + +app.MapGet("/todoitems/{id}", async (int id, TodoDb db) => + await db.Todos.FindAsync(id) + is Todo todo + ? Results.Ok(todo) + : Results.NotFound()); + +app.MapPost("/todoitems", async (Todo todo, TodoDb db) => +{ + db.Todos.Add(todo); + await db.SaveChangesAsync(); + + return Results.Created($"/todoitems/{todo.Id}", todo); +}); + +app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) => +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return Results.NotFound(); + + todo.Name = inputTodo.Name; + todo.IsComplete = inputTodo.IsComplete; + + await db.SaveChangesAsync(); + + return Results.NoContent(); +}); + +// +app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) => +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return Results.NotFound(); + + if (inputTodo.Name is not null) todo.Name = inputTodo.Name; + if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value; + + await db.SaveChangesAsync(); + + return Results.NoContent(); +}); +// + +app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) => +{ + if (await db.Todos.FindAsync(id) is Todo todo) + { + db.Todos.Remove(todo); + await db.SaveChangesAsync(); + return Results.NoContent(); + } + + return Results.NotFound(); +}); + +app.Run(); #elif TYPEDR using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; diff --git a/aspnetcore/tutorials/min-web-api/samples/7.x/todo/TodoPatchDto.cs b/aspnetcore/tutorials/min-web-api/samples/7.x/todo/TodoPatchDto.cs new file mode 100644 index 000000000000..ab225cf2280b --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/7.x/todo/TodoPatchDto.cs @@ -0,0 +1,5 @@ +public class TodoPatchDto +{ + public string? Name { get; set; } + public bool? IsComplete { get; set; } +} diff --git a/aspnetcore/tutorials/min-web-api/samples/8.x/todo/Program.cs b/aspnetcore/tutorials/min-web-api/samples/8.x/todo/Program.cs index 54881cf93541..0cd1540ced1f 100644 --- a/aspnetcore/tutorials/min-web-api/samples/8.x/todo/Program.cs +++ b/aspnetcore/tutorials/min-web-api/samples/8.x/todo/Program.cs @@ -1,4 +1,4 @@ -#define FINAL // MINIMAL FINAL TYPEDR +#define FINAL // MINIMAL FINAL WITHPATCH TYPEDR #if MINIMAL // var builder = WebApplication.CreateBuilder(args); @@ -77,6 +77,77 @@ is Todo todo app.Run(); // +#elif WITHPATCH +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); +var app = builder.Build(); + +app.MapGet("/todoitems", async (TodoDb db) => + await db.Todos.ToListAsync()); + +app.MapGet("/todoitems/complete", async (TodoDb db) => + await db.Todos.Where(t => t.IsComplete).ToListAsync()); + +app.MapGet("/todoitems/{id}", async (int id, TodoDb db) => + await db.Todos.FindAsync(id) + is Todo todo + ? Results.Ok(todo) + : Results.NotFound()); + +app.MapPost("/todoitems", async (Todo todo, TodoDb db) => +{ + db.Todos.Add(todo); + await db.SaveChangesAsync(); + + return Results.Created($"/todoitems/{todo.Id}", todo); +}); + +app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) => +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return Results.NotFound(); + + todo.Name = inputTodo.Name; + todo.IsComplete = inputTodo.IsComplete; + + await db.SaveChangesAsync(); + + return Results.NoContent(); +}); + +// +app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) => +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return Results.NotFound(); + + if (inputTodo.Name is not null) todo.Name = inputTodo.Name; + if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value; + + await db.SaveChangesAsync(); + + return Results.NoContent(); +}); +// + +app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) => +{ + if (await db.Todos.FindAsync(id) is Todo todo) + { + db.Todos.Remove(todo); + await db.SaveChangesAsync(); + return Results.NoContent(); + } + + return Results.NotFound(); +}); + +app.Run(); #elif TYPEDR using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; diff --git a/aspnetcore/tutorials/min-web-api/samples/8.x/todo/TodoPatchDto.cs b/aspnetcore/tutorials/min-web-api/samples/8.x/todo/TodoPatchDto.cs new file mode 100644 index 000000000000..ab225cf2280b --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/8.x/todo/TodoPatchDto.cs @@ -0,0 +1,5 @@ +public class TodoPatchDto +{ + public string? Name { get; set; } + public bool? IsComplete { get; set; } +} diff --git a/aspnetcore/tutorials/min-web-api/samples/9.x/todo/Program.cs b/aspnetcore/tutorials/min-web-api/samples/9.x/todo/Program.cs index 54881cf93541..0cd1540ced1f 100644 --- a/aspnetcore/tutorials/min-web-api/samples/9.x/todo/Program.cs +++ b/aspnetcore/tutorials/min-web-api/samples/9.x/todo/Program.cs @@ -1,4 +1,4 @@ -#define FINAL // MINIMAL FINAL TYPEDR +#define FINAL // MINIMAL FINAL WITHPATCH TYPEDR #if MINIMAL // var builder = WebApplication.CreateBuilder(args); @@ -77,6 +77,77 @@ is Todo todo app.Run(); // +#elif WITHPATCH +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); +var app = builder.Build(); + +app.MapGet("/todoitems", async (TodoDb db) => + await db.Todos.ToListAsync()); + +app.MapGet("/todoitems/complete", async (TodoDb db) => + await db.Todos.Where(t => t.IsComplete).ToListAsync()); + +app.MapGet("/todoitems/{id}", async (int id, TodoDb db) => + await db.Todos.FindAsync(id) + is Todo todo + ? Results.Ok(todo) + : Results.NotFound()); + +app.MapPost("/todoitems", async (Todo todo, TodoDb db) => +{ + db.Todos.Add(todo); + await db.SaveChangesAsync(); + + return Results.Created($"/todoitems/{todo.Id}", todo); +}); + +app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) => +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return Results.NotFound(); + + todo.Name = inputTodo.Name; + todo.IsComplete = inputTodo.IsComplete; + + await db.SaveChangesAsync(); + + return Results.NoContent(); +}); + +// +app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) => +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return Results.NotFound(); + + if (inputTodo.Name is not null) todo.Name = inputTodo.Name; + if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value; + + await db.SaveChangesAsync(); + + return Results.NoContent(); +}); +// + +app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) => +{ + if (await db.Todos.FindAsync(id) is Todo todo) + { + db.Todos.Remove(todo); + await db.SaveChangesAsync(); + return Results.NoContent(); + } + + return Results.NotFound(); +}); + +app.Run(); #elif TYPEDR using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; diff --git a/aspnetcore/tutorials/min-web-api/samples/9.x/todo/TodoPatchDto.cs b/aspnetcore/tutorials/min-web-api/samples/9.x/todo/TodoPatchDto.cs new file mode 100644 index 000000000000..ab225cf2280b --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/9.x/todo/TodoPatchDto.cs @@ -0,0 +1,5 @@ +public class TodoPatchDto +{ + public string? Name { get; set; } + public bool? IsComplete { get; set; } +}