اشتراکها
انتشار Giraffe 1.0.0
اشتراکها
IdentityServer4.1.0.0 منتشر شد
در ادامهی تمرین قسمت قبل که مقدمات ثبت درخواست رزرو یک اتاق را فراهم کردیم، اکنون میخواهیم اگر کاربری بر روی دکمهی checkout now یک اتاق کلیک کرد، به درگاه مجازی پرباد منتقل شده، پرداخت را تکمیل کند، به برنامه هدایت شود و در آخر درخواست او در سیستم ثبت گردد. مزیت کار کردن با درگاه مجازی پرباد، امکان آزمایش محلی برنامه، بدون نیاز به یک درگاه بانکی واقعی است و زمانیکه قرار است با یک درگاه بانکی واقعی کار شود، فقط قسمت معرفی و تنظیمات ابتدایی مشخصات درگاه بانکی آن باید تغییر کند و نه هیچ قسمت دیگری از کدهای برنامه.
نصب پرباد و انجام تنظیمات اولیهی آن
بستههای نیوگت پرباد را در دو پروژهی زیر نصب خواهیم کرد:
الف) پروژهی Web API (و یا همان BlazorWasm.WebApi در مثال این سری):
که شامل بستههای ASP.NET Core آن و همچنین محل ذخیره سازی مبتنی بر EF-Core آن است.
ب) پروژهای که محل قرارگیری فایلهای Migration است (و یا همان BlazorServer.DataAccess) در این مثال:
که در اینجا فقط نیاز به بستهی EF-Core آن است تا بتوان Context مخصوص پرباد را در حین اعمال مهاجرتها شناسایی کرد.
پس از نصب این بستهها، به کلاس آغازین پروژهی Web API مراجعه کرده و تنظیمات سرویسها و همچنین میانافزار پرباد را انجام میدهیم:
چند نکته:
- در متد ConfigureGateways میتوان چندین درگاه را معرفی کرد که برای مثال در اینجا از درگاه مجازی و محلی آن استفاده شدهاست.
- در متد ConfigureStorage، تنظیمات EF-Core آنرا مشاهده میکنید. پرباد به همراه DbContext خاص خودش است. یعنی در این حالت برنامهی شما حداقل دو DbContext خواهد داشت؛ یکی ApplicationDbContext و دیگری ParbadDataContext.
- میخواهیم شمارهی تراکنشها را به صورت خودکار توسط پرباد مدیریت کنیم. به همین جهت میتوان عدد ابتدای آنرا توسط متد ConfigureAutoTrackingNumber مشخص کرد.
- در پایان هم تعاریف مسیریابی میانافزار آنرا مشاهده میکنید که میتواند برای حالت توسعه و ارائهی نهایی متفاوت باشد.
تکمیل خواص موجودیت RoomOrderDetail جهت کار با پرباد
موجودیت RoomOrderDetail را در قسمت قبل معرفی کردیم. پرباد به ازای هر تراکنش بانکی که صورت میگیرد، یا نیاز به یک TrackingNumber خودکار را دارد و یا دستی. یعنی یا میتوانیم شماره تراکنش خاص خودمان را تولید کنیم و در اختیار آن قرار دهیم و یا از آن درخواست کنیم تا این شماره را مدیریت کرده و به صورت خودکار تولید کند. در هر دو حالت نیاز است این شماره را به ردیفهای جدول جزئیات سفارشات اتاقهای هتل اضافه کرد که در این مثال ParbadTrackingNumber نام دارد:
همچنین در پایان عملیات هم فیلدهای IsPaymentSuccessful و وضعیت اتاق را تکمیل میکنیم.
ایجاد جداول متناظر با ParbadDataContext
همانطور که عنوان شد، اکنون برنامه به همراه دو DbContext است. بنابراین در این حالت در حین اجرای مهاجرتها، ذکر نام Context مدنظر اجباری است.
برای ایجاد مهاجرتهای متناظر با ParbadDataContext، از طریق خط فرمان به پوشهی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا میکنیم:
چون برنامه دو Context ای است، نیاز است دوبار دستور تولید مهاجرتها و دوبار دستور اعمال آنها را به بانک اطلاعاتی صادر کرد که روش آنرا در دستورات فوق مشاهده میکنید. پس از این دستورات، بانک اطلاعاتی برنامه شامل دو جدول جدید مخصوص پرباد خواهد بود:
روش یکپارچه سازی پرباد با یک برنامهی SPA
روش متداول کار با پرباد، بر اساس طراحی مخصوص ASP.NET Core آن است. ابتدا درخواستی را به آن ارسال میکنید. سپس پرباد شماره تراکنشی را تولید کرده و شروع تراکنش را در بانک اطلاعاتی ثبت میکند. در ادامه به صورت خودکار، کار ارسال اطلاعات به درگاه بانکی (برای مثال ارسال تمام فیلدهای یک فرم ویژهی آن بانک، بر اساس مستندات آن) و هدایت به درگاه بانکی را انجام میدهد. پس از پایان کار پرداخت، کار هدایت به اکشن متد دریافت تائیدیهی نهایی صورت میگیرد و همینجا کار به پایان میرسد. این روش هرچند برای برنامههای سمت سرور ASP.NET Core کار میکند، اما ... به همین نحو با برنامههای تک صفحهای وب مانند Blazor WASM قابل استفاده نیست. در اینجا روش تبادل اطلاعات با اکشن متدهای وب سرویسهای برنامه از طریق یک HttpClient است و در این حالت دیگر نمیتوان از مزایای Post و Redirect خودکار پرباد که در سمت سرور صورت میگیرد استفاده کرد. با استفاده از HttpClient، یک شیء را به سمت Web API ارسال میکنیم و در پاسخ، فقط یک شیء را دریافت میکنیم. در اینجا دیگر خبری از Redirect به درگاه اصلی بانکی و Post اطلاعات به آن نیست. بنابراین روش کار با پرباد در اینجا به صورت زیر خواهد بود:
الف) شماره Id سفارش و مبلغ نهایی آنرا از طریق یک درخواست Get معمولی به اکشن متدی در سمت سرور ارسال میکنیم. یعنی نیاز است ابتدا Url زیر را تشکیل داد که شماره سفارش و مبلغ آن، به صورت کوئری استرینگهایی به اکشن متد PayRoomOrder ارسال میشوند:
برنامهی کلاینت برای اینکه بتواند این هدایت را انجام دهد، نیاز به نکتهی خاصی را دارد که در ادامه توضیح داده خواهد شد.
ب) اکنون چون یک redirect سمت سرور صورت گرفته، به صورت معمولی در اکشن متد PayRoomOrder با پرباد پردازش صورت گرفته و به سمت درگاه هدایت میشویم. پس از پرداخت نهایی، باز هم به صورت خودکار به اکشن متد دیگری جهت تائید عملیات هدایت خواهیم شد.
ج) در پایان کار، اکشن متد سمت سرور، ما را به سمت کامپوننتی در برنامهی کلاینت Redirect خواهد کرد:
در اینجا شماره سفارش ابتدایی که مشخص است. همان شمارهای است که کار را با آن از سمت کلاینت آغاز کردیم. نکتهی مهم، TrackingNumber تراکنش است که بر اساس آن رکورد متناظری یافت شده و وضعیت نهایی آنرا به کاربر نمایش میدهیم.
بنابراین روش یکپارچه سازی پربابد با برنامههای SPA، بر اساس Redirectهای کامل است که سبب بارگذاری مجدد کل صفحه و آدرسها میشوند و در اینجا از HttpClient برای کار با پرباد استفاده نخواهیم کرد؛ چون تمام اعمال خودکار آنرا از دست خواهیم داد و مجبور به بازنویسی آنها خواهیم شد که در دراز مدت با تغییرات این کتابخانه، قابل نگهداری نخواهند بود. بنابراین بهتر است خود پرباد کار Redirectها و ارسال اطلاعات به درگاههای بانکی را مدیریت کند و نه ما از طریق کار با یک HttpClient.
آشنایی با گردش کار برنامه
در این مثال، مراحل زیر را طی خواهیم کرد:
1- شروع به انتخاب یک بازهی زمانی و تعداد شب اقامت
2- انتخاب یک اتاق از لیست اتاقها با کلیک بر روی دکمهی Book آن
3- کلیک بر روی دکمهی checkout، در صفحهی مشاهدهی جزئیات اتاق و شروع به پرداخت
4- هدایت به درگاه مجازی پرباد در سمت برنامهی Web API
5- پرداخت و هدایت خودکار به سمت برنامهی Web API، جهت تائید نهایی
6- هدایت نهایی به سمت برنامهی کلاینت، جهت نمایش اطلاعات پرداخت
ایجاد کنترلر پرداخت، توسط درگاه مجازی پرباد
پس از آشنایی با گردش کاری اطلاعات در اینجا، نیاز است بتوان لینک زیر را در برنامهی کلاینت تولید کرد و سپس کاربر را به سمت اکشن متد PayRoomOrder هدایت نمود:
این اکشن متد و کنترلر آن به صورت زیر تهیه میشود:
توضیحات:
در اینجا کدهای کامل ParbadPaymentController مشاهده میکنید.
- گردش کاری پرداخت، با فراخوانی اکشن متد PayRoomOrder شروع میشود که دو پارامتر شماره سفارش و مبلغ آنرا دریافت میکند.
نوع آن هم عمدا، HttpGet درنظر گرفته شدهاست تا دقیقا مشخص باشد که فقط با Redirect کامل به آن (هدایت کامل از سمت کلاینت به سمت سرور)، کار خواهد کرد و هدف دیگری را دنبال نمیکند.
- در اکشن متد PayRoomOrder، نیاز است لینک بازگشت از درگاه بانکی را مشخص کنیم. پس از اینکه کاربر پرداختی را انجام داد، مجددا به صورت خودکار، به سمت آدرسی در همین Web API و نه برنامهی سمت کلاینت هدایت میشود؛ چون هنوز کار پرباد به پایان نرسیده و باید عملیات انجام شده را تصدیق کند. به همین جهت ابتدا آدرس اکشن متدی که کار تائید نهایی را انجام میدهد، تولید کرده و به متد RequestAsync آن به همراه مبلغ نهایی و نوع درگاه، ارسال میکنیم.
- استفاده از UseAutoIncrementTrackingNumber سبب میشود تا پرباد خودش مدیریت TrackingNumber را انجام دهد که پس از پایان عملیات، توسط خاصیت result.TrackingNumber در دسترس خواهد بود.
- پس از پایان عملیات ابتدایی RequestAsync که سشن پرباد را ایجاد کرده و همچنین رکوردی را در بانک اطلاعاتی نیز ثبت میکند (در جداول درونی خود پرباد)، نیاز است رکورد سفارشی را که با آن کار را شروع کردیم یافته و TrackingNumber آنرا با مقدار واقعی دریافتی از پرباد، به روز رسانی کنیم. اینکار توسط متد UpdateRoomOrderTrackingNumberAsync انجام میشود:
بر اساس شماره سفارشی که داریم، رکورد متناظر با آنرا یافته و سپس trackingNumber تولیدی را در آن به روز رسانی میکنیم.
- اکنون با فراخوانی متد ()result.GatewayTransporter.TransportToGateway، دو کار مهم رخ میدهند:
الف) ارسال خودکار اطلاعات به سمت درگاه بانکی
ب) Redirect خودکار به سمت درگاه بانگی
به همین جهت است که علاقمند نبودیم تا این مراحل را توسط HttpClient برنامهی Blazor WASM مدیریت و بازنویسی کنیم.
- پس از هدایت به سمت درگاه بانکی و تکمیل پرداخت، اکنون مجددا به همان verifyUrl هدایت میشویم. یعنی اکنون به مرحلهی پردازش اکشن متد VerifyRoomOrderPayment در سمت Web API رسیدهایم.
در اینجا ابتدا invoice.TrackingNumber در حال پردازش را دریافت میکنیم. به کمک این عدد میتوان رکورد سفارش متناظر با آنرا یافت. به همین جهت است که آنرا به لیست فیلدهای جدول سفارشات اضافه کردیم. اینکار هم توسط متد GetOrderDetailByTrackingNumberAsync صورت میگیرد:
- در ادامه پرباد کار تصدیق اطلاعات دریافتی از درگاه بانکی را انجام میدهد. دراینجا اگر عملیات با موفقیت مواجه شود، سه فیلدی را که در ابتدای بحث در مورد ثبت اطلاعات تراکنش اضافه کردیم، به روز رسانی میکنیم:
- در اینجا بر اساس trackingNumber، سند متناظری را یافته و سپس بررسی میکنیم که آیا مبلغ سند، با مبلغ تائید شده، یکی هست یا خیر؟ اگر خیر، نیاز هست پرداخت را برگشت بزنیم که اینکار توسط متد کنسل پرباد قابل انجام است.
- در تمام این مراحل، کار Redirect به سمت کلاینت و کامپوننت payment-result آن، با فراخوانی متد return Redirect اکشن متدها صورت میگیرد که Url آن به صورت زیر تامین میشود:
در این متد Client_URL را از فایل appsettings.json برنامهی Web API دریافت میکنیم که به آدرس ریشهی برنامهی کلاینت اشاره میکند:
تکمیل قسمت سمت کلاینت عملیات پرداخت بانکی، توسط درگاه مجازی پرباد
تا اینجا کنترلری که کار پرداخت آنلاین را مدیریت میکند، پیاده سازی کردیم. قسمت آخر این بحث به تکمیل جزئیات این گردش کاری که شامل شروع عملیات سفارش و پرداخت از سمت کلاینت و نمایش پیام خطا یا موفقیت پرداخت به کاربر است، اختصاص دارد.
الف) تکمیل کامپوننت RoomDetails.razor جهت شروع به پرداخت آنلاین
کامپوننت RoomDetails.razor را در قسمت قبل آغاز کردیم و توسعهی آنرا تا جائی پیش بردیم که اعتبارسنجیهای آنرا به علت استفادهی از خواص تو در تو، به صورت دستی انجام دادیم. پس از مرحلهی اعتبارسنجی، اکنون میخواهیم کاربر را به سمت درگاه بانکی جهت پرداخت، هدایت کنیم:
متد HandleValidation را در انتهای قسمت قبل تکمیل کردیم. اکنون OrderDetails را بر اساس اطلاعات فرم و انتخابهای کاربر، تکمیل کرده و به متد SaveRoomOrderDetailsAsync ارسال میکنیم تا Id سفارش را تولید کنیم. این همان Id ای است که قرار است به سمت سرور و Web API ارسال کنیم تا بر اساس آن تراکنش و Tracking Number ای را بتوان به رکورد جاری انتساب داد. بنابراین نیاز به کنترلر سمت Web API ای را داریم که بتواند اینکار را انجام دهد:
- متد Create، بر اساس اطلاعات وارد شدهی توسط کاربر، آنها را تبدیل به یک رکورد سفارش جدید میکند و به سمت کلاینت بازگشت میدهد.
- متد GetOrderDetail، بر اساس trackingNumber دریافتی از پرباد، کار بازگشت رکورد متناظری را انجام میدهد. از آن در پایان کار، جهت نمایش وضعیت پرداخت، استفاده میکنیم.
این دو متد در سرویس سمت سرور RoomOrderDetailsService، به صورت زیر تامین شدهاند:
اکنون که Web API Endpoint مدنظر را ایجاد کردیم، نیاز است سرویس سمت کلاینتی را نیز جهت تعامل با آن تهیه کنیم:
- متد GetOrderDetailAsync بر اساس trackingNumber دریافتی پس از عملیات تصدیق پرداخت، کار بازگشت جزئیات رکورد متناظری را انجام میدهد.
- متد SaveRoomOrderDetailsAsync، یک رکورد سفارش جدید را ایجاد میکند. در اینجا با روش مشاهدهی خطای کامل بازگشتی از سمت سرور (در صورت وجود) هم آشنا شدهایم که در مواقع لزوم میتواند راهگشا باشد.
- در متد SaveRoomOrderDetailsAsync فعلا مقدار UserId اجباری را به عبارتی دلخواه، تنظیم کردهایم. این مورد را در قسمتهای بعدی با معرفی اعتبارسنجی و احراز هویت سمت کلاینت، تکمیل خواهیم کرد.
این سرویس جدید را هم باید به سیستم تزریق وابستگیهای برنامهی کلاینت معرفی کرد تا قابل استفاده شود:
بنابراین در متد HandleCheckout ای که در حال بررسی آن هستیم، ابتدا متد SaveRoomOrderDetailsAsync فوق فراخوانی میشود تا توسط Web API Endpoint متناظری، یک رکورد سفارش ابتدایی را ایجاد کرده و Id آنرا در اختیار ما قرار دهد.
سپس به قطعه کد مهم زیر میرسیم:
اینجا است که برای نمونه آدرس 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 را ایجاد کرده و به صورت زیر تکمیل میکنیم:
این کامپوننت بر اساس مسیریابی که دارد:
سه پارامتر شماره سفارش، شماره تراکنش و پیامی را پس از پایان عملیات تصدیق پرداخت، از Web API، در طی یک redirect کامل دریافت میکند. در ادامه به کمک متد RoomOrderDetailService.GetOrderDetailAsync که آنرا پیشتر توسعه دادیم، اصل رکورد متناظر با این سفارش را بازیابی کرده و فیلدهای IsPaymentSuccessful و Status آنرا بررسی میکنیم (این فیلدها در زمان تصدیق پرداخت، در همان سمت سرور مقدار دهی میشوند). همچنین جهت محکمکاری، قسمتی از این اطلاعات را با Local Storage نیز انطباق دادهایم. اگر پرداخت، موفقیت آمیز باشد، شماره سفارش و همچنین شماره تراکنش را به کاربر نمایش میدهیم و یا پیام دریافتی از سرور را در صفحه درج میکنیم.
جلوگیری از ثبت سفارش اتاقی که رزرو شدهاست
پس از پایان عملیات سفارش یک اتاق، بهتر است امکان سفارش اتاقی را که دیگر در دسترس نیست، غیرفعال کنیم (تصویر فوق) که اینکار را میتوان توسط خاصیت IsBooked مدل UI کامپوننت نمایش لیست اتاقها انجام داد:
این خاصیت را در متدهای بازگشت لیست تمام اتاقها و یا بازگشت اطلاعات یک اتاق، به صورت زیر محاسبه و مقدار دهی میکنیم:
متد isRoomBooked، یک محاسبهی سمت سرور محسوب نمیشود؛ چون با استفاده از Includeهای نوشته شده، اطلاعات کامل اتاق و وابستگیهای آن (سرهای دیگر رابطهی تشکیل شده) را داریم و این محاسبات سبب رفت و برگشتی به سمت سرور نمیشوند.
اکنون که خاصیت IsBooked مقدار دهی شدهاست، در دو قسمت از آن استفاده خواهیم کرد:
الف) در کامپوننت نمایش لیست اتاقها
ب) در کامپوننت نمایش جزئیات یک اتاق
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-30.zip
نصب پرباد و انجام تنظیمات اولیهی آن
بستههای نیوگت پرباد را در دو پروژهی زیر نصب خواهیم کرد:
الف) پروژهی 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>
ب) پروژهای که محل قرارگیری فایلهای Migration است (و یا همان BlazorServer.DataAccess) در این مثال:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" /> </ItemGroup> </Project>
پس از نصب این بستهها، به کلاس آغازین پروژهی 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; } } }
ایجاد جداول متناظر با 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
روش یکپارچه سازی پرباد با یک برنامهی 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
بنابراین روش یکپارچه سازی پربابد با برنامههای 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)
- در اکشن متد 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(); } } }
- اکنون با فراخوانی متد ()result.GatewayTransporter.TransportToGateway، دو کار مهم رخ میدهند:
الف) ارسال خودکار اطلاعات به سمت درگاه بانکی
ب) Redirect خودکار به سمت درگاه بانگی
به همین جهت است که علاقمند نبودیم تا این مراحل را توسط HttpClient برنامهی Blazor WASM مدیریت و بازنویسی کنیم.
- پس از هدایت به سمت درگاه بانکی و تکمیل پرداخت، اکنون مجددا به همان verifyUrl هدایت میشویم. یعنی اکنون به مرحلهی پردازش اکشن متد VerifyRoomOrderPayment در سمت Web API رسیدهایم.
[HttpGet, HttpPost] public async Task<IActionResult> VerifyRoomOrderPayment()
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); } } }
- در تمام این مراحل، کار 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": "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); } } // ... }
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); } } }
- متد 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; } // ... } }
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); } } } }
- متد SaveRoomOrderDetailsAsync، یک رکورد سفارش جدید را ایجاد میکند. در اینجا با روش مشاهدهی خطای کامل بازگشتی از سمت سرور (در صورت وجود) هم آشنا شدهایم که در مواقع لزوم میتواند راهگشا باشد.
- در متد SaveRoomOrderDetailsAsync فعلا مقدار UserId اجباری را به عبارتی دلخواه، تنظیم کردهایم. این مورد را در قسمتهای بعدی با معرفی اعتبارسنجی و احراز هویت سمت کلاینت، تکمیل خواهیم کرد.
این سرویس جدید را هم باید به سیستم تزریق وابستگیهای برنامهی کلاینت معرفی کرد تا قابل استفاده شود:
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { // ... builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
سپس به قطعه کد مهم زیر میرسیم:
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);
نمایش وضعیت پرداخت، به کاربر در پایان گردش کاری آن
پس از این مراحل، مرحلهی آخر کار باقی ماندهاست؛ یعنی بازگشت از اکشن متد 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}"
جلوگیری از ثبت سفارش اتاقی که رزرو شدهاست
پس از پایان عملیات سفارش یک اتاق، بهتر است امکان سفارش اتاقی را که دیگر در دسترس نیست، غیرفعال کنیم (تصویر فوق) که اینکار را میتوان توسط خاصیت 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)) ); } } }
اکنون که خاصیت 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
مقدمه
اگر با Apiها کار کرده باشید احتمالاً با این چالش که گاهی نیاز است منابعی (Resources) که به کاربر ارسال میشوند مرتب (Sort)، بر اساس درخواست کاربر فیلتر (Filter) و در صفحهبندی (Paging) مشخصی تحویل داده شوند، برخورد کردهاید. این نیاز خصوصاً در پاسخ (Response) با روش GET از استاندارد HTTP مشهود است. در این مطلب به معرفی کتابخانهای میپردازیم که با استفاده از آن میتوان عملیات فوق را پیادهسازی نمود. Sieve یک چارچوب (Framework) ساده، تمیز و قابل توسعه برای NET Core. است. در زمان نگارش این مقاله ویرایش ۲.۱.۳ از این کتابخانه در دسترس است و همانگونه که اشاره شد، این کتابخانه منبع باز (Open Source) بوده و میتوانید آن را از مخزن گیتهاب در این پیوند دریافت نمایید.سیستمی با یک موجودیت به نام "پست" (Post) مفروض است. با استفاده از کتابخانه Sieve عملیات مرتبسازی، فیلتر و صفحهبندی را هنگام درخواست (GET) تمامی پستها اعمال خواهیم کرد.
// Post public int Id { get; set; } public string Title { get; set; } public int LikeCount { get; set; } public int CommentCount { get; set; } public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;
۱. نصب کتابخانه
ابتدا لازم است از طریق Package Manager Console و اجرای دستور فوق اقدام به نصب این کتابخانه نمایید: Install-Package Sieve -Version 2.1.3
۲. اضافه کردن سرویس
در فایل Startup.cs سرویس SieveProcessor را تزریق کنید:
services.AddScoped<SieveProcessor>();
۳. تعیین ویژگیهایی از کلاس برای اعمال مرتبسازی و فیلتر
باید ویژگیهایی (Properties) از کلاس را که میخواهید اعمال مرتبسازی و فیلتر بر روی آنها انجام شوند، مشخص کنید. به دو روش این امر ممکن است:
۱.۳. از طریق اضافه کردن صفت (Attribute) به ویژگیها
تنها ویژگیهایی از کلاس که دارای صفت [(Sieve(CanSort = true, CanFilter = true] باشند، میتوانند مرتب و یا فیلتر شوند (میتوان تنها از یکی از آنها نیز استفاده نمود).
لذا کلاس پست به صورت زیر ویرایش میشود:
// Post public int Id { get; set; } [Sieve(CanFilter = true, CanSort = true)] public string Title { get; set; } [Sieve(CanFilter = true, CanSort = true)] public int LikeCount { get; set; } [Sieve(CanFilter = true, CanSort = true)] public int CommentCount { get; set; } [Sieve(CanFilter = true, CanSort = true, Name = "created")] public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;
۲.۳. از طریق Fluent API
برای استفاده از این روش، ابتدا کلاسی را ایجاد و از کلاس SieveProcessor مشتق کنید. سپس تابع MapProperties موجود در کلاس والد را override کنید.// ApplicationSieveProcessor public class ApplicationSieveProcessor : SieveProcessor { public ApplicationSieveProcessor( IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods) { } protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) { mapper.Property<Post>(p => p.Title) .CanFilter() .HasName("a_different_query_name_here"); mapper.Property<Post>(p => p.CommentCount) .CanSort(); mapper.Property<Post>(p => p.DateCreated) .CanSort() .CanFilter() .HasName("created_on"); return mapper; } }
حال باید کلاس جدید را تزریق نمایید:
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
۴. دریافت پرسوجوهای (Queries) مرتب/فیلتر/صفحهبندی با اضافه کردن SieveModel به کنترلر (Controller)
برای دریافت پرسوجوهای مرتب/فیلتر/صفحهبندی، SieveModel را به اکشنی (Action) که پستها را برگشت میدهد به عنوان پارامتر اضافه کنید. سپس با فراخوانی تابع Apply با استفاده از تزریق SieveProcessor در کنترلر خود، پرسوجوها را به منابع خود اعمال کنید.
[HttpGet] public JsonResult GetPosts(SieveModel sieveModel) { var result = _dbContext.Posts; result = _sieveProcessor.Apply(sieveModel, result); return Json(result.ToList()); }
۵. ارسال درخواست
با تمام موارد گفته شده، اکنون میتوانید درخواستی را برای دریافت (GET) شامل پرسوجوهای مرتب/فیلتر/صفحهبندی ارسال نمایید. برای مثال:
GET /GetPosts ?sorts= LikeCount,CommentCount,-created // sort by likes, then comments, then descendingly by date created &filters= LikeCount>10,Title@=awesome title, // filter to posts with more than 10 likes, and a title that contains the phrase "awesome title" &page= 1 // get the first page... &pageSize= 10 // ...which contains 10 posts
filters= LikeCount>10,Title@=awesome title &: فیلتر بر اساس پستهایی که تعداد محبوبیت آنها بیش از ۱۰ است و در عنوان خود شامل عبارت "awesome title" میباشند.
page=1 &: صفحه اول ...
pageSize=10 &: ... که شامل ۱۰ پست است.
به صورت رسمیتر:
sorts: فهرست دستورالعملهایی شامل نام ویژگیهایی است که مرتبسازی بر روی آنها اعمال میشود و از طریق کاما (,) از یکدیگر تمایز داده میشوند. با اضافه کردن - قبل از نام ویژگی، آن را به صورت نزولی مرتب نمایید.
- filters: دستورالعملهای جدا شده توسط کاما (,) به صورت {Name}{Operator}{Value} که در آن:
- {Name} نام ویژگیای است که صفت Sieve بر روی آن تعریف شده و یا نام سفارشیای است که کاربر تعیین کرده است.
- همچنین میتوانید بیش از یک نام (برای یای منطقی (OR)) در درون جفت پرانتز باز و بسته و جداکننده یای منطقی (|) داشته باشید. برای مثال: LikeCount|CommentCount)>10) مشخص میکند مقدار LikeCount و یا CommentCount بیش از ۱۰ باشد.
- {Operator} یکی از عملگرهای ممکن است.
- {Value} مقداری است که در عمل فیلتر مورد استفاده قرار میگیرد.
- page: شماره صفحهای است که برگشت داده میشود.
- pageSize: تعداد مواردی است که در هر صفحه برگردانده خواهد شد.
۶. عملگرها (Operators)
عملگر | توضیحات | عملگر | توضیحات |
== | برابر | =@ | شامل |
=! | مخالف | =_ | شروع شود با |
< | بزرگتر | *=@ | شامل (حساس به حروف)* |
> | کوچکتر | *=_ | شروع شود با (حساس به حروف) |
=< | بزرگتر مساوی | *== | برابر (حساس به حروف) |
=> | کوچکتر مساوی | |
۷. پیکربندی
برای پیکربندی شامل مواردی چون حساس به بزرگ و کوچک بودن نام ویژگی، تعداد صفحات پیشفرض، حداکثر تعداد صفحه مجاز و نحوه برخورد با نام ویژگی ناموجود در ویژگیهای کلاس ابتدا قطعه زیر را به appsettings اضافه کنید.
{ "Sieve": { "CaseSensitive": "boolean: should property names be case-sensitive? Defaults to false", "DefaultPageSize": "int number: optional number to fallback to when no page argument is given. Set <=0 to disable paging if no pageSize is specified (default).", "MaxPageSize": "int number: maximum allowed page size. Set <=0 to make infinite (default)", "ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false" } }
services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));
اگر به ساز و کار شیرپوینت مایکروسافت دقت کنید، همه چیز را داخل دیتابیس ذخیره میکند (از اطلاعات رکوردها گرفته تا فایلها و غیره). حال شاید این سؤال مطرح شود که برای ذخیره سازی فایلهایی با تعداد بیش از یک میلیون عدد، استفاده از دیتابیس مناسب است یا فایل سیستم متداول. برای پاسخ به این سؤال باید به نکات ذیل توجه داشت:
- هر نوع عملیاتی که بر روی فایلها صورت گیرد، بستن، بازکردن و غیره، نیازمند اعمالی در سطح سیستم عامل است (برای مثال بررسی سطح دسترسی لازم برای انجام اینکارها).
- هر گونه عملیاتی بر روی فایلها نیازمند یک حداقل قفل گذاری بر روی آنها است که این نیز مصرف CPU قابل توجهی را سبب خواهد شد.
- تمامی اعمال ذکر شده کل سرور و تمامی سرویسهای در حال اجرا را تحت تاثیر قرار داده و بازدهی آنهارا کاهش میدهند.
- حتی سیستم عاملها نیز از یک file system database جهت مدیریت اعمال خود استفاده میکنند اما این روش برای مدیریت میلیونها و میلیاردها فایل بهینه سازی نشده است.
- ذخیره سازی میلیونها و میلیاردها فایل به تدریج سبب ایجاد fragmentation قابل توجهی شده و این مورد نیز بر روی کارآیی تاثیر منفی خواهد گذاشت (همچنین این مورد بر روی طول عمر تجهیزات ذخیره سازی دادهها تاثیر منفی دارند).
- تهیه پشتیبان و بازگرداندن میلیونها فایل بسیار زمانگیر است (برای مثال جابجایی یک فایل یک مگابایتی بسیار سریعتر است از جابجایی 100 فایل 10 کیلوبایتی).
- مدیریت تغییرات و همچنین بررسی اینکه چه شخصی چه فایلی را قرار داده، حذف کرده یا تغییر داده است در حالت استفاده از file system مشکل است.
- به صورت پیش فرض عموما مباحث replication و امثال آن توسط روش استفاده از file system خصوصا با تعداد بالای فایل، پشتیبانی نمیشود.
- در حالت استفاده از file system ، برنامههای وب باید دسترسی write بر روی یک سری پوشه داشته باشند که این مورد همیشه از دیدگاه امنیتی مساله ساز بوده و مشکل آفرین.
- کرش file system مساوی است با کرش سیستم عامل و بازگشت اینها زمانبر خواهد بود.
با توجه به این نکات استفاده از دیتابیس برای ذخیره سازی تعداد زیادی فایل، مزایای زیر را به همراه خواهد داشت:
- اکثر سیستمهای دیتابیسی امروزی برای کار با حجم عظیمی از دادهها به حد بلوغ خود رسیدهاند.
- هنگام استفاده از دیتابیس برای ذخیره سازی فایلها دیگر سر و کار ما با میلیونها فایل نخواهد بود و حداکثر چند فایل دیتابیس و ملحقات آن مانند لاگ فایل، کل سیستم را تشکیل میدهند.
- فایلهای دیتابیس برای مثال SQL Server ، همیشه توسط SQL Server در حالت باز قرار داشته و مباحث قفلگذاری بر روی فایلهای دیتابیس و بررسی سطح دسترسی و غیره توسط سیستم عامل در اینجا به حداقل خود میرسد.
- در این حالت بار سیستم عامل شما تنها سیستمی است که مشغول سرویس دهی اطلاعات دیتابیسهای شما است.
- جستجوی فایلها، حتی جستجو در محتوای این فایلهای ذخیره شده در یک دیتابیس بسیار سریعتر از روش file system میباشد. امکان استفاده از کوئریهای SQL انعطاف پذیری خاصی را به این سیستمها خواهند داد (برای مثال قابلیت full text search مربوط به SQL server امکان جستجو بر روی رکوردهایی با محتوای pdf را نیز پس از انجام اندکی تنظیمات، دارا میباشد).
- هنگام کار با دیتابیس مباحث تراکنشی نقش بسیار حائز اهمیتی را بازی میکنند اما عموما سیستم عاملها در این زمینه نیازمند کار و برنامه نویسی قابل توجهی هستند (این قابلیت به ویندوز ویستا اضافه شده است).
- کرش یک دیتابیس عموما سبب کرش سیستم عامل یا حتی کرش سایر دیتابیسهای موجود نخواهد شد.
- امکان تهیه پشتیبان از دیتابیسها و بازیابی آنها ساده است. (حداقل از بازیابی میلیونها فایل سادهتر است)
- امکانات replication به صورت پیش فرض در اکثر سیستمهای دیتابیسی امروزی مهیا است.
- امکان ثبت وقایع و مدیریت اطلاعات افزوده شده به دیتابیس، از طریق نرم افزارهایی که برای این کار نوشته خواهند شد (یا حتی امکانات توکار این برنامهها) از هر لحاظ نسبت به روش file system برتری دارد.
- امکانات سوئیچ کردن به دیتابیسی دیگر در شبکه در صورت کرش یک نود، مهیا است و پیش بینی شده است.
- برای استفاده از یک دیتابیس توسط یک برنامه وب، نیازی به داشتن دسترسی write بر روی هیچ فولدری وجود ندارد که این خود یک مزیت امنیتی مهم است و همچنین امکان محدود کردن سطوح دسترسی به فایلهای ذخیره شده در دیتابیس با برنامههای نوشته شده نیز سادهتر است. (البته در اینجا مسلما منظور از دیتابیس، دیتابیس Access نیست و SQL Server یا MySQL مد نظر هستند)
There are two MySQL providers for Entity Framework Core:
- The official one from MySQL: MySql.Data.EntityFrameworkCore. As of now, the latest version is 8.0.19, and works with Entity Framework Core 2.1 (and probably also 2.2). Since EF Core 3.0 is a major version with breaking changes, you cannot use it with this provider.
- The Pomelo provider: Pomelo.EntityFrameworkCore.MySql. There is a 3.1 version of this provider.
In other words, if you want to use EF Core 3.0/3.1 with MySQL, at this point you need to use the Pomelo provider (or wait for the official MySQL one to get released).
نظرات اشتراکها
کوئری تایپها در EF Core
استفاده از این قابلیت در جهت استفاده از ویوها، جوین و اجرای SPها و در کل هر خروجی از کوئریهای خام Sql سودمند است. در نسخه سوم این قابلیت به نام Keyless Entity تغییر نام یافته است.
در متد RunAt بازه زمانی را مشخص کنید. در این مثالها یک سری عدد ثابت است؛ شما آنها را از دیتابیس بخوانید. فقط دقت داشته باشید که خروجی true/false این متد ثانیهای یکبار بررسی میشود. بنابراین مباحث caching را جهت واکشی اطلاعات از بانک اطلاعاتی در متد RunAt اعمال کنید تا بیجهت رفت و برگشت اضافی صورت نگیرد.