- Async in Entity Framework 6.0
- Task-based Asynchronous Pattern support in EF
- Async Query & Save with Entity Framework 6
- Async Query & Save
- Entity Framework asynch – behind the magic
- Performing Asynchronous Operations Using Entity Framework
- EF 6: Async
- Async Processing in EF6 and the Microsoft .NET Framework 4.5
- Build Async Services with ASP.NET Web API and Entity Framework 6
- Making TransactionScope Work with async/await in .NET 4.5
- Async Actions in ASP.NET MVC 4
سری مقدماتی داتنت بر روی Azure
.NET on Azure for Beginners
8 videos
.NET on Azure for Beginners will teach you the basics of Azure and how to leverage its services and features to build amazing cloud applications. You will learn how to deploy web apps, work with storage and data, authenticate and use Managed Identity, harness the power of containers, and even deploy with GitHub Actions. By the end of this series, you will have the skills and confidence to start developing your own .NET applications on Azure!
In this post I’ll quickly explain the minimum you need to know in order to publish an Angular component to npm. By the end of the post you’ll know how your module to:
- Be platform independent (i.e. run in Web Workers, Universal).
- Should be bundled and distributed.
- Work with the Angular’s Ahead-of-Time compiler.
- Play well with TypeScript and allow autocompletion and compile-time type checking.
استفاده از محیط گرافیکی IIS جهت لیست کردن، اضافه و حذف ماژولها
به بخش modules درIIS بروید. در پنل سمت راست همه امکانات جهت افزودن و ویرایش و حذف وجود دارند:
روش معرفی ماژول در خط فرمان با استفاده از دستور Appcmd
Appcmd.exe install module /name:MODULE_NAME /image:PATH_TO_DLL
قسمت name که نام ماژول است و قسمت image هم مسیر قرار گرفتن فایل dll هست.
برای نمونه:
%windir%\system32\inetsrv\appcmd.exe install module /name:DefaultDocumentModule /image:%windir%\system32\inetsrv\defdoc.dll
در صورتیکه ماژولی که قبلا افزوده شده باشد را بخواهید اضافه کنید، خطای زیر را دریافت خواهید کرد:
ERROR ( message:Failed to add duplicate collection element "DefaultDocumentModule". )
جهت حذف ماژول دستور زیر را صادر کنید:
Appcmd.exe uninstall module MODULE_NAME
نمونه:
%windir%\system32\inetsrv\appcmd.exe uninstall module DefaultDocumentModule
گرفتن کوئری یا لیستی از ماژولهای فعال برای یک اپلیکیشن یا عمومی:
Appcmd.exe list modules [/app.name:APPLICATION_NAME]
سوپپچ aap.name اختیاری است ولی اگر نام یک اپلیکیشن را به آن بدهید، فقط ماژولهایی را که روی این اپلیکیشن اجرا میشوند، لیست میکند.
نمونه:
%windir%\system32\inetsrv\appcmd.exe list modules /app.name:"Default Web Site"
کد زیر هم نمونه ای برای لیست کردن تمامی ماژولهای عمومی که بر روی تمامی اپلیکیشنها اجرا میشوند:
%windir%\system32\inetsrv\appcmd.exe list modules
خط زیر یک ماژول را برای همه اپلیکیشنها یا اپلیکیشن خاصی فعال میکند که بستگی دارد سوییچ type چگونه مقداردهی شده باشد:
Appcmd.exe add module /name:MODULE_NAME /type:MGD_TYPE
برای مثال خط زیر باعث میشود ماژول Forms Authentication فقط برای وب اپلیکیشن default web site فعال شود:
%windir%\system32\inetsrv\appcmd.exe add module /name:FormsAuthentication /type:System.Web.Security.FormsAuthenticationModule /app.name:"Default Web Site"
یا در پایین آن را به صورت عمومی یا global فعال میکند:
%windir%\system32\inetsrv\appcmd.exe add module /name:FormsAuthentication /type:System.Web.Security.FormsAuthenticationModule
برای غیرفعال کردن یک ماژول از دستور زیر استفاده میشود:
Appcmd.exe delete module MODULE_NAME [/app.name:APPLICATION_NAME]
اگر غیر فعال کردن یک ماژول در یک اپلیکیشن خاص مدنظر شما باشد دستور زیر نمونه آن است:
%windir%\system32\inetsrv\appcmd.exe delete module FormsAuthentication /app.name:"Default Web Site"
اگر قصد دارید آنرا بر روی تمامی اپلیکیشنها غیرفعال کنید، دستور زیر نمونه آن است:
%windir%\system32\inetsrv\appcmd.exe delete module FormsAuthentication
حفظ کردن یا به خاطر سپردن دستورات بالا ممکن است کار سخت و دشواری باشد، به همین جهت از help کمک بگیرید:
Appcmd.exe module /?
یا به شکل اختصاصیتر برای یک دستور
Appcmd.exe install module /?Appcmd add module /?
نصب پرباد و انجام تنظیمات اولیهی آن
بستههای نیوگت پرباد را در دو پروژهی زیر نصب خواهیم کرد:
الف) پروژهی 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
درون سایت Rhino Moq معرفی شده و شما Moq رو معرفی کردید ، بنده با Moq و کدنویسی اون احساس راحتی بیشتری میکنم ، میخواستم بدونم توی عملکرد آیا با هم فرقی دارن ؟
بنده بیشتر درگیر ساخت یک تقلید از کانتکس هستم (دقیقا مشابه کاری که شما در مقاله جاری انجام داده اید)
میخواستم ببینم اگر Rhino امکانات خاصی در این زمینه ارائه نمیده با Moq کار کنم.
(دنبال یک فریم ورک تقید خوب هستم که همیشه با اون کار کنم و باهاش راحت باشم)
Please open an issue in the library repository to alert its author and ask them to package the library using the Angular Package Format (https://goo.gl/jB3GVv).
مراحل ایجاد یک پروژهی «کتابخانه» توسط Angular CLI 6.0
مرحلهی اول ایجاد یک پروژهی کتابخانه، مانند قبل، توسط دستور ng new و ایجاد یک پروژهی دلخواه جدید است:
ng new my-lib-test
پس از ایجاد پروژهی my-lib-test توسط دستور فوق و وارد شدن به پوشهی اصلی آن توسط خط فرمان، میتوان با اجرای دستور زیر، پروژههای دیگری را به پروژهی جاری افزود:
ng generate application my-app-name
ng generate library my-lib
همچنین یک پوشهی جدید به نام projects نیز ایجاد شده و پروژهی my-lib داخل آن قرار گرفتهاست.
فایل جدید public_api.ts
پس از ایجاد کتابخانهی جدید «my-lib»، فایل جدیدی به نام projects\my-lib\src\public_api.ts نیز به آن اضافه شدهاست:
با این محتوا:
/* * Public API Surface of my-lib */ export * from './lib/my-lib.service'; export * from './lib/my-lib.component'; export * from './lib/my-lib.module';
برای مثال اگر فایل جدید projects\my-lib\src\lib\my-lib.models.ts را به این کتابخانه اضافه کنیم که شامل تعدادی مدل و اینترفیس قابل دسترسی توسط استفاده کنندگان باشد، باید یک سطر زیر را به انتهای فایل public_api.ts اضافه کنیم:
export * from './lib/my-lib.models';
این پروژهی کتابخانه حتی به همراه فایلهای package.json, tsconfig.json, tslint.json مخصوص به خود نیز میباشد تا بتوان آنها را صرفا جهت این پروژه سفارشی سازی کرد.
ساختار my-lib.service پیشفرض یک پروژهی کتابخانه
اگر به فایل projects\my-lib\src\lib\my-lib.service.ts دقت کنیم:
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class MyLibService { constructor() { } }
شاید بپرسید چرا؟ هدف اصلی از آن، بهبود فرآیند tree-shaking یا حذف کدهای مرده و استفاده نشدهاست. ممکن است سرویسی را تعریف کنید، اما در برنامه استفاده نشود. این حالت خصوصا در پروژههای کتابخانههای ثالث ممکن است زیاد رخ دهد. به همین جهت با ارائهی این قابلیت، امکان حذف سادهتر سرویسهایی که در برنامه استفاده نشدهاند از خروجی نهایی کامپایل شده، وجود خواهد داشت.
چگونه به پروژهی کتابخانهی جدید، یک کامپوننت جدید را اضافه کنیم؟
تمام دستورات Angular CLI، در اینجا نیز کار میکنند. تنها تفاوت آنها، ذکر صریح نام پروژهی مورد استفاده است:
ng generate component show-data --project=my-lib
البته در اینجا باید فایل my-lib.module.ts را اندکی ویرایش کرد و ShowDataComponent را به قسمت exports نیز افزود:
@NgModule({ imports: [ CommonModule, HttpClientModule ], declarations: [MyLibComponent, ShowDataComponent], exports: [MyLibComponent, ShowDataComponent] }) export class MyLibModule { }
همچنین قسمت imports آن نیز به صورت پیشفرض خالی است. اگر نیاز است با ngIf کار کنید، باید CommonModule را در اینجا قید کنید و اگر نیاز است تبادلات HTTP وجود داشته باشد، ذکر HttpClientModule نیز ضروری است.
مرحلهی ساخت پروژه
پیش از استفادهی از این پروژهی کتابخانه، باید آنرا build کرد:
ng build my-lib
پس از اجرای این دستور، خروجی ذیل مشاهده میشود:
Building Angular Package Building entry point 'my-lib' Rendering Stylesheets Rendering Templates Compiling TypeScript sources through ngc Downleveling ESM2015 sources through tsc Bundling to FESM2015 Bundling to FESM5 Bundling to UMD Minifying UMD bundle Remap source maps Relocating source maps Copying declaration files Writing package metadata Removing scripts section in package.json as it's considered a potential security vulnerability. Built my-lib Built Angular Package! - from: D:\my-lib-test\projects\my-lib - to: D:\my-lib-test\dist\my-lib
استفادهی از کتابخانهی تولید شده
پس از پایان موفقیت آمیز مرحلهی Build، اکنون نوبت به استفادهی از این کتابخانه است. استفادهی از آن نیز همانند تمام کتابخانهها و وابستگیهای ثالثی است که تا پیش از این از آنها استفاده کردهایم. برای مثال ماژول آنرا در قسمت imports مربوط به NgModule کلاس AppModule معرفی میکنیم. برای این منظور به فایل src\app\app.module.ts مراجعه کرده و MyLibModule را به نحو ذیل اضافه میکنیم:
import { MyLibModule } from "my-lib"; @NgModule({ imports: [ BrowserModule, MyLibModule ] }) export class AppModule { }
اما سؤال اینجا است که آیا این پوشه پس از build، داخل پوشهی node_modules نیز کپی شدهاست؟ پاسخ آن خیر است و برای مدیریت خودکار آن، به صورت زیر عمل شدهاست:
اگر به فایل tsconfig.json اصلی و واقع در ریشهی workspace دقت کنید، پس از اجرای دستور «ng generate library my-lib»، قسمت paths آن نیز به صورت خودکار ویرایش شدهاست:
{ "compilerOptions": { "paths": { "my-lib": [ "dist/my-lib" ] } } }
برای نمونه اگر شارهگر ماوس را بر روی my-lib قرار دهید، به درستی مسیر خوانده شدن آن، تشخیص داده میشود.
به این ترتیب مسیر این import، چه در این پروژهی محلی و چه برای کسانیکه پوشهی dist/my-lib را به صورت یک بستهی npm جدید دریافت کردهاند، یکی خواهد بود.
در ادامه اگر به فایل app.component.html مراجعه کرده و selector کامپوننت show-data را به آن اضافه کنیم:
<lib-show-data></lib-show-data>
توزیع کتابخانهی ایجاد شده برای عموم
برای اینکه این کتابخانهی تولیدی را در اختیار عموم، در سایت npm قرار دهیم، ابتدا باید کتابخانه را در حالت production build تولید و سپس آنرا publish کرد:
ng build my-lib --prod cd dist/my-lib npm publish
البته دستور آخر نیاز به ایجاد یک اکانت در سایت npm و وارد شدن به آنرا دارد. جزئیات بیشتر آن در اینجا.
عیب یابی و دیباگ برنامههای SignalR
پردازشگر json، در نگارشهای اخیر IE به آن اضافه شده است. برای رفع این مشکل در نگارشهای قدیمی، نیاز است از اسکریپت کمکی http://nuget.org/List/Packages/json2 استفاده نمائید. همچنین مرورگر IE را نیز باید وادار ساخت تا بر اساس آخرین موتور پردازشی خود کار کند:
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
2) هنگام فراخوانی مسیر signalr/hubs پیغام 404 (یافت نشد) دریافت میشود.
برای رفع این مشکل ابتدا اطمینان حاصل کنید که تنظیمات مسیریابی تعریف شده در فایل global.asax.cs موجود هستند.
در ادامه اطمینان حاصل نمائید مسیر اسکریپتهای signalr/hubs به درستی تعریف شدهاند:
<script type="text/javascript" src="/signalr/hubs"></script>
MVC: <script type="text/javascript" src="@Url.Content("~/signalr/hubs")"></script> Web forms: <script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>
<configuration> <system.webServer> <modules runAllManagedModulesForAllRequests="true"> </modules> </system.webServer> </configuration>
3) متدهای سمت کلاینت من فراخوانی نمیشوند.
بهترین راه برای مشاهده ریز جرئیات خطاها، ذکر سطر ذیل در کدهای سمت کلاینت جاوا اسکریپتی برنامه است:
$.connection.hub.logging = true;
4) خطای «Connection must be started before data can be sent» را دریافت میکنم.
همانطور که در قسمت قبل عنوان شد، کلیه فراخوانیهای SignalR از نوع غیرهمزمان هستند. بنابراین باید با استفاده از callback و زمان فراخوانی آنها که عموما پس از برقراری اتصال رخ میدهد، نسبت به انجام امور دلخواه اقدام کرد.
var connection = $.hubConnection('http://localhost:8081/'); proxy = connection.createProxy('collectionhub') connection.start() .done(function () { proxy.invoke('subscribe', 'Product'); $('#messages').append('<li>invoked subscribe</li>'); }) .fail(function () { alert("Could not Connect!"); });
در حالت استفاده بدون پروکسی نیز چنین callbackهایی قابل تعریف هستند:
$.connection.hub.start() .done(function() { myHub.server.SomeFunction(SomeParam) //e.g. a login or init .done(connectionReady); }) .fail(function() { alert("Could not Connect!"); });
5) بعد از 10 اتصال به IIS، برنامه متوقف میشود.
این مورد، محدودیت ذاتی IIS 7 نصب شده بر روی ویندوز 7 است. بهتر است از یک IIS کامل موجود در ویندوزهای سرور استفاده کنید. در این سرورها عدد پیش فرض تنظیم شده 5000 اتصال است که در صورت نیاز با استفاده از دستور زیر قابل تغییر است:
appcmd.exe set config /section:system.webserver/serverRuntime /appConcurrentRequestLimit:100000
%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet.config
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <runtime> <legacyUnhandledExceptionPolicy enabled="false" /> <legacyImpersonationPolicy enabled="true"/> <alwaysFlowImpersonationPolicy enabled="false"/> <SymbolReadingPolicy enabled="1" /> <shadowCopyVerifyByTimestamp enabled="true"/> </runtime> <startup useLegacyV2RuntimeActivationPolicy="true" /> <system.web> <applicationPool maxConcurrentRequestsPerCPU="20000" /> </system.web> </configuration>
%windir%\Microsoft.NET\Framework\v4.0.30319\Config\machine.config
<processModel autoConfig="false" requestQueueLimit="250000" />
فرآیند رندر شدن در Angular2
یک سری ویدیوی رایگان ایجاد و همچنین برنامه نویسی workflow های SharePoint 2007 در سایت آقای shelton موجود است که متاسفانه این سایت با اینترنت داتک باز نمیشود (یا ازون طرف یا از این طرف فیلتر شده! یا مشکل DNS اینترنت داتک است، نمیدونم).
برای راحتی دریافت آنها یک mirror از این ویدیوها ایجاد شد:
Extracting Document Details with SharePoint Workflow's, download
Extracting email addresses & Sending emails with SharePoint Workflow, download
Programmatically creating user tasks with SharePoint Workflow, download
Programmatically escalating Overdue Tasks , download
Building Basic Approval Workflow, download
Building a Multilevel Approval Workflow, download
Using Active Directory Searching/Lookup in a SharePoint/MOSS 2007 Workflow, download