نظرات مطالب
ساخت یک Form Generator ساده در MVC
با سلام و تشکر
اشکالی در تجزیه و تحلیل و طراحی مدلها هست.با این مدلها تنها یک مرتبه می‌توان فرم را تکمیل کرد و اطلاعات چند کاربر را نمی‌توان دریافت کرد.
برای رفع مشکل پیشنهاد می‌شه که یه مدل برای ثبت هر کاربر به شکل زیر تعریف بشه:
public class row
{
        public int Id { get; set; }
        public string username{ get; set; }
        public string ip{ get; set; }
        .
        .
        public virtual Field Field { get; set; }
        public int fieldId { get; set; }
}
و مدل vlaue به شکل زیر اصلاح بشه

public class Value
{
        public string Val { get; set; }
        public virtual Field Field { get; set; }
        [ForeignKey("Field")]
        [key]
        public int FieldId { get; set; }
        public virtual row row { get; set; }
        [ForeignKey("row")]
        [key]         public int rowid { get; set; }
         
}
مطالب
مقید سازی پارامترهای نوع جنریک
احتمالا در بیشتر مقالات (فارسی/انگلیسی) عبارات هایی مثل نمونه‌های زیر را دیده اید :
where T:clas
where T:struc
...
در این مقاله قصد داریم بپردازیم به «مقید سازی پارامتر‌های نوع جنریک» و اینکه چه کاربردی دارند و در چه زمانی بهتر است از آن‌ها استفاده کنیم و نحوه استفاده از آنها چگونه است. فرض میکنیم که خواننده‌ی محترم با مفاهیم جنریک آشنایی دارد. در صورتیکه با جنریک‌ها آشنا نیستید ابتدا مروری داشته باشید بر جنریک‌ها و بعد این مقاله را مطالعه فرمایید؛ به این دلیل که موضوع مورد بحث بر پایه‌ی جنریک‌ها می‌باشد.

همانطور که مطلع هستید هر عنصری جنریکی را که تعریف میکنید حداقل دارای یک پارامتر نوع هست و در زمان بکارگیری آن جنریک باید نوع آن را مشخص نمایید. برای نمونه مثال زیر را در نظر بگیرید :
public  class MyCollection<T>
        {
            private List<T> collections = new List<T>();
            public void Add(T value)
            {
                collections.Add(value);
            }
        }
کلاس فوق یک کلاس جنریک است که در هنگام ساخت نمونه‌ای از آن، باید ابتدا data type نوعی را که که می‌خواهیم با آن کار کنیم، تعیین کنیم. برای مثال در کد فوق در هنگام ساخت نمونه‌ای از آن، نوع int را برای آن مشخص میکنیم و هر وقت بخواهیم متد Add آن را فراخوانی کنیم، فقط نوعی را قبول خواهد کرد که در ابتدا برای آن تعیین کرده ایم (int):
MyCollection<int> myintObj = new MyCollection<int>();
            myintObj.Add(12);
            myintObj.Add(33);
             myintObj.Add(33.3);// ERROR z 
سؤال: می‌خواهیم فقط نوع‌هایی را بتوان به T نسبت داد که از نوع ارجاعی (reference type) هستن و یا فقط نوع هایی را به T نسبت داد که یک سازنده دارند؛ چگونه؟

ایجاد قید‌ها یا محدودیت‌ها بر روی پارامتر‌های جنریک‌ها شامل پنج حالت می‌باشد:

حالت اول : Where T:struct
در این حالت T باید یک ساختار باشد .

حالت دوم : where T:class
T  باید یک نوع ارجاعی باشد. اگر در مثال فوق این قید را به آن اضافه کنیم، در هنگام ساخت نمونه‌ای از کلاس فوق، اگر یک نوع value type را به T نسبت دهیم، در هنگام وارد کردن یک نوع value type با خطا مواجه خواهیم شد. مثال:
public  class MyCollection<T> where T:class
        {
            private List<T> collections = new List<T>();
            public void Add(T value)
            {
                collections.Add(value);
            }
        }
و برای استفاده :
 MyCollection<int> myintObj = new MyCollection<int>(); // ERROR , int is value type

حالت سوم : ()Where T:new
نوعی که به T نسبت داده می‌شود باید یک سازنده‌ی پیش فرض داشته باشد.
داخل پرانتز : سازنده‌ی پیش فرض: زمانی که شما یک کلاس می‌نویسید اگر آن کلاس دارای هیچ سازنده‌ای نباشد، کامپایلر یک سازنده‌ی بدون پارامتر را به کلاس فوق اضافه می‌کند که کار آن مقدار دهی به فیلد‌های کلاس است. در اینجا از مقادیر پیش فرض استفاده می‌شود. مثلا برای int مقدار صفر و برای string مقدار "" و به همین ترتیب.
اگر از مقدار دهی پیش فرض توسط کامپایلر خرسند نیستید، می‌توانید سازنده پیش فرض را تغییر داده و مطابق میل خود فیلد‌ها را مقدار دهی اولیه کنید .


حالت چهارم : where T:NameOfBaseClass
نوعی که به T نسبت داده می‌شود باید از کلاس NameOfBaseClass ارث بری کرده باشد.

حالت پنجم : where T:NameOfInterface
همانند حالت چهارم می‌باشد؛ با این تفاوت: نوعی که به T نسبت داده می‌شود باید واسط NameOfInterface را پیاده سازی کرده باشد.

پنج حالت فوق نمونه‌هایی از ایجاد محدودیت بر روی پرامتر نوع اعضای جنریک بودند و اما در ادامه قصد داریم نکاتی را در این باب، بیان کنیم:

نکته اول : می‌توانید محدودیت‌های فوق را با هم ترکیب کنید برای اینکار آنها را با کاما از هم جدا کنید :
 public  class MyCollection<T> where T:class,IDisposable,new()
        {
   //content
}
نوعی که به T نسبت داده می‌شود
  • باید از نوع ارجاعی باشد.
  • باید واسط IDisposable را پیاده سازی کرده باشد.
  • باید یک سازنده‌ی پیش فرض داشته باشد.

نکته دوم : زمانیکه از چندین محدودیت استفاده می‌کنید مثل مثال فوق، باید محدودیت ()new در آخرین جایگاه محدودیت‌ها قرار گیرد؛ در غیر اینصورت با خطای زمان ترجمه روبه رو خواهید شد .

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

public void Swap<T>(ref T val1,ref T val2) where T:struct
            {
//content
            }
نکته چهارم : زمانیکه کلاس و یا متدهای شما بیش از یک نوع پارامتر از نوع جنریک را دریافت می‌کنند، باید محدودیت‌های مورد نظر را برای هر کدام به صورت جداگانه قید کنید. به طور مثال به کلاس زیر که دو پارمتر T و K را دارد، باید برای هر کدام جداگانه محدودیت‌های مورد نظر را اعمال کنیم (در صورت نیاز):
 public  class MyCollection<T,K> where T:class where K:IDisposable,new()
        {
//content
}
مطالب
Blazor 5x - قسمت 30 - برنامه‌ی Blazor WASM - افزودن پرداخت آنلاین توسط درگاه مجازی پرباد
در ادامه‌ی تمرین قسمت قبل که مقدمات ثبت درخواست رزرو یک اتاق را فراهم کردیم، اکنون می‌خواهیم اگر کاربری بر روی دکمه‌ی checkout now یک اتاق کلیک کرد، به درگاه مجازی پرباد منتقل شده، پرداخت را تکمیل کند، به برنامه هدایت شود و در آخر درخواست او در سیستم ثبت گردد. مزیت کار کردن با درگاه مجازی پرباد، امکان آزمایش محلی برنامه، بدون نیاز به یک درگاه بانکی واقعی است و زمانیکه قرار است با یک درگاه بانکی واقعی کار شود، فقط قسمت معرفی و تنظیمات ابتدایی مشخصات درگاه بانکی آن باید تغییر کند و نه هیچ قسمت دیگری از کدهای برنامه.


نصب پرباد و انجام تنظیمات اولیه‌ی آن

بسته‌های نیوگت پرباد را در دو پروژه‌ی زیر نصب خواهیم کرد:
الف) پروژه‌ی Web API (و یا همان BlazorWasm.WebApi در مثال این سری):
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Parbad.AspNetCore" Version="1.1.0" />
    <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
  </ItemGroup>
</Project>
که شامل بسته‌ها‌ی ASP.NET Core آن و همچنین محل ذخیره سازی مبتنی بر EF-Core آن است.

ب) پروژه‌ای که محل قرارگیری فایل‌های Migration است (و یا همان BlazorServer.DataAccess) در این مثال:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
  </ItemGroup>
</Project>
که در اینجا فقط نیاز به بسته‌ی EF-Core آن است تا بتوان Context مخصوص پرباد را در حین اعمال مهاجرت‌ها شناسایی کرد.

پس از نصب این بسته‌ها، به کلاس آغازین پروژه‌ی Web API مراجعه کرده و تنظیمات سرویس‌ها و همچنین میان‌افزار پرباد را انجام می‌دهیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
           // ...

            var connectionString = Configuration.GetConnectionString("DefaultConnection");

            services.AddParbad()
                    .ConfigureHttpContext(httpContextBuilder => httpContextBuilder.UseDefaultAspNetCore())
                    .ConfigureGateways(gatewayBuilder =>
                    {
                        gatewayBuilder
                            .AddParbadVirtual()
                            .WithOptions(gatewayOptions => gatewayOptions.GatewayPath = "/MyVirtualGateway");
                    })
                    .ConfigureStorage(storageBuilder =>
                    {
                        storageBuilder.UseEfCore(efCoreOptions =>
                            {
                                var assemblyName = typeof(ApplicationDbContext).Assembly.GetName().Name;
                                efCoreOptions.ConfigureDbContext = db =>
                                    db.UseSqlServer(
                                        connectionString,
                                        sqlServerOptionsAction: sqlOptions => sqlOptions.MigrationsAssembly(assemblyName)
                                    );
                            });
                    })
                    .ConfigureAutoTrackingNumber(opt => opt.MinimumValue = 1)
                    .ConfigureOptions(parbadOptions =>
                    {
                        // parbadOptions.Messages.PaymentSucceed = "YOUR MESSAGE";
                    });

           // ...
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
           // ...

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            if (env.IsDevelopment())
            {
                app.UseParbadVirtualGatewayWhenDeveloping();
            }
            else
            {
                app.UseParbadVirtualGateway();
            }
        }
    }
}
چند نکته:
- در متد ConfigureGateways می‌توان چندین درگاه را معرفی کرد که برای مثال در اینجا از درگاه مجازی و محلی آن استفاده شده‌است.
- در متد ConfigureStorage، تنظیمات EF-Core آن‌را مشاهده می‌کنید. پرباد به همراه DbContext خاص خودش است. یعنی در این حالت برنامه‌ی شما حداقل دو DbContext خواهد داشت؛ یکی ApplicationDbContext و دیگری ParbadDataContext.
- می‌خواهیم شماره‌ی تراکنش‌ها را به صورت خودکار توسط پرباد مدیریت کنیم. به همین جهت می‌توان عدد ابتدای آن‌را توسط متد ConfigureAutoTrackingNumber مشخص کرد.
- در پایان هم تعاریف مسیریابی میان‌افزار آن‌را مشاهده می‌کنید که می‌تواند برای حالت توسعه و ارائه‌ی نهایی متفاوت باشد.


تکمیل خواص موجودیت RoomOrderDetail جهت کار با پرباد

موجودیت RoomOrderDetail را در قسمت قبل معرفی کردیم. پرباد به ازای هر تراکنش بانکی که صورت می‌گیرد، یا نیاز به یک TrackingNumber خودکار را دارد و یا دستی. یعنی یا می‌توانیم شماره تراکنش خاص خودمان را تولید کنیم و در اختیار آن قرار دهیم و یا از آن درخواست کنیم تا این شماره را مدیریت کرده و به صورت خودکار تولید کند. در هر دو حالت نیاز است این شماره را به ردیف‌های جدول جزئیات سفارشات اتاق‌های هتل اضافه کرد که در این مثال ParbadTrackingNumber نام دارد:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        // ...

        [Required]
        public long ParbadTrackingNumber { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        public string Status { get; set; }
    }
}
همچنین در پایان عملیات هم فیلدهای IsPaymentSuccessful و وضعیت اتاق را تکمیل می‌کنیم.


ایجاد جداول متناظر با ParbadDataContext

همانطور که عنوان شد، اکنون برنامه به همراه دو DbContext است. بنابراین در این حالت در حین اجرای مهاجرت‌ها، ذکر نام Context مدنظر اجباری است.
برای ایجاد مهاجرت‌های متناظر با ParbadDataContext، از طریق خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build

dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbadFields --context ApplicationDbContext
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbad --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext

dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext
چون برنامه دو Context ای است، نیاز است دوبار دستور تولید مهاجرت‌ها و دوبار دستور اعمال آن‌ها را به بانک اطلاعاتی صادر کرد که روش آن‌را در دستورات فوق مشاهده می‌کنید. پس از این دستورات، بانک اطلاعاتی برنامه شامل دو جدول جدید مخصوص پرباد خواهد بود:



روش یکپارچه سازی پرباد با یک برنامه‌ی SPA

روش متداول کار با پرباد، بر اساس طراحی مخصوص ASP.NET Core آن است. ابتدا درخواستی را به آن ارسال می‌کنید. سپس پرباد شماره تراکنشی را تولید کرده و شروع تراکنش را در بانک اطلاعاتی ثبت می‌کند. در ادامه به صورت خودکار، کار ارسال اطلاعات به درگاه بانکی (برای مثال ارسال تمام فیلدهای یک فرم ویژه‌ی آن بانک، بر اساس مستندات آن) و هدایت به درگاه بانکی را انجام می‌دهد. پس از پایان کار پرداخت، کار هدایت به اکشن متد دریافت تائیدیه‌ی نهایی صورت می‌گیرد و همینجا کار به پایان می‌رسد. این روش هرچند برای برنامه‌های سمت سرور ASP.NET Core کار می‌کند، اما ... به همین نحو با برنامه‌های تک صفحه‌ای وب مانند Blazor WASM قابل استفاده نیست. در اینجا روش تبادل اطلاعات با اکشن متدهای وب سرویس‌های برنامه از طریق یک HttpClient است و در این حالت دیگر نمی‌توان از مزایای Post و Redirect خودکار پرباد که در سمت سرور صورت می‌گیرد استفاده کرد. با استفاده از HttpClient، یک شیء را به سمت Web API ارسال می‌کنیم و در پاسخ، فقط یک شیء را دریافت می‌کنیم. در اینجا دیگر خبری از Redirect به درگاه اصلی بانکی و Post اطلاعات به آن نیست. بنابراین روش کار با پرباد در اینجا به صورت زیر خواهد بود:
الف) شماره Id سفارش و مبلغ نهایی آن‌را از طریق یک درخواست Get معمولی به اکشن متدی در سمت سرور ارسال می‌کنیم. یعنی نیاز است ابتدا Url زیر را تشکیل داد که شماره سفارش و مبلغ آن، به صورت کوئری استرینگ‌هایی به اکشن متد PayRoomOrder ارسال می‌شوند:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
برنامه‌ی کلاینت برای اینکه بتواند این هدایت را انجام دهد، نیاز به نکته‌ی خاصی را دارد که در ادامه توضیح داده خواهد شد.
ب) اکنون چون یک redirect سمت سرور صورت گرفته، به صورت معمولی در اکشن متد PayRoomOrder با پرباد پردازش صورت گرفته و به سمت درگاه هدایت می‌شویم. پس از پرداخت نهایی، باز هم به صورت خودکار به اکشن متد دیگری جهت تائید عملیات هدایت خواهیم شد.
ج) در پایان کار، اکشن متد سمت سرور، ما را به سمت کامپوننتی در برنامه‌ی کلاینت Redirect خواهد کرد:
https://localhost:5002/payment-result/OrderId/TrackingNumber/Message
در اینجا شماره سفارش ابتدایی که مشخص است. همان شماره‌ای است که کار را با آن از سمت کلاینت آغاز کردیم. نکته‌ی مهم، TrackingNumber تراکنش است که بر اساس آن رکورد متناظری یافت شده و وضعیت نهایی آن‌را به کاربر نمایش می‌دهیم.

بنابراین روش یکپارچه سازی پربابد با برنامه‌های SPA، بر اساس Redirect‌های کامل است که سبب بارگذاری مجدد کل صفحه و آدرس‌ها می‌شوند و در اینجا از HttpClient برای کار با پرباد استفاده نخواهیم کرد؛ چون تمام اعمال خودکار آن‌را از دست خواهیم داد و مجبور به بازنویسی آن‌ها خواهیم شد که در دراز مدت با تغییرات این کتابخانه، قابل نگهداری نخواهند بود. بنابراین بهتر است خود پرباد کار Redirect‌ها و ارسال اطلاعات به درگاه‌های بانکی را مدیریت کند و نه ما از طریق کار با یک HttpClient.


آشنایی با گردش کار برنامه

در این مثال، مراحل زیر را طی خواهیم کرد:

1- شروع به انتخاب یک بازه‌ی زمانی و تعداد شب اقامت


2- انتخاب یک اتاق از لیست اتاق‌ها با کلیک بر روی دکمه‌ی Book آن


3- کلیک بر روی دکمه‌ی checkout، در صفحه‌ی مشاهده‌ی جزئیات اتاق و شروع به پرداخت


4- هدایت به درگاه مجازی پرباد در سمت برنامه‌ی Web API


5- پرداخت و هدایت خودکار به سمت برنامه‌ی Web API، جهت تائید نهایی


6- هدایت نهایی به سمت برنامه‌ی کلاینت، جهت نمایش اطلاعات پرداخت



ایجاد کنترلر پرداخت، توسط درگاه مجازی پرباد

پس از آشنایی با گردش کاری اطلاعات در اینجا، نیاز است بتوان لینک زیر را در برنامه‌ی کلاینت تولید کرد و سپس کاربر را به سمت اکشن متد PayRoomOrder هدایت نمود:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
این اکشن متد و کنترلر آن به صورت زیر تهیه می‌شود:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class ParbadPaymentController : Controller
    {
        private readonly IConfiguration _configuration;
        private readonly IOnlinePayment _onlinePayment;
        private readonly IRoomOrderDetailsService _roomOrderService;

        public ParbadPaymentController(
            IConfiguration configuration,
            IOnlinePayment onlinePayment,
            IRoomOrderDetailsService roomOrderService)
        {
            _configuration = configuration;
            _onlinePayment = onlinePayment ?? throw new ArgumentNullException(nameof(onlinePayment));
            _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
        }

        [HttpGet]
        public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
        {
            var verifyUrl = Url.Action(
                    action: nameof(ParbadPaymentController.VerifyRoomOrderPayment),
                    controller: nameof(ParbadPaymentController).Replace("Controller", string.Empty),
                    values: null, protocol: Request.Scheme);

            var result = await _onlinePayment.RequestAsync(invoiceBuilder =>
                invoiceBuilder.UseAutoIncrementTrackingNumber()
                            .SetAmount(amount)
                            .SetCallbackUrl(verifyUrl)
                            .UseParbadVirtual()
            );

            if (result.IsSucceed)
            {
                await _roomOrderService.UpdateRoomOrderTrackingNumberAsync(orderId, result.TrackingNumber);

                // It will redirect the client to the gateway.
                return result.GatewayTransporter.TransportToGateway();
            }
            else
            {
                return Redirect(getClientReturnUrl(orderId, result.TrackingNumber, result.Message));
            }
        }

        [HttpGet, HttpPost]
        public async Task<IActionResult> VerifyRoomOrderPayment()
        {
            var invoice = await _onlinePayment.FetchAsync();
            var orderDetail = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(invoice.TrackingNumber);
            if (invoice.Status == PaymentFetchResultStatus.AlreadyProcessed)
            {
                return Redirect(getClientReturnUrl(orderDetail.Id, invoice.TrackingNumber, "The payment is already processed."));
            }

            var verifyResult = await _onlinePayment.VerifyAsync(invoice);
            if (verifyResult.Status == PaymentVerifyResultStatus.Succeed)
            {
                var result = await _roomOrderService.MarkPaymentSuccessfulAsync(verifyResult.TrackingNumber, verifyResult.Amount);
                if (result == null)
                {
                    return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, "Can not mark payment as successful"));
                }
                return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
            }
            return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
        }

        private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
        {
            var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
            return new Uri(new Uri(clientBaseUrl),
                $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
        }
    }
}
توضیحات:
در اینجا کدهای کامل ParbadPaymentController مشاهده می‌کنید.

- گردش کاری پرداخت، با فراخوانی اکشن متد PayRoomOrder شروع می‌شود که دو پارامتر شماره سفارش و مبلغ آن‌را دریافت می‌کند.
[HttpGet]
public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
نوع آن هم عمدا، HttpGet درنظر گرفته شده‌است تا دقیقا مشخص باشد که فقط با Redirect کامل به آن (هدایت کامل از سمت کلاینت به سمت سرور)، کار خواهد کرد و هدف دیگری را دنبال نمی‌کند.

- در اکشن متد PayRoomOrder، نیاز است لینک بازگشت از درگاه بانکی را مشخص کنیم. پس از اینکه کاربر پرداختی را انجام داد، مجددا به صورت خودکار، به سمت آدرسی در همین Web API و نه برنامه‌ی سمت کلاینت هدایت می‌شود؛ چون هنوز کار پرباد به پایان نرسیده و باید عملیات انجام شده را تصدیق کند. به همین جهت ابتدا آدرس اکشن متدی که کار تائید نهایی را انجام می‌دهد، تولید کرده و به متد RequestAsync آن به همراه مبلغ نهایی و نوع درگاه، ارسال می‌کنیم.

- استفاده از UseAutoIncrementTrackingNumber سبب می‌شود تا پرباد خودش مدیریت TrackingNumber را انجام دهد که پس از پایان عملیات، توسط خاصیت result.TrackingNumber در دسترس خواهد بود.

- پس از پایان عملیات ابتدایی RequestAsync که سشن پرباد را ایجاد کرده و همچنین رکوردی را در بانک اطلاعاتی نیز ثبت می‌کند (در جداول درونی خود پرباد)، نیاز است رکورد سفارشی را که با آن کار را شروع کردیم یافته و TrackingNumber آن‌را با مقدار واقعی دریافتی از پرباد، به روز رسانی کنیم. اینکار توسط متد UpdateRoomOrderTrackingNumberAsync انجام می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task UpdateRoomOrderTrackingNumberAsync(int roomOrderId, long trackingNumber)
        {
            var order = await _dbContext.RoomOrderDetails.FindAsync(roomOrderId);
            if (order == null)
            {
                return;
            }

            order.ParbadTrackingNumber = trackingNumber;
            _dbContext.RoomOrderDetails.Update(order);
            await _dbContext.SaveChangesAsync();
        }
    }
}
بر اساس شماره سفارشی که داریم، رکورد متناظر با آن‌را یافته و سپس trackingNumber تولیدی را در آن به روز رسانی می‌کنیم.

- اکنون با فراخوانی متد ()result.GatewayTransporter.TransportToGateway، دو کار مهم رخ می‌دهند:
الف) ارسال خودکار اطلاعات به سمت درگاه بانکی
ب) Redirect خودکار به سمت درگاه بانگی
به همین جهت است که علاقمند نبودیم تا این مراحل را توسط HttpClient برنامه‌ی Blazor WASM مدیریت و بازنویسی کنیم.

- پس از هدایت به سمت درگاه بانکی و تکمیل پرداخت، اکنون مجددا به همان verifyUrl هدایت می‌شویم. یعنی اکنون به مرحله‌ی پردازش اکشن متد VerifyRoomOrderPayment در سمت Web API رسیده‌ایم.
[HttpGet, HttpPost]
public async Task<IActionResult> VerifyRoomOrderPayment()
در اینجا ابتدا invoice.TrackingNumber در حال پردازش را دریافت می‌کنیم. به کمک این عدد می‌توان رکورد سفارش متناظر با آن‌را یافت. به همین جهت است که آن‌را به لیست فیلدهای جدول سفارشات اضافه کردیم. اینکار هم توسط متد GetOrderDetailByTrackingNumberAsync صورت می‌گیرد:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }
    }
}
- در ادامه پرباد کار تصدیق اطلاعات دریافتی از درگاه بانکی را انجام می‌دهد. دراینجا اگر عملیات با موفقیت مواجه شود، سه فیلدی را که در ابتدای بحث در مورد ثبت اطلاعات تراکنش اضافه کردیم، به روز رسانی می‌کنیم:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(long trackingNumber, long amount)
        {
            var order = await _dbContext.RoomOrderDetails.FirstOrDefaultAsync(x => x.ParbadTrackingNumber == trackingNumber);
            if (order?.IsPaymentSuccessful != false || order.TotalCost != amount)
            {
                return null;
            }

            order.IsPaymentSuccessful = true;
            order.Status = BookingStatus.Booked;
            var markPaymentSuccessful = _dbContext.RoomOrderDetails.Update(order);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(markPaymentSuccessful.Entity);
        }
    }
}
- در اینجا بر اساس trackingNumber، سند متناظری را یافته و سپس بررسی می‌کنیم که آیا مبلغ سند، با مبلغ تائید شده، یکی هست یا خیر؟ اگر خیر، نیاز هست پرداخت را برگشت بزنیم که اینکار توسط متد کنسل پرباد قابل انجام است.

- در تمام این مراحل، کار Redirect به سمت کلاینت و کامپوننت payment-result آن، با فراخوانی متد return Redirect اکشن متدها صورت می‌گیرد که Url آن به صورت زیر تامین می‌شود:
        private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
        {
            var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
            return new Uri(new Uri(clientBaseUrl),
                $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
        }
در این متد Client_URL را از فایل appsettings.json برنامه‌ی Web API دریافت می‌کنیم که به آدرس ریشه‌ی برنامه‌ی کلاینت اشاره می‌کند:
{
   "Client_URL": "https://localhost:5002/"
}


تکمیل قسمت سمت کلاینت عملیات پرداخت بانکی، توسط درگاه مجازی پرباد

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

الف) تکمیل کامپوننت RoomDetails.razor جهت شروع به پرداخت آنلاین
کامپوننت RoomDetails.razor را در قسمت قبل آغاز کردیم و توسعه‌ی آن‌را تا جائی پیش بردیم که اعتبارسنجی‌های آن‌را به علت استفاده‌ی از خواص تو در تو، به صورت دستی انجام دادیم. پس از مرحله‌ی اعتبارسنجی، اکنون می‌خواهیم کاربر را به سمت درگاه بانکی جهت پرداخت، هدایت کنیم:
@page "/hotel-room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService
@inject IClientRoomOrderDetailsService RoomOrderDetailsService
@inject NavigationManager NavigationManager
@inject HttpClient HttpClient

// ...

@code {

    // ...

    private async Task HandleCheckout()
    {
        if (!await HandleValidation())
        {
            return;
        }

        try
        {
            HotelBooking.OrderDetails.ParbadTrackingNumber = -1;
            HotelBooking.OrderDetails.RoomId = HotelBooking.OrderDetails.HotelRoomDTO.Id;
            HotelBooking.OrderDetails.TotalCost = HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount;
            var roomOrderDetailsSaved = await RoomOrderDetailsService.SaveRoomOrderDetailsAsync(HotelBooking.OrderDetails);

            await LocalStorage.SetItemAsync(ConstantKeys.LocalRoomOrderDetails, roomOrderDetailsSaved);

            var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
                            .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
                            .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
                            .Uri;
            NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }

    // ...
}
متد HandleValidation را در انتهای قسمت قبل تکمیل کردیم. اکنون OrderDetails را بر اساس اطلاعات فرم و انتخاب‌های کاربر، تکمیل کرده و به متد SaveRoomOrderDetailsAsync ارسال می‌کنیم تا Id سفارش را تولید کنیم. این همان Id ای است که قرار است به سمت سرور و Web API ارسال کنیم تا بر اساس آن تراکنش و Tracking Number ای را بتوان به رکورد جاری انتساب داد. بنابراین نیاز به کنترلر سمت Web API ای را داریم که بتواند این‌کار را انجام دهد:
namespace BlazorWasm.WebApi.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class RoomOrderController : Controller
    {
        private readonly IRoomOrderDetailsService _roomOrderService;

        public RoomOrderController(IRoomOrderDetailsService roomOrderService)
        {
            _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
        }

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] RoomOrderDetailsDTO details)
        {
            var result = await _roomOrderService.CreateAsync(details);
            return Ok(result);
        }

        [HttpGet]
        public async Task<IActionResult> GetOrderDetail(int trackingNumber)
        {
            var result = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(trackingNumber);
            return Ok(result);
        }
    }
}
- متد Create، بر اساس اطلاعات وارد شده‌ی توسط کاربر، آن‌ها را تبدیل به یک رکورد سفارش جدید می‌کند و به سمت کلاینت بازگشت می‌دهد.
- متد GetOrderDetail، بر اساس trackingNumber دریافتی از پرباد، کار بازگشت رکورد متناظری را انجام می‌دهد. از آن در پایان کار، جهت نمایش وضعیت پرداخت، استفاده می‌کنیم.
این دو متد در سرویس سمت سرور RoomOrderDetailsService، به صورت زیر تامین شده‌اند:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
        {
            var roomOrder = _mapper.Map<RoomOrderDetail>(details);
            roomOrder.Status = BookingStatus.Pending;
            var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
        }


        public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }

       // ...
    }
}
اکنون که Web API Endpoint مدنظر را ایجاد کردیم، نیاز است سرویس سمت کلاینتی را نیز جهت تعامل با آن تهیه کنیم:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber);
    }
}

namespace BlazorWasm.Client.Services
{
    public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
    {
        private readonly HttpClient _httpClient;

        public ClientRoomOrderDetailsService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/roomorder/GetOrderDetail"))
                            .AddParameter("trackingNumber", trackingNumber.ToString())
                            .Uri;
            return _httpClient.GetFromJsonAsync<RoomOrderDetailsDTO>(uri);

        }

        public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            details.UserId = "unknown user!";
            var response = await _httpClient.PostAsJsonAsync("api/roomorder/create", details);
            var responseContent = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {
                return JsonSerializer.Deserialize<RoomOrderDetailsDTO>(responseContent);
            }
            else
            {
                //var errorModel = JsonSerializer.Deserialize<ErrorModel>(responseContent);
                throw new InvalidOperationException(responseContent);
            }
        }
    }
}
- متد GetOrderDetailAsync بر اساس trackingNumber دریافتی پس از عملیات تصدیق پرداخت، کار بازگشت جزئیات رکورد متناظری را انجام می‌دهد.
- متد SaveRoomOrderDetailsAsync، یک رکورد سفارش جدید را ایجاد می‌کند. در اینجا با روش مشاهده‌ی خطای کامل بازگشتی از سمت سرور (در صورت وجود) هم آشنا شده‌ایم که در مواقع لزوم می‌تواند راه‌گشا باشد.
- در متد SaveRoomOrderDetailsAsync فعلا مقدار UserId اجباری را به عبارتی دلخواه، تنظیم کرده‌ایم. این مورد را در قسمت‌های بعدی با معرفی اعتبارسنجی و احراز هویت سمت کلاینت، تکمیل خواهیم کرد.

این سرویس جدید را هم باید به سیستم تزریق وابستگی‌های برنامه‌ی کلاینت معرفی کرد تا قابل استفاده شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
بنابراین در متد HandleCheckout ای که در حال بررسی آن هستیم، ابتدا متد SaveRoomOrderDetailsAsync فوق فراخوانی می‌شود تا توسط Web API Endpoint متناظری، یک رکورد سفارش ابتدایی را ایجاد کرده و Id آن‌را در اختیار ما قرار دهد.
سپس به قطعه کد مهم زیر می‌رسیم:
var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
    .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
    .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
    .Uri;
NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
اینجا است که برای نمونه آدرس https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000 ساخته شده و توسط متد NavigateTo فراخوانی می‌شود. فراخوانی متداول متد NavigateTo در اینجا کارساز نیست؛ چون سبب reload آدرس درخواستی نمی‌شود. یعنی هدایت‌های صورت گرفته‌ی توسط آن، در همان داخل مرورگر رخ می‌دهند و سبب ارسال درخواستی به سمت سرور نخواهند شد. می‌توان این رفتار را با ذکر پارامتر دوم آن تغییر داد. در اینجا اگر پارامتر forceLoad را به true تنظیم کنیم، ابتدا سبب هدایت به آدرس درخواستی و سپس reload کامل صفحه می‌شود (دقیقا مثل اینکه شخصی، آدرسی را در نوار آدرس مرورگر وارد کند و سپس دکمه‌ی enter را بفشارد). این reload است که برنامه‌ی کلاینت را اکنون به سمت برنامه‌ی Web API هدایت می‌کند.


نمایش وضعیت پرداخت، به کاربر در پایان گردش کاری آن

پس از این مراحل، مرحله‌ی آخر کار باقی مانده‌است؛ یعنی بازگشت از اکشن متد VerifyRoomOrderPayment سمت سرور، به کامپوننت PaymentResult سمت کلاینت، برای نمایش نتیجه‌ی عملیات. به همین جهت کامپوننت جدید Pages\HotelRooms\PaymentResult.razor را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
@inject ILocalStorageService LocalStorage
@inject IClientRoomOrderDetailsService RoomOrderDetailService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager

@if (IsLoading)
{
    <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;">
        <img src="images/ajax-loader.gif" />
    </div>
}
else
{
    <div class="container">
        <div class="row mt-4 pt-4">
            <div class="col-10 offset-1 text-center">
            @if(IsPaymentSuccessful)
            {
                <h2 class="text-success">Booking Confirmed!</h2>
                <p>Your room has been booked successfully with order id @OrderId & tracking number @TrackingNumber .</p>
            }
            else
            {
                <h2 class="text-warning">Booking Failed!</h2>
                <p>@Message</p>
            }
            <a class="btn btn-primary" href="hotel-rooms">Back to rooms</a>
            </div>
        </div>
    </div>
}

@code
{
    private bool IsLoading;
    private bool IsPaymentSuccessful;

    [Parameter] public int OrderId { set; get; }
    [Parameter] public long TrackingNumber { set; get; }
    [Parameter] public string Message { set; get; }

    protected override async Task OnInitializedAsync()
    {
        IsLoading = true;
        try
        {
            var finalOrderDetail = await RoomOrderDetailService.GetOrderDetailAsync(TrackingNumber);
            var localOrderDetail = await LocalStorage.GetItemAsync<RoomOrderDetailsDTO>(ConstantKeys.LocalRoomOrderDetails);
            if(finalOrderDetail is not null &&
                finalOrderDetail.IsPaymentSuccessful &&
                finalOrderDetail.Status == BookingStatus.Booked &&
                localOrderDetail is not null &&
                localOrderDetail.TotalCost == finalOrderDetail.TotalCost)
            {
                IsPaymentSuccessful = true;
                await LocalStorage.RemoveItemAsync(ConstantKeys.LocalRoomOrderDetails);
                await LocalStorage.RemoveItemAsync(ConstantKeys.LocalInitialBooking);
            }
            else
            {
                IsPaymentSuccessful = false;
            }
        }
        catch(Exception ex)
        {
            await JsRuntime.ToastrError(ex.Message);
        }
        finally
        {
            IsLoading = false;
        }
    }
}
این کامپوننت بر اساس مسیریابی که دارد:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
سه پارامتر شماره سفارش، شماره تراکنش و پیامی را پس از پایان عملیات تصدیق پرداخت، از Web API، در طی یک redirect کامل دریافت می‌کند. در ادامه به کمک متد RoomOrderDetailService.GetOrderDetailAsync که آن‌را پیشتر توسعه دادیم، اصل رکورد متناظر با این سفارش را بازیابی کرده و فیلدهای IsPaymentSuccessful و Status آن‌را بررسی می‌کنیم (این فیلدها در زمان تصدیق پرداخت، در همان سمت سرور مقدار دهی می‌شوند). همچنین جهت محکم‌کاری، قسمتی از این اطلاعات را با Local Storage نیز انطباق داده‌ایم. اگر پرداخت، موفقیت آمیز باشد، شماره سفارش و همچنین شماره تراکنش را به کاربر نمایش می‌دهیم و یا پیام دریافتی از سرور را در صفحه درج می‌کنیم.


جلوگیری از ثبت سفارش اتاقی که رزرو شده‌است


پس از پایان عملیات سفارش یک اتاق، بهتر است امکان سفارش اتاقی را که دیگر در دسترس نیست، غیرفعال کنیم (تصویر فوق) که اینکار را می‌توان توسط خاصیت IsBooked مدل UI کامپوننت نمایش لیست اتاق‌ها انجام داد:
    public class HotelRoomDTO
    {
        public bool IsBooked { get; set; }

        // ...
    }
این خاصیت را در متدهای بازگشت لیست تمام اتاق‌ها و یا بازگشت اطلاعات یک اتاق، به صورت زیر محاسبه و مقدار دهی می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
       // ...

        public async Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDateStr, DateTime? checkOutDatestr)
        {
            var hotelRooms = await _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .Include(x => x.RoomOrderDetails)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .ToListAsync();

            foreach (var hotelRoom in hotelRooms)
            {
                hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDateStr, checkOutDatestr);
            }

            return hotelRooms;
        }

        public async Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate)
        {
            var hotelRoom = await _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .Include(x => x.RoomOrderDetails)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
            hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDate, checkOutDate);
            return hotelRoom;
        }

        private bool isRoomBooked(HotelRoomDTO hotelRoom, DateTime? checkInDate, DateTime? checkOutDate)
        {
            if (checkInDate == null || checkOutDate == null)
            {
                return false;
            }

            return hotelRoom.RoomOrderDetails.Any(x => x.IsPaymentSuccessful &&
                        //check if checkin date that user wants does not fall in between any dates for room that is booked
                        ((checkInDate < x.CheckOutDate && checkInDate.Value.Date >= x.CheckInDate)
                        //check if checkout date that user wants does not fall in between any dates for room that is booked
                        || (checkOutDate.Value.Date > x.CheckInDate.Date && checkInDate.Value.Date <= x.CheckInDate.Date))
                    );
        }
    }
}
متد isRoomBooked، یک محاسبه‌ی سمت سرور محسوب نمی‌شود؛ چون با استفاده از Include‌های نوشته شده، اطلاعات کامل اتاق و وابستگی‌های آن (سرهای دیگر رابطه‌ی تشکیل شده) را داریم و این محاسبات سبب رفت و برگشتی به سمت سرور نمی‌شوند.

اکنون که خاصیت IsBooked مقدار دهی شده‌است، در دو قسمت از آن استفاده خواهیم کرد:
الف) در کامپوننت نمایش لیست اتاق‌ها
@if (room.IsBooked)
{
    <button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
    <a href="@($"hotel-room-details/{room.Id}")" class="btn btn-success btn-block">Book</a>
}
ب) در کامپوننت نمایش جزئیات یک اتاق
@if (HotelBooking.OrderDetails.HotelRoomDTO.IsBooked)
{
    <button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
    <button type="submit" class="btn btn-success form-control">Checkout Now</button>
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-30.zip
مطالب
پشتیبانی از انقیاد پویا در سی‌شارپ
زبان سی‌شارپ strongly typed و type safe است. کامپایلر بیشتر کد را از نظر صحت نوع (Type) بررسی میکند و در صورت بروز خطا، روند کامپایل متوقف خواهد شد. با این وجود سی‌شارپ اجازه میدهد که کدهای داینامیک نیز داشته باشیم؛ کدهایی که در زمان کامپایل برای کامپایلر ناشناس هستند و اگر خطای نوع در آنها وجود داشته باشد، در زمان اجرا مشخص شده و باعث توقف برنامه میشود. 

Type Safety

ایمنی نوع، قاعده‌ای است در زبانهای برنامه‌نویسی که اجازه نمیدهد متغیرها، مقادیری را دریافت کنند که متفاوت با نوع تعریف شده‌ی آنها باشد. اگر این بررسی وجود نداشت، در زمان اجرا مقادیر خوانده شده از حافظه باعث رفتاری غیر قابل پیش‌بینی میشد؛ مثلا در یک متغیر عددی، مقدار رشته‌ای ذخیره و در زمان اجرا با یک مقدار عددی دیگر جمع بسته و نمایش داده شود. کامپایلر همچنین بررسی اعضای اعلان نشده‌ی متغیرها را نیز انجام میدهد که در قطعه کد زیر آمده‌است:
string text = “String value”;
int textLength = text.Length;
int textMonth = text.Month; // won’t compile
با این حال ایمنی نوع در سی‌شارپ کاملا قابل اعتماد نیست و میشود به روشی آن را دور زد!  
public interface IGeometricShape
{
     double Circumference { get; }
     double Area { get; }
}
public class Square : IGeometricShape
{
     public double Side { get; set; }
     public double Circumference => 4 * Side;
     public double Area => Side * Side;
}
public class Circle : IGeometricShape
{
     public double Radius { get; set; }
     public double Circumference => 2 * Math.PI * Radius;
     public double Area => Math.PI * Radius * Radius;
}

IGeometricShape circle = new Circle { Radius = 1 };
Square square = ((Square)circle); // no compiler error
var side = square.Side;
در خط کدی که با کامنت مشخص شده، هر چند که دیده میشود نوع circle نمیتواند به نوع square تبدیل شود، اما این کد بدون خطا کامپایل و خطای InvalidCastException  در زمان اجرا رخ خواهد داد. به دلیل اینکه هر دو نوع circle و square از نوع پایه IGeometricShape هستند، کامپایلر خطایی نخواهد گرفت؛ اما در زمان اجرا و زمانیکه برنامه میخواهد اجزاء circle را به square تبدیل کند، مشخص میشود که امکان تبدیل کامل circle به square نیست و خطا رخ خواهد داد.

Dynamic Binding

توسط انقیاد پویا در سی‌شارپ، کامپایلر بررسی نوع را در زمان کامپایل انجام نخواهد داد. کامپایلر فرض را بر این میگیرد که کد معتبر است و تمام متغیرها به درستی قابل دسترسی هستند. بررسی‌ها در زمان اجرا خواهند بود و زمانی خطا رخ خواهد داد که مثلا دسترسی به یک عضو از یک متغیر امکانپذیر نباشد؛ به این دلیل که آن عضو برای آن نوع وجود ندارد. 
توسط کلمه کلیدی dynamic میتوان متغیرهایی را تعریف کرد که در زمان کامپایل از نظر نوع بررسی نشوند؛ مانند مثال زیر.
dynamic text = “String value”;
int textLength = text.Length;
int textMonth = text.Month; // throws exception at runtime
واضح است که مثال بالا بی‌فایده است؛  اولا خطا در زمان کامپایل مشخص نمیشود و ثانیا مدیریت خطا در زمان اجرا بر کارآیی برنامه تاثیر خواهد داشت. روش دیگر استفاده از dynamic که کارآیی پایینی دارد در مثال زیر آمده.  
public dynamic GetAnonymousType()
{
  return new
    {
        Name = “John”,
        Surname = “Doe”,
        Age = 42
    };
}

dynamic value = GetAnonymousType();
Console.WriteLine($”{value.Name} {value.Surname}, {value.Age}”);
در مثال بالا نوع بازگشتی متد و متغیری که برای نگهداری نوع بازگشتی تعریف شده از نوع dynamic هستند. هر چند که در زمان کامپایل میشود هر مقداری و نوعی را از متد بازگشت داد، اما مانند مثال قبل، تا زمان اجرا، صحت اینکه آیا واقعا چنین نوعی جهت بازگشت وجود دارد یا نه و همچنین اساسا نوع بازگشت داده شده قابل استفاده و تبدیل هست یا نه، بررسی نخواهد شد. مضاف بر این مشکلات، IntelliSense نخواهیم داشت و اگر بخواهیم از یک اسمبلی دیگر به متد بالا دسترسی پیدا کنیم با خطای RuntimeBinderException مواجه خواهیم شد؛ علت این است که  نوع‌های anonymous به صورت internal اعلان می‌شوند. اما میشود استفاده‌های بهتری از نوع dynamic داشت؛ برای مثال زمان استفاده از کتابخانه‌ی JSON.NET که نمونه‌ای از آن در زیر آمده.
string json = @"
{
     ""name"": ""John"",
     ""surname"": ""Doe"",
     ""age"": 42
}";

dynamic value = JObject.Parse(json);
Console.WriteLine($"{ value.name} { value.surname}, { value.age}");
مانند نوع anonymous در مثال قبل، متد Parse میتواند مقادیر را به صورت پویا برگشت دهد و میتوان از این مقادیر مانند خصوصیات شیء ایجاد شده، از JSON استفاده کرد، بدون آنکه کامپایلر از وجود آنها اطلاعی داشته باشد. به این ترتیب در زمان اجرا میشود اشیاء JSON را به برنامه داد و از مقادیر آن مانند دسترسی به یک property استفاده کرد؛ کاری که نمیشود با نوعهای anonymous که در مثال بالاتر آورده شد انجام داد. برای حل این مسئله میتوان از دو شیء کمکی در کتابخانه NET Framework. استفاده کرد.

ExpandoObject

بین این دو شیء، ExpandoObject ساده‌تر است. به همراه کلمه کلیدی dynamic، این شیء اجازه میدهد که به نوع ساخته شده از آن در زمان اجرا و به صورت پویا، عضوی اضافه یا حذف کنیم؛ این اعضا میتوانند متد هم باشند.
dynamic person = new ExpandoObject();
person.Name = "John";
person.Surname = "Doe";
person.Age = 42;
person.ToString = (Func<string>)(() => $”{person.Name} {person.Surname}, {person. Age}”);

Console.WriteLine($"{ person.Name}{ person.Surname}, { person.Age}");

  برای اینکه ببینیم در زمان اجرا چه اعضایی به این شی اضافه شده، می‌توان نمونه ساخته شده از آن را به نوع <IDictionary<string, object تبدیل و در یک حلقه به آنها دسترسی پیدا کرد. از همین طریق هم میشود عضوی را حذف کرد.

var dictionary = (IDictionary<string, object>)person;
foreach (var member in dictionary)
{
     Console.WriteLine($”{member.Key} = {member.Value}”);
}
dictionary.Remove(“ToString”);

DynamicObject

از آنجایی که ExpandoObject برای سناریو‌های ساده کاربرد دارد و کنترل کمتری بر روی اعضا و نمونه‌های ایجاد شده‌ی توسط آن داریم، می‌توان از شیء DynamicObject استفاده کرد؛ البته نیاز به کدنویسی بیشتری دارد. پیاده‌سازی اعضا برای شیء DynamicObject در یک کلاس صورت میگیرد که در زیر آورده شده‌است:

class MyDynamicObject : DynamicObject
{
       private readonly Dictionary<string, object> members = new Dictionary<string, object>();

       public override bool TryGetMember(GetMemberBinder binder, out object result)
       {
              if (members.ContainsKey(binder.Name))
              {
                  result = members[binder.Name];
                  return true;
              }
              else
              {
                  result = null;
                  return false;
             }
       }

      public override bool TrySetMember(SetMemberBinder binder, object value)
      {
               members[binder.Name] = value;
              return true;
      }

      public bool RemoveMember(string name)
      {
            return members.Remove(name);
      }

}

dynamic person = new MyDynamicObject();
person.Name = “John”;
person.Surname = “Doe”;
person.Age = 42;
person.AsString = (Func<string>)(() => $”{person.Name} {person.Surname}, {person.
Age}”);
یک نکته در قطعه کد بالا وجود دارد. در شیء ExpandoObject، متد ToString را اضافه کردیم، اما برای شیء DynamicObject نام آن را تغییر داده و مثلا AsString گذاشتیم. اگر از نام ToString استفاده میکردیم در زمان فراخوانی، متد پیش‌فرض کلاس DynamicObject فراخوانی میشد. DynamicObject زمانی یک عضو پویا را فراخوانی میکند که آن عضو جدید از قبل وجود نداشته باشد. از آنجا که خود کلاس، متد ToString را دارد متد TryGetMember برای فراخوانی کردن آن اجرا نخواهد شد.
مطالب
اندازه گیری کارآیی کدها توسط NBench
این روزها جهت اندازه‌گیری کارآیی قطعات کدهای دات نتی، استفاده از فریم ورک‌های مخصوصی که بسیاری از نکات ریز مرتبط با اینگونه اندازه‌گیری‌ها را مانند warmup یا گرم کردن JIT (جهت عدم اندازه گیری زمان کامپایل پویای کدها، بجای زمان واقعی اجرای آن‌ها)، اندازه‌گیری فشار بر روی Garbage collector و غیره را انجام می‌دهند، بجای استفاده‌ی از Stop Watch، متداول است. یکی از معروفترین‌های این گروه، که تقریبا حالت استانداردی را در جهت اندازه گیری کارآیی کدهای دات نتی پیدا کرده‌است، فریم ورک سورس باز NBench است.


شروع به کار با NBench

برای شروع به کار با NBench، ابتدا نیاز است دو بسته‌ی نیوگت ذیل را نصب کرد:
PM> Install-Package NBench
PM> Install-Package NBench.Runner
عملکرد این فریم ورک، شبیه به عملکرد فریم ورک‌های آزمون‌های واحد است. برای مثال فرض کنید که می‌خواهید فشار حافظه و فشار بر روی GC قطعه کدی را اندازه گیری کنید:
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
public void AddMemoryMeasurement()
{
    const int numberOfAdds = 1000000;
    var dictionary = new Dictionary<int, int>();
    for (var i = 0; i < numberOfAdds; i++)
    {
        dictionary.Add(i, i);
    }
}
 
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[GcMeasurement(GcMetric.TotalCollections, GcGeneration.AllGc)]
public void MeasureGarbageCollections()
{
    var dataCache = new List<int[]>();
    for (var i = 0; i < 500; i++)
    {
        for (var j = 0; j < 10000; j++)
        {
            var data = new int[100];
            dataCache.Add(data.ToArray());
        }
 
        dataCache.Clear();
    }
}
همانند نوشتن متدهای آزمون‌های واحد، ابتدا یک یا چند متد public void را در اینجا اضافه می‌کنیم.
سپس هر متد تست به ویژگی PerfBenchmark مزین می‌شود. در اینجا RunMode.Iterations به این معنا است که خودمان قصد داریم در طی یک حلقه، تعداد بار انجام را مشخص کنیم.
ویژگی MemoryMeasurement برای اندازه گیری حافظه‌ی مصرفی یک قطعه کد و GcMeasurement برای اندازه گیری فشار بر روی Garbage collector بکار می‌رود.


اجرای آزمون‌های NBench

پس از تهیه‌ی دو متد فوق، به پوشه‌ی packages\NBench.Runner.0.3.4\lib\net45 مراجعه کنید. یک فایل exe در آن موجود است که کار یافتن و اجرای آزمون‌های NBench را انجام می‌دهد. به عنوان پارامتر آن تنها کافی است مسیر اسمبلی برنامه (فایل exe و یا dll) را به آن ارسال کنیم:
 D:\Prog\NBenchSample\packages\NBench.Runner.0.3.4\lib\net45\NBench.Runner.exe "D:\Prog\NBenchSample\NBenchSample\bin\Release\NBenchSample.exe"
پس از آن، کار اجرای آزمون‌های NBench شروع شده و پس از مدتی ابتدا BEGIN WARMUP و END WARMUP‌ها را می‌توان مشاهده کرد و در آخر یک چنین خروجی ارائه می‌شود:
 --------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement ---------------
TotalBytesAllocated: Max: 47,842,944.00 bytes, Average: 42,002,757.60 bytes, Min: 41,353,848.00 bytes, StdDev: 2,052,032.33 bytes
TotalBytesAllocated: Max / s: 359,074,078.19 bytes, Average / s: 311,474,786.96 bytes, Min / s: 300,926,928.79 bytes, StdDev / s: 16,869,581.62 bytes

--------------- RESULTS: NBenchSample.Program+MeasureGarbageCollections ---------------
TotalCollections [Gen0]: Max: 708.00 collections, Average: 702.80 collections, Min: 697.00 collections, StdDev: 3.65 collections
TotalCollections [Gen0]: Max / s: 111.55 collections, Average / s: 109.87 collections, Min / s: 107.88 collections, StdDev / s: 1.28 collections

TotalCollections [Gen1]: Max: 338.00 collections, Average: 334.60 collections, Min: 330.00 collections, StdDev: 2.41 collections
TotalCollections [Gen1]: Max / s: 53.61 collections, Average / s: 52.31 collections, Min / s: 51.10 collections, StdDev / s: 0.70 collections

TotalCollections [Gen2]: Max: 32.00 collections, Average: 24.80 collections, Min: 18.00 collections, StdDev: 4.73 collections
TotalCollections [Gen2]: Max / s: 4.91 collections, Average / s: 3.87 collections, Min / s: 2.86 collections, StdDev / s: 0.72 collections


نکته‌ای در مورد اندازه گیری فشار حافظه

حافظه توسط سیستم عامل، به صورت صفحات تخصیص داده می‌شود. برای مثال اگر شما به 12 بایت نیاز داشته باشید، سیستم عامل ممکن است 8 کیلوبایت را جهت کاهش تعداد بار تخصیص‌های حافظه و بالا بردن سرعت کار، در اختیار برنامه قرار دهد. بنابراین جهت رسیدن به بهترین نتیجه، در اینجا بهتر است تعداد زیادی شیء را مورد آزمایش قرار داد. برای مثال در آزمایش فوق بجای افزودن یک آیتم به دیکشنری، افزودن میلیون‌ها شیء، نویز استراتژی تخصیص حافظه‌ی توسط سیستم عامل را به حداقل می‌رساند.

شبیه به همین استراتژی، در پیاده سازی Dictionary نیز بکارگرفته شده‌است:
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
public void AddMemoryMeasurement_With_initial_Size()
{
    const int numberOfAdds = 1000000;
    var dictionary = new Dictionary<int, int>(numberOfAdds);
    for (var i = 0; i < numberOfAdds; i++)
    {
        dictionary.Add(i, i);
    }
}
اگر اینبار این آزمون را انجام دهیم، به نتیجه‌ی ذیل خواهیم رسید:
 --------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement_With_initial_Size ---------------
TotalBytesAllocated: Max: 23,245,912.00 bytes, Average: 23,245,912.00 bytes, Min: 23,245,912.00 bytes, StdDev: 0.00 bytes
TotalBytesAllocated: Max / s: 394,032,435.34 bytes, Average / s: 389,108,363.43 bytes, Min / s: 378,502,981.34 bytes, StdDev / s: 5,575,519.09 bytes
در اینجا زمانیکه شیء دیکشنری ایجاد شده‌است، اندازه‌ی اولیه‌ی آن نیز مشخص گردیده‌است. همین مساله سبب شده‌است تا مصرف حافظه‌ی آن از نزدیک به 41 مگابایت (متد AddMemoryMeasurement ابتدای بحث) به نزدیک 24 مگابایت (متد AddMemoryMeasurement_With_initial_Size فوق) کاهش یابد.
علت اینجا است که دیکشنری در پشت صحنه، از یک متد ReSize استفاده می‌کند که شبیه به سیستم عامل، بیشتر از مقدار مورد نیاز جهت ذخیره‌ی اشیاء، برای کاهش تعداد بار تخصیص‌های حافظه، حافظه به خود اختصاص می‌دهد. به همین جهت زمانیکه اندازه‌ی اولیه را مشخص کرد‌ه‌ایم، کار تخصیص حافظه‌ی بیش از اندازه‌ی این شیء، به شدت کاهش یافته‌است.


بررسی متد MeasureGarbageCollections

در متد MeasureGarbageCollections، مقدار زیادی شیء بر روی heap ایجاد شده و GC را وادار به عکس العمل شدید می‌کند.
حلقه‌ی داخلی ایجاد شده نیز تعداد زیادی شیء را در جهت پاکسازی GC تخصیص می‌دهد. این پاکسازی در مرحله‌ا‌ی به نام generation 0 صورت می‌گیرد.
اشیاء اضافه شده‌ی به لیست، طول عمر بیشتری دارند (تا پایان حلقه). بنابراین از garbage collection at generation 0 جان سالم به در خواهند برد و در garbage collection at generation 1  به عمر آن‌ها پایان داده خواهد شد. هرچند ممکن است تعدادی از آن‌ها پاکسازی نشده و تا پایان full garbage collection (generation 2) باقی بمانند.
در آزمایش انجام شده، با ذکر GcGeneration.AllGc، هر سه مورد Gen0 تا Gen2 اندازه گیری خواهند شد. عموما اندازه گیری Gen0 و Gen1 مهم نیستند و این‌ها خیلی زود به پایان خواهند رسید. اگر تعداد بار رخ‌دادن Gen2 زیاد بود (یا اصلا وجود داشت)، می‌تواند سبب بروز مشکلات کارآیی شدیدی گردد.
بنابراین می‌توان بجای تنظیم GcGeneration.AllGc، صرفا از GcGeneration.Gen2 استفاده کرد.


اندازه‌گیری Throughput یا تعداد بار اجرای یک متد در ثانیه

روش دیگر کار با فریم ورک NBench، ایجاد یک کلاس مخصوص و سپس افزودن متدهای Setup مزین به PerfSetup، متد Cleanup مزین به PerfCleanup و سپس تعدادی متد اندازه گیری کارآیی توسط ویژگی PerfBenchmark است. در اینجا برای اندازه‌گیری سرعت اجرای متدها، از ویژگی CounterThroughputAssertion استفاده خواهد شد که پارامتر اول آن نام یک شمارشگر است. این شمارشگر در متد Setup ایجاد می‌شود (با یک نام دلخواه).
public class DictionaryThroughputTests
{
    private readonly Dictionary<int, int> _dictionary = new Dictionary<int, int>();
 
    private const string AddCounterName = "AddCounter";
    private Counter _addCounter;
    private int _key;
 
    private const int AverageOperationsPerSecond = 20000000;
 
    [PerfSetup]
    public void Setup(BenchmarkContext context)
    {
        _addCounter = context.GetCounter(AddCounterName);
        _key = 0;
    }
 
    [PerfBenchmark(RunMode = RunMode.Throughput, TestMode = TestMode.Test)]
    [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)]
    public void AddThroughput_ThroughputMode(BenchmarkContext context)
    {
        _dictionary.Add(_key++, _key);
        _addCounter.Increment();
    }
 
    [PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Test)]
    [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)]
    public void AddThroughput_IterationsMode(BenchmarkContext context)
    {
        for (var i = 0; i < AverageOperationsPerSecond; i++)
        {
            _dictionary.Add(i, i);
            _addCounter.Increment();
        }
    }
 
    [PerfCleanup]
    public void Cleanup(BenchmarkContext context)
    {
        _dictionary.Clear();
    }
}
در این آزمایش‌ها، RunMode.Throughput به معنای اجرای متد آزمایش به تعداد AverageOperationsPerSecond توسط فریم ورک NBench است. در حالت قید RunMode.Iterations، تعداد بار اجرا، توسط حلقه‌ای که ما مشخص کرده‌ایم، تعیین می‌گردد.
 --------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_ThroughputMode ---------------
[Counter] AddCounter: Max: 575,654.00 operations, Average: 575,654.00 operations, Min: 575,654.00 operations, StdDev: 0.00 operations
[Counter] AddCounter: Max / s: 7,205,997.59 operations, Average / s: 7,163,894.30 operations, Min / s: 7,075,316.79 operations, StdDev / s: 42,518.20 operations

--------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_IterationsMode ---------------
[Counter] AddCounter: Max: 20,000,000.00 operations, Average: 20,000,000.00 operations, Min: 20,000,000.00 operations, StdDev: 0.00 operations
[Counter] AddCounter: Max / s: 7,409,380.61 operations, Average / s: 7,250,991.24 operations, Min / s: 6,880,938.73 operations, StdDev / s: 148,085.19 operations
اگر دقت کنید، کارآیی اندازه گیری شده‌ی در حالت RunMode.Iterations بیشتر است از حالت RunMode.Throughput. چون در حالت RunMode.Throughput، فریم ورک کار اجرای متد را از طریق Reflection انجام می‌دهد. بنابراین بهتر است از حالت RunMode.Iterations، جهت رسیدن به نتایج دقیق‌تری استفاده کرد.
در اینجا برای گزارش دادن، عددهای Average و  Average / s باید مورد استفاده قرار گیرند.
مطالب
بررسی تغییرات Blazor 8x - قسمت چهاردهم - امکان استفاده از کامپوننت‌های Blazor در برنامه‌های ASP.NET Core 8x
ASP.NET Core 8x به همراه یک IResult جدید به‌نام RazorComponentResult است که توسط آن می‌توان در Endpoint‌های Minimal-API و همچنین اکشن متدهای MVC، از کامپوننت‌های Blazor، خروجی گرفت. این خروجی نه فقط static یا به عبارتی SSR، بلکه حتی می‌تواند تعاملی هم باشد. در این مطلب، جزئیات فعالسازی و استفاده از این IResult جدید را در یک برنامه‌ی Minimal-API بررسی می‌کنیم.


ایجاد یک برنامه‌ی Minimal-API جدید در دات نت 8

پروژه‌ای را که در اینجا پیگیری می‌کنیم، بر اساس قالب استاندارد تولید شده‌ی توسط دستور dotnet new webapi تکمیل می‌شود.


ایجاد یک صفحه‌ی Blazor 8x به همراه مسیریابی و دریافت پارامتر

در ادامه قصد داریم که یک کامپوننت جدید را به نام SsrTest.razor در پوشه‌ی جدید Components\Tests ایجاد کرده و برای آن مسیریابی از نوع page@ هم تعریف کنیم. یعنی نه‌فقط قصد داریم آن‌را توسط RazorComponentResult رندر کنیم، بلکه می‌خواهیم اگر آدرس آن‌را در مرورگر هم وارد کردیم، قابل دسترسی باشد.
به همین جهت یک پوشه‌ی جدید را به نام Components در ریشه‌ی پروژه‌ی Web API جاری ایجاد می‌کنیم، با این محتوا:
برای ایده گرفتن از محتوای مورد نیاز، به «معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8» قسمت دوم این سری مراجعه کرده و برای مثال قالب ساده‌ترین حالت ممکن را توسط دستور زیر تولید می‌کنیم (در یک پروژه‌ی مجزا، خارج از پروژه‌ی جاری):
dotnet new blazor --interactivity None
پس از اینکار، محتویات پوشه‌ی Components آن‌را مستقیما داخل پوشه‌ی پروژه‌ی Minimal-API جاری کپی می‌کنیم. یعنی در نهایت در این پروژه‌ی جدید Web API، به فایل‌های زیر می‌رسیم:
- فایل Imports.razor_ ساده شده برای سهولت کار با فضاهای نام در کامپوننت‌های Blazor (فضاهای نامی را که در آن وجود ندارند و مرتبط با پروژه‌ی دوم هستند، حذف می‌کنیم).
- فایل App.razor، برای تشکیل نقطه‌ی آغازین برنامه‌ی Blazor.
- فایل Routes.razor برای معرفی مسیریابی صفحات Blazor تعریف شده.
- پوشه‌ی Layout برای معرفی فایل MainLayout.razor که در Routes.razor استفاده شده‌است.

و ... یک فایل آزمایشی جدید به نام Components\Tests\SsrTest.razor با محتوای زیر:
@page "/ssr-page/{Data:int}"

<PageTitle>An SSR component</PageTitle>

<h1>An SSR component rendered by a Minimal-API!</h1>

<div>
    Data: @Data
</div>

@code {

    [Parameter]
    public int Data { get; set; }

}
این فایل، می‌تواند پارامتر Data را از طریق فراخوانی مستقیم آدرس فرضی http://localhost:5227/ssr-page/2 دریافت کند و یا ... از طریق خروجی جدید RazorComponentResult که توسط یک Endpoint سفارشی ارائه می‌شود:




تغییرات مورد نیاز در فایل Program.cs برنامه‌ی Web-API برای فعالسازی رندر سمت سرور Blazor

در ادامه کل تغییرات مورد نیاز جهت اجرای این برنامه را مشاهده می‌کنید:
var builder = WebApplication.CreateBuilder(args);

// ...

builder.Services.AddRazorComponents();

// ...

// http://localhost:5227/ssr-component?data=2
// or it can be called directly http://localhost:5227/ssr-page/2
app.MapGet("/ssr-component",
           (int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               return new RazorComponentResult<SsrTest>(parameters);
           });

app.UseStaticFiles();
app.UseAntiforgery();

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

// ...
توضیحات:
- همین اندازه تغییر در جهت فعالسازی رندر سمت سرور کامپوننت‌های Blazor در یک برنامه‌ی ASP.NET Core کفایت می‌کند. یعنی اضافه شدن:
AddRazorComponents ،UseAntiforgery و MapRazorComponents
- در اینجا نحوه‌ی ارسال پارامترها را به یک RazorComponentResult نیز مشاهده می‌کنید.
- در حالت فراخوانی از طریق مسیر endpoint (یعنی فراخوانی مسیر http://localhost:5227/ssr-component در مثال فوق)، خود کامپوننت فراخوانی شده، بدون layout تعریف شده‌ی در فایل App.razor، رندر می‌شود. علت اینجا است که layout برنامه به همراه کامپوننت Router و RouteView آن فعال می‌شود که این دو هم مختص به صفحات دارای مسیریابی Blazor هستند و برای رندر کامپوننت‌های خالص آن بکار گرفته نمی‌شوند. خروجی RazorComponentResult تنها یک static SSR خالص است؛ مگر اینکه فایل blazor.web.js را نیز بارگذاری کند.

یک نکته: اگر در حالت رندر توسط RazorComponentResult، علاقمند به استفاده‌ی از layout هستید، می‌توان از کامپوننت LayoutView داخل یک کامپوننت فرضی به صورت زیر استفاده کرد؛ اما این مورد هم شامل اطلاعات فایل App.razor نمی‌شود:
<LayoutView Layout="@typeof(MainLayout)">
    <PageTitle>Home</PageTitle>

    <h2>Welcome to your new app.</h2>
</LayoutView>


سؤال: آیا در این حالت کامپوننت‌های تعاملی هم کار می‌کنند؟

پاسخ: بله. فقط برای ایده گرفتن، یک نمونه پروژه‌ی تعاملی Blazor 8x را در ابتدا ایجاد کنید و قسمت‌های اضافی AddRazorComponents و MapRazorComponents آن‌را در اینجا کپی کنید؛ یعنی برای مثال جهت فعالسازی کامپوننت‌های تعاملی Blazor Server، به این دو تغییر زیر نیاز است:
// ...

builder.Services.AddRazorComponents()
       .AddInteractiveServerComponents();

// ...

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

// ...
همچنین باید دقت داشت که امکانات تعاملی، به دلیل وجود و دسترسی به یک سطر ذیل که در فایل Components\App.razor واقع شده، اجرایی می‌شوند:
<script src="_framework/blazor.web.js"></script>
و همانطور که عنوان شد، اگر از روش new RazorComponentResult استفاده می‌شود، باید این سطر را به صورت دستی اضافه‌کرد؛ چون به همراه رندر layout تعریف شده‌ی در فایل App.razor نیست. برای مثال فرض کنید کامپوننت معروف Counter را به صورت زیر داریم که حالت رندر آن به InteractiveServer تنظیم شده‌است:
@rendermode InteractiveServer

<h1>Counter</h1>

<p role="status">Current count: @_currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int _currentCount;

    private void IncrementCount()
    {
        _currentCount++;
    }

}
در این حالت پس از تعریف endpoint زیر، خروجی آن فقط یک صفحه‌ی استاتیک SSR خواهد بود و دکمه‌ی Click me آن کار نمی‌کند:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<Counter>());
علت اینجا است که اگر به سورس HTML رندر شده مراجعه کنیم، خبری از درج اسکریپت blazor.web.js در انتهای آن نیست. به همین جهت برای مثال فایل جدید CounterInteractive.razor را به صورت زیر اضافه می‌‌کنیم که ساختار آن شبیه به فایل App.razor است:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive server component</title>
    <base href="/"/>
</head>
<body>
   <h1>Interactive server component</h1>

   <Counter/>

  <script src="_framework/blazor.web.js"></script>
</body>
</html>
و هدف اصلی از آن، تشکیل یک قالب و درج اسکریپت blazor.web.js در انتهای آن است.
سپس تعریف endpoint متناظر را به صورت زیر تغییر می‌دهیم:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<CounterInteractive>());
اینبار به علت بارگذاری فایل blazor.web.js، امکانات تعاملی کامپوننت Counter فعال شده و قابل استفاده می‌شوند.


سؤال: آیا می‌توان این خروجی static SSR کامپوننت‌های بلیزر را در سرویس‌های یک برنامه ASP.NET Core هم دریافت کرد؟

منظور این است که آیا می‌توان از یک کامپوننت Blazor، به همراه تمام پیشرفت‌های Razor در آن که در Viewهای MVC قابل دسترسی نیستند، به‌شکل یک رشته‌ی خالص، خروجی گرفت و برای مثال از آن به‌عنوان قالب پویای محتوای ایمیل‌ها استفاده کرد؟
پاسخ: بله! زیر ساخت RazorComponentResult که از سرویس HtmlRenderer استفاده می‌کند، بدون نیاز به برپایی یک endpoint هم قابل دسترسی است:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace WebApi8x.Services;

public class BlazorStaticRendererService
{
    private readonly HtmlRenderer _htmlRenderer;

    public BlazorStaticRendererService(HtmlRenderer htmlRenderer) => _htmlRenderer = htmlRenderer;

    public Task<string> StaticRenderComponentAsync<T>() where T : IComponent
        => RenderComponentAsync<T>(ParameterView.Empty);

    public Task<string> StaticRenderComponentAsync<T>(Dictionary<string, object?> dictionary) where T : IComponent
        => RenderComponentAsync<T>(ParameterView.FromDictionary(dictionary));

    private Task<string> RenderComponentAsync<T>(ParameterView parameters) where T : IComponent =>
        _htmlRenderer.Dispatcher.InvokeAsync(async () =>
                                             {
                                                 var output = await _htmlRenderer.RenderComponentAsync<T>(parameters);
                                                 return output.ToHtmlString();
                                             });
}
برای کار با آن، ابتدا باید سرویس فوق را به صورت زیر ثبت و معرفی کرد:
builder.Services.AddScoped<HtmlRenderer>();
builder.Services.AddScoped<BlazorStaticRendererService>();
و سپس یک نمونه مثال فرضی نحوه‌ی تزریق و فراخوانی سرویس BlazorStaticRendererService به صورت زیر است که در آن روش ارسال پارامترها هم بررسی شده‌است:
app.MapGet("/static-renderer-service-test",
           async (BlazorStaticRendererService renderer, int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               var html = await renderer.StaticRenderComponentAsync<SsrTest>(parameters);
               return Results.Content(html, "text/html");
           });

کدهای کامل این مطلب را می‌توانید از اینجا دریافت کنید: WebApi8x.zip
نظرات مطالب
T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
با سلام
T4MVC یکسری تغییرات در برنامه ما ایجاد می‌کنه از جمله مثلا یک کانستراکتور برای کنترلر Home  اضافه می‌کنه و من چگونه باید کدی مثل کد زیر رو داخل کنترل ام بنویسیم که خطای تکراری نگیره؟
private readonly ICacheService _cacheService;
public HomeController() : this(MvcApplication.CacheService)
{            
}
public HomeController(ICacheService cacheService)
{
      _cacheService = cacheService;
}
نظرات مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت دوم
دوست عزیز آوردن پراپرتی Tedad در کلاس KalaViewModel اشتباهه و همچنین map کردن Anbar_kala با آن.
کار زیر را میتونی انجام بدی:
public class Anbar_KalaViewModel
    {
        public Anbar Anbar { get; set; }
        public Kala Kala{ get; set; }
        public int Tedad { get; set; }
    }
//Class Configure
Mapper.CreateMap<Anbar_Kala,Anbar_KalaViewModel>();


مطالب
کش خروجی API در ASP.NET Core با Redis
در این مقاله نمی‌خواهیم به طور عمیقی وارد جزییاتی مثل توضیح Redis یا کش بشویم؛ فرض شده‌است که کاربر با این مفاهیم آشناست. به طور خلاصه کش کردن یعنی همیشه به دیتابیس یا هارددیسک برای گرفتن اطلاعاتی که می‌خواهیم و گرفتنش هم کند است، وصل نشویم و بجای آن، اطلاعات را در یک محل موقتی که گرفتنش خیلی سریعتر بوده قرار دهیم و برای استفاده به آنجا برویم و اطلاعات را با سرعت بالا بخوانیم. کش کردن هم دسته بندی‌های مختلفی دارد که بر حسب سناریوهای مختلفی که وجود دارد، کاربرد خود را دارند. مثلا ساده‌ترین کش در ASP.NET Core، کش محلی (In-Memory Cache) می‌باشد که اینترفیس IMemoryCache را اعمال می‌کند و نیازی به هیچ پکیجی ندارد و به صورت درونی در ASP.NET Core در دسترس است که برای حالت توسعه، یا حالتیکه فقط یک سرور داشته باشیم، مناسب است؛ ولی برای برنامه‌های چند سروری، نوع دیگری از کش که به اصطلاح به آن Distributed Cache می‌گویند، بهتر است استفاده شود. چند روش برای پیاده‌سازی با این ساختار وجود دارد که نکته مشترکشان اعمال اینترفیس واحد IDistributedCache می‌باشد. در نتیجه‌ی آن، تغییر ساختار کش به روش‌های دیگر، که اینترفیس مشابهی را اعمال می‌کنند، با کمترین زحمت صورت می‌گیرد. این روش‌ها به طور خیلی خلاصه شامل موارد زیر می‌باشند: 

1- Distributed Memory Cache: در واقع Distributed نیست و کش معمولی است؛ فقط برای اعمال اینترفیس IDistributedCache که امکان تغییر آن در ادامه‌ی توسعه نرم‌افزار میسر باشد، این روش توسط مایکروسافت اضافه شده‌است. نیاز به نصب پکیجی را ندارد و به صورت توکار در ASP.NET Core در دسترس است.
2- Distributed SQL Server Cache: کاربرد چندانی ندارد. با توجه به اینکه هدف اصلی از کش کردن، افزایش سرعت و عدم اتصال به دیتابیس است، استفاده از حافظه‌ی رم، بجای دیتابیس ترجیح داده می‌شود.
3- Distributed Redis Cache: استفاده از Redis که به طور خلاصه یک دیتابیس Key/Value در حافظه است. سرعت بالایی دارد و محبوب‌ترین روش بین برنامه‌نویسان است. برای اعمال آن در ASP.NET Core نیاز به نصب پکیج می‌باشد.

موارد بالا انواع زیرساخت و ساختار (Cache Provider) برای پیاده‌سازی کش می‌باشند. روش‌های مختلفی برای استفاده از این Cache Providerها وجود دارد. مثلا یک روش، استفاده مستقیم در کدهای درونی متد یا کلاسمان می‌باشد و یا در روش دیگر می‌توانیم به صورت یک Middleware این پروسه را مدیریت کنیم، یا در روش دیگر (که موضوع این مقاله است) از ActionFilterAttribute استفاده می‌کنیم. یکی از روش‌های جالب دیگر کش کردن، اگر از Entity Framework به عنوان ORM استفاده می‌کنیم، استفاده از سطح دوم کش آن (EF Second Level Cache) می‌باشد. EF دو سطح کش دارد که سطح اول آن توسط خود Context به صورت درونی استفاده می‌شود و ما می‌توانیم از سطح دوم آن استفاده کنیم. مزیت آن به نسبت روش‌های قبلی این است که نتیجه‌ی کوئری ما (که با عبارات لامبدا نوشته می‌شود) را کش می‌کند و علاوه بر امکان تنظیم زمان انقضا برای این کش، در صورت تغییر یک entity خاص (انجام عملیات Update/Insert/Delete) خود به خود، کش کوئری مربوط به آن entity پاک می‌شود تا با مقدار جدید آن جایگزین شود که روش‌های دیگر این مزیت را ندارند. در این مقاله قرار نیست در مورد این روش کش صحبت کنیم. استفاده از این روش کش به صورت توکار در EF Core وجود ندارد و برای استفاده از آن در صورتی که از EF Core قبل از ورژن 3 استفاده می‌کنید می‌توانید از پکیج  EFSecondLevelCache.Core  و در صورت استفاده از EF Core 3 از پکیج  EF Core Second Level Cache Interceptor  استفاده نمایید که در هر دو حالت می‌توان هم از Memory Cache Provider و هم از Redis Cache Provider استفاده نمود.

در این مقاله می‌خواهیم Responseهای APIهایمان را در یک پروژه‌ی Web API، به ساده‌ترین حالت ممکن کش کنیم. زیرساخت این کش می‌تواند هر کدام از موارد ذکر شده‌ی بالا باشد. در این مقاله از Redis برای پیاده‌سازی آن استفاده می‌کنیم که با نصب پکیج Microsoft.Extensions.Caching.StackExchangeRedis انجام می‌گیرد. این بسته‌ی نیوگت که متعلق به مایکروسافت بوده و روش پایه‌ی استفاده از Redis در ASP.NET Core است، اینترفیس IDistributedCache را اعمال می‌کند:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis

سپس اینترفیس IResponseCacheService را می‌سازیم تا از این اینترفیس به جای IDistributedCache استفاده کنیم. البته می‌توان از IDistributedCache به طور مستقیم استفاده کرد؛ ولی چون همه‌ی ویژگی‌های این اینترفیس را نمی‌خواهیم و هم اینکه می‌خواهیم serialize کردن نتایج API را در کلاسی که از این اینترفیس ارث‌بری می‌کند (ResponseCacheService) بیاوریم (تا آن را کپسوله‌سازی (Encapsulation) کرده باشیم تا بعدا بتوانیم مثلا بجای پکیج Newtonsoft.Json، از System.Text.Json برای serialize کردن‌ها استفاده کنیم):
public interface IResponseCacheService
    {
        Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive);
        Task<string> GetCachedResponseAsync(string cacheKey);
    }
یادآوری: Redis قابلیت ذخیره‌ی داده‌هایی از نوع آرایه‌ی بایت‌ها را دارد (و نه هر نوع دلخواهی را). بنابراین اینجا ما بجای ذخیره‌ی مستقیم نتایج APIهایمان (که ممکن نیست)، می‌خواهیم ابتدا آن‌ها را با serialize کردن به نوع رشته‌ای (که فرمت json دارد) تبدیل کنیم و سپس آن را ذخیره نماییم.

حالا کلاس ResponseCacheService که این اینترفیس را اعمال می‌کند می‌سازیم: 
    public class ResponseCacheService : IResponseCacheService, ISingletonDependency
    {
        private readonly IDistributedCache _distributedCache;

        public ResponseCacheService(IDistributedCache distributedCache)
        {
            _distributedCache = distributedCache;
        }

        public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive)
        {
            if (response == null) return;
            var serializedResponse = JsonConvert.SerializeObject(response);
            await _distributedCache.SetStringAsync(cacheKey, serializedResponse, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = timeToLive
            });
        }

        public async Task<string> GetCachedResponseAsync(string cacheKey)
        {
            var cachedResponse = await _distributedCache.GetStringAsync(cacheKey);
            return string.IsNullOrWhiteSpace(cachedResponse) ? null : cachedResponse;
        }
    }
دقت کنید که اینترفیس IDistributedCache در این کلاس استفاده شده است. اینترفیس ISingletonDependency صرفا یک اینترفیس نشان گذاری برای اعمال خودکار ثبت سرویس به صورت Singleton می‌باشد (اینترفیس را خودمان ساخته‌ایم و آن را برای رجیستر راحت سرویس‌هایمان تنظیم کرده‌ایم). اگر نمی‌خواهید از این روش برای ثبت این سرویس استفاده کنید، می‌توانید به صورت عادی این سرویس را رجیستر کنید که در ادامه، در قسمت مربوطه به صورت کامنت شده آمده است.

حالا کدهای لازم برای رجیستر کردن Redis و تنظیمات آن را در برنامه اضافه می‌کنیم. قدم اول ایجاد یک کلاس POCO به نام RedisCacheSettings است که به فیلدی به همین نام در appsettings.json نگاشت می‌شود:
public class RedisCacheSettings
    {
        public bool Enabled { get; set; }
        public string ConnectionString { get; set; }
        public int DefaultSecondsToCache { get; set; }
    }

این فیلد را در appsettings.json هم اضافه می‌کنیم تا در استارتاپ برنامه، با مپ شدن به کلاس RedisCacheSettings، قابلیت استفاده شدن در تنظیمات Redis را داشته باشد. 
"RedisCacheSettings": {
      "Enabled": true,
      "ConnectionString": "192.168.1.107:6379,ssl=False,allowAdmin=True,abortConnect=False,defaultDatabase=0,connectTimeout=500,connectRetry=3",
      "DefaultSecondsToCache": 600
    },

  حالا باید سرویس Redis را در متد ConfigureServices، به همراه تنظیمات آن رجیستر کنیم. می‌توانیم کدهای مربوطه را مستقیم در متد ConfigureServices بنویسیم و یا به صورت یک متد الحاقی در کلاس جداگانه بنویسیم و از آن در ConfigureServices استفاده کنیم و یا اینکه از روش Installer برای ثبت خودکار سرویس و تنظیماتش استفاده کنیم. اینجا از روش آخر استفاده می‌کنیم. برای این منظور کلاس CacheInstaller را می‌سازیم: 
    public class CacheInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            var redisCacheService = appSettings.RedisCacheSettings;
            services.AddSingleton(redisCacheService);

            if (!appSettings.RedisCacheSettings.Enabled) return;

            services.AddStackExchangeRedisCache(options =>
                options.Configuration = appSettings.RedisCacheSettings.ConnectionString);

            // Below code applied with ISingletonDependency Interface
            // services.AddSingleton<IResponseCacheService, ResponseCacheService>();
        }
    }

خب تا اینجا اینترفیس اختصاصی خودمان را ساختیم و Redis را به همراه تنظیمات آن، رجیستر کردیم. برای اعمال کش، چند روش وجود دارد که همانطور که گفته شد، اینجا از روش ActionFilterAttribute استفاده می‌کنیم که یکی از راحت‌ترین راه‌های اعمال کش در APIهای ماست. کلاس CachedAttribute را ایجاد می‌کنیم:
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class CachedAttribute : Attribute, IAsyncActionFilter
    {
        private readonly int _secondsToCache;
        private readonly bool _useDefaultCacheSeconds;
        public CachedAttribute()
        {
            _useDefaultCacheSeconds = true;
        }
        public CachedAttribute(int secondsToCache)
        {
            _secondsToCache = secondsToCache;
            _useDefaultCacheSeconds = false;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var cacheSettings = context.HttpContext.RequestServices.GetRequiredService<RedisCacheSettings>();

            if (!cacheSettings.Enabled)
            {
                await next();
                return;
            }

            var cacheService = context.HttpContext.RequestServices.GetRequiredService<IResponseCacheService>();

            // Check if request has Cache
            var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request);
            var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey);

            // If Yes => return Value
            if (!string.IsNullOrWhiteSpace(cachedResponse))
            {
                var contentResult = new ContentResult
                {
                    Content = cachedResponse,
                    ContentType = "application/json",
                    StatusCode = 200
                };
                context.Result = contentResult;
                return;
            }

            // If No => Go to method => Cache Value
            var actionExecutedContext = await next();

            if (actionExecutedContext.Result is OkObjectResult okObjectResult)
            {
                var secondsToCache = _useDefaultCacheSeconds ? cacheSettings.DefaultSecondsToCache : _secondsToCache;
                await cacheService.CacheResponseAsync(cacheKey, okObjectResult.Value,
                    TimeSpan.FromSeconds(secondsToCache));
            }
        }

        private static string GenerateCacheKeyFromRequest(HttpRequest httpRequest)
        {
            var keyBuilder = new StringBuilder();
            keyBuilder.Append($"{httpRequest.Path}");
            foreach (var (key, value) in httpRequest.Query.OrderBy(x => x.Key))
            {
                keyBuilder.Append($"|{key}-{value}");
            }

            return keyBuilder.ToString();
        }
    }
در این کلاس، تزریق وابستگی‌های IResponseCacheService و RedisCacheSettings به روش خاصی انجام شده است و نمی‌توانستیم از روش Constructor Dependency Injection استفاده کنیم چون در این حالت می‌بایستی این ورودی در Controller مورد استفاده هم تزریق شود و سپس در اتریبیوت [Cached] بیاید که مجاز به اینکار نیستیم؛ بنابراین از این روش خاص استفاده کردیم. مورد دیگر فرمول ساخت کلید کش می‌باشد تا بتواند کش بودن یک Endpoint خاص را به طور خودکار تشخیص دهد که این متد در همین کلاس آمده است. 
 
حالا ما می‌توانیم با استفاده از attributeی به نام  [Cached]  که روی APIهای از نوع HttpGet قرار می‌گیرد آن‌ها را براحتی کش کنیم. کلاس بالا هم طوری طراحی شده (با دو سازنده متفاوت) که در حالت استفاده به صورت [Cached] از مقدار زمان پیشفرضی استفاده می‌کند که در فایل appsettings.json تنظیم شده است و یا اگر زمان خاصی را مد نظر داشتیم (مثال 1000 ثانیه) می‌توانیم آن را به صورت  [(Cached(1000]  بیاوریم. کلاس زیر نمونه‌ی استفاده‌ی از آن می‌باشد:
[Cached]
[HttpGet]
public IActionResult Get()
  {
    var rng = new Random();
    var weatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
      Date = DateTime.Now.AddDays(index),
      TemperatureC = rng.Next(-20, 55),
      Summary = Summaries[rng.Next(Summaries.Length)]
    })
      .ToArray();
    return Ok(weatherForecasts);
  }
بنابراین وقتی تنظیمات اولیه، برای پیاده‌سازی این کش انجام شود، اعمال کردن آن به سادگی قرار دادن یک اتریبیوت ساده‌ی [Cached] روی هر apiی است که بخواهیم خروجی آن را کش کنیم. فقط توجه نمایید که این روش فقط برای اکشن‌هایی که کد 200 را بر می‌گردانند، یعنی متد Ok را return می‌کنند (OkObjectResult) کار می‌کند. بعلاوه اگر از اتریبیوت ApiResultFilter یا مفهوم مشابه آن برای تغییر خروجی API به فرمت خاص استفاده می‌کنید، باید در آن تغییرات کوچکی را انجام دهید تا با این حالت هماهنگ شود. 
نظرات مطالب
مدیریت اسپم‌ها در SignalR
بسیار مفید و کاربردی. البته یک نکته در استفاده از این راه وجود دارد که در هنگام پیاده سازی بهتر هست لحاظ شود:
- بسته به سیستمی که در آن استفاده میشود نیاز است "تعداد درخواست‌ها در زمان مورد نظر"، تغییر کند (به این دلیل که بعضی از درخواست‌های مهم در سیستمی که غیر از چت، همزمان از عملیات دیگری نیز استفاده میکند مختل میشود) یا متغیری برای متمایز کردن درخواست‌ها در کلاس ActivityInfo ایجاد شود. از قطعه کد زیر میتوان به نام تابعی که از سمت کلاینت صدا زده شده است دسترسی داشت:
var methodName = context.MethodDescriptor.Name;
برای دوستانی که از سی شارپ استفاده میکنند کدهای درج شده در پست به شکل زیر است:
public class ActivityInfo
    {
        public ActivityInfo(string connectionId)
        {
            ConnectionId = connectionId;
            Time = DateTime.Now;
        }
        public string ConnectionId { get; set; }

        public DateTime Time { get; set; }
    }
   
    public class SpamDetectionPiplelineModule : HubPipelineModule
    {
        public static HashSet<ActivityInfo> SpamDetection = new HashSet<ActivityInfo>();
        private readonly object _spamDetectionLock = new object();
        public bool IsSpam(string connectionId)
        {
            lock (_spamDetectionLock)
            {
                //Remove all old info before 3 seconds ago
                SpamDetection.RemoveWhere(q => q.Time < DateTime.Now.AddSeconds(-3));

                SpamDetection.Add(new ActivityInfo(connectionId));

                //Check activities from 3 seconds ago
                if (SpamDetection.Count(q => q.ConnectionId == connectionId) > 3)
                {
                    return true;
                }
                return false;
            }
        }
        protected override bool OnBeforeIncoming(IHubIncomingInvokerContext context)
        {
            if (IsSpam(context.Hub.Context.ConnectionId))
            {
                return false;
            }
            return base.OnBeforeIncoming(context);
        }
    }