Sfoglia il codice sorgente

приделал swagger

Евгений Колесников 2 anni fa
parent
commit
9dc55aad4a
8 ha cambiato i file con 140 aggiunte e 51 eliminazioni
  1. 34 0
      api.http
  2. 97 46
      articles/api_asp_net_core.md
  3. 8 4
      articles/cs_product_material.md
  4. BIN
      img/asp003.png
  5. BIN
      img/asp004.png
  6. BIN
      img/asp005.png
  7. BIN
      img/asp006.png
  8. 1 1
      readme.md

+ 34 - 0
api.http

@@ -10,3 +10,37 @@ GET https://api.openweathermap.org/data/2.5/forecast?q=Йошкар-Ола&appid
 ### Запрос погоды за 5 дней https://openweathermap.org/forecast5
 GET https://api.openweathermap.org/data/2.5/forecast?lat={{lat}}&lon={{lon}}&appid={{token}}&lang=ru&units=metric&mode=xml
 
+### 
+GET http://localhost:8080/product?pageNum=15
+Authorization: Basic admin:passwor
+
+### 
+GET http://localhost:8080/product/150
+
+### 
+POST http://localhost:8080/product
+Content-Type: application/json
+
+{
+    "Title": "post title",
+    "ProductTypeId": 141,
+    "ArticleNumber": "1234",
+    "MinCostForAgent": 100.50
+}
+
+### 
+GET http://localhost:8080/test/1/2?pageNum=10
+
+###
+PUT http://localhost:8080/product/1227
+Content-Type: application/json
+
+{
+    "Title": "post title",
+    "ProductTypeId": 141,
+    "ArticleNumber": "1234",
+    "MinCostForAgent": 100.50
+}
+
+### 
+DELETE http://localhost:8080/product/232

+ 97 - 46
articles/api_asp_net_core.md

@@ -1,5 +1,9 @@
 # API. REST API. Создание сервера ASP.NET Core.
 
+* [API. REST API.](#api-rest-api)
+* [Swagger](#open-api-swagger)
+* [Создание сервера ASP.NET Core.](#создание-сервера-aspnet-core)
+
 ## API. REST API.
 
 Взято [отсюда](https://cloud.yandex.ru/docs/glossary/rest-api)
@@ -131,6 +135,17 @@ REST API — самый популярный сегодня стандарт в
 
 Первым широко распространенным стандартом стал **SOAP** (Simple Object Access Protocol). Но SOAP-сообщения довольно громоздки (как минимум потому, что используют формат XML, а не более лаконичный JSON), что стало особенно неудобно с распространением мобильного интернета. SOAP изначально предназначался для описания вызова удаленных процедур — RPC (Remote Procedure Call), когда клиентское приложение выполняет функцию или процедуру на сервере, а сервер отправляет результат обратно клиенту.
 
+## Open API (Swagger)
+
+**Swagger** - это фреймворк для спецификации *RESTful API*. Его прелесть заключается в том, что он дает возможность не только интерактивно просматривать спецификацию, но и отправлять запросы.
+
+**ASP.NET** поддерживает генерацию спецификации и конечной точки для использования этой спецификации.
+
+Для поддержки Swagger нужно установить NuGet пакеты:
+
+* Microsoft.OpenApi
+* Swashbuckle.AspNetCore
+
 ## Создание сервера ASP.NET Core.
 
 ### Маршрутизация в ASP.NET
@@ -152,12 +167,23 @@ REST API — самый популярный сегодня стандарт в
 
 #### Основы маршрутизации
 
-В следующем коде приведен базовый пример маршрутизации.
+В следующем коде приведен базовый пример маршрутизации. Заодно в комментариях расписана структура.
 
 ```cs
 var builder = WebApplication.CreateBuilder(args);
+
+/* 
+сюда дописываются дополнительные сервисы, всё что дальше начинается с "builder.Services"
+*/
+
 var app = builder.Build();
 
+/* 
+тут запускаются так называемые "middleware": swagger, авторизация и т.п. процедуры, которые должны быть выполнены до обработки конечных точек
+
+они начинаются с "app.Use"
+*/
+
 app.MapGet("/", () => "Hello World!");
 
 app.Run();
@@ -167,7 +193,7 @@ app.Run();
 
 * При отправке HTTP-запроса **GET** в корневой URL-адрес `/`:
     * Выполняется делегат запроса.
-    * В ответ HTTP записывается Hello World!.
+    * В ответ HTTP записывается "Hello World!".
 * Если метод запроса не является **GET** или если корневой URL-адрес не `/`, сопоставление маршрута не выполняется и возвращается сообщение об ошибке HTTP 404.
 
 Маршрутизация использует пару ПО промежуточного слоя: UseRouting и UseEndpoints.
@@ -175,7 +201,7 @@ app.Run();
 * **UseRouting** добавляет соответствие маршрута в конвейер ПО промежуточного слоя. Это ПО промежуточного слоя обращается к набору конечных точек, определенных в приложении, и выбирает наиболее подходящее на основе запроса.
 * **UseEndpoints** добавляет выполнение конечной точки в конвейер ПО промежуточного слоя. Он запускает делегат, связанный с выбранной конечной точкой.
 
-Приложениям обычно не требуется вызывать **UseRouting** или **UseEndpoints**. WebApplicationBuilder настраивает конвейер ПО промежуточного слоя, который создает программу-оболочку для ПО промежуточного слоя, добавленное в `Program.cs` с использованием **UseRouting** и **UseEndpoints**. Но приложения могут изменять порядок, в котором выполняются **UseRouting** и **UseEndpoints**, вызывая эти методы явным образом. Например, следующий код явным образом вызывает **UseRouting**:
+Приложениям обычно не требуется вызывать **UseRouting** или **UseEndpoints**. **WebApplicationBuilder** настраивает конвейер ПО промежуточного слоя, который создает программу-оболочку для ПО промежуточного слоя, добавленное в `Program.cs` с использованием **UseRouting** и **UseEndpoints**. Но приложения могут изменять порядок, в котором выполняются **UseRouting** и **UseEndpoints**, вызывая эти методы явным образом. Например, следующий код явным образом вызывает **UseRouting**:
 
 ```cs
 app.Use(async (context, next) =>
@@ -269,7 +295,7 @@ users/{id:int:min(1)}
 
 #### Параметры пути и поиска (querystring)
 
-ASP.NET достаточно умный, чтобы вытащить из запроса все параметры, независимо от того где в URL они расположены. Параметры из **пути**, **строки запроса** и **тела запроса** в итоге попадают в коллекцию параметров делегата:
+**ASP.NET** достаточно умный, чтобы вытащить из запроса все параметры, независимо от того где в URL они расположены. Параметры из **пути**, **строки запроса** и **тела запроса** в итоге попадают в коллекцию параметров делегата:
 
 Например, есть такой запрос:
 
@@ -352,8 +378,27 @@ param1 = 1, param2 = 2, pageNum = 10, pageLen =
 
 1. Подключение БД
 
+    <!-- TODO прикрутить в контекст? -->
+
     Используйте тот же подход, что и для обычного приложения: [Создание подключения к БД MySQL.](./cs_mysql_connection3.md)
 
+1. Сразу настроим **Swagger**:
+
+    ```cs
+    builder.Services.AddEndpointsApiExplorer();
+    builder.Services.AddSwaggerGen();
+
+    ...
+
+    app.UseSwagger();
+    app.UseSwaggerUI(c =>
+    {
+        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
+    });
+    ```
+
+    [Выше](#основы-маршрутизации) было расписано где должны располагаться эти команды.
+
 1. Добавление *конечной точки* для получения списка продукции (**C****_Read_****UD**):
 
     Добавьте ещё один **MapGet** с конечной точной `/product`: 
@@ -365,10 +410,24 @@ param1 = 1, param2 = 2, pageNum = 10, pageLen =
         {
             return context.Products.ToList();
         }
-    });
+    })
+    .WithTags("CRUD продукции")
+    .Produces<Product>(StatusCodes.Status200OK);
     ```
 
-    т.е. при запросе `GET http://хост:порт/product` мы получим список продукции
+    Метод **WithTags** используется для задания тега в Swagger (группировка запросов по какому-то признаку)
+
+    Метод **Produces** тоже для **Swagger**. Он описывает какого типа будет результат и с каким кодом ответа (таких методов может быть несколько в цепочке).
+
+    Можно запустить проект и перейти по ссылке `http://хост:порт/swagger/`. Должно получиться примерно такое (у меня уже чуть больше реализовано)
+
+    ![](../img/asp003.png)
+
+    Т.е. есть описание конечной точки `GET /product`, возможных параметров запроса и возможных ответов. Причём можно нажать кнопку **"Try it out"**, заполнить, при необходимости параметры запроса, затем нажать кнопку **"Execute"** и получим ответ сервера!
+
+    ![](../img/asp004.png)
+
+    Получим список продукции
 
     ```json
     [
@@ -399,6 +458,7 @@ param1 = 1, param2 = 2, pageNum = 10, pageLen =
     Можно настроить конвертер, чтобы он игнорировал циклические ссылки
 
     ```cs
+    // это в код добавлять не нужно, оставил на всякий случай
     builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
     {
         options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
@@ -470,7 +530,7 @@ param1 = 1, param2 = 2, pageNum = 10, pageLen =
             .ToList();
     ```
 
-    Т.е. у нас для каждого экземпляра модели (`p`) создаётся новый экземпляр объекта (`new {...}`) в инициализаторе которого мы и прописываем поля, которые хотим видеть на выходе. При этом значение для `p.ProductType.TitleType` подтягивается динамически
+    Т.е. у нас для каждого экземпляра модели (`p` в делегате) создаётся новый экземпляр объекта (`new {...}`) в инициализаторе которого мы и прописываем поля, которые хотим видеть на выходе. При этом значение для `p.ProductType.TitleType` подтягивается динамически
 
     ```json
     [
@@ -519,7 +579,9 @@ param1 = 1, param2 = 2, pageNum = 10, pageLen =
                 .Take(pageLen ?? 20)
                 .ToList();
         }
-    });
+    })
+    .WithTags("CRUD продукции")
+    .Produces<Product>(StatusCodes.Status200OK);
     ```
 
 1. Добавление новой продукции (**_Create_****RUD**)
@@ -536,48 +598,32 @@ param1 = 1, param2 = 2, pageNum = 10, pageLen =
             context.Add(postProduct);
             context.SaveChanges();
         }
-    });
+    })
+    .WithTags("CRUD продукции")
+    .Produces(StatusCodes.Status401Unauthorized)
+    .Produces(StatusCodes.Status200OK);
     ```
 
+    Для **Swagger** я дописал `Produces(StatusCodes.Status401Unauthorized)`. Авторизацию мы прикрутим в следующей лекции, но сразу заложим возможные коды ответов.
+
     Здесь у нас в параметрах делегата объект **Product**, который автоматически получен из тела запроса (ни в пути, ни в строке поиска параметров нет).
 
-    Пример запроса в формате плагина **REST Client** для **VSCode**:
+    Попробуйте выполнить этот запрос в **Swagger** (id типа продукции возьмите из своей базы):
 
-    ```
-    ### добавление продукта
-    POST http://localhost:8080/product
-    Content-Type: application/json
+    ![](../img/asp005.png)
 
-    {
-        "Title": "post title",
-        "ProductTypeId": 141,
-        "ArticleNumber": "1234",
-        "MinCostForAgent": 100.50
-    }
-    ```
+    - тело запроса в формате **JSON**, причём названия полей должны соответствовать **модели** *Product* (но не надо заполнять весь тот треш, который покажет **Swagger**, достаточно только тех полей, которые нужны для **таблицы** *Product*)
+    - должны быть указаны все обязательные поля (кроме `Id` - он автоматически создастся при добавлении), остальные желательно, но не обязательно.
 
-    - метод запроса: `POST`
-    - путь: `/product`
-    - в заголовке нужно обязательно указать тип содержимого: `Content-Type: application/json`
-    - тело запроса в формате **JSON**, причём названия полей должны соответствовать модели **Product**
-    - должны быть указаны все обязательные поля (кроме `Id` - он автоматически создастся при добавлении), остальные желательно, но не обязательно
+    >Чтобы не было лишних полей, можно описать отдельный класс, например **ShortProduct**, в котором прописать только те поля, которые есть в таблице
 
-1. Изменение существующей продукции (**СR****_Update_****D**)
+    ![](../img/asp006.png)
 
-    **Id** изменяемой продукции будем передавать в пути запроса (это не обязательно, но как пример)
+    Если всё нормально, то код ответа будет 200, а содержимого мы никакого не возвращаем (если нужно, то тут можно вернуть вновь созданный объект, чтобы сразу знать его `id`). 
 
-    ```
-    ### изменение продукта
-    PUT http://localhost:8080/product/227
-    Content-Type: application/json
+1. Изменение существующей продукции (**СR****_Update_****D**)
 
-    {
-        "Title": "post title",
-        "ProductTypeId": 141,
-        "ArticleNumber": "1234",
-        "MinCostForAgent": 100.50
-    }
-    ```
+    **Id** изменяемой продукции будем передавать в пути запроса (это не обязательно, но как пример)
 
     * используем метод `PUT`
     * в пути передаём параметр id продукции `/product/227`
@@ -611,18 +657,19 @@ param1 = 1, param2 = 2, pageNum = 10, pageLen =
         // без этой строки ругается, видимо если мы задействовали 
         // метод Results, то обязаны его использовать при любом результате
         return Results.Ok();
-    });
+    })
+    .WithTags("CRUD продукции")
+    .Produces(StatusCodes.Status200OK)
+    .Produces(StatusCodes.Status401Unauthorized)
+    .Produces(StatusCodes.Status404NotFound);
     ```
 
+    В **Swagger** протестируйте сами, тут уже ничего нового нет.
+
 1. Удаление существующей продукции (**СRU****_Delete_**)
 
     Тут, в принципе, должно быть уже понятно (метод `DELETE` с указанием `id` продукции в пути):
 
-    ```
-    ### удаление продукта
-    DELETE http://localhost:8080/product/232
-    ```
-
     Реализация **конечной точки**:
 
     ```cs
@@ -643,7 +690,11 @@ param1 = 1, param2 = 2, pageNum = 10, pageLen =
         }
 
         return Results.Ok();
-    });
+    })
+    .WithTags("CRUD продукции")
+    .Produces(StatusCodes.Status200OK)
+    .Produces(StatusCodes.Status401Unauthorized)
+    .Produces(StatusCodes.Status404NotFound);
     ```
 <!-- 
 авторизация

+ 8 - 4
articles/cs_product_material.md

@@ -20,7 +20,7 @@
 
 ## Read (вывод списка)
 
-Получаем из базы список материалов *редактируемого продукта* и выводим этот список используя **ListView**
+Получаем из базы список материалов *редактируемого продукта* и выводим этот список используя **ListBox**
 
 1. Получение списка материалов редактируемого продукта. 
 
@@ -28,13 +28,14 @@
     
     Материалы продукта (в принципе это очевидно по названию) хранятся в таблице **ProductMaterial**. Это, так называемая, **таблица связей**  - реализация отношения *многие (продукты) - ко - многим (материалам)*. При чтении надо учитывать, что нам нужны материалы только редактируемого продукта, т.е. указать фильтр по редактируемому продукту при выборке: 
 
+    >Далее старый код от WPF, адаптируйте сами, отличий не много
+
     ```cs
     public IEnumerable<ProductMaterial> ProductMaterialList { get; set; }
 
     public EditProductWindow(Product EditProduct)
     {
         InitializeComponent();
-        DataContext = this;
         CurrentProduct = EditProduct;
         using (var context = new dbContext())
         {
@@ -63,9 +64,9 @@
     }
     ```
 
-2. Вывод списка материалов продукта
+1. Вывод списка материалов продукта
 
-    Тут обычный **ListView**, но в качестве одного из элементов мы нарисуем кнопку "Удалить", чтобы удалять элемент списка можно было не заходя в окно редактирования
+    Тут обычный **ListBox**, но в качестве одного из элементов мы нарисуем кнопку "Удалить", чтобы удалять элемент списка можно было не заходя в окно редактирования
 
     ```xml
     <ListView
@@ -135,6 +136,7 @@ private void DeleteMaterialTextBlock_MouseDown(object sender, MouseButtonEventAr
         // при поиске удаляемого материала нужно учитывать, 
         // что у него составной первичный ключ и указывать поля ключа 
         // в том же порядке, в котором они перечислены в первичном ключе
+
         var deletedProductMaterial = context.ProductMaterials
             .Find(productMaterial.ProductId, productMaterial.MaterialId);
 
@@ -142,6 +144,8 @@ private void DeleteMaterialTextBlock_MouseDown(object sender, MouseButtonEventAr
         
         if (context.SaveChanges() > 0)
             LoadProductMaterials();
+
+        // Вместо этого кода можно использовать RAW SQL запрос удаления учитывая, что искать надо по двум ключам
     }
 }
 ```

BIN
img/asp003.png


BIN
img/asp004.png


BIN
img/asp005.png


BIN
img/asp006.png


+ 1 - 1
readme.md

@@ -309,7 +309,7 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 ## Тема 5.1.5. Разработка своего API.
 
-1. [API. REST API. Создание сервера ASP.NET Core.](./articles/api_asp_net_core.md)
+1. [API. REST API. Создание сервера ASP.NET Core. Swagger.](./articles/api_asp_net_core.md)
 
 API. Авторизация и аутентификация. Методы авторизации. Basic-авторизация.