Консольная утилита погоды на C# с помощью .Net

    Что необходимо получить и изучить, чтобы начать получать прогноз погоды на 5 дней? 

    Во-первых, определиться с поставщиком погодных данных. Во-вторых, разобрать, в каком виде поставляются данные и как мы их можем собирать и отображать с помощью языка программирования C#. 

    В качестве поставщика погодных данных я выбрал сервис Accuweather. В бесплатной учётной записи на данный момент можно сделать 50 запросов в сутки. Этого достаточно, чтобы можно было посмотреть данные о погоде несколько раз в сутки (даже можно поделиться с другом!). 

    Для регистрации необходимо пройти по ссылке: https://developer.accuweather.com. После регистрации нужно нажать на кнопку "Add a new App" и заполнить небольшую анкету. По итогу, вы получите ваш личный ApiKey с помощью которого в дальнейшем можно получать обновлённые данные. 

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

    В разделе "API Reference" самым первым списке установлен раздел "Locations API" с него и начнём. Забегая вперёд, скажу сразу, нельзя так просто взять и отправить в GET запросе название города. Для этого, нам нужно сперва получить Location Key конкретного города. Это значение представлено в виде цифр и уникально для каждого города. 

    Итак, в разделе Locations API нас интересует метод City Search. Читаем краткое описание к нему: Returns information for an array of cities that match the search text. Сразу берём себе на заметку, что нам возвращается массив с названиями городов. 

    В странице для запроса вставляем ApiKey, название интересующего города и ставим RU, если хотим получать локализованные данные. 

    После нажатия на кнопку "Send this request" ниже на странице вы получите результат выполнения. В моём случае он выглядит так:

    [
      {
        "Version": 1,
        "Key": "292332",
        "Type": "City",
        "Rank": 21,
        "LocalizedName": "Chelyabinsk",
        "EnglishName": "Chelyabinsk",
        "PrimaryPostalCode": "",
        "Region": {
          "ID": "ASI",
          "LocalizedName": "Asia",
          "EnglishName": "Asia"
        },
        "Country": {
          "ID": "RU",
          "LocalizedName": "Russia",
          "EnglishName": "Russia"
        },
        "AdministrativeArea": {
          "ID": "CHE",
          "LocalizedName": "Chelyabinsk",
          "EnglishName": "Chelyabinsk",
          "Level": 1,
          "LocalizedType": "Oblast",
          "EnglishType": "Oblast",
          "CountryID": "RU"
        },
        "TimeZone": {
          "Code": "YEKT",
          "Name": "Asia/Yekaterinburg",
          "GmtOffset": 5,
          "IsDaylightSaving": false,
          "NextOffsetChange": null
        },
        "GeoPosition": {
          "Latitude": 55.16,
          "Longitude": 61.403,
          "Elevation": {
            "Metric": {
              "Value": 233,
              "Unit": "m",
              "UnitType": 5
            },
            "Imperial": {
              "Value": 764,
              "Unit": "ft",
              "UnitType": 0
            }
          }
        },
        "IsAlias": false,
        "SupplementalAdminAreas": [
          {
            "Level": 2,
            "LocalizedName": "Chelyabinsk",
            "EnglishName": "Chelyabinsk"
          }
        ],
        "DataSets": [
          "AirQualityCurrentConditions",
          "AirQualityForecasts",
          "Alerts",
          "ForecastConfidence"
        ]
      }
    ]

    Как видим, нам вернулся массив из одного города. Как будет выглядеть результат, если городов с одинаковым названием два и больше:

    [
      {
        "Version": 1,
        "Key": "294021",
        "Type": "City",
        "Rank": 10,
        "LocalizedName": "Москва",
        "EnglishName": "Moscow",
        "PrimaryPostalCode": "",
        "Region": {
          "ID": "ASI",
          "LocalizedName": "Азия",
          "EnglishName": "Asia"
        },
        "Country": {
          "ID": "RU",
          "LocalizedName": "Россия",
          "EnglishName": "Russia"
        },
        "AdministrativeArea": {
          "ID": "MOW",
          "LocalizedName": "Москва",
          "EnglishName": "Moscow",
          "Level": 1,
          "LocalizedType": "Город федерального подчинения",
          "EnglishType": "Federal City",
          "CountryID": "RU"
        },
        "TimeZone": {
          "Code": "MSK",
          "Name": "Europe/Moscow",
          "GmtOffset": 3,
          "IsDaylightSaving": false,
          "NextOffsetChange": null
        },
        "GeoPosition": {
          "Latitude": 55.752,
          "Longitude": 37.619,
          "Elevation": {
            "Metric": {
              "Value": 155,
              "Unit": "m",
              "UnitType": 5
            },
            "Imperial": {
              "Value": 508,
              "Unit": "ft",
              "UnitType": 0
            }
          }
        },
        "IsAlias": false,
        "SupplementalAdminAreas": [
          {
            "Level": 2,
            "LocalizedName": "Tsentralny",
            "EnglishName": "Tsentralny"
          }
        ],
        "DataSets": [
          "AirQualityCurrentConditions",
          "AirQualityForecasts",
          "Alerts",
          "ForecastConfidence"
        ]
      },
      {
        "Version": 1,
        "Key": "1397263",
        "Type": "City",
        "Rank": 85,
        "LocalizedName": "Москва",
        "EnglishName": "Moskwa",
        "PrimaryPostalCode": "",
        "Region": {
          "ID": "EUR",
          "LocalizedName": "Европа",
          "EnglishName": "Europe"
        },
        "Country": {
          "ID": "PL",
          "LocalizedName": "Польша",
          "EnglishName": "Poland"
        },
        "AdministrativeArea": {
          "ID": "10",
          "LocalizedName": "Лодзинское воеводство",
          "EnglishName": "Łódź",
          "Level": 1,
          "LocalizedType": "Воеводство",
          "EnglishType": "Voivodship",
          "CountryID": "PL"
        },
        "TimeZone": {
          "Code": "CET",
          "Name": "Europe/Warsaw",
          "GmtOffset": 1,
          "IsDaylightSaving": false,
          "NextOffsetChange": "2021-03-28T01:00:00Z"
        },
        "GeoPosition": {
          "Latitude": 51.816,
          "Longitude": 19.657,
          "Elevation": {
            "Metric": {
              "Value": 238,
              "Unit": "m",
              "UnitType": 5
            },
            "Imperial": {
              "Value": 780,
              "Unit": "ft",
              "UnitType": 0
            }
          }
        },
        "IsAlias": false,
        "SupplementalAdminAreas": [
          {
            "Level": 2,
            "LocalizedName": "Восточно-Лодзинский повят",
            "EnglishName": "Łódź East"
          },
          {
            "Level": 3,
            "LocalizedName": "Новосольна",
            "EnglishName": "Nowosolna"
          }
        ],
        "DataSets": [
          "AirQualityCurrentConditions",
          "AirQualityForecasts",
          "Alerts",
          "ForecastConfidence",
          "FutureRadar",
          "MinuteCast",
          "Radar"
        ]
      },
      {
        "Version": 1,
        "Key": "580845",
        "Type": "City",
        "Rank": 85,
        "LocalizedName": "Москва",
        "EnglishName": "Moskva",
        "PrimaryPostalCode": "",
        "Region": {
          "ID": "ASI",
          "LocalizedName": "Азия",
          "EnglishName": "Asia"
        },
        "Country": {
          "ID": "RU",
          "LocalizedName": "Россия",
          "EnglishName": "Russia"
        },
        "AdministrativeArea": {
          "ID": "KIR",
          "LocalizedName": "Киров",
          "EnglishName": "Kirov",
          "Level": 1,
          "LocalizedType": "Республика",
          "EnglishType": "Republic",
          "CountryID": "RU"
        },
        "TimeZone": {
          "Code": "MSK",
          "Name": "Europe/Moscow",
          "GmtOffset": 3,
          "IsDaylightSaving": false,
          "NextOffsetChange": null
        },
        "GeoPosition": {
          "Latitude": 57.968,
          "Longitude": 49.104,
          "Elevation": {
            "Metric": {
              "Value": 207,
              "Unit": "m",
              "UnitType": 5
            },
            "Imperial": {
              "Value": 678,
              "Unit": "ft",
              "UnitType": 0
            }
          }
        },
        "IsAlias": false,
        "SupplementalAdminAreas": [
          {
            "Level": 2,
            "LocalizedName": "Verkhoshizhemsky",
            "EnglishName": "Verkhoshizhemsky"
          }
        ],
        "DataSets": [
          "AirQualityCurrentConditions",
          "AirQualityForecasts",
          "Alerts",
          "ForecastConfidence"
        ]
      },
      {
        "Version": 1,
        "Key": "2488304",
        "Type": "City",
        "Rank": 85,
        "LocalizedName": "Москва",
        "EnglishName": "Moskva",
        "PrimaryPostalCode": "",
        "Region": {
          "ID": "ASI",
          "LocalizedName": "Азия",
          "EnglishName": "Asia"
        },
        "Country": {
          "ID": "RU",
          "LocalizedName": "Россия",
          "EnglishName": "Russia"
        },
        "AdministrativeArea": {
          "ID": "PSK",
          "LocalizedName": "Псков",
          "EnglishName": "Pskov",
          "Level": 1,
          "LocalizedType": "Область",
          "EnglishType": "Oblast",
          "CountryID": "RU"
        },
        "TimeZone": {
          "Code": "MSK",
          "Name": "Europe/Moscow",
          "GmtOffset": 3,
          "IsDaylightSaving": false,
          "NextOffsetChange": null
        },
        "GeoPosition": {
          "Latitude": 57.449,
          "Longitude": 29.185,
          "Elevation": {
            "Metric": {
              "Value": 161,
              "Unit": "m",
              "UnitType": 5
            },
            "Imperial": {
              "Value": 528,
              "Unit": "ft",
              "UnitType": 0
            }
          }
        },
        "IsAlias": false,
        "SupplementalAdminAreas": [
          {
            "Level": 2,
            "LocalizedName": "Porkhovsky",
            "EnglishName": "Porkhovsky"
          }
        ],
        "DataSets": [
          "AirQualityCurrentConditions",
          "AirQualityForecasts",
          "Alerts",
          "Radar"
        ]
      },
      {
        "Version": 1,
        "Key": "580847",
        "Type": "City",
        "Rank": 85,
        "LocalizedName": "Москва",
        "EnglishName": "Moskva",
        "PrimaryPostalCode": "",
        "Region": {
          "ID": "ASI",
          "LocalizedName": "Азия",
          "EnglishName": "Asia"
        },
        "Country": {
          "ID": "RU",
          "LocalizedName": "Россия",
          "EnglishName": "Russia"
        },
        "AdministrativeArea": {
          "ID": "TVE",
          "LocalizedName": "Тверь",
          "EnglishName": "Tver'",
          "Level": 1,
          "LocalizedType": "Область",
          "EnglishType": "Oblast",
          "CountryID": "RU"
        },
        "TimeZone": {
          "Code": "MSK",
          "Name": "Europe/Moscow",
          "GmtOffset": 3,
          "IsDaylightSaving": false,
          "NextOffsetChange": null
        },
        "GeoPosition": {
          "Latitude": 56.918,
          "Longitude": 32.163,
          "Elevation": {
            "Metric": {
              "Value": 251,
              "Unit": "m",
              "UnitType": 5
            },
            "Imperial": {
              "Value": 823,
              "Unit": "ft",
              "UnitType": 0
            }
          }
        },
        "IsAlias": false,
        "SupplementalAdminAreas": [
          {
            "Level": 2,
            "LocalizedName": "Penovsky",
            "EnglishName": "Penovsky"
          }
        ],
        "DataSets": [
          "AirQualityCurrentConditions",
          "AirQualityForecasts",
          "Alerts"
        ]
      }
    ]

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

    Итак, представление о том, с чем нам нужно работать с начала есть. Теперь, осталось переложить всё в C#. Для большего интереса, я буду программировать на языке C# и используя редактор VSCodium. Всё это работает на OpenSuSe Leap 15.2.

    Для начала, чтобы не заморачиваться каждый раз со вводом очень длинного ApiKey, реализуем метод, который запишет ApiKey на диск и ещё один метод, который этот ApiKey будет восстанавливать в памяти.

    Класс, который описывает UserApi:

    namespace habraweatherappconsole
    {
        public class UserApi
        {
            public string UserApiProperty { get;set; }
        }
    }

    Далее, создаю класс, который будет реализовывать методы чтения и записи:

            /// <summary>
            /// Метод реализуе возможность восстановления списка APIKey в памяти
            /// </summary>
            public static void ReadUserApiToLocalStorage()
            {
                XmlSerializer xmlSerializer = new XmlSerializer(typeof(ObservableCollection<UserApi>));
    
                try
                {
                    using (StreamReader sr = new StreamReader("UserApi.xml"))
                    {
                        userApiList = xmlSerializer.Deserialize(sr) as ObservableCollection<UserApi>;
                    }
                }
    
                catch(Exception ex)
                {
                    /* Не вывожу никаких сообщений об ошибке. Потому как, если утилита была запущена впервые
                    / то файла скорее всего нет. Даже если бы он был и из-за каких-то аппаратных проблем стал недоступен
                    / то что я могу с этим поделать в таком случае?
                    */
                }
            }

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

    Сделано. Дальше, начинается та часть работы, которая требует внимательного подхода к ней. Нужно реализовать класс, который будет соответствовать получаемому Json объекту. Любая ошибка в переносе может привести к тому, что утилита будет падать с ошибками.

    Итак, что мы видим в верхушке получаемого объекта:

        "Version": 1,
        "Key": "292332",
        "Type": "City",
        "Rank": 21,
        "LocalizedName": "Chelyabinsk",
        "EnglishName": "Chelyabinsk",
        "PrimaryPostalCode": "",
    

    Мы видим версию API в виде числа, уникальный ключ города в виде числа, тип населённого пункта в виде строки, Ранг (не смог найти, что это значит) в виде числа, оригинальное и локализованное название города в виде строки и почтовый индекс. Почтовый индекс вернулся пустой, поэтому, беру строку из-за её универсальности.

    Следовательно, реализовываем эту часть так:

        public class RootBasicCityInfo    {
            public int Version { get; set; } 
            public string Key { get; set; } 
            public string Type { get; set; } 
            public int Rank { get; set; } 
            public string LocalizedName { get; set; } 
            public string EnglishName { get; set; } 
            public string PrimaryPostalCode { get; set; } 
    

    Так же, в json объекте присутствует и другие сведения об искомом городе:

          "Region": {
            "ID": "ASI",
            "LocalizedName": "Азия",
            "EnglishName": "Asia"
          },
          "Country": {
            "ID": "RU",
            "LocalizedName": "Россия",
            "EnglishName": "Russia"
          },
          "AdministrativeArea": {
            "ID": "MOW",
            "LocalizedName": "Москва",
            "EnglishName": "Moscow",
            "Level": 1,
            "LocalizedType": "Город федерального подчинения",
            "EnglishType": "Federal City",
            "CountryID": "RU"
          },
    

    Поэтому, там же реализуем классы для Region, Country, AdministrativeArea примерно так:

    public class Region    {
            public string ID { get; set; } 
            public string LocalizedName { get; set; } 
            public string EnglishName { get; set; } 
        }
    
        public class Country    {
            public string ID { get; set; } 
            public string LocalizedName { get; set; } 
            public string EnglishName { get; set; } 
        }
    
        public class AdministrativeArea    {
            public string ID { get; set; } 
            public string LocalizedName { get; set; } 
            public string EnglishName { get; set; } 
            public int Level { get; set; } 
            public string LocalizedType { get; set; } 
            public string EnglishType { get; set; } 
            public string CountryID { get; set; } 
        }
    

    Итоговый класс должен выглядеть примерно так:

        public class RootBasicCityInfo    {
            public int Version { get; set; } 
            public string Key { get; set; } 
            public string Type { get; set; } 
            public int Rank { get; set; } 
            public string LocalizedName { get; set; } 
            public string EnglishName { get; set; } 
            public string PrimaryPostalCode { get; set; } 
            public Region Region { get; set; } 
            public Country Country { get; set; } 
            public AdministrativeArea AdministrativeArea { get; set; } 
            public TimeZone TimeZone { get; set; } 
            public GeoPosition GeoPosition { get; set; } 
            public bool IsAlias { get; set; } 
            public List<SupplementalAdminArea> SupplementalAdminAreas { get; set; } 
            public List<string> DataSets { get; set; } 
        }
    

    Дальше, описываем класс, который реализует метод получения списка городов:

    using System;
    using System.Collections.ObjectModel;
    using System.Net;
    using System.Text.Json;
    
    using static System.Console;
    
    namespace habraweatherappconsole
    {
        /// <summary>
        /// Класс описывает возможность получения поиска
        /// и добавления городов для их последующего мониторинга.
        /// </summary>
        public static class SearchCity
        {
            /// <summary>
            /// Метод реализует возможность получения списка городов.
            /// В качестве формального параметра принимается название города
            /// которое должно быть указано в классе MainMenu.
            /// </summary>
            /// <param name="formalCityName"></param>
            public static void GettingListOfCitiesOnRequest(string formalCityName)
            {
                // Получаю ApiKey из списка
                string apiKey = UserApiManager.userApiList[0].UserApiProperty;
                try
                {
                    string jsonOnWeb = $"http://dataservice.accuweather.com/locations/v1/cities/search?apikey={apiKey}&q={formalCityName}";
    
                    WebClient webClient = new WebClient();
                    string prepareString = webClient.DownloadString(jsonOnWeb);
    
                    ObservableCollection<RootBasicCityInfo> rbci = JsonSerializer.Deserialize<ObservableCollection<RootBasicCityInfo>>(prepareString);
    
                    DataRepo.PrintКeceivedСities(rbci);
                }
                catch (Exception ex)
                {
                    WriteLine("Неполучилось отобразить запрашиваемый город."
                    + "Возможные причины: \n" + 
                    "* Неправильно указано название города\n"
                    + "* Нет доступа к интернету\n"
                    + "Подробнее ниже: \n"
                    + ex.Message);
                }
    
            }
        }
    }

    Реализую метод, который выводит на экран полученный список:

            /// <summary>
            /// Метод реализует возможность отображать список запрашиваемых городов
            /// (Если городов с таким названием больше, чем 1).
            /// </summary>
            /// <param name="formalListOfCityes"></param>
            public static void PrintКeceivedСities (ObservableCollection<RootBasicCityInfo> formalListOfCityes)
            {
                string pattern = "=====\n" + "Номер в списке: {0}\n" + "Название в оригинале: {1}\n"
                + "В переводе:  {2} \n" + "Страна: {3}\n" + "Административный округ: {4}\n"
                + "Тип: {5}\n" + "====\n";
                int numberInList = 0;
    
                foreach (var item in formalListOfCityes)
                {
                    WriteLine(pattern, numberInList.ToString(),
                    item.EnglishName, item.LocalizedName, item.Country.LocalizedName,
                    item.AdministrativeArea.LocalizedName, item.AdministrativeArea.LocalizedType);
                    numberInList++;
                }
    
                Write ("Номер какого города добавить в мониторинг: ");
                int num = Convert.ToInt32(Console.ReadLine());
    
                try
                {
                    listOfCityForMonitorWeather.Add(formalListOfCityes[num]);
                }
    
                catch (Exception ex)
                {
                    WriteLine("Похоже, вы ошиблись цифрой.\n");
                    WriteLine(ex.Message);
                }
                WriteListOfCityMonitoring();
            }
    

    Метод, который реализует запись списка со всеми отслеживаемыми городами на диск (чтобы при повторном запуске не тратить драгоценный APIKey)

             /// <summary>
            /// Метод реализует возможность записывать список отслеживаемых городов
            /// на жёсткий диск.
            /// </summary>
            private static void WriteListOfCityMonitoring()
            {
                XmlSerializer xmlSerializer = new XmlSerializer(typeof(ObservableCollection<RootBasicCityInfo>));
    
                using (StreamWriter sw = new StreamWriter("RootBasicCityInfo.xml"))
                {
                    xmlSerializer.Serialize(sw, listOfCityForMonitorWeather);
                }
            }
    

    Отлично. Всё необходимое сделано для того, что бы перейти к следующей части реализации работы утилиты - получение информации и погоде на следующие 5 дней.

    Поскольку данные о погоде это отдельный Json объект, то по аналогии с тем, как была разобрана информация о искомом городе, нужно разобрать информацию о погоде в этом городе.

    Accuweather может предоставить информацию на 1 текущий день, 5, 10 и 15 дней. В ответ будет приходить json объект одного и того же типа. Разница будет только в Get запросе и количестве возвращаемых дней.

    Пример того, что возвращается в json объекте:

      "Headline": {
        "EffectiveDate": "2021-02-23T07:00:00+03:00",
        "EffectiveEpochDate": 1614052800,
        "Severity": 3,
        "Text": "Окончание понижения температуры: Среда",
        "Category": "cold",
        "EndDate": "2021-02-24T19:00:00+03:00",
        "EndEpochDate": 1614182400,
        "MobileLink": "http://m.accuweather.com/ru/ru/moscow/294021/extended-weather-forecast/294021?unit=c",
        "Link": "http://www.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?unit=c"
      },
      "DailyForecasts": [
        {
          "Date": "2021-02-23T07:00:00+03:00",
          "EpochDate": 1614052800,
          "Temperature": {
            "Minimum": {
              "Value": -24.4,
              "Unit": "C",
              "UnitType": 17
            },
            "Maximum": {
              "Value": -20.6,
              "Unit": "C",
              "UnitType": 17
            }
          },
          "Day": {
            "Icon": 31,
            "IconPhrase": "Холодно",
            "HasPrecipitation": false
          },
          "Night": {
            "Icon": 31,
            "IconPhrase": "Холодно",
            "HasPrecipitation": false
          },
          "Sources": [
            "AccuWeather"
          ],
          "MobileLink": "http://m.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?day=1&unit=c",
          "Link": "http://www.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?day=1&unit=c"
        },
    

    Следовательно, класс должен выглядеть примерно так:

        public class DailyForecast    {
            public DateTime Date { get; set; } 
            public int EpochDate { get; set; } 
            public Temperature Temperature { get; set; } 
            public Day Day { get; set; } 
            public Night Night { get; set; } 
            public List<string> Sources { get; set; } 
            public string MobileLink { get; set; } 
            public string Link { get; set; } 
        }
        
    
         public class RootWeather    {
            public Headline Headline { get; set; } 
            public List<DailyForecast> DailyForecasts { get; set; } 
        }
    

    Исходим из того, что пользователь может добавить (и уже добавил) один или несколько городов. Для того, чтобы показать погоду по интересующему городу, сперва выведем на экран все города, какие уже есть:

                string pattern = "=====\n" + "Номер в списке: {0}\n" + "Название в оригинале: {1}\n"
                + "В переводе:  {2} \n" + "Страна: {3}\n" + "Административный округ: {4}\n"
                + "Тип: {5}\n" + "====\n";
                int numberInList = 0;
    
                foreach (var item in DataRepo.listOfCityForMonitorWeather)
                {
                    WriteLine(pattern, numberInList.ToString(),
                    item.EnglishName, item.LocalizedName, item.Country.LocalizedName,
                    item.AdministrativeArea.LocalizedName, item.AdministrativeArea.LocalizedType);
                    numberInList++;
                }
                
                bool ifNotExists = false;
                string cityKey = null;
                int num = 0;
                do
                {
                    ifNotExists = false;
                    Write("Номер города для просмотра погоды: ");
                    num = Convert.ToInt32(Console.ReadLine());
                    
                    if (num < 0 || num > DataRepo.listOfCityForMonitorWeather.Count - 1)
                    {
                        WriteLine("Такого номера нет. Попробуйте ещё раз.");
                        ifNotExists = true;
                    }
                } while(ifNotExists);
                
                cityKey = DataRepo.listOfCityForMonitorWeather[num].Key;
    

    А затем получаю информацию о погоде:

     // Получаю ApiKey из списка
                string apiKey = UserApiManager.userApiList[0].UserApiProperty;
                
                string jsonUrl = $"http://dataservice.accuweather.com/forecasts/v1/daily/5day/{cityKey}?apikey={apiKey}&language=ru&metric=true";
    
                jsonUrl = webClient.DownloadString(jsonUrl);
    
                RootWeather weatherData = JsonSerializer.Deserialize<RootWeather>(jsonUrl);
    
                string patternWeather = "=====\n" + "Дата: {0}\n" + "Температура минимальная: {1}\n"
                +"Температура максимальная: {2}\n" + "Примечание на день: {3}\n" + "Примечание на ночь: {4}\n" + "====\n";
    
                foreach (var item in weatherData.DailyForecasts)
                {
                    WriteLine(patternWeather, item.Date, item.Temperature.Minimum.Value,
                    item.Temperature.Maximum.Value, item.Day.IconPhrase, item.Night.IconPhrase);
                }

    Таким образом будет получена и напечатана информация о погоде на следующие 5 дней.

    В заключении: в этой статье я показал основные моменты, которые были необходимы для того, чтобы данные о погоде были показаны. Полный исходный код утилиты вы сможете найти на ГитЛаб и ГитХаб. Так же, я буду рад любой критике по делу и совету от старших программистов.

    Спасибо за уделённое время, удачи!

    Комментарии 33

      –5
      Почему не yandex погода?
        +8
        Если вы хотите использовать API, напишите нам на api-weather@support.yandex.ru. В письме обязательно укажите, какую организацию вы представляете и для чего планируете использовать данные.

        Когда я увидел эту надпись, подумал, что не хочу сильно заморачиваться с получением доступа к данным. Accuweather оказался проще в этом вопросе.
          +2
          Они и не дали бы.
          Несколько раз просил…
          Видимо только юрикам.
        0

        О, шарп семилетний давности, настоявшийся.

          0

          Не смог понять, это комплимент или оскорбление?)

            +1

            Шарп еще не стал классикой, чтобы наслаждаться его старостью).


            public class AdministrativeArea    {
                    public string ID { get; set; } 
                    public string LocalizedName { get; set; } 
                    public string EnglishName { get; set; } 
                    public int Level { get; set; } 
                    public string LocalizedType { get; set; } 
                    public string EnglishType { get; set; } 
                    public string CountryID { get; set; } 
                }

            ->


            public record AdministrativeArea(string ID, string LocalizedName, string EnglishName, int Level, string LocalizedType, string EnglishType, string CountryID);

            Ну и много каких моментов можно переписать.


            UPD: я правда с json не работаю, нужно чекать, умеет ли он в рекорд писать.

              0
              UPD: я правда с json не работаю, нужно чекать, умеет ли он в рекорд писать.

              Умеет. Даже проверял. В CLR все свойства record будут public… { get; set; } — "immutable" они только на уровне компилятора для твоего собственного кода.

                0
                На сколько я помню, Newtonsoft.Json падал в рантайме с сообщением об отсутствии конструктора без параметров.
                  0

                  Да не, проверил, на версии 12.0.3 все вроде норм:


                  #pragma warning disable CA1050 // Declare types in namespaces
                  
                  using System;
                  using System.Text.Json;
                  using Newtonsoft.Json;
                  using JsonSerializer = System.Text.Json.JsonSerializer;
                  
                  var json = @"{ ""bar"": ""Bar"" }";
                  
                  // System.Text.Json
                  var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
                  var foo = JsonSerializer.Deserialize<Foo>(json, options);
                  Console.WriteLine(foo?.Bar);
                  
                  // Newtonsoft
                  foo = JsonConvert.DeserializeObject<Foo>(json);
                  Console.WriteLine(foo?.Bar);
                  
                  public record Foo(string Bar);

                  Update: Впрочем, вот, даже в документации есть: https://www.newtonsoft.com/json/help/html/DeserializeConstructorHandling.htm

                    0
                    Странно. Возможно я с чем-то спутал
          0
          А зачем получать JSON, а сохранять XML?
            0

            Я написал об этом внутри текста. Моё личное предпочтение :)

            +1

            Автор, извини, но более ужасный код было бы очень сложно написать. Одни статики, использование WebClient, catch(Exception), передача параметрами конкретных классов вместо абстракций (кстати, так и не понял — зачем в консольном приложении вообще ObservableCollection), никакого разделения на UI и все остальное, и т.п. и т.д.

              0
              Одни статики,

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

              catch(Exception)

              Есть участки кода, при использовании которых что-то может пойти не так и есть мои предположения, что приблизительно может пойти не так. Если была ошибка ввода номера города — то моя собственная проверка на корректность ввода присутствует и сообщение об этом будет показано. Соответственно, работа не будет прервана аварийно. Так же, я беру в расчёт то, что пользователь может сделать то, чего я никак не мог предусмотреть. Поэтому, чтобы избежать аварийного завершения работы утилиты я использую оператор Try Catch. Уверен, многие делают так же.

              зачем в консольном приложении вообще ObservableCollection

              Всё верно, в текущем виде незачем. Скажем так, это на будущее. Так как Майкрософт в дорожной карте .Net написали о том, что в будущем для Linux будет добавлена возможность создавать оконные приложения.
              WebClient

              Что не так с WebClient?
                +1
                не вижу смысла создавать экземпляры классов

                И очень жаль, что не видишь.


                я использую оператор Try Catch. Уверен, многие делают так же.

                И очень плохо делают. "Catch all" должен быть только на самом верху стека вызовов, чтобы ловить то, что уже совсем нигде ниже не поймалось.


                Что не так с WebClient?

                https://docs.microsoft.com/en-us/dotnet/api/system.net.webclient?view=net-5.0#remarks

                  +1
                  Поскольку утилита выполняет одну простую задачу — показывать погоду, то не вижу смысла создавать экземпляры классов.


                  Тогда и утилиту создавать особого смысла нет. HTTP запрос к погодному API можно и с помощью консольной утилиты curl отправить.
                    +1
                    Тогда и утилиту создавать особого смысла нет.

                    И тем более публиковать тут её код. Пускай это будет простая утилита на пару дюжин строк, но которая на примере хорошего кода демонстрирует что-то интересное (в данном случае, например, это могло бы быть http client factory, typed http client, или json-расширения для http). Тут же все наоборот — на примере плохого кода показано использование устаревшего класса.

                0

                Или всё-таки на .NET с помощью C#?

                  0

                  Да, всё верно. Самую стремную опечатку увидел, когда опубликовал.
                  Самое правильное название: на Платформе .Net с помощью С#

                  0
                  Денис Андреевич, добрый день!
                  Вопрос — а можно ли получать текущие данные,
                  температуру, направление и скорость ветра,
                  влажность воздуха, осадки (дождь/снег) и их объем,
                  причем с территории с заданным радиусом
                  вокруг интересующего меня города?
                  Понятно, для чего — для построения прогнозирующей нейросети.
                  Надоели эти прогнозисты — что ни прогнозист, то новый прогноз.
                  Другой раз так и хочется им сказать:
                  «Вы хоть в окно посмотрите!»
                    0
                    Добрый вечер.
                    Отвечая коротко на ваш вопрос — нет.
                    Есть 2 ветки Api под названием:
                    • Weather Alarms API
                    • Alerts API

                    Но на бесплатной учётно записи доступ к ним закрыт. Возможно там содержится нечто такое.
                    0
                    Да, конечно!
                    Это коммерческая информация,
                    стоит денег.
                      0
                      Сгенерируйте 500 ключей, например.
                      0

                      Тип проекта — .net5, но при этом не использован ни один современный инструмент, предлагаемый экосистемой. Автор не умеет в DI? Или он все проекты пишет подобным образом "а, давайте везде будут статики" не рассчитывая на дальнейшее развитие/доработки проекта? Автор не в курсе, что WebClient уже несколько лет вендор не рекомендует использовать? Ну, такое. "Привет из нулевых" практически.


                      Поэтому я бы не рекомендовал смотреть код подобных проектов: в них нет никаких образовательных моментов. Возможно, есть люди, кому надо решить конкретную практическую задачу и поэтому они согласны на хряк-хряк — и в продакшен, не глядя, что за код скопипастили, но у меня есть некоторый стандарт на пет-проекты: делаешь — делай с душой, красиво.

                        0
                        string apiKey = UserApiManager.userApiList[0].UserApiProperty;


                        Куда лучше и безопаснее было бы написать

                        string apiKey = UserApiManager.userApiList.FirstOrDefault().UserApiProperty;


                        Использовать HttpClient вместо WebClient, record вместо классов и не делать всё статичным.

                        На счёт WebClient

                        Если коротко:

                        We don't recommend that you use the WebClient class for new development. Instead, use the System.Net.Http.HttpClient class.
                          0
                          Спасибо за советы. Буквально на днях нашёл прекрасную запись на тему как лучше работать с C# 9.0:
                          devblogs.microsoft.com/dotnet/c-9-0-on-the-record
                            +1
                            Использовать FirstOrDefault без проверки на null ничем не лучше обращения по индексу в потенциально пустую коллекцию.
                              0
                              Использовать FirstOrDefault без проверки на null ничем не лучше обращения по индексу в потенциально пустую коллекцию.


                              Но в случае обращения по индексу уже будет ошибка, если коллекция пустая, а в случае FirstOrDefault мы просто получим null, который можно будет потом легко проверить на наличие значения, благо у нас есть оператор для этого и не надо создавать блоки с if.
                                0
                                У вас
                                string apiKey = UserApiManager.userApiList.FirstOrDefault().UserApiProperty;
                                , а должно быть
                                string apiKey = UserApiManager.userApiList.FirstOrDefault()?.UserApiProperty;.
                                Не помню как называется оператор проверки на null '?', но у вас его не было и в результате был бы именно NullReferenceException, а не null в переменной apiKey.
                                  0
                                  null в переменной apiKey — значит, оно упадет немного позже.
                                    0
                                    Да это понятно, что упадёт если ничего не делать. Можно сразу после проверить на null и кинуть более осмысленное исключение, чем NullRef….
                                    А суть коммента вообще в чём была? В том, что автор первого коммента в ветке сказал «по индексу плохо, используй FirstOrDef, а то можешь словить Ex». После чего я лишь указал ему на то, что в своём совете он забыл про оператор '?', так что после его совета просто сменится один непонятных эксепшен в логах на другой.
                                      0

                                      Всё равно либо обрабатывать ошибки, либо надеяться, что оно где то упадет.

                            0
                            Если бы автор использовал Visual Studio, то можно сгенерировать классы из Json, а не писать руками. Edit->Paste Special->Paste JSON As Classes. image

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

                            Самое читаемое