مطالب
Blazor 5x - قسمت 32 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 2 - ثبت نام،‌ ورود به سیستم و خروج از آن
در قسمت 25، سرویس‌های سمت سرور اعتبارسنجی و احراز هویت مبتنی بر ASP.NET Core Identity را تهیه کردیم. همچنین در قسمت قبل، سرویس‌های سمت کلاینت کار با این Web API Endpoints را توسعه دادیم. در این مطلب، رابط کاربری متصل کننده‌ی بخش‌های سمت کلاینت و سمت سرور را تکمیل خواهیم کرد.


تکمیل فرم ثبت نام کاربران


در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده می‌کنید:
@page "/registration"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager


<EditForm Model="UserForRegistration" OnValidSubmit="RegisterUser" class="pt-4">
    <DataAnnotationsValidator />
    <div class="py-4">
        <div class=" row form-group ">
            <div class="col-6 offset-3 ">
                <div class="card border">
                    <div class="card-body px-lg-5 pt-4">
                        <h3 class="col-12 text-success text-center py-2">
                            <strong>Sign Up</strong>
                        </h3>
                        @if (ShowRegistrationErrors)
                        {
                            <div>
                                @foreach (var error in Errors)
                                {
                                    <p class="text-danger text-center">@error</p>
                                }
                            </div>
                        }
                        <hr style="background-color:aliceblue" />
                        <div class="py-2">
                            <InputText @bind-Value="UserForRegistration.Name" class="form-control" placeholder="Name..." />
                            <ValidationMessage For="(()=>UserForRegistration.Name)" />
                        </div>
                        <div class="py-2">
                            <InputText @bind-Value="UserForRegistration.Email" class="form-control" placeholder="Email..." />
                            <ValidationMessage For="(()=>UserForRegistration.Email)" />
                        </div>
                        <div class="py-2 input-group">
                            <div class="input-group-prepend">
                                <span class="input-group-text"> +1</span>
                            </div>
                            <InputText @bind-Value="UserForRegistration.PhoneNo" class="form-control" placeholder="Phone number..." />
                            <ValidationMessage For="(()=>UserForRegistration.PhoneNo)" />
                        </div>
                        <div class="form-row py-2">
                            <div class="col">
                                <InputText @bind-Value="UserForRegistration.Password" type="password" id="password" placeholder="Password..." class="form-control" />
                                <ValidationMessage For="(()=>UserForRegistration.Password)" />
                            </div>
                            <div class="col">
                                <InputText @bind-Value="UserForRegistration.ConfirmPassword" type="password" id="confirm" class="form-control" placeholder="Confirm Password..." />
                                <ValidationMessage For="(()=>UserForRegistration.ConfirmPassword)" />
                            </div>
                        </div>
                        <hr style="background-color:aliceblue" />
                        <div class="py-2">
                            @if (IsProcessing)
                            {
                                <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button>
                            }
                            else
                            {
                                <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Register</button>
                            }
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</EditForm>

@code{
    UserRequestDTO UserForRegistration = new UserRequestDTO();
    bool IsProcessing;
    bool ShowRegistrationErrors;
    IEnumerable<string> Errors;

    private async Task RegisterUser()
    {
        ShowRegistrationErrors = false;
        IsProcessing = true;
        var result = await AuthenticationService.RegisterUserAsync(UserForRegistration);
        if (result.IsRegistrationSuccessful)
        {
            IsProcessing = false;
            NavigationManager.NavigateTo("/login");
        }
        else
        {
            IsProcessing = false;
            Errors = result.Errors;
            ShowRegistrationErrors = true;
        }
    }
}
توضیحات:
- مدل این فرم بر اساس UserRequestDTO تشکیل شده‌است که همان شیءای است که اکشن متد ثبت نام سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آن‌را در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال می‌شود.
- در اینجا روشی را جهت غیرفعال کردن یک دکمه، پس از کلیک بر روی آن مشاهده می‌کنید. می‌توان پس از کلیک بر روی دکمه‌ی ثبت نام، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمه‌ی جاری را برای مثال با ویژگی disabled در صفحه درج کرد و یا حتی آن‌را از صفحه حذف کرد. این روش، یکی از روش‌های جلوگیری از کلیک چندباره‌ی کاربر، بر روی یک دکمه‌است.
- فرم جاری، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش می‌دهد که حاصل از ارسال آن‌ها توسط اکشن متد ثبت نام است:


- پس از پایان موفقیت آمیز ثبت نام، کاربر را به سمت فرم لاگین هدایت می‌کنیم.


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


در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده می‌کنید:
@page "/login"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager

<div id="logreg-forms">
    <h1 class="h3 mb-3 pt-3 font-weight-normal text-primary" style="text-align:center;">Sign In</h1>
    <EditForm Model="UserForAuthentication" OnValidSubmit="LoginUser">
        <DataAnnotationsValidator />
        @if (ShowAuthenticationErrors)
        {
            <p class="text-center text-danger">@Errors</p>
        }
        <InputText @bind-Value="UserForAuthentication.UserName" id="email" placeholder="Email..." class="form-control mb-2" />
        <ValidationMessage For="(()=>UserForAuthentication.UserName)"></ValidationMessage>
        <InputText @bind-Value="UserForAuthentication.Password" type="password" placeholder="Password..." id="password" class="form-control mb-2" />
        <ValidationMessage For="(()=>UserForAuthentication.Password)"></ValidationMessage>
        @if (IsProcessing)
        {
            <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button>
        }
        else
        {
            <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Sign in</button>
        }
        <a href="/registration" class="btn btn-primary text-white mt-3"><i class="fas fa-user-plus"></i> Register as a new user</a>
    </EditForm>
</div>
@code
{
    AuthenticationDTO UserForAuthentication = new AuthenticationDTO();
    bool IsProcessing = false;
    bool ShowAuthenticationErrors;
    string Errors;
    string ReturnUrl;

    private async Task LoginUser()
    {
        ShowAuthenticationErrors = false;
        IsProcessing = true;
        var result = await AuthenticationService.LoginAsync(UserForAuthentication);
        if (result.IsAuthSuccessful)
        {
            IsProcessing = false;
            var absoluteUri = new Uri(NavigationManager.Uri);
            var queryParam = HttpUtility.ParseQueryString(absoluteUri.Query);
            ReturnUrl = queryParam["returnUrl"];
            if (string.IsNullOrEmpty(ReturnUrl))
            {
                NavigationManager.NavigateTo("/");
            }
            else
            {
                NavigationManager.NavigateTo("/" + ReturnUrl);
            }
        }
        else
        {
            IsProcessing = false;
            Errors = result.ErrorMessage;
            ShowAuthenticationErrors = true;
        }
    }
}
توضیحات:
- مدل این فرم بر اساس AuthenticationDTO تشکیل شده‌است که همان شیءای است که اکشن متد لاگین سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آن‌را در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال می‌شود.
- در اینجا نیز همانند فرم ثبت نام، پس از کلیک بر روی دکمه‌ی ورود به سیستم، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمه‌ی جاری را با ویژگی disabled در صفحه درج کرد‌ه‌ایم تا از کلیک چندباره‌ی کاربر، جلوگیری شود.
- این فرم، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش می‌دهد که حاصل از ارسال آن‌ها توسط اکشن متد لاگین است:


- پس از پایان موفقیت آمیز ورود به سیستم، یا کاربر را به آدرسی که پیش از این توسط کوئری استرینگ returnUrl مشخص شده، هدایت می‌کنیم و یا به صفحه‌ی اصلی برنامه. همچنین در اینجا Local Storage نیز مقدار دهی شده‌است:


همانطور که مشاهده می‌کنید، مقدار JWT تولید شده‌ی پس از لاگین و همچنین مشخصات کاربر دریافتی از Web Api، جهت استفاده‌های بعدی، در Local Storage مرورگر درج شده‌اند.


تغییر منوی راهبری سایت، بر اساس وضعیت لاگین شخص


تا اینجا قسمت‌های ثبت نام و ورود به سیستم را تکمیل کردیم. در ادامه نیاز داریم تا منوی سایت را هم بر اساس وضعیت اعتبارسنجی شخص، تغییر دهیم. برای مثال اگر شخصی به سیستم وارد شده‌است، باید در منوی سایت، لینک خروج و نام خودش را مشاهده کند و نه مجددا لینک‌های ثبت‌نام و لاگین را. جهت تغییر منوی راهبری سایت، کامپوننت Shared\NavMenu.razor را گشوده و لینک‌های قبلی ثبت‌نام و لاگین را با محتوای زیر جایگزین می‌کنیم:
<AuthorizeView>
    <Authorized>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="#">
             <span class="p-2">
                Hello, @context.User.Identity.Name!
             </span>
          </NavLink>
        </li>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="logout">
             <span class="p-2">
                Logout
             </span>
          </NavLink>
        </li>
    </Authorized>
    <NotAuthorized>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="registration">
            <span class="p-2">
               Register
            </span>
          </NavLink>
        </li>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="login">
            <span class="p-2">
              Login
            </span>
          </NavLink>
        </li>
    </NotAuthorized>
</AuthorizeView>
نمونه‌ی چنین منویی را در قسمت 22 نیز مشاهده کرده بودید. AuthorizeView، یکی از کامپوننت‌های استانداردBlazor  است. زمانیکه کاربری به سیستم لاگین کرده باشد، فرگمنت Authorized و در غیر اینصورت قسمت NotAuthorized آن‌را مشاهده خواهید کرد و همانطور که در قسمت قبل نیز عنوان شد، این کامپوننت برای اینکه کار کند، نیاز دارد به اطلاعات AuthenticationState (و یا همان لیستی از User Claims) دسترسی داشته باشد که آن‌را توسط یک AuthenticationStateProvider سفارشی، به سیستم معرفی و توسط کامپوننت CascadingAuthenticationState، به صورت آبشاری در اختیار تمام کامپوننت‌های برنامه قرار دادیم که نمونه‌ای از آن، درج مقدار context.User.Identity.Name در منوی سایت است.


تکمیل قسمت خروج از سیستم

اکنون که لینک logout را در منوی سایت، پس از ورود به سیستم نمایش می‌دهیم، می‌توان کدهای کامپوننت آن‌را (Pages\Authentication\Logout.razor) به صورت زیر تکمیل کرد:
@page "/logout"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager

@code
{
    protected async override Task OnInitializedAsync()
    {
        await AuthenticationService.LogoutAsync();
        NavigationManager.NavigateTo("/");
    }
}
در اینجا در ابتدا توسط سرویس IClientAuthenticationService و متد LogoutAsync آن، کلیدهای Local Storage مربوط به JWT و اطلاعات کاربر حذف می‌شوند و سپس کاربر به صفحه‌ی اصلی هدایت خواهد شد.

مشکل! پس از کلیک بر روی logout، هرچند می‌توان مشاهده کرد که اطلاعات Local Storage به درستی حذف شده‌اند، اما ... پس از هدایت به صفحه‌ی اصلی برنامه، هنوز هم لینک logout و نام کاربری شخص نمایان هستند و به نظر هیچ اتفاقی رخ نداده‌است!
علت اینجا است که AuthenticationStateProvider سفارشی را که تهیه کردیم، فاقد اطلاع رسانی تغییر وضعیت است:
namespace BlazorWasm.Client.Services
{
    public class AuthStateProvider : AuthenticationStateProvider
    {
        // ...

        public void NotifyUserLoggedIn(string token)
        {
            var authenticatedUser = new ClaimsPrincipal(
                                        new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
                                    );
            var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
            base.NotifyAuthenticationStateChanged(authState);
        }

        public void NotifyUserLogout()
        {
            var authState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
            base.NotifyAuthenticationStateChanged(authState);
        }
    }
}
در اینجا نیاز است پس از لاگین و یا خروج شخص، حتما متد NotifyAuthenticationStateChanged کلاس پایه‌ی AuthenticationStateProvider فراخوانی شود تا وضعیت AuthenticationState ای که در اختیار کامپوننت‌ها قرار می‌گیرد نیز تغییر کند. در غیراینصورت login و logout و یا هر تغییری در لیست user claims، به صورت آبشاری در اختیار کامپوننت‌های برنامه قرار نمی‌گیرند. به همین جهت دو متد عمومی NotifyUserLoggedIn و NotifyUserLogout را به AuthStateProvider اضافه می‌کنیم، تا این متدها را در زمان‌های لاگین و خروج از سیستم، در سرویس ClientAuthenticationService، فراخوانی کنیم:
namespace BlazorWasm.Client.Services
{
    public class ClientAuthenticationService : IClientAuthenticationService
    {
        private readonly HttpClient _client;
        private readonly ILocalStorageService _localStorage;
        private readonly AuthenticationStateProvider _authStateProvider;

        public ClientAuthenticationService(
            HttpClient client,
            ILocalStorageService localStorage,
            AuthenticationStateProvider authStateProvider)
        {
            _client = client;
            _localStorage = localStorage;
            _authStateProvider = authStateProvider;
        }

        public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication)
        {
            // ...
            if (response.IsSuccessStatusCode)
            {
                //...
                ((AuthStateProvider)_authStateProvider).NotifyUserLoggedIn(result.Token);

                return new AuthenticationResponseDTO { IsAuthSuccessful = true };
            }
            //...
        }

        public async Task LogoutAsync()
        {
            //...
            ((AuthStateProvider)_authStateProvider).NotifyUserLogout();
        }
    }
}
در اینجا تغییرات لازم اعمالی به سرویس ClientAuthenticationService را مشاهده می‌کنید:
- ابتدا AuthenticationStateProvider به سازنده‌ی کلاس تزریق شده‌است.
- سپس در حین لاگین موفق، متد NotifyUserLoggedIn آن فراخوانی شده‌است.
- در آخر پس از خروج از سیستم، متد NotifyUserLogout فراخوانی شده‌است.

پس از این تغییرات اگر بر روی لینک logout کلیک کنیم، این گزینه به درستی عمل کرده و اینبار شاهد نمایش مجدد لینک‌های لاگین و ثبت نام خواهیم بود.


محدود کردن دسترسی به صفحات برنامه بر اساس نقش‌های کاربران

پس از ورود کاربر به سیستم و تامین AuthenticationState، اکنون می‌خواهیم تنها این نوع کاربران اعتبارسنجی شده بتوانند جزئیات اتاق‌ها (برای شروع رزرو) و یا صفحه‌ی نمایش نتیجه‌ی پرداخت را مشاهده کنند. البته نمی‌خواهیم صفحه‌ی نمایش لیست اتاق‌ها را محدود کنیم. برای این منظور ویژگی Authorize را به ابتدای تعاریف کامپوننت‌های PaymentResult.razor و RoomDetails.razor، اضافه می‌کنیم:
@attribute [Authorize(Roles = ‍ConstantRoles.Customer)]
که البته در اینجا ذکر فضای نام آن در فایل BlazorWasm.Client\_Imports.razor، ضروری است:
@using Microsoft.AspNetCore.Authorization

با این تعریف، دسترسی به صفحات کامپوننت‌های یاد شده، محدود به کاربرانی می‌شود که دارای نقش Customer هستند. برای معرفی بیش از یک نقش، فقط کافی است لیست نقش‌های مدنظر را که می‌توانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننت‌ها معرفی کرد و نمونه‌ای از آن‌را در مطلب 23 مشاهده کردید.
نکته‌ی مهم: فیلتر Authorize را باید بر روی اکشن متدهای متناظر سمت سرور نیز قرار داد؛ در غیراینصورت تنها نیمی از کار انجام شده‌است و هنوز آزادانه می‌توان با Web API Endpoints موجود کار کرد.


تکمیل مشخصات هویتی شخصی که قرار است اتاقی را رزرو کند

پیشتر در فرم RoomDetails.razor، اطلاعات ابتدایی کاربر را مانند نام او، دریافت می‌کردیم. اکنون با توجه به محدود شدن این کامپوننت به کاربران لاگین کرده، می‌توان اطلاعات کاربر وارد شده‌ی به سیستم را نیز به صورت خودکار بارگذاری و تکمیل کرد:
@page "/hotel-room-details/{Id:int}"

// ...

@code {
     // ...

    protected override async Task OnInitializedAsync()
    {
        try
        {
            HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
            if (Id != null)
            {
                // ...

                if (await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails) != null)
                {
                    var userInfo = await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails);
                    HotelBooking.OrderDetails.UserId = userInfo.Id;
                    HotelBooking.OrderDetails.Name = userInfo.Name;
                    HotelBooking.OrderDetails.Email = userInfo.Email;
                    HotelBooking.OrderDetails.Phone = userInfo.PhoneNo;
                }
            }
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
در اینجا با توجه به اینکه UserId هم مقدار دهی می‌شود، می‌توان سطر زیر را از ابتدای متد SaveRoomOrderDetailsAsync سرویس ClientRoomOrderDetailsService، حذف کرد:
public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
{
   // details.UserId = "unknown user!";
به این ترتیب هویت کاربری که کار خرید را انجام می‌دهد، دقیقا مشخص خواهد شد و همچنین پس از بازگشت از صفحه‌ی پرداخت بانکی، اطلاعات او مجددا از Local Storage دریافت و مقدار دهی اولیه می‌شود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-32.zip
مطالب
نمایش پیام هشدار در Blazor با استفاده از کامپوننت Alert بوت استرپ ۵

بر اساس آموزش مدیریت حالت در Blazor، قصد داریم یک سرویس پیام هشدار ساده، ولی زیبا را بوسیله کامپوننت Alert بوت استرپ ۵ ، بدون استفاده از توابع جاوا اسکریپتی، طراحی کنیم.

در ابتدا کتابخانه‌های css زیر را بوسیله LibMan به پروژه اضافه کرده و مداخل فایل‌های را  css   نیز اضافه می‌کنیم:

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "provider": "unpkg",
      "library": "bootstrap@5.0.0",
      "destination": "wwwroot/lib/bootstrap"
    },
    {
      "provider": "unpkg",
      "library": "open-iconic@1.1.1",
      "destination": "wwwroot/lib/open-iconic"
    },
   
    {
      "provider": "unpkg",
      "library": "animate.css@4.1.1",
      "destination": "wwwroot/lib/animate"
    },
    {
      "provider": "unpkg",
      "library": "bootstrap-icons@1.5.0",
      "destination": "wwwroot/lib/bootstrap-icons/"
    }
   ]
}

در ادامه کلاس سرویس پیام را  پیاده سازی و   آن‌ را با طول عمر Scoped به سیستم تزریق وابستگی‌های برنامه، معرفی میکنیم
    public enum  AlertType
    {
        Success,
        Info,
        Danger,
        Warning
    }

    public class AlertService
    {
        
        public void ShowAlert(string message, AlertType alertType,  string animate = "animate__fadeIn")
            {
             OnChange?.Invoke(message, alertType,animate);
            }

           public event Action<string,AlertType, string> OnChange;        
        }
services.AddScoped<AlertService>();

توضیحات:

در کدهای نهایی برنامه قرار است به این نحو کار نمایش Alertها را در کامپوننت‌های مختلف انجام دهیم:

@inject AlertService AlertService

@code {
    private void Success()
    {
        AlertService.ShowAlert("Success!", AlertType.Success);
    }
این کامپوننت‌ها هم الزاما در یک سلسله مراتب قرار ندارند و ارسال پارامترهای آبشاری به آن‌ها صدق نمی‌کند. به همین جهت یک سرویس Scoped را طراحی کرده‌ایم که در برنامه‌های Blazor WASM، طول عمر آن، با طول عمر برنامه یکی است؛ یعنی به صورت Singleton عمل می‌کند و در تمام کامپوننت‌ها و سرویس‌های دیگر نیز در دسترس خواهد بود. زمانیکه متد AlertService.ShowAlert فراخوانی می‌شود، سبب بروز رویداد OnChange خواهد شد و تمام گوش دهندگان به آن که در اینجا تنها کامپوننت Alert سفارشی ما است (برای مثال آن‌را در MainLayout.razor قرار می‌دهیم )، مطلع شده و بلافاصله محتوایی را نمایش می‌دهند.

کدهای کامپوننت Alert.razor

@inject AlertService AlertService
@implements IDisposable
 <style>
        .alert-show {
            display: flex;
            flex-direction: row;
           }

        .alert-hide {
            display: none;
        }
  </style>
    <div style="z-index: 5">
        <div " + "alert-show" :"alert-hide")">
            <i width="24" height="24"></i>
            <div>
                @Message
            </div>
            <button type="button" data-bs-dismiss="alert" aria-label="Close" @onclick="CloseClick"></button>
        </div>
    </div>


        @code {

            AlertType AlertType { get; set; }
            string Icon { get; set; }
            string Css { get; set; }
            string Animation { get; set; }
            private bool IsVisible { get; set; }
            private string Message { get; set; }
            System.Timers.Timer _alertTimeOutTimer;
            protected override void OnInitialized()
            {
              AlertService.OnChange += ShowAlert;
            }

            private void ShowAlert(string message, AlertType alertType, string animate)
            {
                _alertTimeOutTimer = new System.Timers.Timer
                {
                    Interval = 5000,
                    Enabled = true,
                    AutoReset = false
                };
                _alertTimeOutTimer.Elapsed += HideAlert;
                Message = message;
                switch (alertType)
                {
                    case AlertType.Success:
                        Css = "bg-success";
                        Icon = "bi-check-circle";
                        break;
                    case AlertType.Info:
                        Css = "bg-info";
                        Icon = "bi-info-circle-fill";
                        break;
                    case AlertType.Danger:
                        Css = "bg-danger";
                        Icon = "bi-exclamation-circle";
                        break;
                    case AlertType.Warning:
                        Css = "bg-warning";
                        Icon = "bi-exclamation-triangle-fill";
                        break;
                    default:
                        Css = Css;
                        break;
                }
                AlertType = alertType;
                Animation = animate;
                IsVisible = true;
                InvokeAsync(StateHasChanged);
            }
            private void HideAlert(Object source, System.Timers.ElapsedEventArgs e)
            {
                IsVisible = false;
                InvokeAsync(StateHasChanged);
                _alertTimeOutTimer.Close();
            }
            public void Dispose()
            {
                if (AlertService != null) AlertService.OnChange -= ShowAlert;
                if (_alertTimeOutTimer != null)
                {
                    _alertTimeOutTimer.Elapsed -= HideAlert;
                    _alertTimeOutTimer?.Dispose();
                }
            }
            private void CloseClick()
            {
                IsVisible = false;
                _alertTimeOutTimer.Close();
                InvokeAsync(StateHasChanged);
            }

        }
توضیحات:
همانطور که مشاهده می‌کنید، کامپوننت Alert، از سرویس تزریق شده‌ی AlertService استفاده می‌کند. بنابراین در هرجائی از برنامه که AlertService.ShowAlert فراخوانی شود، سبب بروز رویداد OnChange شده و به این ترتیب کامپوننت فوق، Alert ای را نمایش می‌دهد که البته نمایش آن به همراه یک Timeout و محو شدن خودکار نیز هست. برای استفاده از کامپوننت Alert.razor، آن‌را در صفحه اصلی MainLayout یا هرجای دلخواهی قرار می‌دهیم:
<div>
        <Alert></Alert>
و سپس با تزریق AlertService در کامپوننت مورد نظر (که محل آن مهم نیست) و اجرای متد ShowAlert آن به‌همراه پارامترهای آن، پیام هشداری را که توسط MainLayout نمایش داده می‌شود، مشاهده خواهیم کرد.

دریافت کدهای کامل برنامه:  BlazorBootstrapAlert.zip
اشتراک‌ها
لیستی از منابع DDD

A curated list of Domain-Driven Design (DDD), Command Query Responsibility Segregation (CQRS), Event Sourcing, and Event Storming resources.

 

لیستی از منابع DDD
اشتراک‌ها
مجموعه Awesome Domain-Driven Design

A curated list of Domain-Driven Design (DDD), Command Query Responsibility Segregation (CQRS), Event Sourcing, and Event Storming resources 

مجموعه Awesome Domain-Driven Design
مطالب
راهنمای گام به گام انتقال پروژه از MVC 5 به MVC 6
با اینکه چند ماهی از انتشار نسخه‌های مختلف ASP.NET 5 و MVC 6 می‌گذرد و مطالب زیادی هم در همین سایت در مورد ویژگی‌ها و امکانات جدید آن قرار داده شده، اما شاید افرادی هم باشند که مانند من از تغییرات زیادی که در ساختار پروژه‌ها در MVC 6 به وجود آمده این ترس را داشته باشند که مهاجرت به آن، کار سخت و زمانبری است و ترجیح می‌دهند که پروژه‌های قدیمی خود را به همان حالت حفظ کنند و همین امر باعث شده که از لذت امکانات جدید و فوق العاده‌ی ASP.NET 5 محروم بمانند. خب باید بگویم که من این کار را برای پروژه‌ی خودم انجام دادم و پیچیدگی زیادی ندارد. در ادامه سعی می‌کنم، گام به گام مراحلی را که طی کرده‌ام خدمتتان عرض کنم و تجربیاتی را که در این بین کسب نموده‌ام، در اختیارتان قرار دهم.

شما برای این کار باید چند مرحله را انجام دهید:

مرحله‌ی اول: یک پروژه‌ی جدید بسازید

در ویژوال 2015 یک پروژه‌ی جدید را از نوع ASP.NET Web Application ایجاد نمایید. نام دلخواهی را قرار داده و در قسمت بعد ASP.NET 5 Empty template را انتخاب نمایید. پروژه را اجرا کنید تا از صحت و درستی آن مطمئن شوید.
نکته: برای اینکار می‌توانید پروژه را از نوع ASP.NET 5 Web Application نیز انتخاب نمایید و مراحل کوتاه‌تری را طی نمایید. اما راه اندازی دستی قسمت‌های مختلف پروژه برای یکبار، به درک بهتر ساختار آن کمک زیادی می‌کند. از طرفی کار کردن بر روی یک پروژه‌ی تمیز و خالی، برای انتقال داده‌های مورد نیاز از یک پروژه‌ی دیگر، از بروز خطاهای پیش بینی نشده و تداخل‌های احتمالی نیز جلوگیری می‌کند.

مرحله‌ی دوم: اعمال تنظیمات جهت استفاده‌ی از MVC

همان طور که مشاهده می‌کنید در این پروژه دیگر خبری از web.config نیست. اما نگران نباشید، امکان اعمال تنظیمات، باز هم وجود دارد و فقط به فایل‌های json منتقل شده‌اند که project.json هم یکی از آن‌هاست. برای استفاده‌ی از Microsoft.AspNet.Mvc فقط کافی است فایل project.json را باز کنید و در قسمت "dependencies" پکیج "Microsoft.AspNet.Mvc" را به آن اضافه نمایید تا به صورت خودکار دانلود و به پروژه اضافه گردد.

"dependencies": {
    "Microsoft.AspNet.Server.IIS": "1.0.0-beta5",
    "Microsoft.AspNet.Server.WebListener": "1.0.0-beta5",
    "Microsoft.AspNet.Mvc": "6.0.0-beta5"
  },
همان طور که می‌بینید خبری از فایل Global.asax و همچنین فایل‌های موجود در پوشه‌ی App_Start هم نیست. خب در عوض Routing به فایل startup.cs منتقل شده است. این فایل را باز کنید و دو تغییر زیر را در آن انجام دهید:
public void ConfigureServices(IServiceCollection services)
{
      services.AddMvc();
}

public void Configure(IApplicationBuilder app)
{
     app.UseMvc(routes =>
     {
            routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
       });
}
تنها کاری که در این مرحله می‌ماند، ایجاد یک view و controller تستی در پروژه است تا کار را تا همین‌جا تست کنید. سپس دو پوشه‌ی Controllers و Views را اضافه کنید. یک کنترلر جدید را به نام HomeController به پوشه‌ی Controller و سپس متناسب با آن یک پوشه‌ی جدید را به نام Home در پوشه‌ی Views اضافه نمایید. در آخر هم یک View به نام Index در مسیر Views/Home اضافه نمایید و داخل آن را به صورت زیر قرار دهید:
<h1>My First MVC6 Website!</h1>
دقت کنید ساختار پروژه تا به اینجای کار به شکل زیر خواهد شد:


برنامه را اجرا کنید تا خروجی مورد انتظار را مشاهده نمایید.


مرحله‌ی سوم: انتقال فایل‌های پروژه‌ی قبلی به پروژه‌ی جدید

خب زمان انتقال فایل‌ها فرا رسیده است. برای این منظور باید تمام فایل‌های client-side، شامل استایل‌ها و فایل‌های js و نیز فایل‌های موجود در پوشه‌های model و view و controller، به پروژه‌ی جدید منتقل شوند. بهتر است تمام فایل‌ها را با هم انتقال ندهید. با یک فایل سبک شروع کنید. در صورت موفقیت آمیز بودن بقیه را هم منتقل کنید. به عنوان مثال می‌توانید از کنترلر Home شروع کنید که معمولا اکشن‌های Contact و About را دارا هستند. تمام محتویات این کنترلر را انتقال دهید و به این نکته نیز توجه داشته باشید که خروجی تمامی این اکشن‌ها در MVC 5 از نوع ActionResult است؛ ولی در MVC6 به IActionResult تغییر می‌کند. اما چندان هم مهم نیست؛ چرا که با ActionResult هم مشکلی نخواهد داشت.
View‌های مربوط به این controller را نیز به پوشه‌ی Views/Home منتقل نمایید و پروژه را بار دیگر اجرا نمایید. حال باید نمایی از محتوای این فایل‌ها بدون اعمال استایل‌ها را مشاهده نمایید.
همان طور که می‌دانیم MVC 5 برای استایل صفحات خود از bootstrap استفاده می‌کند و فایل‌های مورد نیاز آن در پوشه‌های Content و Script -که در root سایت موجود هستند- قرار دارند و نیز ارجاع به این فایل‌ها در Layout.cshtml_ قرار دارد. اما در MVC 6 قضیه کمی متفاوت و البته بهتر شده است. در MVC 6 تمام فایل‌های client-side شامل css و js در پوشه‌ی wwwroot قرار دارند و ما می‌تواینم فایل‌های bootstrap و غیره را از پروژه‌ی خود، به این مکان کپی نماییم. ولی روش بهتر، استفاده از ابزارهای bower و gulp برای این کار است. همان طور که میدانید bower یک package manager برای نصب، به روزرسانی و مدیریت کتابخانه‌های سمت کلاینت و gulp نیز یک task runner برای انجام کارهای مختلف از قبیل script minification و ... در سمت کلاینت است. gulp و bower به طور تو کار در ویژوال 2015 پشتیبانی می‌شوند و حتی اگر پروژه‌ی خود را از نوع ASP.NET 5 Web Application انتخاب کرده باشید، به صورت پیش فرض از آنها استفاده می‌کند.
در اینجا برای استفاده، ابتدا یک فایل از نوع Bower JSON Configuration را به root پروژه اضافه کرده و آن را bower.json بنامید و در خاصیت "dependencies" آن bootstrap, jquery,  jquery-validation, jquery-validation-unobtrusive را اضافه نمایید.

نکته: من در این قسمت، در restore کردن پکیج‌ها با استفاده از bower، با خطای زیر مواجه شدم:

visual 2015 ECMDERR Failed to execute "git ls-remote --tags --heads git://github.com/jquery/jquery.git", exit code of #-532462766

من از نسخه‌ی Visual Studio 2015 Update 1 CTP استفاده می‌کنم، ولی ظاهرا این مشکل در نسخه‌های دیگر هم وجود دارد و فایل bower.cmd ویژوال به درستی کار نمی‌کند. من برای حل این مشکل، ابتدا git را نصب کردم و در تنظیمات bower، مسیر پیش فرض ویژال رو به مسیر نصب git تغییر دادم. یعنی در پروژه بر روی پوشه‌ی Bower کلیک راست و configure external tools را انتخاب کردم و تیک $(DevEnvDir)\Extensions\Microsoft\Web Tools\External\git را برداشته و در عوض مسیر پیش فرض خودم یعنی C:\Program Files\Git\bin را اضافه کردم. 

اگر راه درست و اصولی‌تری برای حل این مشکل وجود دارد ممنون می‌شوم دوستان راهنمایی بفرمایند.

خب بعد از اضافه کردن خاصیت dependencies و پکیج‌های مورد نیاز، خاصیت exportsOverride را نیز مانند نمونه به فایل bower.json اضافه نمایید.

{
  "name": "ASP.NET",
  "private": true,
  "dependencies": {
    "bootstrap": "3.3.6",
    "jquery": "2.1.4",
    "jquery-validation": "1.14.0",
    "jquery-validation-unobtrusive": "3.2.4"
  },
  "exportsOverride": {
    "bootstrap": {
      "js": "dist/js/*.*",
      "css": "dist/css/*.*",
      "fonts": "dist/fonts/*.*"
    },

    "jquery": {
      "": "jquery.{js,min.js,min.map}"
    },
    "jquery-validation": {
      "": "jquery.validate.js"
    },
    "jquery-validation-unobtrusive": {
      "": "jquery.validate.unobtrusive.{js,min.js}"
    }
  }
}
Bower به صورت خودکار پکیج‌ها و وابستگی‌های آن‌ها را دانلود می‌کند، ولی توجه نمایید که هنوز فایل‌ها در پوشه‌ی wwwroot قرار نگرفته‌اند و قابل استفاده نیستند. پس باید از gulp کمک بگیریم تا این فایل‌ها را پردازش و در wwwroot قرار دهد. برای نصب gulp در پروژه، یک فایل NPM configuration به پروژه اضافه می‌کنیم و آن را package.json می‌نامیم. خاصیت "devDependencies" آن را همانند زیر تکمیل می‌کنیم:
"devDependencies": {
    "gulp": "3.9.0",
    "rimraf": "2.4.4",
    "gulp-concat": "2.6.0"
  }
بعد از ذخیره‌ی تغییرات باید پوشه‌ی NPM را در مسیر Dependencies مشاهده نمایید که شامل gulp و بقیه پکیج‌ها باشد.

حال که از نصب بودن gulp در پروژه مطمئن هستید فایل تنظیمات آن یعنی یک فایل Gulp Configuration را به root پروژه اضافه نمایید و آن را Gulpfile.js بنامید. برای استفاده از bower باید تنظیمات gulp را به صورت زیر انجام دهید:
var gulp = require('gulp');
var rimraf = require('rimraf');

var paths = {
    bower: "./bower_components/",
    lib: "./wwwroot/lib/"
};

gulp.task('clean', function (callback) {
    rimraf(paths.lib, callback);
});

gulp.task('default', ['clean'], function () {
    var bower = {
        "bootstrap": "bootstrap/dist/**/*.{js,map,css,ttf,svg,woff,eot}",
        "jquery": "jquery/jquery*.{js,map}",
        "jquery-validation": "jquery-validation/jquery.validate.js",
        "jquery-validation-unobtrusive":
                "jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"
    };

    for (var destinationDir in bower) {
        gulp.src(paths.bower + bower[destinationDir])
                .pipe(gulp.dest(paths.lib + destinationDir));
    }
});
تنظیمات مورد نیاز برای استفاده‌ی از bower و gulp تمام شد. حال باید از این ابزارها برای مدیریت فایل‌های client-side خود استفاده نماییم. روی Gulpfile.js کلیک راست کرده و Task Runner Explorer را انتخاب نمایید. از پنجره‌ی سمت چپ روی default دوبار کلیک نمایید تا خروجی زیر نمایان شود.


اگر پردازش فوق با موفقیت و بدون خطا انجام شود، می‌توانید پکیج‌های ایجاد شده را در مسیر wwwroot/lib، مشاهده نمایید.


مرحله‌ی چهارم: ویرایش برخی از view ها

حال که پکیج‌های مورد نیاز پروژه، در پوشه‌ی wwwroot قرار گرفتند، باید view هایی که ارجاعی را به این فایل‌ها دارند، نیز ویرایش نماییم. یکی از این فایل‌ها Layout.cshtml_ است که در مسیر Views/Shared قرار دارد. این فایل را باز کرده و به جای متد ()Styles .Render از عنصر <link> برای لود کردن استایل‌های بوت استرپ و غیره استفاده نمایید.

<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
برای فایل‌های js نیز متد () Scripts.Render را با عنصر <script> جایگزین نمایید.
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>


مرحله‌ی پنجم(اختیاری): جایگزین کردن متدهای Html helper با ساختار Tag Helper

یکی از ویژگی‌های جالب و مفید MVC 6 ساختار TagHalper ‌ها هستند که در واقع جایگزینی برای متدهای HtmlHelper و عملکردی مشابه به آنها دارند. البته استفاده از این ویژگی اجباری نیست ولی اگر تعداد ویوهای شما زیاد نیست و خواهان استفاده‌ی از این قابلیت در پروژه‌ی خود هستید، تنها کاری که باید انجام دهید، پیدا کردن HtmlHelper‌ها و جایگزینی آنها به صورت زیر می‌باشد: 

@Html.TextBoxFor(model => model.Name, new { style = "width: 100px" })
جایگزین شود با
<input asp-for="Name" style="width: 100px" />


نتیجه گیری
همان طور ملاحظه نمودید انتقال پروژه از ASP.NET MVC 5 به ASP.NET MVC 6 شامل انجام چند مرحله است و دشواری خاصی ندارد. عمده‌ی این تغییرات و پیچیدگی‌ها هم مربوط به انتقال فایل‌های client-side و نحوه‌ی کار با ابزارهای مدیریت پکیج می‌شود و البته تنظیماتی که در این بین باید انجام شوند. البته قسمت‌های دیگری مانند تنظیمات bundling و connection string نیز با MVC 5 تفاوت هایی دارد و کار با آن‌ها نیز بسیار ساده می‌باشد.

مطالب
زیرنویس فارسی ویدئوهای مقدمات AngularJS - قسمت چهارم
زیرنویس‌های فارسی قسمت چهارم را از اینجا  می‌توانید دانلود کنید.
لیست سرفصل‌های قسمت چهارم به شرح زیر است :
01. Introduction to Routing
02. Websites of Yore
03. Single Page Applications
04. Demo - Adding Your First Route
05. Demo - More Routing and Browser History
06. Demo - Creating a Default Route
07. Demo - Accessing Parameters from the Route
08. Demo - Using the $route Service
09. Demo - Enabling HTML5 Routing
10. Demo - Template and Resolve Properties
11. Demo - Using the $location Service
12. Summary
13. Suggested Exercises
در این قسمت به مبحث مسیریابی در انگولار و اهمیت آن جهت ساخت برنامه‌های تک صفحه ایی یا به اصطلاح SPA پرداخته می‌شود. همچنین به بررسی اینکه چگونه می‌توان با کمک سیستم مسیریابی، برنامه‌مان را تبدیل به یک برنامه تک صفحه‌ایی کنیم نیز پرداخته می‌شود. در واقع سیستم مسیریابی به ما کمک می‌کند یک برنامه تک صفحه ایی را به view‌های مختلفی تقسیم کنیم؛ هر چقدر برنامه درگیر جزئیات بیشتری شود، مدیریت آن نیز به مراتب سخت‌تر خواهد شد. تقسیم برنامه به view‌های مختلف و بارگذاری قسمت‌های مختلف برنامه با استفاده از Routing، مدیریت برنامه را برای ما ساده‌تر خواهد کرد.
این قسمت ابتدا شما را با ماهیت برنامه‌های تک صفحه ایی وب آشنا می‌کند و بعد از آن به بررسی مثال‌های عملی در این رابطه خواهد پرداخت. همچنین مواردی ازقبیل Browser History، ایجاد route‌های پیش فرض، افزودن پارامتر به route، استفاده از سرویس $route، فعال سازی سیستم مسیریابی مهیا در HTML5، بررسی پراپرتی‌های Template و Resolve، استفاده از سرویس location بخوبی آموزش و به آنها پرداخته می‌شود.


پ.ن. در مورد نحوه‌ی تهیه اصل ویدیوها در نظرات قسمت‌های قبل این سری مطالب، بیشتر بحث شده‌است.
نظرات مطالب
EF Code First #6
در مورد ASP.NET Web API و UpShot و اینها که از EF 4.3 تو خودشون استفاده کردند چی ؟
متاسفانه دیگه نمی‌شه با assembly binding بشون بگیم که از EF 5 استفاده کنید
چون Runtime خطا می‌دن و مثلا می‌گن که System.ComponentMode.DataAnnotation.ForeignKey در Entity Framework 5 نیست !
بله نیست، چون رفته به DLL مربوط به Component Model.Data Annotation
راه حلی جز گرفتن سورس کد upshot و Build مجددش هست ؟
و غیر از عقب گرد به EF 3
ممنون
مطالب
Blazor 5x - قسمت 24 - تهیه API مخصوص Blazor WASM - بخش 1 - ایجاد تنظیمات ابتدایی
تا اینجا با اصول توسعه‌ی برنامه‌های مبتنی بر Blazor Server آشنا شدیم. در ادامه‌ی این سری، روش توسعه برنامه‌های مبتنی بر Blazor WASM را بررسی خواهیم کرد و پیش از شروع آن، باید بتوان امکانات سمت سرور مورد نیاز این نوع برنامه‌های سمت کلاینت را از طریق یک Web API تامین کرد که شامل دریافت و ارائه‌ی اطلاعات و همچنین اعتبارسنجی و احراز هویت مبتنی بر JWT یکپارچه‌ی با ASP.NET Core Identity است.


ایجاد پروژه‌ی ASP.NET Core Web API

برای تامین اطلاعات برنامه‌ی سمت کلاینت Blazor WASM و همچنین فراهم آوردن زیرساخت اعتبارسنجی کاربران آن، نیاز به یک پروژه‌ی ASP.NET Core Web API داریم که آن‌را با اجرای دستور dotnet new webapi در یک پوشه‌ی خالی، برای مثال به نام BlazorWasm.WebApi ایجاد می‌کنیم.
البته این پروژه، از زیرساختی که در برنامه‌ی Blazor Server بررسی شده‌ی تا این قسمت، ایجاد کردیم نیز استفاده خواهد کرد. همانطور که پیشتر نیز عنوان شد، هدف از قسمت Blazor Server مثال این سری، آشنایی با مدل برنامه نویسی خاص آن بود؛ وگرنه می‌توان کل این پروژه را با Blazor Server و یا کل آن‌را با Web API + Blazor WASM نیز پیاده سازی کرد. در این مثال، قسمت‌های مدیریتی برنامه‌ی مدیریت هتل را توسط Blazor Server (مانند قسمت‌های تعریف اتاق‌ها و امکانات رفاهی هتل) و قسمت مخصوص کاربران آن‌را مانند رزرو کردن اتاق‌ها، توسط Blazor WASM پیاده سازی می‌کنیم. به همین جهت قسمت‌هایی از این دو پروژه، مانند سرویس‌های استفاده شده‌ی در پروژه‌ی Blazor server، در پروژه‌ی Web API مکمل Blazor WASM، قابلیت استفاده‌ی مجدد را دارند.


افزودن سرویس‌های آغازین مورد نیاز، به پروژه‌ی Web API

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

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAutoMapper(typeof(MappingProfile).Assembly);

            services.AddScoped<IHotelRoomService, HotelRoomService>();
            services.AddScoped<IAmenityService, AmenityService>();
            services.AddScoped<IHotelRoomImageService, HotelRoomImageService>();

            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));

            services.AddIdentity<IdentityUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            //...
در اینجا سرویس‌های AutoMapper، تنظیمات ابتدایی DbContext برنامه، به همراه سرویس‌های Identity (بدون UI آن) و افزودن سرویس‌های اتاق‌ها و امکانات رفاهی هتل را نیاز داریم. به همین جهت ارجاعات و وابستگی‌های زیر را به فایل csproj جاری اضافه می‌کنیم تا پروژه‌های DataAccess ،Services و Mappings قابل دسترسی و استفاده شوند:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" />
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.Services\BlazorServer.Services.csproj" />
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.Models.Mappings\BlazorServer.Models.Mappings.csproj" />
  </ItemGroup>
</Project>
همچنین در این پروژه نیز از همان بانک اطلاعاتی پروژه‌ی Blazor Server که تاکنون تکمیل کردیم، استفاده می‌کنیم. بنابراین محتوای فایل BlazorWasm\BlazorWasm.WebApi\appsettings.json آن نیز مشابه‌است:
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}


تعریف کنترلر HotelRoom

در ادامه کدهای اولین کنترلر Web API را مشاهده می‌کنید که مرتبط است با بازگشت اطلاعات تمام اتاق‌های ثبت شده و یا بازگشت اطلاعات یک اتاق ثبت شده:
using BlazorServer.Models;
using BlazorServer.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]")]
    public class HotelRoomController : ControllerBase
    {
        private readonly IHotelRoomService _hotelRoomService;

        public HotelRoomController(IHotelRoomService hotelRoomService)
        {
            _hotelRoomService = hotelRoomService;
        }

        [HttpGet]
        public IAsyncEnumerable<HotelRoomDTO> GetHotelRooms()
        {
            return _hotelRoomService.GetAllHotelRoomsAsync();
        }

        [HttpGet("{roomId}")]
        public async Task<IActionResult> GetHotelRoom(int? roomId)
        {
            if (roomId == null)
            {
                return BadRequest(new ErrorModel
                {
                    Title = "",
                    ErrorMessage = "Invalid Room Id",
                    StatusCode = StatusCodes.Status400BadRequest
                });
            }

            var roomDetails = await _hotelRoomService.GetHotelRoomAsync(roomId.Value);
            if (roomDetails == null)
            {
                return BadRequest(new ErrorModel
                {
                    Title = "",
                    ErrorMessage = "Invalid Room Id",
                    StatusCode = StatusCodes.Status404NotFound
                });
            }

            return Ok(roomDetails);
        }
    }
}
- این کنترلر، از سرویس IHotelRoomService که در قسمت‌های قبل تکمیل کردیم، استفاده می‌کند.
- ErrorModel آن‌را در همان پروژه‌ی قبلی مدل‌ها، در فایل BlazorServer\BlazorServer.Models\ErrorModel.cs به صورت زیر ایجاد کرده‌ایم:
namespace BlazorServer.Models
{
    public class ErrorModel
    {
        public string Title { get; set; }

        public int StatusCode { get; set; }

        public string ErrorMessage { get; set; }
    }
}
در این حالت اگر برنامه‌ی Web API را اجرا کنیم، به خروجی Swagger زیر می‌رسیم که جزئیات این فناوری را در سری «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» پیشتر بررسی کردیم:


یکی از مزایای آن، امکان آزمایش API تهیه شده، بدون نیاز به تهیه‌ی هیچ نوع کلاینت خاصی است. برای مثال اگر بر روی api​/hotelroom آن کلیک کنیم، گزینه‌ی «try it out» آن ظاهر شده و با کلیک بر روی آن، اینبار دکمه‌ی execute ظاهر می‌شود. در ادامه با کلیک بر روی دکمه‌ی اجرای آن، اکشن متد GetHotelRooms اجرا شده و خروجی زیر ظاهر می‌شود:


و یا اگر بخواهیم متد GetHotelRoom را توسط آن آزمایش کنیم، بر اساس پارامترهای آن، رابط کاربری زیر را تشکیل می‌دهد که امکان دریافت شماره‌ی اتاق را دارد:



انجام تنظیمات ابتدایی CORS و خروجی JSON برنامه

قرار است این API را از طریق پروژه‌ی Blazor سمت کلاینت خود استفاده کنیم که آدرس آن، با آدرس API یکی نیست. به همین جهت نیاز است تنظیمات CORS را به صورت زیر اضافه کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
         // ... 

            services.AddCors(o => o.AddPolicy("HotelManagement", builder =>
            {
                builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
            }));

            services.AddControllers()
                    .AddJsonOptions(options =>
                    {
                        options.JsonSerializerOptions.PropertyNamingPolicy = null;
                        // To avoid `JsonSerializationException: Self referencing loop detected error`
                        options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
                    });
         // ... 
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ... 

            app.UseCors("HotelManagement");
            app.UseRouting();

            app.UseAuthentication();
            // ...
        }
    }
}
در اینجا علاوه بر تنظیمات CORS، تنظیمات JsonSerializer را هم تغییر داده‌ایم تا خطاهای Self referencing loop را در حین ارائه‌ی خروجی‌های Web API، مشاهده نکنیم (همان نکته‌ی «تهیه خروجی JSON از مدل‌های مرتبط، بدون Stack overflow»).


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-24.zip
اشتراک‌ها
ReSharper 2021.1 منتشر شد

ReSharper 2021.1: Updates in Code Analysis, Support for ASP.NET Route Templates, and Rename for Tuple Components 

ReSharper 2021.1 منتشر شد