Интересную особенность работы ASP.NET Core приложения в Linux мне удалось недавно зафиксировать. Было установлено, что со временем системынй файлы Linux (а именно: syslog и daemon.log) вырастают до угрожающих размеров. Оказалось, что весь консольный вывод ASP.NET Core приложений попадал туда. А консольный вывод включал помимо AddConsole() ещё и консоль Serilog Write().ConsoleColored(). Выставление корректных настроек секции Logging конфигурационных файлов appsetting.json позволил установить их уровень высоким и существенно уменьшить размеры системных файлов Linux.
Оказывается, что системная служба journald ведёт логирование вывода на консоль в свои бинарные логи, а кроме того вносит записи о выводе на консоль в системные журналы Linux (syslog и daemon.log). Поэтому будьте внимательны при выставлении уровня логирования ваших приложений, особенно на продакшене.
Ранее мною уже был рассмотрен один из механизмов сопоставления с образцом (паттерн матчинга) в статье Сопоставление с образцом на основе значений свойств объекта. В данной статье рассмотрим сопоставление с образцом на основе кортежей, что делает код намного короче.
Рассмотрим класс комнаты и перечисление статуса члена клуба такого вида:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
publicclassRoom { publicint Number { get; set; } publicstring Type { get; set; } publicstring Size { get; set; }
publicstatic List<Room> GetRooms() { returnnew List<Room> { new Room { Number = 3, Size = "Large", Type = "Deluxe" }, new Room { Number = 2, Size = "Large", Type = "Regular" }, new Room { Number = 1, Size = "Standard", Type = "Regular" } }; }
Начиная с C# 8.0 появилась возможность сопоставлять с образцом в выражении switch, используя деконструктор класса (см. ранее конструкцию Deconstruct в определении класса Room):
if (clientCategory == membership) return room.Number; }
return RoomNotAvailable; }
Код ветки _ => Membership.Bronze аналогичен случаю default: в стандартном switch.
Тогда основной метод программы может использовать вышеприведённый код таким образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
static voir Main() { Console.Write("Choose status: 0 for Bronze, 1 for Silver, 2 for Gold"); string clientStatus = Console.ReadLine();
Enum.TryParse(clientStatus, out Membership membership);
int roomNumber = AssignRoom(membership);
if (roomNumber == RoomNotAvailable) Console.WriteLine("Room not available"); else Console.WriteLine($"Room number is {roomNumber}"); }
Видно на сколько короче стала конструкция switch благодаря использованию кортежей. Компактный синтаксис кортежей делает их идеальным инструментом для сопоставления с образцом (паттерн матчинга). В остальном код и результаты идентичны приведённому в статье Сопоставление с образцом на основе значений свойств объекта.
Встречается необходимость, сгруппировав элементы, преобразовать их в структуру данных для поиска группы по ключу группировки. Это можно сделать, например, так:
1 2 3 4 5 6 7 8 9
string[] names = {"Pavel", "Peter", "Andrew", "Anna", "Alice", "John"}; var namesByLetter = new Dictionary<char, List<string>>();
В отличие от словаря, Lookup является неизменяемым (immutable) типом. У него нет методов типа Add и открытого конструктора. Интересно, что Lookup по неизвестному ключу возвращает пустую коллекцию, а Dictionary в такой ситуации выбрасывает исключение. Также в Lookup можно использовать ключи типа null. Замечу, что операции list.ToLookup(x => x) и list.GroupBy(x => x).ToDictionary(group => group.Key) семантически эквивалентны.
Пример с foreach и ToDictionary в случае ToLookup примет следующий вид:
ILookup<char, string> namesByLetter = names.ToLookup(name => name[0], name => name.ToLower());
Assert.That(namesByLetter['J'], Is.EquivalentTo(new[] {"john"})); Assert.That(namesByLetter['P'], Is.EquivalentTo(new[] {"pavel", "peter"})); // Lookup по неизвестному ключу возвращает пустую коллекцию. // Это бывает удобнее, чем поведение Dictionary, который в такой ситуации бросает исключение. Assert.That(namesByLetter['Z'], Is.Empty);
В русскоязычной литературе Lookup раньше именовался таблицей истинности. Lookup можно использовать в построении обратного индекса. Обратный индекс — это структура данных, часто использующаяся в задачах полнотекстового поиска нужного документа в большой базе документов. По своей сути обратный индекс напоминает индекс в конце бумажных энциклопедий, где для каждого ключевого слова указан список страниц, где оно встречается.
Создание кортежа с помощью конструктора выглядит громоздко. Чтобы облегчить синтаксис создания кортежей существует класс Tuple с серией статических методов, создающих кортежи:
1 2 3
var t1 = Tuple.Create(42, "abc"); // Эквивалентно: // var t1 = new Tuple<int, string>(42, "abc");
Полезное свойство кортежей состоит в том, что они реализуют интерфейс IComparable, сравнивающий кортежи по компонентам. То есть Tuple.Create(1, 2) будет меньше Tuple.Create(2, 1). Этот интерфейс по умолчанию используется в методах сортировки и поиска минимума/максимума.
Использование данного факта рассмотрим на примере:
Дан текст. Нужно составить список всех встречающихся в тексте слов, упорядоченный сначала по возрастанию длины слова, а потом лексикографически. Удивительно, но данную задачу можно решить совсем не используя ThenBy.
Наивное решение заключается в том, чтобы сформировать нужные кортежи, отсортировать их, а затем извлечь вторые компоненты кортежей, содержащих решение:
Однако следует напомнить, что keySelector функции OrderBy — это функция, которая каждому элементу последовательности ставит в соответствие некоторый ключ, по которому его будут сравнивать при сортировке. Поэтому решение можно записать намного короче, поместив кортеж (типа ValueTuple) в keySelector функции OrderBy:
Первые два дают на выходе последовательность, упорядоченную по возрастанию/убыванию ключей. А keySelector — это как раз функция, которая каждому элементу последовательности ставит в соответствие некоторый ключ, по которому его будут сравнивать при сортировке.
Из сигнатуры метода SelectMany видно, что он применяется к последовательности типа IEnumerable<T>. К каждому элементу этой последовательности применяется функция Func<T, IEnumerable<R>>, которая преобразует элемент типа T в последовательность типа IEnumerable<R>. И на выходе мы имеем последовательность типа IEnumerable<R>. Получается, что каждый элемент превращается в множество, которое может быть закрыто отличным типом от исходного, которое затем спрямляется. Результатом работы SelectMany является конкатенация всех полученных последовательностей, т.е. мы имеем не последовательность последовательностей, а единую последовательность типа, отличного от исходного (не обязательно отличного, можно получить и последовательность того же типа).
Впрочем строка уже сама по себе является последовательностью символов и реализует интерфейс IEnumerable, поэтому вызов ToCharArray на самом деле лишний.
Одно из не совсем очевидных применений SelectMany — это вычисление декартова произведения двух множеств. Декартово произведение множества {-1, 0, 1} на само себя даст все возможные относительные координаты соседей условной точки Point, где Point – класс, имеющий координаты X и Y в качестве открытых полей. Для вычисления декартова произведения двух множеств также потребуется использовать метод Select внутри SelectMany, как показано в примере ниже:
1 2 3 4 5
publicstatic IEnumerable<Point> GetNeighbours(Point p) { int[] d = {-1, 0, 1}; return d.SelectMany(i => d.Select(s => new Point(p.X + i, p.Y + s))); }
Вот ещё один интересный пример использования SelectMany. Требуется составить лексикографически упорядоченный список всех встречающихся слов в массиве строк. Слова нужно сравнивать регистронезависимо и выводить в нижнем регистре.
publicstaticvoidMain() { var vocabulary = GetSortedWords( "Hello, hello, hello, how low", "", "With the lights out, it's less dangerous", "Here we are now; entertain us", "I feel stupid and contagious", "Here we are now; entertain us", "A mulatto, an albino, a mosquito, my libido...", "Yeah, hey" ); foreach (var word in vocabulary) Console.WriteLine(word); }
Здесь для разбиения строки на слова используется класс Regex из пространства имён System.Text.RegularExpressions. Все полученные слова “спрямляются” (объединяются из разных массивов в один массив), удаляются пустые строки и повторы. В конце массив лексикографически упорядочивается и возвращается.
Без использования SelectMany пришлось бы очень трудно, т.к. каждая входящая строка после разбиения Regex.Split превращается в массив слов и мы бы получили массив массивов.
a albino an and are contagious dangerous entertain feel hello here hey how i it less libido lights low mosquito mulatto my now out s stupid the us we with yeah
Потребовалось давеча в рамках тестирования формировать Guid таким образом, чтобы он генерировался не случайно, а нужным мне управляемым способом. А именно так, чтобы при использовании цикла for по int формировался Guid следующим образом:
для int = 1 получить Guid = ‘00000000-0000-0000-0000-000000000001’ для int = 2 получить Guid = ‘00000000-0000-0000-0000-000000000002’ для int = 3 получить Guid = ‘00000000-0000-0000-0000-000000000003’
и так далее…
Как этого добиться? Решение данной задачи представлено ниже:
if (clientCategory == membership) return room.Number; }
return RoomNotAvailable; }
Код ветки _ => Membership.Bronze аналогичен случаю default: в стандартном switch.
Тогда основной метод программы может использовать вышеприведённый код таким образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
static voir Main() { Console.Write("Choose status: 0 for Bronze, 1 for Silver, 2 for Gold"); string clientStatus = Console.ReadLine();
Enum.TryParse(clientStatus, out Membership membership);
int roomNumber = AssignRoom(membership);
if (roomNumber == RoomNotAvailable) Console.WriteLine("Room not available"); else Console.WriteLine($"Room number is {roomNumber}"); }
Основная магия сопоставления с образцом с использованием значений свойств объектов происходит в методе AssignRoom. Раньше можно было делать switch только на основе единственного значения, но начиная с С# 8.0 паттерн матчинг можно делать на основе значений нескольких свойств экзмепляров классов. Такой подход намного более читабельный и улучшает ясность кода.
Читая книгу Андрея Акиньшина “Профессиональный бенчмарк” решил проверять приводимые в книге примеры на практике (автор сам так рекомендует). Начал, конечно же, со столь любимого мною в прошлом “бенчмарка новичков”: измерения DateTime.Now до и после испытуемого на скорость кода и последующего вычисления разности.
Но вот в чем засада по словам автора: для Windows10 частота обновления DateTime по умолчанию 64Гц. А это значит, что новое значение получается с интервалами 15,625 мс и это же значение составляет точность подхода с DateTime. Т.е для всех операций, которые выполнятся быстрее 15,625 мс мы получим значение времени выполнения интересующего кода равное 0, либо, если не повезёт, то 15,625 мс. Такая точность не всегда бывает недопустима, особенно если испытуемый на скорость код выполняется быстрее.
1 2 3 4 5 6
var list = Enumerable.Range(0, 10000).ToList(); DateTime start = DateTime.Now; list.Sort(); DateTime end = DateTime.Now; TimeSpan elapsedTime = end - start; Console.WriteLine(elapsedTime.TotalMilliseconds);
Пробую код из книги на Windows10 C#6.0 и получаю результаты около 4,5 мс. Предсказанный Андреем 0 никак получить не удалось, как и собственно 15,625 мс.
Двигаемся дальше.
По словам автора, лучшей альтернативой DateTime.Now является класс System.Diagnostics.Stopwatch. Типичное разрешение Stopwatch в Windows10 составляет величину порядка 300-500 нс.
1 2 3 4 5 6
var list = Enumerable.Range(0, 10000).ToList(); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); list.Sort(); stopwatch.Stop(); TimeSpan elapsedTime = stopwatch.Elapsed; Console.WriteLine(elapsedTime.TotalMilliseconds);
Этот код на машине автора выдавал результат в 0,05 мс. Однако на моей машине средний результат крутился в районе 0,491 мс, что в 10 раз больше, чем ожидалось!
Никакого анализа причин этих цифр пока привести не могу. Только начинаю погружаться в тонкости бенчмарка. Однако отмечу, что поведение DateTime.Now на моей машине много точнее, чем на машине автора, а System.Diagnostics.Stopwatch на моей машине на порядок хуже, чем на машине автора. В целом нужно согласиться с автором, что Stopwatch точнее (в моей случае на порядок), чем DateTime.Now.
Также следует иметь ввиду, что кроме низкого разрешения DateTime.Now работает медленнее DateTime.UtcNow, т.к. он пересчитывает часовые пояса. Соглашусь с советом Андрея Акиньшина, что в 99% случаев следует выбирать Stopwatch, а не DateTime. DateTime может потребоваться только в случае, если вам действительно нужно знать текущее время. Если реальное время не требуется, то используйте Stopwatch.
Swashbuckle Swagger – это фреймворк для спецификаций RESTful API, которые выраженны с помощью JSON. Swagger используется вместе с набором программных инструментов с открытым исходным кодом для проектирования, создания, документирования и использования веб-служб RESTful. Его сильная сторона заключается в том, что он дает возможность не только интерактивно просматривать спецификацию контроллеров WebAPI, но и отправлять запросы через Swagger UI.
Swagger – удобный инструмент для реализации встроенной в код документации. Добавление XML-комментариев методам контроллеров делает описание методов наглядными при работе через Swagger UI. Отличное руководство по Началу работы с Swashbuckle и ASP.NET Core расположено на сайте Майкрософт. Там приведены рекомендации по подключению Swagger и по оформлению XML-комментариев к методам контроллеров WebAPI.
Рассмотрим пример одного из таких XML-комментариев метода Create:
///<summary> /// Creates a TodoItem. ///</summary> ///<param name="item"></param> ///<returns>A newly created TodoItem</returns> ///<remarks> /// Sample request: /// /// POST /Todo /// { /// "id": 1, /// "name": "Item #1", /// "isComplete": true /// } /// ///</remarks> ///<response code="201">Returns the newly created item</response> ///<response code="400">If the item is null</response> [HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] publicasync Task<IActionResult> Create(TodoItem item) { _context.TodoItems.Add(item); await _context.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = item.Id }, item); }
Обратите внимание, как эти дополнительные комментарии улучшают пользовательский интерфейс:
Однако я столкнулся со следующей проблемой: в случае присутствия одного из следующих символов: & > < “ ‘ в XML-комментарии, описание метода в Swagger UI полностью ломается и перестаёт отображаться, т.е. метод начинает выглядеть так, как будто никакого XML-комментария вовсе и нет.
Проблема заключается в природе самого XML, который по своей сути знает только об & < > ' ", а также числовых объектах. Поэтому вместо & > < ‘ “ нужно использовать & < > ' ". В таком случае комментарии к методам WebAPI исчезать из Swagger UI не будут.