Руководство. Создание минимального API с помощью ASP.NET Core

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 8 этой статьи.

Авторы: Рик Андерсон (Rick Anderson) и Том Дайкстра (Tom Dykstra)

Архитектура минимальных API позволяет создавать API для HTTP с минимальным числом зависимостей. Они идеально подходят для микрослужб и приложений, которые хотят включать только минимальные файлы, функции и зависимости в ASP.NET Core.

В этом руководстве описаны основы создания минимального API с помощью ASP.NET Core. Другим подходом к созданию API в ASP.NET Core является использование контроллеров. Сведения о выборе между минимальными API и API на основе контроллера см. в обзоре API. Руководство по созданию проекта API на основе контроллеров , содержащих дополнительные функции, см. в разделе "Создание веб-API".

Обзор

В этом руководстве создается следующий API-интерфейс:

API Description Текст запроса Текст ответа
GET /todoitems Получение всех элементов задач нет Массив элементов задач
GET /todoitems/complete Получение элементов выполненных заданий из списка нет Массив элементов задач
GET /todoitems/{id} Получение объекта по идентификатору нет Элемент задачи
POST /todoitems Добавление нового элемента Элемент задачи Элемент задачи
PUT /todoitems/{id} Обновление существующего элемента Элемент задачи нет
DELETE /todoitems/{id}     Удаление элемента нет нет

Необходимые компоненты

Создание проекта API

  • Запустите Visual Studio 2022 и нажмите Создать проект.

  • В диалоговом окне Создание нового проекта выполните следующие действия.

    • Введите Empty в поле Поиск шаблонов.
    • Выберите шаблон ASP.NET Core Empty и нажмите кнопку "Далее".

    Создание проекта в Visual Studio

  • Присвойте проекту имя TodoApi и щелкните Далее.

  • В диалоговом окне Дополнительные сведения выполните следующие действия.

    • Выберите .NET 9.0 (предварительная версия)
    • Отмена флажка "Не использовать операторы верхнего уровня"
    • Нажмите кнопку Создать

    Дополнительная информация:

Изучение кода

Файл Program.cs содержит следующий код:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Предыдущий код:

  • WebApplicationBuilder Создает и WebApplication использует предварительно настроенные значения по умолчанию.
  • Создает конечную точку / HTTP GET, которая возвращает Hello World!:

Выполнить приложение

Нажмите клавиши CTRL+F5, чтобы выполнить запуск без отладчика.

Visual Studio отображает следующее диалоговое окно.

Этот проект настроен для использования SSL. Вы можете сделать самозаверяющий сертификат, созданный IIS Express, доверенным, чтобы не получать предупреждения SSL в браузере. Сделать SSL-сертификат IIS Express доверенным?

Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.

Отобразится следующее диалоговое окно.

Диалоговое окно

Выберите Да, если согласны доверять сертификату разработки.

Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

Visual Studio запускает Kestrel веб-сервер и открывает окно браузера.

Hello World! отображается в браузере. Файл Program.cs содержит минимальное, но полное приложение.

Закройте окно браузера.

Добавление пакетов NuGet

Для поддержки возможностей базы данных и диагностики, которые используются в этом руководстве, необходимо добавить пакеты NuGet.

  • В меню Средства выберите Диспетчер пакетов NuGet > Управление пакетами NuGet для решения.
  • Откройте вкладку Browse (Обзор).
  • Выберите " Включить предварительную версию".
  • Введите Microsoft.EntityFrameworkCore.InMemory в поле поиска и щелкните Microsoft.EntityFrameworkCore.InMemory.
  • Установите флажок Проект в области справа и выберите Установить.
  • Добавьте пакет Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore по представленным выше инструкциям.

Классы контекста базы данных и модели

  • В папке проекта создайте файл с именем Todo.cs со следующим кодом:
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Приведенный выше код создает модель для этого приложения. Класс модели представляет данные, которыми управляет наше приложение.

  • Создайте файл с именем TodoDb.cs со следующим кодом:
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Предыдущий код определяет контекст базы данных, который является основным классом, который координирует функциональные возможности Entity Framework для модели данных. Этот класс является производным от класса Microsoft.EntityFrameworkCore.DbContext.

Добавление кода API

  • Замените содержимое файла Program.cs приведенным ниже кодом.
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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();

Выделенный ниже код добавляет контекст базы данных в контейнер внедрения зависимостей (DI) и позволяет отображать исключения, связанные с базой данных:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

Контейнер DI предоставляет доступ к контексту базы данных и другим службам.

В этом руководстве для тестирования API используются обозреватель конечных точек и HTTP-файлы .

Проверка публикации данных

В следующем коде создается Program.cs конечная точка /todoitems HTTP POST, которая добавляет данные в базу данных в памяти:

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

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

Выполнить приложение. В браузере отображается ошибка 404, так как конечная / точка больше не существует.

Конечная точка POST будет использоваться для добавления данных в приложение.

  • Выберите "Просмотреть>другие конечные точки Windows".>

  • Щелкните правой кнопкой мыши конечную точку POST и выберите "Создать запрос".

    Контекстное меню обозревателя конечных точек, в котором выделен пункт меню

    Новый файл создается в папке TodoApi.httpпроекта с содержимым, аналогичным следующему примеру:

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • Первая строка создает переменную, используемую для всех конечных точек.
    • Следующая строка определяет запрос POST.
    • Тройная строка хэштега (###) — это разделитель запросов: то, что происходит после того, как он предназначен для другого запроса.
  • Запрос POST нуждается в заголовках и тексте. Чтобы определить эти части запроса, добавьте следующие строки сразу после строки запроса POST:

    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    

    Предыдущий код добавляет заголовок Content-Type и текст запроса JSON. Теперь файл TodoApi.http должен выглядеть следующим образом, но с номером порта:

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Выполнить приложение.

  • Выберите ссылку "Отправить запрос", которая находится над строкой POST запроса.

    Окно HTTP-файла с выделенным ссылкой запуска.

    Запрос POST отправляется приложению, а ответ отображается в области ответа .

    Окно HTTP-файла с ответом из запроса POST.

Изучение конечных точек GET

Пример приложения реализует несколько конечных точек GET путем вызова MapGet:

API Description Текст запроса Текст ответа
GET /todoitems Получение всех элементов задач нет Массив элементов задач
GET /todoitems/complete Получение всех выполненных элементов заданий нет Массив элементов задач
GET /todoitems/{id} Получение объекта по идентификатору нет Элемент задачи
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());

Тестирование конечных точек GET

Протестируйте приложение, вызвав GET конечные точки из браузера или используя обозреватель конечных точек. Ниже приведены действия для обозревателя конечных точек.

  • В обозревателе конечных точек щелкните правой кнопкой мыши первую конечную точку GET и выберите команду "Создать запрос".

    В файл добавляется следующее содержимое TodoApi.http :

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Выберите ссылку "Отправить запрос", которая находится над новой GET строкой запроса.

    Запрос GET отправляется приложению, а ответ отображается в области ответа .

  • Текст ответа аналогичен следующему json:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • В обозревателе конечных точек щелкните правой кнопкой мыши конечную точку /todoitems/{id} GET и выберите команду "Создать запрос". В файл добавляется следующее содержимое TodoApi.http :

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Замените {id} на 1.

  • Выберите ссылку "Отправить запрос" , которая находится над новой строкой запроса GET.

    Запрос GET отправляется приложению, а ответ отображается в области ответа .

  • Текст ответа аналогичен следующему json:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Это приложение использует выполняющуюся в памяти базу данных. После перезапуска приложения запрос GET не возвращает никаких данных. Если данные не возвращаются, POST-данные в приложение и повторите запрос GET.

Возвращаемые значения

ASP.NET Core автоматически сериализует объект в формат JSON и записывает данные JSON в тело сообщения ответа. Код ответа для этого типа возвращаемого значения равен 200 OK, что свидетельствует об отсутствии необработанных исключений. Необработанные исключения преобразуются в ошибки 5xx.

Типы возвращаемых значений могут представлять широкий спектр кодов состояний HTTP. Например, метод GET /todoitems/{id} может возвращать два разных значения состояния:

  • Если запрошенному идентификатору не соответствует ни один элемент, метод возвращает код ошибки 404 NotFound.
  • В противном случае метод возвращает код 200 с телом ответа JSON. При возвращении item возвращается ответ HTTP 200.

Изучение конечной точки PUT

Этот пример приложения реализует одну конечную точку PUT с помощью MapPut:

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();
});

Этот метод отличается от метода MapPost только тем, что использует метод HTTP PUT. Успешный ответ возвращает состояние 204 (без содержимого). Согласно спецификации HTTP, запрос PUT требует, чтобы клиент отправлял всю обновленную сущность, а не только изменения. Чтобы обеспечить поддержку частичных обновлений, используйте HTTP PATCH.

Тестирование конечной точки PUT

В этом примере используется база данных в памяти, которая должна быть инициирована при каждом запуске приложения. При выполнении вызова PUT в базе данных уже должен существовать какой-либо элемент. Для этого перед вызовом PUT выполните вызов GET, чтобы убедиться в наличии такого элемента в базе данных.

Обновите элемент, имеющий Id = 1 и задали его имя "feed fish".

  • В обозревателе конечных точек щелкните правой кнопкой мыши конечную точку PUT и выберите " Создать запрос".

    В файл добавляется следующее содержимое TodoApi.http :

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • В строке запроса PUT замените {id} на 1.

  • Добавьте следующие строки сразу после строки запроса PUT:

    Content-Type: application/json
    
    {
      "name": "feed fish",
      "isComplete": false
    }
    

    Предыдущий код добавляет заголовок Content-Type и текст запроса JSON.

  • Выберите ссылку "Отправить запрос" , которая находится над новой строкой запроса PUT.

    Запрос PUT отправляется приложению, а ответ отображается в области ответа . Текст ответа пуст, а код состояния — 204.

Проверка и проверка конечной точки DELETE

Этот пример приложения реализует одну конечную точку DELETE с помощью MapDelete:

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();
});
  • В обозревателе конечных точек щелкните правой кнопкой мыши конечную точку DELETE и выберите "Создать запрос".

    Запрос DELETE добавляется в TodoApi.http.

  • Замените {id} строку запроса DELETE на 1. Запрос DELETE должен выглядеть следующим образом:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Выберите ссылку "Отправить запрос" для запроса DELETE.

    Запрос DELETE отправляется приложению, а ответ отображается в области ответа . Текст ответа пуст, а код состояния — 204.

Использование API MapGroup

Пример кода приложения повторяет todoitems префикс URL-адреса при каждом настройке конечной точки. API часто имеют группы конечных точек с общим префиксом URL-адреса, а MapGroup метод доступен для организации таких групп. Это уменьшает повторяющийся код и позволяет настраивать целые группы конечных точек с одним вызовом методов, таких как RequireAuthorization и WithMetadata.

Замените все содержимое Program.cs следующим кодом:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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();

Предыдущий код имеет следующие изменения:

  • Добавляется var todoItems = app.MapGroup("/todoitems"); для настройки группы с помощью префикса /todoitemsURL-адреса.
  • Изменяет все app.Map<HttpVerb> методы todoItems.Map<HttpVerb>на .
  • Удаляет префикс /todoitems URL-адреса из Map<HttpVerb> вызовов метода.

Проверьте конечные точки, чтобы убедиться, что они работают одинаково.

Использование API TypedResults

TypedResults Возврат, а не Results имеет нескольких преимуществ, включая тестируемость и автоматически возвращая метаданные типа ответа для OpenAPI, чтобы описать конечную точку. Дополнительные сведения см. в разделе TypedResults и Results.

Методы Map<HttpVerb> могут вызывать методы обработчика маршрутов вместо использования лямбда-кодов. Чтобы просмотреть пример, обновите Program.cs со следующим кодом:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> 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<IResult> 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();
}

Теперь Map<HttpVerb> код вызывает методы вместо лямбда-кодов:

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

Эти методы возвращают объекты, реализующие IResult и определяемые следующими способами TypedResults:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> 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<IResult> 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();
}

Модульные тесты могут вызывать эти методы и проверять, что они возвращают правильный тип. Например, если метод имеет следующий GetAllTodosтип:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

Код модульного теста может убедиться, что объект типа Ok<Todo[]> возвращается из метода обработчика. Например:

public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}

Предотвращение избыточной публикации

В настоящее время пример приложения предоставляет весь объект Todo. Рабочие приложения в рабочих приложениях, подмножество модели часто используется для ограничения данных, которые могут быть входными и возвращаемыми. Это связано с несколькими причинами, и безопасность является основной. Подмножество модели обычно называется объектом передачи данных (DTO), моделью ввода или моделью представления. В этой статье используется DTO.

DTO можно использовать для:

  • Предотвращение избыточной публикации.
  • Скрытие свойств, которые клиенты не должны просматривать.
  • Опустить некоторые свойства для уменьшения размера полезных данных.
  • Сведение графов объектов, содержащих вложенные объекты. Сведенные графы объектов могут быть удобнее для клиентов.

Чтобы продемонстрировать подход с применением DTO, обновите класс Todo, включив в него поле секрета:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

Поле секрета должно быть скрыто в этом приложении, однако административное приложение может отобразить его.

Убедитесь, что вы можете отправить и получить секретное поле.

Создайте файл с именем TodoItemDTO.cs со следующим кодом:

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

Замените содержимое Program.cs файла следующим кодом, чтобы использовать эту модель DTO:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> 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<IResult> 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<IResult> 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();
}

Убедитесь, что вы можете опубликовать и получить все поля, кроме секретного поля.

Устранение неполадок с завершенным примером

Если вы столкнулись с проблемой, которую не можете решить, сравните свой код с кодом готового проекта. Просмотрите или скачайте завершенный проект (порядок загрузки).

Следующие шаги

Подробнее

Краткий справочник по минимальным API

Архитектура минимальных API позволяет создавать API для HTTP с минимальным числом зависимостей. Они идеально подходят для микрослужб и приложений, которым нужен небольшой набор файлов, компонентов и зависимостей на платформе ASP.NET Core.

В этом руководстве описаны основы создания минимального API с помощью ASP.NET Core. Другим подходом к созданию API в ASP.NET Core является использование контроллеров. Сведения о выборе между минимальными API и API на основе контроллера см . в обзоре API. Руководство по созданию проекта API на основе контроллеров , содержащих дополнительные функции, см. в разделе "Создание веб-API".

Обзор

В этом руководстве создается следующий API-интерфейс:

API Description Текст запроса Текст ответа
GET /todoitems Получение всех элементов задач нет Массив элементов задач
GET /todoitems/complete Получение элементов выполненных заданий из списка нет Массив элементов задач
GET /todoitems/{id} Получение объекта по идентификатору нет Элемент задачи
POST /todoitems Добавление нового элемента Элемент задачи Элемент задачи
PUT /todoitems/{id} Обновление существующего элемента Элемент задачи нет
DELETE /todoitems/{id}     Удаление элемента нет нет

Необходимые компоненты

  • Visual Studio 2022 с рабочей нагрузкой ASP.NET и веб-разработка.

    Рабочие нагрузки установщика VS22

Создание проекта API

  • Запустите Visual Studio 2022 и нажмите Создать проект.

  • В диалоговом окне Создание нового проекта выполните следующие действия.

    • Введите Empty в поле Поиск шаблонов.
    • Выберите шаблон ASP.NET Core Empty и нажмите кнопку "Далее".

    Создание проекта в Visual Studio

  • Присвойте проекту имя TodoApi и щелкните Далее.

  • В диалоговом окне Дополнительные сведения выполните следующие действия.

    • Выберите .NET 7.0
    • Отмена флажка "Не использовать операторы верхнего уровня"
    • Нажмите кнопку Создать

    Дополнительная информация:

Изучение кода

Файл Program.cs содержит следующий код:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Предыдущий код:

  • WebApplicationBuilder Создает и WebApplication использует предварительно настроенные значения по умолчанию.
  • Создает конечную точку / HTTP GET, которая возвращает Hello World!:

Выполнить приложение

Нажмите клавиши CTRL+F5, чтобы выполнить запуск без отладчика.

Visual Studio отображает следующее диалоговое окно.

Этот проект настроен для использования SSL. Вы можете сделать самозаверяющий сертификат, созданный IIS Express, доверенным, чтобы не получать предупреждения SSL в браузере. Сделать SSL-сертификат IIS Express доверенным?

Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.

Отобразится следующее диалоговое окно.

Диалоговое окно

Выберите Да, если согласны доверять сертификату разработки.

Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

Visual Studio запускает Kestrel веб-сервер и открывает окно браузера.

Hello World! отображается в браузере. Файл Program.cs содержит минимальное, но полное приложение.

Добавление пакетов NuGet

Для поддержки возможностей базы данных и диагностики, которые используются в этом руководстве, необходимо добавить пакеты NuGet.

  • В меню Средства выберите Диспетчер пакетов NuGet > Управление пакетами NuGet для решения.
  • Откройте вкладку Browse (Обзор).
  • Введите Microsoft.EntityFrameworkCore.InMemory в поле поиска и щелкните Microsoft.EntityFrameworkCore.InMemory.
  • Установите флажок "Проект" в правой области.
  • В раскрывающемся списке "Версия" выберите последнюю версию 7, например 7.0.17, и нажмите кнопку "Установить".
  • Следуйте приведенным выше инструкциям, чтобы добавить Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore пакет с последней доступной версией 7.

Классы контекста базы данных и модели

В папке проекта создайте файл с именем Todo.cs со следующим кодом:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Приведенный выше код создает модель для этого приложения. Класс модели представляет данные, которыми управляет наше приложение.

Создайте файл с именем TodoDb.cs со следующим кодом:

using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Предыдущий код определяет контекст базы данных, который является основным классом, который координирует функциональные возможности Entity Framework для модели данных. Этот класс является производным от класса Microsoft.EntityFrameworkCore.DbContext.

Добавление кода API

Замените содержимое файла Program.cs приведенным ниже кодом.

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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();

Выделенный ниже код добавляет контекст базы данных в контейнер внедрения зависимостей (DI) и позволяет отображать исключения, связанные с базой данных:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

Контейнер DI предоставляет доступ к контексту базы данных и другим службам.

Создание пользовательского интерфейса тестирования API с помощью Swagger

Существует множество доступных средств тестирования веб-API, которые можно выбрать, и вы можете следовать инструкциям в этом руководстве по тестированию API с помощью собственного предпочтительного средства.

В этом руководстве используется пакет .NET NSwag.AspNetCore, который интегрирует средства Swagger для создания тестового пользовательского интерфейса, который соответствует спецификации OpenAPI:

  • NSwag: библиотека .NET, которая интегрирует Swagger непосредственно в приложения ASP.NET Core, предоставляя ПО промежуточного слоя и конфигурацию.
  • Swagger: набор средств с открытым исходным кодом, таких как OpenAPIGenerator и SwaggerUI, создающий страницы тестирования API, которые соответствуют спецификации OpenAPI.
  • Спецификация OpenAPI: документ, описывающий возможности API на основе заметок XML и атрибутов в контроллерах и моделях.

Дополнительные сведения об использовании OpenAPI и NSwag с ASP.NET см . в документации по веб-API ASP.NET Core с помощью Swagger /OpenAPI.

Установка инструментов Swagger

  • Выполните следующую команду:

    dotnet add package NSwag.AspNetCore
    

Предыдущая команда добавляет пакет NSwag.AspNetCore , содержащий средства для создания документов Swagger и пользовательского интерфейса.

Настройка ПО промежуточного слоя Swagger

  • Добавьте следующий выделенный код перед app определением в строке var app = builder.Build();

    using Microsoft.EntityFrameworkCore;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddDbContext<TodoDb>(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();
    

В предыдущем коде:

  • builder.Services.AddEndpointsApiExplorer();: включает обозреватель API, который представляет собой службу, которая предоставляет метаданные о HTTP-API. Обозреватель API используется Swagger для создания документа Swagger.

  • builder.Services.AddOpenApiDocument(config => {...});: добавляет генератор документов Swagger OpenAPI в службы приложений и настраивает его для предоставления дополнительных сведений об API, таких как название и версия. Сведения о предоставлении более надежных сведений об API см. в статье "Начало работы с NSwag" и ASP.NET Core

  • Добавьте следующий выделенный код в следующую строку после app определения в строке var app = builder.Build();

    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";
        });
    }
    

    Предыдущий код включает ПО промежуточного слоя Swagger для обслуживания созданного документа JSON и пользовательского интерфейса Swagger. Swagger включен только в среде разработки. Включение Swagger в рабочей среде может предоставлять потенциально конфиденциальные сведения о структуре и реализации API.

Проверка публикации данных

В следующем коде создается Program.cs конечная точка /todoitems HTTP POST, которая добавляет данные в базу данных в памяти:

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

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

Выполнить приложение. В браузере отображается ошибка 404, так как конечная / точка больше не существует.

Конечная точка POST будет использоваться для добавления данных в приложение.

  • При выполнении приложения в браузере перейдите на https://localhost:<port>/swagger страницу тестирования API, созданную Swagger.

    Страница тестирования API, созданная Swagger

  • На странице тестирования API Swagger выберите "Post /todoitems>", чтобы попробовать его.

  • Обратите внимание, что поле текста запроса содержит созданный формат примера, отражающий параметры API.

  • В тексте запроса введите JSON для элемента , не указывая необязательное id:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Выберите Выполнить.

    Swagger с Post

Swagger предоставляет область "Ответы" под кнопкой "Выполнить ".

Swagger с ответом Post

Обратите внимание на некоторые полезные сведения:

  • cURL: Swagger предоставляет пример команды cURL в синтаксисе Unix/Linux, которая может выполняться в командной строке с любой оболочкой bash, используюющей синтаксис Unix/Linux, включая Git Bash из Git для Windows.
  • URL-адрес запроса: упрощенное представление HTTP-запроса, сделанного кодом JavaScript пользовательского интерфейса Swagger для вызова API. Фактические запросы могут включать такие сведения, как заголовки и параметры запроса, а также текст запроса.
  • Ответ сервера: включает текст ответа и заголовки. Текст ответа показывает id , что задано значение 1.
  • Код ответа: возвращен код состояния 201 HTTP , указывающий, что запрос успешно обработан и привел к созданию нового ресурса.

Изучение конечных точек GET

Пример приложения реализует несколько конечных точек GET путем вызова MapGet:

API Description Текст запроса Текст ответа
GET /todoitems Получение всех элементов задач нет Массив элементов задач
GET /todoitems/complete Получение всех выполненных элементов заданий нет Массив элементов задач
GET /todoitems/{id} Получение объекта по идентификатору нет Элемент задачи
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());

Тестирование конечных точек GET

Протестируйте приложение, вызвав конечные точки из браузера или Swagger.

  • В Swagger выберите GET /todoitems>Попробовать выполнить.>

  • Кроме того, вызовите GET /todoitems из браузера, введя универсальный код ресурса (URI http://localhost:<port>/todoitems). Например: http://localhost:5001/todoitems

Вызов метода GET /todoitems создает ответ, аналогичный следующему:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Вызовите GET /todoitems/{id} в Swagger, чтобы вернуть данные из определенного идентификатора:

    • Выберите get /todoitems>Try it.
    • Задайте для поля идентификатора значение 1 и нажмите кнопку "Выполнить".
  • Кроме того, вызовите GET /todoitems из браузера, введя универсальный код ресурса (URI https://localhost:<port>/todoitems/1). Например: https://localhost:5001/todoitems/1

  • Ответ аналогичен следующему:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Это приложение использует выполняющуюся в памяти базу данных. После перезапуска приложения запрос GET не возвращает никаких данных. Если данные не возвращаются, POST-данные в приложение и повторите запрос GET.

Возвращаемые значения

ASP.NET Core автоматически сериализует объект в формат JSON и записывает данные JSON в тело сообщения ответа. Код ответа для этого типа возвращаемого значения равен 200 OK, что свидетельствует об отсутствии необработанных исключений. Необработанные исключения преобразуются в ошибки 5xx.

Типы возвращаемых значений могут представлять широкий спектр кодов состояний HTTP. Например, метод GET /todoitems/{id} может возвращать два разных значения состояния:

  • Если запрошенному идентификатору не соответствует ни один элемент, метод возвращает код ошибки 404 NotFound.
  • В противном случае метод возвращает код 200 с телом ответа JSON. При возвращении item возвращается ответ HTTP 200.

Изучение конечной точки PUT

Этот пример приложения реализует одну конечную точку PUT с помощью MapPut:

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();
});

Этот метод отличается от метода MapPost только тем, что использует метод HTTP PUT. Успешный ответ возвращает состояние 204 (без содержимого). Согласно спецификации HTTP, запрос PUT требует, чтобы клиент отправлял всю обновленную сущность, а не только изменения. Чтобы обеспечить поддержку частичных обновлений, используйте HTTP PATCH.

Тестирование конечной точки PUT

В этом примере используется база данных в памяти, которая должна быть инициирована при каждом запуске приложения. При выполнении вызова PUT в базе данных уже должен существовать какой-либо элемент. Для этого перед вызовом PUT выполните вызов GET, чтобы убедиться в наличии такого элемента в базе данных.

Обновите элемент, имеющий Id = 1 и задали его имя "feed fish".

Используйте Swagger для отправки запроса PUT:

  • Выберите Put /todoitems/{id}>Try it.

  • Задайте для поля идентификатора значение 1.

  • Задайте для текста запроса следующий код JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Выберите Выполнить.

Проверка и проверка конечной точки DELETE

Этот пример приложения реализует одну конечную точку DELETE с помощью MapDelete:

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();
});

Используйте Swagger для отправки запроса DELETE:

  • Выберите DELETE /todoitems/{id}>Попробовать его.

  • Задайте для поля идентификатора значение 1 и нажмите кнопку "Выполнить".

    Запрос DELETE отправляется приложению, а ответ отображается на панели "Ответы ". Текст ответа пуст, а код состояния ответа сервера — 204.

Использование API MapGroup

Пример кода приложения повторяет todoitems префикс URL-адреса при каждом настройке конечной точки. API часто имеют группы конечных точек с общим префиксом URL-адреса, а MapGroup метод доступен для организации таких групп. Это уменьшает повторяющийся код и позволяет настраивать целые группы конечных точек с одним вызовом методов, таких как RequireAuthorization и WithMetadata.

Замените все содержимое Program.cs следующим кодом:

using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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();

Предыдущий код имеет следующие изменения:

  • Добавляется var todoItems = app.MapGroup("/todoitems"); для настройки группы с помощью префикса /todoitemsURL-адреса.
  • Изменяет все app.Map<HttpVerb> методы todoItems.Map<HttpVerb>на .
  • Удаляет префикс /todoitems URL-адреса из Map<HttpVerb> вызовов метода.

Проверьте конечные точки, чтобы убедиться, что они работают одинаково.

Использование API TypedResults

TypedResults Возврат, а не Results имеет нескольких преимуществ, включая тестируемость и автоматически возвращая метаданные типа ответа для OpenAPI, чтобы описать конечную точку. Дополнительные сведения см. в разделе TypedResults и Results.

Методы Map<HttpVerb> могут вызывать методы обработчика маршрутов вместо использования лямбда-кодов. Чтобы просмотреть пример, обновите Program.cs со следующим кодом:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> 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<IResult> 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();
}

Теперь Map<HttpVerb> код вызывает методы вместо лямбда-кодов:

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

Эти методы возвращают объекты, реализующие IResult и определяемые следующими способами TypedResults:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> 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<IResult> 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();
}

Модульные тесты могут вызывать эти методы и проверять, что они возвращают правильный тип. Например, если метод имеет следующий GetAllTodosтип:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

Код модульного теста может убедиться, что объект типа Ok<Todo[]> возвращается из метода обработчика. Например:

public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}

Предотвращение избыточной публикации

В настоящее время пример приложения предоставляет весь объект Todo. Рабочие приложения в рабочих приложениях, подмножество модели часто используется для ограничения данных, которые могут быть входными и возвращаемыми. Это связано с несколькими причинами, и безопасность является основной. Подмножество модели обычно называется объектом передачи данных (DTO), моделью ввода или моделью представления. В этой статье используется DTO.

DTO можно использовать для:

  • Предотвращение избыточной публикации.
  • Скрытие свойств, которые клиенты не должны просматривать.
  • Опустить некоторые свойства для уменьшения размера полезных данных.
  • Сведение графов объектов, содержащих вложенные объекты. Сведенные графы объектов могут быть удобнее для клиентов.

Чтобы продемонстрировать подход с применением DTO, обновите класс Todo, включив в него поле секрета:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

Поле секрета должно быть скрыто в этом приложении, однако административное приложение может отобразить его.

Убедитесь, что вы можете отправить и получить секретное поле.

Создайте файл с именем TodoItemDTO.cs со следующим кодом:

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

Замените содержимое Program.cs файла следующим кодом, чтобы использовать эту модель DTO:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

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

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.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();

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}


class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Убедитесь, что вы можете опубликовать и получить все поля, кроме секретного поля.

Устранение неполадок с завершенным примером

Если вы столкнулись с проблемой, которую не можете решить, сравните свой код с кодом готового проекта. Просмотрите или скачайте завершенный проект (порядок загрузки).

Следующие шаги

Подробнее

Краткий справочник по минимальным API

Архитектура минимальных API позволяет создавать API для HTTP с минимальным числом зависимостей. Они идеально подходят для микрослужб и приложений, которым нужен небольшой набор файлов, компонентов и зависимостей на платформе ASP.NET Core.

В этом руководстве описаны основы создания минимального API с помощью ASP.NET Core. Другим подходом к созданию API в ASP.NET Core является использование контроллеров. Сведения о выборе между минимальными API и API на основе контроллера см . в обзоре API. Руководство по созданию проекта API на основе контроллеров , содержащих дополнительные функции, см. в разделе "Создание веб-API".

Обзор

В этом руководстве создается следующий API-интерфейс:

API Description Текст запроса Текст ответа
GET /todoitems Получение всех элементов задач нет Массив элементов задач
GET /todoitems/complete Получение элементов выполненных заданий из списка нет Массив элементов задач
GET /todoitems/{id} Получение объекта по идентификатору нет Элемент задачи
POST /todoitems Добавление нового элемента Элемент задачи Элемент задачи
PUT /todoitems/{id} Обновление существующего элемента Элемент задачи нет
DELETE /todoitems/{id}     Удаление элемента нет нет

Необходимые компоненты

Создание проекта API

  • Запустите Visual Studio 2022 и нажмите Создать проект.

  • В диалоговом окне Создание нового проекта выполните следующие действия.

    • Введите Empty в поле Поиск шаблонов.
    • Выберите шаблон ASP.NET Core Empty и нажмите кнопку "Далее".

    Создание проекта в Visual Studio

  • Присвойте проекту имя TodoApi и щелкните Далее.

  • В диалоговом окне Дополнительные сведения выполните следующие действия.

    • Выберите .NET 6.0
    • Отмена флажка "Не использовать операторы верхнего уровня"
    • Нажмите кнопку Создать

Изучение кода

Файл Program.cs содержит следующий код:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Предыдущий код:

  • WebApplicationBuilder Создает и WebApplication использует предварительно настроенные значения по умолчанию.
  • Создает конечную точку / HTTP GET, которая возвращает Hello World!:

Выполнить приложение

Нажмите клавиши CTRL+F5, чтобы выполнить запуск без отладчика.

Visual Studio отображает следующее диалоговое окно.

Этот проект настроен для использования SSL. Вы можете сделать самозаверяющий сертификат, созданный IIS Express, доверенным, чтобы не получать предупреждения SSL в браузере. Сделать SSL-сертификат IIS Express доверенным?

Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.

Отобразится следующее диалоговое окно.

Диалоговое окно

Выберите Да, если согласны доверять сертификату разработки.

Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

Visual Studio запускает Kestrel веб-сервер и открывает окно браузера.

Hello World! отображается в браузере. Файл Program.cs содержит минимальное, но полное приложение.

Добавление пакетов NuGet

Для поддержки возможностей базы данных и диагностики, которые используются в этом руководстве, необходимо добавить пакеты NuGet.

  • В меню Средства выберите Диспетчер пакетов NuGet > Управление пакетами NuGet для решения.
  • Откройте вкладку Browse (Обзор).
  • Введите Microsoft.EntityFrameworkCore.InMemory в поле поиска и щелкните Microsoft.EntityFrameworkCore.InMemory.
  • Установите флажок "Проект" в правой области.
  • В раскрывающемся списке "Версия" выберите последнюю версию 7, например 6.0.28, и нажмите кнопку "Установить".
  • Следуйте приведенным выше инструкциям, чтобы добавить Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore пакет с последней доступной версией 7.

Классы контекста базы данных и модели

В папке проекта создайте файл с именем Todo.cs со следующим кодом:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Приведенный выше код создает модель для этого приложения. Класс модели представляет данные, которыми управляет наше приложение.

Создайте файл с именем TodoDb.cs со следующим кодом:

using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Предыдущий код определяет контекст базы данных, который является основным классом, который координирует функциональные возможности Entity Framework для модели данных. Этот класс является производным от класса Microsoft.EntityFrameworkCore.DbContext.

Добавление кода API

Замените содержимое файла Program.cs приведенным ниже кодом.

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

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();

Выделенный ниже код добавляет контекст базы данных в контейнер внедрения зависимостей (DI) и позволяет отображать исключения, связанные с базой данных:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

Контейнер DI предоставляет доступ к контексту базы данных и другим службам.

Создание пользовательского интерфейса тестирования API с помощью Swagger

Существует множество доступных средств тестирования веб-API, которые можно выбрать, и вы можете следовать инструкциям в этом руководстве по тестированию API с помощью собственного предпочтительного средства.

В этом руководстве используется пакет .NET NSwag.AspNetCore, который интегрирует средства Swagger для создания тестового пользовательского интерфейса, который соответствует спецификации OpenAPI:

  • NSwag: библиотека .NET, которая интегрирует Swagger непосредственно в приложения ASP.NET Core, предоставляя ПО промежуточного слоя и конфигурацию.
  • Swagger: набор средств с открытым исходным кодом, таких как OpenAPIGenerator и SwaggerUI, создающий страницы тестирования API, которые соответствуют спецификации OpenAPI.
  • Спецификация OpenAPI: документ, описывающий возможности API на основе заметок XML и атрибутов в контроллерах и моделях.

Дополнительные сведения об использовании OpenAPI и NSwag с ASP.NET см . в документации по веб-API ASP.NET Core с помощью Swagger /OpenAPI.

Установка инструментов Swagger

  • Выполните следующую команду:

    dotnet add package NSwag.AspNetCore
    

Предыдущая команда добавляет пакет NSwag.AspNetCore , содержащий средства для создания документов Swagger и пользовательского интерфейса.

Настройка ПО промежуточного слоя Swagger

  • В Program.cs добавьте следующие using инструкции в верхней части:

    using NSwag.AspNetCore;
    
  • Добавьте следующий выделенный код перед app определением в строке var app = builder.Build();

    using NSwag.AspNetCore;
    using Microsoft.EntityFrameworkCore;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddDbContext<TodoDb>(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();
    

В предыдущем коде:

  • builder.Services.AddEndpointsApiExplorer();: включает обозреватель API, который представляет собой службу, которая предоставляет метаданные о HTTP-API. Обозреватель API используется Swagger для создания документа Swagger.

  • builder.Services.AddOpenApiDocument(config => {...});: добавляет генератор документов Swagger OpenAPI в службы приложений и настраивает его для предоставления дополнительных сведений об API, таких как название и версия. Сведения о предоставлении более надежных сведений об API см. в статье "Начало работы с NSwag" и ASP.NET Core

  • Добавьте следующий выделенный код в следующую строку после app определения в строке var app = builder.Build();

    
    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";
        });
    }
    
    

    Предыдущий код включает ПО промежуточного слоя Swagger для обслуживания созданного документа JSON и пользовательского интерфейса Swagger. Swagger включен только в среде разработки. Включение Swagger в рабочей среде может предоставлять потенциально конфиденциальные сведения о структуре и реализации API.

Проверка публикации данных

В следующем коде создается Program.cs конечная точка /todoitems HTTP POST, которая добавляет данные в базу данных в памяти:

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

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

Выполнить приложение. В браузере отображается ошибка 404, так как конечная / точка больше не существует.

Конечная точка POST будет использоваться для добавления данных в приложение.

  • При выполнении приложения в браузере перейдите на https://localhost:<port>/swagger страницу тестирования API, созданную Swagger.

    Страница тестирования API, созданная Swagger

  • На странице тестирования API Swagger выберите "Post /todoitems>", чтобы попробовать его.

  • Обратите внимание, что поле текста запроса содержит созданный формат примера, отражающий параметры API.

  • В тексте запроса введите JSON для элемента , не указывая необязательное id:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Выберите Выполнить.

    Swagger с данными Post

Swagger предоставляет область "Ответы" под кнопкой "Выполнить ".

Панель пересоновок Swagger с помощью области

Обратите внимание на некоторые полезные сведения:

  • cURL: Swagger предоставляет пример команды cURL в синтаксисе Unix/Linux, которая может выполняться в командной строке с любой оболочкой bash, используюющей синтаксис Unix/Linux, включая Git Bash из Git для Windows.
  • URL-адрес запроса: упрощенное представление HTTP-запроса, сделанного кодом JavaScript пользовательского интерфейса Swagger для вызова API. Фактические запросы могут включать такие сведения, как заголовки и параметры запроса, а также текст запроса.
  • Ответ сервера: включает текст ответа и заголовки. Текст ответа показывает id , что задано значение 1.
  • Код ответа: возвращен код состояния 201 HTTP , указывающий, что запрос успешно обработан и привел к созданию нового ресурса.

Изучение конечных точек GET

Пример приложения реализует несколько конечных точек GET путем вызова MapGet:

API Description Текст запроса Текст ответа
GET /todoitems Получение всех элементов задач нет Массив элементов задач
GET /todoitems/complete Получение всех выполненных элементов заданий нет Массив элементов задач
GET /todoitems/{id} Получение объекта по идентификатору нет Элемент задачи
app.MapGet("/", () => "Hello World!");

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());

Тестирование конечных точек GET

Протестируйте приложение, вызвав конечные точки из браузера или Swagger.

  • В Swagger выберите GET /todoitems>Попробовать выполнить.>

  • Кроме того, вызовите GET /todoitems из браузера, введя универсальный код ресурса (URI http://localhost:<port>/todoitems). Например: http://localhost:5001/todoitems

Вызов метода GET /todoitems создает ответ, аналогичный следующему:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Вызовите GET /todoitems/{id} в Swagger, чтобы вернуть данные из определенного идентификатора:

    • Выберите get /todoitems>Try it.
    • Задайте для поля идентификатора значение 1 и нажмите кнопку "Выполнить".
  • Кроме того, вызовите GET /todoitems из браузера, введя универсальный код ресурса (URI https://localhost:<port>/todoitems/1). Например, например, https://localhost:5001/todoitems/1

  • Ответ аналогичен следующему:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Это приложение использует выполняющуюся в памяти базу данных. После перезапуска приложения запрос GET не возвращает никаких данных. Если данные не возвращаются, POST-данные в приложение и повторите запрос GET.

Возвращаемые значения

ASP.NET Core автоматически сериализует объект в формат JSON и записывает данные JSON в тело сообщения ответа. Код ответа для этого типа возвращаемого значения равен 200 OK, что свидетельствует об отсутствии необработанных исключений. Необработанные исключения преобразуются в ошибки 5xx.

Типы возвращаемых значений могут представлять широкий спектр кодов состояний HTTP. Например, метод GET /todoitems/{id} может возвращать два разных значения состояния:

  • Если запрошенному идентификатору не соответствует ни один элемент, метод возвращает код ошибки 404 NotFound.
  • В противном случае метод возвращает код 200 с телом ответа JSON. При возвращении item возвращается ответ HTTP 200.

Изучение конечной точки PUT

Этот пример приложения реализует одну конечную точку PUT с помощью MapPut:

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();
});

Этот метод отличается от метода MapPost только тем, что использует метод HTTP PUT. Успешный ответ возвращает состояние 204 (без содержимого). Согласно спецификации HTTP, запрос PUT требует, чтобы клиент отправлял всю обновленную сущность, а не только изменения. Чтобы обеспечить поддержку частичных обновлений, используйте HTTP PATCH.

Тестирование конечной точки PUT

В этом примере используется база данных в памяти, которая должна быть инициирована при каждом запуске приложения. При выполнении вызова PUT в базе данных уже должен существовать какой-либо элемент. Для этого перед вызовом PUT выполните вызов GET, чтобы убедиться в наличии такого элемента в базе данных.

Обновите элемент, имеющий Id = 1 и задали его имя "feed fish".

Используйте Swagger для отправки запроса PUT:

  • Выберите Put /todoitems/{id}>Try it.

  • Задайте для поля идентификатора значение 1.

  • Задайте для текста запроса следующий код JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Выберите Выполнить.

Проверка и проверка конечной точки DELETE

Этот пример приложения реализует одну конечную точку DELETE с помощью MapDelete:

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();
});

Используйте Swagger для отправки запроса DELETE:

  • Выберите DELETE /todoitems/{id}>Попробовать его.

  • Задайте для поля идентификатора значение 1 и нажмите кнопку "Выполнить".

    Запрос DELETE отправляется приложению, а ответ отображается на панели "Ответы ". Текст ответа пуст, а код состояния ответа сервера — 204.

Предотвращение избыточной публикации

В настоящее время пример приложения предоставляет весь объект Todo. Рабочие приложения в рабочих приложениях, подмножество модели часто используется для ограничения данных, которые могут быть входными и возвращаемыми. Это связано с несколькими причинами, и безопасность является основной. Подмножество модели обычно называется объектом передачи данных (DTO), моделью ввода или моделью представления. В этой статье используется DTO.

DTO можно использовать для:

  • Предотвращение избыточной публикации.
  • Скрытие свойств, которые клиенты не должны просматривать.
  • Опустить некоторые свойства для уменьшения размера полезных данных.
  • Сведение графов объектов, содержащих вложенные объекты. Сведенные графы объектов могут быть удобнее для клиентов.

Чтобы продемонстрировать подход с применением DTO, обновите класс Todo, включив в него поле секрета:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

Поле секрета должно быть скрыто в этом приложении, однако административное приложение может отобразить его.

Убедитесь, что вы можете отправить и получить секретное поле.

Создайте файл с именем TodoItemDTO.cs со следующим кодом:

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

Замените содержимое Program.cs файла следующим кодом, чтобы использовать эту модель DTO:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

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

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.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();

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}


class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Убедитесь, что вы можете опубликовать и получить все поля, кроме секретного поля.

Тестирование минимального API

Пример тестирования для минимального приложения API см. в этом примере на GitHub.

Публикация в Azure

Сведения о развертывании в Azure см. в разделе Краткое руководство. Развертывание веб-приложения ASP.NET.

Дополнительные ресурсы

Архитектура минимальных API позволяет создавать API для HTTP с минимальным числом зависимостей. Они идеально подходят для микрослужб и приложений, которые хотят включать только минимальные файлы, функции и зависимости в ASP.NET Core.

В этом руководстве описаны основы создания минимального API с помощью ASP.NET Core. Другим подходом к созданию API в ASP.NET Core является использование контроллеров. Сведения о выборе между минимальными API и API на основе контроллера см. в обзоре API. Руководство по созданию проекта API на основе контроллеров , содержащих дополнительные функции, см. в разделе "Создание веб-API".

Обзор

В этом руководстве создается следующий API-интерфейс:

API Description Текст запроса Текст ответа
GET /todoitems Получение всех элементов задач нет Массив элементов задач
GET /todoitems/complete Получение элементов выполненных заданий из списка нет Массив элементов задач
GET /todoitems/{id} Получение объекта по идентификатору нет Элемент задачи
POST /todoitems Добавление нового элемента Элемент задачи Элемент задачи
PUT /todoitems/{id} Обновление существующего элемента Элемент задачи нет
DELETE /todoitems/{id}     Удаление элемента нет нет

Необходимые компоненты

  • Visual Studio 2022 с рабочей нагрузкой ASP.NET и веб-разработка.

    Рабочие нагрузки установщика VS22

Создание проекта API

  • Запустите Visual Studio 2022 и нажмите Создать проект.

  • В диалоговом окне Создание нового проекта выполните следующие действия.

    • Введите Empty в поле Поиск шаблонов.
    • Выберите шаблон ASP.NET Core Empty и нажмите кнопку "Далее".

    Создание проекта в Visual Studio

  • Присвойте проекту имя TodoApi и щелкните Далее.

  • В диалоговом окне Дополнительные сведения выполните следующие действия.

    • Выберите .NET 8.0 (долгосрочная поддержка)
    • Отмена флажка "Не использовать операторы верхнего уровня"
    • Нажмите кнопку Создать

    Дополнительная информация:

Изучение кода

Файл Program.cs содержит следующий код:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Предыдущий код:

  • WebApplicationBuilder Создает и WebApplication использует предварительно настроенные значения по умолчанию.
  • Создает конечную точку / HTTP GET, которая возвращает Hello World!:

Выполнить приложение

Нажмите клавиши CTRL+F5, чтобы выполнить запуск без отладчика.

Visual Studio отображает следующее диалоговое окно.

Этот проект настроен для использования SSL. Вы можете сделать самозаверяющий сертификат, созданный IIS Express, доверенным, чтобы не получать предупреждения SSL в браузере. Сделать SSL-сертификат IIS Express доверенным?

Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.

Отобразится следующее диалоговое окно.

Диалоговое окно

Выберите Да, если согласны доверять сертификату разработки.

Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

Visual Studio запускает Kestrel веб-сервер и открывает окно браузера.

Hello World! отображается в браузере. Файл Program.cs содержит минимальное, но полное приложение.

Закройте окно браузера.

Добавление пакетов NuGet

Для поддержки возможностей базы данных и диагностики, которые используются в этом руководстве, необходимо добавить пакеты NuGet.

  • В меню Средства выберите Диспетчер пакетов NuGet > Управление пакетами NuGet для решения.
  • Откройте вкладку Browse (Обзор).
  • Введите Microsoft.EntityFrameworkCore.InMemory в поле поиска и щелкните Microsoft.EntityFrameworkCore.InMemory.
  • Установите флажок Проект в области справа и выберите Установить.
  • Добавьте пакет Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore по представленным выше инструкциям.

Классы контекста базы данных и модели

  • В папке проекта создайте файл с именем Todo.cs со следующим кодом:
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Приведенный выше код создает модель для этого приложения. Класс модели представляет данные, которыми управляет наше приложение.

  • Создайте файл с именем TodoDb.cs со следующим кодом:
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Предыдущий код определяет контекст базы данных, который является основным классом, который координирует функциональные возможности Entity Framework для модели данных. Этот класс является производным от класса Microsoft.EntityFrameworkCore.DbContext.

Добавление кода API

  • Замените содержимое файла Program.cs приведенным ниже кодом.
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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();

Выделенный ниже код добавляет контекст базы данных в контейнер внедрения зависимостей (DI) и позволяет отображать исключения, связанные с базой данных:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

Контейнер DI предоставляет доступ к контексту базы данных и другим службам.

В этом руководстве для тестирования API используются обозреватель конечных точек и HTTP-файлы .

Проверка публикации данных

В следующем коде создается Program.cs конечная точка /todoitems HTTP POST, которая добавляет данные в базу данных в памяти:

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

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

Выполнить приложение. В браузере отображается ошибка 404, так как конечная / точка больше не существует.

Конечная точка POST будет использоваться для добавления данных в приложение.

  • Выберите "Просмотреть>другие конечные точки Windows".>

  • Щелкните правой кнопкой мыши конечную точку POST и выберите "Создать запрос".

    Контекстное меню обозревателя конечных точек, в котором выделен пункт меню

    Новый файл создается в папке TodoApi.httpпроекта с содержимым, аналогичным следующему примеру:

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • Первая строка создает переменную, используемую для всех конечных точек.
    • Следующая строка определяет запрос POST.
    • Тройная строка хэштега (###) — это разделитель запросов: то, что происходит после того, как он предназначен для другого запроса.
  • Запрос POST нуждается в заголовках и тексте. Чтобы определить эти части запроса, добавьте следующие строки сразу после строки запроса POST:

    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    

    Предыдущий код добавляет заголовок Content-Type и текст запроса JSON. Теперь файл TodoApi.http должен выглядеть следующим образом, но с номером порта:

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Выполнить приложение.

  • Выберите ссылку "Отправить запрос", которая находится над строкой POST запроса.

    Окно HTTP-файла с выделенным ссылкой запуска.

    Запрос POST отправляется приложению, а ответ отображается в области ответа .

    Окно HTTP-файла с ответом из запроса POST.

Изучение конечных точек GET

Пример приложения реализует несколько конечных точек GET путем вызова MapGet:

API Description Текст запроса Текст ответа
GET /todoitems Получение всех элементов задач нет Массив элементов задач
GET /todoitems/complete Получение всех выполненных элементов заданий нет Массив элементов задач
GET /todoitems/{id} Получение объекта по идентификатору нет Элемент задачи
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());

Тестирование конечных точек GET

Протестируйте приложение, вызвав GET конечные точки из браузера или используя обозреватель конечных точек. Ниже приведены действия для обозревателя конечных точек.

  • В обозревателе конечных точек щелкните правой кнопкой мыши первую конечную точку GET и выберите команду "Создать запрос".

    В файл добавляется следующее содержимое TodoApi.http :

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Выберите ссылку "Отправить запрос", которая находится над новой GET строкой запроса.

    Запрос GET отправляется приложению, а ответ отображается в области ответа .

  • Текст ответа аналогичен следующему json:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • В обозревателе конечных точек щелкните правой кнопкой мыши конечную точку /todoitems/{id} GET и выберите команду "Создать запрос". В файл добавляется следующее содержимое TodoApi.http :

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Замените {id} на 1.

  • Выберите ссылку "Отправить запрос" , которая находится над новой строкой запроса GET.

    Запрос GET отправляется приложению, а ответ отображается в области ответа .

  • Текст ответа аналогичен следующему json:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Это приложение использует выполняющуюся в памяти базу данных. После перезапуска приложения запрос GET не возвращает никаких данных. Если данные не возвращаются, POST-данные в приложение и повторите запрос GET.

Возвращаемые значения

ASP.NET Core автоматически сериализует объект в формат JSON и записывает данные JSON в тело сообщения ответа. Код ответа для этого типа возвращаемого значения равен 200 OK, что свидетельствует об отсутствии необработанных исключений. Необработанные исключения преобразуются в ошибки 5xx.

Типы возвращаемых значений могут представлять широкий спектр кодов состояний HTTP. Например, метод GET /todoitems/{id} может возвращать два разных значения состояния:

  • Если запрошенному идентификатору не соответствует ни один элемент, метод возвращает код ошибки 404 NotFound.
  • В противном случае метод возвращает код 200 с телом ответа JSON. При возвращении item возвращается ответ HTTP 200.

Изучение конечной точки PUT

Этот пример приложения реализует одну конечную точку PUT с помощью MapPut:

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();
});

Этот метод отличается от метода MapPost только тем, что использует метод HTTP PUT. Успешный ответ возвращает состояние 204 (без содержимого). Согласно спецификации HTTP, запрос PUT требует, чтобы клиент отправлял всю обновленную сущность, а не только изменения. Чтобы обеспечить поддержку частичных обновлений, используйте HTTP PATCH.

Тестирование конечной точки PUT

В этом примере используется база данных в памяти, которая должна быть инициирована при каждом запуске приложения. При выполнении вызова PUT в базе данных уже должен существовать какой-либо элемент. Для этого перед вызовом PUT выполните вызов GET, чтобы убедиться в наличии такого элемента в базе данных.

Обновите элемент, имеющий Id = 1 и задали его имя "feed fish".

  • В обозревателе конечных точек щелкните правой кнопкой мыши конечную точку PUT и выберите " Создать запрос".

    В файл добавляется следующее содержимое TodoApi.http :

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • В строке запроса PUT замените {id} на 1.

  • Добавьте следующие строки сразу после строки запроса PUT:

    Content-Type: application/json
    
    {
      "name": "feed fish",
      "isComplete": false
    }
    

    Предыдущий код добавляет заголовок Content-Type и текст запроса JSON.

  • Выберите ссылку "Отправить запрос" , которая находится над новой строкой запроса PUT.

    Запрос PUT отправляется приложению, а ответ отображается в области ответа . Текст ответа пуст, а код состояния — 204.

Проверка и проверка конечной точки DELETE

Этот пример приложения реализует одну конечную точку DELETE с помощью MapDelete:

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();
});
  • В обозревателе конечных точек щелкните правой кнопкой мыши конечную точку DELETE и выберите "Создать запрос".

    Запрос DELETE добавляется в TodoApi.http.

  • Замените {id} строку запроса DELETE на 1. Запрос DELETE должен выглядеть следующим образом:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Выберите ссылку "Отправить запрос" для запроса DELETE.

    Запрос DELETE отправляется приложению, а ответ отображается в области ответа . Текст ответа пуст, а код состояния — 204.

Использование API MapGroup

Пример кода приложения повторяет todoitems префикс URL-адреса при каждом настройке конечной точки. API часто имеют группы конечных точек с общим префиксом URL-адреса, а MapGroup метод доступен для организации таких групп. Это уменьшает повторяющийся код и позволяет настраивать целые группы конечных точек с одним вызовом методов, таких как RequireAuthorization и WithMetadata.

Замените все содержимое Program.cs следующим кодом:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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();

Предыдущий код имеет следующие изменения:

  • Добавляется var todoItems = app.MapGroup("/todoitems"); для настройки группы с помощью префикса /todoitemsURL-адреса.
  • Изменяет все app.Map<HttpVerb> методы todoItems.Map<HttpVerb>на .
  • Удаляет префикс /todoitems URL-адреса из Map<HttpVerb> вызовов метода.

Проверьте конечные точки, чтобы убедиться, что они работают одинаково.

Использование API TypedResults

TypedResults Возврат, а не Results имеет нескольких преимуществ, включая тестируемость и автоматически возвращая метаданные типа ответа для OpenAPI, чтобы описать конечную точку. Дополнительные сведения см. в разделе TypedResults и Results.

Методы Map<HttpVerb> могут вызывать методы обработчика маршрутов вместо использования лямбда-кодов. Чтобы просмотреть пример, обновите Program.cs со следующим кодом:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> 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<IResult> 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();
}

Теперь Map<HttpVerb> код вызывает методы вместо лямбда-кодов:

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

Эти методы возвращают объекты, реализующие IResult и определяемые следующими способами TypedResults:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> 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<IResult> 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();
}

Модульные тесты могут вызывать эти методы и проверять, что они возвращают правильный тип. Например, если метод имеет следующий GetAllTodosтип:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

Код модульного теста может убедиться, что объект типа Ok<Todo[]> возвращается из метода обработчика. Например:

public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}

Предотвращение избыточной публикации

В настоящее время пример приложения предоставляет весь объект Todo. Рабочие приложения в рабочих приложениях, подмножество модели часто используется для ограничения данных, которые могут быть входными и возвращаемыми. Это связано с несколькими причинами, и безопасность является основной. Подмножество модели обычно называется объектом передачи данных (DTO), моделью ввода или моделью представления. В этой статье используется DTO.

DTO можно использовать для:

  • Предотвращение избыточной публикации.
  • Скрытие свойств, которые клиенты не должны просматривать.
  • Опустить некоторые свойства для уменьшения размера полезных данных.
  • Сведение графов объектов, содержащих вложенные объекты. Сведенные графы объектов могут быть удобнее для клиентов.

Чтобы продемонстрировать подход с применением DTO, обновите класс Todo, включив в него поле секрета:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

Поле секрета должно быть скрыто в этом приложении, однако административное приложение может отобразить его.

Убедитесь, что вы можете отправить и получить секретное поле.

Создайте файл с именем TodoItemDTO.cs со следующим кодом:

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

Замените содержимое Program.cs файла следующим кодом, чтобы использовать эту модель DTO:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(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<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> 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<IResult> 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<IResult> 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();
}

Убедитесь, что вы можете опубликовать и получить все поля, кроме секретного поля.

Устранение неполадок с завершенным примером

Если вы столкнулись с проблемой, которую не можете решить, сравните свой код с кодом готового проекта. Просмотрите или скачайте завершенный проект (порядок загрузки).

Следующие шаги

Подробнее

Краткий справочник по минимальным API