بالا بردن سرعت بارگذاری اولیه EF Code first با تعداد مدل‌های زیاد
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

EF Code first هربار در حین آغاز اجرای برنامه و اولین کوئری که به بانک اطلاعاتی ارسال می‌کند، کار تشخیص روابط بین کلاس‌ها و همچنین نگاشت آن‌ها را به بانک اطلاعاتی، انجام می‌دهد. این مورد شاید با تعداد کم کلاس‌ها آنچنان به نظر نرسد، اما اگر تعداد کلاس‌های شما به بالای 200 عدد رسید، زمان آغاز برنامه آزار دهنده خواهد شد. راه حلی برای این مساله وجود دارد به نام ایجاد Viewهای متناظر با نگاشت‌ها و سپس کامپایل آن به عنوان جزئی از برنامه، که در ادامه نحوه انجام این‌کار را مرور خواهیم کرد.

بررسی ساختار pre-generated views

برای کامپایل نگاشت‌های EF در خود برنامه (بجای تولید پویای هربار آن‌ها)، ابتدا باید فایل edmx متناظر با مدل‌ها و روابط بین آن‌ها تشکیل شود:
    var ms = new MemoryStream();
    using (var writer = XmlWriter.Create(ms))
    {
        EdmxWriter.WriteEdmx(new Context(), writer);
    }
پس از اینکه edmx تشکیل شد، باید از ساختار فشرده آن سه جزء زیر را استخراج کرد:
الف) ssdl : storageModels
ب) csdl : conceptualModels
ج) msl : mappings

اینکار را به صورت زیر می‌توان انجام داد:
    var xDoc = XDocument.Load(ms);

    var ssdl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2009/02/edm/ssdl}Schema").Single();
    var csdl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2008/09/edm}Schema").Single();
    var msl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2008/09/mapping/cs}Mapping").Single();
پس از آن باید محتوای این سه جزء را توسط متد Save هر کدام، در فایل‌های xml ایی ذخیره کرد و توسط ابزاری به نام EdmGen.exe که جزئی از  ویژوال استودیو است، فایل Context.Views.cs را تولید، به برنامه اضافه و سپس کامپایل کرد:
 EdmGen.exe /mode:ViewGeneration /incsdl:Context.csdl  /inmsl:Context.msl /inssdl:Context.ssdl /outviews:Context.Views.cs
بهتر است این پروسه هر بار که قرار است ارائه نهایی برنامه صورت گیرد، انجام شود.

علاوه بر این‌ها اگر علاقمند باشید که کار فایل EdmGen را شبیه سازی کنید، کلاس زیر این‌کار را انجام داده و قادر است خروجی vb یا cs متناظری را نیز تولید کند:
using System;
using System.Data.Entity;
using System.Data.Entity.Design;
using System.Data.Entity.Infrastructure;
using System.Data.Mapping;
using System.Data.Metadata.Edm;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;

namespace EfUtils
{
    public static class PreGeneratedViewsWriter
    {
        public static void CreatePreGeneratedViewsFile(
                this DbContext contextInstance,
                LanguageOption language = LanguageOption.GenerateCSharpCode,
                string viewsFile = "Context.Views.cs",
                string edmxFile = "context.edmx",
                string ssdlFile = "context.ssdl.xml",
                string csdlFile = "context.csdl.xml",
                string mslFile = "context.msl.xml")
        {
            using (var contextViewsMemoryStream = new MemoryStream())
            {
                using (var edmxMemoryStream = new MemoryStream())
                {
                    var edmx = createEdmx(contextInstance, edmxFile, edmxMemoryStream);
                    var mappingItemCollection = createMappingItemCollection(ssdlFile, csdlFile, mslFile, edmx);
                    generateViews(language, viewsFile, contextViewsMemoryStream, mappingItemCollection);
                }
            }
        }

        private static void generateViews(LanguageOption language, string viewsFile, MemoryStream contextViewsMemoryStream, StorageMappingItemCollection mappingItemCollection)
        {
            var viewGenerator = new EntityViewGenerator // It's defined in System.Data.Entity.Design.dll
            {
                LanguageOption = language
            };
            using (var streamWriter = new StreamWriter(contextViewsMemoryStream))
            {
                var errors = viewGenerator.GenerateViews(mappingItemCollection, streamWriter).ToList();

                if (errors.Any())
                    throw new InvalidOperationException(errors.First().Message);

                contextViewsMemoryStream.Position = 0;
                using (var reader = new StreamReader(contextViewsMemoryStream))
                {
                    var codeData = reader.ReadToEnd();
                    File.WriteAllText(viewsFile, codeData);
                }
            }
        }

        private static StorageMappingItemCollection createMappingItemCollection(string ssdlFile, string csdlFile, string mslFile, XDocument edmx)
        {
            var ssdl = edmx.Descendants("{http://schemas.microsoft.com/ado/2009/02/edm/ssdl}Schema").Single();
            ssdl.Save(ssdlFile);
            var storeItemCollection = new StoreItemCollection(new[] { ssdl.CreateReader() });

            var csdl = edmx.Descendants("{http://schemas.microsoft.com/ado/2008/09/edm}Schema").Single();
            csdl.Save(csdlFile);
            var edmItemCollection = new EdmItemCollection(new[] { csdl.CreateReader() });

            var msl = edmx.Descendants("{http://schemas.microsoft.com/ado/2008/09/mapping/cs}Mapping").Single();
            msl.Save(mslFile);

            var mappingItemCollection = new StorageMappingItemCollection(edmItemCollection, storeItemCollection, new[] { msl.CreateReader() });
            return mappingItemCollection;
        }

        private static XDocument createEdmx(DbContext contextInstance, string edmxFile, MemoryStream edmxMemoryStream)
        {
            var settings = new XmlWriterSettings { Indent = true };
            using (var writer = XmlWriter.Create(edmxMemoryStream, settings))
            {
                EdmxWriter.WriteEdmx(contextInstance, writer);
            }
            File.WriteAllBytes(edmxFile, edmxMemoryStream.ToArray());

            edmxMemoryStream.Position = 0;
            var edmx = XDocument.Load(edmxMemoryStream);
            return edmx;
        }
    }
}
در اینجا همان مراحلی که عنوان شد، تکرار می‌شود. فایل edmx متناظر با وهله‌ای از DbContext برنامه، تولید شده و سه جزء آن استخراج می‌شوند. سپس این موارد به EntityViewGenerator موجود در اسمبلی System.Data.Entity.Design.dll ارسال شده و کد نهایی متناظر قابل کامپایل در برنامه تولید می‌گردد.
پس از تولید فایل Context.Views.cs یا Context.Views.vb، آن‌را به پروژه اضافه کنید.
اینبار نحوه استفاده از آن باید به صورت زیر باشد:
 Database.SetInitializer<MyContext>(null);
 از این جهت که تمام اطلاعات لازم جهت آغاز کار، در فایل تولیدی Context.Views وجود دارد و اکنون جزئی از فایل اجرایی برنامه است و نیازی به تکرار ساخت مجدد پویای آن نیست.

مرجع:
Entity Framework Code First View Generation Templates On Visual Studio Code Gallery

  • #
    ‫۱۱ سال و ۱۲ ماه قبل، دوشنبه ۱۰ مهر ۱۳۹۱، ساعت ۰۰:۰۸
    در یکی از پروژه هایی که بجای تعداد 200 جدول رابطه زیادی هم داشت مجبور به استفاده از این روش شدم، البته سبک Model First
  • #
    ‫۱۱ سال و ۱۲ ماه قبل، سه‌شنبه ۱۱ مهر ۱۳۹۱، ساعت ۱۵:۱۶
    با سلام
    آیا امکان این هست که بشه از این در روش DataBase First استفاده کرد . با تشکر
    • #
      ‫۱۱ سال و ۱۲ ماه قبل، سه‌شنبه ۱۱ مهر ۱۳۹۱، ساعت ۱۵:۳۰
      بله. همین روال رو می‌تونید توسط افزونه «Entity Framework Power Tools» هم انجام بدید (گزینه Generate Views را به منوی کلیک راست بر روی Entity Data Model/*.EDMX اضافه می‌کند).
      • #
        ‫۱۱ سال و ۱۲ ماه قبل، سه‌شنبه ۱۱ مهر ۱۳۹۱، ساعت ۲۱:۲۰
        با سلام
        ممنوم از راهنماییتون خیلی عالیه .
        با استفاده از این کار یه view در کنار Model ام ایجاد شد و سرعت بارگذاری برای این اولین بار رو از 10 ثانیه به کمتر از 2 ثانیه تقلیل داد آیا چیز دیگه ای نیاز نیست و نیز من برای ایجاد دیتابیسم با استفاده از Model در اولین واکشی به این صورت عمل می‌کنم آیا مشکلی پیش نمیاد در استفاده از این روش . با تشکر
         if (!db_Context.DatabaseExists())
               db_Context.CreateDatabase();
        • #
          ‫۱۱ سال و ۱۲ ماه قبل، سه‌شنبه ۱۱ مهر ۱۳۹۱، ساعت ۲۲:۲۵
          - خیر. به تنظیم دیگری نیاز ندارد. این کلا‌س‌ها به صورت خودکار تشخیص داده شده و استفاده می‌شوند. البته به ازای هربار تغییر مدل‌ها نیاز است مجددا تولید شوند.
          - اگر روش شما db first است که عنوان کردید، بررسی فوق (ایجاد بانک اطلاعاتی) کار اضافی است. اگر روش code first است، باز هم نیازی نیست چون در حالت خودکار migrations اینکار را انجام می‌دهد.
          در کل بهتر است تمام جوانب را بررسی و آزمایش کنید.
          کاری که در اینجا انجام می‌شود ایجاد یک cached metadata کامپایل شده است بجای تولید پویای هربار آن (تفاوت مهم و اصلی با روش‌های متداول).

  • #
    ‫۱۱ سال و ۸ ماه قبل، شنبه ۲۸ بهمن ۱۳۹۱، ساعت ۰۴:۲۲
    سلام و خسته نباشید
    سوالی که من دارم اینکه بعد از ساختن فایل Context.Views.cs  چطور باید ازش استفاده کرد . من این فایلو ساختم و کنار Context.cs تو پروژهام گذاشتم . ولی فرق خاصی احساس نمی‌کنم . میشه بیشتر راهنمایی کنید
    • #
      ‫۱۱ سال و ۸ ماه قبل، شنبه ۲۸ بهمن ۱۳۹۱، ساعت ۰۴:۲۸

      اگر فرقی احساس نکردید منتظر نگارش بعدی EF باشید. این مورد رو برطرف کردن:

      «significantly improved warm up time (view generation), especially for large models»

  • #
    ‫۱۰ سال و ۱۰ ماه قبل، دوشنبه ۴ آذر ۱۳۹۲، ساعت ۱۵:۲۰
    آیا بعد از ایجاد View برای بالا بردن لود اولیه ، باز هم migration کار میکنه یا نه ؟
    • #
      ‫۱۰ سال و ۱۰ ماه قبل، دوشنبه ۴ آذر ۱۳۹۲، ساعت ۱۵:۴۱
      بله. هم روش دستی migrations و هم روش خودکار در اینجا کار می‌کنند. اینکه در انتهای بحث عنوان شده مقدار را null قرار بدید، برای رسیدن به حداکثر سرعت بارگذاری برنامه است. در این حالت می‌توانید از روش دستی برای انجام migrations استفاده کنید. این نکته در انتهای مطلب پنجم سری EF سایت بحث شده.
  • #
    ‫۱۰ سال و ۸ ماه قبل، سه‌شنبه ۲۲ بهمن ۱۳۹۲، ساعت ۲۱:۴۵
    باسلام.این کد را در کجا و چطوری در مدل code first استفاده بکنیم؟
    using (var ctx = new MyContext())
    {
        InteractiveViews
            .SetViewCacheFactory(
                ctx, 
                new FileViewCacheFactory(@"C:\MyViews.xml"));
    }
  • #
    ‫۹ سال و ۹ ماه قبل، شنبه ۲۹ آذر ۱۳۹۳، ساعت ۱۷:۵۴
    با سلام
     میشه خواهش کنم یک نمونه پروژه برای استفاده از این روش بزارید.
    • #
      ‫۹ سال و ۹ ماه قبل، شنبه ۲۹ آذر ۱۳۹۳، ساعت ۱۸:۰۳
      نیازی به مثال آنچنانی ندارد. ابتدا بسته‌ی نیوگت این پروژه را نصب کنید:
      PM> Install-Package EFInteractiveViews
      بعد یکبار در ابتدای برنامه در اولین کوئری، متد InteractiveViews.SetViewCacheFactory آن‌را فراخوانی کنید. البته بهتر است آن‌را درون یک سینگلتون thread safe قرار دهید. بار اولی که فایل xml آن ایجاد می‌شود زمان خواهد برد. بار دوم اجرای برنامه سریع است.
      private static bool _isPreGeneratedViewCacheSet;
      
      private void InitializationPreGeneratedViews()
      {
         if (_isPreGeneratedViewCacheSet) return;
      
         var precompiledViewsFilePath = new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName + @”\EF6PrecompiledViews.xml”;
         InteractiveViews.SetViewCacheFactory(this, new FileViewCacheFactory(precompiledViewsFilePath));
         _isPreGeneratedViewCacheSet = true;
      }
      • #
        ‫۹ سال و ۹ ماه قبل، سه‌شنبه ۲ دی ۱۳۹۳، ساعت ۱۴:۰۶
        با سلام و ممنون از جوابتون 
        روشی رو گفتید رفتم گفتم اینجا بزارم هم دیگران استفاده کنند و اگه هم اشتباه کردم بفرمایید اصلاح کنم.
        پکیج رو تو پروژه ای که کلاس context هست نصب کردم و تابع زیر رو
        private static bool _isPreGeneratedViewCacheSet;
         
        private void InitializationPreGeneratedViews()
        {
           if (_isPreGeneratedViewCacheSet) return;
         
           var precompiledViewsFilePath = new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName + @”\EF6PrecompiledViews.xml”;
           InteractiveViews.SetViewCacheFactory(this, new FileViewCacheFactory(precompiledViewsFilePath));
           _isPreGeneratedViewCacheSet = true;
        }
         توی کلاس context گذاشتم  بعد از اجرای یک فایل Xml  در مسیر 
        C:\Users\Hadi\AppData\Local\Temp\Temporary ASP.NET Files\root\2781dacc\5d62fdaf\assembly\dl3\142eef19\00077ffc_731ed001 
        میسازه، البته من بصورت دستی این تابع رو یک بار اجرا کردم و بعد  غیرفعالش کردم.
         بعد این تابع رو در application_start  نوشتم:
         InteractiveViews
                        .SetViewCacheFactory(ctx, new FileViewCacheFactory(new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName + @"\EF6PrecompiledViews.xml"));
                 
        • #
          ‫۹ سال و ۹ ماه قبل، سه‌شنبه ۲ دی ۱۳۹۳، ساعت ۱۴:۰۹
          - این فایل باید به ازای هربار تغییر در مدل‌ها دوباره ساخته شود. بنابراین تولید دوباره آن نباید غیرفعال شود.
          - Assembly.GetExecutingAssembly().Location برای برنامه‌های دسکتاپ مفید است (یعنی مسیر فایل اجرایی). در برنامه‌های وب، به این مسیر عموما دسترسی ندارید. به همین جهت بهتر است از HttpRuntime.AppDomainAppPath استفاده کنید.
  • #
    ‫۹ سال و ۵ ماه قبل، سه‌شنبه ۱۵ اردیبهشت ۱۳۹۴، ساعت ۰۱:۵۲
    سلام.آیا برای بهبود سرعت در EF Designer  database Model در  WPF هست؟
    در این روش برای بار اول وقتی با دیتابیس برخورد میکنه حدود 5 ثانیه طول میکشه از دفعات بعد سرعت عالی میشه ، لطفا راهی بهم پیشنهاد بدین
    • #
      ‫۹ سال و ۵ ماه قبل، سه‌شنبه ۱۵ اردیبهشت ۱۳۹۴، ساعت ۰۶:۰۰
      - نگارش‌های جدید EF بر اساس روش Code first کار می‌کنند (حتی اگر به ظاهر DB First باشند). بنابراین روش تکمیلی مطرح شده در نظرات، با تمام نگارش‌های EF کار می‌کند.
      - همچنین علت زمانبر بودن را می‌توانید با DNT Profiler بررسی کنید که چه مراحلی در پشت صحنه انجام می‌شوند. این مراحل، بار اول در بانک اطلاعاتی پردازش و plan آن‌ها کش خواهند شد. به همین جهت برای بار دوم سریعتر است.
      • #
        ‫۹ سال و ۲ ماه قبل، شنبه ۲۰ تیر ۱۳۹۴، ساعت ۲۳:۱۶
        بخشید منظورتون از ورژن چند به بعد که مثل هم رفتار میشه؟
        سوال دومم این هست که ما وقتی از روش DatabaseFrist استفاده کنم در Entity 6.1.3 تمامی کلاس‌ها از جمله DbContext با کوچکترین تغییر دوباره ایحاد میشه و تابعی که قراره داخل اون قرار بدماز بین می‌ره. چجوری باید اونو رفع کرد؟
        • #
          ‫۹ سال و ۲ ماه قبل، یکشنبه ۲۱ تیر ۱۳۹۴، ساعت ۰۰:۱۴
          «... تابعی که قراره داخل اون قرار بدم ...»
          نظرات را مطالعه کردید یکبار؟ مطلب نوشته شده با یک افزونه‌ی جدید جایگزین شده‌است. این مورد هم نیازی به دستکاری Context شما ندارد و هیچ کدی را نیازی نیست، داخل آن قرار دهید. 
  • #
    ‫۶ سال و ۱۰ ماه قبل، پنجشنبه ۱۱ آبان ۱۳۹۶، ساعت ۱۴:۲۵
    یک نکته‌ی تکمیلی: امکان بالابردن سرعت بارگذاری برنامه‌های مبتنی بر EF 6.2 به صورت توکار

    با به روز رسانی ارجاعات EF مورد استفاده:
    To Update 
    PM> Update-Package EntityFramework -Version 6.2.0
    
    To install
    PM> Install-Package EntityFramework -Version 6.2.0
     تنها کافی است قطعه کد ذیل را به اسمبلی حاوی Context خود اضافه کنید:
    public class MyDbConfiguration : DbConfiguration
    {
       public MyDbConfiguration() : base()
       {
           SetModelStore(new DefaultDbModelStore(Directory.GetCurrentDirectory()));
       }
    }
    تشخیص و استفاده‌ی از آن توسط EF 6.2 خودکار است. پس از آن یک کش محلی، از مدل سیستم تهیه می‌شود (Entity Framework Code First Model Cache) و در مسیری که قید شده (پارامتر DefaultDbModelStore)، ذخیره خواهد شد؛ مانند:
     /bin/Debug/MyAssembly.MyNamespace.MyDbContext.edmx
    بازسازی این کش محلی بجای تولید پویای آن در هربار بارگذاری برنامه (که سرعت آغاز برنامه را کاهش می‌دهد)، بر اساس مقایسه‌ی تاریخ آخرین تغییر اسمبلی حاوی Context برنامه با تاریخ آخرین تغییر فایل کش محلی است. اگر این دو یکی نبودند، این کش بازتولید خواهد شد.

    پس از این تغییر کوچک، اولین بار اجرای برنامه همانند حالت تولید پویای Entity Framework Code First Model Cache که پیشتر در حافظه انجام می‌شد، اندکی طول کشیده و نتیجه‌ی آن در فایل edmx یاد شده ذخیره می‌شود. از بار دوم اجرای برنامه، Model Cache از فایل edmx محلی خوانده شده و به این ترتیب سرعت آغاز برنامه به شدت افزایش خواهد یافت.
    برای نمونه عنوان شده‌است که با استفاده از این روش، سرعت بارگذاری Context ایی با 600 مدل، از 14 ثانیه به 2 ثانیه کاهش یافته‌است.

    یک نکته: برای برنامه‌های وب بهتر است از مسیر پوشه‌ی محافظت شده‌ی App_Data به عنوان پارامتر DefaultDbModelStore استفاده کنید:
    var appDataDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data");
    //OR
    var appDataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString();
    • #
      ‫۵ سال قبل، جمعه ۲۹ شهریور ۱۳۹۸، ساعت ۰۱:۴۲
      ممنون بابت این روش.
      خطای Access denied
      من هنگام استفاده از این روش وقتی عملیات Migration رو انجام میدادم خطای Access denied میداد:
      Access to the path 'C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\WorkWithEF2.DataLayer.Context.edmx' is denied.
      کد رو یکم تغییر دادم و مشکل حل شد ( + ):
      public class MyDbConfiguration : DbConfiguration
      {
          public MyDbConfiguration() : base()
          {
              var path = Path.GetDirectoryName(this.GetType().Assembly.Location);
              SetModelStore(new DefaultDbModelStore(path));
          }
      }

  • #
    ‫۲ سال و ۷ ماه قبل، دوشنبه ۴ بهمن ۱۴۰۰، ساعت ۱۵:۴۱
    یک نکته‌ی تکمیلی: اضافه شدن قابلیت مشابهی جهت «بالا بردن سرعت بارگذاری اولیه EF-Core 6x با تعداد مدل‌های زیاد»

    اگر تعداد مدل‌های زیادی را دارید و بارگذاری اولیه‌ی برنامه‌ی مبتنی بر EF-Core 6x شما کند است، می‌توانید با استفاده از قابلیت جدید Compiled models آن، این سرعت را حداقل 8 برابر افزایش دهید. روش انجام آن نیز ساده‌است:
    public class ExampleContext : DbContext
    {
        public DbSet<Person> People { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        { 
            options.UseModel(CompiledModelsExample.ExampleContextModel.Instance);
            options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=Test;Trusted_Connection=True;"); 
        }
    }
    به همراه اجرای یکی از دو دستور زیر:
    CLI:
    dotnet ef dbcontext optimize -c ExampleContext -o CompliledModels -n CompiledModelsExample
    Package Manager Console:
    Optimize-DbContext -Context ExampleContext -OutputDir CompiledModels -Namespace CompiledModelsExample

    بنابراین به Context و متد OnConfiguring آن چنین سطری باید اضافه شود:
    options.UseModel(CompiledModelsExample.ExampleContextModel.Instance);
    که CompiledModelsExample آن همان نام فضای نام دلخواهی است که به دستور dotnet ef ارسال کرده‌ایم. مابقی آن نام Context به همراه Model است. البته اگر این سطر را هم اضافه نکنید، دستور dotnet ef فوق، آن‌را گوشزد خواهد کرد.

    محدودیت‌های این روش در نگارش 6x:
    - از فیلترهای سراسری (global query filters) پشتیبانی نمی‌کند.
    - از تشکیل Lazy loading proxies پشتیبانی نمی‌کند. 
    - از تشکیل Change tracking proxies پشتیبانی نمی‌کند. 
    همچنین هربار که تغییری را در مدل‌ها ایجاد کردید، دستورات فوق را باید به صورت دستی مجددا اجرا کنید.