نظرات مطالب
EF Code First #2

سلام

من از ابزار Power tools جهت استخراج Contex از دیتابیس موجود استفاده نکردم بلکه از Ado.net data model خود ویژوال استدیو اما مدلی که به من میده خیلی شلوغ و پیچیده است و قابل استفاده نیست و این درحالی است که من یک جدول ساده بدون هیچ گونه ریلیشن و یا روابط پیچیده را استفاده کردم !

مطالب
مدیریت طول عمر DbContext در برنامه‌های Blazor SSR

فرض کنید یک صفحه‌ی 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 آن اجرا خواهد شد.

بازخوردهای پروژه‌ها
اضافه کردن یک ردیف بالای جدول
با سلام،میشه یک ردیف برای نمایش عنوان به بالای جدول اضافه کنیم.من اضافه کردن row رو نتونستم پیدا کنم؟  

نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت ششم - فارسی سازی پیام‌ها
سلام و تشکر. اگر در این حالت بخواهیم سایت را به صورت چندزبانه طراحی کنیم چطور باید این خطاها را سفارشی سازی کنیم که برای زبانهای دیگر هم متن خطا به همان زبان نمایش داده شود؟ همینطور اگه بخواهیم متن این خطاها رو از بانک اطلاعاتی لود کنیم (به دلیل اینکه امکان اضافه و ویرایش کردن متن پیام‌ها به هر زبانی رو در پنل ادمین بخواهیم فراهم کنیم) چطور باید عمل کرد؟
نظرات مطالب
1# آموزش سیستم مدیریت کد Git
سلام
تشکر از مقالات مفیدتون - بنده تا بحال از سیستم مدیریت کد استفاده نکردم . به نظر شما برای شروع ، بهتر هست که از چه سیستمی شروع کنم ؟ تعریف SVN و Git رو شنیدم ، اما نیاز به راهنمایی دقیق‌تری دارم . با تشکر
نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
با سلام و تشکر از از این مطلب عالی.
یک سوال:
اگر جدول BlogComment با یک یا چند جدول دیگر رابطه داشته باشد (مثلاً جدول کاربر، جدول دسترسی و غیره)، چه راه حلی وجود دارد تا در روش خود ارجاع دهنده سایر رابطه‌ها شرکت نکنند، یعنی فقط تعدادی فیلد از جدول BlogComment انتخاب شود. در حالت فعلی تمام فیلدها از تمام جدولهای مرتبط استخراج میشوند.
نظرات مطالب
معرفی افزونه CAT.NET
سلام
نه افزونه خاصی نصب نکردم !!! ؟
پرسش‌ها
آیا استفاده از Handler در یک Handler دیگر در الگوی Mediator صحیح است؟

سلام

یک پروژه با کتابخانه MediatR ایجاد کردم و برای عملیات CRUD به ازای تمام Entityها Handlerهای مورد نیاز را پیاده سازی کردم. برخی از این Handlerها در زمان اجرای Bussiness مربوط به خودش، نیاز داره تا اطلاعاتی را در یک جدول دیگر در بانک اطلاعاتی ذخیره کنه ولی افزودن اطلاعات جدید در جدول دوم، مستلزم لحاظ نمودن Business مربوط به Handler دوم است. به تمامی Handlerها IEntityRepository مربوط به Entity که در Domain تعریف شده است تزریق می شود. در لایه Infrastructure نیز تمام متدهای موجود در IEntityRepository تحت نام EntityRepository پیاده سازی شده اند.

سوال:

  1. آیا با توجه به سناریوی بالا، امکان تزریق MediatR.IMediator در Handler اول وجود دارد (از حیث Best Practice بودن)؟
  2. اگر آیتم یک مناسب نباشد آیا باید یک لایه دیگری به پروژه افزوده شود تا امکان استفاده از Handlerهای مشترک وجود داشته باشد؟
  3. در یکی از منابع اینترنتی به آیتم شماره یک عنوان Anti Pattern داده بودند. آیا درست است؟

تشکر

پاسخ به بازخورد‌های پروژه‌ها
چگونگی قرار دادن عکس به صورت Watermark(پس زمینه گزارش)
در قسمت DocumentPreferences از متد BackgroundImage استفاده کنید.
البته باید دقت داشته باشید که اگر تصویر زمینه، پس از تنظیم ذکر شده مشاهده نشد، نیاز است رنگ سلول‌های گرید رو شفاف کنید. یعنی باید یک قالب سفارشی طراحی کنید که رنگ‌ها رو null برگردونه تا transparency لازم جهت نمایش تصویر قرار گرفته در زیر جدول مهیا شود.