diff --git a/aspnetcore/tutorials/min-web-api.md b/aspnetcore/tutorials/min-web-api.md index 1152b039d6f7..6fd61b78ecf4 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: Create a Minimal API with ASP.NET Core using Visual Studio or Visual Studio Code. This tutorial covers GET, POST, PUT, PATCH, and DELETE endpoints for a to-do app. ai-usage: ai-assisted ms.author: wpickett -ms.date: 06/03/2026 +ms.reviewer: wpickett +ms.date: 06/28/2026 monikerRange: '>= aspnetcore-6.0' uid: tutorials/min-web-api --- @@ -13,12 +14,11 @@ uid: tutorials/min-web-api [!INCLUDE[](~/includes/not-latest-version.md)] - By [Wade Pickett](https://github.com/wadepickett) and [Tom Dykstra](https://github.com/tdykstra) :::moniker range=">= aspnetcore-10.0" -Minimal APIs are architected to create HTTP APIs with minimal dependencies. They're ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core. +Minimal APIs are designed to create HTTP APIs with minimal dependencies. They're ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core. This tutorial teaches the basics of building a Minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help with choosing between Minimal APIs and controller-based APIs, see . For a tutorial on creating an API project based on [controllers](xref:web-api/index) that contains more features, see [Create a web API](xref:tutorials/first-web-api). @@ -54,30 +54,27 @@ This tutorial creates the following API: * Start Visual Studio 2026 and select **Create a new project**. * In the **Create a new project** dialog: - * Enter `Empty` in the **Search for templates** search box. - * Select the **ASP.NET Core Empty** template and select **Next**. - - ![Visual Studio Create a new project](~/tutorials/min-web-api/static/10/create-new-project-empty-vs18-6-2.png) - -* Name the project *TodoApi* and select **Next**. + * Select the **ASP.NET Core Web API** project type, and select **Next**. + * Name the project *TodoApi*, and select **Next**. * In the **Additional information** dialog: - * Select **.NET 10.0 (Long Term Support)** - * Uncheck **Do not use top-level statements** - * Select **Create** + * Confirm the **Framework** is **.NET 10.0 (Long Term Support)**. + * Confirm the checkbox for **Enable OpenAPI support** is checked. + * Confirm the checkbox for **Use controllers** is **not** checked. Uncheck this setting to create a Minimal API project as required for this tutorial rather than a controller-based one. + * Select **Create**. - ![Additional information](~/tutorials/min-web-api/static/10/add-info-vs18-6-2.png) + ![Additional information](~/tutorials/min-web-api/static/10/add-information-dialog-18-7-1.png) # [Visual Studio Code](#tab/visual-studio-code) -* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal). -* Change directories (`cd`) to the folder that will contain the project folder. +* Start Visual Studio Code, select **View**, and then select **Terminal** to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal). +* Change directory(`cd`) to the folder that contains the project folder. * Run the following commands: - ```dotnetcli - dotnet new web -o TodoApi +```dotnetcli + dotnet new webapi -o TodoApi cd TodoApi code -r ../TodoApi - ``` +``` * When a dialog box asks if you want to trust the authors, select **Yes**. * When a dialog box asks if you want to add required assets to the project, select **Yes**. @@ -88,48 +85,30 @@ This tutorial creates the following API: ### Examine the code -The `Program.cs` file contains the following code: - -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todo/Program.cs" id="snippet_min"::: - -The preceding code: - -* Creates a and a with preconfigured defaults. -* Creates an HTTP GET endpoint `/` that returns `Hello World!`. - -### Run the app +The `Program.cs` file generated by the template contains the following code: # [Visual Studio](#tab/visual-studio) - - -Press Ctrl+F5 to run without the debugger. - -[!INCLUDE[](~/includes/trustCertVS22.md)] - -Visual Studio launches the [Kestrel web server](xref:fundamentals/servers/kestrel) and opens a browser window. - -`Hello World!` is displayed in the browser. The `Program.cs` file contains a minimal but complete app. - -Close the browser window. +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_templatestart"::: # [Visual Studio Code](#tab/visual-studio-code) -[!INCLUDE[](~/includes/trustCertVSC.md)] - -In Visual Studio Code, press Ctrl+F5 (Windows) or control+F5 (macOS) to run the app without debugging. +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_templatestart"::: -The default browser launches with the following URL: `https://localhost:` where `` is the randomly generated port number. +--- -Close the browser window. +The preceding code: -In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press Shift+F5 to stop the app. +* Creates a and a with preconfigured defaults. +* Registers the in-box OpenAPI document generator by using `builder.Services.AddOpenApi()`. +* Maps the generated OpenAPI document at `/openapi/v1.json` by using `app.MapOpenApi()`, in the Development environment only. +* Defines a sample `GET /weatherforecast` endpoint that returns five randomly generated `WeatherForecast` records. ---- +In this tutorial, you replace the `WeatherForecast` with a new Todo sample, with endpoints to create, read, update, and delete items, backed by a model and a database. ## Add NuGet packages -NuGet packages must be added to support the database and diagnostics used in this tutorial. +Add NuGet packages to support the database used in this tutorial. # [Visual Studio](#tab/visual-studio) @@ -137,121 +116,142 @@ NuGet packages must be added to support the database and diagnostics used in thi * Select the **Browse** tab. * Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`. * Select the **Project** checkbox in the right pane and then select **Install**. -* Follow the preceding instructions to add the `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` package. # [Visual Studio Code](#tab/visual-studio-code) -* Run the following commands: +* Run the following command: - ```dotnetcli +```dotnetcli dotnet add package Microsoft.EntityFrameworkCore.InMemory - dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore - ``` +``` --- + + ## The model and database context classes * In the project folder, create a file named `Todo.cs` with the following code: -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoGroup/Todo.cs"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Todo.cs"::: The preceding code creates the model for this app. A *model* is a class that represents data that the app manages. +The `Secret` property is included to demonstrate a common real-world need: data the app stores and uses internally, such as the ID of the user who owns the item, that you don't want clients to see or set. In the [Prevent over-posting](#prevent-over-posting) step later in this tutorial, you use a Data Transfer Object (DTO) to keep fields like `Secret` out of the API's input and responses. + * Create a file named `TodoDb.cs` with the following code: -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoGroup/TodoDb.cs"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoDb.cs"::: The preceding code defines the *database context*, which is the main class that coordinates [Entity Framework](/ef/core/) functionality for a data model. This class derives from the class. -## Add the API code +The app uses an in-memory database named `TodoList`. The database is registered later in `Program.cs` with `builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList"));`, where the string `"TodoList"` is the database name. Because the data is stored in memory, it's reset every time the app restarts. -* Replace the contents of the `Program.cs` file with the following code: +## Replace the `WeatherForecast` sample with the Todo API -[!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo/Program.cs?name=snippet_all)] +The `webapi` template adds a sample `GET /weatherforecast` endpoint to `Program.cs` and a `WeatherForecast` record at the bottom of the file. The sample is just a placeholder to show the template works - replace both with the Todo endpoints described in the following section. -The following highlighted code adds the database context to the [dependency injection (DI)](xref:fundamentals/dependency-injection) container and enables displaying database-related exceptions: +* Replace all of the code in `Program.cs` with the following. The `WeatherForecast` sample endpoint and record are removed, and the Todo endpoints take their place: -[!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo/Program.cs?name=snippet_DI&highlight=2-3)] +# [Visual Studio](#tab/visual-studio) -The DI container provides access to the database context and other services. +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_minimal_start_all"::: + +# [Visual Studio Code](#tab/visual-studio-code) + +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_minimal_start_all"::: + +The pasted code also includes `using Scalar.AspNetCore;` and `app.MapScalarApiReference();`, which add a browser UI for testing the API. You configure the Scalar package for these lines later in this tutorial, in [Add a browser UI to view the OpenAPI document](#add-a-browser-ui-to-view-the-openapi-document). The project won't build until then. + +--- + +The OpenAPI lines that the template adds (`builder.Services.AddOpenApi()` and `app.MapOpenApi()`) stay in place. They now describe the Todo endpoints instead of the sample weather endpoint. + +The following highlighted code registers the app's services in the [dependency injection (DI)](xref:fundamentals/dependency-injection) container: the database context (`AddDbContext`) and the OpenAPI document generator (`AddOpenApi`): # [Visual Studio](#tab/visual-studio) -This tutorial uses [Endpoints Explorer and .http files](xref:test/http-files#use-endpoints-explorer) to test the API. +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_DI" highlight="2-3"::: # [Visual Studio Code](#tab/visual-studio-code) -## Test the web API - -There are many available web API testing tools to choose from, and you can follow this tutorial's introductory API test steps with your own preferred tool. +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_DI" highlight="2-3"::: -This tutorial utilizes the .NET package [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/), which integrates Swagger tools for generating a testing UI adhering to the OpenAPI specification: +--- -* NSwag: A .NET library that integrates Swagger directly into ASP.NET Core applications, providing middleware and configuration. -* Swagger: A set of open-source tools such as OpenAPIGenerator and SwaggerUI that generate API testing pages that follow the OpenAPI specification. -* OpenAPI specification: A document that describes the capabilities of the API, based on the XML and attribute annotations within the controllers and models. +The DI container provides access to the database context and other services. -For more information on using OpenAPI and NSwag with ASP.NET, see . +# [Visual Studio](#tab/visual-studio) -### Install Swagger tooling +This tutorial uses [Endpoints Explorer and .http files](xref:test/http-files#use-endpoints-explorer) to test the API. -* Run the following command: +# [Visual Studio Code](#tab/visual-studio-code) - ```dotnetcli - dotnet add package NSwag.AspNetCore - ``` + -The previous command adds the [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/) package, which contains tools to generate Swagger documents and UI. -This project is using OpenAPI, so the NSwag package is only used to generate the Swagger UI. +### Add a browser UI to view the OpenAPI document -### Configure Swagger middleware +The template already generates the OpenAPI document at `/openapi/v1.json`. To explore and test the API from a browser, add a UI that consumes that document. This tutorial uses Scalar. -* In Program.cs add the following highlighted code before `app` is defined in line `var app = builder.Build();` +Add the [Scalar.AspNetCore](https://www.nuget.org/packages/Scalar.AspNetCore/) package, which provides the middleware that serves the UI: - [!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo_SwaggerVersion/Program.cs?name=snippet_swagger_add_service&highlight=7-13)] +* Run the following command: -In the previous code: +```dotnetcli + dotnet add package Scalar.AspNetCore +``` - * `builder.Services.AddEndpointsApiExplorer();`: Enables the API Explorer, which is a service that provides metadata about the HTTP API. The API Explorer is used by Swagger to generate the Swagger document. - * `builder.Services.AddOpenApiDocument(config => {...});`: Adds the Swagger OpenAPI document generator to the application services and configures it to provide more information about the API, such as its title and version. For information on providing more robust API details, see +In this tutorial, Scalar is used only for its API reference middleware, which reads the OpenAPI document generated by the AddOpenApi and MapOpenApi calls. -* Add the following highlighted code to the next line after `app` is defined in line `var app = builder.Build();` +Confirm that `Program.cs` includes the highlighted `MapScalarApiReference` line inside the `if (app.Environment.IsDevelopment())` block, right after the `app.MapOpenApi();` line, and that `using Scalar.AspNetCore;` is at the top of the file. You added these lines when you pasted the Todo code earlier. - [!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo_SwaggerVersion/Program.cs?name=snippet_swagger_enable_middleware&highlight=2-12)] + :::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_scalar" highlight="4"::: - The previous code enables the Swagger middleware for serving the generated JSON document and the Swagger UI. Swagger is only enabled in a development environment. Enabling Swagger in a production environment could expose potentially sensitive details about the API's structure and implementation. + The preceding code enables the Scalar UI in Development only. It reads `/openapi/v1.json`, the document produced by `MapOpenApi()`, and serves the API reference at `/scalar/v1`. Be sure to add `using Scalar.AspNetCore;` to the top of the file. - + > [!NOTE] + > Scalar is one of several options for an OpenAPI UI. Alternatives such as `NSwag.AspNetCore` or `Swashbuckle.AspNetCore.SwaggerUi` consume the same OpenAPI document and can be substituted for Scalar without changing the rest of the tutorial. --- + ## Test posting data The following code in `Program.cs` creates an HTTP POST endpoint `/todoitems` that adds data to the in-memory database: -[!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo/Program.cs?name=snippet_post)] +# [Visual Studio](#tab/visual-studio) + +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_post"::: -Run the app. The browser displays a 404 error because there's no longer a `/` endpoint. +# [Visual Studio Code](#tab/visual-studio-code) + +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_post"::: + +--- -The POST endpoint will be used to add data to the app. # [Visual Studio](#tab/visual-studio) +* Press Ctrl+F5 to run the app without debugging. Visual Studio launches the [Kestrel web server](xref:fundamentals/servers/kestrel) and trusts the development certificate if needed. + +[!INCLUDE[](~/includes/trustCertVS26.md)] + +Use the POST endpoint to add data to the app. + * Select **View** > **Other Windows** > **Endpoints Explorer**. * Right-click the **POST** endpoint and select **Generate request**. - ![Endpoints Explorer context menu highlighting Generate Request menu item.](~/tutorials/min-web-api/static/9.x/generate-request-vs17.8.0.png) + ![Endpoints Explorer context menu highlighting Generate Request menu item.](~/tutorials/min-web-api/static/10/generate-request-vs17-8-0.png) A new file is created in the project folder named `TodoApi.http`, with contents similar to the following example: - ```http +```http @TodoApi_HostAddress = https://localhost:7031 POST {{TodoApi_HostAddress}}/todoitems ### - ``` +``` * The first line creates a variable that is used for all of the endpoints. * The next line defines a POST request. @@ -259,18 +259,18 @@ The POST endpoint will be used to add data to the app. * The POST request needs headers and a body. To define those parts of the request, add the following lines immediately after the POST request line: - ``` +``` Content-Type: application/json { "name":"walk dog", "isComplete":true } - ``` +``` The preceding code adds a Content-Type header and a JSON request body. The TodoApi.http file should now look like the following example, but with your port number: - ```http +```http @TodoApi_HostAddress = https://localhost:7057 POST {{TodoApi_HostAddress}}/todoitems @@ -282,9 +282,7 @@ The POST endpoint will be used to add data to the app. } ### - ``` - -* Run the app. +``` * Select the **Send request** link that is above the `POST` request line. @@ -292,45 +290,56 @@ The POST endpoint will be used to add data to the app. The POST request is sent to the app and the response is displayed in the **Response** pane. - ![.http file window with response from the POST request.](~/tutorials/min-web-api/static/9.x/http-file-window-with-response-vs17.8.0.png) + ![.http file window with response from the POST request.](~/tutorials/min-web-api/static/10/http-file-window-with-response-vs18-6-2.png) # [Visual Studio Code](#tab/visual-studio-code) -* With the app still running, in the browser, navigate to `https://localhost:/swagger` to display the API testing page generated by Swagger. +[!INCLUDE[](~/includes/trustCertVSC.md)] + +* In Visual Studio Code, press Ctrl+F5 (Windows) or control+F5 (macOS) to run the app without debugging. + +* You're prompted to select a debugger. Select **C#**. + +* You're prompted to select a launch configuration. Select **C#:TodoApi [Default Configuration] TodoApi** + + ![Select a launch configuration prompt.](~/tutorials/min-web-api/static/10/vsc-select-luanch-2026.png) + +The `TodoApi` app starts and listens on a randomly assigned local port. In the **Terminal** panel, find the `Now listening on:` line to get the URL and port, for example: + +```output +info: Microsoft.Hosting.Lifetime[14] + Now listening on: http://localhost:5090 +``` - ![Swagger generated API testing page](~/tutorials/min-web-api/static/9.x/swagger.png) +In this example, the `TodoApi` app is listening at `http://localhost:5090`, so the port is `5090`. Your port number is likely different. -* On the Swagger API testing page, select **Post /todoitems** > **Try it out**. -* Note that the **Request body** field contains a generated example format reflecting the parameters for the API. -* In the request body enter JSON for a to-do item, without specifying the optional `id`: +* To open the app in your default browser, hold Ctrl (Windows) or Cmd (macOS) and select the URL on the `Now listening on:` line in the **Terminal** panel. The browser opens at the app's root, `http://localhost:{port}`. - ```json +* In the browser address bar, append `/scalar/v1` to the URL to display the API reference page generated by Scalar, for example `http://localhost:5090/scalar/v1`. + +* On the Scalar page, select the **POST /todoitems** operation, and then select **Test Request**. + +* In the request body, enter JSON for a to-do item without specifying the optional `id`: + +```json { "name":"walk dog", "isComplete":true } - ``` - -* Select **Execute**. - - ![Swagger with Post request](~/tutorials/min-web-api/static/9.x/swagger-post-1.png) - -Swagger provides a **Responses** pane below the **Execute** button. +``` - ![Swagger with Post response](~/tutorials/min-web-api/static/9.x/swagger-post-responses.png) +* Select **Send**. -Note a few of the useful details: +The Scalar UI displays the response, including the status code and response body. The response body shows: -* cURL: Swagger provides an example cURL command in Unix/Linux syntax, which can be run at the command line with any bash shell that uses Unix/Linux syntax, including Git Bash from [Git for Windows](https://git-scm.com/downloads). -* Request URL: A simplified representation of the HTTP request made by Swagger UI's JavaScript code for the API call. Actual requests can include details such as headers and query parameters and a request body. -* Server response: Includes the response body and headers. The response body shows the `id` was set to `1`. -* Response Code: A 201 `HTTP` status code was returned, indicating that the request was successfully processed and resulted in the creation of a new resource. +* The `id` is set to `1`. +* A 201 `HTTP` status code is returned, which indicates that the request was successfully processed and resulted in the creation of a new resource. --- ## Examine the GET endpoints -The sample app implements several GET endpoints by calling `MapGet`: +Your `Program.cs` includes several GET endpoints, defined with `MapGet`: |API | Description | Request body | Response body | |--- | ---- | ---- | ---- | @@ -338,7 +347,7 @@ The sample app implements several GET endpoints by calling `MapGet`: |`GET /todoitems/complete` | Get all completed to-do items | None | Array of to-do items| |`GET /todoitems/{id}` | Get an item by ID | None | To-do item| -[!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo/Program.cs?name=snippet_get)] +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_get"::: ## Test the GET endpoints @@ -350,11 +359,11 @@ Test the app by calling the `GET` endpoints from a browser or by using **Endpoin The following content is added to the `TodoApi.http` file: - ```http +```http GET {{TodoApi_HostAddress}}/todoitems ### - ``` +``` * Select the **Send request** link that is above the new `GET` request line. @@ -362,24 +371,25 @@ Test the app by calling the `GET` endpoints from a browser or by using **Endpoin * The response body is similar to the following JSON: - ```json +```json [ { "id": 1, "name": "walk dog", - "isComplete": true + "isComplete": true, + "secret": null } ] - ``` +``` * In **Endpoints Explorer**, right-click the `/todoitems/{id}` **GET** endpoint and select **Generate request**. The following content is added to the `TodoApi.http` file: - ```http +```http GET {{TodoApi_HostAddress}}/todoitems/{id} ### - ``` +``` * Replace `{id}` with `1`. @@ -389,21 +399,22 @@ Test the app by calling the `GET` endpoints from a browser or by using **Endpoin * The response body is similar to the following JSON: - ```json +```json { "id": 1, "name": "walk dog", - "isComplete": true + "isComplete": true, + "secret": null } - ``` +``` # [Visual Studio Code](#tab/visual-studio-code) -Test the app by calling the endpoints from a browser or Swagger. +Test the app by calling the endpoints from a browser or the Scalar UI. -* In Swagger select **GET /todoitems** > **Try it out** > **Execute**. +* In the Scalar UI, select **GET /todoitems**, and then select **Test Request** > **Send**. -* Alternatively, call **GET /todoitems** from a browser by entering the URI `http://localhost:/todoitems`. For example, `http://localhost:5001/todoitems` +* Alternatively, call **GET /todoitems** from a browser by entering the URI `http://localhost:{port}/todoitems`. For example, `http://localhost:7032/todoitems`. The call to `GET /todoitems` produces a response similar to the following: @@ -412,30 +423,32 @@ The call to `GET /todoitems` produces a response similar to the following: { "id": 1, "name": "walk dog", - "isComplete": true + "isComplete": true, + "secret": null } ] ``` -* Call **GET /todoitems/{id}** in Swagger to return data from a specific id: - * Select **GET /todoitems** > **Try it out**. - * Set the **id** field to `1` and select **Execute**. +* Call **GET /todoitems/{id}** in Scalar to return data from a specific id: + * Select **GET /todoitems/{id}**, and then select **Test Request**. + * Set the **id** field to `1` and select **Send**. -* Alternatively, call **GET /todoitems** from a browser by entering the URI `https://localhost:/todoitems/1`. For example, `https://localhost:5001/todoitems/1` +* Alternatively, call **GET /todoitems/{id}** from a browser by entering the URI `http://localhost:{port}/todoitems/1`. For example, `http://localhost:7032/todoitems/1`. * The response is similar to the following: - ```json +```json { "id": 1, "name": "walk dog", - "isComplete": true + "isComplete": true, + "secret": null } - ``` +``` --- -This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, [POST](#post) data to the app and try the GET request again. +This app uses an in-memory database. If you restart the app, the data is lost: `GET /todoitems` returns an empty array (`[]`), and `GET /todoitems/{id}` returns a `404 Not Found`. To repopulate the app, [POST](#post) data to the app and try the GET request again. ## Return values @@ -450,15 +463,23 @@ The return types can represent a wide range of HTTP status codes. For example, ` The sample app implements a single PUT endpoint using `MapPut`: -[!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo/Program.cs?name=snippet_put)] +# [Visual Studio](#tab/visual-studio) + +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_put"::: + +# [Visual Studio Code](#tab/visual-studio-code) + +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_put"::: + +--- This method is similar to the `MapPost` method, except it uses HTTP PUT. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](xref:Microsoft.AspNetCore.Mvc.HttpPatchAttribute). ## Test the PUT 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 PUT call. Call GET to ensure there's an item in the database before making a PUT call. +This sample uses an in-memory database that you must initialize each time you start the app. You need an item in the database before you make a PUT call. Call POST to ensure there's an item in the database before making a PUT call. -Update the to-do item that has `Id = 1` and set its name to `"feed fish"`. +Update the Todo item that has `Id = 1` and set its name to `"feed fish"`. # [Visual Studio](#tab/visual-studio) @@ -466,17 +487,17 @@ Update the to-do item that has `Id = 1` and set its name to `"feed fish"`. The following content is added to the `TodoApi.http` file: - ```http +```http PUT {{TodoApi_HostAddress}}/todoitems/{id} ### - ``` +``` * In the PUT request line, replace `{id}` with `1`. * Add the following lines immediately after the PUT request line: - ```http +```http Content-Type: application/json { @@ -484,116 +505,163 @@ Update the to-do item that has `Id = 1` and set its name to `"feed fish"`. "name": "feed fish", "isComplete": false } - ``` +``` The preceding code adds a Content-Type header and a JSON request body. * Select the **Send request** link that is above the new PUT request line. - - The PUT 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 PUT request: +Use the Scalar UI to send a PUT request: -* Select **Put /todoitems/{id}** > **Try it out**. +* Select **PUT /todoitems/{id}**, and then select **Test Request**. * Set the **id** field to `1`. * Set the request body to the following JSON: - ```json +```json { "id": 1, "name": "feed fish", "isComplete": false } - ``` +``` -* Select **Execute**. +* Select **Send**. --- -## Examine the PATCH endpoint +The PUT 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. + +## Create and examine the PATCH endpoint -Create a file named `TodoPatchDto.cs` with the following code: +A PATCH endpoint lets clients send only the fields they want to update, such as renaming a Todo item without resending its completion status. This approach differs from a PUT request, which replaces the entire item, so the client must send every field even if only one is changed. -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todo/TodoPatchDto.cs"::: +The next steps add a new file and modify the `Program.cs` file. Stop the `TodoApi` app before making these changes. Leave the Scalar page in the browser running. + +# [Visual Studio](#tab/visual-studio) + +* To stop the app, select the **Stop** button (the red square) in the Visual Studio toolbar. + +# [Visual Studio Code](#tab/visual-studio-code) + +* To stop the app, press Shift+F5. + +--- + +This sample uses an in-memory database that you must initialize each time the app starts. The database must contain an item before you make a PATCH call. Call POST to ensure the database has an item before making a PATCH call. + +The PATCH endpoint uses a `TodoPatchDto` class with nullable properties to properly handle partial updates. By using nullable properties, the endpoint can distinguish between a field that wasn't provided (null) and a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool defaults to false, which could potentially overwrite an existing true value when that field isn't included in the request. + +* Create a file named `TodoPatchDto.cs` with the following code: + +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/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`: +* In `Program.cs`, add the following PATCH endpoint immediately after the `MapPut` endpoint: -[!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo/Program.cs?name=snippet_patch)] +# [Visual Studio](#tab/visual-studio) -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. +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_patch"::: + +# [Visual Studio Code](#tab/visual-studio-code) -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. +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_patch"::: + +--- + +This method is similar to the `MapPut` method, but 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). > [!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. +# [Visual Studio](#tab/visual-studio) + +* Press Ctrl+F5 to rebuild and run the app with the new PATCH endpoint. + +# [Visual Studio Code](#tab/visual-studio-code) + +* Run the app again so it rebuilds with the new PATCH endpoint. In Visual Studio Code, press Ctrl+F5 (Windows) or control+F5 (macOS) to run the app without debugging. + +--- + + +This sample uses an in-memory database that you must initialize each time the app starts. The database must contain an item before you make a PATCH call. Call POST to ensure the database has an item 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"`. +Update only the `name` property of the Todo 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**. +* In **Endpoints Explorer**, select the refresh button. Then, right-click the **PATCH** endpoint, and select **Generate request**. The following content is added to the `TodoApi.http` file: - ```http +```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 +```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: +Use the Scalar UI to send a PATCH request: + +* Refresh the scalar page in the browser so that the new `PATCH` endpoint appears. -* Select **Patch /todoitems/{id}** > **Try it out**. +* Select **PATCH /todoitems/{id}**, and then select **Test Request**. * Set the **id** field to `1`. * Set the request body to the following JSON: - ```json +```json { "name": "run errands" } - ``` +``` -* Select **Execute**. +* Select **Send**. --- -## Examine and test the DELETE endpoint +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. -The sample app implements a single DELETE endpoint using `MapDelete`: +## Examine the DELETE endpoint -[!code-csharp[](~/tutorials/min-web-api/samples/9.x/todo/Program.cs?name=snippet_delete)] +Your `Program.cs` file includes a single DELETE endpoint, defined with `MapDelete`: + +# [Visual Studio](#tab/visual-studio) + +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_delete"::: + +# [Visual Studio Code](#tab/visual-studio-code) + +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_delete"::: + +--- + +## Test the DELETE endpoint # [Visual Studio](#tab/visual-studio) @@ -603,40 +671,50 @@ The sample app implements a single DELETE endpoint using `MapDelete`: * Replace `{id}` in the DELETE request line with `1`. The DELETE request should look like the following example: - ```http +```http DELETE {{TodoApi_HostAddress}}/todoitems/1 ### - ``` +``` * Select the **Send request** link for the DELETE request. - - The DELETE 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 DELETE request: +Use the Scalar UI to send a DELETE request: -* Select **DELETE /todoitems/{id}** > **Try it out**. -* Set the **ID** field to `1` and select **Execute**. - - The DELETE request is sent to the app and the response is displayed in the **Responses** pane. The response body is empty, and the **Server response** status code is 204. +* Select **DELETE /todoitems/{id}**, then **Test Request**. +* Set the **id** field to `1` and select **Send**. --- +The DELETE 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. + ## Use the MapGroup API -The sample app code repeats the `todoitems` URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the method is available to help organize such groups. It reduces repetitive code and allows for customizing entire groups of endpoints with a single call to methods like and . +* The next steps modify the `Program.cs` file, so stop the app before making these changes. Leave the scalar page in the browser running. -Replace the contents of `Program.cs` with the following code: +# [Visual Studio](#tab/visual-studio) + +* To stop the app, select the **Stop** button (the red square) in the Visual Studio toolbar. + +# [Visual Studio Code](#tab/visual-studio-code) + +* To stop the app, press Shift+F5. + +--- + +The `Program.cs` file you wrote repeats the `todoitems` URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the method helps organize such groups. It reduces repetitive code and allows you to customize entire groups of endpoints with a single call to methods like and . + +* Replace the contents of `Program.cs` with the following code: # [Visual Studio](#tab/visual-studio) -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoGroup/Program.cs" id="snippet_all"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_mapgroup_all"::: # [Visual Studio Code](#tab/visual-studio-code) -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoGroup_SwaggerVersion/Program.cs" id="snippet_all"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_mapgroup_all"::: --- @@ -646,35 +724,37 @@ The preceding code has the following changes: * Changes all the `app.Map` methods to `todoItems.Map`. * Removes the URL prefix `/todoitems` from the `Map` method calls. -Test the endpoints to verify that they work the same. +* Runs the app and tests the endpoints to verify that they work the same. ## Use the TypedResults API +The next steps modify the `Program.cs` file, so stop the app before making these changes. Leave the scalar page in the browser running. + Returning rather than has several advantages, including testability and automatically returning the response type metadata for OpenAPI to describe the endpoint. For more information, see [TypedResults vs Results](/aspnet/core/fundamentals/minimal-apis/responses#typedresults-vs-results). -The `Map` methods can call route handler methods instead of using lambdas. To see an example, update *Program.cs* with the following code: +* The `Map` methods can call route handler methods instead of using lambdas. To see an example, update *Program.cs* with the following code: # [Visual Studio](#tab/visual-studio) -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoTypedResults/Program.cs" id="snippet_all"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_typed_all"::: # [Visual Studio Code](#tab/visual-studio-code) -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoTypedResults_SwaggerVersion/Program.cs" id="snippet_all"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_typed_all"::: --- The `Map` code now calls methods instead of lambdas: -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoTypedResults/Program.cs" id="snippet_group"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_typed_group"::: -These methods return objects that implement and are defined by : +The following methods return objects that implement and are defined by : -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoTypedResults/Program.cs" id="snippet_handlers"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_typed_handlers"::: Unit tests can call these methods and test that they return the correct type. For example, if the method is `GetAllTodos`: -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoTypedResults/Program.cs" id="snippet_getalltodos"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_typed_getalltodos"::: Unit test code can verify that an object of type [Ok\](xref:Microsoft.AspNetCore.Http.HttpResults.Ok%601.Value) is returned from the handler method. For example: @@ -692,46 +772,42 @@ public async Task GetAllTodos_ReturnsOkOfTodosResult() } ``` - + ## Prevent over-posting -Currently the sample app exposes the entire `Todo` object. In production applications, a subset of the model is often used to restrict the data that can be input and returned. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this article. +Currently, your API exposes the entire `Todo` object, including the `Secret` property you added in [The model and database context classes](#model-db-classes). In production applications, use a subset of the model to restrict the data that clients can input and receive. Security is a major reason for this restriction. This subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. This article uses **DTO**. -A DTO can be used to: +Use a DTO to: * Prevent over-posting. * Hide properties that clients aren't supposed to view. * Omit some properties to reduce payload size. * Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients. -To demonstrate the DTO approach, update the `Todo` class to include a secret field: - -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoDTO/Todo.cs"::: - -The secret field needs to be hidden from this app, but an administrative app could choose to expose it. +This app needs to hide the `Secret` field, but an administrative app could choose to expose it. -Verify you can post and get the secret field. +* The next steps add a file, so stop the app before making these changes. Leave the scalar page in the browser running. -Create a file named `TodoItemDTO.cs` with the following code: +* Create a file named `TodoItemDTO.cs` with the following code: -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoDTO/TodoItemDTO.cs"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoItemDTO.cs"::: -Replace the contents of the `Program.cs` file with the following code to use this DTO model: +* Replace the contents of the `Program.cs` file with the following code to use this DTO model: # [Visual Studio](#tab/visual-studio) -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoDTO/Program.cs" id="snippet_all"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs" id="snippet_dto_all"::: # [Visual Studio Code](#tab/visual-studio-code) -:::code language="csharp" source="~/tutorials/min-web-api/samples/9.x/todoDTO_SwaggerVersion/Program.cs" id="snippet_all"::: +:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs" id="snippet_dto_all"::: --- -Verify you can post and get all fields except the secret field. +* Run the app and verify you can post and get all fields except the `Secret` field. - + ## Troubleshooting with the completed sample @@ -754,4 +830,4 @@ See [!INCLUDE[](~/tutorials/min-web-api/includes/min-web-api-6-7.md)] [!INCLUDE[](~/tutorials/min-web-api/includes/min-web-api-8.md)] -[!INCLUDE[](~/tutorials/min-web-api/includes/min-web-api-9.md)] +[!INCLUDE[](~/tutorials/min-web-api/includes/min-web-api-9.md)] \ No newline at end of file diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs new file mode 100644 index 000000000000..09dc9665a583 --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Program.cs @@ -0,0 +1,489 @@ +#define PREVENTOVERPOST // TEMPLATESTART MINIMALSTART WITHPATCH MAPGROUP TYPED PREVENTOVERPOST +#if TEMPLATESTART +// +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast"); + +app.Run(); + +internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} +// + +#elif MINIMALSTART +// +using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; + +// +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); +// + +// +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(); +} +// + +// +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.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 WITHPATCH +// +using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(); +} + +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 MAPGROUP +// +using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(); +} + +var todoItems = app.MapGroup("/todoitems"); + +todoItems.MapGet("/", async (TodoDb db) => + await db.Todos.ToListAsync()); + +todoItems.MapGet("/complete", async (TodoDb db) => + await db.Todos.Where(t => t.IsComplete).ToListAsync()); + +todoItems.MapGet("/{id}", async (int id, TodoDb db) => + await db.Todos.FindAsync(id) + is Todo todo + ? Results.Ok(todo) + : Results.NotFound()); + +todoItems.MapPost("/", async (Todo todo, TodoDb db) => +{ + db.Todos.Add(todo); + await db.SaveChangesAsync(); + + return Results.Created($"/todoitems/{todo.Id}", todo); +}); + +todoItems.MapPut("/{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(); +}); + +todoItems.MapPatch("/{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(); +}); + +todoItems.MapDelete("/{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 TYPED +// +using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(); +} + +// +var todoItems = app.MapGroup("/todoitems"); + +todoItems.MapGet("/", GetAllTodos); +todoItems.MapGet("/complete", GetCompleteTodos); +todoItems.MapGet("/{id}", GetTodo); +todoItems.MapPost("/", CreateTodo); +todoItems.MapPut("/{id}", UpdateTodo); +todoItems.MapPatch("/{id}", PatchTodo); +todoItems.MapDelete("/{id}", DeleteTodo); +// + +app.Run(); + +// +// +static async Task GetAllTodos(TodoDb db) +{ + return TypedResults.Ok(await db.Todos.ToArrayAsync()); +} +// + +static async Task GetCompleteTodos(TodoDb db) +{ + return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync()); +} + +static async Task GetTodo(int id, TodoDb db) +{ + return await db.Todos.FindAsync(id) + is Todo todo + ? TypedResults.Ok(todo) + : TypedResults.NotFound(); +} + +static async Task CreateTodo(Todo todo, TodoDb db) +{ + db.Todos.Add(todo); + await db.SaveChangesAsync(); + + return TypedResults.Created($"/todoitems/{todo.Id}", todo); +} + +static async Task UpdateTodo(int id, Todo inputTodo, TodoDb db) +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return TypedResults.NotFound(); + + todo.Name = inputTodo.Name; + todo.IsComplete = inputTodo.IsComplete; + + await db.SaveChangesAsync(); + + return TypedResults.NoContent(); +} + +static async Task PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db) +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return TypedResults.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 TypedResults.NoContent(); +} + +static async Task DeleteTodo(int id, TodoDb db) +{ + if (await db.Todos.FindAsync(id) is Todo todo) + { + db.Todos.Remove(todo); + await db.SaveChangesAsync(); + return TypedResults.NoContent(); + } + + return TypedResults.NotFound(); +} +// +// + +#elif PREVENTOVERPOST +// +using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(); +} + +var todoItems = app.MapGroup("/todoitems"); + +todoItems.MapGet("/", GetAllTodos); +todoItems.MapGet("/complete", GetCompleteTodos); +todoItems.MapGet("/{id}", GetTodo); +todoItems.MapPost("/", CreateTodo); +todoItems.MapPut("/{id}", UpdateTodo); +todoItems.MapPatch("/{id}", PatchTodo); +todoItems.MapDelete("/{id}", DeleteTodo); + +app.Run(); + +static async Task GetAllTodos(TodoDb db) +{ + return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync()); +} + +static async Task GetCompleteTodos(TodoDb db) +{ + return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync()); +} + +static async Task GetTodo(int id, TodoDb db) +{ + return await db.Todos.FindAsync(id) + is Todo todo + ? TypedResults.Ok(new TodoItemDTO(todo)) + : TypedResults.NotFound(); +} + +static async Task CreateTodo(TodoItemDTO todoItemDTO, TodoDb db) +{ + var todoItem = new Todo + { + IsComplete = todoItemDTO.IsComplete, + Name = todoItemDTO.Name + }; + + db.Todos.Add(todoItem); + await db.SaveChangesAsync(); + + todoItemDTO = new TodoItemDTO(todoItem); + + return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO); +} + +static async Task UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db) +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return TypedResults.NotFound(); + + todo.Name = todoItemDTO.Name; + todo.IsComplete = todoItemDTO.IsComplete; + + await db.SaveChangesAsync(); + + return TypedResults.NoContent(); +} + +static async Task PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db) +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return TypedResults.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 TypedResults.NoContent(); +} + +static async Task DeleteTodo(int id, TodoDb db) +{ + if (await db.Todos.FindAsync(id) is Todo todo) + { + db.Todos.Remove(todo); + await db.SaveChangesAsync(); + return TypedResults.NoContent(); + } + + return TypedResults.NotFound(); +} +// +#endif \ No newline at end of file diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/Todo.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Todo.cs similarity index 88% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/Todo.cs rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Todo.cs index d9ea203029c5..44593bed1e7c 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/Todo.cs +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/Todo.cs @@ -1,4 +1,4 @@ -public class Todo +public class Todo { public int Id { get; set; } public string? Name { get; set; } diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/TodoApi.csproj b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoApi.csproj similarity index 51% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/TodoApi.csproj rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoApi.csproj index 067275c7128b..a336ed7d8b27 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/TodoApi.csproj +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoApi.csproj @@ -1,15 +1,14 @@ - net8.0 + net10.0 enable enable - - - + + diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoDb.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoDb.cs similarity index 79% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoDb.cs rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoDb.cs index c1584b807819..b8229b1b6a25 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoDb.cs +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoDb.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; class TodoDb : DbContext { diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoItemDTO.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoItemDTO.cs similarity index 90% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoItemDTO.cs rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoItemDTO.cs index 76f99abe44bd..68ecca65299c 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoItemDTO.cs +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoItemDTO.cs @@ -1,4 +1,4 @@ -public class TodoItemDTO +public class TodoItemDTO { public int Id { get; set; } public string? Name { get; set; } @@ -7,4 +7,4 @@ public class TodoItemDTO public TodoItemDTO() { } public TodoItemDTO(Todo todoItem) => (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete); -} +} \ No newline at end of file diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoPatchDto.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoPatchDto.cs similarity index 74% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoPatchDto.cs rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoPatchDto.cs index ab225cf2280b..af8f27b8cf05 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoPatchDto.cs +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/TodoPatchDto.cs @@ -1,4 +1,4 @@ -public class TodoPatchDto +public class TodoPatchDto { public string? Name { get; set; } public bool? IsComplete { get; set; } diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/appsettings.Development.json b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/appsettings.Development.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/appsettings.json b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/appsettings.json new file mode 100644 index 000000000000..10f68b8c8b4f --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VSC_Scalar/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs new file mode 100644 index 000000000000..7883c5ad9c17 --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Program.cs @@ -0,0 +1,479 @@ +#define PREVENTOVERPOST // TEMPLATESTART MINIMALSTART WITHPATCH MAPGROUP TYPED PREVENTOVERPOST +#if TEMPLATESTART +// +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast"); + +app.Run(); + +internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} +// + +#elif MINIMALSTART +// +using Microsoft.EntityFrameworkCore; + +// +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); +// + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +// +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.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 WITHPATCH +// +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +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 MAPGROUP +// +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +var todoItems = app.MapGroup("/todoitems"); + +todoItems.MapGet("/", async (TodoDb db) => + await db.Todos.ToListAsync()); + +todoItems.MapGet("/complete", async (TodoDb db) => + await db.Todos.Where(t => t.IsComplete).ToListAsync()); + +todoItems.MapGet("/{id}", async (int id, TodoDb db) => + await db.Todos.FindAsync(id) + is Todo todo + ? Results.Ok(todo) + : Results.NotFound()); + +todoItems.MapPost("/", async (Todo todo, TodoDb db) => +{ + db.Todos.Add(todo); + await db.SaveChangesAsync(); + + return Results.Created($"/todoitems/{todo.Id}", todo); +}); + +todoItems.MapPut("/{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(); +}); + +todoItems.MapPatch("/{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(); +}); + +todoItems.MapDelete("/{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 TYPED +// +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +// +var todoItems = app.MapGroup("/todoitems"); + +todoItems.MapGet("/", GetAllTodos); +todoItems.MapGet("/complete", GetCompleteTodos); +todoItems.MapGet("/{id}", GetTodo); +todoItems.MapPost("/", CreateTodo); +todoItems.MapPut("/{id}", UpdateTodo); +todoItems.MapPatch("/{id}", PatchTodo); +todoItems.MapDelete("/{id}", DeleteTodo); + +// +app.Run(); + +// +// +static async Task GetAllTodos(TodoDb db) +{ + return TypedResults.Ok(await db.Todos.ToArrayAsync()); +} +// + +static async Task GetCompleteTodos(TodoDb db) +{ + return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync()); +} + +static async Task GetTodo(int id, TodoDb db) +{ + return await db.Todos.FindAsync(id) + is Todo todo + ? TypedResults.Ok(todo) + : TypedResults.NotFound(); +} + +static async Task CreateTodo(Todo todo, TodoDb db) +{ + db.Todos.Add(todo); + await db.SaveChangesAsync(); + + return TypedResults.Created($"/todoitems/{todo.Id}", todo); +} + +static async Task UpdateTodo(int id, Todo inputTodo, TodoDb db) +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return TypedResults.NotFound(); + + todo.Name = inputTodo.Name; + todo.IsComplete = inputTodo.IsComplete; + + await db.SaveChangesAsync(); + + return TypedResults.NoContent(); +} + +static async Task PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db) +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return TypedResults.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 TypedResults.NoContent(); +} + +static async Task DeleteTodo(int id, TodoDb db) +{ + if (await db.Todos.FindAsync(id) is Todo todo) + { + db.Todos.Remove(todo); + await db.SaveChangesAsync(); + return TypedResults.NoContent(); + } + + return TypedResults.NotFound(); +} + +// +// +#elif PREVENTOVERPOST +// +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); +builder.Services.AddOpenApi(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +var todoItems = app.MapGroup("/todoitems"); + +todoItems.MapGet("/", GetAllTodos); +todoItems.MapGet("/complete", GetCompleteTodos); +todoItems.MapGet("/{id}", GetTodo); +todoItems.MapPost("/", CreateTodo); +todoItems.MapPut("/{id}", UpdateTodo); +todoItems.MapPatch("/{id}", PatchTodo); +todoItems.MapDelete("/{id}", DeleteTodo); + +app.Run(); + +static async Task GetAllTodos(TodoDb db) +{ + return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync()); +} + +static async Task GetCompleteTodos(TodoDb db) +{ + return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync()); +} + +static async Task GetTodo(int id, TodoDb db) +{ + return await db.Todos.FindAsync(id) + is Todo todo + ? TypedResults.Ok(new TodoItemDTO(todo)) + : TypedResults.NotFound(); +} + +static async Task CreateTodo(TodoItemDTO todoItemDTO, TodoDb db) +{ + var todoItem = new Todo + { + IsComplete = todoItemDTO.IsComplete, + Name = todoItemDTO.Name + }; + + db.Todos.Add(todoItem); + await db.SaveChangesAsync(); + + todoItemDTO = new TodoItemDTO(todoItem); + + return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO); +} + +static async Task UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db) +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return TypedResults.NotFound(); + + todo.Name = todoItemDTO.Name; + todo.IsComplete = todoItemDTO.IsComplete; + + await db.SaveChangesAsync(); + + return TypedResults.NoContent(); +} + +static async Task PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db) +{ + var todo = await db.Todos.FindAsync(id); + + if (todo is null) return TypedResults.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 TypedResults.NoContent(); +} + +static async Task DeleteTodo(int id, TodoDb db) +{ + if (await db.Todos.FindAsync(id) is Todo todo) + { + db.Todos.Remove(todo); + await db.SaveChangesAsync(); + return TypedResults.NoContent(); + } + + return TypedResults.NotFound(); +} +// +#endif diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/Todo.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Todo.cs similarity index 88% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/Todo.cs rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Todo.cs index d9ea203029c5..44593bed1e7c 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/Todo.cs +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/Todo.cs @@ -1,4 +1,4 @@ -public class Todo +public class Todo { public int Id { get; set; } public string? Name { get; set; } diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/TodoApi.csproj b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoApi.csproj similarity index 51% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/TodoApi.csproj rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoApi.csproj index 067275c7128b..a336ed7d8b27 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/TodoApi.csproj +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoApi.csproj @@ -1,15 +1,14 @@ - net8.0 + net10.0 enable enable - - - + + diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoApi.http b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoApi.http new file mode 100644 index 000000000000..eadf989b9134 --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoApi.http @@ -0,0 +1,6 @@ +@TodoApi_HostAddress = http://localhost:5290 + +GET {{TodoApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoDb.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoDb.cs similarity index 79% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoDb.cs rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoDb.cs index c1584b807819..b8229b1b6a25 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoDb.cs +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoDb.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; class TodoDb : DbContext { diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoItemDTO.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoItemDTO.cs similarity index 90% rename from aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoItemDTO.cs rename to aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoItemDTO.cs index 76f99abe44bd..68ecca65299c 100644 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoItemDTO.cs +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoItemDTO.cs @@ -1,4 +1,4 @@ -public class TodoItemDTO +public class TodoItemDTO { public int Id { get; set; } public string? Name { get; set; } @@ -7,4 +7,4 @@ public class TodoItemDTO public TodoItemDTO() { } public TodoItemDTO(Todo todoItem) => (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete); -} +} \ No newline at end of file diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoPatchDto.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/TodoPatchDto.cs new file mode 100644 index 000000000000..af8f27b8cf05 --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/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/10.x/TodoApi_VS_EndpointExplorer/appsettings.Development.json b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/appsettings.Development.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/appsettings.json b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/appsettings.json new file mode 100644 index 000000000000..10f68b8c8b4f --- /dev/null +++ b/aspnetcore/tutorials/min-web-api/samples/10.x/TodoApi_VS_EndpointExplorer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Message.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Message.cs deleted file mode 100644 index b352fffdee18..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Message.cs +++ /dev/null @@ -1,8 +0,0 @@ -public class Message -{ - public Message() - { - } - - public string Text { get; set; } -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Program.cs deleted file mode 100644 index 0cd1540ced1f..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Program.cs +++ /dev/null @@ -1,205 +0,0 @@ -#define FINAL // MINIMAL FINAL WITHPATCH TYPEDR -#if MINIMAL -// -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); - -app.MapGet("/", () => "Hello World!"); - -app.Run(); -// -#elif FINAL -// -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.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 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; - -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.MapGet("/todoitems/{id}", async Task, NotFound>> (int id, TodoDb db) => - await db.Todos.FindAsync(id) - is Todo todo - ? TypedResults.Ok(todo) - : TypedResults.NotFound()); -// -*/ - -/* -// -app.MapGet("/todoitems/{id}", async (int id, TodoDb db) => - await db.Todos.FindAsync(id) - is Todo todo - ? TypedResults.Ok(todo) - : TypedResults.NotFound()); -// -*/ - -// -app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" })) - .Produces(); -// - -// -app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" })); -// - -app.Run(); -#endif - diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Todo.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Todo.cs deleted file mode 100644 index 2900a835b043..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/Todo.cs +++ /dev/null @@ -1,6 +0,0 @@ -class Todo -{ - public int Id { get; set; } - public string? Name { get; set; } - public bool IsComplete { get; set; } -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoApi.csproj b/aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoApi.csproj deleted file mode 100644 index abc9f7648bf9..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo/TodoApi.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - - - diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/Program.cs deleted file mode 100644 index 861c355b7387..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/Program.cs +++ /dev/null @@ -1,84 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); -builder.Services.AddDatabaseDeveloperPageExceptionFilter(); -var app = builder.Build(); - -// -RouteGroupBuilder todoItems = app.MapGroup("/todoitems"); - -todoItems.MapGet("/", GetAllTodos); -todoItems.MapGet("/complete", GetCompleteTodos); -todoItems.MapGet("/{id}", GetTodo); -todoItems.MapPost("/", CreateTodo); -todoItems.MapPut("/{id}", UpdateTodo); -todoItems.MapDelete("/{id}", DeleteTodo); -// - -app.Run(); - -// -// -static async Task GetAllTodos(TodoDb db) -{ - return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync()); -} -// - -static async Task GetCompleteTodos(TodoDb db) { - return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync()); -} - -static async Task GetTodo(int id, TodoDb db) -{ - return await db.Todos.FindAsync(id) - is Todo todo - ? TypedResults.Ok(new TodoItemDTO(todo)) - : TypedResults.NotFound(); -} - -static async Task CreateTodo(TodoItemDTO todoItemDTO, TodoDb db) -{ - var todoItem = new Todo - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - db.Todos.Add(todoItem); - await db.SaveChangesAsync(); - - todoItemDTO = new TodoItemDTO(todoItem); - - return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO); -} - -static async Task UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db) -{ - var todo = await db.Todos.FindAsync(id); - - if (todo is null) return TypedResults.NotFound(); - - todo.Name = todoItemDTO.Name; - todo.IsComplete = todoItemDTO.IsComplete; - - await db.SaveChangesAsync(); - - return TypedResults.NoContent(); -} - -static async Task DeleteTodo(int id, TodoDb db) -{ - if (await db.Todos.FindAsync(id) is Todo todo) - { - db.Todos.Remove(todo); - await db.SaveChangesAsync(); - return TypedResults.NoContent(); - } - - return TypedResults.NotFound(); -} -// -// diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoApi.csproj b/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoApi.csproj deleted file mode 100644 index abc9f7648bf9..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO/TodoApi.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - - - diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/Program.cs deleted file mode 100644 index aa53df894a24..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/Program.cs +++ /dev/null @@ -1,105 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); -builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddOpenApiDocument(config => -{ - config.DocumentName = "TodoAPI"; - config.Title = "TodoAPI v1"; - config.Version = "v1"; -}); - -var app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.UseOpenApi(); - app.UseSwaggerUi(config => - { - config.DocumentTitle = "TodoAPI"; - config.Path = "/swagger"; - config.DocumentPath = "/swagger/{documentName}/swagger.json"; - config.DocExpansion = "list"; - }); -} - -// -RouteGroupBuilder todoItems = app.MapGroup("/todoitems"); - -todoItems.MapGet("/", GetAllTodos); -todoItems.MapGet("/complete", GetCompleteTodos); -todoItems.MapGet("/{id}", GetTodo); -todoItems.MapPost("/", CreateTodo); -todoItems.MapPut("/{id}", UpdateTodo); -todoItems.MapDelete("/{id}", DeleteTodo); -// - -app.Run(); - -// -// -static async Task GetAllTodos(TodoDb db) -{ - return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync()); -} -// - -static async Task GetCompleteTodos(TodoDb db) { - return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync()); -} - -static async Task GetTodo(int id, TodoDb db) -{ - return await db.Todos.FindAsync(id) - is Todo todo - ? TypedResults.Ok(new TodoItemDTO(todo)) - : TypedResults.NotFound(); -} - -static async Task CreateTodo(TodoItemDTO todoItemDTO, TodoDb db) -{ - var todoItem = new Todo - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - db.Todos.Add(todoItem); - await db.SaveChangesAsync(); - - todoItemDTO = new TodoItemDTO(todoItem); - - return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO); -} - -static async Task UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db) -{ - var todo = await db.Todos.FindAsync(id); - - if (todo is null) return TypedResults.NotFound(); - - todo.Name = todoItemDTO.Name; - todo.IsComplete = todoItemDTO.IsComplete; - - await db.SaveChangesAsync(); - - return TypedResults.NoContent(); -} - -static async Task DeleteTodo(int id, TodoDb db) -{ - if (await db.Todos.FindAsync(id) is Todo todo) - { - db.Todos.Remove(todo); - await db.SaveChangesAsync(); - return TypedResults.NoContent(); - } - - return TypedResults.NotFound(); -} -// -// diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoApi.csproj b/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoApi.csproj deleted file mode 100644 index 067275c7128b..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoApi.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoDb.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoDb.cs deleted file mode 100644 index c1584b807819..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/TodoDb.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -class TodoDb : DbContext -{ - public TodoDb(DbContextOptions options) - : base(options) { } - - public DbSet Todos => Set(); -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/Program.cs deleted file mode 100644 index 2a82ad8c9f76..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/Program.cs +++ /dev/null @@ -1,60 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); -builder.Services.AddDatabaseDeveloperPageExceptionFilter(); -var app = builder.Build(); - -// -var todoItems = app.MapGroup("/todoitems"); - -todoItems.MapGet("/", async (TodoDb db) => - await db.Todos.ToListAsync()); - -todoItems.MapGet("/complete", async (TodoDb db) => - await db.Todos.Where(t => t.IsComplete).ToListAsync()); - -todoItems.MapGet("/{id}", async (int id, TodoDb db) => - await db.Todos.FindAsync(id) - is Todo todo - ? Results.Ok(todo) - : Results.NotFound()); - -todoItems.MapPost("/", async (Todo todo, TodoDb db) => -{ - db.Todos.Add(todo); - await db.SaveChangesAsync(); - - return Results.Created($"/todoitems/{todo.Id}", todo); -}); - -todoItems.MapPut("/{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(); -}); - -todoItems.MapDelete("/{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(); -// \ No newline at end of file diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/Todo.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/Todo.cs deleted file mode 100644 index 986d0c7de8c4..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/Todo.cs +++ /dev/null @@ -1,6 +0,0 @@ -public class Todo -{ - public int Id { get; set; } - public string? Name { get; set; } - public bool IsComplete { get; set; } -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/TodoApi.csproj b/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/TodoApi.csproj deleted file mode 100644 index abc9f7648bf9..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/TodoApi.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - - - diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/TodoDb.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/TodoDb.cs deleted file mode 100644 index c1584b807819..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup/TodoDb.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -class TodoDb : DbContext -{ - public TodoDb(DbContextOptions options) - : base(options) { } - - public DbSet Todos => Set(); -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/Program.cs deleted file mode 100644 index 3787cd75a0ce..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/Program.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); -builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddOpenApiDocument(config => -{ - config.DocumentName = "TodoAPI"; - config.Title = "TodoAPI v1"; - config.Version = "v1"; -}); - -var app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.UseOpenApi(); - app.UseSwaggerUi(config => - { - config.DocumentTitle = "TodoAPI"; - config.Path = "/swagger"; - config.DocumentPath = "/swagger/{documentName}/swagger.json"; - config.DocExpansion = "list"; - }); -} - -// -var todoItems = app.MapGroup("/todoitems"); - -todoItems.MapGet("/", async (TodoDb db) => - await db.Todos.ToListAsync()); - -todoItems.MapGet("/complete", async (TodoDb db) => - await db.Todos.Where(t => t.IsComplete).ToListAsync()); - -todoItems.MapGet("/{id}", async (int id, TodoDb db) => - await db.Todos.FindAsync(id) - is Todo todo - ? Results.Ok(todo) - : Results.NotFound()); - -todoItems.MapPost("/", async (Todo todo, TodoDb db) => -{ - db.Todos.Add(todo); - await db.SaveChangesAsync(); - - return Results.Created($"/todoitems/{todo.Id}", todo); -}); - -todoItems.MapPut("/{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(); -}); - -todoItems.MapDelete("/{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(); -// diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/Todo.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/Todo.cs deleted file mode 100644 index 986d0c7de8c4..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/Todo.cs +++ /dev/null @@ -1,6 +0,0 @@ -public class Todo -{ - public int Id { get; set; } - public string? Name { get; set; } - public bool IsComplete { get; set; } -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/TodoDb.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/TodoDb.cs deleted file mode 100644 index c1584b807819..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/TodoDb.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -class TodoDb : DbContext -{ - public TodoDb(DbContextOptions options) - : base(options) { } - - public DbSet Todos => Set(); -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/Program.cs deleted file mode 100644 index 591f97decb52..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/Program.cs +++ /dev/null @@ -1,79 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); -builder.Services.AddDatabaseDeveloperPageExceptionFilter(); -var app = builder.Build(); - -// -var todoItems = app.MapGroup("/todoitems"); - -todoItems.MapGet("/", GetAllTodos); -todoItems.MapGet("/complete", GetCompleteTodos); -todoItems.MapGet("/{id}", GetTodo); -todoItems.MapPost("/", CreateTodo); -todoItems.MapPut("/{id}", UpdateTodo); -todoItems.MapDelete("/{id}", DeleteTodo); -// - -app.Run(); - -// -// -static async Task GetAllTodos(TodoDb db) -{ - return TypedResults.Ok(await db.Todos.ToArrayAsync()); -} -// - -static async Task GetCompleteTodos(TodoDb db) -{ - return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync()); -} - -// -static async Task GetTodo(int id, TodoDb db) -{ - return await db.Todos.FindAsync(id) - is Todo todo - ? TypedResults.Ok(todo) - : TypedResults.NotFound(); -} -// - -static async Task CreateTodo(Todo todo, TodoDb db) -{ - db.Todos.Add(todo); - await db.SaveChangesAsync(); - - return TypedResults.Created($"/todoitems/{todo.Id}", todo); -} - -static async Task UpdateTodo(int id, Todo inputTodo, TodoDb db) -{ - var todo = await db.Todos.FindAsync(id); - - if (todo is null) return TypedResults.NotFound(); - - todo.Name = inputTodo.Name; - todo.IsComplete = inputTodo.IsComplete; - - await db.SaveChangesAsync(); - - return TypedResults.NoContent(); -} - -static async Task DeleteTodo(int id, TodoDb db) -{ - if (await db.Todos.FindAsync(id) is Todo todo) - { - db.Todos.Remove(todo); - await db.SaveChangesAsync(); - return TypedResults.NoContent(); - } - - return TypedResults.NotFound(); -} -// -// diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/Todo.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/Todo.cs deleted file mode 100644 index 2900a835b043..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/Todo.cs +++ /dev/null @@ -1,6 +0,0 @@ -class Todo -{ - public int Id { get; set; } - public string? Name { get; set; } - public bool IsComplete { get; set; } -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/TodoApi.csproj b/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/TodoApi.csproj deleted file mode 100644 index abc9f7648bf9..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/TodoApi.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - - - diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/TodoDb.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/TodoDb.cs deleted file mode 100644 index c1584b807819..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults/TodoDb.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -class TodoDb : DbContext -{ - public TodoDb(DbContextOptions options) - : base(options) { } - - public DbSet Todos => Set(); -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/Program.cs deleted file mode 100644 index 522968205c21..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/Program.cs +++ /dev/null @@ -1,100 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); -builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddOpenApiDocument(config => -{ - config.DocumentName = "TodoAPI"; - config.Title = "TodoAPI v1"; - config.Version = "v1"; -}); - -var app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.UseOpenApi(); - app.UseSwaggerUi(config => - { - config.DocumentTitle = "TodoAPI"; - config.Path = "/swagger"; - config.DocumentPath = "/swagger/{documentName}/swagger.json"; - config.DocExpansion = "list"; - }); -} - -// -var todoItems = app.MapGroup("/todoitems"); - -todoItems.MapGet("/", GetAllTodos); -todoItems.MapGet("/complete", GetCompleteTodos); -todoItems.MapGet("/{id}", GetTodo); -todoItems.MapPost("/", CreateTodo); -todoItems.MapPut("/{id}", UpdateTodo); -todoItems.MapDelete("/{id}", DeleteTodo); -// - -app.Run(); - -// -// -static async Task GetAllTodos(TodoDb db) -{ - return TypedResults.Ok(await db.Todos.ToArrayAsync()); -} -// - -static async Task GetCompleteTodos(TodoDb db) -{ - return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync()); -} - -// -static async Task GetTodo(int id, TodoDb db) -{ - return await db.Todos.FindAsync(id) - is Todo todo - ? TypedResults.Ok(todo) - : TypedResults.NotFound(); -} -// - -static async Task CreateTodo(Todo todo, TodoDb db) -{ - db.Todos.Add(todo); - await db.SaveChangesAsync(); - - return TypedResults.Created($"/todoitems/{todo.Id}", todo); -} - -static async Task UpdateTodo(int id, Todo inputTodo, TodoDb db) -{ - var todo = await db.Todos.FindAsync(id); - - if (todo is null) return TypedResults.NotFound(); - - todo.Name = inputTodo.Name; - todo.IsComplete = inputTodo.IsComplete; - - await db.SaveChangesAsync(); - - return TypedResults.NoContent(); -} - -static async Task DeleteTodo(int id, TodoDb db) -{ - if (await db.Todos.FindAsync(id) is Todo todo) - { - db.Todos.Remove(todo); - await db.SaveChangesAsync(); - return TypedResults.NoContent(); - } - - return TypedResults.NotFound(); -} -// -// diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/Todo.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/Todo.cs deleted file mode 100644 index 2900a835b043..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/Todo.cs +++ /dev/null @@ -1,6 +0,0 @@ -class Todo -{ - public int Id { get; set; } - public string? Name { get; set; } - public bool IsComplete { get; set; } -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/TodoDb.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/TodoDb.cs deleted file mode 100644 index c1584b807819..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/TodoDb.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -class TodoDb : DbContext -{ - public TodoDb(DbContextOptions options) - : base(options) { } - - public DbSet Todos => Set(); -} diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/Program.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/Program.cs deleted file mode 100644 index b657be5cd7ad..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/Program.cs +++ /dev/null @@ -1,149 +0,0 @@ -#define FINAL // MINIMAL FINAL TYPEDR -#if MINIMAL -// -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); - -app.MapGet("/", () => "Hello World!"); - -app.Run(); -// -#elif FINAL -// -// -using Microsoft.EntityFrameworkCore; - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); -builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddOpenApiDocument(config => -{ - config.DocumentName = "TodoAPI"; - config.Title = "TodoAPI v1"; - config.Version = "v1"; -}); -// -var app = builder.Build(); -// -if (app.Environment.IsDevelopment()) -{ - app.UseOpenApi(); - app.UseSwaggerUi(config => - { - config.DocumentTitle = "TodoAPI"; - config.Path = "/swagger"; - config.DocumentPath = "/swagger/{documentName}/swagger.json"; - config.DocExpansion = "list"; - }); -} -// -// -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.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; - -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.MapGet("/todos/{id}", async Task, NotFound>> (int id, TodoDb db) => - await db.Todos.FindAsync(id) - is Todo todo - ? TypedResults.Ok(todo) - : TypedResults.NotFound()); -// - -/* -// -app.MapGet("/todos/{id}", async (int id, TodoDb db) => - await db.Todos.FindAsync(id) - is Todo todo - ? TypedResults.Ok(todo) - : TypedResults.NotFound()); -// -*/ - -// -app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" })) - .Produces(); -// - -// -app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" })); -// - -app.Run(); -#endif \ No newline at end of file diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/Todo.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/Todo.cs deleted file mode 100644 index 86aba6a37cee..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/Todo.cs +++ /dev/null @@ -1,6 +0,0 @@ -public class Todo -{ - public int Id { get; set; } - public string? Name { get; set; } - public bool IsComplete { get; set; } -} \ No newline at end of file diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/TodoApi.csproj b/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/TodoApi.csproj deleted file mode 100644 index 067275c7128b..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/TodoApi.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - diff --git a/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/TodoDb.cs b/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/TodoDb.cs deleted file mode 100644 index d202ecad4cc8..000000000000 --- a/aspnetcore/tutorials/min-web-api/samples/10.x/todo_SwaggerVersion/TodoDb.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -class TodoDb : DbContext -{ - public TodoDb(DbContextOptions options) - : base(options) { } - - public DbSet Todos => Set(); -} \ No newline at end of file diff --git a/aspnetcore/tutorials/min-web-api/static/10/add-information-dialog-18-7-1.png b/aspnetcore/tutorials/min-web-api/static/10/add-information-dialog-18-7-1.png new file mode 100644 index 000000000000..a3c3e3bd8116 Binary files /dev/null and b/aspnetcore/tutorials/min-web-api/static/10/add-information-dialog-18-7-1.png differ diff --git a/aspnetcore/tutorials/min-web-api/static/10/generate-request-vs17-8-0.png b/aspnetcore/tutorials/min-web-api/static/10/generate-request-vs17-8-0.png new file mode 100644 index 000000000000..9434776fd525 Binary files /dev/null and b/aspnetcore/tutorials/min-web-api/static/10/generate-request-vs17-8-0.png differ diff --git a/aspnetcore/tutorials/min-web-api/static/10/http-file-window-with-response-vs18-6-2.png b/aspnetcore/tutorials/min-web-api/static/10/http-file-window-with-response-vs18-6-2.png new file mode 100644 index 000000000000..02e77ceeb651 Binary files /dev/null and b/aspnetcore/tutorials/min-web-api/static/10/http-file-window-with-response-vs18-6-2.png differ diff --git a/aspnetcore/tutorials/min-web-api/static/10/vsc-select-luanch-2026.png b/aspnetcore/tutorials/min-web-api/static/10/vsc-select-luanch-2026.png new file mode 100644 index 000000000000..a3aa9fde2951 Binary files /dev/null and b/aspnetcore/tutorials/min-web-api/static/10/vsc-select-luanch-2026.png differ