понедельник, 2 декабря 2024 г.

Надоели многочисленные if , условные операции и switch? Тогда мы идём к вам!

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

Уверен, что каждый уважающий себя разработчик за свою карьеру писал выражения ветвления, которые не ограничиваются 1-2 условиями, а содержат многочисленные условия, проверки каких-либо выражений. Зачастую такие участки кода разрастаются ещё больше, обрастают дополнительной бизнес-логикой. Чем могут причинить не меньшую боль, чем известное растение-сорняк (фото ниже):


Существует множество паттернов проектирования, которые позволят минимизировать количество таких выражений. В зависимости от ситуации, можно использовать как поведенческие паттерны "Стратегия", "Состояние", да даже структурный "Декоратор" иногда может подойти. Но сегодня хотел бы рассмотреть с вами ещё один поведенческий вариант - "Цепочка обязанностей".

Начнём с определения. Паттерн "Цепочка обязанностей" позволяет создавать цепочку объектов для обработки запроса или выполнения задачи. Как и многие другие поведенческие паттерны, "Цепочка обязанностей" основывается на том, чтобы сформировать отдельные поведения в объекты-обработчики. Таким образом паттерн подразумевает связывание объектов-обработчиков в одну цепь.

Назначение: данный паттерн позволяет избежать привязки отправителя запроса к его получателю с целью обработки запроса несколькими объектами. Т.е. происходит связывание объектов-получателей цепочку и передача запроса вдоль этой цепочки. Отправитель запроса знает только о первом обработчике, но не знает о конкретном обработчике, который в итоге и обработает запрос.

Схема паттерна выглядит следующим образом (прилагается ниже):


где Client - отправляет запрос объекту Handler, Handler - определяет интерфейс для обработки запроса. Также может определять ссылку на следующий обработчик запроса, а ConcreteHandler1 и ConcreteHandler2 - конкретные обработчики, которые реализуют функционал обработки запроса.

Рассмотрим реализацию паттерна в .NET на практическом примере.

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

  • VIP - 20%,
  • постоянный клиент - 10%,
  • новый клиент - 5%.
  • иначе не применять скидку.
Конечно же мы изначально могли бы написать такой код (и скорее всего написали бы) с помощью цепочки операторов if (можно и switch expression):

public decimal CalculateTotalOrderSummary(Customer customer, decimal price)
{
  if (customer.IsVIP)
    return price * 0.8m;
  
  if (customer.IsRegular)
    return price * 0.9m;

  if (customer.IsNew)
    return price * 0.95m;

  return price;
}

Но ведь такой подход может стать громоздким, когда количество правил разрастётся и правила усложняются. Предлагаю внедрить новый паттерн с целью рефакторинга участка кода.

1. Создадим абстрактный класс обработчика DiscountHandler, который определяет общий интерфейс для всех обработчиков скидок:

public abstract class DiscountHandler
{
  protected DiscountHandler _next;

  public DiscountHandler SetNextHandler(DiscountHandler next)
  {
    _next = next;
    return next;
  }

  public abstract decimal CalculateTotalOrderSummary(Customer customer, decimal price);
}

2. Теперь пора реализовать обработчики для каждого типа скидки, т.е. производные классы от DiscountHandler. Каждый обработчик будет отвечать за определённое правило и решать, применять ли скидку или передавать запрос следующему обработчику:

public sealed class VIPDiscountHandler : DiscountHandler
{
  public override decimal CalculateTotalOrderSummary(Customer customer, decimal price)
  {
    if (customer.IsVIP)
      return price * 0.8m;

    return _next?.CalculateDiscount(customer, price) ?? price;
  }
}

По аналогии можно реализовать и остальные, но оставлю это за рамками статьи.

3. Создадим цепочку связанных обработчиков:

var discountHandler = new VIPDiscountHandler();

discountHandler
.SetNextHandler(new RegularDiscountHandler()) .SetNextHandler(new NewCustomerDiscountHandler()) .SetNextHandler(new NoDiscountHandler());

И вот теперь можно вызвать цепочку, обратившись к методу CalculateTotalOrderSummary первого обработчика:

decimal totalOrder = discountHandler
 .CalculateTotalOrderSummary(customer, price);

Самое время разобраться в преимуществах и недостатках нового подхода.

Плюсы паттерна, в чём-то совпадают с принципами SOLID:

1. Гибкость
Позволяет динамически изменять или расширять цепочку (добавлять или удалять обработчики), не затрагивая другие части кода.

2. Слабая связанность
Каждому обработчику нужно знать только о своем непосредственном преемнике, что минимизирует зависимости.

3. Единая ответственность
Отделяет классы, которые вызывают операции, от классов, которые выполняют операции.

Минусы подхода:

1. Запрос может остаться необработанным
Если ни один из обработчиков в цепочке не может обработать запрос, он может остаться необработанным, что приведет к неожиданному поведению. Важно иметь обработчик по умолчанию или способ обработки таких сценариев.

2. Потенциальное влияние на производительность
Если цепочка становится очень длинной, это может привести к накладным расходам производительности из-за обхода нескольких обработчиков. И стоит учитывать, что добавляются объекты для последующей сборки мусора (но это применимо к большинству паттернов).

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

Источники:
- Тепляков С. "Паттерны проектирования на платформе .NET." — СПб.: Питер, 2015. Глава 7.
https://refactoring.guru/ru/design-patterns/chain-of-responsibility
https://thecodeman.net/posts/chain-responsibility-pattern

понедельник, 15 июля 2024 г.

Что не так с Activator.CreateInstance(Type) ?

  Будьте аккуратны при использовании Activator.CreateInstance(Type). Он может вернуть null


  Для справкиActivator.CreateInstance(Type) позволяет создать экземпляр типа, не зная тип во время компиляции. Например, можно создать экземпляр типа на основе файла конфигурации или ввода пользователя. Всегда, когда нельзя использовать ключевое слово new, вы можете использовать Activator.CreateInstance(Type).

  Но есть одна маленькая особенность: все мы ожидаем на выходе получить не-null объект, поскольку явно запрашиваем создание нового экземпляра, но это не всегда так. Для типов Nullable<T> Activator.CreateInstance(Type) возвращает значение null.

object? value = Activator.CreateInstance(typeof(int?));
Console.WriteLine(value is null); // true

Стоит отметить, что такое же поведение наблюдается при использовании ключевого слова new:

object? value = new int?();
Console.WriteLine(value is null); // true

Однако можно использовать оператор подавления null (!), чтобы подавить предупреждение о том, что значение может быть null, если уверены, что тип не может быть Nullable<T>:

object? value = Activator.CreateInstance(typeof(object))!;
Console.WriteLine(value.GetHashCode());
Console.WriteLine(value is null); // всегда false

Источник:
https://www.meziantou.net/activator-createinstance-type-may-return-null.htm

среда, 29 мая 2024 г.

Первичные конструкторы. Используем или это всё от лукавого?

  Когда несколько лет назад в первый раз увидел использование первичных конструкторов (primary constructors) в языке Typescript, то всё ждал появления аналогичной конструкции и в C#. Ведь можно тогда будет не инжектить в конструкторе каждую зависимость. 

  Как и многим коллегам, мне "зашла" новая фича 12-ой версии языка C#, однако были и противники нововведения. Один из разработчиков сетовал на неудобства и приговаривал "кто это придумал, того надо избить".


  Так ли всё плохо? Давайте подробно разбираться!
  Осторожно - лонгрид!

  Введение

  Если вспомнить историю новинок языка, то впервые первичные конструкторы в C# появились не в 12-ой версии, а ещё в 9-ой. И были они представлены как часть функциональности записей (record). Чтобы создать тип Human до версии C# 12 нужно было бы написать такой класс:

public class Human
{
  public string FirstName { get; }
  public string SecondName { get; }
  public int Age { get; }
  
  public Human(string firstName, string secondName, int age)
  {
    FirstName = firstName;
    SecondName = secondName;
    Age = age;
  }
}

  Можно отметить, что присутствует много дублирования. И это ещё небольшой класс. А теперь представьте, что у вас есть сервис с непростой бизнес-логикой, и он инкапсулирует работу множества операций: проводит валидацию данных, проверяет привилегии пользователя, обращается к другим сервисам за вспомогательными данными и т.д. В итоге в таком классе сервиса могут быть объявлены несколько readonly-полей, конструктор с параметрами и инициализация этих же полей в конструкторе, что будет выглядеть весьма ёмко. Как раз целью первичных конструкторов и является упрощение определения и уменьшение дублирования.

Способы использования параметров первичного конструктора:

  1. Инициализировать члены (или передать их базовому конструктору).
  2. Использовать прямую ссылку на параметры.

Разберём примеры использования:

1. Инициализация членов параметрами

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

public class Human(string firstName, string secondName, int age)
{
  public string FirstName { get; } = firstName;
  public string SecondName { get; } = secondName;
  public int Age { get; } = age;
}

Мы почти вдвое сократили количество кода, при этом сохранив его намерение понятным и очевидным!


Заметьте, здесь мы используем классы, но для структур всё будет так же. Аналогично можно задать и приватные поля:

public class Human(string firstName, string secondName, int age)
{
  private readonly string _firstName = firstName;
  private readonly string _secondName = secondName;
  private readonly int _age = age;

  public string FullInfo => $"{_firstName} {_secondName}, возраст {_age}";
}

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

2. Использовать прямую ссылку на параметры.

Если значения приватных полей не используются нигде, кроме свойства FullInfo, мы можем использовать первичный конструктор, чтобы ещё больше «упростить» класс:

public class Human(string firstName, string secondName, int age)
{
  // Напрямую ссылаемся на 
  // параметры первичного конструктора
  public string FullInfo => $"{firstName} {secondName}, {age}";
}

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

public class Human(string firstName, string secondName, int age)
{

  public void SetInfo(string first, string second, int newAge)
  {
    firstName = first;
    secondName = second;
    age = newAge;
  }
}

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

var man = new Human("Pavel", "Stepanov", 30);
man.SetInfo("Andrew", "Lock", 41);
Console.WriteLine(p.FullInfo); // "Andrew Lock, 41"

Теперь посмотрим подробнее, как дела обстоят "под капотом".

Отличный способ понять многие новые функции C#, особенно те, которые по сути являются синтаксическим сахаром, — использовать Sharplab.io, чтобы увидеть, как компилятор «понижает» функции до более простого C#.

В примере, где мы инициализировали приватные поля через первичный конструктор, компилятор просто сгенерировал код обычного конструктора за нас код обычного конструктора за нас.

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

Если ссылаться на параметры основного конструктора где-либо, кроме инициализатора поля или свойства, компилятор автоматически сгенерирует поля с невалидными в обычном C# именами, например:

    private string <firstName>P;

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

Добавление проверки к инициализации

Одной из общих проблем, связанных с такими функциями, как первичные конструкторы, является проблема «роста». Есть риск, что функция отлично работает в простых случаях, но как только вам нужно что-то более сложное, приходится полностью от неё отказаться.

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

using System;

public class Human(string firstName, string secondName, int? age)
{
  private readonly string _firstName = 
   firstName 
   ?? throw new ArgumentNullException(nameof(firstName));
  private readonly string _secondName = 
   IsValidName(secondName) 
   ? secondName 
   : throw new ArgumentNullException(nameof(secondName));
  private readonly int _age = age 
   ?? throw new ArgumentNullException(nameof(age));

  public string FullInfo => $"{_firstName} {_secondName}, {_age}";
    
  private static bool IsValidName(string name)
    => name is not "";
}

И вот таков будет результат компиляции результат компиляции.

Конечно, с этим кодом инициализации можно зайти далеко, и в определённый момент, вероятно, имеет смысл отказаться от первичного конструктора и использовать обычный. Стоит отметить, что вы можете добавить проверку параметров конструктора, только если вы используете их для инициализации, но не если вы ссылаетесь на них напрямую.

Теперь мы должны разобрать главное: а стоит ли их использовать? Прав ли один из моих коллег?

Лучшие варианты использования


Рассмотрим случаи, для которых первичные конструкторы хорошо подходят.

1. Базовая инициализация поля

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

public class Human(string firstName, string secondName)
{
  private readonly string _firstName = firstName;
  private readonly string _secondName = secondName;
}

Однако, даже добавление валидации уже делает код не таким очевидным:

public class Human(string firstName, string secondName)
{
  private readonly string _firstName = 
   !string.IsNullOrEmpty(firstName) 
    ? firstName 
    : throw new ArgumentException(
       "Must not be null or empty", nameof(firstName));
  /////
}

Вы не можете использовать защитные методы, доступные в .NET 7+:

ArgumentException.ThrowIfNullOrEmpty(firstName);

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

2. Инициализация в тестовом коде

Возьмем простой пример теста xunit для типа Human:

public class HumanTests(ITestOutputHelper output)
{
  [Theory]
  [InlineData("Pasha", "", 30)]
  public void Human_throws_if_null_or_empty(
   string firstName, string secondName, int age)
  {
    output.WriteLine(
     $"Testing '{firstName}', '{secondName}' and {age}");
    Assert.Throws<ArgumentException>(() => 
     new Human(firstName, secondName, age));
  }
}

Опустим тут ценность этого теста, и нужно ли нам использовать ITestOutputHelper в методе. Здесь интересно то, что добавление ITestOutputHelper в качестве параметра первичного конструктора упрощает тестовый класс. Для теста не имеет значения, хранится ли ITestOutputHelper в поле или нет, и является ли он изменяемым.

3. Внедрение зависимостей в контроллерах MVC

В ASP.NET Core вероятно использование внедрения зависимостей и сложно проверять зависимости, которые внедряются в контроллеры. Это делает их хорошими кандидатами для первичных конструкторов (можно даже использовать значения напрямую):

public class MyController(
 ILogger<MyController> logger, 
 IService service) : ControllerBase
{
  [HttpGet]
  public ActionResult<Thing> Get()
  {
    logger.LogInformation("Get the thing");
    return service.GetThing();
  }
}

Очевидно, что это коротко и ясно. Но имейте в виду, что теперь зависимости изменяемы, и им можно назначать значения в теле контроллера, даже если вы, скорее всего, никогда не захотите этого делать.

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

Тем не менее, при использовании первичных конструкторов есть вещи, на которые стоит обратить внимание. И об этом пойдёт речь далее.

Подводные камни

Замечание: описанные ниже «проблемы» не означают, что нельзя использовать первичные конструкторы, и для вас они даже могут показаться несущественными.

1. Повторный захват

Если вы инициализируете поле с помощью параметра первичного конструктора, а также используете его значение в другом члене класса, у вас будет два поля, хранящих одно и то же значение:

public class Human(string name)
{
  private string _name = name;

  public string Greeting 
   => $"Hello, {name}";
}

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

2. Неявные поля не могут быть readonly

Если вы неявно захватываете параметр первичного конструктора, компилятор генерирует поле для его хранения. Важно, что они изменяемы, т.е. параметру (и этому полю) можно задавать значения в теле класса. Если это то, что вам нужно, нет проблем. Но если поле не должно быть изменяемо, обычно стоит отметить его как readonly, что упрощает понимание назначения поля и устраняет целый класс ошибок, связанных со случайным изменением поля. Не удивлюсь, если это исправят в будущих версиях C#, но сейчас это немного раздражает. Решение в том, чтобы не использовать неявный захват, если вам нужны поля только для чтения.

3. Путаница в соглашениях об именах

Не большая проблема, но это придётся обсудить в команде на раннем этапе: какие соглашения следует использовать для параметров первичного конструктора? Использовать ли для параметров префикс _? Если вы используете неявный захват в коде класса, то там они будут по сути как приватные поля:

public class Human(string _firstName, string _secondName, int _age)
{
  public string FullInfo => $"{_firstName} {_secondName}, {_age}";
}

С другой стороны, создание экземпляра такого класса будет выглядеть немного странно:

var h = new Human(_firstName: "Pasha", _secondName: "Stepanov", _age: 30);

Microsoft рекомендует именование как параметров (без префикса). Кстати, вы не можете использовать this. в коде для обращения к параметрам первичного конструктора, такой код просто не скомпилируется.

4. Неявные поля меняют размер структур

В высокопроизводительном коде или при работе с interop иногда бывает важен размер экземпляров и как выстроено их содержимое. Если у нас есть следующая структура:

public struct Human(
 string first,
 string second,
 int age)
{
  public int Age = age;
  public string FullInfo => $"{first} {second}";
}

То мы имеем 3 поля (1 – int и 2 – ссылки на string) для неявных полей, всего 32*2 + 4 = 70 байта на экземпляр (если строки пустые). Теперь, если мы всего лишь поменяем первичный конструктор:

public struct Human(
 string name,
 int age)
{
  public int Age = age;
  public string FullInfo => name;
}

Внезапно вместо 3 полей останется 2 и размер станет 32 + 4 = 36 байт, но это без явного изменения публичных полей структуры (если вы не знаете, как реализованы первичные конструкторы). Это может иметь очень плохие последствия, если мы полагаемся на размер Human где-то в коде!

5. Путаница с записями

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

public record Human(string FirstName, string SecondName);

public class Human(string FirstName, string SecondName);

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

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

Источник:
1) https://andrewlock.net/thoughts-about-primary-constructors-3-pros-and-5-cons/

понедельник, 20 мая 2024 г.

Вредный совет по чистому коду? Бывает и такое!

Наверняка многим из вас доводилось видеть подборку Best Practices на языке C#, где тот или иной разработчик-эксперт советует применить новую технику.


Часто в таких случаях используется принцип проектирования KISS (Keep It Simple, Stupid - "Делай проще, тупица"). Данный принцип был принят в ВМС США ещё в 1960 году. Принцип KISS утверждает, что большинство систем работают лучше всего, если они остаются простыми, а не усложняются. Поэтому в области проектирования простота должна быть одной из ключевых целей и следует избегать ненужной сложности. Фраза ассоциировалась с авиаконструктором Кларенсом Джонсоном (1910—1990)[3]. В 1970-х годах широко использовался термин «KISS-принцип» (англ. KISS principle)[4]. Вариации на фразу включают (обычно как эвфемизм для более грубого «stupid»): «keep it super simple», «keep it simple, silly», «keep it short and simple», «keep it short and sweet», «keep it simple and straightforward»[5], «keep it small and simple»[6], «keep it simple, soldier», «keep it simple, sailor», «keep it simple, sweetie»[7], «keep it stupidly simple» и «keep it sweet and simple».

Но мы немного отвлеклись от темы, нужно разобрать конкретный кейс:


Может показаться, что во 2-ом случае код стал меньше, значит понятнее. Однако, это не так, ведь код стал чуть менее читабельным. Т.к. теперь приходится "в голове" коппилировать результат исполнения метода Reverse, потом ToArray() и заключительной аллокации памяти из конструктора new string.

С аллокациями разобрались, что во 2-ом случае их стало больше. А что насчёт быстродействия? 
Пока сложно ответить на этот вопрос, поэтому подготовим тестовый проект с подключенной библиотекой BenchmarkDotNet и воссоздадим условия для тестирования:

Далее откроем созданный проект в Visual Studio (или Rider) и установим пакет BenchmarkDotNet через Package Manager Console:

PM> Nuget\Install-Package BenchmarkDotNet

Теперь поставим шаблон для работы с новой библиотекой:

Также в проекте создадим класс Benchmarks, который будет содержать методы для замера производительности и выделения памяти:

В качестве тестовых данных выступают фамилия и имя автора, метод Reverse_BadExample - это "плохой" пример написания, а Reverse_CleanCode - это пример "чистого кода".
Также не забудем в главном классе программы, в методе Main прописать следующий код для запуска бенчмарков:

Запустим приложение через консоль с использованием команды:
Получаем следующие результаты:

Высказанные выше догадки подтвердились. 2-ая версия метода, основанная на принципе KISS провалилась как по "сложности восприятия", по времени исполнения, так и по аллокациям. Значит, совет оказался действительно не таким уж и действенным. Чистый код - не всегда быстрый код. Особенно когда дело касается строк.

Что ж, попробуем улучшить 2-ую версию метода. Для этого вызовем у исходной строки тот же метод Reverse, получим IEnumerable<char>, но не будем преобразовывать его в массив. Сразу передадим в метод, который принимает такой тип данных. И это string.Concat, у которого есть аж 13 перегрузок. Получится следующий метод, назовем его ReverseString_Concat:

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

Как можно заметить - выделения памяти в 3-ем методе действительно стало меньше, чем у 2-ой версии. Также ускорилось и время исполнения. Однако это всё ещё хуже 1-го варианта.
В таком случае обратимся к официальной документации по классу string и увидим, что есть метод создания строки Create, который принимает на вход длину создаваемой строки, состояние и делегат-колбэк, который будет оперировать буфером-структурой и состоянием. Документация по методу: string.Create<TState>(Int32, TState, SpanAction<Char,TState>)

Так как нам необходимо произвести перестановку символов, то длина целевой строки известна и равна исходной. В качестве начального состояния передадим значение поля InputText. А делегат будет исходное состояние строки копировать в буфер chars, после чего этот буфер можно отсортировать в обратном порядке. Должна получиться следующая реализация метода:

В результате реализация метода получилась длиннее, однако если взглянуть глобально, то вызывается всего лишь 1 метод string.Create.
Сразу же запускаем проект и смотрим результаты:


Результат превзошёл все ожидания! По выделению памяти добились отличных показателей, снизив их до минимума за счёт использования структуры Span, которая не выделяет дополнительной памяти при работе с данными. Также дополнительной памяти (если посмотреть реализацию) не выделяет и метод CopyTo, он лишь копирует ссылки на указатели без какого-либо копирования самих элементов.

Вывод: далеко не все советы по чистому коду полезны и их стоит брать на вооружение в свои проекты. Всё проверяйте и меньше ошибайтесь. Да прибудет с Вами сила!

пятница, 17 мая 2024 г.

Как обновить устаревшие версии зависимостей в солюшене в .NET


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

Если обратиться к книге "Роберта Мартина. Чистый код", то автор в главе 1 приводит очень интересный пример одной компании, которая в конце 80-х годов написала приложение-бестселлер. Приложение было настолько популярным, что его повсеместно покупали и использовали. Но в последующем итерации разработки стали затягиваться, выпуски новых версий происходили с большими задержками. Ошибки и уязвимости между версиями не исправлялись, сбои происходили всё чаще. В конечном итоге пользователи перестали использовать данное приложение, а вскоре компания обанкротилась.

В данном случае плохой код привел к краху компании. Однако в наше время к этому может привести несвоевременное обновление версий зависимостей разрабатываемого продукта.

В этой статье разберем разные способы определения устаревших пакетов и обновления их при необходимости.

Способ №1. Диспетчер пакетов в IDE (самый распространённый)

В Visual Studio или Rider можно легко узнать, доступны ли новые версии пакетов, используемых вашим проектом, с помощью диспетчера пакетов NuGet.

Если используете MS Visual Studio сделайте следующее:

1) откройте Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution… (Инструменты -> Менеджер NuGet-Пакетов -> Управлять NuGet-Пакетами в Решении…);

2) перейдите на вкладку Updates (Обновления);

3) выберите все обновления и нажмите Update (Обновить). Есть также возможность обновить их по одному;

4) перейдите на вкладку Consolidate (Консолидация). Будут показаны пакеты, установленные в разных версиях в проектах решения, и что позволит привести все установленные пакеты к одной версии.

Если используете Jetbrains Rider:

1) откройте View -> Tool Windows -> NuGet (Вид -> Окна Инструментов -> NuGet);

2) для обновления установленных пакетов, нажмите Upgrade Packages in Solution (Обновить пакеты в решении) на панели инструментов окна NuGet

3) далее необходимо выбрать, какие пакеты следует обновить. Либо можно выбрать один из установленных пакетов в левой части окна NuGet, выбрать нужную версию в правой части, а затем обновить версию пакета в конкретных проектах.

Способ №2. Утилита dotnet-outdated

Это утилита командной строки с открытым исходным кодом. Подробнее о ней: https://github.com/dotnet-outdated/dotnet-outdated

Сначала нужно установить данную утилиту, выполнив следующую команду:

Теперь можно вызвать её, выполнив команду в папке проекта или решения:

Утилита выдаст список всех доступных обновлений, выделенных цветом:

- зеленым – патчи и исправления ошибок (обратно совместимо),

- желтым – обновления минорных версий (обратно совместимо, добавлены новые функции),

- красным – обновления мажорных версий (возможны ломающие изменения).

Чтобы обновить все устаревшие пакеты, выполните команду:

Способ №3. Через команду dotnet

Также можно использовать утилиты командной строки dotnet напрямую. Чтобы получить список устаревших пакетов, выполните следующую команду:

Чтобы обновить устаревший пакет, нужно выполнить команду:

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

Если проект размещён на GitHub, не обязательно постоянно помнить о необходимости проверки обновлений. Утилиты GitHub, вроде Dependabot помогут в этом, проанализировав зависимости проектов и создавая пул-реквесты каждый раз, когда появляется новая версия какой-то из зависимостей.

Источники:

1) https://bartwullems.blogspot.com/2024/05/net-core-view-outdated-packages.html?m=1

2) Роберт Мартин. Чистый код.

четверг, 16 мая 2024 г.

Hello .NET & C# !


 Всем привет! Меня по-прежнему зовут Степанов Павел, и я наконец-то (заново) открыл свой блог (спустя почти 11 лет).

Здесь буду делиться опытом разработки, а также буду делиться полезной информацией по языку C# и платформе .NET

Приятного чтения!