Сделать вызовы методов генерации асинхронными. Реализовать механизм отмены процесса генерации.
Транзакции
Добавить асинхронный вывод статистки по данным в БД которая будет обновляться по таймеру.
- Реализовать работу генераторов в транзакции
- Добавить checkbox, с помощью которого можно контролировать выполнять добавление в транзакции или нет
Как делать задание 1
В прошлых заданиях я уже говорил, что мы не совсем корректно вызываем генерацию. Точнее вызываем ее корректно, но она блокирует интерфейс. Сейчас попробуем сделать, чтобы блокирования не происходило.
Для этого воспользуемся возможностью запускать функции в отдельном потоке с помощью Task.Run
.
Возьмем, например, наш обработчик по добавлению котов
private void btnGenerateCats_Click(object sender, EventArgs e)
{
using (var connection = GetConnection())
{
connection.Open();
pbCatGenerator.Value = 0;
pbCatGenerator.Maximum = (int)udCount.Value;
var breeds = new Dictionary<string, TrackBar>
{
["Сибирская"] = tbSiberian,
["Бенгальская"] = tbBengalskaya,
["Сиамская"] = tbSiamskaya,
["Мейн-кун"] = tbMainKun,
["Египетская"] = tbEgipetskaya,
["Персидская"] = tbPersidkaya,
["Манчкин"] = tbManchkin,
["Шоколадный йорк"] = tbShocoladniyYork,
};
int totalBreed = breeds.Values.Sum(tb => tb.Value);
Dictionary<string, Range> breedsProbablity = new();
double previousProbabilty = 0;
foreach (var item in breeds)
{
double k = (double)breeds[item.Key].Value / totalBreed;
if (k > 0)
{
breedsProbablity[item.Key] = new Range
{
min = previousProbabilty,
max = previousProbabilty + k
};
previousProbabilty += k;
}
}
Random rnd = new();
for (var i = 0; i < udCount.Value; ++i)
{
string breed = null;
var k = rnd.NextDouble(); // бросаем случайное число от 0 до 1
foreach (var item in breedsProbablity)
{
if (item.Value.CheckContains(k))
{
breed = item.Key;
break;
}
}
if (breed == null)
continue;
var isMale = ((double)tbCatMaleFemale.Value / 100) < rnd.NextDouble();
var gender = isMale ? "муж" : "жен";
var name = GenerateCatName(gender);
SqlCommand command = new($@"
INSERT INTO Cats(name, breed)
VALUES('{name}', '{breed}')"
, connection);
command.ExecuteNonQuery();
pbCatGenerator.Value++;
}
}
}
давайте вынесем его в функцию.
// создал функцию
private void GenerateCats()
{
using (var connection = GetConnection())
{
connection.Open();
pbCatGenerator.Value = 0;
pbCatGenerator.Maximum = (int)udCount.Value;
var breeds = new Dictionary<string, TrackBar>
{
["Сибирская"] = tbSiberian,
["Бенгальская"] = tbBengalskaya,
["Сиамская"] = tbSiamskaya,
["Мейн-кун"] = tbMainKun,
["Египетская"] = tbEgipetskaya,
["Персидская"] = tbPersidkaya,
["Манчкин"] = tbManchkin,
["Шоколадный йорк"] = tbShocoladniyYork,
};
int totalBreed = breeds.Values.Sum(tb => tb.Value);
Dictionary<string, Range> breedsProbablity = new();
double previousProbabilty = 0;
foreach (var item in breeds)
{
// ...
}
Random rnd = new();
for (var i = 0; i < udCount.Value; ++i)
{
// ...
pbCatGenerator.Value++;
}
}
}
// в клике на кнопку теперь вызываем функцию
private void btnGenerateCats_Click(object sender, EventArgs e)
{
GenerateCats(); // теперь вызываем тут функцию
}
чтобы функция запустилась асинхронно ее надо обернуть в Task.Run, делается это так:
private void btnGenerateCats_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
GenerateCats();
});
}
Правда, если ее теперь попробовать запустить, то получим ошибку
собственно, проблема, что мы внутри асинхронной функции пытаемся обратится к объекту на форме, обращение к которому всегда должно быть синхронным.
Соответственно надо все операции по взаимодействию с формой надо вынести из функции. То есть своего рода подготовить данные для генерации, а потом передать их уже функции GenerateCats в качестве параметров.
Сначала вынесем функцию GetConnection() из GenerateCats, вот так:
private void GenerateCats(SqlConnection connection) // добавил параметр
{
using (connection) // тут теперь просто передаю переменную в using
{
connection.Open();
// ...
}
}
private void btnGenerateCats_Click(object sender, EventArgs e)
{
var connection = GetConnection(); // создаю соединение перед асинхронным вызовом
Task.Run(() =>
{
GenerateCats(connection); // передаю его в функцию
});
}
попробуем еще раз запустить:
теперь получаем ошибку на обнуление прогрессбара. Вынесем обнуление как и создание конекшена перед Task.Run
private void GenerateCats(SqlConnection connection)
{
using (connection)
{
connection.Open();
/* УБРАЛ
pbCatGenerator.Value = 0;
pbCatGenerator.Maximum = (int)udCount.Value;
*/
// ...
}
}
private void btnGenerateCats_Click(object sender, EventArgs e)
{
var connection = GetConnection();
// добавил
pbCatGenerator.Value = 0;
pbCatGenerator.Maximum = (int)udCount.Value;
Task.Run(() =>
{
GenerateCats(connection);
});
}
запускаем еще раз:
теперь ошибка на расчёт вероятности выбора пароды. Нам по идее все эти подсчеты тоже надо вынести из функции и просто передавать словарик в качестве результата.
private void GenerateCats(SqlConnection connection, Dictionary<string, Range> breedsProbablity) // добавил параметр
{
using (connection)
{
connection.Open();
/* УБИРАЕМ
var breeds = new Dictionary<string, TrackBar>
{
["Сибирская"] = tbSiberian,
["Бенгальская"] = tbBengalskaya,
["Сиамская"] = tbSiamskaya,
["Мейн-кун"] = tbMainKun,
["Египетская"] = tbEgipetskaya,
["Персидская"] = tbPersidkaya,
["Манчкин"] = tbManchkin,
["Шоколадный йорк"] = tbShocoladniyYork,
};
int totalBreed = breeds.Values.Sum(tb => tb.Value);
Dictionary<string, Range> breedsProbablity = new();
double previousProbabilty = 0;
foreach (var item in breeds)
{
// ...
}
*/
Random rnd = new();
for (var i = 0; i < udCount.Value; ++i)
{
// ...
pbCatGenerator.Value++;
}
}
}
private void btnGenerateCats_Click(object sender, EventArgs e)
{
var connection = GetConnection();
pbCatGenerator.Value = 0;
pbCatGenerator.Maximum = (int)udCount.Value;
// все связанное с расчетом вероятности утащил сюда
// НАЧАЛО расчета вероятности
var breeds = new Dictionary<string, TrackBar>
{
["Сибирская"] = tbSiberian,
["Бенгальская"] = tbBengalskaya,
["Сиамская"] = tbSiamskaya,
["Мейн-кун"] = tbMainKun,
["Египетская"] = tbEgipetskaya,
["Персидская"] = tbPersidkaya,
["Манчкин"] = tbManchkin,
["Шоколадный йорк"] = tbShocoladniyYork,
};
int totalBreed = breeds.Values.Sum(tb => tb.Value);
Dictionary<string, Range> breedsProbablity = new();
double previousProbabilty = 0;
foreach (var item in breeds)
{
double k = (double)breeds[item.Key].Value / totalBreed;
if (k > 0)
{
breedsProbablity[item.Key] = new Range
{
min = previousProbabilty,
max = previousProbabilty + k
};
previousProbabilty += k;
}
}
// КОНЕЦ расчета вероятности
Task.Run(() =>
{
// передаю теперь сюда breedsProbablity
GenerateCats(connection, breedsProbablity);
});
}
пробуем запустить еще раз. Очередная ошибка
В общем если сейчас глянуть на код, то остаются следующие значения которые надо вынести в параметры:
и если первые два с ними в принципе понятно, что делать, так как это просто значения, то с pbCatGenerator сложнее, так как это оповещение о прогрессе.
Но о нем позже. Как обычно создаем два новых параметра и подготавливаем значения в функции клика
private void GenerateCats(
SqlConnection connection,
Dictionary<string, Range> breedsProbablity,
int count, // добавили параметр по количество
int maleFemale // добавили параметр под долю котов/кошек
)
{
using (connection) // тут теперь просто передаю переменную в using
{
connection.Open();
Random rnd = new();
for (var i = 0; i < count; ++i) // тут теперь count использую
{
// ...
var isMale = ((double)maleFemale / 100) < rnd.NextDouble(); // тут теперь maleFemale использую
var gender = isMale ? "муж" : "жен";
var name = GenerateCatName(gender);
SqlCommand command = new($@"
INSERT INTO Cats(name, breed)
VALUES('{name}', '{breed}')"
, connection);
command.ExecuteNonQuery();
pbCatGenerator.Value++;
}
}
}
private void btnGenerateCats_Click(object sender, EventArgs e)
{
var connection = GetConnection();
pbCatGenerator.Value = 0;
pbCatGenerator.Maximum = (int)udCount.Value;
var breeds = new Dictionary<string, TrackBar>
{
// ...
};
int totalBreed = breeds.Values.Sum(tb => tb.Value);
Dictionary<string, Range> breedsProbablity = new();
double previousProbabilty = 0;
foreach (var item in breeds)
{
// ...
}
// считываю значения с формы
int count = (int)udCount.Value;
int maleFemale = (int)tbCatMaleFemale.Value;
Task.Run(() =>
{
// и передаю теперь сюда count и maleFemale
GenerateCats(connection, breedsProbablity, count, maleFemale);
});
}
запускаем:
Ура дошли до последней строки. Собственно оповещение о прогрессе.
Тут проблема в том, что мы из асинхронной функции должны обратится к интерфейсу. Специально для таких целей в C# есть специальный класс Progress, через экземпляр которого можно пробрасывать оповещения в другой поток (интерфейс всегда выполняется в главном потоке приложения). Работать с ним достаточно просто.
private void GenerateCats(
SqlConnection connection,
Dictionary<string, Range> breedsProbablity,
int count,
int maleFemale,
IProgress<int> progress // добавил поле для оповещения о прогрессе
)
{
using (connection) // тут теперь просто передаю переменную в using
{
connection.Open();
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
// УБИРАЕМ pbCatGenerator.Value++;
// добавляем вызов функции с помощью которого оповестим об изменении
// вот это самое i попадет в value лямбда-фунуции которая будет ниже
progress?.Report(i + 1);
}
}
}
private void btnGenerateCats_Click(object sender, EventArgs e)
{
// ...
// создаем экземпляр оповещателя об изменении
// передаем лямбда выражение в качестве параметра,
// которое как раз будет использоваться в качестве окошка между
// асинхронной функцией и интерфейсом
var progress = new Progress<int>(value =>
{
// меняем значение прогрессбара
pbCatGenerator.Value = value;
});
Task.Run(() =>
{
// передаю теперь сюда еще и progress
GenerateCats(connection, breedsProbablity, count, maleFemale, progress);
});
}
проверяем
ура! =)
И кстати интерфейс теперь не блокируется. В принципе мы можем даже тыкнуть на кнопку запустить дважды.
Блокируем кнопку с помощью async/await
Давайте чтобы такого не случилось будем ее блокировать.
Но сначала для наглядности запихаем в асинхронную функцию, небольшую паузу между добавлениями записей. Ну типа добавил одну запись, приостановись на 300мс.
private void GenerateCats(
SqlConnection connection,
Dictionary<string, Range> breedsProbablity,
int count,
int maleFemale,
IProgress<int> progress
)
{
using (connection) // тут теперь просто передаю переменную в using
{
connection.Open();
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
progress?.Report(i + 1);
Thread.Sleep(300); // добавил, не забудьте вверху добавить using System.Threading
}
}
}
проверяем:
как видно я могу свободно переключать вкладки, и даже второй раз запустить программу. Правда в этом случае прогрессбар начинает глючить и пытаться информировать сразу о двух процессах.
Чтобы такого не происходило надо перед запуском будем блочить кнопку. У меня кнопка называется btnGenerateCats
private void btnGenerateCats_Click(object sender, EventArgs e)
{
// ...
// блочим кнопку
btnGenerateCats.Enabled = false;
Task.Run(() =>
{
GenerateCats(connection, breedsProbablity, count, maleFemale, progress);
});
// снимаем блок с кнопки
btnGenerateCats.Enabled = true;
}
хм, чего-то не блочится
тут дело в том, что у нас код работает так
// заблочь кнопку
btnGenerateCats.Enabled = false;
// запусти процесс в отдельном потоке
Task.Run(() =>
{
GenerateCats(connection, breedsProbablity, count, maleFemale, progress);
});
// и сразу разблочь кнопку
btnGenerateCats.Enabled = true;
чтобы разблокировка кнопку произошла только после выполнения процесса в C# встроен механизм async/await, который позволяет прервать выполнение функции и вернуться к ней по завершению какого-нибудь процесса.
Делается это так.
// помечаем функцию как асинхронную добавив async
private async void btnGenerateCats_Click(object sender, EventArgs e)
{
// ...
// блокируем кнопку
btnGenerateCats.Enabled = false;
// тут добавляем ключевое слово await, это значит, что запустится генерацию
// C# выйдет из этой функции и вернется продолжать выполнять код в ней только когда генерация закончится
await Task.Run(() =>
{
GenerateCats(connection, breedsProbablity, count, maleFemale, progress);
});
// то есть теперь разблокировка сработает только после завершения генерации
btnGenerateCats.Enabled = true;
}
пробуем:
ура, кнопка заблокировалась! =)
Добавлям отмену генерации
А давайте сделаем еще лучше. Вместо блокирования кнопки, будем писать на ней отмена. И при клике на нее, прерывать генерацию.
// создаем поле под токен-отмены, по умолчанию пусть он равен null
CancellationTokenSource catGeneratorCanceletionToken = null;
private void GenerateCats(
SqlConnection connection,
Dictionary<string, Range> breedsProbablity,
int count,
int maleFemale,
IProgress<int> progress,
// добавляем специальный паарметр-токен
// с помощью которого мы сможем определить что был запрос на завершение
CancellationTokenSource cancellationToken
)
{
using (connection) // тут теперь просто передаю переменную в using
{
connection.Open();
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
progress?.Report(i + 1);
// если пришел запрос на завершение
if (cancellationToken.IsCancellationRequested)
{
return; // то выходим их функции
}
Thread.Sleep(30);
}
}
}
private async void btnGenerateCats_Click(object sender, EventArgs e)
{
// когда токен не равен нулю значит идет генерацию
if (catGeneratorCanceletionToken != null)
{
// отправляем запрос на отмену генерации
catGeneratorCanceletionToken.Cancel();
// выходим из функции
return;
}
// если токен равен нулю значит генерация не идет, создаем новый токен
catGeneratorCanceletionToken = new CancellationTokenSource();
var connection = GetConnection();
pbCatGenerator.Value = 0;
pbCatGenerator.Maximum = (int)udCount.Value;
// ...
//btnGenerateCats.Enabled = false; убрал
btnGenerateCats.Text = "Отмена"; // теперь просто меняю текст кнопки
await Task.Run(() =>
{
GenerateCats(
connection,
breedsProbablity,
count,
maleFemale,
progress,
catGeneratorCanceletionToken // передаем токен на отмену
);
});
//btnGenerateCats.Enabled = true; убрал
btnGenerateCats.Text = "Запустить"; возвращаю текст кнопки
catGeneratorCanceletionToken.Dispose(); // по завершению генерации очищаем память под токен
catGeneratorCanceletionToken = null; // и сбрасываем его в null;
}
проверяем:
красота! =)
Как делать задание 2
Добавляем на форму панельку куда будем выводить статистику, и делаем на ней лейбл lblStatistics чтобы просто в него писать данные
Добавляем timer на форму
и ставим ему свойства какие-то такие
Enabled – это чтобы таймер запустился. А интервал — это как часто мы будем опрашивать базу.
Теперь тыкаем два раза на таймер.
Добавим теперь метод для расчета статистики. Делаем его по аналогии с методами для генерации. То есть он должен принимать на вход конекшен.
// метод для генерации статистики
private void GetStatistics(SqlConnection connection)
{
using (connection)
{
connection.Open();
}
}
// метод вызывающийся каждую итерацию таймера, не забываем добавить async
private async void timer1_Tick(object sender, EventArgs e)
{
var connection = GetConnection();
await Task.Run(() =>
{
GetStatistics(connection);
});
}
Теперь мне надо сделать чтобы метод собственно эту статистику возвращал. Так как статистика может содержать кучу данных. То создадим под нее отдельный класс.
я хочу, чтобы мой метод который GetStatistics, возвращал экземпляр этого класса. А реакция на тик таймера, которая запускает этот метод асинхронно получала этот экземпляр и выводила его на форму. К счастью тут почти не надо править код. Достаточно просто написать код — вот так
// тут меняем void на Statistics
private Statistics GetStatistics(SqlConnection connection)
{
var statistics = new Statistics();
using (connection)
{
connection.Open();
}
return statistics;
}
private async void timer1_Tick(object sender, EventArgs e)
{
var connection = GetConnection();
// тут сохраняем результат асинхронного вызова в переменную
Statistics statistics = await Task.Run(() =>
{
// ну и тут return добавляем
return GetStatistics(connection);
});
}
ну теперь можно добавить немного логики для расчёта значений. Например, посчитаю количество кошек, а также отдельные значения по породам
private Statistics GetStatistics(SqlConnection connection)
{
var statistics = new Statistics();
using (connection)
{
connection.Open();
// запрашиваем количество кошек
var command = new SqlCommand("SELECT count(*) FROM cats", connection);
statistics.catsCount = (int)command.ExecuteScalar(); // сохраняем в статистку
// запрашиваем количество кошек по породам
command = new SqlCommand("SELECT breed, count(*) as count FROM cats GROUP BY breed", connection);
// создаем экземпляр словарика
statistics.catsCountsByBreed = new Dictionary<string, int>();
// ну и проходим по строкам которые вернул запрос
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
if (!reader.IsDBNull("breed")) // проверяем что порода не пустая
{
// добавляем в словарик запись, в качестве ключа название породы, в качестве значение количество
statistics.catsCountsByBreed[reader.GetString("breed")] = reader.GetInt32("count");
}
}
}
}
return statistics;
}
private async void timer1_Tick(object sender, EventArgs e)
{
var connection = GetConnection();
var statistics = await Task.Run(() =>
{
return GetStatistics(connection);
});
// тут хитро формируем строку со списком пород
var breedsInfo = String.Join(
"\n",
statistics.catsCountsByBreed
.Select(x => x.Key)
.OrderBy(x => x)
.Select(x => $" - {x}: {statistics.catsCountsByBreed[x]}")
);
// и теперь выводим на форму
lblStatistics.Text = $"Кошек и котов: {statistics.catsCount}\n"
+ $"{breedsInfo}";
}
запускаем и проверяем:
красота! =)
Как делать задание 3
Транзакция — это способ управления набором операций над объектами в базе данных как единой сущностей.
То есть, если некоторое действие требует манипуляции сразу несколькими объектами. То обычно если мы добавляем или удаляем или изменяем сразу несколько объектов и в процессе выполнения операции и что-то идет не так, то база у нас оказывается в не консистентном состоянии.
Ну то есть условно я хотел добавить дом с пятью этажами. Но во время добавления 3-го этажа у меня разорвалось соединение с базой данных и программа упала либо выкинула ошибку. В общем дом у меня теперь остается недостроенным. И в реальной жизни исправление такое ситуации может потребовать большого количества ручной работы.
Специально для таких целей были придуманы транзакции. Это такой способ выполнить набор команд как одну мета команду. С точки зрения SQL Server выглядит это примерно так.
BEGIN TRANSACTION; -- открываем транзакцию
INSERT Table(title, info) VALUES("...", "")
INSERT Table(title, info) VALUES("...", "")
-- ... куча инсертов либо каких то других манипуляций с данными
INSERT Table(title, info) VALUES("...", "")
COMMIT; -- выполняем команды которые попали в транзакцию
то есть транзакция как бы накапливает внутри себя набор изменений и выполняет их только по итогу выполнения команды COMMIT
Точнее даже не совсем так. Она их выполняет, но не выкатывает на общее обозрение пока не будет вызван COMMIT.
Например, если вы добавили новую строчку в таблицу Table
внутри транзакции, то внутри транзакции при запросе данных из таблицы Table
вы увидите эту строчку. А вот если кто-то другой попробуем запросить данные из этой таблицы, то для него новой строчки не будет существовать пока в транзакции не будет выполнена команда COMMIT;
Если пока не понятно, то сейчас разберем на реальном примере и все станет понятнее.
В общем, глянем как с этим работать в C#
Допустим я хочу, чтобы коты у меня добавлялись все кучей сразу, а не последовательно. Для этого я иду в метод генерации котов
private void GenerateCats(
SqlConnection connection,
Dictionary<string, Range> breedsProbablity,
int count,
int maleFemale,
IProgress<int> progress,
CancellationTokenSource cancellationToken
)
{
using (connection)
{
connection.Open();
var transaction = connection.BeginTransaction(); // создаю транзакцию (то есть по сути делаю BEGIN TRANASCTION)
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
SqlCommand command = new($@"
INSERT INTO Cats(name, breed)
VALUES('{name}', '{breed}')"
, connection);
// передаю транзакцию команду
// эту нужно чтобы команда попала в блок BEGIN TRANASCTION ... COMMIT
command.Transaction = transaction;
command.ExecuteNonQuery();
// ...
}
}
}
попробуем запустить
и видим, что хотя процесс создания записей идет, несмотря на это Статистика не обновляется. И более того даже по завершению процесса данные не добавляются.
Почему так происходит?
Потому что транзакцию обязательно надо закоммитить. Для этого после того как все операции который вы хотели выполнить в транзакции закончены надо добавить еще одну команду
using (connection)
{
connection.Open();
var transaction = connection.BeginTransaction();
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
SqlCommand command = new($@"
INSERT INTO Cats(name, breed)
VALUES('{name}', '{breed}')"
, connection);
command.Transaction = transaction;
command.ExecuteNonQuery();
// ...
}
transaction.Commit(); // <<< ВОТ ЭТУ, закоммитил транзакцию
}
пробуем запустить еще раз:
как видите добавление данных произошло только один раз. В момент, когда процесс генерации запустился.
Попробуем для полного понимания процесса еще выдавать информацию о количестве объектов изнутри транзакции. Как раз чтобы увидить что внутри транзакции все данные уже сохранены.
Для этого нам правда придется немного скорректировать IProgress, который мы передаем в функцию.
Короче, добавим структуру под информацию о прогрессе.
Теперь добавим на форму еще один лейбл, куда будем писать реальное количество кошек, которые висят в транзакции
теперь идем в реакцию на кнопку запустить
private async void btnGenerateCats_Click(object sender, EventArgs e)
{
// ...
// ЭТО ЗАМЕНЯЕМ
// var progress = new Progress<int>(value =>
// {
// pbCatGenerator.Value = value;
// });
// на вот это
var progress = new Progress<ProgressInfo>(progress =>
{
pbCatGenerator.Value = progress.value; // прогресс пихаем в прогресс бар
lblRealCatsCounts.Text = progress.info; // а текст пихаем в лейбл
});
// остальное не трогаем
btnGenerateCats.Text = "Отмена";
await Task.Run(() =>
{
GenerateCats(
connection,
breedsProbablity,
count,
maleFemale,
progress,
catGeneratorCanceletionToken
);
});
// ...
}
правлю метод GenerateCats, чтобы он на вход принимал IProgress<ProgressInfo>
private void GenerateCats(
SqlConnection connection,
Dictionary<string, Range> breedsProbablity,
int count,
int maleFemale,
IProgress<ProgressInfo> progress, // ТУТ ЗАМЕНИЛ на IProgress<ProgressInfo>
CancellationTokenSource cancellationToken
)
{
using (connection) // тут теперь просто передаю переменную в using
{
connection.Open();
var transaction = connection.BeginTransaction();
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
//progress?.Report(i + 1); старый прогресс убираю
// добавляю запрос чтобы узнать реальное количество кошек
command = new("SELECT count(*) FROM Cats", connection);
// опять подключаем транзакцию, так как уже новый запрос
command.Transaction = transaction;
// формирую информацию о прогрессе
var info = $"Кошек в транзакции {command.ExecuteScalar()}";
// ну и новый Report теперь так выглядит
progress?.Report(new ProgressInfo
{
value = i + 1,
info = info,
});
// ...
}
transaction.Commit();
}
}
проверяем:
то есть видно, что делая запрос изнутри транзакции у нас есть более актуальная информация о количестве кошек, но так как потенциально мы можем процесс отменить, то эти кошки никогда не будут добавлены в базу.
Проще говоря, если я буду тыкать Отмена
и запускать процесс по новой, то отсчет будет начинаться с последнего закомиченного изменения: