‫۱ ماه قبل، یکشنبه ۲۸ مرداد ۱۴۰۳، ساعت ۱۹:۲۱

یک نکته‌ی تکمیلی: چگونه تاریخ Build را به اسمبلی برنامه اضافه کنیم؟

شاید علاقمند باشید که بجای نمایش شماره نگارش برنامه، تاریخ Build آن‌را در قسمتی خاص، نمایش دهید. برای اینکار می‌توان یک ویژگی جدید را به صورت زیر به اسمبلی برنامه اضافه کرد:

[AttributeUsage(AttributeTargets.Assembly)]
public sealed class BuildDateAttribute : Attribute
{
    public BuildDateAttribute(string buildDateTime) => BuildDateTime = buildDateTime;

    public string BuildDateTime { get; }
}

تا در زمان کامپایل برنامه، فایل obj\Debug\net8.0\DntSite.Web.AssemblyInfo.cs را به این صورت تکمیل کند:

using System;
using System.Reflection;

[assembly: DNTCommon.Web.Core.BuildDateAttribute("1403.05.28.15.17")]
[assembly: System.Reflection.AssemblyCompanyAttribute("DntSite.Web")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]

مقدار دهی این این ویژگی جدید، در زمان Build و توسط تنظیمات زیر در فایل csproj. برنامه انجام می‌شود:

<Project>
  <ItemGroup>
        <AssemblyAttribute Include="DNTCommon.Web.Core.BuildDateAttribute">
            <_Parameter1>$([System.DateTime]::Now.ToString("yyyy.MM.dd.HH.mm"))</_Parameter1>
        </AssemblyAttribute>
  </ItemGroup>

با استفاده از AssemblyAttribute می‌توان پارامترهای دلخواهی را به ویژگی Include شده، ارسال کرد؛ برای مثال، تاریخ جاری سیستم را. اگر تعداد پارامترهای سازنده بیشتر بود، می‌توان Parameter2_ و Parameter3_ و ... را هم تنظیم کرد.

همین اندازه تنظیم برای اضافه شدن خودکار این ویژگی جدید به اسمبلی نهایی برنامه کافی است (و همانطور که عنوان شد، محل درج خودکار اولیه‌ی آن، در فایل AssemblyInfo.cs پوشه‌ی obj برنامه‌است). برای خواندن و نمایش آن هم می‌توان به صورت زیر عمل کرد:

public static class BuildDateAttributeExtensions
{
    public static string? GetBuildDateTime(this Assembly? assembly) =>
        assembly?.GetCustomAttribute<BuildDateAttribute>()?.BuildDateTime ??
        assembly?.GetName().Version?.ToString();
}

برای نمونه می‌توان اطلاعات BuildDateAttribute اسمبلی جاری را به صورت زیر استخراج کرد:

private static string? GetVersionInfo() => Assembly.GetExecutingAssembly().GetBuildDateTime();
‫۱ ماه قبل، شنبه ۲۷ مرداد ۱۴۰۳، ساعت ۱۹:۰۱

یک نکته‌ی تکمیلی: چگونه آنالایزرهای بسیار کند را پیدا کنیم؟!

پس از غنی‌سازی پروژه با تعدادی Roslyn Analyzer، با اولین موردی که مواجه خواهیم شد، بالا رفتن زمان کامپایل است؛ گاهی از اوقات تا حدی که کار کامپایل، چند دقیقه طول می‌کشد. در این حالت شاید این سؤال مطرح شود که کدامیک از این آنالایزرهای اضافه شده، بیشترین زمان را به خودش اختصاص داده؟

برای پاسخ به این سؤال، فقط کافی است پروژه را با سوئیچ bl/، کامپایل کنیم:

dotnet build /bl

خروجی آن که یک فایل باینری است به نام msbuild.binlog، توسط برنامه‌ی «MSBuild Binary and Structured Log Viewer» قابل مشاهده‌است:

پس از باز کردن این فایل در برنامه‌ی یاد شده، می‌توان ذیل قسمت Analyzer summary (مانند تصویر فوق)، زمان صرف شده‌ی توسط هر آنالایزر را مشاهده کرد و در صورت نیاز، موارد زمانبر را از پروژه حذف کرد.

یا حتی می‌توانید اجرای تمام آنالایزرها را برای حالت دیباگ، غیرفعال کنید:

dotnet run -p:RunAnalyzers=false
‫۱ ماه قبل، جمعه ۲۶ مرداد ۱۴۰۳، ساعت ۱۵:۳۷

یک نکته‌ی تکمیلی: تعدیل خطاهای بررسی امنیتی بسته‌های نیوگت در حالت کار offline در دات‌نت 8

اگر در پروژه‌ی خود، تنظیم گزارش اخطارها را به صورت خطا، فعال کرده باشید:

<PropertyGroup>
   <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

و ... از دات‌نت 8 هم استفاده می‌کنید، هربار با صدور فرمان dotnet build و یا dotnet restore، با خطای زیر مواجه خواهید شد:

warning NU1900: Error occurred while getting package vulnerability data: (more information)

البته یکبار که اطلاعات امنیتی بسته‌ها ذخیره شدند، ممکن است در طول یک روز دیگر شاهد این خطا نباشید، اما ... دوباره فردا تکرار خواهد شد و اگر بخواهید offline کار کنید، این خطا واقعا مشکل ساز می‌شود!

برای کنترل آن یا می‌توان به صورت زیر عمل کرد:

<PropertyGroup>
    <NuGetAudit>false</NuGetAudit>
</PropertyGroup>

که بررسی امنیتی بسته‌های نیوگت را کاملا غیرفعال می‌کند و یا می‌توان به صورت زیر، این بررسی را فقط به حالت Release خلاصه کرد:

<PropertyGroup>
  <NuGetAudit>true</NuGetAudit>
  <NuGetAuditMode>all</NuGetAuditMode>
  <NuGetAuditLevel>low</NuGetAuditLevel>
  <WarningsNotAsErrors Condition="'$(Configuration)' != 'Release'">
    $(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904
  </WarningsNotAsErrors>
</PropertyGroup>

در این حالت هرچند اخطارهای NU1900 و دردسترس نبودن اینترنت ظاهر می‌شوند، اما دیگر به‌عنوان خطا پردازش نخواهند شد (چون در قسمت WarningsNotAsErrors ذکر شده‌اند) و پروسه‌ی build را متوقف نمی‌کنند.

یک نکته‌ی تکمیلی: به روز رسانی کتابخانه‌ی UAParser

در این مطلب از کتابخانه‌ی UAParser استفاده شد. این کتابخانه، چندسالی است که به‌روز نشده؛ البته چون نیازی نبوده! در اصل، این کتابخانه از فایل yaml مخصوصی که به صورت جاسازی شده (embedded) در آن قرار دارد، برای شناسایی مرورگرها استفاده می‌کند (مفهوم استفاده از متد ()Parser.GetDefault همین مورد است). بنابراین یا باید خودتان این فایل yaml را دستی به روز کرده (کار مخزن کد فعال UAParser-Core، فقط همین یک مورد است) و سپس کتابخانه را مجددا کامپایل و استفاده کنید و یا می‌توانید محتویات فایل yaml ذکر شده را دریافت و سپس با استفاده از متد Parser.FromYaml این کتابخانه، اطلاعات جدید دریافتی را پردازش و استفاده کنید؛ مانند UAParserService.

‫۱ ماه قبل، سه‌شنبه ۲۳ مرداد ۱۴۰۳، ساعت ۱۸:۵۲

یک نکته‌ی تکمیلی: کار با اینترفیس IMiddleware جهت تعریف میان‌افزارهای سفارشی

اگر امروز قصد تعریف میان‌افزارهای سفارشی را دارید، بهتر است از روش باز public async Task Invoke(HttpContext context) که در این مطلب معرفی شد، دیگر استفاده نکنید؛ چون مهم‌ترین محدودیت‌های آن، داشتن طول عمر Singleton غیرقابل تغییر و همچنین عدم امکان پیاده سازی اینترفیس IDisposable در آن جهت پاکسازی خودکار منابع است. امروز روش توصیه شده‌، استفاده از اینترفیس IMiddleware است. در این حالت متد Task Invoke فوق، به متد مشخص و ثابت Task InvokeAsync(HttpContext context, RequestDelegate next) تغییر می‌کند. چون در این حالت دیگر نمی‌توان پارامترهای این متد مشخص را مانند قبل که اینترفیسی را پیاده سازی نمی‌کرد، به صورت پویا کم و زیاد کرد، می‌توان سرویس‌های مدنظر را به سازنده‌ی کلاس، تزریق کرد. به همین جهت نیاز است، آن‌را به نحو زیر به سیستم تزریق وابستگی‌ها معرفی کرد:

builder.Services.AddTransient<MyNewMiddleware>();

این الزام به تعریف آن به صورت یک سرویس رسمی، مزیت‌های زیر را به همراه دارد:

الف) می‌توان طول عمری، غیر از Singleton را هم در صورت نیاز، تعریف کرد (و مشکل کار با سرویس‌هایی با طول عمرهای غیر از Singleton کمتر می‌شود).

ب) چون طول عمر این میان‌افزار اکنون توسط سیستم تزریق وابستگی‌ها مدیریت می‌شود، اگر این میان‌افزار اینترفیس IDisposable را پیاده سازی کند، کار پاکسازی منابع آن خودکار خواهد شد.

نکته 1: روش معرفی آن به سیستم تزریق وابستگی‌ها، به صورت Concrete type است؛ یعنی اصل کلاس باید معرفی شود (مانند سطر فوق) و نه اینکه به صورت متداول زیر به همراه ذکر اینترفیس IMiddleware باشد:

builder.Services.AddTransient<IMiddleware, MyNewMiddleware>();

مابقی کار با آن، با میان‌افزارهای متداول، تفاوتی ندارد. یعنی قسمت UseMiddleware آن یکی است:

app.UseMiddleware<MyNewMiddleware>();

بنابراین این روش نسبت به روش متداول قبلی، دو تفاوت پیاده سازی اینترفیس مشخص IMiddleware و ثبت کلاس آن به صورت یک سرویس رسمی را دارد؛ مابقی نکات آن، مانند قبل است.

نکته 2: اگر از Scrutor برای ثبت خودکار سرویس‌های برنامه استفاده می‌کنید، روش ثبت خودکار اینگونه سرویس‌ها به صورت زیر و با استفاده از متد ()AsSelf است:

services.Scan(scan => scan.FromAssembliesOf(typeof(IDataSeedersRunner))
            .AddClasses(classes => classes.Where(type =>
            {
                var allInterfaces = type.GetInterfaces();

                return allInterfaces.Contains(typeof(IMiddleware)) && 
                       allInterfaces.Contains(typeof(ISingletonService));
            }))
            .AsSelf()
            .WithSingletonLifetime());

در این مثال، تمام IMiddleware هایی که با نشانگر ISingletonService هم مزین شده‌اند، یافت شده و به صورت Concrete type هایی، با طول عمر Singleton، به سیستم تزریق وابستگی‌ها اضافه می‌شوند.

‫۱ ماه قبل، دوشنبه ۲۲ مرداد ۱۴۰۳، ساعت ۲۱:۳۶

با اجرای این کوئری خاص به همراه AsNoTrackingWithIdentityResolution خطای زیر مشاهده می‌شود و اصلا کار نمی‌کند (چون فیلد JSON هم دارد):

System.InvalidOperationException: Invalid token type: 'StartObject'.

در EF-Core و در طی طول عمر «یک» Context:

  • اگر یک کوئری معمولی، به همراه Change Tracker گرفته شود، اطلاعات اشیاء بازگشتی از آن، بر اساس ID آن‌ها «کش موقت می‌شوند». اگر در ادامه‌ی همین Context، مجددا این کوئری تکرار شود، این اشیاء مشابه با ID یکسان، نمونه سازی مجدد «نمی‌شوند» (هرچند یکبار دیگر از بانک اطلاعاتی دریافت شده‌اند و این کش موقت، به معنای عدم واکشی اطلاعات از بانک اطلاعاتی نیست) و از همان کش محلی Change Tracker مجددا خوانده می‌شوند.
  • اگر یک کوئری از نوع AsNoTracking گرفته شود، اشیاء بازگشتی از آن، بر اساس ID آن‌ها «کش نمی‌شوند». اگر همین کوئری مجددا در طول عمر Context جاری تکرار شود، نمونه سازی «مجددی» از اشیاء مشابه با ID یکسان را شاهد «خواهیم بود» و EF آن‌ها را از کش موقت Change Tracker جاری، بازیابی مجدد نمی‌کند. بنابراین تکرار این کوئری، مصرف حافظه‌ی بیشتری را به همراه دارد؛ هرچند اگر فقط قرار است یکبار انجام شود (برای انجام یک عملیات گزارشگیری فقط خواندنی)، به دلیل نداشتن سیستم ردیابی، سربار کمتری را از حالت اول دارد.
  • اگر یک کوئری از نوع AsNoTrackingWithIdentityResolution گرفته شود، اشیاء بازگشتی از آن بر اساس ID آن‌ها «کش موقت می‌شوند». اگر همین کوئری مجددا در طول عمر Context جاری تکرار شود، نمونه سازی مجددی از اشیاء مشابه با ID یکسان را شاهد «نخواهیم بود» (هر چند کوئری از بانک اطلاعاتی، داده‌هایی را واکشی کرده‌است)؛ اما وارد سیستم Tracking هم نمی‌شوند و تغییرات بر روی آن‌ها ردیابی نمی‌شوند. به همین جهت این روش در حین تکرار کوئری‌های مشابه در طول عمر یک Context نسبت به AsNoTracking، مصرف حافظه‌ی کمتری دارد.

بنابراین وجود AsNoTrackingWithIdentityResolution فقط در طی طول عمر یک Conetxt و برای کاهش سربار نمونه سازی اشیاء مشابه با IDهای یکسان کوئری‌های آن Context ارزشمند است و اگر Context شما از چندین کوئری مشابه تشکیل نمی‌شود، عملا کار بیشتری را انجام نمی‌دهد و تفاوتی با AsNoTracking ندارد.

‫۱ ماه قبل، یکشنبه ۲۱ مرداد ۱۴۰۳، ساعت ۱۹:۵۰

نگارش به‌روز «یک نکته‌ی تکمیلی: استفاده از DateTime.UtcNow و EF Core»

به صورت خلاصه اگر در بانک اطلاعاتی خود، اطلاعات زمانی را برای مثال توسط DateTime.UtcNow ذخیره می‌کنید، هرچند DateTimeKind آن به Utc تنظیم می‌شود، اما هنگام دریافت مقدار آن از بانک اطلاعاتی، این DateTimeKind بازیابی نشده (ADO.NET چنین قابلیتی را ندارد) و ... شاهد ناهماهنگی‌هایی خواهیم بود. برای یک‌دست سازی برنامه در این حالت می‌توان از تبدیلگرهای زیر استفاده کرد (برای دو حالت DateTime و ?DateTime):

public class NullableDateTimeAsUtcValueConverter() : ValueConverter<DateTime?, DateTime?>(
    v => !v.HasValue ? v : ToUtc(v.Value), v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v)
{
    private static DateTime? ToUtc(DateTime v) => v.Kind == DateTimeKind.Utc ? v : v.ToUniversalTime();
}

public class DateTimeAsUtcValueConverter() : ValueConverter<DateTime, DateTime>(
    v => v.Kind == DateTimeKind.Utc ? v : v.ToUniversalTime(), v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

که اینبار و در نگارش‌های جدیدتر EF، روش معرفی سراسری آن‌ها به صورت زیر ساده می‌شود:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
   ArgumentNullException.ThrowIfNull(configurationBuilder);

   configurationBuilder.Properties<DateTime>().HaveConversion<DateTimeAsUtcValueConverter>();
   configurationBuilder.Properties<DateTime?>().HaveConversion<NullableDateTimeAsUtcValueConverter>();
}
‫۱ ماه قبل، یکشنبه ۲۱ مرداد ۱۴۰۳، ساعت ۱۹:۲۸

یک نکته‌ی تکمیلی: تاثیر فراخوانی متد AsNoTracking بر روی کوئری‌های خود ارجاعی

همانطور که در مطلب «مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first» هم مشاهده کردید، خود EF، قابلیت تشکیل درخت نهایی خود ارجاع دهنده را دارد و به این ترتیب کوئری گرفتن از نتیجه‌ی آن، بسیار ساده می‌شود. اما ... اگر در این بین، از متد AsNoTracking برای بهینه سازی، کاهش میزان حافظه و حذف پروکسی‌های ردیابی تغییرات EF استفاده شود، دیگر این درخت خودکار، تشکیل نخواهد شد. برای پوشش این حالت می‌توان به صورت زیر عمل کرد:

الف) تشکیل یک کلاس پایه برای تعریف ساده‌تر و مشخص رابطه‌های خود ارجاعی

public abstract class BaseEntity
{
    public int Id { get; set; }
}

public abstract class BaseSelfReferencingEntity<TSelfEntity> : BaseEntity
    where TSelfEntity : BaseEntity
{
    public virtual TSelfEntity? Reply { set; get; }

    public int? ReplyId { get; set; }

    public virtual ICollection<TSelfEntity>? Children { get; set; }
}

که ساختار معرفی شده‌ی در اینجا، با توضیحات موجود در متن، انطباق دارد.

ب) پر کردن درخت نهایی حاصل به صورت دستی:

چون دیگر EF این درخت را برای ما تشکیل نمی‌دهد، اکنون باید خودمان کار تشکیل آن‌را به صورت زیر انجام دهیم:

public static class SelfReferencingExtensions
{ 
    public static List<TEntity> ToSelfReferencingTree<TEntity>(this ICollection<TEntity>? originalList)
        where TEntity : BaseSelfReferencingEntity<TEntity>
    {
        var results = new List<TEntity>();

        if (originalList is null || originalList.Count == 0)
        {
            return results;
        }

        foreach (var rootItem in originalList.Where(x => !x.ReplyId.HasValue))
        {
            results.Add(rootItem);
            AppendChildren(originalList, rootItem);
        }

        return results;
    }

    private static void AppendChildren<TEntity>(ICollection<TEntity> originalList, TEntity parentItem)
        where TEntity : BaseSelfReferencingEntity<TEntity>
    {
        foreach (var kid in originalList.Where(x => x.ReplyId.HasValue && x.ReplyId.Value == parentItem.Id))
        {
            parentItem.Children ??= new List<TEntity>();
            parentItem.Children.Add(kid);
            AppendChildren(originalList, kid);
        }
    }
}

در اینجا کار تشکیل درخت نهایی، با استفاده از یک متد بازگشتی، انجام می‌شود.

پس از این مقدمات، نحوه‌ی استفاده از آن به صورت زیر است:

var comments = await _comments.AsNoTracking()
            .Where(x => x.ParentId == postId)
            .OrderBy(x => x.Id)
            .Take(count)
            .ToListAsync();

var commentsTree = comments.ToSelfReferencingTree();

کوئری نویسی ابتدایی آن، کاملا استاندارد و بدون هیچگونه نکته‌ی خاصی است. ابتدا تمام نظرات یک مطلب (به صورت AsNoTracking) بازگشت داده می‌شوند و سپس متد ToSelfReferencingTree کار اتصالات نهایی درخت پاسخ‌ها را به صورت خودکار انجام می‌دهد.

‫۱ ماه قبل، یکشنبه ۲۱ مرداد ۱۴۰۳، ساعت ۱۹:۰۰

یک نکته‌ی تکمیلی: امکان تعریف مسیریابی صفحات، با استفاده از ویژگی Route

عموما مسیریابی‌های صفحات Blazor، به صورت زیر تعریف می‌شوند:

@page "/counter"

و اگر نیاز باشد تا این مسیر را در قسمت‌های دیگری هم ذکر کنیم (برای مثال در لینک‌ها و یا متد NavigateTo)، باید دقیقا همین مسیر و عبارت را در چندین قسمت برنامه تکرار کنیم. برای رفع این مشکل، با استفاده از ویژگی Route می‌توان مسیریابی فوق را به صورت زیر بازنویسی کرد:

@attribute [Route(Constants.CounterRoute)]

در این حالت می‌توان از مزیت تعریف مسیر مدنظر به صورت یک ثابت، به صورت زیر استفاده کرد:

public static class Constants
{
    public const string CounterRoute = "/counter";
}

و اگر در قسمت دیگری از برنامه نیاز به ارجاعی به آن بود، می‌توان همین رشته‌ی ثابت را مجددا مورد استفاده قرار داد:

NavigationManager.NavigateTo(Constants.CounterRoute);

خیر و بهتر است اگر نیاز به انجام چندین عملیات را در این بین دارید، «... به‌صورت صریحی، یک تراکنش مشخص، برای کل این مجموعه باز شود ...». قسمت «د) پیاده سازی optimistic locking» انتهای این بحث، فقط برای حالتی است که عملیات انجام شده، تحت نظر سیستم Tracking یک ORM قرار می‌گیرد و «Bulk operations» جزو آن نیست.