Browse Source

ApiDataProvider

Евгений Колесников 1 year ago
parent
commit
fd28a12256
2 changed files with 50 additions and 236 deletions
  1. 49 235
      articles/cs_http.md
  2. 1 1
      readme.md

+ 49 - 235
articles/cs_http.md

@@ -2,29 +2,21 @@
 :----------------:|:----------:|:----------------:
 [Аутентификация и авторизация](./api_auth.md) | [Содержание](../readme.md#разработка-своего-api) | 
 
-# HTTP запросы в C#. Получение списка материалов выбранного продукта. Авторизация.
+# HTTP запросы в C#.
 
 * [HTTP-запросы (класс **HttpClient**)](#http-запросы)
     * [GET-запрос](#get-запрос)
     * [Разбор JSON ответа.](#разбор-json-ответа)
-    * [DELETE (Удаление записей)](#delete-удаление-записей)
     * [POST запросы с JSON (Добавление записей в модель в терминологии REST API)](#post-запросы-с-json-добавление-записей-в-модель-в-терминологии-rest-api)
-* [Авторизация](#авторизация)
-    * [Реализация конечной точки в АПИ](#реализация-конечной-точки-в-апи)
-    * [Авторизация в клиентском приложении](#авторизация-в-клиентском-приложении)
 
-Возвращаемся к проекту на C#.
+Возвращаемся к основному проекту (список продукции).
 
-Мы остановились на списке материалов.
-
-Реализуем получение списка материалов выбранного продукта с помощью HTTP-запроса.
-
->Напоминаю, в лекции про АПИ было задание реализовать конечную точку `GET /material/{productId:int}`:
+Реализуем получение данных не через базу, а через АПИ с помощью HTTP-запросов.
 
 На C# нам надо решить две задачи:
 
-* получить JSON-строку с помощью GET-запроса
-* распарсить JSON-строку, получив массив материалов
+* получить JSON-строку с помощью HTTP-запроса
+* распарсить JSON-строку
 
 ## HTTP-запросы
 
@@ -57,264 +49,86 @@ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basi
 
 >[Асинхронность в C#](./cs_async_await.md)
 
-Я для облегчения вашей работы нарисовал реализацию:
-
-```cs
-// в параметрах URL
-private Task<string> GetBody(string url){
-    var basic = Convert.ToBase64String(
-        ASCIIEncoding.ASCII.GetBytes("esmirnov:111103"));
-        
-    var client = new HttpClient();
-
-    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basic);
+Но можно не заморачиваться и использовать свойство _Result_, которое вернет **синхронный** результат.
 
-    return client.GetStringAsync(url);
-}
-```
+Напишем класс **ApiDataPrivider**:
 
-Метод *GetStringAsync* возвращает объект `Task<string>`, т.е. задачу, которая выполняется в отдельном потоке и по завершении вернёт строку (тело ответа)
+>Можно как обычно реализовать его "с нуля", реализуя интерфейс **IDataProvider** (в этом случае придется сразу писать все методы), но можно реализовать механизм наследования: сделать потомка класса **DBDataProvider**, постепенно переопределяя методы.
 
-### Разбор JSON ответа.
+Ниже показана реализация одного метода класса, остальные перепишите сами. И не забудьте в конструкторе главного окна поменять инициализацию _dataProvider_: `Global.dataProvider = new ApiDataProvider();`
 
-Для работы с JSON нужно установить **NuGet** пакет `Newtonsoft.Json` и использовать метод десериализации объекта:
+>В базовом классе (**DBDataProvider**) нужно добавить методам модификатор **virtual**
 
 ```cs
-// в месте, где нам нужно распарсить ответ сервера
-materialList = JsonConvert.DeserializeObject<Material[]>("тут ответ вашего АПИ");
-```
-
-В общем случае может быть десериализован любой валидный JSON, но мы явно в угловых скобках указываем, что ожидаем массив материалов.
-
-Весь метод получения списка материалов помещается в 3 строки:
-
-```cs
-private async void GetMaterials()
+public class ApiDataProvider: DBDataProvider
 {
-    var resp = await GetBody(
-        $"http://localhost:8080/material/{currentProduct.Id}");
+    private static string baseUrl = "http://localhost:5000";
+    public override IEnumerable<Product> getProduct(int pageNum)
+    {
+        var client = new HttpClient();
 
-    materialList = JsonConvert
-        .DeserializeObject<Material[]>(resp);
+        var body = client.GetStringAsync(
+            $"{baseUrl}/product?pageNum={pageNum}"
+        ).Result;
 
-    Invalidate("materialList");
+        return JsonConvert.DeserializeObject<Product[]>(body);
+    }
 }
 ```
 
-Так как метод *GetBody* возвращает задачу (**Task**), то мы ставим перед ним ключевое слово **await**, то есть ждём завершения задачи, а сам метод помечаем как асинхронный (**async**), чтобы система знала, что этот метод асинхронный.
-
-### DELETE (Удаление записей)
-
-Для вызова http-метода `DELETE` используется метод *DeleteAsync*:
+Если вы используете авторизацию, то при запросе данных добавляйте заголовок:
 
 ```cs
-// тут, как в GetBody, создайте клиента и задайте заголовок
-
-client.DeleteAsync(
-    $"http://localhost:8080/material/{productId}/{materialId}");
-```
-
-При работе с таблицей **ProductMaterial** не забываем, что у нас составной первичный ключ и нужны идентификаторы и продукта и материала.
-
-### POST запросы с JSON (Добавление записей в модель в терминологии REST API)
-
-1. В проекте нарисуйте форму добавления материала в текущий продукт (в окне продукции)
-
-1. Реализуйте отправку POST-запроса, который добавит новый материал в таблицу **ProductMaterial**
-
-    ```cs
-    // при добавлении материала 
-    // у вас должен быть выбран материал 
-    // и указано его количество
-
-    // сначала запихиваем объект в JSON-строку. 
-    var jsonString = JsonConvert.SerializeObject(
-        new {
-            MaterialId = Id выбранного материала,
-            Count = количество
-        });
-
-    // создаём контент для http-запроса
-    var json = new StringContent(
-        jsonString, 
-        Encoding.UTF8, 
-        "application/json");
-
-    // и вызываем POST-запрос
-    var client = new HttpClient();
-
-    // не забываем добавить авторизацию
-    client.DefaultRequestHeaders.Authorization = 
-        new AuthenticationHeaderValue("Basic", basic);
-
-    var result = client.PostAsync(
-        $"http://localhost:8080/material/{currentProduct.Id}", 
-        json);
-    ```
-
-## Авторизация
-
-Часто в встречается задача сделать авторизацию пользователя и скрыть часть информации для неавторизованного пользователя.
-
-Скрытие сделать просто - сделайте привязку (binding) атрибута _IsVisible_ к какому-нибудь свойству (мы это уже делали для кнопки массовой смены цены продукции).
-
-Показать форму с полями ввода для `логина/пароля` вы тоже уже можете.
-
-Осталось разобраться как отправить запрос авторизации на сервер и обработать его на сервере...
-
-Если отправлять обычным POST-запросом с телом запроса в JSON-формате, то вы это тоже уже умеете. Но для авторизации часто используется старый формат запроса: `application/x-www-form-urlencoded`.
-
-Запрос с таким форматом выглядит примерно так:
-
-```
-POST /login
-Content-Type: application/x-www-form-urlencoded
-
-login=qwerty
-&password=asdf
-```
-
-т.е. параметры запроса формируются как _querystring_, но передаются в теле запроса.
-
-### Реализация конечной точки в АПИ
+var basic = Convert.ToBase64String(
+    ASCIIEncoding.ASCII.GetBytes("esmirnov:123456"));
+    
+var client = new HttpClient();
 
-```cs
-app.MapPost(
-    "/login", 
-    async (context) =>
-    {
-        /*
-            параметры в таком формате автоматически не парсятся
-            приходится их вручную доставать из тела запроса
-        */
-        var formData = await context.Request.ReadFormAsync();
-
-        var login = formData["login"].ToString();
-        var password = formData["password"].ToString();
-
-        /*
-            "Успешность" авторизации определяем кодом ответа
-            у меня, как обычно, логин и пароль прибиты гвоздями
-            но в реальном приложении надо читать из базы
-        */
-        if (login == "admin" && password == "password") 
-        {
-            /*
-                Тут можно передать не только код, 
-                но и информацию о пользователе
-            */
-            context.Response.StatusCode = 200;
-        }
-        else
-            context.Response.StatusCode = 401;
-    }).AllowAnonymous();
+client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basic);
 ```
 
-### Авторизация в клиентском приложении
+Метод *GetStringAsync* возвращает объект `Task<string>`, т.е. **задачу**, которая выполняется в отдельном потоке и по завершении вернёт строку (тело ответа). Чтобы не возиться с асинхронностью, можно запросить свойство _Result_.
 
->Кнопку "Авторизоваться" и окно авторизации сделайте сами
+### Разбор JSON ответа.
 
-Для хранения информации об авторизации заведем класс **Globals** со статическими свойствами:
+Для работы с JSON нужно установить **NuGet** пакет `Newtonsoft.Json` и использовать метод десериализации объекта:
 
 ```cs
-public class Globals
-{
-    public static string? token = null;
-
-    public static bool isAuth() => token != null;
-
-    public static void createToken(string login, string password)
-    {
-        token = Convert.ToBase64String(
-            ASCIIEncoding.ASCII.GetBytes($"{login}:{password}"));
-    }
-}
+return JsonConvert.DeserializeObject<Product[]>(body);
 ```
 
-У нас остаётся базовая авторизация и я сразу формирую токен в нужном формате. Если используется авторизация по токену (**Bearer**), то метод _createToken_ не нужен, просто сохранить токен: `Globals.token = "токен, полученный при авторизации"`.
+В общем случае может быть десериализован любой валидный JSON, но мы явно в угловых скобках указываем, что ожидаем массив продуктов.
 
-И в окне авторизации реализуем отправку запроса:
+### POST запросы с JSON (Добавление записей в модель в терминологии REST API)
 
 ```cs
-private async void AuthButton_OnClick(object? sender, RoutedEventArgs e)
-{
-    /* 
-        создаем словарь
-        и записываем в него данные в виде пары ключ/значение
-        для кадого передаваемого параметра
-    */
-    var param = new Dictionary<string, string>
-    {
-        { "login", LoginTextBox.Text },
-        { "password", PasswordTextBox.Text }
-    };
-    var client = new HttpClient();
-
-    /*
-        В инициализатор запроса добавляется содержимое
-    */
-    var req = new HttpRequestMessage(
-        HttpMethod.Post, 
-        "http://localhost:8080/login")
-    {
-        Content = new FormUrlEncodedContent(param)
-    };
-
-    var response = await client.SendAsync(req);
-
-    /*
-        При успешной авторизации сохраняем токен
-    */
-    if (response.StatusCode == HttpStatusCode.OK)
-    {
-        Globals.createToken(
-            LoginTextBox.Text, 
-            PasswordTextBox.Text);
-
-        /*
-            тут закрываем окно авторизации,
-            а в главном окне обновляем состояние 
-            визуальных элементов доступных только
-            авторизованному пользователю
-        */
-    }
-    /*
-        в else можно написать логику для неуспешной авторизации,
-        например, показать окно
-    */
-
-}
-```
-
->В коде используются свойства *LoginTextBox* и *PasswordTextBox* - это имена соответствующих визуальных элементов в окне авторизации.
+// сначала запихиваем объект в JSON-строку. 
+var jsonString = JsonConvert.SerializeObject(product)
 
-Ну и во всех запросах, требующих авторизации, вставляем токен авторизации:
+// создаём контент для http-запроса
+var json = new StringContent(
+    jsonString, 
+    Encoding.UTF8, 
+    "application/json"
+);
 
-```cs
-private Task<string> GetBody(string url)
-{
-    var client = new HttpClient();
+// и вызываем POST-запрос
+var client = new HttpClient();
 
-    client.DefaultRequestHeaders.Authorization = 
-        new AuthenticationHeaderValue(
-            "Basic", 
-            Globals.token   // берём готовый
-        );
+// не забываем добавить авторизацию
+client.DefaultRequestHeaders.Authorization = 
+    new AuthenticationHeaderValue("Basic", basic);
 
-    return client.GetStringAsync(url);
-}
+var result = client.PostAsync(
+    $"{baseUrl}/product", 
+    json).Result;
 ```
 
 ---
 
 **Задание:**
 
-* реализовать авторизацию:
-    - доработка АПИ
-    - окно с вводом логина/пароля и авторизация
-    - скрытие кнопок "создать продукт" и "сменить цену"
-    - не открывать окно редактирование продукции, если пользователь не авторизован
-* реализовать в окне редактирования продукции CRUD для списка материалов продукта, используя HTTP-запросы
+Полностью реализовать класс **ApiDataProvider**
 
 Предыдущая лекция |  | Следующая лекция
 :----------------:|:----------:|:----------------:

+ 1 - 1
readme.md

@@ -312,7 +312,7 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 1. [Авторизация и аутентификация. Методы авторизации. Basic-авторизация.](./articles/api_auth.md)
 
-1. [HTTP запросы в C#. Получение списка материалов выбранного продукта. Авторизация.](./articles/cs_http.md)
+1. [HTTP запросы в C#.](./articles/cs_http.md)
 
 **Лабораторные работы**