[CLI] Build-time performance improvements.
[CLI] Global tools; replaces .NET CLI Tools (DotNetCliToolReference).
[CoreCLR] Minor-version roll-forward.
[CoreCLR] No-copy array slicing with Span<T>.
[CoreFX] HttpClient performance improvements.
[CoreFX] Windows Compatibility Pack.
[ASP.NET] SignalR is available for .NET Core.
[ASP.NET] HTTPS is on by default for ASP.NET.
[EF] Basic lazy loading support.
[EF] Support for Azure Cosmos DB.
اشتراکها
- Accessibility improvements in narration, high contrast and focus control areas
- .NET Framework support for .NET Standard 2.0 and compiler features
- More secure SHA-2 support in ASP.NET and System.Messaging
- Configuration builders
- ASP.NET Execution step feature
- ASP.NET HttpCookie parsing
- Enhancements in Visual Tree for WPF applications
- Performance and reliability improvements
اشتراکها
ویژگیهای جدید NET Core 3.0.
در قسمت بعد، ارتباطات self referencing را بررسی خواهیم کرد و چون EF Core هیچ راه حل بهینهای را برای کوئری گرفتن از این نوع روابط سلسله مراتبی ارائه نمیدهد (درEF 6.x نیز به همین ترتیب)، نیاز است مستقیما SQL نویسی کرد. به همین جهت در این قسمت نحوهی نوشتن کوئریهای مستقیم SQL و اجرای آنها را در EF Core بررسی میکنیم.
اجرای کوئریهای خام SQL بر روی بانک اطلاعاتی، توسط EF Core
گاهی از اوقات نیاز به استفادهی قابلیت خاصی از بانک اطلاعاتی مدنظر وجود دارد که توسط LINQ پشتیبانی نمیشود و یا کوئری SQL حاصل از LINQ to Entities آنچنان بهینه نیست. در یک چنین حالاتی راهی بجز نوشتن کوئریهای خام SQL وجود ندارد. امکان اجرای یک چنین کوئریهایی توسط EF Core پیش بینی شدهاست؛ اما با این محدودیتها:
- خروجی کوئری SQL، تنها باید معادل یکی از کلاسهای موجودیتهای شما باشد. قرار است این محدودیت در نگارش 1.1 برطرف شود.
- کوئری SQL نوشته شده باید تمام خواص موجودیتی را که قرار است به آن نگاشت شود، بازگشت دهد.
- نام ستونهای بازگشت داده شدهی توسط کوئری SQL باید با نام خواص موجودیت در حال کار، یکی باشند و برخلاف EF 6.x، از یک چنین عدم تطابقهایی صرفنظر نخواهد شد.
- کوئری SQL نوشته شده نباید به همراه اطلاعات ارتباطات موجودیتها باشد.
در اینجا برای نوشتن کوئریهای خام SQL میتوان از متد FromSql مرتبط با یکی از DbSetهای برنامه استفاده کرد:
و یا حتی میتوان از رویهی ذخیره شدهای استفاده کرد که خروجی ستونهای آن، معادل تمام خواص کلاس Blog باشد:
بنابراین رفتار EF Core اندکی متفاوت است با EF 6.x. در اینجا اگر میخواهید از عبارت SQL خود خروجی بگیرید، باید از یکی از DbSetهای خود شروع کنید و متد FromSql را بر روی آن فراخوانی نمائید. همچنین کوئری نوشته شده باید اولا تمام ستونهای آن DbSet رابازگشت دهد و به علاوه این ستونها دقیقا با نامهای خواص آن کلاس، تطابق داشته باشند.
علت این مسایل نیز به این دلیل است که بتوان نتیجهی کوئری را به صورت خودکار وارد سیستم change tracking کرد و همچنین کوئریهای ترکیبی LINQ را نیز در اینجا فعال کرد.
ارسال پارامترها به کوئریهای خام SQL
تنها حالتی در EF Core که مستعد به حملات تزریق SQL است، دقیقا همین مورد دور شدن از LINQ و نوشتن عبارات مستقیم SQL است. در اینجا برای نوشتن کوئریهای پارامتری دو حالت پیش بینی شدهاست:
الف) روش parameter place holders
در اینجا متد FromSql، بسیار شبیه به متد String.Format است، اما در عمل اینطور نیست و تمام place holders آن به صورت خودکار تبدیل به پارامتر میشوند:
ب) روش ساخت دستی DbParameterها
اگر میخواهید از پارامترهای نام دار استفاده کنید، با وهلهای از SqlParameter شروع کرده و سپس آنرا به متد FromSql ارسال کنید:
و یا این حالت را به شکل ساده شدهی ذیل نیز میتوان مورد استفاده قرار داد:
که در اینجا p0@ به name1 و p1@ به name2 نگاشت خواهد شد.
مزیت کار کردن با SqlParameter این است که میتوان برای مثال Direction و SqlDbType را نیز صریحا ذکر کرد (بسته به نوع پارامترهای رویهی ذخیره شده):
امکان ترکیب کوئریهای SQL و LINQ نیز پیش بینی شدهاست
در کوئری ذیل، قسمت select از جدولی به صورت SQL و قسمت where و order by آن توسط LINQ تهیه شدهاند که در نهایت به یک کوئری ترجمه شده و بر روی بانک اطلاعاتی اجرا میشوند.
یک مثال جالب آن، امکان کوئری گرفتن از Table Value Functionها و سپس ترکیب آنها با LINQ است (این ترکیب، تنها یک کوئری SQL نهایی را تولید میکند):
واکشی ارتباطات یک موجودیت توسط SQL و LINQ
در ابتدای بحث در قسمت محدودیتهای کوئریهای SQL نوشته شده، ذکر شد «کوئری SQL نوشته شده نباید به همراه اطلاعات ارتباطات موجودیتها باشد». برای رفع این محدودیت میتوان از ترکیب SQL و LINQ به صورت ذیل استفاده کرد:
در اینجا برای واکشی ارتباطات یک موجودیت از متد Include استفاده شدهاست.
اجرای عبارات SQL، بدون بازگشت مقداری
تا اینجا در مورد عبارات SQL از نوع Select و یا اجرای رویههای ذخیره شده، بحث شد. برای اجرای عبارات SQL ایی مانند update و delete میتوان از متد ExecuteSqlCommand مربوط به context.Database استفاده کرد:
و یا برای ارسال پارامترها به آن میتوان به این صورت عمل کرد (اجرای یک رویهی ذخیره شده با دو پارامتر ارسالی به آن):
اجرای عبارات SQL و دریافت خروجیهایی به غیر از موجودیتهای برنامه
در ابتدا بحث عنوان شد که محدودیت فعلی کوئریهای FromSQL که میتوانند خروجی را نیز ارائه دهند، مقید بودن آنها به DbSet در حال استفاده است و محدود بودن آنها به خواص کلاس متناظر تعریف شده. در این حالت اگر بخواهیم یک محاسبهی عددی را بازگشت دهیم چه باید کرد؟
متد ExecuteSqlCommand تنها وضعیت نهایی اجرای عملیات را بازگشت میدهد و FromSQL مقید است به DbSet متناظر. برای رفع این محدودیتها میتوان مستقیما به DbConnection دسترسی یافت و سپس کوئری گرفت؛ به نحو ذیل:
به عبارتی در اینجا امکان بازگشت به حالت ADO.NET خام نیز پیش بینی شدهاست.
اجرای کوئریهای خام SQL بر روی بانک اطلاعاتی، توسط EF Core
گاهی از اوقات نیاز به استفادهی قابلیت خاصی از بانک اطلاعاتی مدنظر وجود دارد که توسط LINQ پشتیبانی نمیشود و یا کوئری SQL حاصل از LINQ to Entities آنچنان بهینه نیست. در یک چنین حالاتی راهی بجز نوشتن کوئریهای خام SQL وجود ندارد. امکان اجرای یک چنین کوئریهایی توسط EF Core پیش بینی شدهاست؛ اما با این محدودیتها:
- خروجی کوئری SQL، تنها باید معادل یکی از کلاسهای موجودیتهای شما باشد. قرار است این محدودیت در نگارش 1.1 برطرف شود.
- کوئری SQL نوشته شده باید تمام خواص موجودیتی را که قرار است به آن نگاشت شود، بازگشت دهد.
- نام ستونهای بازگشت داده شدهی توسط کوئری SQL باید با نام خواص موجودیت در حال کار، یکی باشند و برخلاف EF 6.x، از یک چنین عدم تطابقهایی صرفنظر نخواهد شد.
- کوئری SQL نوشته شده نباید به همراه اطلاعات ارتباطات موجودیتها باشد.
در اینجا برای نوشتن کوئریهای خام SQL میتوان از متد FromSql مرتبط با یکی از DbSetهای برنامه استفاده کرد:
var blogs = context.Blogs .FromSql("SELECT * FROM dbo.Blogs") .ToList();
var blogs = context.Blogs .FromSql("EXECUTE dbo.GetMostPopularBlogs") .ToList();
بنابراین رفتار EF Core اندکی متفاوت است با EF 6.x. در اینجا اگر میخواهید از عبارت SQL خود خروجی بگیرید، باید از یکی از DbSetهای خود شروع کنید و متد FromSql را بر روی آن فراخوانی نمائید. همچنین کوئری نوشته شده باید اولا تمام ستونهای آن DbSet رابازگشت دهد و به علاوه این ستونها دقیقا با نامهای خواص آن کلاس، تطابق داشته باشند.
علت این مسایل نیز به این دلیل است که بتوان نتیجهی کوئری را به صورت خودکار وارد سیستم change tracking کرد و همچنین کوئریهای ترکیبی LINQ را نیز در اینجا فعال کرد.
ارسال پارامترها به کوئریهای خام SQL
تنها حالتی در EF Core که مستعد به حملات تزریق SQL است، دقیقا همین مورد دور شدن از LINQ و نوشتن عبارات مستقیم SQL است. در اینجا برای نوشتن کوئریهای پارامتری دو حالت پیش بینی شدهاست:
الف) روش parameter place holders
در اینجا متد FromSql، بسیار شبیه به متد String.Format است، اما در عمل اینطور نیست و تمام place holders آن به صورت خودکار تبدیل به پارامتر میشوند:
var user = "johndoe"; var blogs = context.Blogs .FromSql("EXECUTE dbo.GetMostPopularBlogsForUser {0}", user) .ToList();
اگر میخواهید از پارامترهای نام دار استفاده کنید، با وهلهای از SqlParameter شروع کرده و سپس آنرا به متد FromSql ارسال کنید:
var user = new SqlParameter("user", "johndoe"); var blogs = context.Blogs .FromSql("EXECUTE dbo.GetMostPopularBlogsForUser @user", user) .ToList();
var results = _context.Contacts.FromSql( @"SELECT Id, Name Address, City, State, Zip FROM Contacts WHERE Name IN (@p0, @p1)", name1, name2);
مزیت کار کردن با SqlParameter این است که میتوان برای مثال Direction و SqlDbType را نیز صریحا ذکر کرد (بسته به نوع پارامترهای رویهی ذخیره شده):
var nameParameter = new SqlParameter { ParameterName = "@name", Value = "doc", Direction = ParameterDirection.Input, SqlDbType = SqlDbType.NVarChar };
امکان ترکیب کوئریهای SQL و LINQ نیز پیش بینی شدهاست
در کوئری ذیل، قسمت select از جدولی به صورت SQL و قسمت where و order by آن توسط LINQ تهیه شدهاند که در نهایت به یک کوئری ترجمه شده و بر روی بانک اطلاعاتی اجرا میشوند.
یک مثال جالب آن، امکان کوئری گرفتن از Table Value Functionها و سپس ترکیب آنها با LINQ است (این ترکیب، تنها یک کوئری SQL نهایی را تولید میکند):
var posts = context.Posts .FromSql("SELECT * FROM dbo.GetMatchingPostByTitle({0})", searchTerm) .Where(p => p.BlogId == 1) .OrderByDescending(p => p.CreateDate) .ToList();
واکشی ارتباطات یک موجودیت توسط SQL و LINQ
در ابتدای بحث در قسمت محدودیتهای کوئریهای SQL نوشته شده، ذکر شد «کوئری SQL نوشته شده نباید به همراه اطلاعات ارتباطات موجودیتها باشد». برای رفع این محدودیت میتوان از ترکیب SQL و LINQ به صورت ذیل استفاده کرد:
var searchTerm = ".NET"; var blogs = context.Blogs .FromSql("SELECT * FROM dbo.SearchBlogs {0}", searchTerm) .Include(b => b.Posts) .ToList();
اجرای عبارات SQL، بدون بازگشت مقداری
تا اینجا در مورد عبارات SQL از نوع Select و یا اجرای رویههای ذخیره شده، بحث شد. برای اجرای عبارات SQL ایی مانند update و delete میتوان از متد ExecuteSqlCommand مربوط به context.Database استفاده کرد:
context.Database.ExecuteSqlCommand("UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 30");
context.Database.ExecuteSqlCommand("usp_CreateShipper @p0, @p1", parameters: new[] { "hello", "world" });
اجرای عبارات SQL و دریافت خروجیهایی به غیر از موجودیتهای برنامه
در ابتدا بحث عنوان شد که محدودیت فعلی کوئریهای FromSQL که میتوانند خروجی را نیز ارائه دهند، مقید بودن آنها به DbSet در حال استفاده است و محدود بودن آنها به خواص کلاس متناظر تعریف شده. در این حالت اگر بخواهیم یک محاسبهی عددی را بازگشت دهیم چه باید کرد؟
متد ExecuteSqlCommand تنها وضعیت نهایی اجرای عملیات را بازگشت میدهد و FromSQL مقید است به DbSet متناظر. برای رفع این محدودیتها میتوان مستقیما به DbConnection دسترسی یافت و سپس کوئری گرفت؛ به نحو ذیل:
using (var connection = context.Database.GetDbConnection()) { connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = "SELECT COUNT(*) FROM Contacts"; var result = command.ExecuteScalar().ToString(); } }
اشتراکها
معرفی NET Core 3 Local Tools.
چندی قبل مطلبی را در مورد پیاده سازی سطح دوم کش در EF در این سایت مطالعه کردید. اساس آن مقالهای بود که نحوهی کش کردن اطلاعات حاصل از LINQ to Objects را بیان کرده بود (^). این مقاله پایهی بسیاری از سیستمهای کش مشابه نیز شدهاست (^ و ^ و ...).
مشکل مهم این روش عدم سازگاری کامل آن با EF است. برای مثال در آن تفاوتی بین (Include(x=>x.Tags و (Include(x=>x.Users وجود ندارد. به همین جهت در این نوع موارد، قادر به تولید کلید منحصربفردی جهت کش کردن اطلاعات یک کوئری مشخص نیست. در اینجا یک کوئری LINQ، به معادل رشتهای آن تبدیل میشود و سپس Hash آن محاسبه میگردد. این هش، کلید ذخیره سازی اطلاعات حاصل از کوئری، در سیستم کش خواهد بود. زمانیکه دو کوئری Include دار متفاوت EF، هشهای یکسانی را تولید کنند، عملا این سیستم کش، کارآیی خودش را از دست میدهد. برای رفع این مشکل پروژهی دیگری به نام EF cache ارائه شدهاست. این پروژه بسیار عالی طراحی شده و میتواند جهت ایده دادن به تیم EF نیز بکار رود. اما در آن فرض بر این است که شما میخواهید کل سیستم را در یک کش قرار دهید. وارد مکانیزم DBCommand و DataReader میشود و در آنجا کار کش کردن تمام کوئریها را انجام میدهد؛ مگر آنکه به آن اعلام کنید از کوئریهای خاصی صرفنظر کند.
با توجه به این مشکلات، روش بهتری برای تولید هش یک کوئری LINQ to Entities بر اساس کوئری واقعی SQL تولید شده توسط EF، پیش از ارسال آن به بانک اطلاعاتی به صورت زیر وجود دارد:
این متد یک کوئری LINQ مخصوص EF را دریافت میکند و با کمک Reflection، اطلاعات درونی آن که شامل ObjectQuery اصلی است را استخراج میکند. سپس فراخوانی متد objectQuery.ToTraceString بر روی حاصل آن، سبب تولید SQL معادل کوئری LINQ اصلی میگردد. همچنین objectQuery امکان دسترسی به پارامترهای تنظیم شدهی کوئری را نیز میسر میکند. به این ترتیب میتوان به معادل رشتهای منطقیتری از یک کوئری LINQ رسید که قابلیت تشخیص JOINها و متد Include نیز به صورت خودکار در آن لحاظ شدهاست.
این اطلاعات، پایهی تهیهی کتابخانهی جدیدی به نام EFSecondLevelCache گردید. برای نصب آن کافی است دستور ذیل را در کنسول پاورشل نیوگت صادر کنید:
سپس برای کش کردن کوئری معمولی مانند:
میتوان از متد جدید Cacheable آن به نحو ذیل استفاده کرد (این روش بسیار تمیزتر است از روش مقالهی قبلی و امکان استفادهی از انواع و اقسام متدهای EF را به صورت متداولی میسر میکند):
پس از آن نیاز است کدهای کلاس Context خود را نیز به نحو ذیل ویرایش کنید (به روز رسانی شدهی آن در اینجا):
متد InvalidateCacheDependencies سبب میشود تا اگر تغییری در بانک اطلاعاتی رخداد، به صورت خودکار کشهای کوئریهای مرتبط غیر معتبر شوند و برنامه اطلاعات قدیمی را از کش نخواند.
کدهای کامل این پروژه را از مخزن کد ذیل میتوانید دریافت کنید:
EFSecondLevelCache
پ.ن.
این کتابخانه هم اکنون در سایت جاری در حال استفاده است.
مشکل مهم این روش عدم سازگاری کامل آن با EF است. برای مثال در آن تفاوتی بین (Include(x=>x.Tags و (Include(x=>x.Users وجود ندارد. به همین جهت در این نوع موارد، قادر به تولید کلید منحصربفردی جهت کش کردن اطلاعات یک کوئری مشخص نیست. در اینجا یک کوئری LINQ، به معادل رشتهای آن تبدیل میشود و سپس Hash آن محاسبه میگردد. این هش، کلید ذخیره سازی اطلاعات حاصل از کوئری، در سیستم کش خواهد بود. زمانیکه دو کوئری Include دار متفاوت EF، هشهای یکسانی را تولید کنند، عملا این سیستم کش، کارآیی خودش را از دست میدهد. برای رفع این مشکل پروژهی دیگری به نام EF cache ارائه شدهاست. این پروژه بسیار عالی طراحی شده و میتواند جهت ایده دادن به تیم EF نیز بکار رود. اما در آن فرض بر این است که شما میخواهید کل سیستم را در یک کش قرار دهید. وارد مکانیزم DBCommand و DataReader میشود و در آنجا کار کش کردن تمام کوئریها را انجام میدهد؛ مگر آنکه به آن اعلام کنید از کوئریهای خاصی صرفنظر کند.
با توجه به این مشکلات، روش بهتری برای تولید هش یک کوئری LINQ to Entities بر اساس کوئری واقعی SQL تولید شده توسط EF، پیش از ارسال آن به بانک اطلاعاتی به صورت زیر وجود دارد:
private static ObjectQuery TryGetObjectQuery<T>(IQueryable<T> source) { var dbQuery = source as DbQuery<T>; if (dbQuery != null) { const BindingFlags privateFieldFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public; var internalQuery = source.GetType().GetProperty("InternalQuery", privateFieldFlags) .GetValue(source); return (ObjectQuery)internalQuery.GetType().GetProperty("ObjectQuery", privateFieldFlags) .GetValue(internalQuery); } return null; }
این اطلاعات، پایهی تهیهی کتابخانهی جدیدی به نام EFSecondLevelCache گردید. برای نصب آن کافی است دستور ذیل را در کنسول پاورشل نیوگت صادر کنید:
PM> Install-Package EFSecondLevelCache
var products = context.Products.Include(x => x.Tags).FirstOrDefault();
var products = context.Products.Include(x => x.Tags).Cacheable().FirstOrDefault(); // Async methods are supported too.
پس از آن نیاز است کدهای کلاس Context خود را نیز به نحو ذیل ویرایش کنید (به روز رسانی شدهی آن در اینجا):
namespace EFSecondLevelCache.TestDataLayer.DataLayer { public class SampleContext : DbContext { // public DbSet<Product> Products { get; set; } public SampleContext() : base("connectionString1") { } public override int SaveChanges() { return SaveAllChanges(invalidateCacheDependencies: true); } public int SaveAllChanges(bool invalidateCacheDependencies = true) { var changedEntityNames = getChangedEntityNames(); var result = base.SaveChanges(); if (invalidateCacheDependencies) { new EFCacheServiceProvider().InvalidateCacheDependencies(changedEntityNames); } return result; } private string[] getChangedEntityNames() { return this.ChangeTracker.Entries() .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified || x.State == EntityState.Deleted) .Select(x => ObjectContext.GetObjectType(x.Entity.GetType()).FullName) .Distinct() .ToArray(); } } }
کدهای کامل این پروژه را از مخزن کد ذیل میتوانید دریافت کنید:
EFSecondLevelCache
پ.ن.
این کتابخانه هم اکنون در سایت جاری در حال استفاده است.