Пакет RPM - это просто файл, содержащий другие файлы и информацию о них, необходимую для развёртывания файлов в операционной системе. Для сборки RPM-пакета мною использовалась операционная система CentOS 7.
Для начала работы из домашней директории текущего пользователя выполните команду в терминале:
1
rpmdev-setuptree
В результате выполнения команды в домашней директории текущего пользователя появится папка rpmbuild следующей структуры:
BUILD
RPMS
SOURCES
SPECS
SRPMS
Сборка RPM-пакета
Для сборки потребуется:
SPEC-файл (в нашем случае LinuxInstaller-2.15.2.4.spec), который нужно расположить в папке SPECS
Архив tar.gz (в нашем случае LinuxInstaller-2.15.2.4.tar.gz) с бинарниками, и в нашем случае bash-скриптом install, который нужно расположить в папке SOURCES
Важно, чтобы имя архива (в нашем случае LinuxInstaller-2.15.2.4.tar.gz) и указанные в SPEC-файле параметры Name, Version и Release соответствовали друг другу!
Перед сборкой RPM-пакета рекомендуется применить линтер к SPEC-файлу командой в терминале:
Ключ -bb означает, что RPM-пакет будет собираться из готовых бинарников. Также RPM-пакет можно собрать из исходников. SPEC-файл разворачивает бинарники из папки SOURCES в папку BUILD, копирует их в новую папку BUILDROOT и упаковывает их в RPM-пакет, который появится в папке RPMS (подпапке x86_64) в rpm-файле LinuxInstaller-2.15.2-4.x86_64.rpm. При установке rpm-пакет разворачивает содержимое во временную папку /tmp/LinuxInstaller-2.15.2.4/, запускает bash-скрипт install командой /bin/bash /tmp/%{name}-%{version}.%{release}/install и после установки удаляет временную папку. Основная логика развёртывания файлов вынесена в bash-скрипт install, несущественного для рассматриваемого здесь вопроса.
Развёртывание RPM-пакета
Развернуть RPM-пакет можно как из терминала под root-пользователем, так и из специальной программы в графической оболочке операционной системы.
Для развёртывания RPM-пакета из терминала выполните под root-пользователем следующую команду:
Для удаления RPM-пакета из терминала выполните под root-пользователем следующую команду:
1
rpm -ev LinuxPrintClientInstaller-2.15.2-4
Обратите внимание, что при удалении x86_64.rpm указывать в конце пакета не надо!
Обновление RPM-пакета
Обновление пакета требует собрать версию с большими значениями Version и Release. Обратите внимание, что имя и содержимое архива tar.gz также должены соответствовать этим Version и Release.
Если для обновления использовать GUI операционной системы, то RPM-пакет предыдущей версии будет удалён автоматически в случае успешного развертывания пакета обновления. При установке обновления из командной строки предыдущую версию придётся удалять из операционной системы вручную также из командной строки.
Всем хорош подход FluentApi при выполнении запросов в LINQ, но одна вещь ему не по зубам. Это выполнение удобных Left Join запросов. Но SQL-style запросы в LINQ позволяют решить данную проблему.
Конструкция join в LINQ даёт INNER JOIN соединение таблиц (если проводить аналогию с SQL). А для LEFT JOIN рассмотрим код, показывающий как решить данную проблему в LINQ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
var salesProducts = ( from product in context.Products join person in context.SalesPeople on (product.Region, product.Type) equals (person.Region, person.ProductType) into productPeople from productPerson in productPeople.DefaultIfEmpty() selectnew { Person = productPerson?.Name ?? string.Empty, Product = product.Name, product.Region, product.Type } ).ToList();
DefaultIfEmpty() как раз и осуществляет LEFT JOIN в случае, если productPerson не имеет соответствующей сущности в таблице продаванов SalesPeople. Результат join запроса, соединяющего сущности таблицы Products с сущностями таблицы SalesPeople помещается во временную переменную productPersons с помощью синтаксической конструкции into productPersons и затем оператор DefaultIfEmpty() в конструкции from productPerson in productPersons.DefaultIfEmpty() помещает null в качестве productPerson, если для указаного продукта не имеется соответствующего продавца. Затем мы формируем с помощью проекции select экземпляр анонимного класса, содержащего допустимые значения в случае отсутствия продавца person у требуемого продукта product. Это достигается с помощью конструкции Person = productPerson?.Name ?? string.Empty, которая позволяет избавиться от последующей проверки значения на null.
Когда в приложении прогнозируется присутствие проблем при запросах по сети и не требуется сдаваться в получении ответа при появлении первой же проблемы, я использую шаблон увеличивающегося интервала повторения запроса. Суть шаблона такова: в случае отсутствия ответа (от МФУ например) в отведённый таймаут приложение ожидает определённый интервал времени и затем пытается отправить запрос снова. Если и во второй раз нет ответа, то ожидаем в два раза дольший интервал времени и т.д. (в три, четыре… Вид прогрессии можно выбрать самостоятельно). Получается, что запросы идут всё реже и реже не перегружая устройство (или сайт) в случае сетевых проблем на его стороне. В случае отсутствия ответов заданное число раз, можно полностью прекратить отправку запросов.
Ниже приводится код условного метода Method. База интервала повторения запроса составляет одну секунду и задана арифметическая прогрессия увеличения интервала. Можно задать геометрическую прогрессию, умножая текущий интервал повторения на 2 каждый раз или даже степенную, возводя базу в степень текущего числа повторений (var currentDelay = Math.Pow(DelayInMilliseconds, tryCounter);). Ожидание реализовано синхронно, а асинхронный вариант ожидания (требующий изменения сигнатуры метода на Task Method()) показан в комментарии.
try { do { try { response = // Http запрос к сайту, устройству или т.п. success = true; } catch(HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.RequestTimeout) { tryCounter += 1; var currentDelay = DelayInMilliseconds * tryCounter; Thread.Sleep(currentDelay); // await Task.Delay(currentDelay); } } while(tryCounter < RetryCount); } finally { if (success) return response; else thrownew HttpRequestException("Request failed"); } }
Обратите внимание, на двойное обёртывание блоками try-catch-finally самого сетевого запроса. Именно во внешнем блоке finally формируется возвращаемый методом ответ return response;. Иначе он выкидывает наружу исключение HttpRequestException.
При создании API-интерфейсов, которые возвращают значительный объем данных, нам необходимо принимать проектные решения, основываясь на нижележащей базе данных. Например, разработчик может реализовать специальный endpoint в API, который позволит клиентам получать наборы значений из заданного диапазона. Как разработчики API, мы можем выбирать между постраничной пагинацией и пагинацией с помощью курсора.
В этом посте показано, как реализовать оба подхода и почему разбиение по страницам с помощью курсора - это более предпочтительный по производительности вариант.
Что такое пагинация?
Для разработчиков, только начинающих создавать управляемые данными API, понятие пагинации, как разбиения на страницы, становится важной для понимания концепцией. Набор значений может быть как ограниченным, так и безграничным.
Ограниченный набор значений имеет предел. Например, семья будет иметь верхнюю границу количества детей. В большинстве случаев в семье бывает 2-4 ребенка. Таким образом, мы, скорее всего, вернем всех детей в едином ответе при построении API на базе семьи.
Безграничный набор значений не имеет предела. Обычно это коллекции, связанные с вводом пользователя, данными временных рядов или другими механизмами. Например, все платформы социальных сетей работают с безграничным ресурсом. Например, в Твиттере твиты отправляют миллионы людей по всей планете одновременно. В этих случаях невозможно вернуть полный набор данных любому клиенту, но вместо этого API создает фрагмент на основе запрашиваемового критерия.
Что касается API, то пагинация - это попытка разработчика предоставить клиенту частичный набор результатов из огромного источника данных, с которым невозможно взаимодействовать как то иначе.
Выбор правильного подхода к пагинации
Существует два подхода к разбиению коллекций на страницы:
Пагинация по номеру и размеру страницы
Пагинация с использованием курсора и размера страницы
Термин курсор переопределён. Его не следует путать с понятием курсора из реляционных баз данных. Хотя идея имеет сходство с реализацией в базе данных, они не связаны между собой.
Курсор - это идентификатор, который извлекает следующий элемент в наших последовательных запросах пагинации. Таким образом, пользователи могут размышлять об этом отвечая на вопрос: «Что последует за этим идентификатором курсора?»
Давайте рассмотрим на два подхода в формате запроса:
1 2 3 4 5 6 7
### Пагинация по номеру и размеру страницы GET https://localhost:5001/pictures/paging?page=100000&size=10 Accept: application/json
### Пагинация с использованием курсора и размера страницы GET https://localhost:5001/pictures/cursor?after=999990&size=10 Accept: application/json
Два HTTP-запроса выглядят очень похоже с точки зрения пользователя, но работают принципиально по-разному.
Для запроса пагинации мы вычисляем диапазон в нашем хранилище данных и извлекаем эти элементы. Здесь мы разместим коллекцию изображений, чтобы получить результаты на определенной странице:
1 2 3 4 5
var query = db .Pictures .OrderBy(x => x.Id) .Skip((page - 1) * size) .Take(size);
Хотя данный подход работает, разработчики должны помнить о предостережениях, самым большим из которых является смещение диапазона по мере поступления новых данных. Другая проблема в данном подходе зависит от нижележащей базы данных. При разбиении на страницы с помощью диапазонов база данных должна сканировать всё множество результатов, чтобы вернуть клиенту страницу. Таким образом, когда мы перемещаемся всё ближе к концу коллекции, производительность начинает ухудшаться.
А как работает пагинация с помощью курсора? В отличие от создания диапазона для получения множества результатов, мы используем предыдущий результат для получения следующего. Курсором может быть любое значение, гарантирующее следующий диапазон. Поля, такие как авто-инкрементируемые идентификаторы или отметки времени, идеально подходят для пагинации с помощью курсора. Пагинация с помощью курсора не страдает от проблемы, связанной с тем, что входящие данные искажают наши результаты разбиения по страницам, т.к. наш порядок является детерминированным.
Давайте рассмотрим, как реализовать пагинацию с помощью курсора в виде запроса LINQ:
1 2 3 4 5 6
var query = db .Pictures .OrderBy(x => x.Id) // будет задействован индекс .Where(x => x.Id > after) .Take(size);
Пагинация достаточно дорогостоящая операция, какой бы подход мы не использовали. Вы когда-нибудь задумывались, почему Google перестает показывать страницы результатов поиска после максимум 25 страниц? Что ж, существует способ уменьшения этой стоимости и повышения производительности.
Сравнивая производительность
Один из наиболее заметных недостатков пагинации по номеру и размеру страницы по сравнению с пагинацией с помощью курсора - это влияние на производительность. Давайте сравним две реализации веб-приложения в ASP.NET Core.
Примечание: В этом примере используются минималистические ASP.NET Core API-интерфейсы.
using System.Linq; using CursorPaging; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<Database>();
var app = builder.Build(); awaitusing (var scope = app.Services.CreateAsyncScope()) { var db = scope.ServiceProvider.GetService<Database>(); var logger = scope.ServiceProvider.GetService<ILogger<WebApplication>>(); var result = await Database.SeedPictures(db); logger.LogInformation($"Seed operation returned with code {result}"); }
if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
awaitusingvar db = http.RequestServices.GetRequiredService<Database>(); var total = await db.Pictures.CountAsync(); var query = db .Pictures .OrderBy(x => x.Id) .Skip((page - 1) * size) .Take(size);
var logger = http.RequestServices.GetRequiredService<ILogger<Database>>(); logger.LogInformation($"Using Paging:\n{query.ToQueryString()}");
Ого! 52 мс против 225 мс! Это существенная разница при том же результате. Откуда же возникло это улучшение производительности? Давайте рассмотрим SQL запросы, сгенерированные EF Core, и сравним их.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
-- Использование страницы/размера .param set @__p_1 10 .param set @__p_0 999990
SELECT "p"."Id", "p"."Created", "p"."Url" FROM "Pictures" AS "p" ORDERBY "p"."Id" LIMIT @__p_1 OFFSET @__p_0
-- Использование курсора .param set @__after_0 999990 .param set @__p_1 10
SELECT "p"."Id", "p"."Created", "p"."Url" FROM "Pictures" AS "p" WHERE "p"."Id" > @__after_0 ORDERBY "p"."Id" LIMIT @__p_1
Видно, что первый запрос (страница/размер) использует ключевое слово OFFSET. Это ключевое слово говорит нашему провайдеру базы данных SQLite сканировать до смещения перед вызовом ключевого слова LIMIT. Как можно себе представить, сканирование таблицы из миллиона строк достаточно дорогостоящая операция.
Для запроса с курсором видно, что используется столбец Id с условием WHERE. Использование индекса позволяет SQLite эффективно перемещаться по таблице, обеспечивая и более эффективное выполнение запроса.
Заключение
Как мы видели в примерах выше, пагинация с помощью курсора становится более производительной по мере увеличения объ ёма данных. Тем не менее, пагинация с использованием страницы и размера может имееть свое место в приложениях. Этот подход намного проще реализовать и для небольших наборов данных, которые меняются нечасто, это может быть отличным выбором. Как разработчики, мы обязаны выбрать лучший вариант для нашего сценария. Тем не менее, учитывая почти четырехкратное улучшение производительности запросов, трудно поспорить с пагинацией с помощью курсора.
using System.Net.Http.Formatting; using Owin; using System.Web.Http; using System.Web.Http.Cors; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization;
publicclassStartup { // Don't delete! This code configures Web API. // The Startup class is specified as a type parameter in the WebApp.Start method publicvoidConfiguration(IAppBuilder appBuilder) { var config = new HttpConfiguration(); // Configure Web API for self-host config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.Formatters.Clear(); config.Formatters.Add(new JsonMediaTypeFormatter()); config.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter()); var cors = new EnableCorsAttribute("*", "*", "*"); config.EnableCors(cors); appBuilder.UseWebApi(config); } }
Выше происходит настройка роутинга, JSON формата запроса/ответа и cors.
Далее в проект требуется добавить WebAPI контроллер. Для примера возьмём контроллер из оригинальной статьи на сайте Microsoft, т.к. используемые мною контроллеры только загромоздят суть повествования излишним содержимым:
using System; using Topshelf; using Microsoft.Owin.Hosting; using System.Configuration;
publicclassProgram { privateconststring ServiceName = "Authentication and WebAPI service";
staticvoidMain() { var port = ConfigurationManager.AppSettings["Port"];
var options = new StartOptions(); options.Urls.Add($"http://*:{port}/");
using (WebApp.Start<Startup>(options)) // Start OWIN host { HostFactory.Run(config => { config.StartAutomatically(); // startup type config.EnableShutdown(); config.Service<AuthApiService>(service => { service.ConstructUsing(svc => new AuthApiService()); service.WhenStarted(svc => svc.Start()); service.WhenStopped(svc => svc.Stop()); }); config.RunAsLocalSystem(); config.SetServiceName(ServiceName); config.SetDisplayName(ServiceName); config.SetDescription(ServiceName); config.EnableServiceRecovery(service => { service.OnCrashOnly(); service.RestartService(delayInMinutes: 0); // first failure service.RestartService(delayInMinutes: 0); // second failure service.RestartService(delayInMinutes: 1); // subsequent failures service.SetResetPeriod(days: 1); // reset period for restart options }); }); } } }
В результате WebAPI начинает прослушивать все входящие запросы на порт, считанный из файла конфигурации и работает Windows-служба, разработанная с помощью фреймворка Topshelf. Вот так вота! Или как говорят французы: Вуаля!
Все, кто сталкивались с разработкой служб для операционной системы Windows, отмечали трудность отладки подобного ПО. Трудности старта разработки кода решаются шаблонами Visual Studio для Windows-служб, но вот сама отладка – очень неудобная. Приходится подключаться к запущеному процессу через Debug –> Attach to Process… , выбирать процесс, ставить точку останова и т.д. Но всё изменилось когда появился TopShelf. Этот фреймворк (а именно так они себя величают) позволяет разрабатывать и отлаживать Windows-службы как обычные консольные приложения!
Рассмотрим как разрабатывать Windows-службы с помощью TopShelf.
Установить TopShelf в ваш проект можно через менеджер пакетов NuGet:
1
nuget Install-Package Topshelf
Посмотреть исходный код фреймворка TopShelf можно на GitHub
Основной класс (в нашем случае AuthApiService) должен содержать методы public void Start() и public void Stop(), которые используются TopShelf при старте и остановке службы. В моём случае файл Program.cs же будет иметь следующий вид:
classProgram { privateconststring ServiceName = "Authentication and WebAPI service";
staticvoidMain(string[] args) { HostFactory.Run(config => { config.StartAutomatically(); // startup type config.EnableShutdown(); config.Service<AuthApiService>(service => { service.ConstructUsing(svc => new AuthApiService()); service.WhenStarted(svc => svc.Start()); service.WhenStopped(svc => svc.Stop()); }); config.RunAsLocalSystem(); config.SetServiceName(ServiceName); config.SetDisplayName(ServiceName); config.SetDescription(ServiceName); config.EnableServiceRecovery(service => { service.OnCrashOnly(); service.RestartService(delayInMinutes: 0); // first failure service.RestartService(delayInMinutes: 0); // second failure service.RestartService(delayInMinutes: 1); // subsequent failures service.SetResetPeriod(days: 1); // reset period for restart options }); }); } }
Всю магию делает метод HostFactory.Run. В нём указывается класс основного тела службы с методами Start и Stop, задаётся имя службы и описание в менеджере служб Windows (Services). TopShelf позволяет провести почти такую же настройку режима работы службы, которую выполняет менеджер служб Windows, в частности задать из под кого будет запускаться служба RunAsLocalSystem или задать параметры перезапуска в случае падения службы с помощью EnableServiceRecovery.
Установить службу в операционную систему Windows можно из командной строки, запущенной от Администратора, прямо в папке где находится служба командой:
1
AuthApiService.exe install
Удалить службу можно командой:
1
AuthApiService.exe uninstall
Всю магию установки и настройки службы берёт на себя Topshelf!
Службу можно установить и самостоятельно, но тогда настройки из раздела EnableServiceRecovery применены не будут.
1
sc create "Authentication and WebAPI service" binPath="<путь_к_службе>\AuthService.exe"
Работа с перечислениями Enum в C# имеет очень большие издержки. Это странно, т.к. перечисления целочисленны по своей природе, но из-за требований безопасности типов даже простые операции обходятся дорого. Подробно этот аспект освещён в книге Бена Уотсона “Высокопроизводительный код на платформе .NET” (2-е издание) в главе 6 “Использование среды .NET Framework” раздел “Удивительно высокие издержки использования перечислений”. Там рассмотрено внутреннее устройство метода Enum.HasFlag. Анализ же метода Enum.IsDefined оставлен в виде домашнего задания. Займёмся анализом, учтя основную рекомендацию Бена: “если окажется, что проверять наличие флага приходится часто, реализуйте проверку самостоятельно”. Посмотрим внутренее устройство Enum.IsDefined, реализуем самодельную альтернативу MyEnum.MyIsDefined и сравним их в деле.
ILSpy показывает следующее устройство метода Enum.IsDefined:
// System.Type publicvirtualboolIsEnumDefined(objectvalue) { if (value == null) { thrownew ArgumentNullException("value"); } if (!IsEnum) { thrownew ArgumentException(SR.Arg_MustBeEnum, "value"); } Type type = value.GetType(); if (type.IsEnum) { if (!type.IsEquivalentTo(this)) { thrownew ArgumentException(SR.Format(SR.Arg_EnumAndObjectMustBeSameType, type, this)); } type = type.GetEnumUnderlyingType(); } if (type == typeof(string)) { string[] enumNames = GetEnumNames(); object[] array = enumNames; if (Array.IndexOf(array, value) >= 0) { returntrue; } returnfalse; } if (IsIntegerType(type)) { Type enumUnderlyingType = GetEnumUnderlyingType(); if (enumUnderlyingType.GetTypeCodeImpl() != type.GetTypeCodeImpl()) { thrownew ArgumentException(SR.Format(SR.Arg_EnumUnderlyingTypeAndObjectMustBeSameType, type, enumUnderlyingType)); } Array enumRawConstantValues = GetEnumRawConstantValues(); return BinarySearch(enumRawConstantValues, value) >= 0; } thrownew InvalidOperationException(SR.InvalidOperation_UnknownEnumType); }
Выглядит этот код сложно, причем я не привожу ещё реализации многочисленных внутренних функций типа GetEnumUnderlyingType, GetEnumRawConstantValues и др., чтобы не загромождать повествование. Видно, что в Enum.IsDefined можно передать и численное значение int, и имя в виде строки, и аргумент в виде значения преречисления PetType.Dog - во всех случаях метод проверит есть ли такое значение в перечислении (вернёт True, если есть, либо False, если нет).
Реализуем метод Enum.IsDefined самостоятельно. Для этого преобразуем перечисление в словарь equivalentEnumDictionary внутри класса MyEnum. Тогда эквивалентом метода Enum.IsDefined будет метод MyEnum.MyIsDefined с таким же набором входных аргументов:
Преобразование перечисления в словарь произойдёт единожды при первом обращении к методу MyEnum.MyIsDefined.
Примечание: LINQ преобразование в словарь я также пробовал в виде .ToDictionary(k => (int)k, v => v.ToString()) и .ToDictionary(k => (int)k, v => Enum.GetName(enumType, (int)v)). Вариант, представленный в теле метода MyIsDefined, оказался самым шустрым.
Теперь сравним насколько самостоятельная проверка быстрее встроенной. Для этого воспользуемся библиотекой BenchmarkDotNet:
Получается, что самостоятельная проверка MyEnum.MyIsDefined более чем в два раза быстрее даже при такой прямолинейной реализации. Стоит согласиться с Беном Уотсоном, что если проверять Enum.IsDefined приходится часто, то реализация самостоятельной проверки даст существенную выгоду по быстродействию. При обычном сценарии достаточно пользоваться встроенной Enum.IsDefined.
Существует возможность создания разнообразных видов проектов .NET 5 прямо из терминала VSCode. Терминал можно вызвать горячими клавишами Ctrl+` или с помощью меню View -> Terminal. Взгляните:
D:\Github\EnumSharp>dotnet new Template Name Short Name Language Tags -------------------------------------------- ------------------- ---------- ---------------------- Console Application console [C#],F#,VB Common/Console Class library classlib [C#],F#,VB Common/Library WPF Application wpf [C#],VB Common/WPF WPF Class library wpflib [C#],VB Common/WPF WPF Custom Control Library wpfcustomcontrollib [C#],VB Common/WPF WPF User Control Library wpfusercontrollib [C#],VB Common/WPF Windows Forms App winforms [C#],VB Common/WinForms Windows Forms Control Library winformscontrollib [C#],VB Common/WinForms Windows Forms Class Library winformslib [C#],VB Common/WinForms Worker Service worker [C#],F# Common/Worker/Web MSTest Test Project mstest [C#],F#,VB Test/MSTest NUnit 3 Test Project nunit [C#],F#,VB Test/NUnit NUnit 3 Test Item nunit-test [C#],F#,VB Test/NUnit xUnit Test Project xunit [C#],F#,VB Test/xUnit Razor Component razorcomponent [C#] Web/ASP.NET Razor Page page [C#] Web/ASP.NET MVC ViewImports viewimports [C#] Web/ASP.NET MVC ViewStart viewstart [C#] Web/ASP.NET Blazor Server App blazorserver [C#] Web/Blazor Blazor WebAssembly App blazorwasm [C#] Web/Blazor/WebAssembly ASP.NET Core Empty web [C#],F# Web/Empty ASP.NET Core Web App (Model-View-Controller) mvc [C#],F# Web/MVC ASP.NET Core Web App webapp [C#] Web/MVC/Razor Pages ASP.NET Core with Angular angular [C#] Web/MVC/SPA ASP.NET Core with React.js react [C#] Web/MVC/SPA ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA Razor Class Library razorclasslib [C#] Web/Razor/Library ASP.NET Core Web API webapi [C#],F# Web/WebAPI ASP.NET Core gRPC Service grpc [C#] Web/gRPC dotnet gitignore file gitignore Config global.json file globaljson Config NuGet Config nugetconfig Config Dotnet local tool manifest file tool-manifest Config Web Config webconfig Config Solution File sln Solution Protocol Buffer File proto Web/gRPC
Examples: dotnet new mvc --auth Individual dotnet new web dotnet new --help dotnet new nunit --help
Создадим консольное приложение командой: dotnet new console
Но как теперь добавить в наш проект NuGet пакеты также из терминала?! Install-Package не работает :(
Тут нам поможет команда: dotnet add package <ИМЯ_ПАКЕТА>
Например, добавим в наш проект пакеты BenchmarkDotNet и BenchmarkDotNet.Annotations. Для этого в терминале выполним следующие команды: dotnet add package BenchmarkDotNet dotnet add package BenchmarkDotNet.Annotations
Вуаля! Требующиеся NuGet пакеты добавлены в наш проект:
При построении отчетов в различных системах используется Excel. Для его формирования из C# кода часто используется EPPlus. EPPlus интуитивно понятен:
1 2 3 4 5 6 7 8 9
var file = new FileInfo(@"c:\temp\myWorkbook.xlsx"); using(var package = new ExcelPackage(file)) { var sheet = package.Workbook.Worksheets.Add("My Sheet"); sheet.Cells["A1"].Value = "Hello World!";
// Save to file package.Save(); }
EPPlus имеет много встроенных механизмов для получения одного и того же результата разными путями, но не все пути одинаково эффективны для больших и очень больших размеров рабочих листов. В частности, я столкнулся с проблемой обведения контуров ячеек и выделения жирным шрифтом документов Excel, содержащих свыше 10000 столбцов.
Используемый мной подход “в лоб” заключался в обводке контуров ячеек непосредственно в месте их заполнения данными с помощью функции DrawBorder:
Использование именованных стилей NamedStyle привело к намного более эффективному построению отчетов Excel. Отчеты стали строиться за 7-10 минут, что почти в 10 раз быстрее первоначального подхода с DrawBorder. Как говорилось в рекламе из 90-х, “не все йогурты одинаково полезны…”
Согласно ГОСТ ИСО 8601-2001 “ПРЕДСТАВЛЕНИЕ ДАТ И ВРЕМЕНИ” расчет номера недели в Российской Федерации осуществляется так:
Первой неделей года является та неделя, которая содержит число 4 января. То есть если первое января выпадает на пятницу, субботу, или воскресенье, то все еще продолжается последняя неделя предыдущего года.
Это может приводить к интересным спецэффектам - в году может содержаться 52 или 53 недели, 29, 30, 31 декабря могут относиться к первой неделе следующего года, и наоборот, 1, 2, и 3 января могут относиться к 52 или 53 неделе прошлого года. В частности 53 недели содержали 2015 и 2020 годы.
Привычный подход к вычислению количества недель в году с помощью Calendar.GetWeekOfYear для нашего ГОСТа даёт сбой и работает некорректно. 53-и недели при таком подходе отсустсвуют.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Такой подход вычисляет 53-ю неделю НЕКОРРЕКТНО var ruCi = new CultureInfo("ru-RU"); var ruCal = ruCi.Calendar; var ruCwr = ruCi.DateTimeFormat.CalendarWeekRule; var ruFirstDow = ruCi.DateTimeFormat.FirstDayOfWeek;
for (var i = 1; i < dateTimeRanges.Count; i++) { var year = dateTimeRanges[i].Add(reportDto.ClientOffset).Year.ToString("D4"); var nn = ruCal.GetWeekOfYear( dateTimeRanges[i].Add(reportDto.ClientOffset), ruCwr, ruFirstDow ).ToString("D2"); // вычисляет 53-ю неделю НЕКОРРЕКТНО !!! worksheet.Cells[4, shift + i].Value = $"{nn}.{year}"; // НЕКОРРЕКТНОЕ значение для 53-ей недели!!! }
Метод Calendar.GetWeekOfYear вычиляет недели в году не по ГОСТ ИСО 8601-2001 и для календаря РФ не подходит!
Для расчёта недель согласно ГОСТ ИСО 8601-2001 следует использовать статический класс ISOWeek и метод GetWeekOfYear:
1 2 3 4 5 6 7 8
for (var i = 1; i < dateTimeRanges.Count; i++) { var year = ISOWeek.GetYear(dateTimeRanges[i].Add(reportDto.ClientOffset)).ToString("D4"); var nn = ISOWeek.GetWeekOfYear( dateTimeRanges[i].Add(reportDto.ClientOffset) ).ToString("D2"); // ПРАВИЛЬНОЕ значение для 53-ей недели worksheet.Cells[4, shift + i].Value = $"{nn}.{year}"; }
Подробнее с ISOWeek.GetWeekOfYear можно познакомиться по ссылке