‫۲ روز قبل، جمعه ۱۶ شهریور ۱۴۰۳، ساعت ۱۲:۱۷

یک نکته‌ی تکمیلی: امکان کار همزمان هم با HttpClient وجود دارد!

تا پیش از ارائه‌ی NET Core.، روش متداول دریافت فایل‌ها، عموما به صورت زیر و همزمان/synchronous بود:

var client = new WebClient();
client.DownloadFile(downloadUrl, filePath);

هرچند ... WebClient امکان دریافت فایل‌ها را به صورت غیرهمزمان هم دارد، اما API آن با async/await هماهنگ نیست و طراحی آن قدیمی است.

پس از آن،‌ HttpClient ارائه شد که از روز اول، async بود و کاملا هماهنگ با async/await و روش کدنویسی جدید آن. اما ... شاید در قسمت‌هایی نیاز باشد تا بتوان کدهای قدیمی را بدون تبدیل کردن آن‌ها به نمونه‌های async، به همان شکل همزمان، بازنویسی کنیم. برای رفع این مشکل، از زمان دات‌نت 5، متد Send همزمان هم به API آن اضافه شده‌است:

var response = httpClient.Send(new HttpRequestMessage(HttpMethod.Post, "http://site.com"));
using var reader = new StreamReader(response.Content.ReadAsStream());            
var content = reader.ReadToEnd();
‫۲ روز قبل، پنجشنبه ۱۵ شهریور ۱۴۰۳، ساعت ۲۲:۱۰

یک نکته‌ی تکمیلی: روش لغو صف درخواست‌های مکرر fetch ارسالی به سمت سرور

ورودی جستجوی بالای صفحه‌ را درنظر بگیرید که به‌ازای هربار ورود حرفی، یک درخواست fetch جدید را به سمت سرور ارسال می‌کند تا نتایج جستجوی حاصل را دریافت کند. مشکل اینجاست که ما تنها به آخرین درخواست fetch ارسال شده‌ی به سمت سرور نیاز داریم و نه به تمام درخواست‌های دیگری که صادر شده‌اند. به همین جهت این صف درخواست‌های fetch قبلی، غیربهینه بوده و ترافیک بالایی را سبب می‌شوند. یک روش مواجه شدن با این مساله، استفاده از مفهومی به نام debounce است که در پشت صحنه، از یک تایمر استفاده می‌کند و فقط هر چند ثانیه یکبار، یک درخواست جدید را به همراه آخرین متن ورودی، به سمت سرور ارسال خواهد کرد. راه دیگری هم برای مواجه شدن با این مشکل، در مرورگرهای جدید پیش‌بینی شده‌است که AbortController نام دارد. با استفاده از آن می‌توان «سیگنالی» را به صف درخواست‌های پرتعداد fetch قبلی حاصل از ورود اطلاعات کاربر ارسال کنیم که ... «لغو شوید» و به سمت سرور ارسال نشوید.

برای توضیح بهتر آن، به مثال زیر دقت کنید:

<!DOCTYPE html>
<html>
  <body>
    <input id="search" type="number" />
    <script>
      const results = [];
      const search = document.getElementById("search");

      let controller = new AbortController();
      let signal = controller.signal;

      const onChange = () => {
        const value = search.value;
        if (value) {
          controller.abort();
          controller = new AbortController();
          signal = controller.signal;
          getPost(value, signal);
        }
      };
      search.onkeyup = onChange;
    </script>
  </body>
</html>

- در اینجا یک input box را داریم که ابتدا، یافت شده و سپس به رخ‌داد onkeyup آن، متد onChange نسبت داده شده‌است تا هربار که کاربر، اطلاعاتی را وارد می‌کند، فراخوانی شود.

- در ابتدای اسکریپت هم نحوه‌ی نمونه سازی شیء استاندارد جاوااسکریپتی AbortController و دسترسی به شیء signal آن‌را مشاهده می‌کنید.

- در متد onChange، ابتدا مقدار جدید ورودی کاربر، دریافت می‌شود، سپس این AbortController، لغو می‌شود و بعد یک نمونه‌ی جدید از آن ایجاد شده و مجددا به شیء signal آن دسترسی پیدا می‌کنیم تا آن‌را به متد getPost ارسال کنیم. این متد هم چنین پیاده سازی را دارد:

const getPost = (value, signal) => {
          fetch(
            `https://site.com/search/${value}`,
            { signal }
          );          
      };

همانطور که مشاهده می‌کنید، تابع fetch، قابلیت پذیرش شیء signal را هم دارد. زمانیکه با هربار تایپ کاربر، متد ()controller.abort فراخوانی می‌شود، سیگنالی را به fetch «قبلی» متصل به آن ارسال می‌کند که ... دیگر به سمت سرور ارسال نشو و متوقف شو. با اینکار فقط آخرین ورودی کاربر، سبب بروز یک fetch موفق می‌شود و ترافیک ارسالی به سمت سرور کاهش پیدا می‌کند (چون تمام fetchهای قبلی، سیگنال abort را دریافت کرده‌اند)؛ مانند مثال زیر که کاربر، 5 بار حروفی را وارد کرده و به ازای هربار ورود حرفی، یک درخواست fetch جدید، ایجاد شده، اما ... فقط آخرین درخواست ارسالی او موفق بوده و نتیجه‌ای را بازگشت داده و مابقی درخواست‌ها ... abort شده‌اند. این عملیات abort، در سمت کاربر اعمال می‌شود؛ یعنی اصلا درخواستی به سمت سرور ارسال نمی‌شود و این لغو درخواست، توسط برنامه‌ی سمت سرور انجام نشده‌است.

‫۴ روز قبل، چهارشنبه ۱۴ شهریور ۱۴۰۳، ساعت ۰۸:۱۴

یک نکته‌ی تکمیلی: بین مسیرهای مطلق و نسبی در لینوکس و ویندوز، تفاوت وجود دارد!

فرض کنید با استفاده از قطعه کد زیر، سعی در تبدیل یک مسیر نسبی را به مسیری مطلق، داریم:

private string GetAbsoluteApiUrl(string url)
    => Uri.TryCreate(url, UriKind.Absolute, out _) ? url : NavigationManager.ToAbsoluteUri(url).ToString();

این قطعه کد در ویندوز بدون مشکل کار می‌کند. برای مثال اگر مسیر api/test/ را به آن معرفی کنیم، آن‌را تبدیل به یک مسیر مطلق شروع شده‌ی با آدرس دامنه‌ی سایت می‌کند. اما ... این قطعه کد در لینوکس کار نمی‌کند! چون مسیر api/test/ در لینوکس، یک مسیر مطلق به‌شمار می‌رود! برای رفع این مشکل، قرار است چنین API ای در آینده اضافه شود:

new Uri("/foo", new UriCreationOptions { AllowImplicitFilePaths = false });

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

یک نکته‌ی تکمیلی: غنی سازی کامپایلر سی‌شارپ جهت نمایش اخطارهایی در مورد متدهایی بیش از اندازه پیچیده

پس از فعالسازی یکسری از آنالایزرها، اکنون می‌توان بررسی cyclomatic complexity را هم به آن‌ها سپرد. برای اینکار باید مراحل زیر طی شوند:

ابتدا یک سطر زیر را به فایل editorconfig. اضافه کنید:

dotnet_diagnostic.CA1502.severity = warning

سپس فایل جدید CodeMetricsConfig.txt را به ریشه‌ی پروژه اضافه کرده و سطر زیر را به آن اضافه کنید:

CA1502: 20

مقدار پیش‌فرض آستانه‌ی گزارش خطا در اینجا، 25 است که به روش فوق، قابل بازنویسی است.

البته نیاز است این فایل را به صورت یک فایل اضافی، به فایل csproj. نیز معرفی کرد:

<ItemGroup>
   <AdditionalFiles Include="CodeMetricsConfig.txt"/>
</ItemGroup>

همچنین می‌توان تنظیمات آستانه‌ی ریزتری را هم به متدها، نوع‌ها و غیره اعمال کرد:

CA1505(Method): 5
CA1505(Type): 15

مقادیر مجاز در اینجا، شامل SymbolKind, Assembly, Namespace, Type, Method, Field, Event,Property هستند.

در این فایل می‌توان آستانه‌ی گزارش خطای موارد زیر را هم بازنویسی کرد:

CA1501: Avoid excessive inheritance
CA1502: Avoid excessive complexity (this one)
CA1505: Avoid unmaintainable code
CA1506: Avoid excessive class coupling
‫۹ روز قبل، جمعه ۹ شهریور ۱۴۰۳، ساعت ۱۴:۵۲

یک نکته‌ی تکمیلی: روش ثبت خودکار سرویس‌های پس‌زمینه

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

services.Scan(scan => scan.FromAssembliesOf(typeof(Program))
        .AddClasses(classes => classes.AssignableTo<BackgroundService>())
        .As<IHostedService>()
        .WithSingletonLifetime());

یک نکته‌ی تکمیلی: استفاده از BlazorStaticRendererService جهت تولید یک کامپوننت کش کردن قسمتی از محتوای صفحه در برنامه‌های Blazor SSR

فرض کنید هر کدام از آیتم‌های منوی سمت راست صفحه، به همراه آماری مرتبط هم هستند که باید جداگانه محاسبه شوند. اگر قرار باشد به‌ازای هر کاربر و هر بازدید صفحه‌ای، این اطلاعات دوباره محاسبه شوند، بار قابل توجهی به برنامه و سرور وارد خواهد شد و همچنین مرور صفحات هم به‌شدت کند می‌شوند؛ چون قسمت منوی سمت راست صفحه، هربار باید از ابتدا رندر شود. در این مطلب، با سرویس BlazorStaticRendererService آشنا شدیم که کار آن، رندر کردن محتوای یک کامپوننت و بازگشت رشته‌ی نهایی معادل آن است. اگر این مورد را به همراه IMemoryCache‌ توکار دات‌نت، ترکیب کنیم، به کامپوننتی شبیه به cache tag helper توکار ASP.NET Core می‌رسیم:

<cache expires-after="@TimeSpan.FromMinutes(10)">
    @Html.Partial("_WhatsNew")
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>

کدهای کامل آن‌را در اینجا (^ و ^) می‌توانید مطالعه کنید که به این صورت مورد استفاده قرار گرفته‌است تا فقط قسمتی از صفحه را کش کند و نه کل آن‌را.

جالب توجه‌است که OutputCache مخصوص ASP.NET Core، در Blazor SSR هم کار می‌کند. برای استفاده‌ی از آن در Blazor SSR، پس از انجام تنظیمات ابتدایی میان‌افزار مخصوص آن که ترتیب افزودن آن باید به صورت زیر باشد:

app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAntiforgery();

app.UseOutputCache();

app.MapRazorComponents<App>();
app.MapControllers();
app.Run();

فقط کافی است ویژگی OutputCache را به نحو زیر به فایل razor. خود اضافه کنید:

@attribute [OutputCache(Duration = 1000)]

که البته کار آن، کش کردن محتوای کل یک صفحه‌است و نه فقط قسمتی از آن.

یک نکته‌ی تکمیلی: روش طراحی binding دو طرفه در Blazor SSR

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

[Parameter] public T? Value { set; get; }

[Parameter] public EventCallback<T?> ValueChanged { get; set; }

[Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;

اگر این خواص را به کامپوننت‌های توکار خود Blazor متصل کنیم (مانند InputBox آن و مابقی آن‌ها)، نیازی به کدنویسی بیشتری ندارند و کار می‌کنند. اما اگر قرار است از یک input ساده‌ی Html ای استفاده کنیم، نیاز است ValueChanged آن‌را اینبار در متد OnInitialized فراخوانی کنیم؛ چون در زمان post-back به سرور است که مقدار آن در اختیار مصرف کننده‌ی کامپوننت قرار می‌گیرد. این مورد، مهم‌ترین تفاوت نحوه‌ی طراحی binding دوطرفه در Blazor SSR با مابقی حالات و نگارش‌های Blazor است.

بررسی وقوع post-back به سرور به دو روش زیر میسر است:

الف) بررسی کنیم که آیا HttpPost ای رخ‌داده‌است؟ سپس در همین لحظه، متد ValueChanged.InvokeAsync را فراخوانی کنیم:

[CascadingParameter] internal HttpContext HttpContext { set; get; } = null!;

protected override void OnInitialized()
{
   base.OnInitialized();

   if (HttpContext.IsPostRequest())
   {
      SetValueCallback(Value);
   }
}

private void SetValueCallback(string value)
{
   if (!ValueChanged.HasDelegate)
   {
      return;
   }

   _ = ValueChanged.InvokeAsync(value);
}

در این مثال نحوه‌ی فعالسازی ارسال اطلاعات از یک کامپوننت سفارشی را به مصرف کننده‌ی آن ملاحظه می‌کنید. اینکار در قسمت OnInitialized و فقط در صورت ارسال اطلاعات به سمت سرور، فعال خواهد شد.

ب) می‌توان در قسمت OnInitialized، بررسی کرد که آیا درخواست جاری به همراه اطلاعات یک فرم ارسال شده‌ی به سمت سرور است یا خیر؟ روش کار به صورت زیر است:

protected override void OnInitialized()
{
   base.OnInitialized();

   if (HttpContext.Request.HasFormContentType &&
       HttpContext.Request.Form.TryGetValue(ValueField.HtmlFieldName, out var data))
   {
      SetValueCallback(data.ToString());
   }
}

در اینجا از ValueField.HtmlFieldName که در نکته‌ی قبلی معرفی BlazorHtmlField به آن اشاره شد، جهت یافتن نام واقعی فیلد ورودی، استفاده شده‌است.

یک نکته‌ی تکمیلی: نحوه‌ی نامگذاری ویژه‌ی عناصر در فرم‌های جدید Blazor SSR

اگر با نگارش‌های دیگر Blazor کار کرده باشید، عموما یک EditForm را به صفحه اضافه کرده و چند المان را به آن اضافه می‌کنیم و ... کار می‌کند. حتی اگر کامپوننت‌های سفارشی را هم بر این مبنا تهیه کنیم ... بازهم بدون نکته‌ی خاصی کار می‌کنند. اما ... در برنامه‌های Blazor SSR اینطور نیست! زمانیکه برای مثال مدل فرم را به این صورت تعریف می‌کنیم:

[SupplyParameterFromForm]
public OrderPlace? MyModel { get; set; }

و آن‌را به نحو متداولی در صفحه نمایش می‌دهیم:

<InputText @bind-Value="MyModel.City"/>

اگر به المان رندر شده‌ی در مرورگر مراجعه کنیم، ویژگی name حاصل، با MyModel.City مقدار دهی شده‌است و ... این موضوع درج نام خاصیت مدل (و یا اصطلاحا Html Field Prefix)، برای Blazor SSR بسیار مهم است! تاحدی که اگر از آن آگاه نباشید، ممکن است ساعتی را مشغول دیباگ برنامه شوید که چرا، مقدار نالی را دریافت کرده‌اید و یا عناصر تعریف شده‌ی در کامپوننت‌های سفارشی، کار نمی‌کنند و مقدار نمی‌گیرند!

متاسفانه API بازگشت نام کامل عناصری که توسط Blazor SSR تولید می‌شود، عمومی نیست و internal است. اگر از کامپوننت‌های استاندارد خود Blazor استفاده می‌کنید، نیازی نیست تا به این موضوع فکر کنید و مدیریت آن خودکار است؛ اما همینکه قصد تولید کامپوننت‌های سفارشی مخصوص SSR را داشته باشید، اولین مشکلی را که با آن مواجه خواهید شد، دقیقا همین مساله‌ی تولید صحیح HtmlFieldPrefix‌ها است.

برای رفع این مشکل و دسترسی به API پشت صحنه‌ی تولید نام فیلدها در Blazor SSR، می‌توان از کامپوننت پایه‌ی InputBase خود Blazor ارث‌بری کرد و به این ترتیب به خاصیت جدید NameAttributeValue آن دسترسی یافت (این خاصیت به دات‌نت 8 و مخصوص Blazor SSR، اضافه شده‌است) که اینکار در کلاس BlazorHtmlField انجام شده‌است. روش استفاده‌ی از آن هم به صورت زیر است:

private BlazorHtmlField<T?> ValueField
        => new(ValueExpression ?? throw new InvalidOperationException(message: "Please use @bind-Value here."));

[Parameter] public T? Value { set; get; }

[Parameter] public EventCallback<T?> ValueChanged { get; set; }

[Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;

زمانیکه می‌خواهیم در یک کامپوننت سفارشی، خاصیتی bind پذیر را طراحی کنیم، روش کار آن، مانند مثال فوق است که به همراه یک خاصیت، یک EventCallback و یک Expression است تا اعتبارسنجی و انقیاد دوطرفه را فعال کند. اما ... اگر همین Value را مستقیما در فیلدهای کامپوننت استفاده کنیم ... مقدار نمی‌گیرد؛ چون به همراه نام کامل خاصیت بایند شده‌ی به آن نیست. برای مثال بجای MyModel.City فقط City درج می‌شود (که به علت نداشتن .MyModel، سیستم binding از مقدار آن صرفنظر می‌کند). اکنون با استفاده از BlazorHtmlField فوق، می‌توان به نام کامل تولیدی توسط Blazor SSR دسترسی یافت و از آن استفاده کرد:

<input type="text" dir="ltr"
        name="@ValueField.HtmlFieldName" 
        id="@ValueField.HtmlFieldName" />

HtmlFieldName ای که در اینجا درج می‌شود، توسط خود Blazor محاسبه شده و با انتظارات موتور binding آن تطابق دارد و دیگر به خواص بایند شده‌ای که مقدار نمی‌گیرند، نخواهیم رسید.