بررسی بهبودهای کارآیی در NET 7.
TL;DR: .NET 7 is fast. Really fast. A thousand performance-impacting PRs went into runtime and core libraries this release, never mind all the improvements in ASP.NET Core and Windows Forms and Entity Framework and beyond. It’s the fastest .NET ever. If your manager asks you why your project should upgrade to .NET 7, you can say “in addition to all the new functionality in the release, .NET 7 is super fast.”
Metadata Function ها در SQL Server
To be able to make full use of the system catalog to find out more about a database, you need to be familiar with the metadata functions. They save a great deal of time and typing when querying the metadata. Once you get the hang of these functions, the system catalog suddenly seems simple to use, as Robert Sheldon demonstrates in this article.
معرفی کتابخانهی DNTPersianUtils.Core
---> System.PlatformNotSupportedException: This OS[Browser] doesn't support IranStandardTime.
at DNTPersianUtils.Core.DateTimeUtils..cctor()
سری چالش یادگیری NET.
بررسی تازههای Angular CLI 8.2
فرض کنید یک صفحهی Blazor SSR، از سه کامپوننت منوی سمت راست، محتوای اصلی صفحه و فوتر سایت که به همراه متنی است، تشکیل شدهاست. منوی سمت راست، به همراه لینکهاییاست که آمار آنها را نیز نمایش میدهد و این اطلاعات را از بانک اطلاعاتی، به کمک EF-Core دریافت میکند. فوتر صفحه، سال شروع به کار و نام برنامه را از بانک اطلاعاتی دریافت میکند و محتوای اصلی صفحه نیز از بانک اطلاعاتی دریافت میشود. پس از تکمیل این سه کامپوننت مجزا، اگر برنامه را اجرا کنید، بلافاصله با خطای زیر مواجه میشوید:
A second operation started on this context before a previous operation completed
مشکل کجاست؟! مشکل اینجاست که تنها یک نمونه از DbContext، در طول درخواست جاری رسیده، بین سه کامپوننت جاری برنامه به اشتراک گذاشته میشود (به سازندهی سرویسهای مرتبط تزریق میشود) و ... در Blazor SSR، پردازش کامپوننتهای یک صفحه، به صورت موازی و همزمان انجام میشوند؛ یعنی ترتیبی نیست. اگر ابتدا کامپوننت منو، بعد محتوای صفحه و در آخر فوتر، رندر میشدند، هیچگاه پیام فوق را مشاهده نمیکردیم؛ اما ... هر سه کامپوننت، با هم و همزمان رندر میشوند و سپس نتیجهی نهایی در Response درج خواهد شد. یعنی یک DbContext بین چندین ترد به اشتراک گذاشته میشود که چنین حالتی توسط EF-Core پشتیبانی نمیشود و مجاز نیست.
روش مواجه شدن با یک چنین حالتهایی، نمونه سازی مجزای DbContext به ازای هر کامپوننت است که نمونهای از آنرا پیشتر در مطلب «نکات ویژهی کار با EF-Core در برنامههای Blazor Server» مشاهده کردهاید. در این مطلب، راهحل دیگری برای اینکار ارائه میشود که سادهتر است و نیازی به تغییرات آنچنانی در کدهای کامپوننتها و کل برنامه ندارد.
استفاده از کلاس پایهی OwningComponentBase برای نمونه سازی مجدد DbContext بهازای هر کامپوننت
زمانیکه در برنامههای Blazor SSR از روش استاندارد زیر برای دسترسی به سرویسهای مختلف برنامه استفاده میکنیم:
@inject IHotelRoomService HotelRoomService
طول عمر دریافتی سرویس، دقیقا بر اساس طول عمر اصلی تعریف شدهی آن عمل میکند (شبیه به برنامههای ASP.NET Core). یعنی برای مثال اگر Scoped باشد، DbContext تزریق شدهی در آن هم Scoped است و این DbContext، بین تمام کامپوننتهای در حال پردازش موازی در طول یک درخواست، بهاشتراک گذاشته میشود که مطلوب ما نیست. ما میخواهیم بتوانیم به ازای هر کامپوننت مجزای صفحه، یک DbContext جدید داشته باشیم. یعنی باید بتوانیم خودمان این سرویس Scoped را نمونه سازی کنیم و نه اینکه آنرا مستقیما از سیستم تزریق وابستگیها دریافت کنیم.
بنابراین اگر بخواهیم قسمتهای مختلف برنامه را تغییر ندهیم و همان تعاریف ابتدایی services.AddDbContext و Scoped تعریف کردن سرویسهای برنامه بدون تغییر باقی بمانند (و از IDbContextFactory و موارد مشابه دیگر مطلب «نکات ویژهی کار با EF-Core در برنامههای Blazor Server» هم استفاده نکنیم)، باید جایگزینی را برای نمونه سازی سرویسها ارائه دهیم. به همین جهت در ابتدا، یک ویژگی جدیدی را به صورت زیر تعریف میکنیم:
[AttributeUsage(AttributeTargets.Property)] public sealed class InjectComponentScopedAttribute : Attribute { }
تا بتوانیم بجای:
@inject IHotelRoomService HotelRoomService
بنویسیم:
[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;
مرحلهی بعد، نوبت به نمونه سازی خودکار این سرویسهای درخواستی علامتگذاری شدهی با InjectComponentScoped است. برای این منظور، تمام کامپوننتهای برنامه را از کلاس پایه و استاندارد OwningComponentBase ارثبری میکنیم. مزیت اینکار، امکان دسترسی به خاصیتی به نام ScopedServices در تمام کامپوننتهای برنامه است که توسط آن میتوان به متد ScopedServices.GetRequiredService آن دسترسی یافت. یعنی با ارثبری از کلاس پایهی OwningComponentBase به ازای هر کامپوننت، به صورت خودکار Scope جدیدی شروع میشود که توسط آن میتوان به نمونهی جدیدی از سرویس مدنظر دسترسی یافت و نه به نمونهی اشتراکی در طی درخواست جاری.
اکنون اگر از این مزیت به صورت زیر استفاده کنیم، میتوان تمام سرویسهای درخواستی مزین به InjectComponentScopedAttribute یک کامپوننت را به صورت خودکار یافته و با استفاده از ScopedServices.GetRequiredService، مقدار دهی کرد:
public class BlazorScopedComponentBase : OwningComponentBase { private static readonly ConcurrentDictionary<Type, Lazy<List<PropertyInfo>>> CachedProperties = new(); private List<PropertyInfo> InjectComponentScopedPropertiesList => CachedProperties.GetOrAdd(GetType(), type => new Lazy<List<PropertyInfo>>( () => type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) .Where(p => p.GetCustomAttribute<InjectComponentScopedAttribute>() is not null) .ToList(), LazyThreadSafetyMode.ExecutionAndPublication)).Value; protected override void OnInitialized() { foreach (var propertyInfo in InjectComponentScopedPropertiesList) { propertyInfo.SetValue(this, ScopedServices.GetRequiredService(propertyInfo.PropertyType)); } } }
این سرویس، اینبار طول عمری، محدود به کامپوننت جاری را خواهد داشت و بین سایر کامپوننتهای درحال پردازش درخواست جاری، به اشتراک گذاشته نمیشود و همچنین به صورت خودکار هم در پایان درخواست، Dispose میشود.
فعالسازی ارثبری خودکار در تمام کامپوننتهای برنامه
مرحلهی بعد، ارثبری خودکار تمام کامپوننتهای برنامه از OwningComponentBase سفارشی فوق است و در اینجا قصد نداریم تمام کامپوننتها را جهت معرفی آن، به صورت دستی تغییر دهیم. برای اینکار فقط کافی است به فایل Imports.razor_ مراجعه و یک سطر زیر را در آن درج کنیم:
@inherits BlazorScopedComponentBase
با اینکار یک ارثبری سراسری در کل برنامه رخ میدهد و تمام کامپوننتها، از BlazorScopedComponentBase مشتق خواهند شد. یعنی پس از این تغییر، اگر سرویسی را به صورت زیر معرفی و با ویژگی InjectComponentScoped علامتگذاری کردیم:
[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;
به صورت خودکار یافت شده و نمونه سازی Scoped محدود به طول عمر همان کامپوننت میشود که بین سایر کامپوننتها، به اشتراک گذاشته نخواهد شد.
یک نکته: اگر کامپوننت شما متد OnInitialized را بازنویسی میکند، فراموش نکنید که در ابتدای آن باید ()base.OnInitialized را هم فراخوانی کنید تا متد OnInitialized کامپوننت پایهی BlazorScopedComponentBase نیز فراخوانی شود. البته این مورد در حین بازنویسی نمونهی async آن مهم نیست؛ چون همیشه OnInitialized غیر async در ابتدا فراخوانی میشود و سپس نمونهی async آن اجرا خواهد شد.
context.Chapters.Add(new Chapter { Title = "آزمایش متن فارسی", Text = "برای نمونه تهیه شدهاست", User = user1.Entity });
کامپایل افزونهی spell fix1
افزونهی spell fix، به همراه هیچکدام از توزیعهای باینری SQLite ارائه نمیشود. ارائهی آن فقط به صورت سورس کد است و باید خودتان آنرا کامپایل کنید!
برای این منظور ابتدا به آدرس https://www.sqlite.org/src/dir?ci=99749d4fd4930ccf&name=ext/misc مراجعه کرده و فایل ext/misc/spellfix.c آنرا دریافت کنید. اگر بر روی لینک spellfix.c کلیک کنید، در نوار ابزار بالای صفحهی بعدی، لینک download آن هم وجود دارد.
سپس به صفحهی دریافت اصلی SQLite یعنی https://www.sqlite.org/download.html مراجعه کرده و بستهی amalgamation آنرا دریافت کنید. این بسته به همراه کدهای اصلی SQLite است که باید در کنار افزونههای آن قرار گیرند تا بتوان این افزونهها را کامپایل کرد. بنابراین پس از دریافت بستهی amalgamation و گشودن آن، فایل spellfix.c را به داخل پوشهی آن کپی کنید:
اکنون نوبت به کامپایل فایل spellfix.c و تبدیل آن به یک dll است تا بتوان آنرا به صورت یک افزونه در برنامه بارگذاری کرد. برای این منظور از هر کامپایلر ++C ای میتوانید استفاده کنید. برای نمونه به آدرس http://www.codeblocks.org/downloads/binaries مراجعه کرده و بستهی codeblocks-20.03mingw-setup.exe را دریافت کنید (بستهای که به همراه mingw است). پس از نصب آن، در مسیر C:\Program Files (x86)\CodeBlocks\MinGW\bin میتوانید کامپایلر چندسکویی gcc را مشاهده کنید. توسط آن میتوان با اجرای دستور زیر، سبب تولید فایل spellfix1.dll شد:
"C:\Program Files (x86)\CodeBlocks\MinGW\bin\gcc.exe" -g -shared -fPIC -Wall D:\path\to\sqlite-amalgamation-3310100\spellfix.c -o spellfix1.dll
روش معرفی افزونههای SQLite به Microsoft.Data.Sqlite
EF Core، از بستهی Microsoft.Data.Sqlite در پشت صحنه برای کار با SQLite استفاده میکند و در اینجا هم برای معرفی افزونهی کامپایل شده، باید ابتدا آنرا به اتصال برقرار شده، معرفی کرد. خود Sqlite در ویندوز، افزونههایش را بر اساس معرفی مستقیم مسیر فایل dll آنها بارگذاری نمیکند. بلکه path ویندوز را برای جستجوی آنها بررسی کرده و در صورتیکه فایل dll ای را افزونه تشخیص داد، آنرا بارگذاری میکند. بنابراین یا باید به صورت دستی مسیر فایل dll تولید شده را به متغیر محیطی path ویندوز اضافه کرد و یا میتوان توسط قطعه کد زیر، آنرا به صورت پویایی معرفی کرد:
using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; namespace EFCoreSQLiteFTS.DataLayer { public static class LoadSqliteExtensions { public static void AddToSystemPath(string extensionsDirectory) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new NotSupportedException("Modifying the path at runtime only works on Windows. On Linux and Mac, set LD_LIBRARY_PATH or DYLD_LIBRARY_PATH before running the app."); } var path = new HashSet<string>(Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)); if (path.Add(extensionsDirectory)) { Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, path)); } } } }
در ادامه پیش از معرفی services.AddDbContext، باید مسیر پوشهی افزونهها را ثبت کرد و سپس UseSqlite را به همراه اتصالی استفاده کرد که توسط متد LoadExtension آن، افزونهی spellfix1 به آن معرفی شدهاست:
LoadSqliteExtensions.AddToSystemPath("path to .dll file"); services.AddDbContext<ApplicationDbContext>((serviceProvider, optionsBuilder) => { var connection = new SqliteConnection(connectionString); connection.Open(); connection.LoadExtension("spellfix1"); // Passing in an already open connection will keep the connection open between requests. optionsBuilder.UseSqlite(connection); });
ایجاد جداول ویژهی spell fix در برنامه
در قسمت اول، با متد createFtsTables آشنا شدیم. اکنون این متد را برای ایجاد جداول کمکی مرتبط با افزونهی spell fix به صورت زیر تکمیل میکنیم:
private static void createFtsTables(ApplicationDbContext context) { // For SQLite FTS // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too. context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS"" USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");"); // 'SQLite Error 1: 'no such module: spellfix1'.' --> must be loaded ... // EditCost for unicode support context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_Vocab USING fts5vocab('Chapters_FTS', 'row');"); context.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Chapters_FTS_SpellFix_EditCost(iLang INT, cFrom TEXT, cTo TEXT, iCost INT);"); context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_SpellFix USING spellfix1(edit_cost_table=Chapters_FTS_SpellFix_EditCost);"); }
- همانطور که مشاهده میکنید، ابتدا بر اساس Chapters_FTS یا همان جدول مجازی FTS برنامه، یک جدول مجازی از نوع fts5vocab ایجاد میشود. کار آن استخراج توکنهای FTS و آماده سازی آنها برای استفاده در غلط یاب املایی هستند.
- سپس جدول ویژهی EditCost را مشاهده میکنید. نام آن مهم نیست، اما ساختار آن باید دقیقا به همین صورت باشد. اگر این جدول اختیاری را تهیه کنیم، الگوریتم spellfix1 به utf8 سوئیچ خواهد کرد و برای پردازش متون یونیکد، بدون مشکل کار میکند. بدون آن، جستجوهای فارسی نتایج مطلوبی را به همراه نخواهند داشت.
- در آخر جدول مجازی مرتبط با spellfix1 که از جدول cost_table معرفی شده استفاده میکند، ایجاد شدهاست.
اجرای این دستورات، جداول زیر را ایجاد میکنند (که ساختار آنها استاندارد است و باید مطابق فرمولهای مستندات آنها باشد):
به روز رسانی جدول واژه نامهی غلط یابی برنامه
آخرین جدولی را که ایجاد کردیم، Chapters_FTS_SpellFix است که اطلاعات خودش را از Chapters_FTS_Vocab دریافت میکند:
هر بار که بانک اطلاعاتی را به روز میکنیم، نیاز است اطلاعات این جدول را نیز توسط دستور زیر به روز کرد:
database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS_SpellFix(word, rank) SELECT term, cnt FROM Chapters_FTS_Vocab WHERE term not in (SELECT word from Chapters_FTS_SpellFix_vocab)");
database.ExecuteSqlRaw("INSERT INTO Chapters_FTS_SpellFix(command) VALUES(\"reset\");");
کوئری گرفتن از جدول مجازی Chapters_FTS_SpellFix
تا اینجا افزونهی spellfix1 را کامپایل و به سیستم معرفی کردیم. سپس جداول واژه نامهی آنرا نیز تشکیل دادیم، اکنون نوبت به کوئری گرفتن از آن است. به همین جهت یک موجودیت بدون کلید دیگر را بر اساس ساختار خروجی کوئریهای آن ایجاد کرده:
namespace EFCoreSQLiteFTS.Entities { public class SpellCheck { public string Word { get; set; } public decimal Rank { get; set; } public decimal Distance { get; set; } public decimal Score { get; set; } public decimal Matchlen { get; set; } } }
namespace EFCoreSQLiteFTS.DataLayer { public class ApplicationDbContext : DbContext { //... protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<SpellCheck>().HasNoKey().ToView(null); } //... } }
در آخر، کوئری گرفتن از این جدول، ساختار زیر را دارد:
foreach (var item in context.Set<SpellCheck>().FromSqlRaw( @"SELECT word, rank, distance, score, matchlen FROM Chapters_FTS_SpellFix WHERE word MATCH {0} and top=6", "فارشی")) { Console.WriteLine($"Word: {item.Word}"); Console.WriteLine($"Distance: {item.Distance}"); }
top=6 در این کوئری خاص یعنی 6 رکورد را بازگشت بده.
یک نکته: اگر میخواهید کوئری فوق را توسط برنامهی «DB Browser for SQLite» اجرا کنید، باید از منوی tools آن، گزینهی load extension را انتخاب کرده و فایل dll افزونه را به برنامه معرفی کنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.