مطالب
Blazor 5x - قسمت 29 - برنامه‌ی Blazor WASM - یک تمرین: رزرو کردن یک اتاق انتخابی


در قسمت قبل که لیست اتاق‌های دریافتی از Web API را نمایش دادیم، هرکدام از آن‌ها، به همراه یک دکمه‌ی Book هم هستند (تصویر فوق) که هدف از آن، فراهم آوردن امکان رزرو کردن آن اتاق، توسط کاربران سایت است. این قسمت را می‌توان به عنوان تمرینی جهت یادآوری مراحل مختلف تهیه‌ی یک Web API و قسمت‌های سمت کلاینت آن، تکمیل کرد.



تهیه موجودیت و مدل متناظر با صفحه‌ی ثبت رزرو یک اتاق

تا اینجا در برنامه‌ی سمت کلاینت، زمانیکه بر روی دکمه‌ی Go صفحه‌ی اول کلیک می‌کنیم، تاریخ شروع رزرو و تعداد روز مدنظر، به صفحه‌ی مشاهده‌ی لیست اتاق‌ها ارسال می‌شود. اکنون می‌خواهیم در این لیست اتاق‌های نمایش داده شده، اگر بر روی لینک Book اتاقی کلیک شد، به صفحه‌ی اختصاصی رزرو آن اتاق هدایت شویم (مانند تصویر فوق). به همین جهت نیاز است موجودیت متناظر با اطلاعاتی را که قرار است از کاربر دریافت کنیم، به صورت زیر به پروژه‌ی BlazorServer.Entities اضافه کنیم:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        public DateTime CheckInDate { get; set; }

        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        public long TotalCost { get; set; }

        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        [ForeignKey("RoomId")]
        public HotelRoom HotelRoom { get; set; }

        public string Status { get; set; }
    }
}
در اینجا مشخصات شروع و پایان رزرو یک اتاق مشخص و مشخصات کاربری که قرار است این فرم را پر کند، مشاهده می‌کنید که Status یا وضعیت آن، در پروژه‌ی مشترک BlazorServer.Common به صورت زیر تعریف می‌شود:
namespace BlazorServer.Common
{
    public static class BookingStatus
    {
        public const string Pending = "Pending";
        public const string Booked = "Booked";
        public const string CheckedIn = "CheckedIn";
        public const string CheckedOutCompleted = "CheckedOut";
        public const string NoShow = "NoShow";
        public const string Cancelled = "Cancelled";
    }
}
پس از این تعاریف، DbSet آن‌را نیز به ApplicationDbContext اضافه می‌کنیم:
namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; }

        // ...
    }
}
بنابراین مرحله‌ی بعدی، ایجاد و اجرای Migrations متناظر با این جدول جدید است. برای این منظور با استفاده از خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddRoomOrderDetails --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
این دستورات به پروژه‌ی آغازین WebApi اشاره می‌کنند که قرار است از طریق سرویسی، با بانک اطلاعاتی ارتباط برقرار کند.

پس از تعریف یک موجودیت، یک DTO متناظر با آن‌را که جهت مدلسازی UI از آن استفاده خواهیم کرد، در پروژه‌ی BlazorServer.Models ایجاد می‌کنیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class RoomOrderDetailsDTO
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        [Required]
        public DateTime CheckInDate { get; set; }

        [Required]
        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

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

        [Required]
        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        public HotelRoomDTO HotelRoomDTO { get; set; }

        public string Status { get; set; }
    }
}
و همچنین در پروژه‌ی BlazorServer.Models.Mappings، نگاشت دوطرفه‌ی AutoMapper آن‌را نیز برقرار می‌کنیم؛ تا در حین تبدیل اطلاعات بین این دو، نیازی به تکرار سطرهای مقدار دهی اطلاعات خواص، نباشد:
namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            // ... 
            CreateMap<RoomOrderDetail, RoomOrderDetailsDTO>().ReverseMap(); // two-way mapping
        }
    }
}


ایجاد سرویسی برای کار با جدول RoomOrderDetails

در برنامه‌ی سمت کلاینت برای کار با بانک اطلاعاتی، دیگر نمی‌توان از سرویس‌های سمت سرور به صورت مستقیم استفاده کرد. به همین جهت آن‌ها را از طریق یک Web API endpoint، در معرض دید استفاده کننده قرار می‌دهیم. اما پیش از اینکار، سرویس سمت سرور Web API باید بتواند با سرویس دسترسی به اطلاعات جدول RoomOrderDetails، کار کند. بنابراین در ادامه این سرویس را تهیه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details);

        Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync();

        Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId);

        Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate);

        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id);

        Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status);
    }
}
که به صورت زیر پیاده سازی می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public RoomOrderDetailsService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        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 Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync()
        {
            return _dbContext.RoomOrderDetails
                            .Include(roomOrderDetail => roomOrderDetail.HotelRoom)
                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                            .ToListAsync();
        }

        public async Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.Id == roomOrderId);

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

        public Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate)
        {
            return _dbContext.RoomOrderDetails
                .AnyAsync(
                    roomOrderDetail =>
                        roomOrderDetail.RoomId == RoomId &&
                        roomOrderDetail.IsPaymentSuccessful &&
                        (
                            (checkInDate < roomOrderDetail.CheckOutDate && checkInDate > roomOrderDetail.CheckInDate) ||
                            (checkOutDate > roomOrderDetail.CheckInDate && checkInDate < roomOrderDetail.CheckInDate)
                        )
                );
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id)
        {
            throw new NotImplementedException();
        }

        public Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status)
        {
            throw new NotImplementedException();
        }
    }
}
توضیحات:
- از متد CreateAsync برای تبدیل مدل فرم ثبت اطلاعات، به یک رکورد جدول RoomOrderDetails، استفاده می‌کنیم.
- متد GetAllRoomOrderDetailsAsync، لیست تمام سفارش‌های ثبت شده را بازگشت می‌دهد.
- متد GetRoomOrderDetailAsync بر اساس شماره اتاقی که دریافت می‌کند، لیست سفارشات آن اتاق خاص را بازگشت می‌دهد. این لیست به علت استفاده از Include‌های تعریف شده، به همراه مشخصات اتاق و همچنین تصاویر مرتبط با آن اتاق نیز هست.
- متد IsRoomBookedAsync بر اساس شماره اتاق و بازه‌ی زمانی درخواستی توسط یک کاربر مشخص می‌کند که آیا اتاق خالی شده‌است یا خیر؟

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

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IRoomOrderDetailsService, RoomOrderDetailsService>();
            // ...
 
 
تشکیل سرویس ابتدایی کار با RoomOrderDetails در پروژه‌ی WASM

در ادامه، تعاریف خالی سرویس سمت کلاینت کار با RoomOrderDetails  را به پروژه‌ی WASM اضافه می‌کنیم. تکمیل این سرویس را به قسمت بعدی واگذار خواهیم کرد:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
    }
}
با این پیاده سازی ابتدایی:
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> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }

        public Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }
    }
}
که این مورد نیز باید به نحو زیر به سیستم تزریق وابستگی‌های برنامه‌ی سمت کلاینت در فایل Program.cs آن اضافه شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
            // ...
        }
    }
}


تعریف مدل فرم ثبت اطلاعات سفارش

پس از تدارک مقدمات فوق، اکنون می‌توانیم کار تکمیل فرم ثبت اطلاعات سفارش را شروع کنیم. به همین جهت مدل مخصوص آن‌را در برنامه‌ی سمت کلاینت به صورت زیر تشکیل می‌دهیم:
using BlazorServer.Models;

namespace BlazorWasm.Client.Models.ViewModels
{
    public class HotelRoomBookingVM
    {
        public RoomOrderDetailsDTO OrderDetails { get; set; }
    }
}


تعریف کامپوننت جدید RoomDetails و مقدار دهی اولیه‌ی مدل آن

در ادامه فایل جدید BlazorWasm.Client\Pages\HotelRooms\RoomDetails.razor را ایجاد کرده و به صورت زیر مقدار دهی اولیه می‌کنیم:
@page "/hotel/room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService

@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{

}

@code {
    [Parameter]
    public int? Id { get; set; }

    HotelRoomBookingVM HotelBooking  = new HotelRoomBookingVM();
    int NoOfNights = 1;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
            if (Id != null)
            {
                if (await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking) != null)
                {
                    var roomInitialInfo = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking);
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, roomInitialInfo.StartDate, roomInitialInfo.EndDate);
                    NoOfNights = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.CheckInDate = roomInitialInfo.StartDate;
                    HotelBooking.OrderDetails.CheckOutDate = roomInitialInfo.EndDate;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        roomInitialInfo.NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
                else
                {
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, DateTime.Now, DateTime.Now.AddDays(1));
                    NoOfNights = 1;
                    HotelBooking.OrderDetails.CheckInDate = DateTime.Now;
                    HotelBooking.OrderDetails.CheckOutDate = DateTime.Now.AddDays(1);
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = 1;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
            }
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}
- در ابتدا مسیریابی کامپوننت جدید RoomDetails را مشخص کرد‌ه‌ایم که یک Id را می‌پذیرد که همان Id اتاق انتخاب شده‌ی توسط کاربر است. به همین جهت پارامتر عمومی متناظری با آن هم در قسمت کدهای کامپوننت تعریف شده‌است.
- سپس سرویس توکار IJSRuntime به کامپوننت تزریق شده‌است تا توسط آن و Toastr، بتوان خطاهایی را به کاربر نمایش داد.
- از سرویس ILocalStorageService برای دسترسی به اطلاعات شروع به رزرو شخص و تعداد روز مدنظر او استفاده می‌کنیم که در قسمت قبل آن‌را مقدار دهی کردیم.
- همچنین از سرویس IClientHotelRoomService که آن‌را نیز در قسمت قبل افزودیم، برای فراخوانی متد GetHotelRoomDetailsAsync آن استفاده کرده‌ایم.

در روال آغازین OnInitializedAsync، اگر Id تنظیم شده بود، یعنی کاربر به درستی وارد این صفحه شده‌است. سپس بررسی می‌کنیم که آیا اطلاعاتی از درخواست ابتدایی او در Local Storage مرورگر وجود دارد یا خیر؟ اگر این اطلاعات وجود داشته باشد، بر اساس آن، بازه‌ی تاریخی دقیقی را می‌توان تشکیل داد و اگر خیر، این بازه را از امروز، به مدت 1 روز درنظر می‌گیریم.
پس از پایان کار متد OnInitializedAsync، چون اجزای HotelBooking مقدار دهی کامل شده‌اند، نمایش loading ابتدای کامپوننت، متوقف شده و قسمت else شرط نوشته شده اجرا می‌شود؛ یعنی اصل UI فرم نمایان خواهد شد.

در قسمت قبل، متد GetHotelRoomDetailsAsync را تکمیل نکردیم؛ چون به آن نیازی نداشتیم و فقط قصد داشتیم تا لیست تمام اتاق‌ها را نمایش دهیم. اما در اینجا برای تکمیل کدهای آغازین کامپوننت RoomDetails، متد دریافت اطلاعات یک اتاق را نیز تکمیل می‌کنیم تا توسط آن بتوان در این کامپوننت نیز جزئیات اتاق انتخابی را نمایش داد:
namespace BlazorWasm.Client.Services
{
    public class ClientHotelRoomService : IClientHotelRoomService
    {
        private readonly HttpClient _httpClient;

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

        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/hotelroom/{roomId}"))
                            .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}")
                            .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}")
                            .Uri;
            return _httpClient.GetFromJsonAsync<HotelRoomDTO>(uri);
        }

        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate)
        {
           // ...
        }
    }
}

اتصال مدل کامپوننت RoomDetails به فرم ثبت سفارش آن

تا اینجا مدل فرم را مقدار دهی اولیه کردیم. اکنون می‌توانیم قسمت else شرط نوشته شده را تکمیل کرده و در قسمتی از آن، مشخصات اتاق جاری را نمایش دهیم و در قسمتی دیگر، فرم ثبت سفارش را تکمیل کنیم.
الف) نمایش مشخصات اتاق جاری
در کامپوننت جاری با استفاده از خواص مقدار دهی اولیه شده‌ی شیء HotelBooking.OrderDetails.HotelRoomDTO، می‌توان جزئیات اتاق انتخابی را نمایش داد که نمونه‌ای از آن‌را در قسمت قبل هم مشاهده کردید:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
    <div class="mt-4 mx-4 px-0 px-md-5 mx-md-5">
        <div class="row p-2 my-3 " style="border-radius:20px; ">
            <div class="col-12 col-lg-7 p-4" style="border: 1px solid gray">
                <div class="row px-2 text-success border-bottom">
                    <div class="col-8 py-1"><p style="font-size:x-large;margin:0px;">Selected Room</p></div>
                    <div class="col-4 p-0"><a href="hotel/rooms" class="btn btn-secondary btn-block">Back to Room's</a></div>
                </div>
                <div class="row">
                    <div class="col-6">
                        <div id="" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel">
                            <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel">
                                <ol class="carousel-indicators">
                                    <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
                                    <li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
                                </ol>
                                <div class="carousel-inner">
                                    <div class="carousel-item active">
                                        <img class="d-block w-100" src="images/slide1.jpg" alt="First slide">
                                    </div>
                                </div>
                                <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
                                    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Previous</span>
                                </a>
                                <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
                                    <span class="carousel-control-next-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Next</span>
                                </a>
                            </div>
                        </div>
                    </div>
                    <div class="col-6">
                        <span class="float-right pt-4">
                            <span class="float-right">Occupancy : @HotelBooking.OrderDetails.HotelRoomDTO.Occupancy adults </span><br />
                            <span class="float-right pt-1">Size : @HotelBooking.OrderDetails.HotelRoomDTO.SqFt sqft</span><br />
                            <h4 class="text-warning font-weight-bold pt-5">
                                <span style="border-bottom:1px solid #ff6a00">
                                    @HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount.ToString("#,#.00#;(#,#.00#)")
                                </span>
                            </h4>
                            <span class="float-right">Cost for @HotelBooking.OrderDetails.HotelRoomDTO.TotalDays nights</span>
                        </span>
                    </div>
                </div>
                <div class="row p-2">
                    <div class="col-12">
                        <p class="card-title text-warning" style="font-size:xx-large">@HotelBooking.OrderDetails.HotelRoomDTO.Name</p>
                        <p class="card-text" style="font-size:large">
                            @((MarkupString)@HotelBooking.OrderDetails.HotelRoomDTO.Details)
                        </p>
                    </div>

                </div>
            </div>
}
ب) نمایش فرم متصل به مدل کامپوننت
قسمت دوم UI کامپوننت جاری، نمایش فرم زیر است که اجزای مختلف آن به فیلد HotelBooking متصل شده‌اند:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
  // ...
             
            <div class="col-12 col-lg-5 p-4 2 mt-4 mt-md-0" style="border: 1px solid gray;">
                <EditForm Model="HotelBooking" class="container" OnValidSubmit="HandleCheckout">
                    <div class="row px-2 text-success border-bottom"><div class="col-7 py-1"><p style="font-size:x-large;margin:0px;">Enter Details</p></div></div>

                    <div class="form-group pt-2">
                        <label class="text-warning">Name</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Name" type="text" class="form-control" />
                    </div>
                    <div class="form-group pt-2">
                        <label class="text-warning">Phone</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Phone" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Email</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Email" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check in Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckInDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check Out Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckOutDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">No. of nights</label>
                        <select class="form-control" value="@NoOfNights" @onchange="HandleNoOfNightsChange">
                            @for (var i = 1; i <= 10; i++)
                            {
                                if (i == NoOfNights)
                                {
                                    <option value="@i" selected="selected">@i</option>
                                }
                                else
                                {
                                    <option value="@i">@i</option>
                                }
                            }
                        </select>
                    </div>
                    <div class="form-group">
                        <button type="submit" class="btn btn-success form-control">Checkout Now</button>
                    </div>
                </EditForm>
            </div>
        </div>
    </div>
}
در این فرم دو روال رویدادگردان زیر نیز مورد استفاده هستند:
@code {
    // ...

    private async Task HandleNoOfNightsChange(ChangeEventArgs e)
    {
        NoOfNights = Convert.ToInt32(e.Value.ToString());
        HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
            Id.Value,
            HotelBooking.OrderDetails.CheckInDate,
            HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights));

        HotelBooking.OrderDetails.CheckOutDate = HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights);
        HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = NoOfNights;
        HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
    }

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

    private async Task<bool> HandleValidation()
    {
        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Name))
        {
            await JsRuntime.ToastrError("Name cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Phone))
        {
            await JsRuntime.ToastrError("Phone cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Email))
        {
            await JsRuntime.ToastrError("Email cannot be empty");
            return false;
        }
        return true;
    }
}
- کاربر اگر تعداد شب‌های اقامت را از طریق دارپ‌داون فرم تغییر داد، در روال رویدادگردان HandleNoOfNightsChange، محاسبات جدیدی را بر این اساس انجام می‌دهیم؛ چون هزینه و سایر مشخصات جزئیات اتاق نمایش داده شده، باید تغییر کنند.
- همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کرده‌ایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده می‌کنید، کرده‌ایم. این روش دستی را نیز می‌توان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آن‌را توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمی‌کند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کرده‌ایم.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-29.zip
مطالب دوره‌ها
استفاده از RavenDB در ASP.NET MVC به همراه تزریق وابستگی‌ها
جهت تکمیل مباحث این دوره می‌توان به نحوه مدیریت سشن‌ها و document store بانک اطلاعاتی RavenDB با استفاده از یک IoC Container مانند StructureMap در ASP.NET MVC پرداخت. اصول کلی آن به تمام فناوری‌های دات نتی دیگر مانند وب فرم‌ها، WPF و غیره نیز قابل بسط است. تنها پیشنیاز آن مطالعه «کامل» دوره «بررسی مفاهیم معکوس سازی وابستگی‌ها و ابزارهای مرتبط با آن » می‌باشد.


هدف از بحث
ارائه راه حلی جهت تزریق یک وهله از واحد کار تشکیل شده (همان شیء سشن در RavenDB) به کلیه کلاس‌های لایه سرویس برنامه و همچنین زنده نگه داشتن شیء document store آن در طول عمر برنامه است. ایجاد شیء document store که کار اتصال به بانک اطلاعاتی را مدیریت می‌کند، بسیار پرهزینه است. به همین جهت این شیء تنها یکبار باید در طول عمر برنامه ایجاد شود.


ابزارها و پیشنیازهای لازم
ابتدا یک برنامه جدید ASP.NET MVC را آغاز کنید. سپس ارجاعات لازم را به کلاینت RavenDB، سرور درون پروسه‌ای آن (RavenDB.Embedded) و همچنین StructureMap با استفاده از نیوگت، اضافه نمائید:
 PM> Install-Package RavenDB.Client
PM> Install-Package RavenDB.Embedded -Pre
PM> Install-Package structuremap

دریافت کدهای کامل این مثال

این مثال، به همراه فایل‌های باینری ارجاعات یاد شده، نیست (جهت کاهش حجم 100 مگابایتی آن). برای بازیابی آن‌ها می‌توانید به مطلبی در اینباره در سایت مراجعه کنید.
این پروژه از چهار قسمت مطابق شکل زیر تشکیل شده است:


الف) لایه سرویس‌های برنامه
using RavenDB25Mvc4Sample.Models;
using System.Collections.Generic;

namespace RavenDB25Mvc4Sample.Services.Contracts
{
    public interface IUsersService
    {
        User AddUser(User user);
        IList<User> GetUsers(int page, int count = 20);
    }
}

using System.Collections.Generic;
using System.Linq;
using Raven.Client;
using RavenDB25Mvc4Sample.Models;
using RavenDB25Mvc4Sample.Services.Contracts;

namespace RavenDB25Mvc4Sample.Services
{
    public class UsersService : IUsersService
    {
        private readonly IDocumentStore _documentStore;
        private readonly IDocumentSession _documentSession;
        public UsersService(IDocumentStore documentStore, IDocumentSession documentSession)
        {
            _documentStore = documentStore;
            _documentSession = documentSession;
        }

        public User AddUser(User user)
        {
            _documentSession.Store(user);
            return user;
        }

        public IList<User> GetUsers(int page, int count = 20)
        {
            return _documentSession.Query<User>()
                                   .Skip(page * count)
                                   .Take(count)
                                   .ToList();
        }

        //todo: سایر متدهای مورد نیاز در اینجا

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

ب) لایه Infrastructure برنامه
در این لایه کدهای اتصالات IoC Container مورد استفاده قرار می‌گیرند. کدهایی که به برنامه جاری وابسته‌اند، اما حالت عمومی و مشترکی ندارند تا در سایر پروژه‌های مشابه استفاده شوند.
using Raven.Client;
using Raven.Client.Embedded;
using RavenDB25Mvc4Sample.Services;
using RavenDB25Mvc4Sample.Services.Contracts;
using StructureMap;

namespace RavenDB25Mvc4Sample.Infrastructure
{
    public static class IoCConfig
    {
        public static void ApplicationStart()
        {
            ObjectFactory.Initialize(x =>
            {
                // داکیومنت استور سینگلتون تعریف شده چون باید در طول عمر برنامه زنده نگه داشته شود
                x.ForSingletonOf<IDocumentStore>().Use(() =>
                       {
                           return new EmbeddableDocumentStore
                           {
                               DataDirectory = "App_Data"
                           }.Initialize();
                       });

                // سشن در برنامه وب هیبرید تعریف شده تا در طول عمر یک درخواست زنده نگه داشته شود
                // در برنامه‌های ویندوزی حالت هیبرید را حذف کنید
                x.For<IDocumentSession>().HybridHttpOrThreadLocalScoped().Use(context =>
                    {
                        return context.GetInstance<IDocumentStore>().OpenSession();
                    });

                // اتصالات لایه سرویس در اینجا
                x.For<IUsersService>().Use<UsersService>();
                // ...
            });
        }

        public static void ApplicationEndRequest()
        {
            ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
        }
    }
}
تعاریف اتصالات StructureMap را در اینجا ملاحظه می‌کنید.
IDocumentStore و IDocumentSession، دو اینترفیس تعریف شده در کلاینت RavenDB هستند. اولی کار اتصال به بانک اطلاعاتی را مدیریت خواهد کرد و دومی کار مدیریت الگوی واحد کار را انجام می‌دهد. IDocumentStore به صورت Singleton تعریف شده است؛ چون باید در طول عمر برنامه زنده نگه داشته شود. اما IDocumentStore در ابتدای هر درخواست رسیده، وهله سازی شده و سپس در پایان هر درخواست در متد ApplicationEndRequest به صورت خودکار Dispose خواهد شد.
اگر به فایل Global.asax.cs پروژه وب برنامه مراجعه کنید، نحوه استفاده از این کلاس را مشاهده خواهید کرد:
using System;
using System.Globalization;
using System.Web.Mvc;
using System.Web.Routing;
using RavenDB25Mvc4Sample.Infrastructure;
using StructureMap;

namespace RavenDB25Mvc4Sample
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            IoCConfig.ApplicationStart();

            AreaRegistration.RegisterAllAreas();

            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            //Set current Controller factory as StructureMapControllerFactory  
            ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory());
        }

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            IoCConfig.ApplicationEndRequest();
        }
    }

    public class StructureMapControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType == null)
                throw new InvalidOperationException(string.Format("Page not found: {0}", 
                    requestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture)));
            return ObjectFactory.GetInstance(controllerType) as Controller;
        }
    }
}
در ابتدای کار برنامه، متد IoCConfig.ApplicationStart جهت برقراری اتصالات، فراخوانی می‌شود. در پایان هر درخواست نیز شیء سشن جاری تخریب خواهد شد. همچنین کلاس StructureMapControllerFactory نیز جهت وهله سازی خودکار کنترلرهای برنامه به همراه تزریق وابستگی‌های مورد نیاز، تعریف گشته است.

ج) استفاده از کلاس‌های لایه سرویس در کنترلرهای برنامه

using System.Web.Mvc;
using Raven.Client;
using RavenDB25Mvc4Sample.Models;
using RavenDB25Mvc4Sample.Services.Contracts;

namespace RavenDB25Mvc4Sample.Controllers
{
    public class HomeController : Controller
    {
        private readonly IDocumentSession _documentSession;
        private readonly IUsersService _usersService;
        public HomeController(IDocumentSession documentSession, IUsersService usersService)
        {
            _documentSession = documentSession;
            _usersService = usersService;
        }

        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش صفحه ثبت
        }

        [HttpPost]
        public ActionResult Index(User user)
        {
            if (this.ModelState.IsValid)
            {
                _usersService.AddUser(user);
                _documentSession.SaveChanges();

                return RedirectToAction("Index");
            }
            return View(user);
        }
    }
}
پس از این مقدمات و طراحی اولیه، استفاده از کلاس‌های لایه سرویس در کنترل‌ها، ساده خواهند بود. تنها کافی است اینترفیس‌های مورد نیاز را از طریق روش تزریق در سازنده کلاس‌ها تعریف کنیم. سایر مسایل وهله سازی آن خودکار خواهند بود.
مطالب
WF:Windows Workflow #۶
در این قسمت به تکمیل مثالی که در قسمت قبل زده شد پرداخته می‌شود و همچنین کنترل‌های Foreach , Try Catch نیز بررسی خواهند شد.
در ابتدا دو کلاس به نام‌های ItemInfo و OutOfStockException  را به برنامه اضافه می‌کنیم. کلاس اول برای ذخیره سازی مشخصات هر سفارش و کلاس دیگر برای مدیریت خطا‌ها می‌باشد.
public class ItemInfo
    {
        public string ItemCode { get; set; } 
        public string Description { get; set; } 
        public decimal Price { get; set; }
    }

public class OutOfStockException : Exception
    {
        public OutOfStockException() 
            : base() 
        { 

        }

        public OutOfStockException(string message) 
            : base(message) 
        { 

        }
    }
در Workflow مورد نظر که به نام OrderWF.xaml می‌باشد٬ پس از کنترل Assign که برای صفر کردن مقدار متغیر TotalAmount از آن استفاده می‌شود٬ یک کنترل ForEach را به Flow جاری اضافه می‌کنیم. این کنترل دارای دو خاصیت به نام‌های Type Arguments و Values می‌باشد. اولین خاصیت که مقدار پیش فرض آن، مقدار عددی Int32 است٬ برای مشخص کردن نوع متغییر حلقه و دیگری برای مشخص کردن نوع منبع داده حلقه تعریف شده‌اند.

همانطور که در شکل بالا مشخص می‌باشد٬ Type Arguments حلقه برابر با کلاس OrderItem می‌باشد. Values هم برابر با OrderInfo.Items است. از این جهت نوع حلقه  را از جنس کلاس OrderItem مشخص کرد‌‌ه‌ایم تا کنترل بر روی مقادیر Items اجرا شود (لیستی از کلاس مورد نظر).
حال همانند شکل بالا، در قسمت Body کنترل ForEach، یک کنترل Sequence را ایجاد کرده و سپس برای اینکه کنترل LookupItem را ایجاد کنیم٬ ابتدا باید یک Code Activity را به پروژه اضافه کنیم. به همین منظور پروژه جاری را انتخاب کرده و یک Code Activity به آن اضافه و نام آن را LookupItem  می‌گذاریم. سپس کد زیر را به آن اضافه می‌کنیم:
 public sealed class LookupItem : CodeActivity
    {
        // Define an activity input argument of type string
        public InArgument<string> ItemCode { get; set; }         
        public OutArgument<ItemInfo> Item { get; set; }

        // If your activity returns a value, derive from CodeActivity<TResult>
        // and return the value from the Execute method.
        protected override void Execute(CodeActivityContext context)
        {
            // Obtain the runtime value of the Text input argument
            ItemInfo i = new ItemInfo();             
            i.ItemCode = context.GetValue<string>(ItemCode);
            switch (i.ItemCode)
            {

                case "12345": 
                    i.Description = "Widget"; 
                    i.Price = (decimal)10.0; 
                    break;
                case "12346": 
                    i.Description = "Gadget"; 
                    i.Price = (decimal)15.0; 
                    break;
                case "12347": 
                    i.Description = "Super Gadget"; 
                    i.Price = (decimal)25.0; break;
            }
     
            context.SetValue(this.Item, i);
            
        }
    }
در این کد، دو متغییر تعریف شده‌اند؛ یکی از نوع رشته بوده و از طریق آن، دستور Switch تصمیم می‌گیرد که کلاس ItemInfo را با چه مقادیری پرکند. متغییر دیگر از نوع کلاس  ItemInfo می‌باشد و برای گرفتن مقدار کلاس از دستور Switch تعریف شده است.
حال برای اینکه بتوانیم از Code Activity مورد نظر استفاده کنیم٬ ابتدا باید پروژه را یکبار Build کنیم. اکنون در قسمت Toolbox یک٬ Tab ایی به نام پروژه ایجاد شده است و در آن یک کنترل به نام  LookupItem  موجود می‌باشد. آن را گرفته و به درون Sequence انتقال می‌دهیم.
سپس برای مقدار دادن به متغیر‌های تعریف شده در Code Activity، کنترل LookupItem را انتخاب کرده و در قسمت Properties به خصوصیت ItemCode، کد زیر را اضافه می‌کنیم:
item.ItemCode

نکته
: از کلاس Code Activity برای ارسال و دریافت مقادیر به درون Workflow استفاده می‌شود.

 Try Catch 
از این کنترل برای مدیریت خطا‌ها استفاده می‌شود.
ابتدا یک کنترل Try Catch را به Workflow اضافه کرده، مانند شکل زیر:

در بدنه Try می‌توان از کنترل‌های مورد نظر استفاده کنیم و همانطور که در شکل بالا مشخص است٬ از کنترل Throw برای ایجاد خطا استفاده شده‌است. کنترل جاری را انتخاب کرده و از قسمت Properties در خاصیت Exception کد زیر را اضافه می‌کنیم:
new OutOfStockException("Item Code"+item.ItemCode)
این  دستور باعث ایجاد یک خطا از نوع کلاس OutOfStockException می‌شود. برای کنترل خطای مورد نظر در قسمت Catches مانند شکل زیر عمل می‌کنیم.

مطالب
آزمون‌های یکپارچگی در برنامه‌های ASP.NET Core
تا اینجا دو روش را برای آزمایش کلی یک سیستم Web API به همراه تمام زیر ساخت‌های آن، بررسی کردیم:
- استفاده از Postman برای آزمایش یک برنامه‌ی Web API
- استفاده از strest برای آزمایش یک برنامه‌ی Web API

روش سومی هم برای انجام اینکار وجود دارد که به صورت توکار از زمان ارائه‌ی ASP.NET Core 2.1 به همراه TestServer آزمایشی آن میسر شد. این روش در نگارش 3.1، با تغییر روش تعریف فایل program.cs، جهت سازگاری آن با آزمون‌های یکپارچگی/آزمایش کل سیستم، بهبود یافته‌است که خلاصه‌ای از آن را در این مطلب بررسی می‌کنیم.


آزمون‌های یکپارچگی در ASP.NET Core

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

برای ایجاد آزمون‌های یکپارچگی در برنامه‌های ASP.NET Core، حداقل سه مرحله باید طی شوند:
الف) ایجاد یک class library که ارجاعی را به پروژه‌ی اصلی دارد. این پروژه حاوی آزمایش‌های ما خواهد بود.
ب) راه اندازی یک هاست وب آزمایشی برای ارسال درخواست‌ها به آن و دریافت پاسخ‌های نهایی.
ج) استفاده از یک test runner (انواع و اقسام فریم ورک‌های unit testing) برای اجرای آزمایش‌ها


ایجاد یک پروژه‌ی کتابخانه برای هاست و اجرای آزمایش‌های یکپارچگی

فرض کنید می‌خواهیم برای همان پروژه‌ی ایجاد JWTها، آزمایش یکپارچگی بنویسیم. پس از ایجاد یک پروژه‌ی کتابخانه‌ی جدید که قرار است هاست آزمایش‌های ما شود، نیاز است محتوای فایل csproj آن‌را به صورت زیر تغییر داد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <NoWarn>RCS1090</NoWarn>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\ASPNETCore2JwtAuthentication.WebApp\ASPNETCore2JwtAuthentication.WebApp.csproj" />
  </ItemGroup>
  <ItemGroup>
    <None Include="..\ASPNETCore2JwtAuthentication.WebApp\appsettings.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>
  <ItemGroup>
    <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c329}" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="fluentassertions" Version="5.10.3" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
    <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
    <PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
  </ItemGroup>
</Project>
در اینجا، این نکات قابل مشاهده هستند:
1) TargetFramework آن باید به netcoreapp تنظیم شود.
2) باید ارجاع مستقیمی به کل پروژه‌ی نهایی WebApp در آن وجود داشته باشد. چون در ادامه می‌خواهیم فایل Program.cs آن‌را برای راه اندازی یک هاست وب آزمایشی، فراخوانی کنیم.
3) بسته‌ی نیوگتی که کار راه اندازی هاست وب آزمایشی را انجام می‌دهد، Microsoft.AspNetCore.Mvc.Testing نام دارد. این بسته، کار کپی فایل‌های پروژه‌ی اصلی و همچنین تنظیم مسیر پروژه را به این مسیر جدید نیز انجام می‌دهد.
4) روش افزودن بسته‌های MSTest را مشاهده می‌کنید.
5) همچنین جهت ساده‌تر شدن بررسی نتایج آزمون‌های انجام شده می‌توان از fluentassertions نیز استفاده کرد.


راه اندازی هاست وب آزمایشی جهت انجام آزمون‌های واحد

پس از انجام تنظیمات ابتدایی پروژه‌ی آزمون یکپارچگی، نیاز است یک WebApplicationFactory سفارشی را ایجاد کرد:
using ASPNETCore2JwtAuthentication.WebApp;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    public class CustomWebApplicationFactory : WebApplicationFactory<Program>
    {
        protected override IWebHostBuilder CreateWebHostBuilder()
        {
            var builder = base.CreateWebHostBuilder();
            builder.ConfigureLogging(logging =>
            {
                //TODO: ...
            });
            return builder;
        }

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureTestServices(services =>
            {
                // Don't run `IHostedService`s when running as a test
                services.RemoveAll(typeof(IHostedService));
            });
        }
    }
}
در این تعریف، Program در <WebApplicationFactory<Program، دقیقا به همان کلاس Program برنامه‌ی اصلی وب اشاره می‌کند. به همین جهت امضای این کلاس در نگارش 3.1 تغییر کرده‌است تا با WebApplicationFactory بسته‌ی Microsoft.AspNetCore.Mvc.Testing هماهنگ شود.
در ادامه روش سفارشی سازی WebApplicationFactory  را مشاهده می‌کنید. برای مثال اگر خواستید سرویس‌ها و تنظیمات پیش‌فرض برنامه‌ی اصلی را تغییر دهید می‌توانید متد CreateWebHostBuilder را بازنویسی کنید و یا اگر خواستید سرویس جدیدی را اضافه و یا حذف کنید، می‌توان متد ConfigureWebHost را بازنویسی کرد.


استفاده از WebApplicationFactory سفارشی، جهت ایجاد یک HttpClient

هدف اصلی از ایجاد CustomWebApplicationFactory نه فقط راه اندازی یک هاست وب سفارشی است، بلکه توسط متد CreateClient آن می‌توان به یک HttpClient دسترسی یافت که قابلیت ارسال اطلاعات را به برنامه‌ی وبی که در پشت صحنه راه اندازی می‌شود، دارا است. کار CustomWebApplicationFactory شبیه به راه اندازی dotnet run در پشت صحنه‌است. در اینجا دیگر نیازی نیست تا اینکار را به صورت دستی انجام داد. به همین جهت چون برنامه‌ی وب اصلی به نحو متداولی در پشت صحنه اجرا می‌شود، عموما راه اندازی آن که شامل تنظیمات اولیه و یا حتی ایجاد بانک اطلاعاتی است، کمی کند است و اگر قرار باشد هربار اینکار صورت گیرد، به آزمون‌های بسیار کندی خواهیم رسید. به همین جهت می‌توان یک کلاس singleton را برای مدیریت تک وهله‌ی نهایی HttpClient آن به صورت زیر ایجاد کرد:
using System;
using System.Threading;
using System.Net.Http;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    public static class TestsHttpClient
    {
        private static readonly Lazy<HttpClient> _serviceProviderBuilder =
                new Lazy<HttpClient>(getHttpClient, LazyThreadSafetyMode.ExecutionAndPublication);

        /// <summary>
        /// A lazy loaded thread-safe singleton
        /// </summary>
        public static HttpClient Instance { get; } = _serviceProviderBuilder.Value;

        private static HttpClient getHttpClient()
        {
            var services = new CustomWebApplicationFactory();
            return services.CreateClient(); //NOTE: This action is very time consuming, so it should be defined as a singleton.
        }
    }
}
مزیت کار با این کلاس، عدم راه اندازی مجدد برنامه به ازای هر آزمون انجام شده‌است. چون به ازای هر آزمونی که خواهیم نوشت، نیاز است به HttpClient دسترسی داشته باشیم.


نوشتن اولین آزمون یکپارچگی

پس از تنظیم هاست وب آزمایشی و ایجاد یک HttpClient از پیش تنظیم شده که به آن اشاره می‌کند، اکنون می‌توان اولین آزمون یکپارچگی را به صورت زیر نوشت:
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    [TestClass]
    public class JwtTests
    {
        [TestMethod]
        public async Task TestLoginWorks()
        {
            // Arrange
            var client = TestsHttpClient.Instance;

            // Act
            var token = await doLoginAsync(client);

            // Assert
            token.Should().NotBeNull();
            token.AccessToken.Should().NotBeNullOrEmpty();
            token.RefreshToken.Should().NotBeNullOrEmpty();
        }

        [TestMethod]
        public async Task TestCallProtectedApiWorks()
        {
            // Arrange
            var client = TestsHttpClient.Instance;

            // Act
            var token = await doLoginAsync(client);

            // Assert
            token.Should().NotBeNull();
            token.AccessToken.Should().NotBeNullOrEmpty();
            token.RefreshToken.Should().NotBeNullOrEmpty();

            // Act
            const string protectedApiUrl = "/api/MyProtectedApi";
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
            var response = await client.GetAsync(protectedApiUrl);
            response.EnsureSuccessStatusCode();

            // Assert
            var responseString = await response.Content.ReadAsStringAsync();
            responseString.Should().NotBeNullOrEmpty();
            var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
            var apiResponse = JsonSerializer.Deserialize<MyProtectedApiResponse>(responseString, options);
            apiResponse.Title.Should().NotBeNullOrEmpty();
            apiResponse.Title.Should().Be("Hello from My Protected Controller! [Authorize]");
        }

        private static async Task<Token> doLoginAsync(HttpClient client)
        {
            const string loginUrl = "/api/account/login";
            var user = new { Username = "Vahid", Password = "1234" };
            var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, loginUrl)
            {
                Content = new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json")
            });
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            responseString.Should().NotBeNullOrEmpty();
            return JsonSerializer.Deserialize<Token>(responseString);
        }
    }
}
توضیحات:
- در هر آزمونی نیاز است در ابتدا به TestsHttpClient.Instance، که همان HttpClient ساخته شده‌ی توسط CustomWebApplicationFactory است، دسترسی یافت و همانطور که عنوان شد، دسترسی به وهله‌ای از HttpClient که به هاست وب آزمایشی برنامه‌ی اصلی اشاره می‌کند، عموما بسیار زمانبراست و برای مثال در دو آزمایش نوشته شده‌ی در اینجا اگر قرا باشد هربار اینکار از صفر انجام شود، زمان به اتمام رسیدن این آزمایش‌ها بسیار طولانی خواهد شد. به همین جهت طول عمر TestsHttpClient را singleton تعریف کردیم تا فقط یکبار کار برپایی وب سرور آزمایشی در پشت صحنه انجام شود.
- سپس مابقی کار، همان روش استاندارد کار با HttpClient است. در ابتدا درخواستی را به سمت سرور آزمایشی که در پشت صحنه در حال اجرا است، ارسال می‌کنیم. چون HttpClient دریافتی توسط  CustomWebApplicationFactory تنظیم شده‌است، دیگر نیازی به ذکر آدرس پایه‌ی وب سایت مانند https://localhost:5001 نیست و آدرس‌های ذکر شده‌ی در اینجا، نسبی هستند. سپس محتوای Response دریافتی از سرور را جهت تکمیل آزمایشات، بررسی خواهیم کرد.


یک نکته: اگر OpenAPI را در برنامه‌های Web API فعال کنید، می‌توان با استفاده از ابزارهای تولید کد، کدهای مرتبط با HttpClient را نیز به صورت خودکار تولید و سپس از آن‌ها در اینجا استفاده کرد.


اجرای آزمون‌های یکپارچگی نوشته شده

چون ظاهر این آزمون‌ها با آزمون‌های واحد MSTest یا هر فریم ورک مشابه دیگری یکسان است، می‌توان از امکانات IDEها برای اجرای آن‌ها استفاده کرد و یا حتی می‌توان دستور dotnet test را نیز در ریشه‌ی این پروژه‌ی جدید برای اجرای تمام آزمون‌های نوشته شده، اجرا کرد:



کدهای کامل این مطلب را در اینجا می‌توانید مشاهده کنید.
مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
- پیشنیار بحث «معرفی JSON Web Token»

پیاده سازی‌های زیادی را در مورد JSON Web Token با ASP.NET Web API، با کمی جستجو می‌توانید پیدا کنید. اما مشکلی که تمام آن‌ها دارند، شامل این موارد هستند:
- چون توکن‌های JWT، خودشمول هستند (در پیشنیاز بحث مطرح شده‌است)، تا زمانیکه این توکن منقضی نشود، کاربر با همان سطح دسترسی قبلی می‌تواند به سیستم، بدون هیچگونه مانعی لاگین کند. در این حالت اگر این کاربر غیرفعال شود، کلمه‌ی عبور او تغییر کند و یا سطح دسترسی‌های او کاهش یابند ... مهم نیست! باز هم می‌تواند با همان توکن قبلی لاگین کند.
- در روش JSON Web Token، عملیات Logout سمت سرور بی‌معنا است. یعنی اگر برنامه‌ای در سمت کاربر، قسمت logout را تدارک دیده باشد، چون در سمت سرور این توکن‌ها جایی ذخیره نمی‌شوند، عملا این logout بی‌مفهوم است و مجددا می‌توان از همان توکن قبلی، برای لاگین به سرور استفاده کرد. چون این توکن شامل تمام اطلاعات لازم برای لاگین است و همچنین جایی هم در سرور ثبت نشده‌است که این توکن در اثر logout، باید غیرمعتبر شود.
- با یک توکن از مکان‌های مختلفی می‌توان دسترسی لازم را جهت استفاده‌ی از قسمت‌های محافظت شده‌ی برنامه یافت (در صورت دسترسی، چندین نفر می‌توانند از آن استفاده کنند).

به همین جهت راه حلی عمومی برای ذخیره سازی توکن‌های صادر شده از سمت سرور، در بانک اطلاعاتی تدارک دیده شد که در ادامه به بررسی آن خواهیم پرداخت و این روشی است که می‌تواند به عنوان پایه مباحث Authentication و Authorization برنامه‌های تک صفحه‌ای وب استفاده شود. البته سمت کلاینت این راه حل با jQuery پیاده سازی شده‌است (عمومی است؛ برای طرح مفاهیم پایه) و سمت سرور آن به عمد از هیچ نوع بانک اطلاعات و یا ORM خاصی استفاده نمی‌کند. سرویس‌های آن برای بکارگیری انواع و اقسام روش‌های ذخیره سازی اطلاعات قابل تغییر هستند و الزامی نیست که حتما از EF استفاده کنید یا از ASP.NET Identity یا هر روش خاص دیگری.


نگاهی به برنامه


در اینجا تمام قابلیت‌های این پروژه را مشاهده می‌کنید.
- امکان لاگین
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize جهت کاربری با نقش Admin
- پیاده سازی مفهوم ویژه‌ای به نام refresh token که نیاز به لاگین مجدد را پس از منقضی شدن زمان توکن اولیه‌ی لاگین، برطرف می‌کند.
- پیاده سازی logout


بسته‌های پیشنیاز برنامه

پروژه‌ای که در اینجا بررسی شده‌است، یک پروژه‌ی خالی ASP.NET Web API 2.x است و برای شروع به کار با JSON Web Tokenها، تنها نیاز به نصب 4 بسته‌ی زیر را دارد:
PM> Install-Package Microsoft.Owin.Host.SystemWeb
PM> Install-Package Microsoft.Owin.Security.Jwt
PM> Install-Package structuremap
PM> Install-Package structuremap.web
بسته‌ی Microsoft.Owin.Host.SystemWeb سبب می‌شود تا کلاس OwinStartup به صورت خودکار شناسایی و بارگذاری شود. این کلاسی است که کار تنظیمات اولیه‌ی JSON Web token را انجام می‌دهد و بسته‌ی Microsoft.Owin.Security.Jwt شامل تمام امکاناتی است که برای راه اندازی توکن‌های JWT نیاز داریم.
از structuremap هم برای تنظیمات تزریق وابستگی‌های برنامه استفاده شده‌است. به این صورت قسمت تنظیمات اولیه‌ی JWT ثابت باقی خواهد ماند و صرفا نیاز خواهید داشت تا کمی قسمت سرویس‌های برنامه را بر اساس بانک اطلاعاتی و روش ذخیره سازی خودتان سفارشی سازی کنید.


دریافت کدهای کامل برنامه

کدهای کامل این برنامه را از اینجا می‌توانید دریافت کنید. در ادامه صرفا قسمت‌های مهم این کدها را بررسی خواهیم کرد.


بررسی کلاس AppJwtConfiguration

کلاس AppJwtConfiguration، جهت نظم بخشیدن به تعاریف ابتدایی توکن‌های برنامه در فایل web.config، ایجاد شده‌است. اگر به فایل web.config برنامه مراجعه کنید، یک چنین تعریفی را مشاهده خواهید کرد:
<appJwtConfiguration
    tokenPath="/login"
    expirationMinutes="2"
    refreshTokenExpirationMinutes="60"
    jwtKey="This is my shared key, not so secret, secret!"
    jwtIssuer="http://localhost/"
    jwtAudience="Any" />
این قسمت جدید بر اساس configSection ذیل که به کلاس AppJwtConfiguration اشاره می‌کند، قابل استفاده شده‌است (بنابراین اگر فضای نام این کلاس را تغییر دادید، باید این قسمت را نیز مطابق آن ویرایش کنید؛ درغیراینصورت، appJwtConfiguration قابل شناسایی نخواهد بود):
 <configSections>
    <section name="appJwtConfiguration" type="JwtWithWebAPI.JsonWebTokenConfig.AppJwtConfiguration" />
</configSections>
- در اینجا tokenPath، یک مسیر دلخواه است. برای مثال در اینجا به مسیر login تنظیم شده‌است. برنامه‌ای که با Microsoft.Owin.Security.Jwt کار می‌کند، نیازی ندارد تا یک قسمت لاگین مجزا داشته باشد (مثلا یک کنترلر User و بعد یک اکشن متد اختصاصی Login). کار لاگین، در متد GrantResourceOwnerCredentials کلاس AppOAuthProvider انجام می‌شود. اینجا است که نام کاربری و کلمه‌ی عبور کاربری که به سمت سرور ارسال می‌شوند، توسط Owin دریافت و در اختیار ما قرار می‌گیرند.
- در این تنظیمات، دو زمان منقضی شدن را مشاهده می‌کنید؛ یکی مرتبط است به access tokenها و دیگری مرتبط است به refresh tokenها که در مورد این‌ها، در ادامه بیشتر توضیح داده خواهد شد.
- jwtKey، یک کلید قوی است که از آن برای امضاء کردن توکن‌ها در سمت سرور استفاده می‌شود.
- تنظیمات Issuer و Audience هم در اینجا اختیاری هستند.

یک نکته
جهت سهولت کار تزریق وابستگی‌ها، برای کلاس AppJwtConfiguration، اینترفیس IAppJwtConfiguration نیز تدارک دیده شده‌است و در تمام تنظیمات ابتدایی JWT، از این اینترفیس بجای استفاده‌ی مستقیم از کلاس AppJwtConfiguration استفاده شده‌است.


بررسی کلاس OwinStartup

شروع به کار تنظیمات JWT و ورود آن‌ها به چرخه‌ی حیات Owin از کلاس OwinStartup آغاز می‌شود. در اینجا علت استفاده‌ی از SmObjectFactory.Container.GetInstance انجام تزریق وابستگی‌های لازم جهت کار با دو کلاس AppOAuthOptions و AppJwtOptions است.
- در کلاس AppOAuthOptions تنظیماتی مانند نحوه‌ی تهیه‌ی access token و همچنین refresh token ذکر می‌شوند.
- در کلاس AppJwtOptions تنظیمات فایل وب کانفیگ، مانند کلید مورد استفاده‌ی جهت امضای توکن‌های صادر شده، ذکر می‌شوند.


حداقل‌های بانک اطلاعاتی مورد نیاز جهت ذخیره سازی وضعیت کاربران و توکن‌های آن‌ها

همانطور که در ابتدای بحث عنوان شد، می‌خواهیم اگر سطوح دسترسی کاربر تغییر کرد و یا اگر کاربر logout کرد، توکن فعلی او صرفنظر از زمان انقضای آن، بلافاصله غیرقابل استفاده شود. به همین جهت نیاز است حداقل دو جدول زیر را در بانک اطلاعاتی تدارک ببینید:
الف) کلاس User
در کلاس User، بر مبنای اطلاعات خاصیت Roles آن است که ویژگی Authorize با ذکر نقش مثلا Admin کار می‌کند. بنابراین حداقل نقشی را که برای کاربران، در ابتدای کار نیاز است مشخص کنید، نقش user است.
همچنین خاصیت اضافه‌تری به نام SerialNumber نیز در اینجا درنظر گرفته شده‌است. این مورد را باید به صورت دستی مدیریت کنید. اگر کاربری کلمه‌ی عبورش را تغییر داد، اگر مدیری نقشی را به او انتساب داد یا از او گرفت و یا اگر کاربری غیرفعال شد، مقدار خاصیت و فیلد SerialNumber را با یک Guid جدید به روز رسانی کنید. این Guid در برنامه با Guid موجود در توکن مقایسه شده و بلافاصله سبب عدم دسترسی او خواهد شد (در صورت عدم تطابق).

ب) کلاس UserToken
در کلاس UserToken کار نگهداری ریز اطلاعات توکن‌های صادر شده صورت می‌گیرد. توکن‌های صادر شده دارای access token و refresh token هستند؛ به همراه زمان انقضای آن‌ها. به این ترتیب زمانیکه کاربری درخواستی را به سرور ارسال می‌کند، ابتدا token او را دریافت کرده و سپس بررسی می‌کنیم که آیا اصلا چنین توکنی در بانک اطلاعاتی ما وجود خارجی دارد یا خیر؟ آیا توسط ما صادر شده‌است یا خیر؟ اگر خیر، بلافاصله دسترسی او قطع خواهد شد. برای مثال عملیات logout را طوری طراحی می‌کنیم که تمام توکن‌های یک شخص را در بانک اطلاعاتی حذف کند. به این ترتیب توکن قبلی او دیگر قابلیت استفاده‌ی مجدد را نخواهد داشت.


مدیریت بانک اطلاعاتی و کلاس‌های سرویس برنامه

در لایه سرویس برنامه، شما سه سرویس را مشاهده خواهید کرد که قابلیت جایگزین شدن با کدهای یک ORM را دارند (نوع آن ORM مهم نیست):
الف) سرویس TokenStoreService
public interface ITokenStoreService
{
    void CreateUserToken(UserToken userToken);
    bool IsValidToken(string accessToken, int userId);
    void DeleteExpiredTokens();
    UserToken FindToken(string refreshTokenIdHash);
    void DeleteToken(string refreshTokenIdHash);
    void InvalidateUserTokens(int userId);
    void UpdateUserToken(int userId, string accessTokenHash);
}
کار سرویس TokenStore، ذخیره سازی و تعیین اعتبار توکن‌های صادر شده‌است. در اینجا ثبت یک توکن، بررسی صحت وجود یک توکن رسیده، حذف توکن‌های منقضی شده، یافتن یک توکن بر اساس هش توکن، حذف یک توکن بر اساس هش توکن، غیرمعتبر کردن و حذف تمام توکن‌های یک شخص و به روز رسانی توکن یک کاربر، پیش بینی شده‌اند.
پیاده سازی این کلاس بسیار شبیه است به پیاده سازی ORMهای موجود و فقط یک SaveChanges را کم دارد.

یک نکته:
در سرویس ذخیره سازی توکن‌ها، یک چنین متدی قابل مشاهده است:
public void CreateUserToken(UserToken userToken)
{
   InvalidateUserTokens(userToken.OwnerUserId);
   _tokens.Add(userToken);
}
استفاده از InvalidateUserTokens در اینجا سبب خواهد شد با لاگین شخص و یا صدور توکن جدیدی برای او، تمام توکن‌های قبلی او حذف شوند. به این ترتیب امکان لاگین چندباره و یا یافتن دسترسی به منابع محافظت شده‌ی برنامه در سرور توسط چندین نفر (که به توکن شخص دسترسی یافته‌اند یا حتی تقاضای توکن جدیدی کرده‌اند)، میسر نباشد. همینکه توکن جدیدی صادر شود، تمام لاگین‌های دیگر این شخص غیرمعتبر خواهند شد.


ب) سرویس UsersService
public interface IUsersService
{
    string GetSerialNumber(int userId);
    IEnumerable<string> GetUserRoles(int userId);
    User FindUser(string username, string password);
    User FindUser(int userId);
    void UpdateUserLastActivityDate(int userId);
}
از کلاس سرویس کاربران، برای یافتن شماره سریال یک کاربر استفاده می‌شود. در مورد شماره سریال پیشتر بحث کردیم و هدف آن مشخص سازی وضعیت تغییر این موجودیت است. اگر کاربری اطلاعاتش تغییر کرد، این فیلد باید با یک Guid جدید مقدار دهی شود.
همچنین متدهای دیگری برای یافتن یک کاربر بر اساس نام کاربری و کلمه‌ی عبور او (جهت مدیریت مرحله‌ی لاگین)، یافتن کاربر بر اساس Id او (جهت استخراج اطلاعات کاربر) و همچنین یک متد اختیاری نیز برای به روز رسانی فیلد آخرین تاریخ فعالیت کاربر در اینجا پیش بینی شده‌اند.

ج) سرویس SecurityService
public interface ISecurityService
{
   string GetSha256Hash(string input);
}
در قسمت‌های مختلف این برنامه، هش SHA256 مورد استفاده قرار گرفته‌است که با توجه به مباحث تزریق وابستگی‌ها، کاملا قابل تعویض بوده و برنامه صرفا با اینترفیس آن کار می‌کند.


پیاده سازی قسمت لاگین و صدور access token

در کلاس AppOAuthProvider کار پیاده سازی قسمت لاگین برنامه انجام شده‌است. این کلاسی است که توسط کلاس AppOAuthOptions به OwinStartup معرفی می‌شود. قسمت‌های مهم کلاس AppOAuthProvider به شرح زیر هستند:
برای درک عملکرد این کلاس، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمه‌ی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر می‌توانید درک کنید. کار آن با فراخوانی متد ValidateClientAuthentication شروع می‌شود. چون با یک برنامه‌ی وب در حال کار هستیم، ClientId آن‌را نال درنظر می‌گیریم و برای ما مهم نیست. اگر کلاینت ویندوزی خاصی را تدارک دیدید، این کلاینت می‌تواند ClientId ویژه‌ای را به سمت سرور ارسال کند که در اینجا مدنظر ما نیست.
مهم‌ترین قسمت این کلاس، متد GrantResourceOwnerCredentials است که پس از ValidateClientAuthentication بلافاصله فراخوانی می‌شود. اگر به کدهای آن دقت کنید،  خود owin دارای خاصیت‌های user name و password نیز هست.
این اطلاعات را به نحو ذیل از کلاینت خود دریافت می‌کند. اگر به فایل index.html مراجعه کنید، یک چنین تعریفی را برای متد login می‌توانید مشاهده کنید:
function doLogin() {
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            username: "Vahid",
            password: "1234",
            grant_type: "password"
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
url آن به همان مسیری که در فایل web.config تنظیم کردیم، اشاره می‌کند. فرمت data ایی که به سرور ارسال می‌شود، دقیقا باید به همین نحو باشد و content type آن نیز مهم است و owin فقط حالت form-urlencoded را پردازش می‌کند. به این ترتیب است که user name و password توسط owin قابل شناسایی شده و grant_type آن است که سبب فراخوانی GrantResourceOwnerCredentials می‌شود و مقدار آن نیز دقیقا باید password باشد (در حین لاگین).
در متد GrantResourceOwnerCredentials کار بررسی نام کاربری و کلمه‌ی عبور کاربر صورت گرفته و در صورت یافت شدن کاربر (صحیح بودن اطلاعات)، نقش‌های او نیز به عنوان Claim جدید به توکن اضافه می‌شوند.

در اینجا یک Claim سفارشی هم اضافه شده‌است:
 identity.AddClaim(new Claim(ClaimTypes.UserData, user.UserId.ToString()));
کار آن ذخیره سازی userId کاربر، در توکن صادر شده‌است. به این صورت هربار که توکن به سمت سرور ارسال می‌شود، نیازی نیست تا یکبار از بانک اطلاعاتی بر اساس نام او، کوئری گرفت و سپس id او را یافت. این id در توکن امضاء شده، موجود است. نمونه‌ی نحوه‌ی کار با آن‌را در کنترلرهای این API می‌توانید مشاهده کنید. برای مثال در MyProtectedAdminApiController این اطلاعات از توکن استخراج شده‌اند (جهت نمایش مفهوم).

در انتهای این کلاس، از متد TokenEndpointResponse جهت دسترسی به access token نهایی صادر شده‌ی برای کاربر، استفاده کرده‌ایم. هش این access token را در بانک اطلاعاتی ذخیره می‌کنیم (جستجوی هش‌ها سریعتر هستند از جستجوی یک رشته‌ی طولانی؛ به علاوه در صورت دسترسی به بانک اطلاعاتی، اطلاعات هش‌ها برای مهاجم قابل استفاده نیست).

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


در اینجا access_token همان JSON Web Token صادر شده‌است که برنامه‌ی کلاینت از آن برای اعتبارسنجی استفاده خواهد کرد.

بنابراین خلاصه‌ی مراحل لاگین در اینجا به ترتیب ذیل است:
- فراخوانی متد  ValidateClientAuthenticationدر کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد GrantResourceOwnerCredentials در کلاس AppOAuthProvider . در اینجا کار اصلی لاگین به همراه تنظیم Claims کاربر انجام می‌شود. برای مثال نقش‌های او به توکن صادر شده اضافه می‌شوند.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token را انجام می‌دهد.
- فراخوانی متد CreateAsync در کلاس RefreshTokenProvider. کار این متد صدور توکن ویژه‌ای به نام refresh است. این توکن را در بانک اطلاعاتی ذخیره خواهیم کرد. در اینجا چیزی که به سمت کلاینت ارسال می‌شود صرفا یک guid است و نه اصل refresh token.
- فرخوانی متد TokenEndpointResponse در کلاس AppOAuthProvider . از این متد جهت یافتن access token نهایی تولید شده و ثبت هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی قسمت صدور Refresh token

در تصویر فوق، خاصیت refresh_token را هم در شیء JSON ارسالی به سمت کاربر مشاهده می‌کنید. هدف از refresh_token، تمدید یک توکن است؛ بدون ارسال کلمه‌ی عبور و نام کاربری به سرور. در اینجا access token صادر شده، مطابق تنظیم expirationMinutes در فایل وب کانفیگ، منقضی خواهد شد. اما طول عمر refresh token را بیشتر از طول عمر access token در نظر می‌گیریم. بنابراین طول عمر یک access token کوتاه است. زمانیکه access token منقضی شد، نیازی نیست تا حتما کاربر را به صفحه‌ی لاگین هدایت کنیم. می‌توانیم refresh_token را به سمت سرور ارسال کرده و به این ترتیب درخواست صدور یک access token جدید را ارائه دهیم. این روش هم سریعتر است (کاربر متوجه این retry نخواهد شد) و هم امن‌تر است چون نیازی به ارسال کلمه‌ی عبور و نام کاربری به سمت سرور وجود ندارند.
سمت کاربر، برای درخواست صدور یک access token جدید بر اساس refresh token صادر شده‌ی در زمان لاگین، به صورت زیر عمل می‌شود:
function doRefreshToken() {
    // obtaining new tokens using the refresh_token should happen only if the id_token has expired.
    // it is a bad practice to call the endpoint to get a new token every time you do an API call.
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            grant_type: "refresh_token",
            refresh_token: refreshToken
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
در اینجا نحوه‌ی عملکرد، همانند متد login است. با این تفاوت که grant_type آن اینبار بجای password مساوی refresh_token است و مقداری که به عنوان refresh_token به سمت سرور ارسال می‌کند، همان مقداری است که در عملیات لاگین از سمت سرور دریافت کرده‌است. آدرس ارسال این درخواست نیز همان tokenPath تنظیم شده‌ی در فایل web.config است. بنابراین مراحلی که در اینجا طی خواهند شد، به ترتیب زیر است:
- فرخوانی متد ValidateClientAuthentication در کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد ReceiveAsync در کلاس RefreshTokenProvider. در قسمت توضیح مراحل لاگین، عنوان شد که پس از فراخوانی متد GrantResourceOwnerCredentials جهت لاگین، متد CreateAsync در کلاس RefreshTokenProvider فراخوانی می‌شود. اکنون در متد ReceiveAsync این refresh token ذخیره شده‌ی در بانک اطلاعاتی را یافته (بر اساس Guid ارسالی از طرف کلاینت) و سپس Deserialize می‌کنیم. به این ترتیب است که کار درخواست یک access token جدید بر مبنای refresh token موجود آغاز می‌شود.
- فراخوانی GrantRefreshToken در کلاس AppOAuthProvider . در اینجا اگر نیاز به تنظیم Claim اضافه‌تری وجود داشت، می‌توان اینکار را انجام داد.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token جدید را انجام می‌دهد.
- فراخوانی CreateAsync در کلاس RefreshTokenProvider . پس از اینکه context.DeserializeTicket در متد ReceiveAsync  بر مبنای refresh token قبلی انجام شد، مجددا کار تولید یک توکن جدید در متد CreateAsync شروع می‌شود و زمان انقضاءها تنظیم خواهند شد.
- فراخوانی TokenEndpointResponse در کلاس AppOAuthProvider . مجددا از این متد برای دسترسی به access token جدید و ذخیره‌ی هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute

در ابتدای بحث عنوان کردیم که اگر مشخصات کاربر تغییر کردند یا کاربر logout کرد، امکان غیرفعال کردن یک توکن را نداریم و این توکن تا زمان انقضای آن معتبر است. این نقیصه را با طراحی یک AuthorizeAttribute سفارشی جدید به نام JwtAuthorizeAttribute برطرف می‌کنیم. نکات مهم این فیلتر به شرح زیر هستند:
- در اینجا در ابتدا بررسی می‌شود که آیا درخواست رسیده‌ی به سرور، حاوی access token هست یا خیر؟ اگر خیر، کار همینجا به پایان می‌رسد و دسترسی کاربر قطع خواهد شد.
- سپس بررسی می‌کنیم که آیا درخواست رسیده پس از مدیریت توسط Owin، دارای Claims است یا خیر؟ اگر خیر، یعنی این توکن توسط ما صادر نشده‌است.
- در ادامه شماره سریال موجود در access token را استخراج کرده و آن‌را با نمونه‌ی موجود در دیتابیس مقایسه می‌کنیم. اگر این دو یکی نبودند، دسترسی کاربر قطع می‌شود.
- همچنین در آخر بررسی می‌کنیم که آیا هش این توکن رسیده، در بانک اطلاعاتی ما موجود است یا خیر؟ اگر خیر باز هم یعنی این توکن توسط ما صادر نشده‌است.

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

و نکته‌ی مهم اینجا است که از این پس بجای فیلتر معمولی Authorize، از فیلتر جدید JwtAuthorize در برنامه استفاده خواهیم کرد:
 [JwtAuthorize(Roles = "Admin")]
public class MyProtectedAdminApiController : ApiController


نحوه‌ی ارسال درخواست‌های Ajax ایی به همراه توکن صادر شده

تا اینجا کار صدور توکن‌های برنامه به پایان می‌رسد. برای استفاده‌ی از این توکن‌ها در سمت کاربر، به فایل index.html دقت کنید. در متد doLogin، پس از موفقیت عملیات دو متغیر جدید مقدار دهی می‌شوند:
var jwtToken;
var refreshToken;
 
function doLogin() {
    $.ajax({
     // same as before
    }).then(function (response) {
        jwtToken = response.access_token;
        refreshToken = response.refresh_token; 
    }
از متغیر jwtToken به ازای هربار درخواستی به یکی از کنترلرهای سایت، استفاده می‌کنیم و از متغیر refreshToken در متد doRefreshToken برای درخواست یک access token جدید. برای مثال اینبار برای اعتبارسنجی درخواست ارسالی به سمت سرور، نیاز است خاصیت headers را به نحو ذیل مقدار دهی کرد:
function doCallApi() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/MyProtectedApi",
        type: 'GET'
    }).then(function (response) {
بنابراین هر درخواست ارسالی به سرور باید دارای هدر ویژه‌ی Bearer فوق باشد؛ در غیراینصورت با پیام خطای 401، از دسترسی به منابع سرور منع می‌شود.


پیاده سازی logout سمت سرور و کلاینت

پیاده سازی سمت سرور logout را در کنترلر UserController مشاهده می‌کنید. در اینجا در اکشن متد Logout، کار حذف توکن‌های کاربر از بانک اطلاعاتی انجام می‌شود. به این ترتیب دیگر مهم نیست که توکن او هنوز منقضی شده‌است یا خیر. چون هش آن دیگر در جدول توکن‌ها وجود ندارد، از فیلتر JwtAuthorizeAttribute رد نخواهد شد.
سمت کلاینت آن نیز در فایل index.html ذکر شده‌است:
function doLogout() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/user/logout",
        type: 'GET'
تنها کاری که در اینجا انجام شده، فراخوانی اکشن متد logout سمت سرور است. البته ذکر jwtToken نیز در اینجا الزامی است. زیرا این توکن است که حاوی اطلاعاتی مانند userId کاربر فعلی است و بر این اساس است که می‌توان رکوردهای او را در جدول توکن‌ها یافت و حذف کرد.


بررسی تنظیمات IoC Container برنامه

تنظیمات IoC Container برنامه را در پوشه‌ی IoCConfig می‌توانید ملاحظه کنید. از کلاس SmWebApiControllerActivator برای فعال سازی تزریق وابستگی‌ها در کنترلرهای Web API استفاده می‌شود و از کلاس SmWebApiFilterProvider برای فعال سازی تزریق وابستگی‌ها در فیلتر سفارشی که ایجاد کردیم، کمک گرفته خواهد شد.
هر دوی این تنظیمات نیز در کلاس WebApiConfig ثبت و معرفی شده‌اند.
به علاوه در کلاس SmObjectFactory، کار معرفی وهله‌های مورد استفاده و تنظیم طول عمر آن‌ها انجام شده‌است. برای مثال طول عمر IOAuthAuthorizationServerProvider از نوع Singleton است؛ چون تنها یک وهله از AppOAuthProvider در طول عمر برنامه توسط Owin استفاده می‌شود و Owin هربار آن‌را وهله سازی نمی‌کند. همین مساله سبب شده‌است که معرفی وابستگی‌ها در سازنده‌ی کلاس AppOAuthProvider کمی با حالات متداول، متفاوت باشند:
public AppOAuthProvider(
   Func<IUsersService> usersService,
   Func<ITokenStoreService> tokenStoreService,
   ISecurityService securityService,
   IAppJwtConfiguration configuration)
در کلاسی که طول عمر singleton دارد، وابستگی‌های تعریف شده‌ی در سازنده‌ی آن هم تنها یکبار در طول عمر برنامه نمونه سازی می‌شوند. برای رفع این مشکل و transient کردن آن‌ها، می‌توان از Func استفاده کرد. به این ترتیب هر بار ارجاهی به usersService، سبب وهله سازی مجدد آن می‌شود و این مساله برای کار با سرویس‌هایی که قرار است با بانک اطلاعاتی کار کنند ضروری است؛ چون باید طول عمر کوتاهی داشته باشند.
در اینجا سرویس IAppJwtConfiguration  با Func معرفی نشده‌است؛ چون طول عمر تنظیمات خوانده شده‌ی از Web.config نیز Singleton هستند و معرفی آن به همین نحو صحیح است.
اگر علاقمند بودید که بررسی کنید یک سرویس چندبار وهله سازی می‌شود، یک سازنده‌ی خالی را به آن اضافه کنید و سپس یک break point را بر روی آن قرار دهید و برنامه را اجرا و در این حالت چندبار Login کنید.
مطالب
طراحی و پیاده سازی زیرساختی برای مدیریت خطاهای حاصل از Business Rule Validationها در ServiceLayer
بعد از انتشار مطلب «Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها»، بخصوص بخش نظرات آن و همچنین R&D در ارتباط با موضوع مورد بحث، در نهایت قصد دارم نتایج بدست آماده را به اشتراک بگذارم.

پیش نیازها
در بخش نهایی مطلب «Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها » پیشنهادی را برای استفاده از استثناءها برای bubble up کردن یکسری پیغام از داخلی‌ترین یا پایین‌ترین لایه، تا لایه Presentation، ارائه دادیم:
استفاده از Exception برای نمایش پیغام برای کاربر نهایی 
با صدور یک استثناء و مدیریت سراسری آن در بالاترین (خارجی ترین) لایه و نمایش پیغام مرتبط با آن به کاربر نهایی، می‌توان از آن به عنوان ابزاری برای ارسال هر نوع پیغامی به کاربر نهایی استفاده کرد. اگر قوانین تجاری با موفقیت برآورده نشده‌اند یا لازم است به هر دلیلی یک پیغام مرتبط با یک اعتبارسنجی تجاری را برای کاربر نمایش دهید، این روش بسیار کارساز می‌باشد و با یکبار وقت گذاشتن برای توسعه زیرساخت برای این موضوع، به عنوان یک Cross Cutting Concern تحت عنوان Exception Management، آزادی عمل زیادی در ادامه توسعه سیستم خود خواهید داشت. 

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

راه حل صحیح برای برخورد با این سناریوها بازگشت یک Result می‌باشد که در مطلب قبلی هم تحت عنوان OperationResult مطرح شد. 


کلاس Result
    public class Result
    {
        private static readonly Result SuccessResult = new Result(true, null);

        protected Result(bool succeeded, string message)
        {
            if (succeeded)
            {
                if (message != null)
                    throw new ArgumentException("There should be no error message for success.", nameof(message));
            }
            else
            {
                if (message == null)
                    throw new ArgumentNullException(nameof(message), "There must be error message for failure.");
            }

            Succeeded = succeeded;
            Error = message;
        }

        public bool Succeeded { get; }
        public string Error { get; }

        [DebuggerStepThrough]
        public static Result Success()
        {
            return SuccessResult;
        }

        [DebuggerStepThrough]
        public static Result Failed(string message)
        {
            return new Result(false, message);
        }

        [DebuggerStepThrough]
        public static Result<T> Failed<T>(string message)
        {
            return new Result<T>(default, false, message);
        }

        [DebuggerStepThrough]
        public static Result<T> Success<T>(T value)
        {
            return new Result<T>(value, true, string.Empty);
        }

        [DebuggerStepThrough]
        public static Result Combine(string seperator, params Result[] results)
        {
            var failedResults = results.Where(x => !x.Succeeded).ToList();

            if (!failedResults.Any())
                return Success();

            var error = string.Join(seperator, failedResults.Select(x => x.Error).ToArray());
            return Failed(error);
        }

        [DebuggerStepThrough]
        public static Result Combine(params Result[] results)
        {
            return Combine(", ", results);
        }

        [DebuggerStepThrough]
        public static Result Combine<T>(params Result<T>[] results)
        {
            return Combine(", ", results);
        }

        [DebuggerStepThrough]
        public static Result Combine<T>(string seperator, params Result<T>[] results)
        {
            var untyped = results.Select(result => (Result) result).ToArray();
            return Combine(seperator, untyped);
        }

        public override string ToString()
        {
            return Succeeded
                ? "Succeeded"
                : $"Failed : {Error}";
        }
    }

مشابه کلاس بالا، در فریمورک ASP.NET Identity کلاسی تحت عنوان IdentityResult برای همین منظور در نظر گرفته شده‌است.

پراپرتی Succeeded نشان دهنده موفقت آمیز بودن یا عدم موفقیت عملیات (به عنوان مثال یک متد ApplicationService) می‌باشد. پراپرتی Error دربرگیرنده پیغام خطایی می‌باشد که قبلا از طریق Message مربوط به یک استثناء صادر شده، در اختیار بالاترین لایه قرار می‌گرفت. با استفاده از متد Combine، امکان ترکیب چندین Result حاصل از عملیات مختلف را خواهید داشت. متدهای استاتیک Failed و Success هم برای درگیر نشدن برای وهله سازی از کلاس Result در نظر گرفته شده‌اند.

متد GetForEdit مربوط به MeetingService را در نظر بگیرید. به عنوان مثال وظیفه این متد بازگشت یک MeetingEditModel می‌باشد؛ اما با توجه به یکسری قواعد تجاری، به‌عنوان مثال «امکان ویرایش جلسه‌ای که پابلیش نهایی شده‌است، وجود ندارد و ...» لازم است خروجی این متد نیز در صورت Fail شدن، دلیل آن را به مصرف کننده ارائه دهد. از این رو کلاس جنریک Result را به شکل زیر خواهیم داشت:

    public class Result<T> : Result
    {
        private readonly T _value;

        protected internal Result(T value, bool succeeded, string error)
            : base(succeeded, error)
        {
            _value = value;
        }

        public T Value
        {
            get
            {
                if (!Succeeded)
                    throw new InvalidOperationException("There is no value for failure.");

                return _value;
            }
        }
    }
حال با استفاده از کلاس بالا امکان مهیا کردن خروجی به همراه نتیجه اجرای متد را خواهیم داشت.
در ادامه با استفاده از تعدادی متد الحاقی بر فراز کلاس Result، روش Railway-oriented Programming را که یکی از روش‌های برنامه نویسی تابعی برای مدیریت خطاها است، در سی شارپ اعمال خواهیم کرد. 
    public static class ResultExtensions
    {
        public static Result<TK> OnSuccess<T, TK>(this Result<T> result, Func<T, TK> func)
        {
            return !result.Succeeded ? Result.Failed<TK>(result.Error) : Result.Success(func(result.Value));
        }

        public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, string message)
        {
            if (!result.Succeeded)
                return Result.Failed<T>(result.Error);

            return !predicate(result.Value) ? Result.Failed<T>(message) : Result.Success(result.Value);
        }

        public static Result<TK> Map<T, TK>(this Result<T> result, Func<T, TK> func)
        {
            return !result.Succeeded ? Result.Failed<TK>(result.Error) : Result.Success(func(result.Value));
        }

        public static Result<T> OnSuccess<T>(this Result<T> result, Action<T> action)
        {
            if (result.Succeeded) action(result.Value);

            return result;
        }

        public static T OnBoth<T>(this Result result, Func<Result, T> func)
        {
            return func(result);
        }

        public static Result OnSuccess(this Result result, Action action)
        {
            if (result.Succeeded) action();

            return result;
        }

        public static Result<T> OnSuccess<T>(this Result result, Func<T> func)
        {
            return !result.Succeeded ? Result.Failed<T>(result.Error) : Result.Success(func());
        }

        public static Result<TK> OnSuccess<T, TK>(this Result<T> result, Func<T, Result<TK>> func)
        {
            return !result.Succeeded ? Result.Failed<TK>(result.Error) : func(result.Value);
        }

        public static Result<T> OnSuccess<T>(this Result result, Func<Result<T>> func)
        {
            return !result.Succeeded ? Result.Failed<T>(result.Error) : func();
        }

        public static Result<TK> OnSuccess<T, TK>(this Result<T> result, Func<Result<TK>> func)
        {
            return !result.Succeeded ? Result.Failed<TK>(result.Error) : func();
        }

        public static Result OnSuccess<T>(this Result<T> result, Func<T, Result> func)
        {
            return !result.Succeeded ? Result.Failed(result.Error) : func(result.Value);
        }

        public static Result OnSuccess(this Result result, Func<Result> func)
        {
            return !result.Succeeded ? result : func();
        }

        public static Result Ensure(this Result result, Func<bool> predicate, string message)
        {
            if (!result.Succeeded)
                return Result.Failed(result.Error);

            return !predicate() ? Result.Failed(message) : Result.Success();
        }

        public static Result<T> Map<T>(this Result result, Func<T> func)
        {
            return !result.Succeeded ? Result.Failed<T>(result.Error) : Result.Success(func());
        }


        public static TK OnBoth<T, TK>(this Result<T> result, Func<Result<T>, TK> func)
        {
            return func(result);
        }

        public static Result<T> OnFailure<T>(this Result<T> result, Action action)
        {
            if (!result.Succeeded) action();

            return result;
        }

        public static Result OnFailure(this Result result, Action action)
        {
            if (!result.Succeeded) action();

            return result;
        }

        public static Result<T> OnFailure<T>(this Result<T> result, Action<string> action)
        {
            if (!result.Succeeded) action(result.Error);

            return result;
        }

        public static Result OnFailure(this Result result, Action<string> action)
        {
            if (!result.Succeeded) action(result.Error);

            return result;
        }
    }
OnSuccess برای انجام عملیاتی در صورت موفقیت آمیز بودن نتیجه یک متد، OnFailed برای انجام عملیاتی در صورت عدم موفقت آمیز بودن نتیجه یک متد و OnBoth در هر صورت، عملیات مورد نظر شما را اجرا خواهد کرد. به عنوان مثال:
[HttpPost, AjaxOnly, ValidateAntiForgeryToken, ValidateModelState]
public virtual async Task<ActionResult> Create([Bind(Prefix = "Model")]MeetingCreateModel model)
{
    var result = await _service.CreateAsync(model);

    return result.OnSuccess(() => { })
                 .OnFailure(() => { })
                 .OnBoth(r => r.Succeeded ? InformationNotification("Messages.Save.Success") : ErrorMessage(r.Error));

}

یا در حالت‌های پیچیده تر:

var result = await _service.CreateAsync(new TenantAwareEntityCreateModel());

return Result.Combine(result, Result.Success(), Result.Failed("نتیجه یک متد دیگر به عنوان مثال"))
    .OnSuccess(() => { })
    .OnFailure(() => { })
    .OnBoth(r => r.Succeeded ? Json("OK") : Json(r.Error));


ترکیب با الگوی Maybe یا Option

قبلا مطلبی در رابطه با الگوی Maybe در سایت منتشر شده‌است. در نظرات آن مطلب، یک پیاده سازی به شکل زیر مطرح کردیم:
    public struct Maybe<T> : IEquatable<Maybe<T>>
        where T : class
    {
        private readonly T _value;

        private Maybe(T value)
        {
            _value = value;
        }

        public bool HasValue => _value != null;
        public T Value => _value ?? throw new InvalidOperationException();
        public static Maybe<T> None => new Maybe<T>();


        public static implicit operator Maybe<T>(T value)
        {
            return new Maybe<T>(value);
        }

        public static bool operator ==(Maybe<T> maybe, T value)
        {
            return maybe.HasValue && maybe.Value.Equals(value);
        }

        public static bool operator !=(Maybe<T> maybe, T value)
        {
            return !(maybe == value);
        }

        public static bool operator ==(Maybe<T> left, Maybe<T> right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Maybe<T> left, Maybe<T> right)
        {
            return !(left == right);
        }

        /// <inheritdoc />
        /// <summary>
        ///     Avoid boxing and Give type safety
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public bool Equals(Maybe<T> other)
        {
            if (!HasValue && !other.HasValue)
                return true;

            if (!HasValue || !other.HasValue)
                return false;

            return _value.Equals(other.Value);
        }

        /// <summary>
        ///     Avoid reflection
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            if (obj is T typed)
            {
                obj = new Maybe<T>(typed);
            }

            if (!(obj is Maybe<T> other)) return false;

            return Equals(other);
        }

        /// <summary>
        ///     Good practice when overriding Equals method.
        ///     If x.Equals(y) then we must have x.GetHashCode()==y.GetHashCode()
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return HasValue ? _value.GetHashCode() : 0;
        }

        public override string ToString()
        {
            return HasValue ? _value.ToString() : "NO VALUE";
        }
    }

متد الحاقی زیر را در نظر بگیرید:
public static Result<T> ToResult<T>(this Maybe<T> maybe, string message)
    where T : class
{
    return !maybe.HasValue ? Result.Failed<T>(message) : Result.Success(maybe.Value);
}

فرض کنید خروجی متدی که در لایه سرویس مورد استفاده قرار می‌گیرد، Maybe باشد. در این حالت می‌توان با متد الحاقی بالا آن را به یک Result تبدیل کرد و در اختیار لایه بالاتر قرار داد. 
Result<Customer> customerResult = _customerRepository.GetById(model.Id)
    .ToResult("Customer with such Id is not found: " + model.Id);

همچنین متدهای الحاقی زیر را نیز برای ساختار داده Maybe می‌توان در نظر گرفت:

        public static T GetValueOrDefault<T>(this Maybe<T> maybe, T defaultValue = default)
            where T : class
        {
            return maybe.GetValueOrDefault(x => x, defaultValue);
        }

        public static TK GetValueOrDefault<T, TK>(this Maybe<T> maybe, Func<T, TK> selector, TK defaultValue = default)
            where T : class
        {
            return maybe.HasValue ? selector(maybe.Value) : defaultValue;
        }

        public static Maybe<T> Where<T>(this Maybe<T> maybe, Func<T, bool> predicate)
            where T : class
        {
            if (!maybe.HasValue)
                return default(T);

            return predicate(maybe.Value) ? maybe : default(T);
        }

        public static Maybe<TK> Select<T, TK>(this Maybe<T> maybe, Func<T, TK> selector)
            where T : class
            where TK : class
        {
            return !maybe.HasValue ? default : selector(maybe.Value);
        }

        public static Maybe<TK> Select<T, TK>(this Maybe<T> maybe, Func<T, Maybe<TK>> selector)
            where T : class
            where TK : class
        {
            return !maybe.HasValue ? default(TK) : selector(maybe.Value);
        }

        public static void Execute<T>(this Maybe<T> maybe, Action<T> action)
            where T : class
        {
            if (!maybe.HasValue)
                return;

            action(maybe.Value);
        }
    }

پیشنهادات
  • استفاده از الگوی Specification برای زمانیکه منطقی قرار است هم برای اعتبارسنجی درون حافظه‌ای استفاده شود و همچنین برای اعمال فیلتر برای واکشی داده‌ها؛ در واقع دو Use-case استفاده از این الگو حداقل یکجا وجود داشته باشد. استفاده از این مورد برای Domain Validation در سناریوهای پیچیده بسیار پیشنهاد می‌شود.
  • استفاده از Domain Eventها برای اعمال اعتبارسنجی‌های مرتبط با قواعد تجاری تنها در شرایط inter-application communication و در شرایط inner-application communication به صورت صریح، اعتبارسنجی‌های مرتبط با قواعد تجاری را در جریان اصلی برنامه پیاده سازی کنید. 

با تشکر از آقای «محسن خان»
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 19 - بومی سازی
- کار رندر اطلاعات در ASP.NET MVC از اکشن متد یک کنترلر شروع می‌شود و به View ختم خواهد شد. سفارشی سازی آن توسط اکشن‌فیلترها میسر است:
    public class FaLanguageActionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            CultureInfo.CurrentCulture = new CultureInfo("fa-IR");
            CultureInfo.CurrentUICulture = new CultureInfo("fa-IR");
            base.OnActionExecuting(context);
        }
    }
برای توضیحات بیشتر و نحو‌ه‌ی ثبت سراسری آن‌ها مراجعه کنید به مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 20 - بررسی تغییرات فیلترها»
+ برای تنظیم اطلاعات تکراری Viewها می‌توان کلاس پایه آن‌ها را نیز سفارشی سازی کرد:
    public abstract class MyCustomBaseView<TModel> : RazorPage<TModel>
    {
        protected MyCustomBaseView()
        {
            CultureInfo.CurrentCulture = new CultureInfo("fa-IR");
            CultureInfo.CurrentUICulture = new CultureInfo("fa-IR");
        }
یک مثال تکمیلی
مطالب
استفاده از فایل Json برای ذخیره و بازیابی تنظیمات برنامه
قطعا شرایطی پیش خواهد آمد که شما مجبور شوید داده‌هایی را به عنوان تنظیمات برنامه در محلی ذخیره کنید و مجددا آن‌ها را فراخوانی کنید. روش‌های مختلفی برای این کار وجود دارند که معروف‌ترین و ساده‌ترین راه، استفاده از Settings خود پروژه می‌باشد. اما این به منزله بهترین راه نیست! در این مطلب قصد داریم تنظیمات برنامه را در یک فایل json، با همان ساختار استانداردش ذخیره و بازیابی کنیم.
برای اینکار نیاز به سریالایز و دیسریالایز کردن مدل داریم. اگر از دات نت کور استفاده میکنید، کتابخانه توکار جیسون، در فضای نام System.Text.Json از عهده این کار بر میاد و اگر از نسخه‌های دات نت فریمورک استفاده میکنید، باید پکیج newtonsoft.json را نصب کنید.
برای شروع یک کلاس را به نام GlobalData (یا هر نام دلخواه دیگری) ایجاد کنید.
چون قرار هست این کلاس هر نوع مدلی را برای ما سریالایز و دیسریالایز کند، پس کلاس را بصورت جنریک تعریف کنید.
public abstract class GlobalData<T> where T : GlobalData<T>, new()
حالا برای دسترسی به این کلاس، یک متغیر جنریک را به نام Config ایجاد میکنیم:
public static T Config { get; set; }
همینطور برای خواندن یک فایل جیسون از محلی مشخص، یک متغیر دیگر را به نام filename ایجاد میکنیم:
private static string _filename { get; set; }
در این کلاس به 2 متد نیاز داریم. متد اول برای دیسریالایز کردن فایل جیسون و متد دوم برای سریالایز کردن اطلاعات:
public static void Init(string FileName = "AppConfig.json")
        {
            _filename = FileName;
            if (File.Exists(FileName))
            {
                string json = File.ReadAllText(FileName);

                Config = (string.IsNullOrEmpty(json) ? new T() : JsonSerializer.Deserialize<T>(json)) ?? new T();
            }
            else
            {
                Config = new T();
            }
        }
بصورت پیشفرض محل خواندن فایل جیسون را در کنار فایل اجرایی exe و با نام AppConfig.json در نظر میگیریم. در صورتی که فایل ما موجود بود، به کمک ReadAllText محتوای فایل جیسون را میخوانیم و در صورتی که خالی نبود، اقدام به دیسریالایز کردن آن میکنیم.
در متد Save نیز:
public static void Save()
        {
            JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true, IgnoreNullValues = true };

            string json = JsonSerializer.Serialize(Config, options);
            File.WriteAllText(_filename, json);
        }
پراپرتی Config را که شامل اطلاعات ما میباشد، در محل موردنظر سریالایز میکنیم.
حالا به سراغ پروژه‌ی دمو می‌رویم. یک کلاس را ایجاد کرده و از کلاس GlobalData ارث بری میکنیم:
internal class AppConfig : GlobalData<AppConfig>
حالا پراپرتی‌های دلخواه خود را ایجاد میکنیم:
 public string ServerUrl { get; set; } = "https://sub.deltaleech.com";
 public bool IsShowNotification { get; set; } = true;
 public NavigationViewPaneDisplayMode PaneDisplayMode { get; set; } = NavigationViewPaneDisplayMode.Left;

 public SkinType Skin { get; set; } = SkinType.Default;
دقت کنید که قبل از خواندن تنظیمات، باید ابتدا فایل تنظیمات را دیسریالایز کرده باشید. پس هنگام اجرای پروژه، متد Init را فراخوانی کنید:
protected override void OnStartup(StartupEventArgs e) {
GlobalData<AppConfig>.Init();
}
برای خواندن تنظیمات به این صورت عمل کنید:
var skin = GlobalData<AppConfig>.Config.Skin;
و برای ذخیره کردن :
GlobalData<AppConfig>.Config.Skin = Skin.Dark;
GlobalData<AppConfig>.Save();

دقت داشته باشید که اگر بعد از ذخیره کردن تنظیمات، قصد داشته باشید اطلاعات جدید را دریافت کنید، باید حتما قبل از دریافت اطلاعات، متد Init را یکبار دیگر فراخوانی کنید تا اطلاعات جدید، نمایش داده شود.

مطالب دوره‌ها
مدیریت استثناءها در حین استفاده از واژه‌های کلیدی async و await
زمانیکه یک متد async، یک Task یا Task of T (نسخه‌ی جنریک Task) را باز می‌گرداند، کامپایلر سی‌شارپ به صورت خودکار تمام استثناءهای رخ داده درون متد را دریافت کرده و از آن برای تغییر حالت Task به اصطلاحا faulted state استفاده می‌کند. همچنین زمانیکه از واژه‌ی کلیدی await استفاده می‌شود، کدهایی که توسط کامپایلر تولید می‌شوند، عملا مباحث Continue موجود در TPL یا Task parallel library معرفی شده در دات نت 4 را پیاده سازی می‌کنند و نهایتا نتیجه‌ی Task را در صورت وجود، دریافت می‌کند. زمانیکه نتیجه‌ی یک Task مورد استفاده قرار می‌گیرد، اگر استثنایی وجود داشته باشد، مجددا صادر خواهد شد. برای مثال اگر خروجی یک متد async از نوع Task of T باشد، امکان استفاده از خاصیتی به نام Result نیز برای دسترسی به نتیجه‌ی آن وجود دارد:
using System.Threading.Tasks;

namespace Async05
{
    class Program
    {
        static void Main(string[] args)
        {
            var res = doSomethingAsync().Result;
        }

        static async Task<int> doSomethingAsync()
        {
            await Task.Delay(1);
            return 1;
        }
    }
}
در این مثال یکی از روش‌های استفاده از متدهای async را در یک برنامه‌ی کنسول مشاهده می‌کنید. هر چند خروجی متد doSomethingAsync از نوع Task of int است، اما مستقیما یک int بازگشت داده شده است. تبدیلات نهایی در اینجا توسط کامپایلر انجام می‌شود. همچنین نحوه‌ی استفاده از خاصیت Result را نیز در متد Main مشاهده می‌کنید.
البته باید دقت داشت، زمانیکه از خاصیت Result استفاده می‌شود، این متد همزمان عمل خواهد کرد و نه غیرهمزمان (ترد جاری را بلاک می‌کند؛ یکی از موارد مجاز استفاده از آن در متد Main برنامه‌های کنسول است). همچنین اگر در متد doSomethingAsync استثنایی رخ داده باشد، این استثناء زمان استفاده از Result، به صورت یک AggregateException مجددا صادر خواهد شد. وجود کلمه‌ی Aggregate در اینجا به علت امکان استفاده‌ی تجمعی و ترکیب چندین Task باهم و داشتن چندین شکست و استثنای ممکن است.
همچنین اگر از کلمه‌ی کلیدی await بر روی یک faulted task استفاده کنیم، AggregateException صادر نمی‌شود. در این حالت کامپایلر AggregateException را بررسی کرده و آن‌را تبدیل به یک Exception متداول و معمول کدهای دات نت می‌کند. به عبارتی سعی شده‌است در این حالت، رفتار کدهای async را شبیه به رفتار کدهای متداول همزمان شبیه سازی کنند.


یک مثال

در اینجا توسط متد getTitleAsync، اطلاعات یک صفحه‌ی وب به صورت async دریافت شده و سپس عنوان آن استخراج می‌شود. در متد showTitlesAsync نیز از آن استفاده شده و در طی یک حلقه، چندین وب سایت مورد بررسی قرار خواهند گرفت. چون متد getTitleAsync از نوع async تعریف شده‌است، فراخوان آن نیز باید async تعریف شود تا بتوان از واژه‌ی کلیدی  await برای کار با آن استفاده کرد.
نهایتا در متد Main برنامه، وظیفه‌ی غیرهمزمان showTitlesAsync اجرا شده و تا پایان عملیات آن صبر می‌شود. چون خروجی آن از نوع Task است و نه Task of T، در اینجا دیگر خاصیت Result قابل دسترسی نیست. متد Wait نیز ترد جاری را همانند خاصیت Result بلاک می‌کند.
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Async05
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = showTitlesAsync(new[]
            {
                "http://www.google.com",
                "https://www.dntips.ir"
            });
            task.Wait();

            Console.WriteLine();
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }

        static async Task showTitlesAsync(IEnumerable<string> urls)
        {
            foreach (var url in urls)
            {
                var title = await getTitleAsync(url);
                Console.WriteLine(title);
            }
        }

        static async Task<string> getTitleAsync(string url)
        {
            var data = await new WebClient().DownloadStringTaskAsync(url);
            return getTitle(data);
        }

        private static string getTitle(string data)
        {
            const string patternTitle = @"(?s)<title>(.+?)</title>";
            var regex = new Regex(patternTitle);
            var mc = regex.Match(data);
            return mc.Groups.Count == 2 ? mc.Groups[1].Value.Trim() : string.Empty;
        }
    }
}
کلیه عملیات مبتنی برشبکه، همیشه مستعد به بروز خطا هستند. قطعی ارتباط یا حتی کندی آن می‌توانند سبب بروز استثناء شوند.
برنامه را در حالت عدم اتصال به اینترنت اجرا کنید. استثنای صادر شده، در متد task.Wait ظاهر می‌شود (چون متدهای async ترد جاری را خالی کرده‌اند):


و اگر در اینجا بر روی لینک View details کلیک کنیم، در inner exception حاصل، خطای واقعی قابل مشاهده است:


همانطور که ملاحظه می‌کنید، استثنای صادر شده از نوع System.AggregateException است. به این معنا که می‌تواند حاوی چندین استثناء باشد که در اینجا تعداد آن‌ها با عدد یک مشخص شده‌است. بنابراین در این حالات، بررسی inner exception را فراموش نکنید.

در ادامه داخل حلقه‌ی foreach متد showTitlesAsync، یک try/catch قرار می‌دهیم:
        static async Task showTitlesAsync(IEnumerable<string> urls)
        {
            foreach (var url in urls)
            {
                try
                {
                    var title = await getTitleAsync(url);
                    Console.WriteLine(title);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
        }
اینبار اگر برنامه را اجرا کنیم، خروجی ذیل را در صفحه می‌توان مشاهده کرد:
 System.Net.WebException: The remote server returned an error: (502) Bad Gateway.
System.Net.WebException: The remote server returned an error: (502) Bad Gateway.

Press any key to exit...
در اینجا دیگر خبری از AggregateException نبوده و استثنای واقعی رخ داده در متد await شده بازگشت داده شده‌است. کار واژه‌ی کلیدی await در اینجا، بررسی استثنای رخ داده در متد async فراخوانی شده و بازگشت آن به جریان متداول متد جاری است؛ تا نتیجه‌ی عملیات همانند یک کد کامل همزمان به نظر برسد. به این ترتیب کامپایلر توانسته است رفتار بروز استثناءها را در کدهای همزمان و غیرهمزمان یک دست کند. دقیقا مانند حالتی که یک متد معمولی در این بین فراخوانی شده و استثنایی در آن رخ داده‌است.


مدیریت تمام inner exceptionهای رخ داده در پردازش‌های موازی

همانطور که عنوان شد، await تنها یک استثنای حاصل از Task در حال اجرا را به کد فراخوان بازگشت می‌دهد. در این حالت اگر این Task، چندین شکست را گزارش دهد، چطور باید برای دریافت تمام آن‌ها اقدام کرد؟ برای مثال استفاده از Task.WhenAll می‌تواند شامل چندین استثنای حاصل از چندین Task باشد، ولی await تنها اولین استثنای دریافتی را بازگشت می‌دهد. اما اگر از خاصیتی مانند Result یا متد Wait استفاده شود، یک AggregateException حاصل تمام استثناءها را دریافت خواهیم کرد. بنابراین هرچند await تنها اولین استثنای دریافتی را بازگشت می‌دهد، اما می‌توان به Taskهای مرتبط مراجعه کرد و سپس بررسی نمود که آیا استثناهای دیگری نیز وجود دارند یا خیر؟
برای نمونه در مثال فوق، حلقه‌ی foreach تشکیل شده آنچنان بهینه نیست. از این جهت که هر بار تنها یک سایت را بررسی می‌کند، بجای اینکه مانند مرورگرها چندین ترد را به یک یا چند سایت باز کرده و نتایج را دریافت کند.
البته انجام کارها به صورت موازی همیشه ایده‌ی خوبی نیست ولی حداقل در این حالت خاص که با یک یا چند سرور راه دور کار می‌کنیم، درخواست‌های همزمان دریافت اطلاعات، سبب کارآیی بهتر برنامه و بالا رفتن سرعت اجرای آن می‌شوند. اما مثلا در حالتیکه با سخت دیسک سیستم کار می‌کنیم، اجرای موازی کارها نه تنها کمکی نخواهد کرد، بلکه سبب خواهد شد تا مدام drive head در مکان‌های مختلفی مشغول به حرکت شده و در نتیجه کارآیی آن کاهش یابد.
برای ترکیب چندین Task، ویژگی خاصی به زبان سی‌شارپ اضافه نشده‌، زیرا نیازی نبوده است. برای این حالت تنها کافی است از متد Task.WhenAll، برای ساخت یک Task مرکب استفاده کرد. سپس می‌توان واژه‌ی کلیدی await را بر روی این Task مرکب فراخوانی کرد.
همچنین می‌توان از متد ContinueWith یک Task مرکب نیز برای جلوگیری از بازگشت صرفا اولین استثنای رخ داده توسط کامپایلر، استفاده کرد. در این حالت امکان دسترسی به خاصیت Result آن به سادگی میسر می‌شود که حاوی AggregateException کاملی است.


اعتبارسنجی آرگومان‌های ارسالی به یک متد async

زمان اعتبارسنجی آرگومان‌های ارسالی به متدهای async مهم است. بعضی از مقادیر را نمی‌توان بلافاصله اعتبارسنجی کرد؛ مانند مقادیری که نباید نال باشند. تعدادی دیگر نیز پس از انجام یک Task زمانبر مشخص می‌شوند که معتبر بوده‌اند یا خیر. همچنین فراخوان‌های این متدها انتظار دارند که متدهای async بلافاصله بازگشت داده شده و ترد جاری را خالی کنند. بنابراین اعتبارسنجی‌های آن‌ها باید با تاخیر انجام شود. در این حالات، دو نوع استثنای آنی و به تاخیر افتاده را شاهد خواهیم بود. استثنای آنی زمان شروع به کار متد صادر می‌شود و استثنای به تاخیر افتاده در حین دریافت نتایج از آن دریافت می‌گردد. باید دقت داشت کلیه استثناهای صادر شده در بدنه‌ی یک متد async، توسط کامپایلر به عنوان یک استثنای به تاخیر افتاده گزارش داده می‌شود. بنابراین اعتبارسنجی‌های آرگومان‌ها را بهتر است در یک متد سطح بالای غیر async انجام داد تا بلافاصله بتوان استثناءهای حاصل را دریافت نمود.


از دست دادن استثناءها

فرض کنید مانند مثال قسمت قبل، دو وظیفه‌ی async آغاز شده و نتیجه‌ی آن‌ها پس از await هر یک، با هم جمع زده می‌شوند. در این حالت اگر کل عملیات را داخل یک قطعه کد try/catch قرار دهیم، اولین await ایی که یک استثناء را صادر کند، صرفنظر از وضعیت await دوم، سبب اجرای بدنه‌ی catch می‌شود. همچنین انجام این عملیات بدین شکل بهینه نیست. زیرا ابتدا باید صبر کرد تا اولین Task تمام شود و سپس دومین Task شروع گردد و به این ترتیب پردازش موازی Taskها را از دست خواهیم داد. در یک چنین حالتی بهتر است از متد await Task.WhenAll استفاده شود. در اینجا دو Task مورد نیاز، تبدیل به یک Task مرکب می‌شوند. این Task مرکب تنها زمانی خاتمه می‌یابد که هر دوی Task اضافه شده به آن، خاتمه یافته باشند. به این ترتیب علاوه بر اجرای موازی Taskها، امکان دریافت استثناءهای هر کدام را نیز به صورت تجمعی خواهیم داشت.
مشکل! همانطور که پیشتر نیز عنوان شد، استفاده از await در اینجا سبب می‌شود تا کامپایلر تنها اولین استثنای دریافتی را بازگشت دهد و نه یک AggregateException نهایی را. روش حل آن‌را نیز عنوان کردیم. در این حالت بهتر است از متد ContinueWith و سپس استفاده از خاصیت Result آن برای دریافت کلیه استثناءها کمک گرفت.
حالت دوم از دست دادن استثناءها زمانی‌است که یک متد async void را ایجاد می‌کنید. در این حالات بهتر است از یک Task بجای بازگشت void استفاده شود. تنها علت وجودی async voidها، استفاده از آن‌ها در روال‌های رویدادگردان UI است (در سایر حالات code smell درنظر گرفته می‌شود).
public async Task<double> GetSum2Async()
        {
            try
            {
                var task1 = GetNumberAsync();
                var task2 = GetNumberAsync();

                var compositeTask = Task.WhenAll(task1, task2);
                await compositeTask.ContinueWith(x => { });

                return compositeTask.Result[0] + compositeTask.Result[1];
            }
            catch (Exception ex)
            {
                //todo: log ex
                throw;
            }
        }
در مثال فوق، نحوه‌ی ترکیب دو Task را توسط Task.WhenAll جهت اجرای موازی و سپس اعمال نکته‌ی یک ContinueWith خالی و در ادامه استفاده از Result نهایی را جهت دریافت تمامی استثناءهای حاصل، مشاهده می‌کنید.
در این مثال دیگر مانند مثال قسمت قبل
        public async Task<double> GetSumAsync()
        {
            var leftOperand = await GetNumberAsync();
            var rightOperand = await GetNumberAsync();

            return leftOperand + rightOperand;
        }
هر بار صبر نشده‌است تا یک Task تمام شود و سپس Task بعدی شروع گردد.
با کمک متد Task.WhenAll ترکیب آن‌ها ایجاد و سپس با فراخوانی await، سبب اجرای موازی چندین Task با هم شده‌ایم.


مدیریت خطاهای مدیریت نشده

ابتدا مثال زیر را در نظر بگیرید:
using System;
using System.Threading.Tasks;

namespace Async01
{
    class Program
    {
        static void Main(string[] args)
        {
            Test2();
            Test();
            Console.ReadLine();

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.ReadLine();
        }

        public static async Task Test()
        {
            throw new Exception();
        }

        public static async void Test2()
        {
            throw new Exception();
        }
    }
}
در این مثال دو متد که یکی async Task و دیگری async void است، تعریف شده‌اند.
اگر برنامه را کامپایل کنید، کامپایلر بر روی سطر فراخوانی متد Test اخطار زیر را صادر می‌کند. البته برنامه بدون مشکل کامپایل خواهد شد.
 Warning  1  Because this call is not awaited, execution of the current method continues before the call is completed.
Consider applying the 'await' operator to the result of the call.
اما چنین اخطاری در مورد async void صادر نمی‌شود. بنابراین ممکن است جایی در کدها، فراخوانی await فراموش شود. اگر خروجی متد شما ازنوع Task و مشتقات آن باشد، کامپایلر حتما اخطاری را جهت رفع آن گوشزد خواهد کرد؛ اما نه در مورد متدهای void که صرفا جهت کاربردهای UI و روال‌های رخدادگردان آن طراحی شده‌اند.
همچنین اگر برنامه را اجرا کنید استثنای صادر شده در متد async void سبب کرش برنامه می‌شود؛ اما نه استثنای صادر شده در متد async Task. متدهای async void چون دارای Synchronization Context نیستند، استثنای صادره را به Thread pool برنامه صادر می‌کنند. به همین جهت در همان لحظه نیز سبب کرش برنامه خواهند شد. اما در حالت async Task به این نوع استثناءها اصطلاحا Unobserved Task Exception گفته شده و سبب بروز  faulted state در Task تعریف شده می‌گردند.
برای مدیریت آن‌ها در سطح برنامه باید در ابتدای کار و در متد Main، توسط TaskScheduler.UnobservedTaskException روال رخدادگردانی را برای مدیریت اینگونه استثناءها تدارک دید. زمانیکه GC شروع به آزاد سازی منابع می‌کند، این استثناءها نیز درنظر گرفته شده و سبب کرش برنامه خواهند شد. با استفاده از متد SetObserved همانند قطعه کد زیر، می‌توان از کرش برنامه جلوگیری کرد:
using System;
using System.Threading.Tasks;

namespace Async01
{
    class Program
    {
        static void Main(string[] args)
        {
            TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

            //Test2();
            Test();
            Console.ReadLine();

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.ReadLine();
        }

        private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            e.SetObserved();
            Console.WriteLine(e.Exception);
        }

        public static async Task Test()
        {
            throw new Exception();
        }

        public static async void Test2()
        {
            throw new Exception();
        }
    }
}
البته لازم به ذکر است که این رفتار در دات نت 4.5 به این شکل تغییر کرده است تا کار با متدهای async ساده‌تر شود. در دات نت 4، یک چنین استثناءهای مدیریت نشده‌ای،‌بلافاصله سبب بروز استثناء و کرش برنامه می‌شدند.
به عبارتی رفتار قطعه کد زیر در دات نت 4 و 4.5 متفاوت است:
Task.Factory.StartNew(() => { throw new Exception(); });

Thread.Sleep(100);
GC.Collect();
GC.WaitForPendingFinalizers();
در دات نت 4  اگر این برنامه را خارج از VS.NET اجرا کنیم، برنامه کرش می‌کند؛ اما در دات نت 4.5 خیر و آن‌ها به UnobservedTaskException یاد شده هدایت خواهند شد. اگر می‌خواهید این رفتار را به همان حالت دات نت 4 تغییر دهید، تنظیم زیر را به فایل config برنامه اضافه کنید:
 <configuration>
    <runtime>
      <ThrowUnobservedTaskExceptions enabled="true"/>
    </runtime>
</configuration>


یک نکته‌ی تکمیلی: ممکن است عبارات lambda مورد استفاده، از نوع async void باشد.

همانطور که عنوان شد باید از async void منهای مواردی که کار مدیریت رویدادهای عناصر UI را انجام می‌دهند (مانند برنامه‌های ویندوز 8)، اجتناب کرد. چون پایان کار آن‌ها را نمی‌توان تشخیص داد و همچنین کامپایلر نیز اخطاری را در مورد استفاده ناصحیح از آن‌ها بدون await تولید نمی‌کند (چون نوع void اصطلاحا awaitable نیست). به علاوه بروز استثناء در آن‌ها، بلافاصله سبب خاتمه برنامه می‌شود. بنابراین اگر جایی در برنامه متد async void وجود دارد، قرار دادن try/catch داخل بدنه‌ی آن ضروری است.
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
    try
    {
        ClickMeButton.Tapped += async (sender, args) =>
        {
             throw new Exception();        

        };
    }
    catch (Exception ex)
    {
        // This won’t catch exceptions!
        TextBlock1.Text = ex.Message;
    }
}
در این مثال خاص ویندوز 8، شاید به نظر برسد که try/catch تعریف شده سبب مهار استثنای صادر شده می‌شود؛ اما خیر!
 public delegate void TappedEventHandler(object sender, TappedRoutedEventArgs e);
امضای متد TappedEventHandler از نوع delegate void است. بنابراین try/catch را باید داخل بدنه‌ی روال رویدادگردان تعریف شده قرار داد و نه خارج از آن.