Как записывать в файл при конкурентном доступе к нему в асинхронном режиме?
Проект asp.net core 5.0. Telegram webhook.
Контроллер принимает объект класса Update создает файл на сервере для этого пользователя и пишет в него данные, чтобы сохранять ответы пользователя получаемые во время диалога, после окончания диалога файл должен удалятся:
[HttpPost]
public async Task<IActionResult> Post([FromBody] Update update)
{
if (update == null) return Ok();
List<string> listlogs = new List<string>();
listlogs.Add(String.Format("Update: Id {0}; Date: {1}; UserName: {2}; Message {3};", update.Id.ToString(), update.Message.Date, update.Message.From.Username, update.Message.Text));
ReadWriteFileTxt.WriteFile(listlogs, _currentPath, "logs_TelegramBot_" + DateTime.Now.Year + "_" + DateTime.Now.Month + "_" + DateTime.Now.Day, "txt", newpath: "logs");
var message = update.Message;
string responseMessage = "";
if (message != null)
{
responseMessage = "Укажите номер в федеральном формате (+7хххххххххх)";
if (message.ReplyToMessage != null)
{
string messageNumberPhone;
switch (message.ReplyToMessage.Text)
{
case "Укажите фамилию имя отчество полностью, через пробел":
string textMessage = message.Text != null ? message.Text.Trim() : null;
if (textMessage != null && textMessage.Length > 4)//
{
string[] fio = message.Text.Split(' ');
if (fio.Length > 1)
{
List<string> fioList = new List<string>();
fioList.Add(fio[0]);
fioList.Add(fio[1]);
if(fio.Length>2) fioList.Add(fio[2]);
else fioList.Add(" ");
string nameUser = message.From.Username != null ? message.From.Username : message.From.Id.ToString();
//Запись в файл
string fullPath = ReadWriteFileTxt.WriteFile(fioList, _currentPath, nameUser + "_" + DateTime.Now.Year + "_" + DateTime.Now.Month + "_" + DateTime.Now.Day, "txt", newpath: "RegisterUsers");
await _telegramBotClient.SendTextMessageAsync(message.Chat.Id, "Укажите номер телефона в федеральном формате (7хххххххххх) для регистрации", replyMarkup: new ForceReplyMarkup { Selective = true });
}
else
{
await _telegramBotClient.SendTextMessageAsync(message.Chat.Id, "Вы не указали или указали в неверном формате ФИО. Укажите ФИО полностью через пробел textMessage", replyMarkup: new ForceReplyMarkup { Selective = true });
}
}
else
{
await _telegramBotClient.SendTextMessageAsync(message.Chat.Id, "Вы не указали или указали в неверном формате ФИО. Укажите ФИО полностью через пробел textMessage != null && textMessage.Length < 5", replyMarkup: new ForceReplyMarkup { Selective = true });
}
break;
case "Укажите номер телефона в федеральном формате (7хххххххххх) для регистрации":
messageNumberPhone = ServicePhoneNumber.LeaveOnlyNumbers(message.Text);
if (messageNumberPhone.Length < 11 || messageNumberPhone.Length > 12)
{
await _telegramBotClient.SendTextMessageAsync(message.From.Id, "Номер телефона указан в неверном формате. Ждем Ваш номер для регистрации в федеральном формате (+7хххххххххх)", ParseMode.Default, replyMarkup: new ForceReplyMarkup { Selective = true });
}
else
{
string nameUser = message.From.Username != null ? message.From.Username : message.From.Id.ToString();
string fullPath = ReadWriteFileTxt.WriteFile(messageNumberPhone, _currentPath, nameUser + "_" + DateTime.Now.Year + "_" + DateTime.Now.Month + "_" + DateTime.Now.Day, "txt", newpath: "RegisterUsers");
//Запись в файл
List<string> strRegisterList = ReadWriteFileTxt.ReadFile(fullPath);
//Удаление
if (fullPath != null)
{
ReadWriteFileTxt.DeleteFile(fullPath);
}
if (strRegisterList.Count < 4) return Ok("Были указаны не все данные. Попробуйте пройти регистрацию повторно.");
MessageRegisterUser messageRegistrationUser = new MessageRegisterUser
{
famileName = strRegisterList[0],
name = strRegisterList[1],
patronimicName = strRegisterList[2],
phoneNumber = strRegisterList[3]
};
// Отправка запроса на API др. сервиса
string jsonRequestRegistration = JsonSerializer.Serialize(messageRegistrationUser);
string jsonResponseData = await PostRequestHttpAsync(urlRequestRegistration, jsonRequestRegistration);
ResponseMessageData responseData = JsonSerializer.Deserialize<ResponseMessageData>(jsonResponseData);
await _telegramBotClient.SendTextMessageAsync(message.Chat.Id, messageRegistrationUser.name +","+ responseData.status);
}
break;
}
}
else if (message.Text.Equals(commandGet))
{
await _telegramBotClient.SendTextMessageAsync(message.From.Id, responseMessage, ParseMode.Default, replyMarkup: new ForceReplyMarkup { Selective = true });
}
else if (message.Text.Equals(commandRegistration))
{
await _telegramBotClient.SendTextMessageAsync(message.Chat.Id, "Укажите фамилию имя отчество полностью, через пробел", replyMarkup: new ForceReplyMarkup { Selective = true });
}
else
{
responseMessage = "Выберите пункт меню:";
await _telegramBotClient.SendTextMessageAsync(message.From.Id, responseMessage, replyMarkup: GetMenuButtons());
}
}
return Ok();
}
Класс записи и удаления файла:
public class ReadWriteFileTxt
{
static char[] charsToTrim = { ' ', '\n', '\r', '\'', '\'' };
public static List<string> ReadFile(string filePath)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
List<String> fileContentList = new List<string>();
StreamReader fileStream = new StreamReader(filePath, Encoding.UTF8);//Encoding.GetEncoding("Windows-1251")
while (!fileStream.EndOfStream)
{
string str = fileStream.ReadLine().Trim().Trim(charsToTrim);
if(str != null && !str.Equals("")) fileContentList.Add(str);
}
return fileContentList;
}
public static string WriteFile(List<string> listWrite, string path, string nameFile, string typeFile, string newpath)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
DirectoryInfo dirInfo = new DirectoryInfo(path);
DirectoryInfo newFolder = dirInfo.Parent.CreateSubdirectory(newpath);
if (!newFolder.Exists)
{
newFolder.Create();
}
string fullPath = String.Format(@"{0}\{1}.{2}", newFolder.FullName, nameFile, typeFile);
// This text is added only once to the file.
try
{
if (!File.Exists(fullPath))
{
using (StreamWriter writer = new StreamWriter(fullPath, false, new UTF8Encoding(false)))
{
foreach (string str in listWrite)
{
writer.WriteLine(str);
}
}
}
else
{
using (StreamWriter writer = new StreamWriter(fullPath, true, new UTF8Encoding(false)))
{
foreach (string str in listWrite)
{
writer.WriteLine(str);
}
}
}
return fullPath;
}
catch {
return fullPath;
}
}
public static string WriteFile(string str, string path, string nameFile, string typeFile, string newpath)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
DirectoryInfo dirInfo = new DirectoryInfo(path);
DirectoryInfo newFolder = dirInfo.Parent.CreateSubdirectory(newpath);
if (!newFolder.Exists)
{
newFolder.Create();
}
string fullPath = String.Format(@"{0}\{1}.{2}", newFolder.FullName, nameFile, typeFile);
// This text is added only once to the file.
try
{
if (!File.Exists(fullPath))
{
using (StreamWriter writer = new StreamWriter(fullPath, false, new UTF8Encoding(false)))
{
writer.WriteLine(str);
}
}
else
{
using (StreamWriter writer = new StreamWriter(fullPath, true, new UTF8Encoding(false)))
{
writer.WriteLine(str);
}
}
return fullPath;
}
catch {
return null;
}
}
public static void DeleteFile(string pathFull)
{
try
{
File.Delete(pathFull);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
Мне выдаёт исключения о том, что я передаю нулевой путь к файлу:
An unhandled exception has occurred while executing the request. System.ArgumentNullException: Value cannot be null. (Parameter 'path') at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize) at TLmessanger.Services.ReadWriteFileTxt.ReadFile(String filePath) in D:\My_PROGRAMS\TLmessanger\Services\ReadWriteFileTxt.cs:line 20 at TLmessanger.Controllers.TelegramListenerController.Post(Update update) in D:\My_PROGRAMS\TLmessanger\Controllers\TelegramListenerController.cs:line 270 at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
Нулевой путь у меня возвращает метод записи, если ему не удалось произвести запись в файл:
try
{
if (!File.Exists(fullPath))
{
using (StreamWriter writer = new StreamWriter(fullPath, false, new UTF8Encoding(false)))
{
writer.WriteLine(str);
}
}
else
{
using (StreamWriter writer = new StreamWriter(fullPath, true, new UTF8Encoding(false)))
{
writer.WriteLine(str);
}
}
return fullPath;
}
catch {
return null;
}
А запись не получается произвести потому что, если я правильно понял, идет конкурентный доступ к файлу - не закончилась запись предыдущего потока. Как можно это исправить?
Вариант после исправлений замечаний:
public class ReadWriteFileTxt
{
static char[] charsToTrim = { ' ', '\n', '\r', '\'', '\'' };
private static readonly object _locker = new object();
private static readonly UTF8Encoding encoder = new UTF8Encoding(false);
public static List<string> ReadFile(string filePath)
{
//Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
List<string> fileContentList = new List<string>();
StreamReader fileStream = new StreamReader(filePath, encoder);//Encoding.GetEncoding("Windows-1251")
while (!fileStream.EndOfStream)
{
string str = fileStream.ReadLine().Trim().Trim(charsToTrim);
if(str != null && !str.Equals("")) fileContentList.Add(str);
}
return fileContentList;
}
public static string WriteFile(List<string> listWrite, string path, string nameFile, string typeFile, string newpath)
{
DirectoryInfo dirInfo = new DirectoryInfo(path);
DirectoryInfo newFolder = dirInfo.Parent.CreateSubdirectory(newpath);
try
{
if (!newFolder.Exists)
{
newFolder.Create();
}
}
catch (Exception ex) { Console.WriteLine(ex.Message); }
string fullPath = Path.Combine(newFolder.FullName, $"{nameFile}.{typeFile}");
lock (_locker)
{
try
{
File.AppendAllLines(fullPath,listWrite);
return fullPath;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
}
public static string WriteFile(string str, string path, string nameFile, string typeFile, string newpath)
{
DirectoryInfo dirInfo = new DirectoryInfo(path);
DirectoryInfo newFolder = dirInfo.Parent.CreateSubdirectory(newpath);
string fullPath = Path.Combine(newFolder.FullName, $"{nameFile}.{typeFile}");
// This text is added only once to the file.
try
{
if (!newFolder.Exists)
{
newFolder.Create();
}
}
catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.Message); }
lock (_locker)
{
try
{
File.AppendAllText(fullPath, str + Environment.NewLine);
return fullPath;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
}
public static List<string[]> SplitString(List<string> stringList, char parserChar)
{
List<string[]> splitStringList = new List<string[]>(stringList.Count);
for (int i = 0; i < stringList.Count; i++)
{
string[] valueColumns = stringList[i].Split(parserChar);
splitStringList.Add(valueColumns);
}
return splitStringList;
}
public static void DeleteFile(string pathFull)
{
try
{
File.Delete(pathFull);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
Исключение ещё осталось, оно указывает на строку метода чтения - 21:
StreamReader fileStream = new StreamReader(filePath, encoder);
и строку из которой вызывается метод чтения - 270:
string fullPath = ReadWriteFileTxt.WriteFile(messageNumberPhone, _currentPath, nameUser + "_" + $"{DateTime.Now:yyyy_MM_dd}", "txt", newpath: "RegisterUsers");
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1] An unhandled exception has occurred while executing the request. System.ArgumentNullException: Value cannot be null. (Parameter 'path') at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize) at TLmessanger.Services.ReadWriteFileTxt.ReadFile(String filePath) in D:\My_PROGRAMS\TLmessanger\Services\ReadWriteFileTxt.cs:line 21 at TLmessanger.Controllers.TelegramListenerController.Post(Update update) in D:\My_PROGRAMS\TLmessanger\Controllers\TelegramListenerController.cs:line 270