|
|
@@ -131,10 +131,182 @@ REST API — самый популярный сегодня стандарт в
|
|
|
|
|
|
Первым широко распространенным стандартом стал **SOAP** (Simple Object Access Protocol). Но SOAP-сообщения довольно громоздки (как минимум потому, что используют формат XML, а не более лаконичный JSON), что стало особенно неудобно с распространением мобильного интернета. SOAP изначально предназначался для описания вызова удаленных процедур — RPC (Remote Procedure Call), когда клиентское приложение выполняет функцию или процедуру на сервере, а сервер отправляет результат обратно клиенту.
|
|
|
|
|
|
-Современная реализация этого подхода — **gRPC**, на ней реализованы API сервисов Yandex Cloud. С их помощью вы можете создавать приложения и сервисы, использующие ресурсы Yandex Cloud.
|
|
|
+## Создание сервера ASP.NET Core.
|
|
|
|
|
|
+### Маршрутизация в ASP.NET
|
|
|
|
|
|
-## Создание сервера ASP.NET Core.
|
|
|
+>Взято из [официальной документации Microsoft](https://learn.microsoft.com/ru-ru/aspnet/core/fundamentals/routing?view=aspnetcore-7.0#endpoints), поэтому язык несколько тяжеловесный.
|
|
|
+
|
|
|
+Маршрутизация обеспечивает сопоставление входящих HTTP-запросов и их распределение по исполняемым конечным точкам приложения. [Конечные точки](#конечные-точки) — это блоки исполняемого кода обработки запросов приложения. Конечные точки определяются в приложении и настраиваются при его запуске. Процесс сопоставления конечных точек может извлекать значения из URL-адреса запроса и предоставлять эти значения для обработки запроса.
|
|
|
+
|
|
|
+>Напомню состав URL-адреса
|
|
|
+>
|
|
|
+>```
|
|
|
+>http://доменное.имя:порт/какой/то/путь?ключ=значение&ещё=чтонибудь
|
|
|
+>```
|
|
|
+>где:
|
|
|
+* `http://` - протокол запроса, обычно используются **HTTP** и/или **HTTPS**
|
|
|
+* `доменное.имя:порт` - адрес сервера, порт можно не указывать
|
|
|
+* `/какой/то/путь` - путь (PATH) запроса
|
|
|
+* `?ключ=значение&ещё=чтонибудь` - параметры запроса (querystring) в виде списка `ключ=значение`, разделитель `&`. Могут отсутствовать, начинаются со знака `?`.
|
|
|
+
|
|
|
+#### Основы маршрутизации
|
|
|
+
|
|
|
+В следующем коде приведен базовый пример маршрутизации.
|
|
|
+
|
|
|
+```cs
|
|
|
+var builder = WebApplication.CreateBuilder(args);
|
|
|
+var app = builder.Build();
|
|
|
+
|
|
|
+app.MapGet("/", () => "Hello World!");
|
|
|
+
|
|
|
+app.Run();
|
|
|
+```
|
|
|
+
|
|
|
+В этом примере используется одна конечная точка с помощью **MapGet** метода:
|
|
|
+
|
|
|
+* При отправке HTTP-запроса **GET** в корневой URL-адрес `/`:
|
|
|
+ * Выполняется делегат запроса.
|
|
|
+ * В ответ HTTP записывается Hello World!.
|
|
|
+* Если метод запроса не является **GET** или если корневой URL-адрес не `/`, сопоставление маршрута не выполняется и возвращается сообщение об ошибке HTTP 404.
|
|
|
+
|
|
|
+Маршрутизация использует пару ПО промежуточного слоя: UseRouting и UseEndpoints.
|
|
|
+
|
|
|
+* **UseRouting** добавляет соответствие маршрута в конвейер ПО промежуточного слоя. Это ПО промежуточного слоя обращается к набору конечных точек, определенных в приложении, и выбирает наиболее подходящее на основе запроса.
|
|
|
+* **UseEndpoints** добавляет выполнение конечной точки в конвейер ПО промежуточного слоя. Он запускает делегат, связанный с выбранной конечной точкой.
|
|
|
+
|
|
|
+Приложениям обычно не требуется вызывать **UseRouting** или **UseEndpoints**. WebApplicationBuilder настраивает конвейер ПО промежуточного слоя, который создает программу-оболочку для ПО промежуточного слоя, добавленное в `Program.cs` с использованием **UseRouting** и **UseEndpoints**. Но приложения могут изменять порядок, в котором выполняются **UseRouting** и **UseEndpoints**, вызывая эти методы явным образом. Например, следующий код явным образом вызывает **UseRouting**:
|
|
|
+
|
|
|
+```cs
|
|
|
+app.Use(async (context, next) =>
|
|
|
+{
|
|
|
+ // ...
|
|
|
+ await next(context);
|
|
|
+});
|
|
|
+
|
|
|
+app.UseRouting();
|
|
|
+
|
|
|
+app.MapGet("/", () => "Hello World!");
|
|
|
+```
|
|
|
+
|
|
|
+В предыдущем коде:
|
|
|
+
|
|
|
+* Вызов `app.Use` регистрирует пользовательское ПО промежуточного слоя, которое выполняется в начале конвейера.
|
|
|
+* При вызове метода **UseRouting** ПО промежуточного слоя сопоставления маршрутов настраивается для запуска после пользовательского ПО промежуточного слоя.
|
|
|
+* Конечная точка, зарегистрированная с использованием **MapGet**, выполняется в конце конвейера.
|
|
|
+
|
|
|
+Если бы предыдущий пример не включал вызов **UseRouting**, то пользовательское ПО промежуточного слоя выполнилось бы после ПО промежуточного слоя сопоставления маршрутов.
|
|
|
+
|
|
|
+#### Конечные точки
|
|
|
+
|
|
|
+Для определения конечной точки используется метод **MapGet** (а также **MapPost**, **MapPut**, **MapDelete** и т.п, т.е. после префикса Map указывается метод HTTP). Конечная точка — это то, что можно:
|
|
|
+
|
|
|
+* выбрать путем сопоставления URL-адреса и метода HTTP;
|
|
|
+* выполнить путем запуска делегата.
|
|
|
+
|
|
|
+Конечные точки, которые могут быть сопоставлены и выполнены приложением, настраиваются в UseEndpoints. Например, **MapGet**, **MapPost** и [аналогичные методы](https://learn.microsoft.com/ru-ru/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions?view=aspnetcore-7.0) подключают делегаты запросов к системе маршрутизации. Для подключения функций платформы **ASP.NET Core** к системе маршрутизации можно использовать дополнительные методы.
|
|
|
+
|
|
|
+* MapRazorPages для Razor Pages
|
|
|
+* MapControllers для контроллеров
|
|
|
+* MapHub<THub> для SignalR
|
|
|
+* MapGrpcService<TService> для gRPC
|
|
|
+
|
|
|
+Ниже представлен пример маршрутизации с более сложным шаблоном маршрута.
|
|
|
+
|
|
|
+```cs
|
|
|
+app.MapGet(
|
|
|
+ "/hello/{name:alpha}",
|
|
|
+ (string name) => $"Hello {name}!");
|
|
|
+```
|
|
|
+
|
|
|
+Строка `/hello/{name:alpha}` является **шаблоном маршрута**. Шаблон маршрута используется для настройки способа сопоставления конечной точки. В этом случае шаблон соответствует следующим условиям.
|
|
|
+
|
|
|
+Любой URL-путь, начинающийся с `/hello/`, после которого следует набор буквенных символов. `:alpha` применяет ограничение маршрута, которое соответствует только буквенным символам. Ограничения маршрута описаны далее в этой статье.
|
|
|
+
|
|
|
+Второй сегмент URL-пути, {name:alpha}:
|
|
|
+
|
|
|
+* привязан к параметру `name`;
|
|
|
+* Записывается и хранится в [HttpRequest.RouteValues](https://learn.microsoft.com/ru-ru/dotnet/api/microsoft.aspnetcore.http.httprequest.routevalues?view=aspnetcore-7.0).
|
|
|
+
|
|
|
+Если человеческим языком, то в пути запроса может быть один или несколько параметров, которые автоматически добавляются в параметры делегата (лямбда-функции)
|
|
|
+
|
|
|
+#### Ограничения маршрута
|
|
|
+
|
|
|
+Ограничения маршрута применяются, когда найдено соответствие входящему URL-адресу и путь URL-адреса был разобран на значения маршрута. Как правило, ограничения маршрута служат для проверки значения маршрута, связанного посредством шаблона маршрута, и принятия решения касательно того, является ли значение приемлемым (истина или ложь). Некоторые ограничения маршрута используют данные, не относящиеся к значению маршрута, для определения возможности маршрутизации запроса. Например, HttpMethodRouteConstraint может принимать или отклонять запрос в зависимости от HTTP-команды. Ограничения используются в маршрутизации запросов и создании ссылок.
|
|
|
+
|
|
|
+>Предупреждение
|
|
|
+>
|
|
|
+>Не используйте ограничения для проверки входных данных. Если для проверки входных данных используются ограничения, недопустимые входные данные приводят к ошибке 404 ("Не найдено"). Недопустимые входные данные должны привести к ошибке 400 ("Неверный запрос") с соответствующим сообщением об ошибке. Ограничения маршрутов следует использовать для разрешения неоднозначности похожих маршрутов, а не для проверки входных данных определенного маршрута.
|
|
|
+
|
|
|
+В приведенной ниже таблице показаны примеры ограничения маршрутов и их ожидаемое поведение.
|
|
|
+
|
|
|
+ограничение | Пример | Примеры совпадений | Примечания
|
|
|
+------------|--------|--------------------|-----------
|
|
|
+int | {id:int} | 123456789, -123456789 | Соответствует любому целому числу
|
|
|
+bool | {active:bool} | true, FALSE | Соответствует true или false. Без учета регистра
|
|
|
+datetime | {dob:datetime} | 2016-12-31, 2016-12-31 7:32pm | Соответствует допустимому значению DateTime для инвариантного языка и региональных параметров. См. предупреждение выше.
|
|
|
+decimal | {price:decimal} | 49.99, -1,000.01 | Соответствует допустимому значению decimal для инвариантного языка и региональных параметров. См. предупреждение выше.
|
|
|
+double | {weight:double} | 1.234, -1,001.01e8 | Соответствует допустимому значению double для инвариантного языка и региональных параметров. См. предупреждение выше.
|
|
|
+float | {weight:float} | 1.234, -1,001.01e8 | Соответствует допустимому значению float для инвариантного языка и региональных параметров. См. предупреждение выше.
|
|
|
+guid | {id:guid} | CD2C1638-1638-72D5-1638-DEADBEEF1638 | Соответствует допустимому значению Guid
|
|
|
+long | {ticks:long} | 123456789, -123456789 | Соответствует допустимому значению long
|
|
|
+minlength(value) | {username:minlength(4)} | Rick | Строка должна содержать не менее 4 символов
|
|
|
+maxlength(value) | {filename:maxlength(8)} | MyFile | Строка должна содержать не более 8 символов
|
|
|
+length(length) | {filename:length(12)} | somefile.txt | Длина строки должна составлять ровно 12 символов
|
|
|
+length(min,max) | {filename:length(8,16)} | somefile.txt | Строка должна содержать от 8 до 16 символов
|
|
|
+min(value) | {age:min(18)} | 19 | Целочисленное значение не меньше 18
|
|
|
+max(value) | {age:max(120)} | 91 | Целочисленное значение не больше 120
|
|
|
+range(min,max) | {age:range(18,120)} | 91 | Целочисленное значение от 18 до 120
|
|
|
+alpha | {name:alpha} | Rick | Строка должна состоять из одной буквы или нескольких (a-z) без учета регистра.
|
|
|
+regex(expression) | {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} | 123-45-6789 | Строка должна соответствовать регулярному выражению. См. советы по определению регулярного выражения.
|
|
|
+required | {name:required} | Rick | Определяет обязательное наличие значения, не относящегося к параметру, во время формирования URL-адреса
|
|
|
+
|
|
|
+К одному параметру может применяться несколько ограничений, разделённых двоеточием. Например, следующее ограничение ограничивает параметр целочисленным значением 1 или больше:
|
|
|
+
|
|
|
+```
|
|
|
+users/{id:int:min(1)}
|
|
|
+```
|
|
|
+
|
|
|
+#### Параметры пути и поиска (querystring)
|
|
|
+
|
|
|
+ASP.NET достаточно умный, чтобы вытащить из запроса все параметры, независимо от того где в URL они расположены. Параметры из **пути**, **строки запроса** и **тела запроса** в итоге попадают в коллекцию параметров делегата:
|
|
|
+
|
|
|
+Например, есть такой запрос:
|
|
|
+
|
|
|
+```
|
|
|
+GET http://localhost:8080/test/1/2?pageNum=10
|
|
|
+```
|
|
|
+
|
|
|
+Для него написана **конечная точка**:
|
|
|
+
|
|
|
+```cs
|
|
|
+app.MapGet(
|
|
|
+ "/test/{param1:int}/{param2:int}",
|
|
|
+ (int param1, int param2, int? pageNum, int? pageLen) =>
|
|
|
+ {
|
|
|
+ return $"param1 = {param1}, param2 = {param2}, pageNum = {pageNum}, pageLen = {pageLen}";
|
|
|
+ });
|
|
|
+```
|
|
|
+
|
|
|
+* Имена параметров (*param1* и *param2*) для значений получаемых из пути мы задаем при описании **конечной точки**: `/test/{param1:int}/{param2:int}`
|
|
|
+* Значения параметров *pageNum* и *pageLen* мы получаем из **строки запроса** (обратите внимание, параметр *pageLen* в URL отсутствует, но мы предусмотрели его наличие)
|
|
|
+
|
|
|
+При запросе сервер ответит:
|
|
|
+
|
|
|
+```
|
|
|
+HTTP/1.1 200 OK
|
|
|
+Connection: close
|
|
|
+Content-Type: text/plain; charset=utf-8
|
|
|
+Date: Mon, 16 Oct 2023 14:24:36 GMT
|
|
|
+Server: Kestrel
|
|
|
+Transfer-Encoding: chunked
|
|
|
+
|
|
|
+param1 = 1, param2 = 2, pageNum = 10, pageLen =
|
|
|
+```
|
|
|
+
|
|
|
+Причём порядок параметров в делегате значения не имеет, т.к. они берутся из коллекции по имени.
|
|
|
+
|
|
|
+### Создание API-сервера для CRUD продукции
|
|
|
|
|
|
Для создания API сервера на ASP.NET есть два шаблона: на основе контроллеров и минималистичный. Подробнее можно почитать в [официальной документации microsoft](https://learn.microsoft.com/ru-ru/aspnet/core/fundamentals/apis?view=aspnetcore-7.0).
|
|
|
|
|
|
@@ -157,7 +329,7 @@ REST API — самый популярный сегодня стандарт в
|
|
|
app.Run();
|
|
|
```
|
|
|
|
|
|
- Метод **MapGet** как раз и создаёт *конечную точку* `/` для метода **GET**, т.е. GET-запросы с *конечной точкой* `/` будут обработаны лямбда выражением `() => "Hello World!"`
|
|
|
+ Метод **MapGet** как раз и создаёт *конечную точку* `/` для метода **GET**, т.е. GET-запросы с *конечной точкой* `/` будут обработаны делегатом (лямбда выражением) `() => "Hello World!"`
|
|
|
|
|
|
Если запустить проект, то сервер откроет два порта для прослушивания входящих соединений (для HTTP и HTTPS)
|
|
|
|
|
|
@@ -180,11 +352,11 @@ REST API — самый популярный сегодня стандарт в
|
|
|
|
|
|
1. Подключение БД
|
|
|
|
|
|
- Используйте тот же метод, что и для обычного приложения: [Создание подключения к БД MySQL.](./cs_mysql_connection3.md)
|
|
|
+ Используйте тот же подход, что и для обычного приложения: [Создание подключения к БД MySQL.](./cs_mysql_connection3.md)
|
|
|
|
|
|
-1. Добавление *конечной точки* для получения списка продукции:
|
|
|
+1. Добавление *конечной точки* для получения списка продукции (**C****_Read_****UD**):
|
|
|
|
|
|
- Добавьте ещё один **MapGet**:
|
|
|
+ Добавьте ещё один **MapGet** с конечной точной `/product`:
|
|
|
|
|
|
```cs
|
|
|
app.MapGet("/product", () =>
|
|
|
@@ -196,4 +368,286 @@ REST API — самый популярный сегодня стандарт в
|
|
|
});
|
|
|
```
|
|
|
|
|
|
- т.е. при запросе `GET http://хост:порт/product` мы получим список продукции
|
|
|
+ т.е. при запросе `GET http://хост:порт/product` мы получим список продукции
|
|
|
+
|
|
|
+ ```json
|
|
|
+ [
|
|
|
+ {
|
|
|
+ "id": 128,
|
|
|
+ "title": "Колесо R18 Кованый",
|
|
|
+ "productTypeId": 139,
|
|
|
+ "articleNumber": "241659",
|
|
|
+ "description": null,
|
|
|
+ "image": "\\products\\tire_15.jpg",
|
|
|
+ "productionPersonCount": 4,
|
|
|
+ "productionWorkshopNumber": 10,
|
|
|
+ "minCostForAgent": 111.00,
|
|
|
+ "productType": null,
|
|
|
+ "productCostHistories": [],
|
|
|
+ "productMaterials": [],
|
|
|
+ "productSales": []
|
|
|
+ },
|
|
|
+ ...
|
|
|
+ ```
|
|
|
+
|
|
|
+ В принципе этого может быть уже достаточно на демо-экзамене, если в ТЗ явно не указано какие именно поля должен возвращать запрос.
|
|
|
+
|
|
|
+ Но мы помним, что обычный **LINQ**-запрос получает данные только из одной сущности, поэтому поле *productType* не заполнено, и поля *productCostHistories*, _productMaterials_, _productSales_ вообще не относятся к таблице, а являются виртуальными свойствами модели.
|
|
|
+
|
|
|
+ Для решения первой проблемы мы использовали метод _Include_, но при использовании такого подхода у нас получится обратная связь от типов продукции к продукции и JSON-конвертер выдаст исключение (можете сами проверить на практике)
|
|
|
+
|
|
|
+ Можно настроить конвертер, чтобы он игнорировал циклические ссылки
|
|
|
+
|
|
|
+ ```cs
|
|
|
+ builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
|
|
|
+ {
|
|
|
+ options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
|
|
|
+ options.SerializerOptions.WriteIndented = true;
|
|
|
+ });
|
|
|
+ ```
|
|
|
+
|
|
|
+ но итоговый результат получается слишком подробный:
|
|
|
+
|
|
|
+ ```json
|
|
|
+ [
|
|
|
+ {
|
|
|
+ "id": 128,
|
|
|
+ "title": "Колесо R18 Кованый",
|
|
|
+ "productTypeId": 139,
|
|
|
+ "articleNumber": "241659",
|
|
|
+ "description": null,
|
|
|
+ "image": "\\products\\tire_15.jpg",
|
|
|
+ "productionPersonCount": 4,
|
|
|
+ "productionWorkshopNumber": 10,
|
|
|
+ "minCostForAgent": 111.00,
|
|
|
+ "productType": {
|
|
|
+ "id": 139,
|
|
|
+ "titleType": "Колесо",
|
|
|
+ "defectedPercent": 0,
|
|
|
+ "products": [
|
|
|
+ null,
|
|
|
+ {
|
|
|
+ "id": 139,
|
|
|
+ "title": "Колесо R15 Кованый",
|
|
|
+ "productTypeId": 139,
|
|
|
+ "articleNumber": "376388",
|
|
|
+ "description": null,
|
|
|
+ "image": "",
|
|
|
+ "productionPersonCount": 3,
|
|
|
+ "productionWorkshopNumber": 4,
|
|
|
+ "minCostForAgent": 9439.00,
|
|
|
+ "productType": null,
|
|
|
+ "productCostHistories": [],
|
|
|
+ "productMaterials": [],
|
|
|
+ "productSales": []
|
|
|
+ },
|
|
|
+ ...
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ "productCostHistories": [],
|
|
|
+ "productMaterials": [],
|
|
|
+ "productSales": []
|
|
|
+ ```
|
|
|
+
|
|
|
+ Нам в списке продукции не нужны данные о массиве продуктов для каждого типа продукции.
|
|
|
+
|
|
|
+ Мы можем выбрать не все поля модели, а только нужные нам методом _Select_:
|
|
|
+
|
|
|
+ ```cs
|
|
|
+ return context.Products
|
|
|
+ .Select(p => new {
|
|
|
+ id = p.Id,
|
|
|
+ title = p.Title,
|
|
|
+ productTypeId = p.ProductTypeId,
|
|
|
+ articleNumber = p.ArticleNumber,
|
|
|
+ description = p.Description,
|
|
|
+ image = p.Image,
|
|
|
+ productionPersonCount = p.ProductionPersonCount,
|
|
|
+ productionWorkshopNumber = p.ProductionWorkshopNumber,
|
|
|
+ minCostForAgent = p.MinCostForAgent,
|
|
|
+ productTypeTitle = p.ProductType.TitleType
|
|
|
+ })
|
|
|
+ .ToList();
|
|
|
+ ```
|
|
|
+
|
|
|
+ Т.е. у нас для каждого экземпляра модели (`p`) создаётся новый экземпляр объекта (`new {...}`) в инициализаторе которого мы и прописываем поля, которые хотим видеть на выходе. При этом значение для `p.ProductType.TitleType` подтягивается динамически
|
|
|
+
|
|
|
+ ```json
|
|
|
+ [
|
|
|
+ {
|
|
|
+ "id": 128,
|
|
|
+ "title": "Колесо R18 Кованый",
|
|
|
+ "productTypeId": 139,
|
|
|
+ "articleNumber": "241659",
|
|
|
+ "description": null,
|
|
|
+ "image": "\\products\\tire_15.jpg",
|
|
|
+ "productionPersonCount": 4,
|
|
|
+ "productionWorkshopNumber": 10,
|
|
|
+ "minCostForAgent": 111.00,
|
|
|
+ "productTypeTitle": "Колесо"
|
|
|
+ },
|
|
|
+ ```
|
|
|
+
|
|
|
+ Ну и шлифанём этот метод, добавив обработку **querystring**
|
|
|
+
|
|
|
+ Запрос теперь может включать номер и размер страницы:
|
|
|
+
|
|
|
+ ```cs
|
|
|
+ app.MapGet(
|
|
|
+ "/product",
|
|
|
+ (int? pageNum, int? pageLen) =>
|
|
|
+ {
|
|
|
+ using (var context = new esmirnovContext())
|
|
|
+ {
|
|
|
+ var query = context.Products
|
|
|
+ .Select(p => new
|
|
|
+ {
|
|
|
+ id = p.Id,
|
|
|
+ title = p.Title,
|
|
|
+ productTypeId = p.ProductTypeId,
|
|
|
+ articleNumber = p.ArticleNumber,
|
|
|
+ description = p.Description,
|
|
|
+ image = p.Image,
|
|
|
+ productionPersonCount = p.ProductionPersonCount,
|
|
|
+ productionWorkshopNumber = p.ProductionWorkshopNumber,
|
|
|
+ minCostForAgent = p.MinCostForAgent,
|
|
|
+ productTypeTitle = p.ProductType.TitleType
|
|
|
+ });
|
|
|
+
|
|
|
+ return query
|
|
|
+ .Skip(((pageNum ?? 1) - 1) * (pageLen ?? 20))
|
|
|
+ .Take(pageLen ?? 20)
|
|
|
+ .ToList();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ ```
|
|
|
+
|
|
|
+1. Добавление новой продукции (**_Create_****RUD**)
|
|
|
+
|
|
|
+ Добавьте **MapPost** с конечной точной `/product`:
|
|
|
+
|
|
|
+ ```cs
|
|
|
+ app.MapPost(
|
|
|
+ "/product",
|
|
|
+ (Product postProduct) =>
|
|
|
+ {
|
|
|
+ using (var context = new esmirnovContext())
|
|
|
+ {
|
|
|
+ context.Add(postProduct);
|
|
|
+ context.SaveChanges();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ ```
|
|
|
+
|
|
|
+ Здесь у нас в параметрах делегата объект **Product**, который автоматически получен из тела запроса (ни в пути, ни в строке поиска параметров нет).
|
|
|
+
|
|
|
+ Пример запроса в формате плагина **REST Client** для **VSCode**:
|
|
|
+
|
|
|
+ ```
|
|
|
+ ### добавление продукта
|
|
|
+ POST http://localhost:8080/product
|
|
|
+ Content-Type: application/json
|
|
|
+
|
|
|
+ {
|
|
|
+ "Title": "post title",
|
|
|
+ "ProductTypeId": 141,
|
|
|
+ "ArticleNumber": "1234",
|
|
|
+ "MinCostForAgent": 100.50
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+ - метод запроса: `POST`
|
|
|
+ - путь: `/product`
|
|
|
+ - в заголовке нужно обязательно указать тип содержимого: `Content-Type: application/json`
|
|
|
+ - тело запроса в формате **JSON**, причём названия полей должны соответствовать модели **Product**
|
|
|
+ - должны быть указаны все обязательные поля (кроме `Id` - он автоматически создастся при добавлении), остальные желательно, но не обязательно
|
|
|
+
|
|
|
+1. Изменение существующей продукции (**СR****_Update_****D**)
|
|
|
+
|
|
|
+ **Id** изменяемой продукции будем передавать в пути запроса (это не обязательно, но как пример)
|
|
|
+
|
|
|
+ ```
|
|
|
+ ### изменение продукта
|
|
|
+ PUT http://localhost:8080/product/227
|
|
|
+ Content-Type: application/json
|
|
|
+
|
|
|
+ {
|
|
|
+ "Title": "post title",
|
|
|
+ "ProductTypeId": 141,
|
|
|
+ "ArticleNumber": "1234",
|
|
|
+ "MinCostForAgent": 100.50
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+ * используем метод `PUT`
|
|
|
+ * в пути передаём параметр id продукции `/product/227`
|
|
|
+
|
|
|
+ Реализация **конечной точки**:
|
|
|
+
|
|
|
+ ```cs
|
|
|
+ app.MapPut(
|
|
|
+ "/product/{id:int}",
|
|
|
+ (int id, Product postProduct) =>
|
|
|
+ {
|
|
|
+ using (var context = new esmirnovContext())
|
|
|
+ {
|
|
|
+ // сначала проверяем есть ли такой продукт в базе
|
|
|
+ var product = context.Products.Find(id);
|
|
|
+ if (product == null)
|
|
|
+ return Results.NotFound();
|
|
|
+
|
|
|
+ // тут можно приделать проверку данных
|
|
|
+
|
|
|
+ // переписываем данные из тела запроса в модель
|
|
|
+ product.Title = postProduct.Title;
|
|
|
+ product.ProductionWorkshopNumber = postProduct.ProductionWorkshopNumber;
|
|
|
+
|
|
|
+ // ...
|
|
|
+
|
|
|
+ context.Update(product);
|
|
|
+ context.SaveChanges();
|
|
|
+ }
|
|
|
+
|
|
|
+ // без этой строки ругается, видимо если мы задействовали
|
|
|
+ // метод Results, то обязаны его использовать при любом результате
|
|
|
+ return Results.Ok();
|
|
|
+ });
|
|
|
+ ```
|
|
|
+
|
|
|
+1. Удаление существующей продукции (**СRU****_Delete_**)
|
|
|
+
|
|
|
+ Тут, в принципе, должно быть уже понятно (метод `DELETE` с указанием `id` продукции в пути):
|
|
|
+
|
|
|
+ ```
|
|
|
+ ### удаление продукта
|
|
|
+ DELETE http://localhost:8080/product/232
|
|
|
+ ```
|
|
|
+
|
|
|
+ Реализация **конечной точки**:
|
|
|
+
|
|
|
+ ```cs
|
|
|
+ app.MapDelete(
|
|
|
+ "/product/{id:int}",
|
|
|
+ (int id) =>
|
|
|
+ {
|
|
|
+ using (var context = new esmirnovContext())
|
|
|
+ {
|
|
|
+ var product = context.Products.Find(id);
|
|
|
+ if (product == null)
|
|
|
+ return Results.NotFound();
|
|
|
+
|
|
|
+ // тут тоже нужна логика
|
|
|
+
|
|
|
+ context.Products.Remove(product);
|
|
|
+ context.SaveChanges();
|
|
|
+ }
|
|
|
+
|
|
|
+ return Results.Ok();
|
|
|
+ });
|
|
|
+ ```
|
|
|
+<!--
|
|
|
+авторизация
|
|
|
+https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/#:~:text=In.NET%208%2C%20helpers%20have%20been%20added%20to%20ASP.NET,in%20your%20SPA%20app%20that%20uses%20the%20APIs
|
|
|
+
|
|
|
+https://metanit.com/sharp/aspnet6/13.1.php
|
|
|
+-->
|