امیرمسعود
امیرمسعود
خواندن ۴ دقیقه·۱ سال پیش

fire and forget database command with entity framework

در کل برای شما پیش اومده که وقتی میخواین یه کامند دیتابیسی رو اجرا کنید منتظر بموندید که اجرا شدن اون کامند تموم بشه.

برای مثال فرض کنید شما قصدا دارید که یک اطلاعات یک کاربری رو در یک جدول دیتایبس ذخیره کنید و بعد از اون یک لاگ هم به عنوان تاریخچه از این عملیات در یک جدوله دیگه ذخیره کنید و خب احتمالا روال شما برای انجام اینکار شبیه کد زیر باشه

public class UserService { private readonly IUserRepository _userRepository; private readonly IUserHistoryRepository _historyRepository; private readonly IMapper _mapper; public UserService(IUserRepository userRepository,IMapper mapper ,IUserHistoryRepository historyRepository) { _userRepository = userRepository; _mapper = mapper; _historyRepository = historyRepository; } public async Task AddUser(AddUserDto userDto) { var user = _mapper.Map<DomainClass.User.User>(userDto); await _userRepository.Insert(user); await _historyRepository.AddUserHistory(user); } }

که یک متدی هستش که یک اطلاعات کاربر رو دریافت میکنه و بعد از ذخیره کردن در جدول اصلی اون اطلاعات رو هم در یک جدول دیگه به عنوان تاریخچه ذخیره میکنه(احتمالا شما روی دیتای اصلی یک عملیات منطقی پیاده میکنید و بعد اونو به عنوان تاریخچه ذخیره میکنید اما خب اینجا به اون اشاره نشده ) کد بالاهم هیچ مشکلی نداره و کامندها خیلی قشنگ در داخل دیتابیس اجرا میشه و هم اطلاعات کاربر در جدول خودش ذخیره میشه هم به عنوان تاریخچه در جدول دیگه .

اما خب همیشه قرار نیست سناریو های ما اینقدر ساده باشه یعنی شما فرض کنید که عملیات لاگ زدن ما بنابر منطقی که ما میخوایم پیاده کنیم عملیات زمان بری باشه اون موقعه چی؟ ینی اگه عملیات لاگ ما 5 ثانیه طول بکشه ما باید 5 ثانیه منتظر بمونیم تا تموم بشه و بعدش بریم بقیه کارمون رو انجام بدیم خب صد در صد نه پس بریم ببینیم چیکار باید بکنیم که مشکل Performance برای ما پیش نیاد

راه حل!

میتونیم ما اون بخش از عملیاتی که مربوط به لاگ زدن هستش رو بیایم و در background ایجاد کنیم ( خواهشا خیلی سخت فکر نکنید در اینجا که بیایم صف درست کنیم یا از channel ها استفاده کنیم) باید کار زیر رو انجام بدیم


public class UserService { private readonly IUserRepository _userRepository; private readonly IUserHistoryRepository _historyRepository; private readonly IMapper _mapper; public UserService(IUserRepository userRepository,IMapper mapper ,IUserHistoryRepository historyRepository) { _userRepository = userRepository; _mapper = mapper; _historyRepository = historyRepository; } public async Task AddUser(AddUserDto userDto) { var user = _mapper.Map<DomainClass.User.User>(userDto); await _userRepository.Insert(user); await _userRepository.SaveChangesAsync(); await Task.Run(async () => { await _historyRepository.AddUserHistory(user); await _historyRepository.SaveChangesAsync(); }); } }

اما خب این تیکه کد بعد اجرا شدن error میده و اونم این هستش که میگه DbContext رو نمیشه تو چندتا ترد استفاده کرد یا به اصطلاح thread safe نیستش پس اینجا ما برای رفع این مشکل باید بیایم و یک dbcontext جدید بسازیم اونم به این صورت



public class UserService { private readonly IUserRepository _userRepository; private readonly IUserHistoryRepository _historyRepository; private readonly IMapper _mapper; private readonly IServiceProvider _serviceProvider; public UserService(IUserRepository userRepository,IMapper mapper ,IUserHistoryRepository historyRepository,IServiceProvider serviceProvider) { _userRepository = userRepository; _mapper = mapper; _historyRepository = historyRepository; _serviceProvider = serviceProvider; } public async Task AddUser(AddUserDto userDto) { var user = _mapper.Map<DomainClass.User.User>(userDto); await _userRepository.Insert(user); await _userRepository.SaveChangesAsync(); await Task.Run(async () => { var ctx = _serviceProvider.CreateScope().ServiceProvider.GetService<IUserHistoryRepository>(); await ctx.AddUserHistory(user); await ctx.SaveChangesAsync(); }); } }

برای اینکه ما بهخوایم یک نمونه جدید از ریپازیتوری خودمون بسازیم باید اول IServiceProvider رو در داخل کلاس خودمون inject کنیم

private readonly IServiceProvider _serviceProvider; public UserService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; }

و خب با انجام این کار ما عملکرد برنامه خودمون رو زیاد کردیم برنامه ما user رو در جدول مورد نظرمون insert میکنه و بدون اینکه منتظر بمونه لاگ user در جدول مورد نظر insert بشه .

بهترش کن!

و خب ما همیشه سعی میکنیم از IServiceScopeFactory که یک dependency injector container تو کدمون کمتر استفاده کنیم و خب استفاده از اون یک انتی پترن هستش اما خب ما در این مورد مجبور هستیم اما میتونیم یک سازوکاری برای این قضیه تهیه کنیم که بتونیم اونو محدود و از دید پنهان کنیم


public sealed class FireForget
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public FireForget(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public void Execute<IRepository>(Func<TService, Task> func)
{
Task.Run(async () =>
{
try
{
// initialize the DbContext (or other service) from the service scope
var context = _serviceScopeFactory.CreateScope().ServiceProvider.GetService<IRepository>();
// executing the function
await func(context);
}
catch (Exception e)
{
Console.WriteLine(e);
}
});
}
}

یک کلاس ساختیم به اسم FireForget که یک متد داره به اسم Execute که Generic هستش که یک type میگیره و یه نمونه سازی از اون میکنه و اون رو روی یک thread دیگه از thread pool اجرا میکنه و یک lambda function هم داره فقط یادمون باشه که باید fireforget رو به عنوان Singleton تو برنامه خودمون register کنیم

builder.Services.AddSingleton<FireForget>();

بعد از اینکه کار خودمون رو کردیم حالا باید بیایم و کدمون رو refactor کنیم

public class UserService { private readonly IUserRepository _userRepository; private readonly IUserHistoryRepository _historyRepository; private readonly IMapper _mapper; private readonly FireForget _fireForget; public UserService(IUserRepository userRepository,IMapper mapper ,IUserHistoryRepository historyRepository,FireForget _fireForget) { _userRepository = userRepository; _mapper = mapper; _historyRepository = historyRepository; } public async Task AddUser(AddUserDto userDto) { var user = _mapper.Map<DomainClass.User.User>(userDto); await _userRepository.Insert(user); await _userRepository.SaveChangesAsync(); _fireForget.Execute<IUserHistoryRepository>( async ctx => { await ctx.AddUserHistory(user); await ctx.SaveChangesAsync(); }); } }

و بدیه صورت میتونید لاگ بزنید و عملکرد سیستمون هم خوب باشه.




سخن پایانی

این اولین مقاله من در ویرگول هستش قصد دارم چیزایی که یاد میگیرم رو بیشتر با شما به اشتراک بزارم

اینم ایمیل من هستش خوشحال میشم اگه گفته ای داشته باشید میتونید با این ادرس با من درارتباط باشید.

masoudpaidar76@gamil.com

cdatabase
مهندس نرم افزار که علاقه داره هرچیزی رو که یاد میگیره با بقیه به اشتراک بزاره
شاید از این پست‌ها خوشتان بیاید