浏览代码

поправил переходы

Евгений Колесников 2 年之前
父节点
当前提交
f197430b8b

+ 461 - 7
articles/api_asp_net_core.md

@@ -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
+-->

+ 9 - 7
articles/cs_coloring2.md

@@ -6,6 +6,10 @@
 <a href="../articles/cs_edit_product2.md">Создание, изменение продукции
 </a></td><tr></table>
 
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Пагинация, сортировка, фильтрация, поиск](./cs_pagination2.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Создание, изменение продукции](./cs_edit_product2.md)
+
 # Подсветка элементов по условию. Массовая смена цены продукции.
 
 * [Подсветка элементов по условию](#раскраска-по-условию)
@@ -324,10 +328,8 @@ public string costChangeButtonVisible {
     }
     ```
 
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/cs_pagination2.md">Пагинация, сортировка, фильтрация, поиск
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/cs_edit_product2.md">Создание, изменение продукции
-</a></td><tr></table>
+---
+
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Пагинация, сортировка, фильтрация, поиск](./cs_pagination2.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Создание, изменение продукции](./cs_edit_product2.md)

+ 8 - 14
articles/cs_edit_product2.md

@@ -1,10 +1,6 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/cs_coloring2.md">Подсветка элементов по условию. Дополнительные выборки.Массовая смена цены продукции.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/kotlin.md">Основы языка Kotlin
-</a></td><tr></table>
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Подсветка элементов по условию. Дополнительные выборки.Массовая смена цены продукции.](./cs_pagination2.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Вывод списка материалов продукта. CRUD материалов продукта](./cs_product_material.md)
 
 # Добавление/редактирование продукции
 
@@ -400,10 +396,8 @@ private void DeleteProductButton_Click(object sender, RoutedEventArgs e)
 
 [Подробнее про RAW SQL](https://learn.microsoft.com/en-us/ef/core/querying/sql-queries)
 
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/cs_coloring2.md">Подсветка элементов по условию. Дополнительные выборки.Массовая смена цены продукции.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/kotlin.md">Основы языка Kotlin
-</a></td><tr></table>
+---
+
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Подсветка элементов по условию. Дополнительные выборки.Массовая смена цены продукции.](./cs_pagination2.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Вывод списка материалов продукта. CRUD материалов продукта](./cs_product_material.md)

+ 11 - 127
articles/cs_http.md

@@ -43,7 +43,7 @@ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basi
 
 Теперь можно запрашивать данные. 
 
-У **HttpClient** все методы асинхронные, нужно понимать как работают async/await. Я для облегчения вашей работы нарисовал синхронную реализацию:
+У **HttpClient** все методы асинхронные, нужно понимать как работают `async/await`. Я для облегчения вашей работы нарисовал синхронную реализацию:
 
 ```cs
 // в параметрах URL
@@ -122,46 +122,6 @@ public IEnumerable<MaterialTC> GetMaterials(int ProductId) {
 }
 ```
 
-### Вариант с регулярками
-
->Этот вариант не рекомендую, т.к. JSON-строка может быть в юникодной кодировке
-
-Стандартный вариант слишком монстрообразный, на мой взгляд. Можно тоже самое реализовать через регулярки:
-
-Класса нам достаточно одного:
-
-```cs
-internal class MaterialTC
-{
-    public string Title { get; set; }
-    public int Count { get; set; }
-}
-```
-
-Реализация метода **GetMaterials**:
-
-```cs
-public IEnumerable<MaterialTC> GetMaterials(int ProductId) {
-    var result = new List<MaterialTC>();
-
-    var resp = GetString($"http://localhost:8080/Material?product_id={ProductId}");
-
-    Regex regex = new Regex(@"\{""Title"":""(.*?)"",""Count"":(.*?)\}", RegexOptions.Singleline);
-    MatchCollection matches = regex.Matches(resp);
-    if (matches.Count > 0)
-    {
-        foreach (Match match in matches)
-            res.Add(new MaterialTC { 
-                Title = match.Groups[1].ToString(), 
-                Count=Convert.ToInt32(match.Groups[2].ToString()) 
-            });
-    }
-    return result;
-}
-```
-
-Надо, конечно, ещё проверить валидность ответа (notice->data|notice->answer), но с регулярками это тоже проще - вообще не нужно рисовать новый класс.
-
 ### Вариант с JavaScriptSerializer
 
 Оказывается на **WorldSkills** можно использовать не только "голый" **.NET Framework**, но и библиотеки из других компонентов **Visual Studio**.
@@ -208,63 +168,23 @@ DELETE {{url}}/Product?id=131
 Authorization: Basic esmirnov 111103
 ```
 
-1. Сначала надо доработать наш PHP-класс:
-
-    В конструкторе добавляем обработку метода DELETE:
-
-    ```php
-    switch($_SERVER['REQUEST_METHOD'])
-    {
-        case 'DELETE':
-            $this->processDelete($_SERVER['PATH_INFO']);
-            break;
-        ...
-    ```
-
-    При реализации метода идентификатор удаляемой записи достаём из глобальной переменной **$_GET**
-
-    ```php
-    private function processDelete($path)
-    {
-        switch($path)
-        {
-            case '/Product':
-                $this->auth();
-
-                // print_r($_GET);
-                $id = $_GET['id'] ?: 0;
-
-                // методы, которые не подразумевают ответа, выполняются командой execute
-                if($id)
-                    $this->db->query("DELETE FROM Product WHERE id=$id")
-                        ->execute();
-                $this->response['status'] = 0;
-                break;
-            default:
-                header("HTTP/1.1 404 Not Found");
-        }
-    }
-    ```
-
-2. Теперь в C# осталось реализовать метод удаления выбранной записи
+Строку запроса, надеюсь, сформируете сами.
 
-    Строку запроса, надеюсь, сформируете сами.
+Для вызова http-метода DELETE используется метод DeleteAsync:
 
-    Для вызова http-метода DELETE используется метод DeleteAsync:
-
-    ```cs
-    var basic = Convert.ToBase64String(
-                ASCIIEncoding.ASCII.GetBytes("esmirnov:111103"));
-    var client = new HttpClient();
-    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basic);
-    client.DeleteAsync($"http://localhost:8080/Product?id={id}").Result;
-    ```
+```cs
+var basic = Convert.ToBase64String(
+            ASCIIEncoding.ASCII.GetBytes("esmirnov:111103"));
+var client = new HttpClient();
+client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basic);
+client.DeleteAsync($"http://localhost:8080/Product?id={id}").Result;
+```
 
 ## POST запросы с JSON (Добавление записей в модель в терминологии REST API)
 
 1. В проекте C# нарисуйте форму добавления продажи (в окне продукции)
 
-2. В **DataProvider** добавьте метод *AddProductSale*, который на входе получает экземпляр класса **ProductSale** и реализуйте отправку POST-запроса в локальный PHP-сервер, который добавит эту продажу в соответствующую таблицу
+1. В **DataProvider** добавьте метод *AddProductSale*, который на входе получает экземпляр класса **ProductSale** и реализуйте отправку POST-запроса в локальный PHP-сервер, который добавит эту продажу в соответствующую таблицу
 
     ```cs
     // сначала запихиваем объект в JSON-строку. 
@@ -281,39 +201,3 @@ Authorization: Basic esmirnov 111103
 
     // Console.WriteLine(result.Content.ReadAsStringAsync().Result);
     ```
-
-3. Обработка POST-запроса с типом *application/json*
-
-    В PHP есть глобальная переменная **$_POST** в которую автоматически парсятся данные POST запроса, но только если тип запроса *application/x-www-form-urlencoded*.
-
-    Тип JSON появился сравнительно недавно, поэтому PHP автоматически его не парсит. Приходится писать разбор вручную
-
-    ```php
-    private function processPost($path)
-    {
-        // входной поток данных (содержимое запроса, content) считывается в строку
-        $rawData = file_get_contents('php://input');
-        // и преобразуется в объект 
-        $json = json_decode($rawData);
-        switch($path)
-        {
-            case '/ProductSale':
-                $this->auth();
-
-                // тут пишем запрос вставки данных в таблицу 
-                // к полям объекта JSON можно обращаться так: $json->Title, ...
-
-                $query = $this->db->prepare("INSERT INTO ProductSale (SomeName) VALUES (:SomeName)");
-                $query->execute([':SomeName'=>$json->Title]);
-
-                $this->response['status'] = 0;
-
-                break;
-
-            default:
-                header("HTTP/1.1 404 Not Found");
-        }
-    }
-    ```
-
-    При формировании SQL-запроса нужно экранировать значения, PDO может это сделать за нас. Для этого сначала нужно "подготовить" запрос, выполнив метод **prepare**, в котором вместо реальных значений пишутся алиасы (символьное имя с каким-либо префиксом). А затем, при выполнении подготовленного запроса передаётся ассоциативный массив "алиас"=>"значение".

+ 6 - 8
articles/cs_layout2.md

@@ -1,10 +1,6 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/cs_mysql_connection3.md">Создание подключения к БД MySQL. Получение данных с сервера.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/cs_pagination2.md">Пагинация, сортировка, фильтрация, поиск
-</a></td><tr></table>
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Создание подключения к БД MySQL. Получение данных с сервера.](./cs_mysql_connection3.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Пагинация, сортировка, фильтрация, поиск](./cs_pagination2.md)
 
 # Вывод данных согласно макету (ListBox, Image).
 
@@ -299,6 +295,8 @@ foreach (var item in ProductMaterials)
 
 ![](../img/rider015.png)
 
+---
+
 Предыдущая лекция |  | Следующая лекция
 :----------------:|:----------:|:----------------:
-[Создание подключения к БД MySQL. Получение данных с сервера.](../articles/cs_mysql_connection3.md) | [Содержание](../readme.md) | [Пагинация, сортировка, фильтрация, поиск](../articles/cs_pagination2.md)
+[Создание подключения к БД MySQL. Получение данных с сервера.](./cs_mysql_connection3.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Пагинация, сортировка, фильтрация, поиск](./cs_pagination2.md)

+ 10 - 8
articles/cs_mysql_connection3.md

@@ -1,12 +1,8 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/sql_import.md">Создание базы данных. Импорт данных.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/cs_layout2.md">Вывод данных согласно макету (ListView, Image).
-</a></td><tr></table>
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Хранимые процедуры. Триггеры.](./sql_trigger.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Вывод данных согласно макету (ListBox, Image). Вывод данных плиткой.](./cs_layout2.md)
 
-# Создание подключения к БД MySQL.
+# Создание подключения к БД MySQL. Получение данных с сервера.
 
 * [Установка Rider](#установка-rider)
 * [Создание проекта, подключение пакетов для работы с БД](#создание-проекта-подключение-пакетов-для-работы-с-бд)
@@ -296,3 +292,9 @@ public partial class MainWindow : Window
 **Всё работает!!!**
 
 ![](../img/rider011.png)
+
+---
+
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Хранимые процедуры. Триггеры.](./sql_trigger.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Вывод данных согласно макету (ListBox, Image). Вывод данных плиткой.](./cs_layout2.md)

+ 2 - 2
articles/cs_pagination2.md

@@ -1,6 +1,6 @@
 Предыдущая лекция |  | Следующая лекция
 :----------------:|:----------:|:----------------:
-[Вывод данных согласно макету (ListBox, Image)](../articles/cs_layout2.md) | [Содержание](../readme.md) | [Подсветка элементов по условию. Дополнительные выборки.](../articles/cs_coloring2.md)
+[Вывод данных согласно макету (ListBox, Image)](./cs_layout2.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Подсветка элементов по условию. Дополнительные выборки.](./cs_coloring2.md)
 
 # Продолжаем реализовывать макет
 
@@ -377,4 +377,4 @@ public IEnumerable<Product> productList {
 
 Предыдущая лекция |  | Следующая лекция
 :----------------:|:----------:|:----------------:
-[Вывод данных согласно макету (ListBox, Image)](../articles/cs_layout2.md) | [Содержание](../readme.md) | [Подсветка элементов по условию. Дополнительные выборки.](../articles/cs_coloring2.md)
+[Вывод данных согласно макету (ListBox, Image)](./cs_layout2.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [Подсветка элементов по условию. Дополнительные выборки.](./cs_coloring2.md)

+ 9 - 7
articles/cs_product_material.md

@@ -1,10 +1,6 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/cs_edit_product2">Добавление/редактирование продукции
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<!-- <a href="../articles/cs_layout2.md">Вывод данных согласно макету (ListView, Image).</a> -->
-</td><tr></table>
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Создание, изменение, удаление продукции](./cs_edit_product2.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [API. REST API. Создание сервера ASP.NET Core.](./api_asp_net_core.md)
 
 # Вывод списка материалов продукта. CRUD материалов продукта
 
@@ -153,3 +149,9 @@ private void DeleteMaterialTextBlock_MouseDown(object sender, MouseButtonEventAr
 Составной первичный ключ в таблице **ProductMaterial**
 
 ![](../img/cs009.png)
+
+---
+
+Предыдущая лекция |  | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Создание, изменение, удаление продукции](./cs_edit_product2.md) | [Содержание](../readme.md#тема-514-c-и-mysql) | [API. REST API. Создание сервера ASP.NET Core.](./api_asp_net_core.md)

+ 13 - 29
readme.md

@@ -297,13 +297,13 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 1. [Создание подключения к БД MySQL. Получение данных с сервера.](./articles/cs_mysql_connection3.md)
 
-1. [Вывод данных согласно макету (ListView, Image). Вывод данных плиткой.](./articles/cs_layout2.md)
+1. [Вывод данных согласно макету (ListBox, Image). Вывод данных плиткой.](./articles/cs_layout2.md)
 
 1. [Пагинация, сортировка, фильтрация, поиск](./articles/cs_pagination2.md)<!-- datepicker -->
 
 1. [Подсветка элементов по условию. Массовая смена цены продукции.](./articles/cs_coloring2.md)
 
-1. [Создание, изменение продукции](./articles/cs_edit_product2.md)
+1. [Создание, изменение, удаление продукции](./articles/cs_edit_product2.md)
 
 1. [Вывод списка материалов продукта. CRUD материалов продукта](./articles/cs_product_material.md)
 
@@ -311,12 +311,14 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 1. [API. REST API. Создание сервера ASP.NET Core.](./articles/api_asp_net_core.md)
 
+API. Авторизация и аутентификация. Методы авторизации. Basic-авторизация.
+
 1. [HTTP запросы в C#. Получение списка материалов выбранного продукта](./articles/cs_http.md)
 
-1. [C# Параллельное программирование и асинхронность](./articles/cs_async_await.md)
+<!-- 1. [C# Параллельное программирование и асинхронность](./articles/cs_async_await.md) -->
 
-1. [ASP NET Web API (Метанит)](https://metanit.com/sharp/aspnet_webapi/1.1.php)
-1. [ASP NET Core (Метанит)](https://metanit.com/sharp/aspnet6/2.11.php)
+<!-- 1. [ASP NET Web API (Метанит)](https://metanit.com/sharp/aspnet_webapi/1.1.php)
+1. [ASP NET Core (Метанит)](https://metanit.com/sharp/aspnet6/2.11.php) -->
 
 
 
@@ -330,11 +332,11 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 ## [Code Review](./articles/code_review.md)
 
-## Введение в WEB-разработку
+<!-- ## Введение в WEB-разработку -->
 
 <!-- codesandbox -->
 
-1. [Intro](./articles/web_01.md)
+<!-- 1. [Intro](./articles/web_01.md)
 1. [Зачем нужен Vue.js? - Vue.js: концепции](./articles/web_02.md)
 1. [Реактивность - Vue.js: концепции](./articles/web_03.md)
 1. [Двустороннее связывание - Vue.js: концепции](./articles/web_04.md)
@@ -357,7 +359,7 @@ https://office-menu.ru/uroki-sql Уроки SQL
 1. [Криптономикон: улучшаем API - Vue.js: практика. #22 Криптономикон: refs - Vue.js: практика. #23 nextTick - Vue.js: нюансы](./articles/web_21.md)
 1. [Криптономикон: компоненты - Vue.js: практика](./articles/web_22.md)
 1. [Введение в HTML](./articles/web_html_1.md)
-1. [Введение в CSS](./articles/web_css_1.md)
+1. [Введение в CSS](./articles/web_css_1.md) -->
 
 <!-- TODO накидать рыбу для tailwind с плиткой -->
 <!-- 1. Создать web-приложение на VUE, реализующее отображение списка товаров/услуг из базы данных вашего курсового проекта
@@ -491,25 +493,7 @@ tablayout
 # [Курсовой проект](articles/kp2.md)
 
 <!-- 
--- создание базы и пользователя для MSSQL
-DECLARE @userName AS VARCHAR(50) = 'test'
-DECLARE @password AS VARCHAR(50) = 'qzesc'
--- создаю базу
-DECLARE @createDB AS VARCHAR(50) = 'CREATE DATABASE {userName}'
-SET @createDB = REPLACE(@createDB, '{userName}', @userName)
-EXECUTE (@createDB)
--- создаю логин
-DECLARE @createLogin AS VARCHAR(150) = 'CREATE LOGIN {userName} WITH PASSWORD=''{password}'', CHECK_POLICY = OFF'
-SET @createLogin = REPLACE(@createLogin, '{userName}', @userName)
-SET @createLogin = REPLACE(@createLogin, '{password}', @password)
-EXECUTE (@createLogin)
--- создаю пользователя
-DECLARE @createUser AS VARCHAR(MAX) = '
-USE {userName}
-CREATE USER {userName} FOR LOGIN {userName};
-EXEC sp_addrolemember ''db_owner'', ''{userName}'';
-Grant Execute on Schema :: dbo TO [{userName}];
-'
-SET @createUser = REPLACE(@createUser, '{userName}', @userName)
-EXECUTE (@createUser)
+-- разрешение пользователю создавать базы 
+GRANT ALL PRIVILEGES ON `isergeev%`.* TO 'isergeev'@'%';
+FLUSH PRIVILEGES;
 -->