نظرات مطالب
روش ذخیره‌ی لاگ‌های ILogger در پایگاه داده در Blazor
یک نکته‌ی تکمیلی: نیاز به استفاده‌ی از یک Context مجزای از Context درخواست جاری، در حین لاگ کردن

اگر استثنای حاصله به علت وجود مشکلی در اعمال خود EF-Core باشد، از این لحظه به بعد نمی‌توان از Context موجود، برای ثبت کردن اطلاعاتی در بانک اطلاعاتی برنامه استفاده کرد. یعنی زمانیکه Context ای به سازنده‌ی لاگر تزریق شد:
public class DbLoggerProvider:ILoggerProvider
{
     public DbLoggerProvider(ApplicationDbContext dbContext)
این Context، دقیقا همان Context درخواست جاری برنامه است (الگوی واحد کار) و وضعیت داخلی آن به علت بروز استثناء، برای انجام اعمال بیشتری بر روی آن مناسب نیست.
برای اینکه بتوان یک Context مجزای از Context درخواست جاری را در لاگر مورد استفاده قرار داد، ابتدا باید IServiceProvider را بجای Context، به سازنده‌ی لاگر تزریق کرد و سپس با استفاده از آن، یک scoped context جدید را ایجاد کرد (مانند کاری که در اینجا شده):
var scopeFactory = _serviceProvider.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
  var scopedProvider = scope.ServiceProvider;
  using (var newDbContext = scopedProvider.GetRequiredService<ApplicationDbContext>())
  {
       // ...
  }
}
اطلاعات بیشتر
مطالب
معرفی پروژه فروشگاهی Iris Store
پروژه IrisStore، یک سیستم فروشگاهی متن باز برای راه اندازی فروشگاه‌های اینترنتی کوچک است که سورس آن را می‌توانید از آدرس زیر دریافت کنید و برای اجرای آن نیاز به VS 2015 دارید (به دلیل استفاده‌ی از قابلیت‌های جدید زبان سی‌شارپ):
 
https://github.com/MehdiSaeedifar/IrisStore
 
همچنین نمونه‌ی آنلاین آن‌را می‌توانید در فروشگاه آیریس مشاهده کنید.
 

در ادامه برخی از قابلیت‌های این سیستم را مشاهده می‌کنید:
 

جست و جو با قابلیت دسته بندی نتایج

 
به هنگام جست و جو، لیستی از موارد پیشنهادی به صورت دسته بندی شده نمایش داده می‌شود.



جست و جوی پیشرفته کالا‌ها
 
جست و جو بر اساس قیمت، گروه، کلمات کلیدی و مرتب سازی نتایج انجام می‌گیرد. همچنین نتایج جست و جو بدون رفرش شدن صفحه و به صورت AJAX ای به همراه تغییر URL صفحه صورت می‌گیرد.



نمایش نمودار تغییرات قیمت
 
امکان نمایش نمودار تغییرات قیمت کالا در بازه‌ی زمانی نیز پیش بینی شده است.

   
ویرایش اطلاعات به صورت inline
 
امکان ویرایش قیمت و تاریخ به صورت inline وجود دارد.



   

مدیریت تصاویر کالا

 
در این قسمت امکان آپلود همزمان چندین فایل به همراه پیش نمایش آن‌ها وجود دارد. همچنین امکان کشیدن و رها کردن برای تغییر ترتیب چیدمان عکس‌ها نیز مهیا است.( تصویر اول به عنوان کاور کالا در نظر گرفته می‌شود.)


   

قابلیت‌های دیگر:

 
- مدیریت تصاویر اسلایدشو و تغییر ترتیب آن‌ها از طریق کشیدن و رها کردن (drag & drop)
- تعریف برگه و تغییر ترتیب نمایش آن‌ها از طریق کشیدن و رها کردن
- امکان ارسال پست
- تعریف دسته بندی
- مدیریت کاربران
- تعریف تنظیمات سایت
- نمایش کالا و پست‌های مشابه

کارهایی که باید انجام شود:

 
- پیاده سازی سبد خرید و خرید آنلاین
 

تصویر پنل مدیریت

 

تصویر صفحه‌ی اصلی:



همچنین به راحتی می‌توان با طراحی قالب جدیدی، از این سیستم برای کاری غیر از فروشگاه اینترنتی استفاده کرد؛ سایت‌های زیر نمونه‌های آنلاین دیگری از این سیستم هستند:

- http://www.petrapars.ir
- http://www.ava-tarh.ir
  
در نهایت فهرستی از کتاب خانه‌ها و فناوری‌های استفاده شده و همچنین مقالات مرتبط با این پروژه را قرار داده‌ام.

کتابخانه‌ها و فریم ورک‌های سمت سرور:

 فناوری یا کتابخانه   توضیحات  
مقالات مرتبط
 ASP.NET MVC 5.x
 فریم ورک و موتور اصلی سایت
-ASP.NET MVC
-How to handle repeating form fields in ASP MVC
-How to dynamically (via AJAX) add new items to a bound list model, in ASP MVC.NET  
 Entity Framework 6.x
 فریم ورک دسترسی به داده
-Entity framework code-first
-Update One-to-Many Entity using DBContext 
-مدیریت اطلاعات وابسته به زمان در بانک‌های اطلاعاتی رابطه‌ای
EFSecondLevelCache
کش سطح دوم EF 6
 -بازنویسی سطح دوم کش برای Entity framework 6  
 AutoMapper
 نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار  - دوره AutoMapper
- خودکارسازی فرآیند نگاشت اشیاء در AutoMapper  
 StructureMap
 تزریق وابستگی‌ها
-EF Code First #12  
 MvcCheckBoxList
 اضافه کردن CheckBoxList  به HtmlHelper 

 DNTScheduler
 برای انجام کارهای زمان بندی شده
-انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
 Lucene.Net
 موتور جستجوی سایت  -جستجوی سریع و پیشرفته با لوسین Lucene.net  
 AspNet.Identity
 سیستم مدیریت کاربران
-اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity  
 ELMAH.MVC
 کتابخانه ثبت وقایع و خطا‌های سیستم  -معرفی ELMAH
 PagedList
 نمایش اطلاعات به صورت صفحه بندی شده

PersianDateTime
جایگزینی است برای System.DateTime برای تاریخ‌های شمسی
-PersianDateTime جایگزینی برای System.DateTime
T4MVC
تعاریف Strongly typed مسیرها 
-T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
Dynamic LINQ
نوشتن کوئری‌های LINQ به صورت رشته ای
-انتخاب پویای فیلد‌ها در LINQ
-فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC 

کتابخانه‌های جاوا اسکریپتی سمت کلاینت:
 
 فناوری یا کتابخانه  
  توضیحات     مقالات مرتبط 
 jQuery  کتاب خانه‌ی پایه جاوا اسکرپتی سایت
 -آموزش (jQuery) جی کوئری
-آموزش JQuery Plugin و مباحث پیشرفته جی کوئری  
 
 jQuery UI  ویجت‌های رابط کاربری
- نمایش رکوردها به ترتیب اولویت به کمک jQuery UI sortable در ASP.NET MVC
- jQuery UI Sortable
-Categorized search result with jQuery UI Autocomplete
- jQuery UI Slider
-rtl jQuery UI Slider
-jquery UI Sortable with table and tr width  
jQuery Validation اعتبار سنجی سمت کلاینت
-مشکل اعتبار سنجی jQuery validator در Bootstrap tabs
-نمایش خطاهای اعتبارسنجی سمت کاربر ASP.NET MVC به شکل Popover به کمک Twitter bootstrap
toastr نمایش پیام و اطلاع رسانی

PersianDatePicker یک DatePicker شمسی کم حجم 
-PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند
CKEDITOR ادیتور متن
-استفاده از ادیتور CKEditor در صفحات ASP.NET
-یکپارچه سازی CKEditor با Lightbox
Roxy Fileman مدیریت فایل ها  -افزونه مدیریت فایل‌های رایگان Roxy FileMan برای TinyMce و CkEditor  
Magnific Popup نمایش عکس‌ها به صورت پاپ آپ

Select2 تغییر شکل drop down list‌ها برای انتخاب گزینه‌ها

jqGrid v4.6 نمایش اطلاعات در قالب جدول
- آموزش jqGrid
Bootstrap Star Rating امتیاز دهی ستاره ای
-پیاده سازی امتیاز دهی ستاره‌ای به مطالب به کمک jQuery در ASP.NET MVC
jQuery File Upload Plugin آپلود فایل به صورت AJAX ای

HIGHCHARTS نمایش نمودار

jQuery Number Plugin برای فرمت کردن اعداد

X-editable ویرایش اطلاعات به صورت inline
-قابل ویرایش کننده‌ی فوق العاده x-editable ؛ قسمت اول  
bootstrap-confirmation نمایش فرم تایید در قالب popover

PathJS برای تغییر URL صفحه برای اعمال Ajax ای 
-پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC  

فریمورک‌های CSS:
 
فناوری یا کتابخانه
 توضیحات  
 مقالات مرتبط  
 Bootstrap 3.x 
 فریم ورک پایه ای css سایت
 - Bootstrap 3 RTL Theme
- Twitter Bootstrap
-سازگارسازی کلاس‌های اعتبارسنجی Twitter Bootstrap 3 با فرم‌های ASP.NET MVC 
-ساخت قالب‌های نمایشی و ادیتور دکمه سه وضعیتی سازگار با Twitter bootstrap در ASP.NET MVC
-نمایش اخطارها و پیام‌های بوت استرپ به کمک TempData در ASP.NET MVC
 AdminLTE 
 قالب مدیریت سایت
 - نسخه راستچین شده AdminLTE 2.2.1
Animate.css   انیمیشن‌های css3 سایت

Font Awesome   پک آیکون‌های برداری

Awesome Bootstrap Checkbox   زیبا سازی چک باکس ها

فونت فارسی وزیر   قلم فارسی



لطفا برای طرح سؤالات و پیشنهادات خود و جهت مدیریت بهتر آن‌ها، از قسمت اختصاصی این پروژه در سایت استفاده نمائید.
پروژه‌ها
فروشگاه IrisStore
پروژه IrisStore، یک سیستم فروشگاهی متن باز برای راه اندازی فروشگاه‌های اینترنتی کوچک است که سورس آن را می‌توانید از آدرس زیر دریافت کنید و برای اجرای آن نیاز به VS 2015 دارید: 

https://github.com/MehdiSaeedifar/IrisStore 

همچنین نمونه‌ی آنلاین آن‌را می‌توانید در فروشگاه آیریس مشاهده کنید.


در ادامه برخی از قابلیت‌های این سیستم را مشاهده می‌کنید: 
 

جست و جو با قابلیت دسته بندی نتایج
 

به هنگام جست و جو، لیستی از موارد پیشنهادی به صورت دسته بندی شده نمایش داده می‌شود. 



جست و جوی پیشرفته کالا‌ها 

جست و جو بر اساس قیمت، گروه، کلمات کلیدی و مرتب سازی نتایج انجام می‌گیرد. همچنین نتایج جست و جو بدون رفرش شدن صفحه و به صورت AJAX ای به همراه تغییر URL صفحه صورت می‌گیرد. 



نمایش نمودار تغییرات قیمت 
 
امکان نمایش نمودار تغییرات قیمت کالا در بازه‌ی زمانی نیز پیش بینی شده است. 


ویرایش اطلاعات به صورت inline 
 
امکان ویرایش قیمت و تاریخ به صورت inline وجود دارد.



 

مدیریت تصاویر کالا

  
در این قسمت امکان آپلود همزمان چندین فایل به همراه پیش نمایش آن‌ها وجود دارد. همچنین امکان کشیدن و رها کردن برای تغییر ترتیب چیدمان عکس‌ها نیز مهیا است.( تصویر اول به عنوان کاور کالا در نظر گرفته می‌شود.)


 

قابلیت‌های دیگر:

  
- مدیریت تصاویر اسلایدشو و تغییر ترتیب آن‌ها از طریق کشیدن و رها کردن (drag & drop)
- تعریف برگه و تغییر ترتیب نمایش آن‌ها از طریق کشیدن و رها کردن
- امکان ارسال پست
- تعریف دسته بندی
- مدیریت کاربران
- تعریف تنظیمات سایت
- نمایش کالا و پست‌های مشابه


تصویر پنل مدیریت


تصویر صفحه‌ی اصلی:



همچنین به راحتی می‌توان با طراحی قالب جدیدی، از این سیستم برای کاری غیر از فروشگاه اینترنتی استفاده کرد؛ سایت‌های زیر نمونه‌های آنلاین دیگری از این سیستم هستند:

http://www.petrapars.ir 
http://www.ava-tarh.ir 

در نهایت فهرستی از کتاب خانه‌ها و فناوری‌های استفاده شده و همچنین مقالات مرتبط با این پروژه را قرار داده‌ام.

کتابخانه‌ها و فریم ورک‌های سمت سرور:

 فناوری یا کتابخانه   توضیحات
مقالات مرتبط
 ASP.NET MVC 5.x 
 فریم ورک و موتور اصلی سایت
-ASP.NET MVC 
-How to handle repeating form fields in ASP MVC 
-How to dynamically (via AJAX) add new items to a bound list model, in ASP MVC.NET  
 Entity Framework 6.x 
 فریم ورک دسترسی به داده
-Entity framework code-first 
-Update One-to-Many Entity using DBContext
-مدیریت اطلاعات وابسته به زمان در بانک‌های اطلاعاتی رابطه‌ای 
EFSecondLevelCache 
کش سطح دوم EF 6
 -بازنویسی سطح دوم کش برای Entity framework 6
 AutoMapper 
 نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار  دوره AutoMapper 
خودکارسازی فرآیند نگاشت اشیاء در AutoMapper
 StructureMap 
 تزریق وابستگی‌ها 
-EF Code First #12
 MvcCheckBoxList 
 اضافه کردن CheckBoxList  به HtmlHelper

 DNTScheduler 
 برای انجام کارهای زمان بندی شده
-انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
 Lucene.Net 
 موتور جستجوی سایت  -جستجوی سریع و پیشرفته با لوسین Lucene.net
 AspNet.Identity 
 سیستم مدیریت کاربران
-اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
 ELMAH.MVC 
 کتابخانه ثبت وقایع و خطا‌های سیستم  -معرفی ELMAH
 PagedList 
 نمایش اطلاعات به صورت صفحه بندی شده

PersianDateTime 
جایگزینی است برای System.DateTime برای تاریخ‌های شمسی 
-PersianDateTime جایگزینی برای System.DateTime
T4MVC 
تعاریف Strongly typed مسیرها
-T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
Dynamic LINQ 
نوشتن کوئری‌های LINQ به صورت رشته ای
-انتخاب پویای فیلد‌ها در LINQ 
-فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC

کتابخانه‌های جاوا اسکریپتی سمت کلاینت:

 فناوری یا کتابخانه
  توضیحات     مقالات مرتبط
 jQuery  کتاب خانه‌ی پایه جاوا اسکرپتی سایت
 -آموزش (jQuery) جی کوئری 
-آموزش JQuery Plugin و مباحث پیشرفته جی کوئری

 jQuery UI  ویجت‌های رابط کاربری
نمایش رکوردها به ترتیب اولویت به کمک jQuery UI sortable در ASP.NET MVC 
jQuery UI Sortable 
-Categorized search result with jQuery UI Autocomplete 
jQuery UI Slider 
-rtl jQuery UI Slider 
-jquery UI Sortable with table and tr width
jQuery Validation اعتبار سنجی سمت کلاینت
-مشکل اعتبار سنجی jQuery validator در Bootstrap tabs 
-نمایش خطاهای اعتبارسنجی سمت کاربر ASP.NET MVC به شکل Popover به کمک Twitter bootstrap
toastr نمایش پیام و اطلاع رسانی

PersianDatePicker یک DatePicker شمسی کم حجم
-PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند
CKEDITOR ادیتور متن
-استفاده از ادیتور CKEditor در صفحات ASP.NET 
-یکپارچه سازی CKEditor با Lightbox 
Roxy Fileman مدیریت فایل ها  -افزونه مدیریت فایل‌های رایگان Roxy FileMan برای TinyMce و CkEditor  
Magnific Popup نمایش عکس‌ها به صورت پاپ آپ

Select2 تغییر شکل drop down list‌ها برای انتخاب گزینه‌ها

jqGrid v4.6 نمایش اطلاعات در قالب جدول
آموزش jqGrid
Bootstrap Star Rating امتیاز دهی ستاره ای
-پیاده سازی امتیاز دهی ستاره‌ای به مطالب به کمک jQuery در ASP.NET MVC
jQuery File Upload Plugin آپلود فایل به صورت AJAX ای

HIGHCHARTS نمایش نمودار

jQuery Number Plugin برای فرمت کردن اعداد

X-editable ویرایش اطلاعات به صورت inline
-قابل ویرایش کننده‌ی فوق العاده x-editable ؛ قسمت اول
bootstrap-confirmation نمایش فرم تایید در قالب popover

PathJS برای تغییر URL صفحه برای اعمال Ajax ای
-پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC

فریمورک‌های CSS:

فناوری یا کتابخانه 
 توضیحات
 مقالات مرتبط
 Bootstrap 3.x
 فریم ورک پایه ای css سایت
 - Bootstrap 3 RTL Theme 
Twitter Bootstrap 
-سازگارسازی کلاس‌های اعتبارسنجی Twitter Bootstrap 3 با فرم‌های ASP.NET MVC
-ساخت قالب‌های نمایشی و ادیتور دکمه سه وضعیتی سازگار با Twitter bootstrap در ASP.NET MVC 
-نمایش اخطارها و پیام‌های بوت استرپ به کمک TempData در ASP.NET MVC
 AdminLTE
 قالب مدیریت سایت
 - نسخه راستچین شده AdminLTE 2.2.1
Animate.css انیمیشن‌های css3 سایت

Font Awesome پک آیکون‌های برداری

Awesome Bootstrap Checkbox زیبا سازی چک باکس ها

فونت فارسی وزیر قلم فارسی
 

مطالب
مشکل همزمانی خواندن و به روز رسانی اطلاعات در برنامه‌های وب
فرض کنید در برنامه‌ی خود «کیف پولی» را طراحی کرده‌اید که بر اساس آن، کاربر می‌تواند خرید کند. این کیف پول، از Id کاربر و موجودی فعلی او تشکیل می‌شود:
CREATE TABLE accounts (
user_id INTEGER PRIMARY KEY,
balance INTEGER NOT NULL
);
و برای مثال موجودی فعلی کاربر 1، مقدار 300 است:
INSERT INTO accounts(user_id, balance)
VALUES (1, 300);
اکنون کوئری‌های متداول زیر را که از یک read و سپس update تشکیل شده‌اند، درنظر بگیرید:
DECLARE @amount INT;

SET @amount = (
SELECT balance
FROM accounts
WHERE user_id = 1
);

SELECT @amount as 'balance'

UPDATE accounts
SET balance =  @amount - 100
WHERE user_id = 1;

SELECT balance as 'balance after shopping'
FROM accounts
WHERE user_id = 1
- دو عمل read و سپس update صورت گرفته‌ی فوق، مربوط به یک درخواست خرید است.
- در اینجا مقدار متغیر amount در ابتدای کار، مساوی 300 است که مربوط به همان insert ابتدایی است.
- سپس از این مقدار در کوئری دومی (برای مثال حاصل از خرید شماره یک)، 100 واحد کم می‌شود (برای مثال قیمت کل خرید است).
- در این حالت نتیجه‌ی آن یا همان موجودی جدید کاربر، 200 خواهد بود.

معادل این عملیات در EF-Core چنین دستورات متداولی است:
var account1 =  context.Accounts.First(x => x.UserId == 1);
account1.Balance -= 100;
context.SaveChanges();

سؤال: اگر کوئری‌های فوق را در یک برنامه‌ی ذاتا چند ریسمانی وب، دوبار به صورت همزمان اجرا کنیم، یعنی دو عمل خرید موازی را شبیه سازی کنیم، چه اتفاقی رخ می‌دهد؟ آیا موجودی نهایی اینبار برای مثال 100 می‌شود (با فرض 300 بودن موجودی ابتدایی)؟
پاسخ خیر است! و آن‌را می‌توانید در تصویر زیر مشاهده کنید:



در اینجا برای شبیه سازی اجرای موازی دو کوئری، از دستور WAITFOR TIME استفاده شده‌است که برای برای آزمایش آن می‌توانید مقدار آن‌را به یک دقیقه بعد تنظیم کرده و سپس آن‌را در دو پنجره‌ی SQL server management studio اجرا کنید.
همانطور که مشاهده می‌کنید، با اجرای موازی این دو کوئری، یعنی دوبار خرید کردن همزمان، 100 واحد گم شده‌است ! به این مشکل همزمانی read و سپس update رخ داده، یک «race condition» گفته می‌شود و این روزها که مطالب منتشر شده‌ی از آسیب پذیری‌های برنامه‌های وب ایرانی را بررسی می‌کنم، این مورد در صدر آن‌ها قرار دارد!
علت اینجا است که عموما برنامه نویس‌ها، برنامه‌های وب را در یک تک سشن باز شده‌ی توسط مرورگر خود آزمایش می‌کنند و در این حالت، همه چیز خوب است و اعمال آن به ترتیب پیش می‌روند. اما فراموش می‌کنند که می‌توان قسمت‌های مختلف برنامه‌های وب را به صورت همزمان، موازی و چندباره نیز اجرا کرد؛ حتی اگر آن قسمت متعلق به یک کاربر باشد.


سؤال: آیا استفاده تراکنش‌ها این مشکل را حل نمی‌کنند؟!

عموما برنامه نویس‌ها تصور می‌کنند که می‌توانند تمام اینگونه مشکلات را با تراکنش‌ها حل کنند:



همانطور که مشاهده می‌کنید، اینبار هرچند هر دو عملیات خرید داخل BEGIN TRAN و COMMIT TRAN قرار گرفته‌اند، اما ... مشکل همزمانی هنوز پابرجا است! چون نوع پیش‌فرض تراکنش مورد استفاده، READ COMMITTED isolation level است و عدم دقت به آن ممکن است این تصور را ایجاد کند که با تعریف تراکنش‌ها، تمام مشکلات همزمانی برطرف می‌شوند.


راه‌حل‌های پیشنهادی جهت حل مشکل همزمانی عملیات read/update

برای حل مشکلات مرتبط با race condition و همزمانی درخواست‌های read/update، می‌توان از یکی از روش‌های زیر استفاده کرد:
الف) بجای اینکه یکبار کوئری read و یکبار کوئری update به صورت جداگانه صادر شوند، فقط یکبار کوئری update داشته باشیم.
ب) پیاده سازی Row level locking؛ در صورت پشتیبانی بانک اطلاعاتی مورد استفاده از آن
ج) استفاده از تراکنش‌هایی از نوع SERIALIZABLE
د) پیاده سازی optimistic locking

این موارد را در ادامه با توضیحات بیشتری بررسی می‌کنیم.


الف) پرهیز از خواندن و به روز رسانی جداگانه

بجای اینکه مانند اعمال فوق، یکبار select داشته باشیم و یکبار  update، بهتر است فقط یک دستور update بکارگرفته شود:
UPDATE accounts
SET balance =  balance - 100
WHERE user_id = 1;


اینبار با خلاصه شدن دو دستور select و update به یک دستور update، دیگر پس از دو خرید همزمان، 100 واحد گم شده مشاهده نمی‌شود (!) و موجودی نهایی صحیح است.


ب) پیاده سازی Row level locking

همیشه امکان تغییر عملیات مورد نیاز، به سادگی حالت الف نیست. در یک چنین حالت‌هایی جهت حداقل شدن تغییرات مورد نیاز، می‌توان از row level locking استفاده کرد:
WAITFOR TIME '13:47:00';

SET NOCOUNT, XACT_ABORT ON;

BEGIN TRAN;

DECLARE @amount INT;

SET @amount = (
 SELECT balance
 FROM accounts WITH (UPDLOCK, HOLDLOCK)
 WHERE user_id = 1
 );

SELECT @amount as 'initial user''s balance'

UPDATE accounts
SET balance =  @amount - 100
WHERE user_id = 1;

SELECT balance as 'user''s balance after shopping 1'
FROM accounts
WHERE user_id = 1;

COMMIT TRAN;



در اینجا اضافه شدن WITH (UPDLOCK, HOLDLOCK) را به Select تعریف شده، مشاهده می‌کنید که به آن‌ها locking hints هم گفته می‌شود و داخل BEGIN TRAN و COMMIT TRAN عمل می‌کنند (که نوع پیش‌فرض آن READ COMMITTED isolation level است). کار UPDLOCK، تبدیل shared lock پیش‌فرض، به update lock است و کار HOLDLOCK، نگه داشتن قفل صورت گرفته تا پایان کار تراکنش تعریف شده‌است.
با این تغییرات، هر تراکنش همزمان دیگری، تا زمانیکه قفل صورت گرفته‌ی بر روی ردیف select، رها نشود (یعنی تا زمانیکه تراکنش قفل کننده، به COMMIT TRAN برسد)، نمی‌تواند آن‌را تغییر دهد. به همین جهت است که در تصویر فوق، هرچند هر دو عملیات همزمان اجرا شده‌اند، اما یکی موجودی ابتدایی 300 را می‌بیند و دیگری پس از صبر کردن تا پایان تراکنش و رها شدن قفل، موجودی تغییر یافته‌ی جدیدی را مشاهده کرده و از آن استفاده می‌کند. به این ترتیب دیگر 100 واحدی که در اولین تصویر این مطلب مشاهده کردید، گم نشده‌است.


ج) استفاده از تراکنش‌هایی از نوع SERIALIZABLE

بجای استفاده از روش row level locking یاد شده، روش دیگری را که می‌توان استفاده کرد، تغییر نوع پیش‌فرض تراکنش مورد استفاده‌است. برای مثال اگر از یک SERIALIZABLE transaction استفاده کنیم؛ یعنی SET TRANSACTION ISOLATION LEVEL SERIALIZABLE  را در ابتدای کار ذکر کنیم و برای مثال دو تراکنش همزمان را اجرا کنیم، اگر در تراکنش اول اطلاعاتی خوانده شود، در هیچ تراکنش دیگری نمی‌توان این اطلاعات خوانده شده را تا پایان کار تراکنش اول، تغییر داد:




د) پیاده سازی optimistic locking

پیاده سازی optimistic locking و یا Optimistic concurrency control عموما در سمت برنامه رخ می‌دهد و توسط ORMها زیاد مورد استفاده قرار می‌گیرد؛ مانند اضافه کردن ستون اضافی version و یا timestamp به جداول تعریف شده. در این حالت تمام updateها به همراه یک where اضافی هستند تا بررسی کنند که آیا version دریافتی در حین خواندن ردیف در حال به روز رسانی، تغییر کرده‌است یا خیر؟ اگر تغییر کرده‌است، تراکنش را با خطایی خاتمه خواهند داد. این روش برخلاف حالت‌های ب و ج، حتی خارج از یک تراکنش نیز کار می‌کند و مشکلات قفل کردن طولانی مدت رکوردها توسط آن‌ها را به همراه ندارد.
مطالب
ایندکس منحصر به فرد با استفاده از Data Annotation در EF Code First
در حال حاضر امکان خاصی برای ایجاد ایندکس منحصر به فرد در EF First Code وجود ندارد, برای این کار راه‌های زیادی وجود دارد مانند پست قبلی آقای نصیری, در این آموزش از Data Annotation و یا همان Attribute  هایی که بالای Property‌های مدل‌ها قرار می‌دهیم, مانند کد زیر : 
public class User
    {
        public int Id { get; set; }

        [Unique]
        public string Email { get; set; }

        [Unique("MyUniqueIndex",UniqueIndexOrder.ASC)]
        public string Username { get; set; }

        [Unique(UniqueIndexOrder.DESC)]
        public string PersonalCode{ get; set; }

        public string Password { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

همانطور که در کد بالا می‌بینید با استفاده از Attribute Unique ایندکس منحصر به فرد آن در دیتابیس ساخته خواهد شد.
ابتدا یک کلاس برای Attribute Unique به صورت زیر ایحاد کنید : 
using System;

namespace SampleUniqueIndex
{
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public class UniqueAttribute : Attribute
    {
        public UniqueAttribute(UniqueIndexOrder order = UniqueIndexOrder.ASC) {
            Order = order;
        }
        public UniqueAttribute(string indexName,UniqueIndexOrder order = UniqueIndexOrder.ASC)
        {
            IndexName = indexName;
            Order = order;
        }
        public string IndexName { get; private set; }
        public UniqueIndexOrder Order { get; set; }
    }

    public enum UniqueIndexOrder
    {
        ASC,
        DESC
    }
}
در کد بالا یک Enum برای مرتب سازی ایندکس به دو صورت صعودی و نزولی قرار دارد, همانند کد ابتدای آموزش که مشاهده می‌کنید امکان تعریف این Attribute به سه صورت امکان دارد که به صورت زیر می‌باشد:
1. ایجاد Attribute بدون هیچ پارامتری که در این صورت نام ایندکس با استفاده از نام جدول و آن فیلد ساخته خواهد شد :  IX_Unique_TableName_FieldName و مرتب ساری آن به صورت صعودی می‌باشد.
2.نامی برای ایندکس انتخاب کنید تا با آن نام در دیتابیس ذخبره شود, در این حالت مرتب سازی آن هم به صورت صعودی می‌باشد.
3. در حالت سوم شما ضمن وارد کردن نام ایندکس مرتب سازی آن را نیز وارد می‌کنید.
بعد از کلاس Attribute حالا نوبت به کلاس اصلی میرسد که به صورت زیر می‌باشد:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
using System.Linq;
using System.Reflection;

namespace SampleUniqueIndex
{
    public static class DbContextExtention
    {
        private static BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;

        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
                foreach (var uniqueProp in GetUniqueProperties(context, entityType, table))
                {
                    var indexName = string.IsNullOrWhiteSpace(uniqueProp.IndexName) ?
                        "IX_Unique_" + uniqueProp.TableName + "_" + uniqueProp.FieldName :
                        uniqueProp.IndexName;

                    if (!currentIndexes.Contains(indexName))
                    {
                        query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] ADD CONSTRAINT [" + indexName + "] UNIQUE ([" + uniqueProp.FieldName + "] " + uniqueProp.Order + "); ";
                    }
                    else
                    {
                        currentIndexes.Remove(indexName);
                    }
                }
                foreach (var index in currentIndexes)
                {
                    query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] DROP CONSTRAINT " + index + "; ";
                }
            }

            if (query.Length > 0)
                context.Database.ExecuteSqlCommand(query);
        }

        private static List<string> GetCurrentUniqueIndexes(DbContext context, string tableName)
        {
            var sql = "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS where table_name = '"
                      + tableName + "' and CONSTRAINT_TYPE = 'UNIQUE'";
            var result = context.Database.SqlQuery<string>(sql).ToList();
            return result;
        }
        private static IEnumerable<PropertyDescriptor> GetDbSets(DbContext context)
        {
            foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(context))
            {
                var notMapped = prop.GetType().GetCustomAttributes(typeof(NotMappedAttribute),true);
                if (prop.PropertyType.Name == typeof(DbSet<>).Name && notMapped.Length == 0)
                    yield return prop;
            }
        }
        private static List<UniqueProperty> GetUniqueProperties(DbContext context, Type entity, TableInfo tableInfo)
        {
            var indexedProperties = new List<UniqueProperty>();
            var properties = entity.GetProperties(PublicInstance);
            var tableName = tableInfo.TableName;
            foreach (var prop in properties)
            {
                if (!prop.PropertyType.IsValueType && prop.PropertyType != typeof(string)) continue;

                UniqueAttribute[] uniqueAttributes = (UniqueAttribute[])prop.GetCustomAttributes(typeof(UniqueAttribute), true);
                NotMappedAttribute[] notMappedAttributes = (NotMappedAttribute[])prop.GetCustomAttributes(typeof(NotMappedAttribute), true);
                if (uniqueAttributes.Length > 0 && notMappedAttributes.Length == 0)
                {
                    var fieldName = GetFieldName(context, entity, prop.Name);
                    if (fieldName != null)
                    {
                        indexedProperties.Add(new UniqueProperty
                        {
                            TableName = tableName,
                            IndexName = uniqueAttributes[0].IndexName,
                            FieldName = fieldName,
                            Order = uniqueAttributes[0].Order.ToString()
                        });
                    }
                }
            }
            return indexedProperties;
        }
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }

        internal class UniqueProperty
        {
            public string TableName { get; set; }
            public string FieldName { get; set; }
            public string IndexName { get; set; }
            public string Order { get; set; }
        }
        internal class TableInfo
        {
            public string EntityName { get; set; }
            public string TableName { get; set; }
            public string TableSchema { get; set; }
        }
    }
}
در کد بالا با استفاده از Extension Method برای کلاس DbContext یک متد با نام ExecuteUniqueIndexes  ایجاد می‌کنیم تا برای ایجاد ایندکس‌ها در دیتابیس از آن استفاده کنیم.
روند اجرای کلاس بالا به صورت زیر می‌باشد:
در ابتدای متد ()ExecuteUniqueIndexes  :
 public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            ...
        }
با استفاده از متد ()GetTables ما تمام جداول ساخته توسط دیتایس توسط DbContext را گرفنه:
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
با استفاده از این طریق چنانچه کاربر نام دیگری برای هر جدول در نظر بگیرد مشکلی ایجاد نمی‌شود و همینطور Schema جدول نیز گرفته می‌شود, سه مشخصه نام مدل و نام جدول و Schema جدول در کلاس TableInfo قرار داده می‌شود و در انتها تمام جداول در یک Collection قرار داده میشوند و به عنوان خروجی متد استفاده می‌شوند.
بعد از آنکه نام جداول متناظر با نام مدل آنها را در اختیار داریم نوبت به گرفتن تمام DbSet‌ها در DbContext می‌باشد که با استفاده از متد ()GetDbSets :
public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
            ....
        }
در این متد چنانچه Property دارای Attribute NotMapped باشد در لیست خروجی متد قرار داده نمی‌شود. 
سپس داخل چرخه DbSet‌ها نوبت به گرفتن ایندکس‌های موجود با استفاده از متد ()GetCurrentUniqueIndexes برای این مدل می‌باشد تا از ایجاد دوباره آن جلوگیری شود و البته اگر ایندکس هایی را در مدل تعربف نکرده باشید از دیتابیس حذف شوند.
        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            ...
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
            }
        }
بعد از آن نوبت به گرفتن Property‌های دارای Attribute Unique می‌باشد که این کار نیز با استفاده از متد ()GetUniqueProperties انجام خواهد شد.
در متد ()GetUniqueProperties چند شرط بررسی خواهد شد از جمله اینکه Property از نوع Value Type باشد و نه یک کلاس سپس Attribute NotMapped را نداشته باشد و بعد از آن می‌بایست نام متناظر با آن Property را در دیتابیس به دست بیاریم برای این کار از متد ()GetFieldName استفاده می‌کنیم:
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }
برای این کار با استفاده از MetadataWorkspace در DbContext دو لیست SSpace و OSpace استفاده می‌کنیم که در ادامه در مورد این گونه لیست ها بیشتر توضیح می‌دهیم , سپس با استفاده از Member‌های این دو لیست و ایندکس‌های متناظر در این دو لیست نام متناظر با Property را در دیتابیس پیدا خواهیم کرد, البته یک نکته مهم هست چنانچه برای فیلد‌های دیتابیس OrderColumn قرار داده باشید دو لیست Member‌ها از نظر ایندکس متناظر متفاوت خواهند شد پس در نتیجه ایندکس به اشتباه برروی یک فیلد دیگر اعمال خواهد شد.
لیست‌ها در MetadataWorkspace:
1. CSpace : این لیست شامل آبجکت‌های Conceptual از مدل‌های شما می‌باشد تا برای Mapping دیتابیس با مدل‌های شما مانند مبدلی این بین عمل کند.
2. OSpace : این لیست شامل آبجکت‌های مدل‌های شما می‌باشد.
3. SSpace : این لیست نیز شامل آبجکت‌های مربوط به دیتابیس از مدل‌های شما می‌باشد
4. CSSpace : این لیست شامل تنظیمات Mapping بین دو لیست SSpace و CSpace می‌باشد.
5. OCSpace : این لیست شامل تنظیمات Mapping بین دو لیست OSpace و CSpace می‌باشد.
روند Mapping مدل‌های شما از OSpace شروع شده و به SSpace ختم میشود که سه لیست دیگز شامل تنظیماتی برای این کار می‌باشند.
و حالا در متد اصلی ()ExecuteUniqueIndexes ما کوئری مورد نیاز برای ساخت ایندکس‌ها را ساخته ایم.

حال برای استفاده از متد()ExecuteUniqueIndexes می‌بایست در متد Seed آن را صدا بزنیم تا کار ساخت ایندکس‌ها شروع شود، مانند کد زیر:
protected override void Seed(myDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
            context.ExecuteUniqueIndexes();
        }
چند نکته برای ایجاد ایندکس منحصر به فرد وجود دارد که در زیر به آنها اشاره می‌کنیم:
1. فیلد‌های متنی باید حداکثر تا 350 کاراکتر باشند تا ایندکس اعمال شود.
2. همانطور که بالاتر اشاره شد برای فیلد‌های دیتابیس OrderColumn اعمال نکنید که علت آن در بالا توضیح داده شد

دانلود فایل پروژه:
Sample_UniqueIndex.zip
نظرات مطالب
Repository ها روی UnitOfWork ایده خوبی نیستند
پیشنهاد بنده برای فرار از نوشتن کدهای تکراری CRUD استفاده از یک سرویس جنریک در پروژه می‌باشد که در کمترین حالتش می‌تونه عملیات Insert و Update و  Delete    رو انجام بده و در متد Select نیز حداقل یک لیست از کل رکوردها خروجی بده.

بدین صورت لایه سرویس یک همچین شکلی میشه :
IGenericService<T>
GenericService<T> : IGenericService

IUserService : IGenericService<User>
UserService : GenericService<User>, IUserService
حال آنکه متدهای Add,Delete,GetAll,Update و بصورت Virtual ایجاد می‌نماییم تا در صورت نیاز (معمولا نیاز خواهیم داشت که متد Update رو در برخی موارد Override کنیم) بتونیم درون کلاس‌ها متدهای را Override کنیم.
بنده این تکنیک در پروژه عملی تست کرده ام و مشکلی نداشته ام
نظرات مطالب
کارهایی جهت بالابردن کارآیی Entity Framework #2
یک همچین کدهایی هم باعث ایجاد دوباره پلن مربوط به کوئری خواهند شد:
using (var context = new MyContext())
{
    var myObject = new NonMappedType();

    var query = from entity in context.MyEntities
                where entity.Name.StartsWith(myObject.MyProperty)
                select entity;
 
   var results = query.ToList();
    ...
}
معادل بهینه شده کد بالا:
using (var context = new MyContext())
{
    var myObject = new NonMappedType();
    var myValue = myObject.MyProperty;
    var query = from entity in context.MyEntities
                where entity.Name.StartsWith(myValue)
                select entity;

    var results = query.ToList();
    ...
}
در پروژه Decision خیلی از این نوع موارد پیاده سازی شده است که میبایست اصلاح شوند. برای مثال متد های GetPagedListAsync.
نظرات مطالب
اجرای وظایف زمان بندی شده با Quartz.NET - قسمت اول
سلام.
من یه وظیفه نوشتم که می‌یاد هر 1 دقیقه 1 بار یه متن رو در Table درج می‌کنه.
این کد کلاس
using Quartz;
using System.Data;
using System.Data.OleDb;
using System.Configuration;


namespace WebApplication1
{
    
    public class TestQuartzClass:IJob
    {
        
       
        public void Execute(IJobExecutionContext context)
        {
            //Page myPage = (Page)HttpContext.Current.Handler;
            //TextBox MyTextBox=(TextBox)myPage.FindControl("txt");
            
            string sql = "Insert into tbl_Test (Content) values (@Content)";
            ExecuteNoneQuery(System.Data.CommandType.Text, sql, new OleDbParameter[]{
                //new OleDbParameter("@Content", MyTextBox.Text)
                new OleDbParameter("@Content","Hello world!")
            });

        }

        public int ExecuteNoneQuery(CommandType commandType, string commandText, params OleDbParameter[] commandParameters)
        {
            using (OleDbConnection con = new OleDbConnection(ConfigurationManager.ConnectionStrings["ConStr"].ConnectionString))
            {
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = con;
                cmd.CommandType = commandType;
                cmd.CommandText = commandText;
                cmd.Parameters.AddRange(commandParameters);
                con.Open();
                int retVal = cmd.ExecuteNonQuery();
                con.Close();
                return retVal;
            }
        }
    }


}



و این هم کد صفحه ای که با کلیک دکمه وظیفه شروع به کار می‌کنه

using System;
using Quartz;
using Quartz.Impl;

namespace WebApplication1
{
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        public static void ConfigureQuartzJobs()
        {
            // construct a scheduler factory
            ISchedulerFactory schedFact = new StdSchedulerFactory();

            // get a scheduler
            IScheduler sched = schedFact.GetScheduler();
            sched.Start();
            IJobDetail job = JobBuilder.Create<TestQuartzClass>()
                .WithIdentity("SendJob")
                .Build();
            var trigger = TriggerBuilder.Create()
                .WithIdentity("SendTrigger")
                .WithSimpleSchedule(x => x.WithIntervalInMinutes(1).RepeatForever())
                //.StartAt(startTime)
                .StartNow()
                .Build();

            sched.ScheduleJob(job, trigger);
        }

        protected void btn_Click(object sender, EventArgs e)
        {
            ConfigureQuartzJobs();
        }
    }
}

همونطور که می‌بینید متن رو به شکل زیر پاس دادم.
new OleDbParameter("@Content","Hello world!")
ولی من می‌خوام این متن رو از TextBox بگیرم.
و برای اینکه در کلاس بتونم به کنترلهای صفحه دسترسی داشته باشم کدهای زیر رو به متد Exceute اضافه کردم
Page myPage = (Page)HttpContext.Current.Handler;
            TextBox MyTextBox=(TextBox)myPage.FindControl("txt");

ولی به محض اینکه این کدها رو اضافه می‌کنم دیگه برنامه کار نمی‌کنه. 

متن داخل تکست باکس رو هم قصد داشتم به شکل زیر پاس بدم.
new OleDbParameter("@Content", MyTextBox.Text)

لطفاً راهنمایی کنید.
من یک نمونه هم به منظور تست آماده کردم که از لینک زیر می‌تونید دانلود کنید:
http://www.4shared.com/rar/1Fu_jpOOba/WebApplication1.html 
نظرات مطالب
EF Code First #12
خیر. به ازای هر SaveChanges یک تراکنش خاتمه یافته و تراکنش جدیدی آغاز می‌شود (این موارد رو می‌تونید با SQL Server Profiler دقیقا مشاهده کنید). 
+ ضرورتی ندارد در یک تراکنش، از چندین و چند SaveChanges استفاده کنید؛ از این جهت که EF از مکانیزم Tracking برخوردار است و می‌تواند با یک SaveChanges ، چندین و چند عملیات insert و update را (بهینه‌ترین حالتی را که محاسبه کرده) با هم در طی یک تراکنش بر اساس مواردی که ردیابی کرده، انجام دهد.  

پاسخ به بازخورد‌های پروژه‌ها
ثبت رکورد جدید به جای بروزرسانی آن
متوجه هستم که منشأ این حذف از کجاست، اما سوال من این است که چگونه میتوان بدون اینکار، یعنی حذف کردن و اضافه کردن مجدد لینکها در دیتابیس؛ بروز رسانی را انجام داد، تا در مواردی که تعداد و حجم این رکوردها بالاست، کارایی پایین نیاید؟(اگر تغییری در لینکها رخ نداده؛ اتفاقی نیافتد و چنانچه لینکی عوض شده، رکورد مربوطه درصورتی که از قبل در DB وجود داشته، ,Update شود و تنها اگر جدید است Insert شود؟)