مطالب
از کار افتادن SQL Server Agent
SQL Server Agent مربوط به SQL Server 2008 از کار افتاده بود و راه اندازی نمی‌شد. خطای مرتبط با آن در لاگ‌های ویندوز به نحو زیر بود:
 SQLServerAgent could not be started (reason: Unable to connect to server '(local)'; SQLServerAgent cannot start).
پس از مدتی جستجو، عنوان شده بود که مسیر رجیستری زیر را یافته:
 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL10.MSSQLSERVER\SQLServerAgent
و در آن ServerHost را به نام سرور ویرایش کنید. سپس سرور را ری استارت نمائید. این تغییر انجام شد اما باز هم SQL Server Agent راه اندازی نمی‌شد.
لاگ‌های آن در مسیر ذیل ثبت می‌شوند:
 C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\Log\SQLAGENT.OUT
در اینجا خطاهای زیر ثبت شده بودند:
 2013-01-28 20:02:34 - ! [298] SQLServer Error: 780, SQL Server Network Interfaces: The logon attempt failed [SQLSTATE HY000]
2013-01-28 20:02:34 - ! [298] SQLServer Error: 780, Cannot generate SSPI context [SQLSTATE HY000]
2013-01-28 20:02:34 - ! [000] Unable to connect to server ; SQLServerAgent cannot start
2013-01-28 20:02:34 - ! [298] SQLServer Error: 780, SQL Server Network Interfaces: The logon attempt failed [SQLSTATE HY000]
2013-01-28 20:02:34 - ! [298] SQLServer Error: 780, Cannot generate SSPI context [SQLSTATE HY000]
2013-01-28 20:02:34 - ! [382] Logon to server failed (DisableAgentXPs)
2013-01-28 20:02:34 - ? [098] SQLServerAgent terminated (normally)
همانطور که مشخص است مشکل اصلی در عدم توانایی لاگین اکانت SQL Server Agent ذکر شده است.
برای تغییر آن مسیر زیر را طی کنید:
 SQL Configuration manager -> SQL Server Agent -> Logon User -> NT/Local Service
به عبارتی در قسمت SQL Configuration manager، تنظیمات SQL Server Agent را یافته و نوع اکانت آن‌را به Local Service تغییر دهید.
پس از آن سرویس بدون مشکل استارت شد.
مطالب
سرورهای متصل شده‌ی SQL Server و مبحث تراکنش‌ها

یکی از قابلیت‌های جالب SQL Server در یک شبکه محلی امکان link و اتصال آن‌ها به یکدیگر است. به این صورت امکان کوئری گرفتن (و یا اعمال متداول SQL ایی) از دو یا چند سرور مختلف با دستورات T-SQL میسر می‌شود؛ به نحوی که حس یکپارچگی دیتابیس‌های این سرورها را حین کوئری نوشتن خواهیم داشت.
برای مثال فرض کنید دو سرور SQL1 و SQL2 را در شبکه داریم. می‌خواهیم در سرور SQL1 اتصالی را به سرور SQL2 ایجاد کنیم.

USE master

EXEC sp_addlinkedserver
'SQL2',
N'SQL Server'

sp_addlinkedsrvlogin @useself='false ', @rmtsrvname = 'SQL2',
@rmtuser = 'sa',
@rmtpassword = 'pass#'

دستورات T-SQL فوق کار ثبت یک liked server جدید و اعمال مشخصات کاربری که توسط آن قرار است به سرور SQL2 دسترسی داشت، انجام می‌دهند.
اکنون جهت بررسی این اتصال در سرور SQL1 کوئری زیر را اجرا می‌کنیم:

select * from sql2.faxManager.dbo.tblErja

که نحوه‌ی فراخوانی جدول مورد نظر باید به صورت Server.DatabaseName.dbo.TableName در آن رعایت شود.
تا اینجا همه چیز خوب است. مشکل از زمانی شروع می‌شود که بخواهیم تراکنش‌ها را نیز دخالت دهیم و اصولی کار کنیم. برای مثال:

begin distributed tran
select * from sql2.faxManager.dbo.tblErja
commit tran

خطایی که در ویندوز سرور 2003 با آخرین به روز رسانی‌ها ظاهر می‌شود به صورت زیر است:

The operation could not be performed because OLE DB provider for linked server was unable to begin a distributed transaction.
OLE DB provider for linked server returned message "The partner transaction manager has disabled its support for remote/network transactions.".


به صورت پیش فرض این نوع تراکنش‌های توزیع شده غیرفعال هستند مگر اینکه فعال شوند و روش حل مشکل نیز به صورت زیر می‌باشد:
قبل از هر کاری به کنسول سرویس‌های ویندوز مراجعه کرده و از در حال اجرا بودن سرویس Distribute Transaction Coordinator اطمینان حاصل کنید.
سپس به قسمت زیر مراجعه نمائید:
Control Panel > Administrative Tools > Component Services


نود مربوط به Component Service را گشوده و سپس بر روی My Computer کلیک راست کرده و گزینه‌ی خواص را انتخاب کنید.
در صفحه‌ی بازه شده به برگه‌ی MSTDC مراجعه کرده و بر روی دکمه‌ی Security Configuration کلیک نمائید.
اکنون تنظیمات آن‌را مطابق شکل زیر تغییر دهید.


این تنظیم باید بر روی هر دو سرور SQL1 و SQL2 انجام شود.

پس از این تغییرات که شامل راه اندازی مجدد سرویس Distribute Transaction Coordinator نیز خواهد شد، مشکل خطای فوق برطرف شده و امکان استفاده از تراکنش‌ها در linked servers نیز میسر می‌شود.

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

Unable to start a nested transaction for OLE DB provider for linked server . A nested transaction was required because the XACT_ABORT option was set to OFF.
OLE DB provider for linked server returned message "Cannot start more transactions on this session.".


برای حل این مشکل یک سطر زیر را باید به ابتدای کوئری خود اضافه کرد که جزو الزامات تراکنش‌های توزیع شده است و به این صورت از rollback کامل تمامی دستورات موجود فراخوانی شده T-SQL در صورت بروز کوچکترین خطایی اطمینان حاصل می‌کند:
SET XACT_ABORT ON


برای مطالعه بیشتر:
MSDTC Troubleshooting

نظرات مطالب
مهاجرت از SQL Membership به ASP.NET Identity
سلام ممنون از مطالب خوبتون .
زمانیکه در اپلکیشن خودم asp.net Configuration و تب Security را انتخاب می‌نمایم با پیغام زیر مواجه می‌شوم Unable to connect to SQL Server database.
دستور زیر را هم اجرا کردم
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regsql.exe -S . -A all -d tt -U sa_tt -P asd123
در صورتیکه از login و password آن مطمئن هستم ولی مشکل حل نشد
در صورتیکه مشکل از permission هست چگونه باید آن را حل کنم
webconfig برنامه به شرح زیر است
Data Source=.;Initial Catalog=tt;User ID=sa_tt;Password=asd123  

با تشکر
نظرات مطالب
مقدمه‌ای بر داکر، قسمت سوم
من دستور زیر را اجرا میکنم 
docker build -f Dockerfile -t alikhll/testasp1 . 
Step 5/7 : RUN npm i
 ---> [Warning] The requested image's platform (linux/amd64) does not match the detected host platform (windows/amd64) and no specific platform was requested
 ---> Running in dc37481d5781

added 48 packages, and audited 49 packages in 6s

found 0 vulnerabilities
npm notice
npm notice New minor version of npm available! 8.5.5 -> 8.7.0
npm notice Changelog: <https://github.com/npm/cli/releases/tag/v8.7.0>
npm notice Run `npm install -g npm@8.7.0` to update!
npm notice
The command '/bin/sh -c npm i' returned a non-zero code: 4294967295: failed to shutdown container: container dc37481d5781615dbf01c129f2322f06fcac8851a8a4078a6273438a8427254a encountered an error during hcsshim::System::waitBackground: failure in a Windows system call: The virtual machine or container with the specified identifier is not running. (0xc0370110): subsequent terminate failed container dc37481d5781615dbf01c129f2322f06fcac8851a8a4078a6273438a8427254a encountered an error during hcsshim::System::waitBackground: failure in a Windows system call: The virtual machine or container with the specified identifier is not running. (0xc0370110)
کلا هر دستور npm که اجرا میکنم خطای بالا رو میده
توی پوشه اش کامند npm i  اجرا میکنم درست کار میکنه و پکیچ‌ها نصب میشه ولی داخل محیط داکر خیر
از docker desktop و ویندوز 10 استفاده میکنم
پکیج نود هم از داکر دانلود و نصب کردم
نظرات مطالب
React 16x - قسمت 16 - مسیریابی - بخش 2 - پارامترهای مسیریابی
به head فایل index.html، این تگ را اضافه کنید:
<base href="/" /> <!-- for local host -->

و یا اگر فایل‌ها را به صورت زیر به فایل index.html معرفی کرده‌اید: 
<head>
    <link rel="stylesheet" href="./style.css" ></link>
</head>
باید به این شکل اصلاح شوند:
<head>
    <link rel="stylesheet" href="/style.css" ></link>
</head>
اطلاعات بیشتر
مطالب
آشنایی با M.A.F - قسمت دوم

قسمت قبل بیشتر آشنایی با یک سری از اصطلاحات مرتبط با فریم ورک MAF بود و همچنین نحوه‌ی کلی استفاده از آن. در این قسمت یک مثال ساده را با آن پیاده سازی خواهیم کرد و فرض قسمت دوم بر این است که افزونه‌ی Visual Studio Pipeline Builder را نیز نصب کرده‌اید.

یک نکته پیش از شروع:
- اگر افزونه‌ی Visual Studio Pipeline Builder پس از نصب به منوی Tools اضافه نشده است، یک پوشه‌ی جدید را به نام Addins در مسیر Documents\Visual Studio 2008 ایجاد کرده و سپس فایل‌های آن‌را در این مسیر کپی کنید.

ساختار اولیه یک پروژه MAF

- پروژ‌ه‌هایی که از MAF استفاده می‌کنند، نیاز به ارجاعاتی به دو اسمبلی استاندارد System.AddIn.dll و System.AddIn.Contract.dll دارند (مطابق شکل زیر):



- ساختار آغازین یک پروژه MAF از سه پروژه تشکیل می‌شود که توسط افزونه‌ی Visual Studio Pipeline Builder به 7 پروژه بسط خواهد یافت.
این سه پروژه استاندارد آغازین شامل موارد زیر هستند:



- هاست: همان برنامه‌ی اصلی که قرار است از افزونه استفاده کند.
- قرار داد: نحو‌ه‌ی تعامل هاست و افزونه در این پروژه تعریف می‌شود. (یک پروژه از نوع class library)
- افزونه: کار پیاده سازی قرار داد را عهده دار خواهد شد. (یک پروژه از نوع class library)

- همچنین مرسوم است جهت مدیریت بهتر خروجی‌های حاصل شده یک پوشه Output را نیز به این solution اضافه کنند:



اکنون با توجه به این محل خروجی، به خواص Build سه پروژه موجود مراجعه کرده و مسیر Build را اندکی اصلاح خواهیم کرد (هر سه مورد بهتر است اصلاح شوند)، برای مثال:



نکته‌ی مهم هم اینجا است که خروجی host باید به ریشه این پوشه تنظیم شود و سایر پروژه‌ها هر کدام خروجی خاص خود را در پوشه‌ای داخل این ریشه باید ایجاد کنند.



تا اینجا قالب اصلی کار آماده شده است. قرارداد ما هم به شکل زیر است (ویژگی AddInContract آن نیز نباید فراموش شود):

using System.AddIn.Pipeline;
using System.AddIn.Contract;

namespace CalculatorConract
{
[AddInContract]
public interface ICalculatorContract : IContract
{
double Operate(string operation, double a, double b);
}
}

به عبارت دیگر برنامه‌ای محاسباتی داریم (هاست) که دو عدد double را در اختیار افزونه‌های خودش قرار می‌دهد و سپس این افزونه‌ها یک عملیات ریاضی را بر روی آن‌ها انجام داده و خروجی را بر می‌گردانند. نوع عملیات توسط آرگومان operation مشخص می‌شود. این آرگومان به کلیه افزونه‌های موجود ارسال خواهد شد و احتمالا یکی از آن‌ها این مورد را پیاده سازی کرده است. در غیر اینصورت یک استثنای عملیات پیاده سازی نشده صادر می‌شود.
البته روش بهتر طراحی این افزونه، اضافه کردن متد یا خاصیتی جهت مشخص کردن نوع و یا انواع عملیات پشتیبانی شده توسط افزونه‌ است که جهت سادگی این مثال، به این طراحی ساده اکتفا می‌شود.

ایجاد pipeline

اگر قسمت قبل را مطالعه کرده باشید، یک راه حل مبتنی بر MAF از 7 پروژه تشکیل می‌شود که عمده‌ترین خاصیت آن‌ها مقاوم کردن سیستم در مقابل تغییرات نگارش قرارداد است. در این حالت اگر قرار داد تغییر کند، نه هاست و نه افزونه‌ی قدیمی، نیازی به تغییر در کدهای خود نخواهند داشت و این پروژه‌های میانی هستند که کار وفق دادن (adapters) نهایی را برعهده می‌گیرند.


برای ایجاد خودکار View ها و همچنین Adapters ، از افزونه‌ی Visual Studio Pipeline Builder که پیشتر معرفی شد استفاده خواهیم کرد.



سه گزینه‌ی آن هم مشخص هستند. نام پروژه‌ی قرارداد، مسیر پروژه‌ی هاست و مسیر خروجی نهایی معرفی شده. پیش از استفاده از این افزونه نیاز است تا یکبار solution مورد نظر کامپایل شود. پس از کلیک بر روی دکمه‌ی OK، پروژه‌های ذکر شده ایجاد خواهند شد:


پس از ایجاد این پروژه‌ها، نیاز به اصلاحات مختصری در مورد نام اسمبلی و فضای نام هر کدام می‌باشد؛ زیرا به صورت پیش فرض هر کدام به نام template نامگذاری شده‌اند:



پیاده سازی افزونه

قالب کاری استفاده از این فریم ورک آماده است. اکنون نوبت به پیاده سازی یک افزونه می‌باشد. به پروژه AddIn مراجعه کرده و ارجاعی را به اسمبلی AddInView خواهیم افزود. به این صورت افزونه‌ی ما به صورت مستقیم با قرارداد سروکار نداشته و ارتباطات، در راستای همان pipeline تعریف شده، جهت مقاوم شدن در برابر تغییرات صورت می‌گیرد:
using System;
using CalculatorConract.AddInViews;
using System.AddIn;

namespace CalculatorAddIn
{
[AddIn]
public class MyCalculatorAddIn : ICalculator
{
public double Operate(string operation, double a, double b)
{
throw new NotImplementedException();
}
}
}

در اینجا افزونه‌ی ما باید اینترفیس ICalculator مربوط به AddInView را پیاده سازی نماید که برای مثال خواهیم داشت:

using System;
using CalculatorConract.AddInViews;
using System.AddIn;

namespace CalculatorAddIn
{
[AddIn("افزونه یک", Description = "توضیحات", Publisher = "نویسنده", Version = "نگارش یک")]
public class MyCalculatorAddIn : ICalculator
{
public double Operate(string operation, double a, double b)
{
switch (operation)
{
case "+":
return a + b;
case "-":
return a - b;
case "*":
return a * b;
default:
throw new NotSupportedException("عملیات مورد نظر توسط این افزونه پشتیبانی نمی‌شود");
}
}
}
}

همانطور که در قسمت قبل نیز ذکر شد، این کلاس باید با ویژگی AddIn مزین شود که توسط آن می‌توان توضیحاتی در مورد نام ، نویسنده و نگارش افزونه ارائه داد.


استفاده از افزونه‌ی تولید شده

هاست برای استفاده از افزونه‌هایی با قرارداد ذکر شده، مطابق pipeline پروژه، نیاز به ارجاعی به اسمبلی HostView دارد و در اینجا نیز هاست به صورت مستقیم با قرارداد کاری نخواهد داشت. همچنین هاست هیچ ارجاع مستقیمی به افزونه‌ها نداشته و بارگذاری و مدیریت آن‌ها به صورت پویا انجام خواهد شد.

نکته‌ی مهم!
در هر دو ارجاع به HostView و یا AddInView باید خاصیت Copy to local به false تنظیم شود، در غیر اینصورت افزونه‌ی شما بارگذاری نخواهد شد.



پس از افزودن ارجاعی به HostView، نمونه‌ای از استفاده از افزونه‌ی تولید شده به صورت زیر می‌تواند باشد که توضیحات مربوطه به صورت کامنت آورده شده است:

using System;
using System.AddIn.Hosting;
using CalculatorConract.HostViews;

namespace Calculator
{
class Program
{
private static ICalculator _calculator;

static void doOperation()
{
Console.WriteLine("1+2: {0}", _calculator.Operate("+", 1, 2));
}

static void Main(string[] args)
{
//مسیر پوشه ریشه مربوطه به خط لوله افزونه‌ها
string path = Environment.CurrentDirectory;

//مشخص سازی مسیر خواندن و کش کردن افزونه‌ها
AddInStore.Update(path);

//یافتن افزونه‌هایی سازگار با شرایط قرارداد پروژه
//در اینجا هیچ افزونه‌ای بارگذاری نمی‌شود
var addIns = AddInStore.FindAddIns(typeof(ICalculator), path);

//اگر افزونه‌ای یافت شد
if (addIns.Count > 0)
{
var addIn = addIns[0]; //استفاده از اولین افزونه
Console.WriteLine("1st addIn: {0}", addIn.Name);

//فعال سازی افزونه و همچنین مشخص سازی سطح دسترسی آن
_calculator = addIn.Activate<ICalculator>(AddInSecurityLevel.Intranet);

//یک نمونه از استفاده آن
doOperation();
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

چند نکته جالب توجه در مورد قابلیت‌های ارائه شده:
- مدیریت load و unload پویا
- امکان تعریف سطح دسترسی و ویژگی‌های امنیتی اجرای یک افزونه
- امکان ایزوله سازی پروسه اجرای افزونه از هاست (در ادامه توضیح داده خواهد شد)
- مقاوم بودن پروژه به نگارش‌های مختلف قرارداد


اجرای افزونه در یک پروسه مجزا

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

//فعال سازی افزونه و همچنین مشخص سازی سطح دسترسی آن
//همچنین جدا سازی پروسه اجرایی افزونه از هاست
_calculator = addIn.Activate<ICalculator>(
new AddInProcess(),
AddInSecurityLevel.Intranet);

در این حالت اگر پس از فعال شدن افزونه، یک break point قرار دهیم و به task manager ویندوز مراجعه نمائیم، پروسه‌ی مجزای افزونه قابل مشاهده است.



برای مطالعه بیشتر + ، + ، + و +

مطالب
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
نظرات مطالب
شروع به کار با DNTFrameworkCore - قسمت 6 - پیاده‌سازی عملیات CRUD موجودیت‌ها با استفاده از ASP.NET Core MVC
نکته تکمیلی
در راستای تکمیل مطلب جاری و مطلب «پیاده سازی Conventional UI در ASP.NET MVC» برای رسیدن به یک قالب مشخص و جلوگیری از تکرار، می‌توان به شکل زیر عمل کرد:
1- انتقال قسمت‌های مشترک فرم‌ها به یک پارشال‌ویو به عنوان Layout فرم‌ها
//_EntityFormLayout.cshtml

@inherits EntityFormRazorPage<dynamic>
@{
    Layout = null;
}
<div class="modal-header">
    <h4 class="modal-title" asp-if="IsNew">Create New @EntityDisplayName</h4>
    <h4 class="modal-title" asp-if="!IsNew">Edit @EntityDisplayName</h4>
    <button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<form asp-action="@(IsNew ? CreateActionName : EditActionName)" asp-modal-form="@FormId">
    <div class="modal-body">
        <input type="hidden" name="continue-editing" value="true" asp-permission="@EditPermission"/>
        <input asp-for="@Version" type="hidden"/>
        <input asp-for="@Id" type="hidden"/>
        @RenderBody()
    </div>
    <div class="modal-footer">

        <a class="btn btn-light btn-circle" asp-modal-delete-link asp-model-id="@Id" asp-modal-toggle="false"
           asp-action="@DeleteActionName" asp-if="!IsNew" asp-permission="@DeletePermission"
           title="Delete Role">
            <i class="fa fa-trash text-danger"></i>
        </a>

        <a class="btn btn-light btn-circle" title="Refresh Role" asp-if="!IsNew" asp-modal-link asp-modal-toggle="false"
           asp-action="@EditActionName" asp-route-id="@Id">
            <i class="fa fa-repeat"></i>
        </a>
        <a class="btn btn-light btn-circle mr-auto" title="New Role" asp-modal-link asp-modal-toggle="false"
           asp-permission="@CreatePermission"
           asp-action="@CreateActionName">
            <i class="fa fa-plus"></i>
        </a>
        <button type="button" class="btn btn-light" data-dismiss="modal">
            <i class="fa fa-ban"></i>&nbsp; Cancel
        </button>
        <button type="submit" class="btn btn-outline-primary">
            <i class="fa fa-save"></i>&nbsp;Save Changes
        </button>
    </div>
</form>

با توجه به اینکه مدل متناظر با یک ویو در Layout آن نیز قابل دسترس می‌باشد. بدین ترتیب امکان دسترسی به خصوصیاتی مانند Id و Version یا متد IsNew وجود دارد؛ این خصوصیات در کلاس MasterModel به عنوان پایه مدل/DTO/ویومدل‌های ثبت/ویرایش، تعریف شده‌اند.
قراداد ما استفاده از همان مدل/DTO‌ها به عنوان ویومدل می‌باشد که در سناریوهای خاص پیشنهاد شد که از مدلی با نام موجودیت + کلمه ModalViewModel یا FormViewModel استفاده شود. برای انتقال سایر دیتا و متادیتای مورد نیاز برای ساخت فرم می‌توان از ViewBag و ViewData پس از امکان تعریف ویومدل پایه (دارای خصوصیات مورد نیاز Layout) که در این طراحی ممکن نیست، استفاده کرد. 
2- طراحی یک EntityFormRazorPage پایه
برای رسیدن به کدی با خوانایی بالا کلاسی را به عنوان پایه ویو‌های فرم‌ها و پارشال‌ویو EntityFormLayout، به شکل زیر طراحی می‌کنیم. در اینجا فرم ما یکسری خصوصیات موجود در کلاس پایه خود را مقداردهی خواهد کرد و در ادامه به دلیل ذخیره شدن این اطلاعات در ViewData، در Layout نیز قابل دسترس خواهند بود. 
    public abstract class EntityFormRazorPage<T> : RazorPage<T>
    {
        protected string EntityName
        {
            get => ViewData[nameof(EntityName)].ToString();
            set => ViewData[nameof(EntityName)] = value;
        }

        protected string EntityDisplayName
        {
            get => ViewData[nameof(EntityDisplayName)].ToString();
            set => ViewData[nameof(EntityDisplayName)] = value;
        }

        protected string DeletePermission
        {
            get => ViewData[nameof(DeletePermission)].ToString();
            set => ViewData[nameof(DeletePermission)] = value;
        }

        protected string CreatePermission
        {
            get => ViewData[nameof(CreatePermission)].ToString();
            set => ViewData[nameof(CreatePermission)] = value;
        }

        protected string EditPermission
        {
            get => ViewData[nameof(EditPermission)].ToString();
            set => ViewData[nameof(EditPermission)] = value;
        }

        protected string CreateActionName
        {
            get => ViewData.TryGetValue(nameof(CreateActionName), out var value) ? value.ToString() : "Create";
            set => ViewData[nameof(CreateActionName)] = value;
        }

        protected string EditActionName
        {
            get => ViewData.TryGetValue(nameof(EditActionName), out var value) ? value.ToString() : "Edit";
            set => ViewData[nameof(EditActionName)] = value;
        }

        protected string DeleteActionName
        {
            get => ViewData.TryGetValue(nameof(DeleteActionName), out var value) ? value.ToString() : "Delete";
            set => ViewData[nameof(DeleteActionName)] = value;
        }

        protected string FormId => $"{EntityName}Form";
        protected bool IsNew => (Model as dynamic).IsNew();
        protected string Id => (Model as dynamic).Id.ToString(CultureInfo.InvariantCulture);
        protected byte[] Version => (Model as dynamic).Version;
    }
3- تنظیم خصوصیات موجود در کلاس پایه
برای این منظور لازم است کلاس پایه را با دایرکتیو inherits مشخص کرده و سپس کار تنظیم Layout و سایر خصوصیات مورد نیاز را انجام دهید:
//_BlogPartial.cshtml

@inherits EntityFormRazorPage<BlogModel>
@{
    Layout = "_EntityFormLayout";
    EntityName = "Blog";
    DeletePermission = PermissionNames.Blogs_Delete;
    CreatePermission = PermissionNames.Blogs_Create;
    EditPermission = PermissionNames.Blogs_Edit;
    EntityDisplayName = "Blog";
}

4 - فرم ثبت و ویرایش متناظر با یک موجودیت
//_BlogPartial.cshtml

@inherits EntityFormRazorPage<BlogModel>
@{
    Layout = "_EntityFormLayout";
    ...
}

<div class="form-group row">
    <div class="col col-md-8">
        <label asp-for="Title" class="col-form-label text-md-left"></label>
        <input asp-for="Title" autocomplete="off" class="form-control"/>
        <span asp-validation-for="Title" class="text-danger"></span>
    </div>
</div>
<div class="form-group row">
    <div class="col">
        <label asp-for="Url" class="col-form-label text-md-left"></label>
        <input asp-for="Url" class="form-control" type="url"/>
        <span asp-validation-for="Url" class="text-danger"></span>
    </div>
</div>

و یا اگر از EditorTemplates استفاده می‌کنید:
//_BlogPartial.cshtml

@inherits EntityFormRazorPage<BlogModel>
@{
    Layout = "_EntityFormLayout";
    EntityName = "Blog";
    DeletePermission = PermissionNames.Blogs_Delete;
    CreatePermission = PermissionNames.Blogs_Create;
    EditPermission = PermissionNames.Blogs_Edit;
    EntityDisplayName = "Blog";
}

@Html.EditorForModel()

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