اشتراکها
نظرات مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
مثال 6 ام این سری به همراه پردازش anti-forgery-token در برنامههای ASP.NET Core به همراه Kendo-UI نبود. نکات پایه آن با مطلب «افزودن و اعتبارسنجی خودکار Anti-Forgery Tokens در برنامههای Angular مبتنی بر ASP.NET Core» یکی است و به صورت خلاصه برای Kendo-UI به این صورت است. این روش با تمام اسکریپتهای مبتنی بر jQuery Ajax کار میکند.
بحث EF متفاوت است و کاربرد گستردهای دارد؛ از وب تا دسکتاپ و غیره. در تعدادی سکوهای کاری، synchronization context نال هست و در تعدادی دیگر خیر. در ASP.NET Core نال هست و در موارد دیگر خیر. خلاصه به همین جهت مجبور شدند اینکار را انجام دهند. باید ببینید استفاده کنندهی از کتابخانهی شما بیشتر چه کاربردی را دنبال میکند؛ وب هست یا دسکتاپ؟ دات نت قدیم هست یا جدید؟ یک زمانی از EF-Core میشد در برنامههای داتنت قدیم هم استفاده کرد (نگارشهای جدیدتر آن خیر).
- این خطا یعنی سرویس IConfigurationRoot را به سیستم تزریق وابستگیها معرفی نکردید.
- تنظیمات اولیهی EF Core از زمان نگارش 2 آن اندکی تغییر کرده؛ خلاصهی نهایی و به روز شدهی آن در پروژهی DNTIdentity و یا پروژهی «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» موجود است (هر دو پروژه به آخرین نگارش SDK، به روز رسانی شدهاند).
نظرات مطالب
روش یکی کردن پروژههای React و ASP.NET Core
رابط کاربری مطلب «اعتبارسنجی مبتنی بر کوکیها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» یک صفحهی HTML خالص است که نکات آن برای برنامههای تک صفحهای وب هم صادق است.
آیا میتوان بدون استفاده از JWT در برنامههای سمت کلاینت هم کار اعتبارسنجی را انجام داد؟
بله. یک نمونه مثال از یک برنامهی Angular که از روش مبتنی بر کوکی ASP.NET Core برای اعتبارسنجی استفاده میکند (بجای استفادهی از JWT).
در دنیای NET. همواره دو نوع LINQ وجود داشته داشته است: LINQ to Objects و ... مابقی. در حالت اول با <IEnumerable<Tها کار میکنیم که تمام عملیات در حافظه انجام میشود و در مابقی حالات یک <IQueryable<T وجود دارد که عبارت حاصل از آن جهت کاربردهای مختلفی به زبانهای متفاوتی مانند SQL ترجمه میشوند. در هر دو حالت کلی، Syntax نهایی یکی است و تنها اگر به منبع دادهی آنها دقت کنیم، میتوانیم نوع آنها را تشخیص دهیم. برای نمونه کوئری ذیل بر اساس منبع Blogs است که میتواند LINQ to Objects باشد و یا حالت <Queryable<Blog که قرار است به زبانی مشخص ترجمه شود:
اکنون فرض کنید که این عبارت قرار است به SQL ترجمه شده و سپس بر روی یک بانک اطلاعاتی اجرا شود. در این حالت مفسر LINQ باید بداند که متد Contains را چگونه به معادل SQL آن ترجمه کند و این ترجمه میتواند بر اساس بانکهای اطلاعاتی مختلف، متفاوت نیز باشد. اما در حالت LINQ to Objects یک چنین مشکلی وجود ندارد و این ترجمه مستقیما بر روی متد Contains کلاس string انجام میشود.
اما اکنون چطور؟
فرض کنید یک متد الحاقی را به نام ComputeHash به کلاس string اضافه کردهایم. یک چنین کوئری را اگر بر روی EF 6.x اجرا کنیم، برنامه با یک استثناء متوقف خواهد شد؛ چون امکان ترجمهی متد ComputeHash را به معادل SQL آن ندارد؛ اما EF Core برای انجام یک چنین کوئریهایی بهبود یافتهاست که به آن، محاسبات سمت کلاینت گفته میشود.
یک مثال: بررسی تاثیر ارزیابیهای سمت کلاینت در EF Core
فرض کنید ساختار جدول بلاگها به صورت زیر است:
همچنین یک متد الحاقی را به نام ComputeHash به صورت ذیل تعریف کردهایم:
اکنون میخواهیم بلاگهایی را پیدا کنیم که Hash مربوط به Url آنها بیشتر از 10 است (صرفا جهت نمایش این قابلیت جدید):
اگر این کوئری را اجرا کنیم، یک چنین خروجی SQL ایی تولید خواهد شد و همچنین برنامه کرش هم نمیکند:
به این معنا که در ارزیابیهای سمت کلاینت:
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده میکند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آنرا از کوئری حذف میکند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده میشود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کردهاست، متد ComputeHash را در سمت کلاینت بر روی این نتیجهی دریافتی اعمال میکند. یعنی مرحلهی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.
چگونه متوجه شویم که ارزیابی سمت کلاینت رخ دادهاست؟
EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابیهای سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
این مثال بدون مشکل توسط EF Core و قابلیت جدید ارزیابی سمت کلاینت آن اجرا میشود، اما بهتر است از وقوع یک چنین رخدادهایی مطلع شویم:
برای این منظور تنها کافی است درخواست فعالسازی لاگ کردن QueryClientEvaluationWarning را در قسمت ConfigureWarnings آن ارائه دهیم. در این حالت اگر برنامه را مجددا اجرا کنیم، ابتدا یک چنین خروجی لاگ میشود:
عنوان کردهاست که قابلیت ترجمهی ComputeHash را به SQL نداشته و آنرا در نهایت به صورت محلی و در سمت کلاینت محاسبه میکند.
اگر میخواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابیها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).
تاثیر ارزیابیهای سمت کلاینت بر روی کارآیی برنامه
هرچند قابلیت ارزیابیهای سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبهی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال میشود.
ب) این قابلیت ممکن است برنامه نویسها را از تفکر در مورد یافتن روشهای محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا میشود، اما اگر آنرا به صورت ذیل جایگزین کنیم:
اینبار به یک خروجی SQL قابل محاسبهی در سمت سرور، خواهیم رسید:
به همین جهت حداقل لاگ کردن ارزیابیهای سمت کلاینت را روشن کنید تا از وقوع یک چنین مسایلی مطلع گردید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: ClientSideEvaluation.zip
var blogs = from blog in Blogs where blog.Name.Contains("Development") select blog;
اما اکنون چطور؟
var blogs = from blog in Blogs where blog.Name.ComputeHash() == 0 select blog;
یک مثال: بررسی تاثیر ارزیابیهای سمت کلاینت در EF Core
فرض کنید ساختار جدول بلاگها به صورت زیر است:
public class Blog { public int BlogId { get; set; } public string Url { get; set; } }
public static class StringExtensions { public static int ComputeHash(this string str) { var hash = 0; foreach (var ch in str) { hash += (int)ch; } return hash; } }
using (var context = new BloggingContext()) { var blogs = context.Blogs .Where(blog => blog.Url.ComputeHash() >= 10) .ToList(); Console.WriteLine(blogs.First().Url); }
SELECT [blog].[BlogId], [blog].[Url] FROM [Blogs] AS [blog]
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده میکند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آنرا از کوئری حذف میکند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده میشود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کردهاست، متد ComputeHash را در سمت کلاینت بر روی این نتیجهی دریافتی اعمال میکند. یعنی مرحلهی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.
چگونه متوجه شویم که ارزیابی سمت کلاینت رخ دادهاست؟
EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابیهای سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
var idUrls = context.Blogs .Select(b => new { IdUrlString = string.Join(", ", b.BlogId, b.Url), }).ToList();
public class BloggingContext : DbContext { public BloggingContext() { } public BloggingContext(DbContextOptions options) : base(options) { } public DbSet<Blog> Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ClientSideEvaluation;Trusted_Connection=True;"); optionsBuilder.ConfigureWarnings(warnings => { warnings.Log(CoreEventId.IncludeIgnoredWarning); warnings.Log(RelationalEventId.QueryClientEvaluationWarning); }); } } }
warn: Microsoft.EntityFrameworkCore.Query[200500] The LINQ expression 'where ([blog].Url.ComputeHash() >= 10)' could not be translated and will be evaluated locally.
اگر میخواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابیها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).
تاثیر ارزیابیهای سمت کلاینت بر روی کارآیی برنامه
هرچند قابلیت ارزیابیهای سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبهی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال میشود.
ب) این قابلیت ممکن است برنامه نویسها را از تفکر در مورد یافتن روشهای محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا میشود، اما اگر آنرا به صورت ذیل جایگزین کنیم:
var idUrls2 = context.Blogs .Select(b => new { IdUrlString = b.BlogId + "," + b.Url }).ToList();
SELECT (CAST([b].[BlogId] AS nvarchar(max)) + N',') + [b].[Url] AS [IdUrlString] FROM [Blogs] AS [b]
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: ClientSideEvaluation.zip
تغییر پویای رشتهی اتصالی به بانک اطلاعاتی در نگارشهای پیشین EF، مشکل بودند که نمونههایی از آن را پیشتر در مطالب زیر مشاهده کردهاید:
- «تنظیم رشته اتصالی Entity Framework به بانک اطلاعاتی به وسیله کد»
- «استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First»
اما EF Core نه تنها این مشکل را پوشش را دادهاست، بلکه امکان تزریق وابستگیها و استفادهی از سرویسهای مختلف را نیز در این حین، پیش بینی کردهاست که در ادامه جزئیات آنرا مرور میکنیم.
نیاز به تغییر رشتهی اتصالی به بانک اطلاعاتی در زمان اجرا
دلایل نیاز به امکان تغییر رشتهی اتصالی در زمان اجرا شامل موارد زیر هستند:
- در برنامههایی کمی پیچیدهتر و سابقه دار، ممکن است عملیات تجاری یکسال را در بانک اطلاعاتی سال 98 و دیگری را در بانک اطلاعاتی سال 99 ثبت کنید. در این حالت کاربران باید بتوانند در زمان اجرا به هر بانک اطلاعاتی که پیشتر با آن کار کردهاند، متصل شده و از آن استفاده کنند.
- یکی از روشهای پیاده سازی برنامههای چند مستاجری، داشتن یک بانک اطلاعاتی مجزا، به ازای هر مستاجر است. در این حالت نیز تک برنامهی ما باید بتواند بر اساس Id مشتری، بانک اطلاعاتی متناظری را در زمان اجرا انتخاب کند.
- نیاز به داشتن چندین context در برنامه و کار با بانکهای اطلاعاتی متفاوت در زمان اجرا؛ مانند کار با SQL Server، اوراکل و یا SQLite
روش تغییر رشتهی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه
اگر به روش ثبت متداول سرویس DbContext برنامه و پروایدر آن دقت کنیم:
یک action delegate قابل مشاهدهاست. کار این اکشن، تنظیم پروایدر و تمام نیازهای یک رشتهی اتصالی به بانک اطلاعاتی، جهت شروع به کار با Context برنامه است. نکتهی مهمی که در اینجا وجود دارد، فراخوانی هربارهی این action، به ازای هر اتصال تشکیل شدهاست. یعنی کدهای داخل این action delegate کش نمیشوند و همین مساله امکان تغییر پویای آنها را میسر میکند.
یک نکته: چون این اطلاعات کش نمیشوند، اگر رشتهی اتصالی شما ثابت است (و نیازی به تغییر آن در زمان اجرای برنامه نیست)، محل تامین آنرا به پیش از سطر services.AddDbContext انتقال دهید و فقط نتیجهی محاسبه شدهی نهایی را استفاده کنید تا کارآیی برنامه افزایش یابد؛ در غیراینصورت فراخوانی Configuration.GetConnectionString مدام تکرار خواهد شد.
دریافت یک قالب قابل تغییر از تنظیمات برنامه و تغییر آن با هدرهای درخواست رسیدهی به آن
فرض کنید قالب رشتهی اتصالی برنامه در فایل appsettings.json به صورت زیر است:
و db_Name آن قرار است برای مثال از یک query string، سشن، کوکی و یا فیلد خاصی در هدر HTTP رسیده تامین شود. برای مثال سال مالی انتخابی و یا شماره مستاجر انتخابی به صورت یک فیلد خاص HTTP به سمت برنامه ارسال میشوند.
بنابراین اکنون نیاز است به ازای هر درخواست رسیده بتوان به سرویس IHttpContextAccessor و شیء HttpContext.Request جاری دسترسی یافت و سپس از هدرهای رسیده، برای مثال هدر ویژهی tenantId و یا year را پردازش کرد؛ اما در تعریف services.AddDbContext فوق چگونه میتوان اینکار را انجام داد؟
خوشبختانه متد services.AddDbContext، دارای یک overload دیگر نیز هست که امکان دسترسی به تمام سرویسهای جاری سیستم را میسر میکند:
همانطور که مشاهده میکنید، overload دوم متد services.AddDbContext، امکان ارسال serviceProvider را نیز به این action delegate دارد. پس از آن میتوان توسط متد GetRequiredService آن به هر سرویس مدنظری که در سیستم ثبت شدهاست، دسترسی یافت و برای مثال در اینجا فیلد هدر سفارشی tenantId را از آن استخراج نمود و در قالب رشتهی اتصالی به بانک اطلاعاتی، در زمان اجرا به صورت پویایی جایگزین کرد.
همچنین در صورت نیاز میتوان UseSqlServer آنرا نیز در این action delegate به هر پروایدر دیگری در زمان اجرا تغییر داد و از این لحاظ محدودیتی وجود ندارد.
یک نکته: البته برنامه نباید هر tenantId ای را پردازش کند و این خودش میتواند تبدیل به یک نقیصهی امنیتی شود. به همین جهت برای مثال میتوان tenantId را در یک JWT قرار داد و در حین تعیین اعتبار آن و کاربر جاری، این مقدار را نیز بررسی کرد.
- «تنظیم رشته اتصالی Entity Framework به بانک اطلاعاتی به وسیله کد»
- «استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First»
اما EF Core نه تنها این مشکل را پوشش را دادهاست، بلکه امکان تزریق وابستگیها و استفادهی از سرویسهای مختلف را نیز در این حین، پیش بینی کردهاست که در ادامه جزئیات آنرا مرور میکنیم.
نیاز به تغییر رشتهی اتصالی به بانک اطلاعاتی در زمان اجرا
دلایل نیاز به امکان تغییر رشتهی اتصالی در زمان اجرا شامل موارد زیر هستند:
- در برنامههایی کمی پیچیدهتر و سابقه دار، ممکن است عملیات تجاری یکسال را در بانک اطلاعاتی سال 98 و دیگری را در بانک اطلاعاتی سال 99 ثبت کنید. در این حالت کاربران باید بتوانند در زمان اجرا به هر بانک اطلاعاتی که پیشتر با آن کار کردهاند، متصل شده و از آن استفاده کنند.
- یکی از روشهای پیاده سازی برنامههای چند مستاجری، داشتن یک بانک اطلاعاتی مجزا، به ازای هر مستاجر است. در این حالت نیز تک برنامهی ما باید بتواند بر اساس Id مشتری، بانک اطلاعاتی متناظری را در زمان اجرا انتخاب کند.
- نیاز به داشتن چندین context در برنامه و کار با بانکهای اطلاعاتی متفاوت در زمان اجرا؛ مانند کار با SQL Server، اوراکل و یا SQLite
روش تغییر رشتهی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه
اگر به روش ثبت متداول سرویس DbContext برنامه و پروایدر آن دقت کنیم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection") ));
یک نکته: چون این اطلاعات کش نمیشوند، اگر رشتهی اتصالی شما ثابت است (و نیازی به تغییر آن در زمان اجرای برنامه نیست)، محل تامین آنرا به پیش از سطر services.AddDbContext انتقال دهید و فقط نتیجهی محاسبه شدهی نهایی را استفاده کنید تا کارآیی برنامه افزایش یابد؛ در غیراینصورت فراخوانی Configuration.GetConnectionString مدام تکرار خواهد شد.
دریافت یک قالب قابل تغییر از تنظیمات برنامه و تغییر آن با هدرهای درخواست رسیدهی به آن
فرض کنید قالب رشتهی اتصالی برنامه در فایل appsettings.json به صورت زیر است:
"ConnectionStrings": { "ConnectionTemplate": "Data Source=.;Initial Catalog={db_Name};Integrated Security=True", }
بنابراین اکنون نیاز است به ازای هر درخواست رسیده بتوان به سرویس IHttpContextAccessor و شیء HttpContext.Request جاری دسترسی یافت و سپس از هدرهای رسیده، برای مثال هدر ویژهی tenantId و یا year را پردازش کرد؛ اما در تعریف services.AddDbContext فوق چگونه میتوان اینکار را انجام داد؟
خوشبختانه متد services.AddDbContext، دارای یک overload دیگر نیز هست که امکان دسترسی به تمام سرویسهای جاری سیستم را میسر میکند:
services.AddDbContext<ApplicationDbContext>((serviceProvider, dbContextBuilder) => { var connectionStringTemplate = Configuration.GetConnectionString("ConnectionTemplate"); var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); var dbName = httpContextAccessor.HttpContext.Request.Headers["tenantId"].First(); var connectionString = connectionStringTemplate.Replace("{db_Name}", dbName); dbContextBuilder.UseSqlServer(connectionString); });
همچنین در صورت نیاز میتوان UseSqlServer آنرا نیز در این action delegate به هر پروایدر دیگری در زمان اجرا تغییر داد و از این لحاظ محدودیتی وجود ندارد.
یک نکته: البته برنامه نباید هر tenantId ای را پردازش کند و این خودش میتواند تبدیل به یک نقیصهی امنیتی شود. به همین جهت برای مثال میتوان tenantId را در یک JWT قرار داد و در حین تعیین اعتبار آن و کاربر جاری، این مقدار را نیز بررسی کرد.