مطالب
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
مطالب
مقایسه پروژه های Web Site و Web Application در Visual Studio
 در ویژوال استودیو می‌توانید پروژه‌ی Web Application و یا Web Site ایجاد نمایید. شما Web Application را با گزینه‌ی New Project ایجاد و با Open Project باز می‌کنید؛ ولی Web Site را با گزینه‌ی New Web Site ایجاد و با Open Web Site باز می‌نمایید. قبل از ساخت یک پروژه‌ی جدید وب لازم است از تفاوت‌های این دو نوع، آگاهی کسب کرده و در انتخاب نوع پروژه دقت نمایید؛ چرا که تغییر و تبدیل یک نوع به نوع دیگر، علاوه بر سختی، موجب اتلاف زمان شده و پروژه را مستعد خطا خواهد کرد.

نکته:
برای ایجاد یک پروژه‌ی جدید مایکروسافت Web Application را به شما پیشنهاد می‌دهد. هرچند در این مبحث، مطالبی را مبنی بر فواید Web Site معرفی خواهد کرد، ولی اکثر توسعه دهندگان وب که Web Site را برگزیده اند سرانجام مضراتی از آن را می‌یابند که سنگینی آن بیشتر از فوایدش است. برای مثال تمامی خصیصه‌های (feature فیچر) ASP.net لزومآ برای وب سایت در دسترس نخواهند بود؛ مثلآ از ویژوال استودیوی 2012 به بعد، ابزاری برای تولید پروژه‌های وب وجود دارد که فقط برای Web Application در اختیار خواهد بود (برای کسب اطلاعات بیشتر می‌توانید مطلب Creating an ASP.net Web Project in Visual Studio  را مطالعه نمایید). 

سناریو :

سناریویی که مبنی برا انتخاب Web Application می‌باشد به شرح زیر است:
· شما نیاز به استفاده از Edit And Continue در دیباگر ویژوال استودیو دارید.
· تمامی کدها، فایل‌ها و کلاس‌هایی که با صفحات ASP.net مرتبط هستند، برای تست بصورت واحد و یکپارچه در نظر گرفته می‌شوند.
· شما برای کلاس‌هایی که وابسته به صفحات هستند و همچنین برای کنترل‌ها و کلاس‌های منحصر آن باید ارجاع داشته باشید.
· وابستگی در حالتی که چندین پروژه‌ی مرتبط به هم را دارید توسط شما مشخص می‌شود.
· برای کل سایت در هنگام کامپایل فقط یک اسمبلی ساخته می‌شود.
· کنترل نام اسمبلی‌ها و همچنین شمارهی ورژن ایجاد شده‌ی برای پروژه در دست شماست.
· برای کامپایل پروژه می‌توانید MSBuild و یا Team Build را انتخاب کنید؛ برای مثال می‌توانید مراحل Prebuild یا Postbuild را مشخص کنید.
· نیازی به قرار دادن سورس برنامه روی سرور نیست.

سناریویی که مبنی برا انتخاب Web Site می‌باشد به شرح زیر است:
· یک پروژه در بر دارنده‌ی  کدهای #C و هم کدهای Visual Basic می‌باشد ( درحالیکه بصورت پیشفرض در Web Application فایل پروژه بر مبنای زبان برنامه‌ی شما کامپایل می‌شود، هرچند می‌توان این حالت پیشفرض را تغییر داد؛ ولی این امر می‌تواند اندکی مشکل باشد).
· شما می‌توانید سایت ایجاد شده را بصورت Real Time توسط FTP  باز نموده و آپدیت نمایید.
· برای توزیع (deploy) پروژه مجبور به کامپایل صریح آن نیستید.
· اگر پروژه را کامپایل نمایید کامپایلر به ازای هر صفحه و یا هر پوشه، یک فایل اسمبلی جداگانه خواهد ساخت.
· برای تغییر یک فایل به تنهایی می‌توانید فقط آنرا تغییر داده و بر روی سرور قرار دهید.
· حتی بعد از کامپایل هم می‌توانید صفحات ASP.net  را بدون نیاز به کامپایل دوباره‌ی کل سایت تغییر داده و جایگزین نمایید.
· سورس کامل پروژه برای اجرا باید روی سرور قرار گیرد.

تفاوت‌ها در یک نگاه:

زمینه 
  پروژه‌های Web Application     پروژه‌های Web Site 
  ساختار فایل پروژه    فایل برنامه (.csproj / vbproj) دربردارنده اطلاعاتی از جمله لیست فایل‌ها و رفرنس‌ها پروژه به پروژه دیگر خواهد بود.    هیچ فایل برنامه ای وجود ندارد و تمامی فایل هایی که داخل پوشه می‌باشند جزو فایل‌های سایت شناخته می‌شوند. 
  کامپایل  · شما پروژه را در سیستم خود کامپایل می‌کنید.
· بصورت پیشفرض کامپایل کد‌ها در یک اسمبلی قرار می‌گیرد. 
  · سورس کدها بصورت اتوماتیک در سرور توسط Asp.net  با اولین درخواست کامپایل میشوند.
(البته شما می‌توانید کامپایل را در سیستم خود نیز انجام دهید)
· بصورت پیشفرض کامپایل برای هر کلاس یک اسمبلی جدا می‌سازد. 
فضا‌های نام  Namespace‌ها بصورت صریح در صفحات و کلاس‌ها و کنترل‌ها افزوده می‌شود.  هیچ namespace ای بصورت پیشفرض اضافه نمی‌شود (شما می‌توانید بصورت دستی آنها را اضافه کنید) 
توزیع
اسمبلی تولید شده در مرحله کامپایل را روی سرور قرار می‌دهید
اکثر مراحل کامپایل توسط ابزارهای ارائه شده ویژوال استودیو انجام می‌شود. 
کل سورس پروژه روی سرور قرار میگیرد.
اکثر مراحل کامپایل توسط ابزارهای ارائه شده ویژوال استودیو انجام می‌شود. 

ساختار فایل پروژه:
پروژه‌های Web Application از فایل پروژه ویژوال استودیو ( .csproj / .vbproj ) برای نگهداری اطلاعات پروژه استفاده می‌کنند. با این امکان می‌توان فایل‌هایی را که در پروژه دخیل هستند و یا باید کامپایل شوند، به تفکیک مشخص کرد.
در مورد Web Site تمامی فایل‌هایی که در داخل پوشه‌ی برنامه قرار دارند، به صورت پیش فرض جزیی از برنامه تلقی شده و کامپایل خواهند شد و برای اینکه فایلی را بخواهید مستثنا کنید یا باید آنرا حذف کنید و یا پسوند آنرا به نامی که توسط سرور IIS قابل شناسایی نیست تغییر دهید.

فایده‌ی فایل پروژه یعنی همان ( .csproj / .vbproj ) در Web Application :
می‌توان فایلی را به طور موقت از برنامه حذف کرد، بدون نگرانی از آنکه فایل بصورت کلی حذف شود. چرا که فایل در ساختار برنامه باقیست. برای مثال اگر صفحه‌ای برای توزیع آماده نیست، می‌توانید به راحتی آن‌را از برنامه خارج کنید ( Exclude ) و برنامه را کامپایل نمایید و بعد از اینکه این صفحه هم آماده شد، دوباره آن را وارد پروژه نمایید ( include ) که اهمیت این امر در مواردی که از برنامه‌های کنترل سورس استفاده می‌کنید، دوچندان می‌شود.

فایده‌ی عدم استفاده از فایل پروژه‌ی برنامه در Web Site :
شما مجبور به کنترل و شخصی سازی ساختار فایل برنامه در ویژوال استودیو نیستید و به راحتی هر فایل یا صفحه‌ای را که می‌خواهید، با کپی کردن به پوشه و یا حذف کردن از آن توسط فایل اکسپلورر انجام می‌دهید.

کامپایل:
برای برنامه‌های Web Application شما بصورت معمول پروژه را Build می‌نمایید و تمامی کد‌های صفحات و همچنین کلاس‌ها به صورت یک فایل اسمبلی در پوشه‌ی bin ذخیره می‌گردد.
برای Web Site شما مجبور به کامپایل دستی پروژه نیستید و می‌توانید از Batch-Compile استفاده کنید و همچنین به ازای هر صفحه و کلاسریال شما یک فایل اسمبلی خواهید داشت.

مزایای کامپایل در Web Application :
· می‌توانید از MSBuild استفاده کنید.
· می‌توانید خصیصه‌های اسمبلی، از جمله نام و ورژن را به راحتی مدیریت نمایید.
· کامپایل قبل از توزیع برنامه این مزیت را دارد که کاربران مجبور نیستند منتظر کامپایل برنامه در سرور باشند.
· مدیریت دقیقی بر روی فایل‌ها و ساختار برنامه و همچنین کلاس‌ها و ارجاعات خواهید داشت.

مزایای کامپایل در Web Site  :
· می‌توانید هر صفحه‌ای را که نیاز دارید بدون در نظر گرفتن آماده شدن دیگر صفحات تست و اجرا نمایید.
· آپدیت و جایگزینی فایل‌ها به راحتی صورت می‌گیرد؛ چرا که اسمبلی تمام فایل‌ها بصورت منحصر همان صفحه ایجاد خواهد شد.
· ایجاد شدن چند اسمبلی می‌تواند در برخی پروژه‌ها به نفع برنامه بوده و performance  را بالا ببرد. برای مثال در حالتیکه یک سایت با صفحات زیاد دارید و برخی صفحات به نسبت دیگر صفحات خیلی کمتر درخواست می‌شوند.

نکته:
هیچ فرقی بین Web Application ,  و web Site از نظر performance  وجود ندارد مگر درحالت ذکر شده در بالا و در سایت‌های خیلی بزرگ.

توزیع : ( Deployment )
در web Site کل فایل‌های پروژه را بر روی سرور قرار می‌دهید؛ درحالی که در Web Application  فایل‌های برنامه بصورت اسمبلی‌ها ( .dll ) روی سرور قرار می‌گیرند. همین امر می‌تواند در برخی حالت‌ها مثلآ زمانی که از هاست share شده استفاده می‌کنید، خیال شما را از بابت سورس برنامه مطمئن سازد.
برای Web Site نیز این مزیت وجود دارد که برای انجام تغییرات کوچک مجبور به کامپایل و آپلود دوباره‌ی کل پروژه نیستید.


ماخذ

 
اشتراک‌ها
از کند شدن Entity Framework دوری کنید
Generally speaking, I don't worry much about tweaking my LINQ queries when working with Entity Framework (this is also true when I'm working with SQL directly, by the way). I'm always telling my clients that if they want to speed up their data access they should look at their database design and, especially, how they're using indexes.
از کند شدن Entity Framework دوری کنید
مطالب
استفاده از #F در پروژه های WPF
در دوره #F این سایت (^) با نحوه کد نویسی و مفاهیم و مزایای این زبان آشنا شده اید. اما دانستن syntax یک زبان برای پیاده سازی یک پروژه کافی نیست و باید با تکنیک‌های مهم دیگر از این زبان آشنا شویم. همان طور که قبلا (فصل اول دوره #F) بیان شد Visual Studio به صورت Visual از پروژه‌های #F پشتیبانی نمی‌کند. یعنی امکان ایجاد یک پروژه WPF یا Windows Application یا حتی پروژه‌های تحت وب برای این زبان همانند زبان #C به صورت Visual در VS.Net تعبیه نشده است. حال چه باید کرد؟ آیا باید در این مواقع این گونه پروژه‌ها را با یک زبان دیگر نظیر #C ایجاد کنیم و از زبان #F در حل برخی مسائل محاسباتی و الگوریتمی استفاده کنیم. این اولین راه حلی است که به نظر می‌رسد. اما در حال حاضر افزونه هایی، توسط سایر تیم‌های برنامه نویسی تهیه شده اند که پیاده سازی و اجرای یک پروژه تحت ویندوز یا وب را به صورت کامل با زبان #F امکان پذیر می‌کنند. در  این پست به بررسی یک مثال از پروژه WPF به کمک این افزونه‌ها می‌پردازیم.

نکته : آشنایی با کد نویسی و مفاهیم #F برای درک بهتر مطالب توصیه می‌شود.

معرفی پروژه FSharpX

پروژه FSharpx یک پروژه متن باز است که توسط یک تیم بسیار قوی از برنامه نویسان #F در حال توسعه می‌باشد. این پروژه شامل چندین زیر پروژه و بخش است که هر بخش آن برای یکی از مباحث دات نت در #F تهیه و توسعه داده می‌شود.
این قسمت‌ها عبارتند از :
FSharpx.Core : شامل مجموعه ای کامل از توابع عمومی، پرکاربرد و ساختاری است که برای این زبان توسعه داده شده اند و با تمام زبان‌های دات نت سازگاری دارند؛
FSharpx.Http : استفاده از #F در برنامه نویسی مدل Http؛
FSharpx.TypeProvider : این پروژه خود شامل چندین بخش است که در این جا چند مورد از آن‌ها را عنوان می‌کنم:
  • FSharpx.TypeProviders.AppSetting : متد خواندن و نوشتن (setter  و getter) را برای فایل‌های تنظیمان پروژه (Application Setting File) فراهم می‌کند.
  • FSharpx.TypeProviders.Vector : برای محاسبات با ساختار‌های برداری استفاده می‌شود.
  • FSharpx.TypeProviders.Machine : برای دسترسی و اعمال تغییرات در رجیستری و فایل‌های سیستمی استفاده می‌شود.
  • FSharpx.TypeProviders.Xaml : با استفاده از این افزونه می‌توانیم از فایل‌های Xaml، در پروژه‌های #F استفاده کنیم و WPF Designer نرم افزار VS.Net هم برای این زبان قابل استفاده خواهد شد.
  • FSharpx.TypeProviders.Regex : امکان استفاده از عبارات با قاعده را در این پروژه فراهم می‌کند.

یک مثال از عبارات با قاعده:

type PhoneRegex = Regex< @"(?<AreaCode>^\d{3})-(?<PhoneNumber>\d{3}-\d{4}$)">

PhoneRegex.IsMatch "425-123-2345"
|> should equal true

PhoneRegex().Match("425-123-2345").CompleteMatch.Value
|> should equal "425-123-2345"

PhoneRegex().Match("425-123-2345").PhoneNumber.Value
|> should equal "123-2345"
َشروع پروژه
ایتدا یک پروژه از نوع F# Console Application ایجاد کنید. از قسمت Project Properties (بر روی پروژه کلیک راست کنید و گزینه Properties را انتخاب کنید) نوع پروژه را به Windows Application تغییر دهید(قسمت Out Put Type). اسمبلی‌های زیر را به پروژه ارجاع دهید:
  • PresentationCore
  • PresentationFramework
  • WindowBase
  • System.Xaml

با استفاده از پنجره Package Manager Console دستور نصب زیر را اجرا کنید(آخرین نسخه این پکیج 1.8.31  و حجم آن کمتر از یک مگابایت است):

PM> Install-Package FSharpx.TypeProviders.Xaml

حال یک فایل Xaml به پروژه اضافه کنید و کد‌های زیر را در آن کپی کنید:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF F# Sample By Masoud Pakdel" Height="350" Width="525">
    <Grid Name="MainGrid">
        <StackPanel Name="StackPanel1" Margin="50">
            <Button Name="Button1">Who are you?</Button>        
        </StackPanel>
    </Grid>
</Window>
کد‌های بالا کاملا واضح است و نیاز به توضیح دیده نمی‌شود. اما اگر دقت کنید می‌بینید که این فایل، فایل Code Behind ندارد. برای این کار باید یک فایل جدید از نوع F# Source File ایجاد کنید. بهتر است که فایل جدید شما همنام با همین فایل باشد. پسوند این فایل fs است. حال کد‌های زیر را در آن کپی کنید:
open System
open System.Windows
open System.Windows.Controls
open FSharpx
 
type MainWindow = XAML<"MainWindow.xaml">
 
let loadWindow() =
    let window = MainWindow()
    window.Button1.Click.Add(fun _ ->
        MessageBox.Show("Masoud Pakdel")
        |> ignore)
    window.Root
 
[<STAThread>]
(new Application()).Run(loadWindow())
|> ignore
نوع XAML استفاده شده  که به صورت generic است در فضای نام FSharpx تعبیه شده است و این اجازه را می‌دهد که یک فایل #F بتواند برای مدیریت یک فایل Xaml  استفاده شود.برای مثال می‌توانید به اشیا و خواص موجود در فایل Xaml دسترسی داشته باشید. در اینجا دیگر خبری از متد InitializeComponent موجود در سازنده کلاس CodeBehind پروژه‌های #C نیست. این تعاریف و آماده سازی کامپوننت‌ها به صورت توکار در نوع XAML موجود در FSharpx انجام می‌شود.


در تابع loadWindow یک نمونه از کلاس MainWindow ساخته می‌شود و برای button1 آن رویداد کلیک تعریف می‌کنیم. دستورات زیر معادل دستورات شروع برنامه در فایل program پروژه‌های #C است.
[<STAThread>]
(new Application()).Run(loadWindow())
|> ignore

پروژه را اجرا کنید و بر روی تنهای Button موجود در صفحه، کلیک کنید و پیغام مورد نظر را مشاهده خواهید کرد. به صورت زیر:

اشتراک‌ها
Uno Platform و انتقال Windows Calculator به Linux

We’ve ported the open-source Windows Calculator to yet another platform: Linux! With all the rumors of a Microsoft browser – Edge – coming to Linux soon, we thought it would be good to show that the open source community can be effective in bringing a piece of Windows to Linux. 

Uno Platform و انتقال Windows Calculator به Linux
اشتراک‌ها
پروژه های Open Source در زمینه e-commerce با ASP.NET Core

 Top 3 Open Source ASP.NET Core based e-commerce web applications


nopCommerce
 SimplCommerce
grandnode ( Powered By nopCommerce) :It’s an open source, free, cross-platform e-commerce software based on ASP.NET Core 2.2 and MongoDB — NoSQL database. You can run it on Linux, Windows, and MacOS easily. GrandNode also supports Docker, so you are able to install it just in seconds with Docker. 

پروژه های Open Source در زمینه e-commerce با ASP.NET Core
مطالب
استفاده از #F در پروژه های MVC4
در این پست با روش پیاده سازی پروژه‌های WPF با استفاده از #F آشنا شدید. قصد دارم در طی چند پست روش پیاده سازی پروژه‌های Asp.Net MVC 4  با استفاده از #F را شرح دهم.
»اگر با #F آشنایی ندارید می‌توانید از اینجا شروع کنید.
به صورت کلی برای استفاده گسترده از #F در پروژه‌های وب نیاز به یک سری template‌های آماده داریم در غیر این صورت کار کمی سخت خواهد شد. به تصویر زیر دقت نمایید:

واضح است که با توجه به تصویر بالا کنترلر‌ها و البته مدل‌های app  و هر آنچه که سمت سرور به آن نیاز است باید با استفاده از #F پیاده سازی شوند. اما هنگامی که کنترلر‌ها با استفاده از #F نوشته شوند سیستم مسیر یابی نیز تحت تاثیر قرار خواهد گرفت. علاوه بر آن باید فکری برای بخش Bundling و همچنین فیلتر‌هاو... نمود. در نتیجه با توجه به template پروژه مورد نظر بر خلاف حالت پیش فرض #C آن که در قالب یک پروژه ارائه می‌شود در این جا حداقل به دو پروژه نیاز داریم. خوشبختانه همانند پروژه FSharpX که برای WPF مناسب است برای MVC نیز template آماده موجود است که در ادامه با آن آشنا خواهیم شد.

شروع به کار

ابتدا در VS.Net یک پروژه جدید ایجاد نمایید. از بخش Online Template گزینه FSharp MVC 4 را جستجو کنید. 

بعد از انتخاب نام پروژه و  کلیک بر روی Ok ( و البته دانلود حدود ده MB اطلاعات) صفحه زیر نمایان می‌شود. در این قسمت تنظیمات مربوط به انتخاب View Engine و نوع قالب پروژه را وجود دارد. در صورتی که قصد استفاده از Web Api را دارید گزینه  Web Api Project را انتخاب کنید در غیر این صورت گزینه Empty Project.

البته از Visual Studio 2012 به بعد این بخش به صورت زیر خواهد بود که قسمت Single Page App به آن اضافه شده است:

بعد از کلیک بر روی Ok یک پروژه بر اساس Template مورد نظر ساخته می‌شود. همانند تصویر زیر:

بررسی تغییرات

در یک نگاه به راحتی می‌توان تغییرات زیر را در پروژه Web تشخیص داد:

»پوشه Controller وجود ندارد؛

»پوشه مدل وجود ندارد؛

»فایل Global.asax دیگر فایلی به نام Global.asax.cs را همراه با خود ندارد.

دلیل اصلی عدم وجود موارد بالا این است که تمام این موارد باید به صورت #F پیاده سازی شوند در نتیجه به پروژه #F ساخته شده منتقل شده اند. فایل Global.asax را باز نمایید. سورس زیر قابل مشاهده است:

<%@ Application Inherits="FsWeb.Global" Language="C#" %> <script Language="C#" RunAt="server">
 // Defines the Application_Start method and calls Start in // System.Web.HttpApplication from which Global inherits. protected void Application_Start(Object sender, EventArgs e) { base.Start(); } </script>
تمامی کاری که تکه کد بالا انجام می‌دهد فراخواهی متد Start در فایل Global متناظر در پروژه #F است. اگر به قسمت reference‌های پروژه Web دقت کنید خواهید که به پروژه #F متناظر رفرنس دارد.
حال به بررسی پروژه #F  ساخته شده خواهیم پرداخت. در این پروژه یک فایل  Global.fs وجود دارد که سورس آن به صورت زیر است:

namespace FsWeb

open System
open System.Web
open System.Web.Mvc
open System.Web.Routing

type Route = { controller : string
               action : string
               id : UrlParameter }

type Global() =
    inherit System.Web.HttpApplication() 

    static member RegisterRoutes(routes:RouteCollection) =
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
        routes.MapRoute("Default", 
                        "{controller}/{action}/{id}", 
                        { controller = "Home"; action = "Index"
                          id = UrlParameter.Optional } )

    member this.Start() =
        AreaRegistration.RegisterAllAreas()
        Global.RegisterRoutes(RouteTable.Routes)
  در پروژه‌های MVC بر اساس زبان #C،  کلاسی به نام RouteConfig وجود دارد که در فایل Global.asax.cs فراخوانی می‌شود:
RouteConfig.RegisterRoutes(RouteTable.Routes);
و کد‌های مورد نظر جهت تعیین الگوی مسیر یابی کنترلر‌ها و درخواست‌ها در کلاس RouteConfig نیز به صورت زیر می‌باشد:
public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
اما در اینجا بر خلاف #C، تعریف و اجرای سیستم مسیر یابی در یک فایل انجام می‌شود. در بخش اول ابتدا یک type تعریف می‌شود که معادل با تعیین Routing در زبان #C است. علاوه بر آن متد RegisterRoutes جهت تعیین و انتساب Route Type تعریف شده به Route Collection مورد نظر نیز در این فایل می‌باشد. 

//تعریف الکوی مسیر یابی
type Route = { controller : string
               action : string
               id : UrlParameter }

type Global() =
    inherit System.Web.HttpApplication() 

    static member RegisterRoutes(routes:RouteCollection) =
       //فراخوانی  و انتساب الگوی مسیر یابی به  مسیر‌های تعریف شده
       routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
        routes.MapRoute("Default", 
                        "{controller}/{action}/{id}", 
                        { controller = "Home"; action = "Index"
                          id = UrlParameter.Optional } )
در نهایت نیز تابع RegisterRoute در تابع Start فراخوانی خواهد شد.
Global.RegisterRoutes(RouteTable.Routes)
نتیجه کلی تا اینجا:
در این پست با Template پروژه‌های F# MVC 4 اشنا شدیم و از طرفی مشخص شد که برای پیاده سازی این گونه پروژه‌ها حداقل نیاز به دو پروژه داریم. یک پروژه که از نوع #C است ولی در آن فقط View ‌ها و فایل جاوااسکریپتی و البته Css وجود دارد. از طرف دیگر کنترلر‌ها و مدل‌ها و هر چیز دیگر که مربوط به سمت سرور است در قالب یک پروژه #F پیاده سازی می‌شود.
در پست بعدی با روش تعریف و توسعه کنترلر‌ها و مدل‌ها آشنا خواهیم شد.
نظرات مطالب
یافتن لیست اسمبلی‌های ارجاعی
سلام
نیازی نبود سورسی را Map کنید یا اصلا نباید این‌کار را می‌کردید. (اگر منظور مپ کردن سورس بوده نه فایل اجرایی برنامه)
SVN وظیفه مدیریت و به اشتراک گذاشتن پروژه رو داره، نه شبکه ویندوزی یا لینوکسی. (حتما از visual svn server استفاده کنید تا این موارد را برای شما ساده کند)
کلاینت‌ها هر کدام نسخه‌ی کامل و لوکال خودشون رو باید داشته باشند (از طریق check out مخزن کد این پروژه لوکال باید تشکیل شود نه کپی دستی). سپس مثل اینکه دارند لوکال کار می‌کنند (نه از روی شبکه در حالت مپ شده). کاملا حالت معمولی و قطع از شبکه. SVN برای مدیریت پروژه روی اینترنت هم بکار میره. نه چیزی map میشه و نه لازم هست کاربر همیشه به شبکه وصل باشه.
نسخه کد شما که روی سرور هست و توسط SVN نگهداری می‌شود، مخزن اصلی است که تغییرات با آن هماهنگ می‌شود. برای انتقال کد به مخزن، باید عملیات check in صورت گیرد.
بعد هر کدام از اعضای تیم زمانیکه check out می‌کنند ، یک نسخه‌ی محلی دریافت می‌کنند و این فولدر تحت کنترل SVN قرار میگیره، حالا مباحث update و commit و غیره کار می‌کند. فقط هر بار که می‌خواهند commit کنند باید اول update کنند ببینند کسی چیزی را تغییر داده، تصادمی هست یا نه؟ بعد commit‌ کنند به سرور. (یعنی ارتباط با شبکه فقط در همین چند لحظه کوتاه است و بسیار سریع هم خواهد بود)
فصل دوم کتابچه‌ای را که من تهیه کردم لطفا مطالعه کنید، گردش کاری آن توضیح داده شده است.
https://www.dntips.ir/2008/10/subversion.html
در فصل یک هم توضیح دادم که چه پورتی را باید روی سرور باز کنید تا SVN توسط فایروال بلاک نشود.

حتما توصیه می‌کنم اگر با VS.Net کار می‌کنید افزونه Visual SVN را نصب کنید تا راحت و بدون دردسر کار کنید .


خطایی رو که توضیح دادید مربوط هست به اشتراک گذاشتن فایل اجرایی یک برنامه دات نتی روی شبکه که این خطا رو می‌گیرید:
Project Location Is not Trusted
این خطا رو با دادن full trust به برنامه می‌تونید حل کنید که اینجا قدم به قدم توضیح داده شده:
http://msdn.microsoft.com/en-us/library/bs2bkwxc(VS.80).aspx
همچنین کامنت‌های اون رو لطفا بخونید. مثلا ممکن هست فایل بلاک شده باشه که با کلیک راست و unblock کردن مشکل حل میشه.

خطای زیر بیشتر مربوط به حالتی است که الف) هنوز مخزن کد ایجاد نشده و ب) عملیات initial import یا check in صورت نگرفته
Unable to open an ra_local url. unable to open repository.
حتما کتابچه فوق را مطالعه نمایید.