نظرات اشتراک‌ها
دوراهی انتخاب NHibernate و Entityframework
1 - با EF Code first بدون نیاز به دیتابیس می‌تونید یک برنامه رو کامل کنید. (منهای بحث آزمایش)
- کد نهایی تمیزتر. چون کلاس‌ها را خودتان طراحی می‌کنید و توسط ابزارها به صورت خودکار تولید نمی‌شوند، کنترل بیشتر و نهایتا کیفیت بالاتری دارند.
- ساده است. درگیر نگهداری edmx modelها نخواهید بود. به روز رسانی بانک اطلاعاتی آن هم می‌تواند کاملا خودکار شود.
 
2 - دیتاست که کلا کارآیی بالایی نداره. اما ... نهایتا مطمئن هستم خروجی EF (به همراه تمام best practices لحاظ شده در آن) سرعت بالاتری از کلاس‌های دست ساز sql helper موجود در وب دارد. برای مثال سطح اول کش آن خیلی از کوئری‌ها را مجددا به بانک اطلاعاتی ارسال نمی‌کند. قابلیت اجرای به تعویق افتاده کوئری‌های لینک امکان تهیه کوئری‌های بسیار پیچیده را در یک رفت و برگشت مهیا می‌کند. کاری که با sql helperهای معمولی نیازی به چندبار رفت و برگشت دارد. قابلیت‌های lazy loading آن می‌تواند مصرف حافظه و بار سرور را درصورت استفاده صحیح کاهش دهد. کوئری‌های آن strongly typed و پارامتری هستند (تحت نظر کامپایلر + امنیت + سرعت (کوئری‌های پارامتری مانند رویه‌های ذخیره شده کش می‌شوند)). به صورت پیش فرض از تراکنش‌ها استفاده می‌کند و ... خیلی از الگوهای مفید دیگر که چندین سال باید وقت صرف کنید تا نمونه آن‌ها را پیاده سازی کنید. یعنی کار اصولی با بانک اطلاعاتی صرفا یک select ساده نیست که بر اساس آن کارآیی و یا بهتر بودن روشی را مشخص کنید.
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 3 - ثبت و واکشی تنظیمات
همانطور که پیشتر گفتیم، Dependency Injection Container، ماژول اصلی ASP.NET Core است. تقریبا تمامی ماژول‌ها و سرویس‌های ASP.NET Core از DI Container Injection استفاده می‌کنند که بعضی از آنها عبارتند از:
  •   Configuration
  •   Routing
  •   MVC
  •   Application
  • و ...
بصورت درونی، چارچوب/ فریم ورک ASP.NET Core، مسئول ارائه‌ی وابستگی‌ها، در زمان فعال سازی ماژول‌های خود فریم ورک ASP.NET Core می‌باشد.
فرض کنید یک درخواست برای صفحه‌ی اول سایت به وبسایتی بر پایه‌ی ASP.NET Core می‌رسد. به صورت گام به گام، این مراحل برای پردازش داده به کار می‌روند:
  1. کاربر یک درخواست Http را توسط مرورگر ارسال می‌کند.
  2. یکی از اولین میان افزار‌ها یعنی میان افزار Routing، آدرس درخواست را می‌خواند، کنترلر و اکشن مورد نظر را می‌یابد و به‌وسیله‌ی Activator Utility، سعی در فعال سازی آن کنترلر می‌کند. 
  3.   DI Container لیست پارامترهای سازنده‌ی کنترلر را مشاهده می‌کند و سرویس‌های مورد نیاز را از درون خود واکشی کرده، از آنها نمونه سازی می‌کند و نمونه‌های ساخته شده را  به درون شیء کنترلر تزریق می‌کند.
  4.  Routing درخواست HttpRequest را تجزیه کرده و اکشن متد مورد نظر را برای اجرای آن فراخوانی کرده
  5. و نتیجه‌ی اجرای اکشن را به درخواست دهنده بر می‌گرداند.

هر چند که کنترلرها درون DI Container ثبت نشده‌اند، ولی توسط کلاس‌هایی درون فریم ورک، از آنها نمونه سازی می‌شود و در حین نمونه سازی، DI Container سرویس‌های مورد نظر آن‌ها را در صورت وجود، فراهم می‌کند.

ثبت تنظیمات وبسایت و فراخوانی آنها در برنامه
در تمام برنامه‌های ASP.NET Core شما نیاز به تنظیماتی برای پیکربندی کار برنامه‌ی خود دارید. این تنظیمات می‌توانند شامل Connection String اتصال به پایگاه داده، تنظیمات اتصال به سرویس‌های خارجی مثل درگاه‌های پرداخت آنلاین بانک‌ها و ... باشند. در اینجا ما تنظیمات اختصاصی را درون فایل AppSetting اضافه می‌کنیم. بعد برای هر بخش از تنظیمات، در پوشه‌ی Configs یک کلاس ساده‌ی سی شارپ را می‌سازیم  و سپس با گرفتن و تزریق کردن این فایل‌های Config درون DI Container، هر زمانی خواستیم، از آنها استفاده می‌کنیم.
ابتدا به سراغ تنظیمات کلی می‌رویم و دو تنظیم نام برنامه و پیغام خوش آمد گویی را به برنامه اضافه می‌کنیم (فایل appSettings را به صورت زیر تغییر می‌دهیم) :
"ApplicationName": "Dependency Injection Demo",
"GreetingMessage": "Welcome to Dependency Injection Demo",
"AllowedHosts": "*",

"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},

برای سادگی کار، با بخش Logging کاری نداریم . اکنون فایل AppConfig.cs را به برنامه اضافه می‌کنیم:

namespace AspNetCoreDependencyInjection.Configs
{
    public class AppConfig
    {
        public string ApplicationName { get; set; }
        public string GreetingMessage { get; set; }
        public string AllowedHosts { get; set; }
    }
}

برای دسترسی بهتر می‌توانیم سازنده‌ی کلاس Startup را تغییر دهیم:

public IWebHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
public IServiceCollection Services { get; set; }

public Startup(IWebHostEnvironment environment)
{
var builder = new ConfigurationBuilder()
        .SetBasePath(environment.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true)
        .AddEnvironmentVariables();
        this.Environment = environment;
        this.Configuration = builder.Build();
}
کد بالا برای زمانی کاربرد دارد که شما بخواهید چند تنظیمات مختلف را در برنامه داشته باشید؛ مثلا در کد بالا در هنگام ساخت متغیر builder، می‌توانید با چک کردن متغیر environment، یک تنظیمات دیگر را داشته باشید (داشتن دو یا چند تنظیمات به خصوص برای زمان  توسعه و انتشار برنامه ضروری است. در ساده‌ترین کاربرد، شما در حالت توسعه به یک پایگاه داده تست وصل می‌شوید، ولی در حالت انتشار به پایگاه داده‌ی اصلی متصل خواهید شد). در اینجا یکی از  ساده‌ترین روش‌ها، استفاده از دو فایل تنظیمات مختلف برای زمان انتشار و غیر انتشار ( توسعه و Staging ) است:
var appSettingsFile = environment.IsProduction() ? "appsettings.json" : "appsettings_dev.json";
var builder = new ConfigurationBuilder()
.SetBasePath(environment.ContentRootPath)
                .AddJsonFile( appSettingsFile , optional: true)
                .AddEnvironmentVariables();
حالا که این تغییرات را انجام دادیم، دوباره به سراغ ثبت سرویس تنظیمات برنامه می‌رویم. برای اینکار در متد ConfigureServices و زیر خط‌های کد قبلی، این خطوط کد را اضافه می‌کنیم: 
services.AddSingleton(services => new AppConfig { 
    ApplicationName = this.Configuration["ApplicationName"],
    GreetingMessage = this.Configuration["GreetingMessage"],
    AllowedHosts = this.Configuration["AllowedHosts"]
});

در کد بالا در هنگام اجرای برنامه، یک نمونه از کلاس AppConfig را با طول حیات Singleton ثبت کردیم و Property ‌های این شیء را به وسیله‌ی ایندکس Configuration[“FieldName”]، تک تک پر کردیم.

حالا می‌توانیم سرویس AppConfig را در هر کلاسی از برنامه‌ی خودمان تزریق و از آن استفاده کنیم. برای مثل در اینجا یک کنترلر به نام AppSettingsController ساختم و کلاس فوق را به آن تزریق کردم: 

public class AppSettingsController : Controller
{
        private readonly AppConfig _appConfig;
        public AppSettingsController(AppConfig appConfig)
        {
            _appConfig = appConfig;
        } 
 // codes here …
}

می توانیم از همین الگو برای تعریف، ثبت و استفاده از سایر تنظیمات نیز استفاده کنیم:
"UserOptionConfig": {
    "UsersAvatarsFolder": "avatars",
    "UserDefaultPhoto": "icon-user-default.png",
    "UserAvatarImageOptions": {
         "MaxWidth": 150,
         "MaxHeight": 150
    }
},

"LiteDbConfig": {
   "ConnectionString": "Filename=\\Data\\DependencyInjectionDemo.db;Connection=direct;Password=@123456;"
}

برای LiteDbConfig مانند AppConfig عمل می‌کنیم، ولی در هنگام ثبت آن، به روش زیر عمل می‌کنیم. تنها تفاوتی که وجود دارد، نحوه‌ی دستیابی به فیلدهای درونی فایل JSON به وسیله‌ی شیء Configuration است: 

services.AddSingleton(services => new LiteDbConfig
{
    ConnectionString = this.Configuration["LiteDbConfig:ConnectionString"],
});

اکنون برای استفاده‌ی از مدخل UserOptionConfig، کلاس‌های زیر را می‌سازیم:

namespace AspNetCoreDependencyInjection.Configs
{
    public class UserOptionConfig
    {
        public string UsersAvatarsFolder { get; set; }
        public string UserDefaultPhoto { get; set; }
        public UserAvatarImageOptions UserAvatarImageOptions { get; set; }
    }

    public class UserAvatarImageOptions
    {
        public int MaxHeight { get; set; }
        public int MaxWidth { get; set; }
    }
}
می‌خواهیم روش Option Pattern را که روش توصیه  شده‌ی Microsoft برای استفاده از پیکربندی برنامه است، بکار ببریم. به صورت خلاصه، Option Pattern بیان می‌کند که بخش‌های مختلف پیکربندی تنظیمات برنامه را از یکدیگر جدا کنیم و به ازای هر بخش، کلاس‌های مختص به خود را داشته باشیم و با ثبت جداگانه‌ی آنها در DI Container ، از  آن‌ها استفاده کنیم.

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

  • Interface Segregation Principle (ISP) or Encapsulation : کلاس‌هایی که به تنظیمات نیاز دارند، فقط به آن بخشی از تنظیمات دسترسی خواهند داشتند که واقعا مورد نیازشان باشد.
  •   Separation Of Concerns : تنظیمات بخش‌های مختلف برنامه، به یکدیگر وابسته و  جفت شده نیستند.

در اینجا  نیاز به استفاده از پکیج Microsoft.Extensions.Options.ConfigurationExtensions را داریم که به صورت درونی در ASP.NET Core تعبیه شده است.

برای ثبت این تنظیمات درون DI Container، از نمونه‌ی جنریک متد Configure در IServiceCollection به صورت زیر استفاده می‌کنیم:

services.Configure<UserOptionConfig>(this.Configuration.GetSection("UserOptionConfig"));

متد GetSection بر اساس نام بخش تنظیمات، خود آن تنظیم و تمامی تنظیمات درونی آن را به صورت یک IConfigurationSection بر می‌گرداند و متد Configure<TOption> یک IConfiguration را گرفته و به صورت خودکار به TOption اتصال می‌دهد و سپس این شیء را درون DI Container به عنوان یک IConfigurationOptions<TOption> و با طول حیات Singleton ثبت می‌کند.

برای دسترس به UserOptionConfig درون کلاس مورد نظر ما، اینترفیس <IOptionMonitor<TOption را به سازنده‌ی کلاس مورد نظر تزریق می‌کنیم. کد زیر را که نسخه‌ی تغییر یافته‌ی کلاس AppSettingsController است را مشاهده کنید: 
private readonly LiteDbConfig _liteDbConfig;
private readonly AppConfig _appConfig;
private readonly UserOptionConfig _userOptionConfig; 

public AppSettingsController(AppConfig appConfig ,
    LiteDbConfig liteDbConfig ,
    IOptionsMonitor<UserOptionConfig> userOptionConfig)
{
    _appConfig = appConfig;
    _liteDbConfig = liteDbConfig;
    _userOptionConfig = userOptionConfig.CurrentValue;
}
در اینجا و در سازنده برای گرفتن TOption ، از CurrentValue که یک property تعریف شده‌ی درون IOptionsMonitor<TOption> است، استفاده می‌کنیم.

نکته ای که وجود دارد، کلاس‌های تعریف شده برای استفاده‌ی از این الگو باید شرایط زیر را داشته باشند ( مثل کلاس UserOptionConfig ) :

  • باید سطح دسترسی public داشته باشند.
  • باید دارای سازنده‌ی پیش فرض باشند.
  •   باید نام Property ‌های آنها دقیقا همنام فیلدهای تنظیمات باشد تا فرایند mapping خودکار به درستی انجام شود.
  •   باید Property ها و Setter آنها ، سطح دسترسی public داشته باشند.

هر دو روش بالا که یکی به صورت عادی تنظیمات را ثبت می‌کند و دیگری با استفاده از Option Pattern بخش‌های مختلف را ثبت می‌کند، مناسب هستند. البته گاهی اوقات فایل‌های تنظیمات پروژه‌ی شما در لایه‌های زیرین (یا درونی‌تر اگر از onion architecture استفاده می‌کنید) قرار دارند و شما نمی‌خواهید در آن لایه‌ها و لایه‌های درونی‌تر، وابستگی به پکیج‌های ASP.NET Core ایجاد کنید. در این حالت با در نظر گرفتن دو اصل ISP و Separation of Concerns ، به ازای هر بخش مختلف از تنظیمات، فایل‌های تنظیمات را در لایه‌های زیرین/درونی تعریف کرده، بعد در لایه‌های بالاتر/بیرونی‌تر آنها را به درون سرویس‌ها یا کلاس‌های مورد نیاز، تزریق کنید. البته مثل همین مثال، ثبت این سرویس‌ها درون برنامه‌ی ASP.NET Core که معمولا بالاترین/بیرونی‌ترین لایه از پروژه‌ی ما هست، انجام می‌شود.

بازخوردهای دوره
آشنایی با نحوه ایجاد یک IoC Container
پاسخ به این سؤال نیاز به مطالعه قسمت‌های بعدی دارد. 
- هدف از قسمت جاری آشنایی اولیه با مراحل ابتدایی کار با یک IoC Container است.
- در حالت اول هنوز شما هستید که مسئول وهله سازی‌های اولیه می‌باشید و کار وهله سازی را به لایه‌ای دیگر واگذار نکرده‌اید.
- در کد حالت اول نمی‌شود این وابستگی ارسالی به سازنده کلاس را به سادگی تعویض کرد. در حالیکه با استفاده از یک IoC container فقط کافی است تنظیمات اولیه نگاشت‌های آن‌را مشخص کنیم تا نوع کلاسی که باید در سازنده‌ها تزریق شوند مشخص شود. مزیت اینکار ساده‌تر شدن نوشتن آزمون‌های واحد و تهیه کلاس‌های Fake است؛ بدون نیازی به تغییری حتی در حد یک سطر در کدهای اصلی برنامه.
- در مورد وهله سازی خودکار چند سطح وابستگی‌ها، در قسمت‌های بعد تحت عنوان Object graph بیشتر بحث شده است و مثال زده شده. همیشه با یک کلاس ساده ویزا مانند مثال فوق سر و کار نداریم. عموما با سرویس‌هایی سر و کار داریم که خودشان نیز از سرویس‌های دیگری استفاده می‌کنند. برای مثال یک سرویس ارسال ایمیل از سرویس کاربران برای دریافت ایمیل‌های کاربران کمک می‌گیرد. وهله سازی تمام این وابستگی‌ها را در چند سطح می‌شود با استفاده از IoC Containers خودکار کرد و به کد‌های نهایی بسیار تمیزتری رسید.
- در حالت اول، طول عمر یک شیء را نمی‌شود مشخص کرد (یا حداقل نیاز به کد نویسی قابل توجهی دارد). برای مثال با استفاده از یک IoC Container می‌شود وهله ایجاد شده را Singleton کرد تا در سراسر برنامه یک وهله از آن استفاده شود یا حتی می‌شود در طول یک درخواست رسیده وب، یک وهله را در اختیار تمام کلاس‌های درگیر قرار داد. به این ترتیب به سربار کمتری در حالت‌های خاصی مانند وهله سازی ObjectContext یا DbContext در EF خواهیم رسید.
- زمان استفاده از IoC Containerها کارهای فراتری از تزریق وابستگی‌ها را هم می‌شود انجام داد. برای مثال فراخوانی‌های متدها را هم تحت نظر قرار داد (برنامه نویسی AOP یا جنبه گرا) و مثلا بدون نوشتن کد اضافه‌ای در برنامه، خروجی متدها را کش کرد. AOP یک سری بحث مفصل را در طی یک دوره جدا به همراه دارد.

نظرات مطالب
C# 8.0 - Async Streams
یک نکته‌ی تکمیلی: محدود کردن IAsyncEnumerable به یک فضای نام
ممکن است در برنامه‌های NET Core 3x. به خطای مبهم بودن محل دریافت IAsyncEnumerable بر بخورید:
The type ‘IAsyncEnumerable’ exists in both ‘System.Interactive.Async, Version=3.2.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263’
and ‘System.Runtime, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’
برای رفع آن نیاز است ارجاع IAsyncEnumerable را از System.Interactive.Async در فایل csproj، حذف کنید:
<Target Name="AddAssemblyAliasToReactiveAsync" AfterTargets="ResolveAssemblyReferences">
    <ItemGroup>
        <ReferencePath Condition=" '%(FileName)' == 'System.Interactive.Async' ">
            <Aliases>reactive</Aliases>
        </ReferencePath>
    </ItemGroup>
</Target>
نظرات مطالب
ویژگی Batching در EF Core
کدهای معادل مطلب فوق در EF 6.x را از اینجا دریافت کنید: EF6TestBatching.zip
این کدها برای حالت انجام 2 به روز رسانی و 6 ثبت، کدهای SQL زیر را تولید می‌کنند:
UPDATE [dbo].[Blogs]
SET [Url] = @0
WHERE ([BlogId] = @1)
-- @0: 'http://sample.com/blogs/dogs' (Type = String, Size = -1)
-- @1: '1' (Type = Int32)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 19 ms with result: 1

UPDATE [dbo].[Blogs]
SET [Url] = @0
WHERE ([BlogId] = @1)
-- @0: 'http://sample.com/blogs/cats' (Type = String, Size = -1)
-- @1: '2' (Type = Int32)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 47 ms with result: 1

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Horse Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/horses' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 31 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Snake Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/snakes' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 73 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Fish Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/fish' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 49 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Koala Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/koalas' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 49 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Parrot Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/parrots' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:41 ب.ظ +04:30
-- Completed in 26 ms with result: SqlDataReader

INSERT [dbo].[Blogs]([Name], [Url])
VALUES (@0, @1)
SELECT [BlogId]
FROM [dbo].[Blogs]
WHERE @@ROWCOUNT > 0 AND [BlogId] = scope_identity()
-- @0: 'The Kangaroo Blog' (Type = String, Size = -1)
-- @1: 'http://sample.com/blogs/kangaroos' (Type = String, Size = -1)
-- Executing at 16/06/1396 02:31:42 ب.ظ +04:30
-- Completed in 38 ms with result: SqlDataReader
یعنی در کل 8 بار رفت و برگشت به بانک اطلاعاتی صورت گرفته‌است (هر Executing در اینجا یعنی یکبار رفت و برگشت)؛ در مقابل تنها یکبار رفت و برگشت به بانک اطلاعاتی در حالت استفاده‌ی از EF Core (با تنظیمات پیش فرض آن).
جمع کل مدت زمان عملیات در اینجا 332 میلی ثانیه است در مقایسه با 44 میلی ثانیه EF Core. یعنی EF 6.x در حالت insert/update/delete چیزی حدود 86 درصد از نمونه‌ی EF Core کندتر است و این مورد اهمیت batching و کاهش تعداد رفت و برگشت‌های به بانک اطلاعاتی است که به صورت پیش فرض در EF Core فعال است.
نظرات مطالب
پایان پروژه ASP.NET Ajax Control Toolkit !
ضمنا یک مورد رو در باره‌ی LINQ to SQL و ASP کلاسیک اضافه کنم. جایگزین شدن entity framework بجای L2S یا ASP.NET به جای ASP کلاسیک، یک روند سالم و سلامت توسعه است. LINQ to SQL فقط محدود است به SQL Server اما الان اکثر بانک‌های اطلاعاتی موجود پروایدر EF دارند و مدل توسعه‌ی آن بسته نیست. ASP کلاسیک رو نمی‌دونم باهاش کار کرده بودید یا نه؟ رسما یک فاجعه بود! مخلوطی از کدهای برنامه داخل کدهای HTML و وابستگی آن به اشیاء COM و غیره (اگر می‌خواستید مثلا رمزنگاری را به آن اضافه کنید باید Active-X می‌نوشتید و در سرور رجیستر می‌کردید!). این مورد اصلا قابل قیاس نیست با ASP.NET و امکانات دات نت فریم ورک.
مطالب
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
اشتراک‌ها
دوره 9 ساعته Clean Architecture + CQRS

Mini Course #1 Clean Architecture + CQRS - YouTube

00:00:00 - Intro
00:01:10 - Why do we record this course?
00:04:39 - Layered architecture: Why I don't like it anymore?
00:27:27 - Clean architecture
00:38:16 - Course prerequisites (still can't pronounce it correctly)
00:42:30 - What do we build?
00:45:18 - *Domain Layer*
00:49:57 - Entity
01:00:20 - Primitive obsession code smell
01:03:20 - Value Object
01:10:54 - Custom exceptions
01:33:25 - Domain model validation
01:38:55 - Aggregate
01:50:50 - Domain event
02:14:30 - Factory
02:28:50 - Policy
02:43:50 - Repository
02:48:55 - Domain Layer: Summary
02:56:45 - CQS: Command Query Separation
03:07:46 - CQRS: Command Query Responsibility Segregation
03:29:48 - *Application Layer*
03:30:25 - Command/Command Handler/Command Dispatcher definitions
03:40:45 - Automatic command handlers registration
03:44:12 - Application & Domain registration
03:50:52 - Command
03:55:31 - Command Handler
04:01:11 - Where to put methods related to reading?
04:07:00 - Read Service
04:14:14 - Weather Service
04:28:25 - Overview of the other commands & command handlers
04:34:35 - Query/Query Handler/Query Dispatcher definitions
04:44:37 - Automatic query handlers registration (BONUS: example of copy/paste pattern)
04:52:50 - How to tackle reading on Query side?
05:06:30 - *Infrastructure Layer*
05:07:56 - Configuration of Entity Framework Core
05:15:15 - Read Models
05:19:05 - ReadDbContext & WriteDbContext
05:24:13 - EF Entity Configuration
05:42:37 - DbContexts registration
05:55:51 - Query Handlers
06:12:18 - Repository implementation on top of EF Core
06:16:45 - Read Service implementation on top of EF Core
06:19:00 - Dummy Weather Service implementation
06:23:20 - EF Migration
06:29:50 - Applying EF migrations automatically
06:39:38 - *Presentation Layer*
06:41:35 - Controller
06:50:43 - How to return ID of resource in CQRS approach?
07:00:02 - Testing API
07:09:29 - *Cross Cutting Concerns*
07:09:30 - Error Handling
07:20:49 - Logging
07:34:57 - *Unit Testing*
07:42:34 - Unit Test on Domain Layer
08:09:45 - Unit Test on Application Layer
08:34:00 - Summary 

دوره 9 ساعته Clean Architecture + CQRS