DbContext pooling در EF Core 2.0
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

روش متداول تنظیمات EF Core در برنامه‌های ASP.NET Core، به صورت معرفی یک DbContext سفارشی، به سیستم تزریق وابستگی‌های آن است و سپس می‌توان به وهله‌ای از این Context، توسط تزریق آن به سازنده‌های کلاس‌های مختلف برنامه، دسترسی یافت. به این معنا که به ازای هر درخواست رسیده، یک وهله‌ی جدید از DbContext ایجاد خواهد شد. در نگارش 2، روش جدیدی برای ثبت DbContext برنامه معرفی شده‌است که در صورت بکارگیری آن، بجای وهله سازی مجدد Contextها، ابتدا استخر موجود Contextها بررسی می‌شود و در صورت مهیا بودن نمونه‌ای، بجای نمونه سازی از صفر آن، از این نمونه‌ی موجود، استفاده‌ی مجدد خواهد شد. در پایان کار درخواست، تنها وضعیت این Context به حالت اولیه برگردانده شده و سپس به استخر Contextها برای استفاده‌ی مجدد بازگشت داده می‌شود. این مفهوم درحقیقت پیاده سازی مفهوم connection pooling موجود در ADO.NET است. به این ترتیب هزینه‌ی ساخت و ایجاد اتصالات به بانک اطلاعاتی به شدت کاهش خواهد یافت.


نحوه‌ی معرفی DbContext pooling

اینبار بجای روش قبلی و استفاده از متد AddDbContext
services.AddDbContext<BloggingContext>(
   options => options.UseSqlServer(connectionString));
از متد جدید AddDbContextPool استفاده می‌شود:
services.AddDbContextPool<BloggingContext>(
   options => options.UseSqlServer(connectionString));


محدودیت‌های روش DbContext pooling

در حالت استفاده‌ی از روش AddDbContextPool، دیگر متد OnConfiguring کلاس Context سفارشی شما فراخوانی نخواهد شد. بنابراین تمام تنظیمات ابتدایی برنامه را باید به همان کلاس آغازین برنامه منتقل کنید و کلاس Context، این تنظیمات را به صورت ذیل از طریق سازنده‌ی آن دریافت می‌کند:
public class BloggingContext : DbContext
{
   public BloggingContext(DbContextOptions<BloggingContext> options) : base(options){}

همچنین باید درنظر داشت که استفاده‌ی مجدد از یک Context به معنای حفظ مقادیر فیلدهای private کلاس Context سفارشی شما نیز می‌شود. در اینجا پس از پایان هر درخواست، تنها وضعیت Context از دیدگاه EF به حالت اول بازگشت داده می‌شود؛ اما حالت شیء Context و تمام اطلاعات فیلدهای خصوصی آن در همان حالت قبلی (و همان وهله‌ی موجود پیشین و اصلی) رها می‌شوند. چون وهله سازی مجددی از آن صورت نخواهد گرفت.


یک مثال: بررسی بهبود کارآیی برنامه در حالت استفاده‌ی از DbContext pooling

کدهای کامل این مثال را برای اجرا می‌توانید از اینجا دریافت کنید: ContextPooling.zip

در اینجا یکبار حالت متداول AddDbContext
        public static void RunWithoutContextPooling()
        {
            Console.WriteLine("\nRun Without ContextPooling");
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkSqlServer()
                .AddDbContext<BloggingContext>(
                    c => c.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ContextPooling;Trusted_Connection=True;ConnectRetryCount=0;"))
                .BuildServiceProvider();

            new RunTests().Start(serviceProvider);
        }
و سپس روش جدید AddDbContextPool
        public static void RunWithContextPooling()
        {
            Console.WriteLine("\nRun With ContextPooling");
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkSqlServer()
                .AddDbContextPool<BloggingContext>(
                    c => c.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ContextPooling;Trusted_Connection=True;ConnectRetryCount=0;"),
                    poolSize: 16)
                .BuildServiceProvider();

            new RunTests().Start(serviceProvider);
        }
بررسی و اجرا شده‌اند. نتیجه‌ی نهایی به صورت ذیل است:
Run Without ContextPooling
[10:49:30.728] Context creations: 637 | Requests per second: 597
[10:49:31.746] Context creations: 1069 | Requests per second: 1050
[10:49:32.765] Context creations: 1088 | Requests per second: 1067
[10:49:33.784] Context creations: 1139 | Requests per second: 1119
[10:49:34.802] Context creations: 1138 | Requests per second: 1117
[10:49:35.831] Context creations: 1153 | Requests per second: 1120
[10:49:36.845] Context creations: 1126 | Requests per second: 1111
[10:49:37.873] Context creations: 1014 | Requests per second: 987
[10:49:38.898] Context creations: 1139 | Requests per second: 1111
[10:49:39.918] Context creations: 1086 | Requests per second: 1065

Total context creations: 10592
Requests per second:     1034

Run With ContextPooling
[10:49:40.982] Context creations: 32 | Requests per second: 1388
[10:49:41.991] Context creations: 0 | Requests per second: 1691
[10:49:43.014] Context creations: 0 | Requests per second: 1684
[10:49:44.031] Context creations: 0 | Requests per second: 1702
[10:49:45.049] Context creations: 0 | Requests per second: 1694
[10:49:46.067] Context creations: 0 | Requests per second: 1401
[10:49:47.075] Context creations: 0 | Requests per second: 1510
[10:49:48.107] Context creations: 0 | Requests per second: 1669
[10:49:49.127] Context creations: 0 | Requests per second: 1679
[10:49:50.147] Context creations: 0 | Requests per second: 1688

Total context creations: 32
Requests per second:     1610
همانطور که ملاحظه می‌کنید، در حالت ContextPooling، تعداد وهله سازی‌های صورت گرفته به شدت کاهش یافته‌است و همچنین قابلیت پاسخ‌دهی برنامه به علت کاهش سربار اتصال به بانک اطلاعاتی نیز حدود 55 درصد بهبود یافته‌است.
  • #
    ‫۷ سال قبل، سه‌شنبه ۱۴ شهریور ۱۳۹۶، ساعت ۰۵:۳۴
    با سلام و احترام
    ظاهرا از این روش فقط زمانی میشه استفاده کرد که کلاس Context برنامه فقط یک سازنده با پارامتر ورودی DbContextOptions داشته باشه.آیا راه حلی برای این موضوع هست ؟
    • #
      ‫۷ سال قبل، سه‌شنبه ۱۴ شهریور ۱۳۹۶، ساعت ۱۵:۵۱
      «... همچنین باید درنظر داشت که استفاده‌ی مجدد از یک Context به معنای حفظ مقادیر فیلدهای private کلاس Context سفارشی شما نیز می‌شود ... »
      این مورد سبب می‌شود تا سایر پارامترهای تزریق شده‌ی به context، طول عمر singleton پیدا کنند؛ چون این context دیگر وهله سازی نخواهد شد و وابستگی‌های آن دوباره از IoC Container سیستم، درخواست مجدد نمی‌شوند. به همین جهت در این حالت، تزریق سایر پارامترها را در سازنده‌ی کلاس، ممنوع کرده‌اند تا این اشتباه را مرتکب نشوید.
      اما ... می‌توان به وابستگی‌های سیستم در کلاس Context برنامه به نحو دیگری دسترسی یافت:
      services.AddEntityFrameworkSqlServer();
      services.AddDbContextPool<ApplicationDbContext>((serviceProvider, optionsBuilder) =>
      {
         optionsBuilder.UseSqlServer("...");
         optionsBuilder.UseInternalServiceProvider(serviceProvider);
      });
      کار متد UseInternalServiceProvider افزودن serviceProvider برنامه به لیست سرویس‌های داخلی EF است. از این متد عموما استفاده نمی‌شود، مگر اینکه بخواهید به سرویس‌های برنامه درون context آن دسترسی پیدا کنید.
      نکته‌ی مهم دیگر، ذکر صریح متد services.AddEntityFrameworkSqlServer است تا اینبار سرویس‌های EF به صورت مستقیم به serviceProvider برنامه اضافه شوند. زمانیکه UseInternalServiceProvider فراخوانی می‌شود، هنوز serviceProvider برنامه ساخته نشده‌است. به همین جهت نیاز است این متد را نیز دستی فراخوانی کنیم که در حالت متداول نیازی به آن نیست و توسط EF مدیریت می‌شود. 
      پس از این تنظیمات ابتدایی، اکنون داخل context برنامه به شکل زیر می‌توان به سرویس‌های مختلف دسترسی یافت:
      using Microsoft.EntityFrameworkCore.Infrastructure;
      using Microsoft.Extensions.DependencyInjection;
      
      var siteSettings = this.GetService<IOptionsSnapshot<SiteSettings>>();
      // Or
      var siteSettings = this.GetInfrastructure().GetRequiredService<IOptionsSnapshot<SiteSettings>>();
      • #
        ‫۳ سال و ۹ ماه قبل، سه‌شنبه ۱۱ آذر ۱۳۹۹، ساعت ۱۴:۲۰
        در EF Core 5.0 من از این روش استفاده کردم و با خطای  Cache entry must specify a value for Size when SizeLimit is set برخورد کردم. پس از بررسی‌های بیشتر همان طور که در متن کامنتی که نوشتید اشاره کردید، این کار سبب می‌شود که EF به سرویس‌های ثبت شده توسط برنامه ما دسترسی پیدا کند و ظاهرا چون خود EF از سرویس کش استفاده می‌کند و برای آن محدودیت سایز هم تعیین کرده است، با سرویس کش استفاده درون برنامه تداخل پیدا میکند .
        به هر حال در EF Core 5.0، من سطر های services.AddEntityFrameworkSqlServer(); و optionsBuilder.UseInternalServiceProvider(serviceProvider); را حذف کردم و توانستم در داخل DbContext هم از سرویس ثبت شده در برنامه استفاده کنم و مشکلی پیش نیامد.
  • #
    ‫۶ سال و ۱۰ ماه قبل، یکشنبه ۷ آبان ۱۳۹۶، ساعت ۱۲:۲۰
    از وقتی که از AddDbContextPool  استفاده کردم بعضی مواقع خطای: 

    Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.  This may have occurred because all pooled connections were in use and max pool size was reached. 
    اتفاق میوفته حتی من مقدار max pool size رو هم در connection string تنظیم میکنم اما باز هم این خطا وجود دارد.
  • #
    ‫۳ سال قبل، دوشنبه ۱۸ مرداد ۱۴۰۰، ساعت ۰۷:۴۳
    با سلام
    وقتی از روش DbContext pooling  استفاده می کنیم آیا استفاده از متد Database.BeginTransaction باعث مشکل نمی‌شود؟
    چون DbContext  به اشتراک گذاشته می‌شود آیا Transaction هایی که بر روی این DbContext به اشتراک گذاشته شده استفاده می‌شود
    بر روی هم تاثیر دارند؟
    آیا محدودیتی در استفاده از Transaction در روش DbContext pooling وجود دارد؟ 
    • #
      ‫۳ سال قبل، دوشنبه ۱۸ مرداد ۱۴۰۰، ساعت ۱۱:۴۹
      خیر. چیزی به اشتراک گذاشته نمی‌شود. از مقدمه: «... وضعیت این Context به حالت اولیه برگردانده شده ...» و سپس استفاده‌ی مجدد می‌شود.