اشتراکها
لیستی از سایر ORMهای مخصوص NET Core.
در قسمت قبل کلاسهای متناظر با جداول پایهی ASP.NET Core Identity را تغییر دادیم. اما هنوز سرویسهای پایهی این فریم ورک مانند مدیریت و ذخیرهی کاربران و مدیریت و ذخیرهی نقشها، اطلاعی از وجود آنها ندارند. در ادامه این سرویسها را نیز سفارشی سازی کرده و سپس به ASP.NET Core Identity معرفی میکنیم.
سفارشی سازی RoleStore
ASP.NET Core Identity دو سرویس را جهت کار با نقشهای کاربران پیاده سازی کردهاست:
- سرویس RoleStore: کار آن دسترسی به ApplicationDbContext ایی است که در قسمت قبل سفارشی سازی کردیم و سپس ارائهی زیر ساختی به سرویس RoleManager جهت استفادهی از آن برای ذخیره سازی و یا تغییر و حذف نقشهای سیستم.
وجود Storeها از این جهت است که اگر علاقمند بودید، بتوانید از سایر ORMها و یا زیرساختهای ذخیره سازی اطلاعات برای کار با بانکهای اطلاعاتی استفاده کنید. در اینجا از همان پیاده سازی پیشفرض آن که مبتنی بر EF Core است استفاده میکنیم. به همین جهت است که وابستگی ذیل را در فایل project.json مشاهده میکنید:
- سرویس RoleManager: این سرویس از سرویس RoleStore و تعدادی سرویس دیگر مانند اعتبارسنج نام نقشها و نرمال ساز نام نقشها، جهت ارائهی متدهایی برای یافتن، افزودن و هر نوع عملیاتی بر روی نقشها استفاده میکند.
برای سفارشی سازی سرویسهای پایهی ASP.NET Core Identity، ابتدا باید سورس این مجموعه را جهت یافتن نگارشی از سرویس مدنظر که کلاسهای موجودیت را به صورت آرگومانهای جنریک دریافت میکند، پیدا کرده و سپس از آن ارث بری کنیم:
تا اینجا مرحلهی اول تشکیل کلاس ApplicationRoleStore سفارشی به پایان میرسد. نگارش جنریک RoleStore پایه را یافته و سپس موجودیتهای سفارشی سازی شدهی خود را به آن معرفی میکنیم.
این ارث بری جهت تکمیل، نیاز به بازنویسی سازندهی RoleStore پایه را نیز دارد:
در اینجا پارامتر اول آنرا به IUnitOfWork بجای DbContext متداول تغییر دادهایم؛ چون IUnitOfWork دقیقا از همین نوع است و همچنین امکان دسترسی به متدهای ویژهی آنرا نیز فراهم میکند.
در نگارش 1.1 این کتابخانه، بازنویسی متد CreateRoleClaim نیز اجباری است تا در آن مشخص کنیم، نحوهی تشکیل کلید خارجی و اجزای یک RoleClaim به چه نحوی است:
در نگارش 2.0 آن، این new RoleClaim به صورت خودکار توسط کتابخانه صورت خواهد گرفت و سفارشی کردن آن سادهتر میشود.
در ادامه اگر به انتهای تعریف امضای کلاس دقت کنید، یک اینترفیس IApplicationRoleStore را نیز مشاهده میکنید. برای تشکیل آن بر روی نام کلاس سفارشی خود کلیک راست کرده و با استفاده از ابزارهای Refactoring کار Extract interface را انجام میدهیم؛ از این جهت که در سایر لایههای برنامه نمیخواهیم از تزریق مستقیم کلاس ApplicationRoleStore استفاده کنیم. بلکه میخواهیم اینترفیس IApplicationRoleStore را در موارد ضروری، به سازندههای کلاسهای تزریق نمائیم.
پس از تشکیل این اینترفیس، مرحلهی بعد، معرفی آنها به سیستم تزریق وابستگیهای ASP.NET Core است. چون تعداد تنظیمات مورد نیاز ما زیاد هستند، یک کلاس ویژه به نام IdentityServicesRegistry تشکیل شدهاست تا به عنوان رجیستری تمام سرویسهای سفارشی سازی شدهی ما عمل کند و تنها با فراخوانی متد AddCustomIdentityServices آن در کلاس آغازین برنامه، کار ثبت یکجای تمام سرویسهای ASP.NET Core Identity انجام شود و بیجهت کلاس آغازین برنامه شلوغ نگردد.
ثبت ApplicationDbContext طبق روش متداول آن در کلاس آغازین برنامه انجام شدهاست. سپس معرفی سرویس IUnitOfWork را که از ApplicationDbContext تامین میشود، در کلاس IdentityServicesRegistry مشاهده میکنید.
در ادامه RoleStore سفارشی ما نیاز به دو تنظیم جدید را خواهد داشت:
ابتدا مشخص کردهایم که اینترفیس IApplicationRoleStore، از طریق کلاس سفارشی ApplicationRoleStore تامین میشود.
سپس RoleStore توکار ASP.NET Core Identity را نیز به ApplicationRoleStore خود هدایت کردهایم. به این ترتیب هر زمانیکه در کدهای داخلی این فریم ورک وهلهای از RoleStore جنریک آن درخواست میشود، دیگر از همان پیاده سازی پیشفرض خود استفاده نخواهد کرد و به پیاده سازی ما هدایت میشود.
این روشی است که جهت سفارشی سازی تمام سرویسهای پایهی ASP.NET Core Identity بکار میگیریم:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
2) سفارشی سازی سازندهی این کلاسها با سرویسهایی که تهیه کردهایم (بجای سرویسهای پیش فرض).
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
سفارشی سازی RoleManager
در اینجا نیز همان 5 مرحلهای را که عنوان کردیم باید جهت تشکیل کلاس جدید ApplicationRoleManager پیگیری کنیم.
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
در اینجا نگارشی از RoleManager را انتخاب کردهایم که بتواند Role سفارشی خود را به سیستم معرفی کند.
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
در این سفارشی سازی دو مورد را تغییر دادهایم:
الف) ذکر IApplicationRoleStore بجای RoleStore آن
ب) ذکر IUnitOfWork بجای ApplicationDbContext
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
RoleManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationRoleManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار RoleManager را به سرویس جدید و سفارشی سازی شدهی ApplicationRoleManager خود هدایت کردهایم.
سفارشی سازی UserStore
در مورد مدیریت کاربران نیز دو سرویس Store و Manager را مشاهده میکنید. کار کلاس Store، کپسوله سازی لایهی دسترسی به دادهها است تا کتابخانههای ثالث، مانند وابستگی Microsoft.AspNetCore.Identity.EntityFrameworkCore بتوانند آنرا پیاده سازی کنند و کار کلاس Manager، استفادهی از این Store است جهت کار با بانک اطلاعاتی.
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationUserStore پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
از بین نگارشهای مختلف UserStore، نگارشی را انتخاب کردهایم که بتوان در آن موجودیتهای سفارشی سازی شدهی خود را معرفی کنیم.
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
در این سفارشی سازی یک مورد را تغییر دادهایم:
الف) ذکر IUnitOfWork بجای ApplicationDbContext
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
در اینجا نیز تکمیل 4 متد از کلاس پایه UserStore جنریک انتخاب شده، جهت مشخص سازی نحوهی انتخاب کلیدهای خارجی جداول سفارشی سازی شدهی مرتبط با جدول کاربران ضروری است:
در نگارش 2.0 آن، این newها به صورت خودکار توسط خود فریم ورک صورت خواهد گرفت و سفارشی کردن آن سادهتر میشود.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserStore مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار UserStore را به سرویس جدید و سفارشی سازی شدهی ApplicationUserStore خود هدایت کردهایم.
سفارشی سازی UserManager
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationUserManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
از بین نگارشهای مختلف UserManager، نگارشی را انتخاب کردهایم که بتوان در آن موجودیتهای سفارشی سازی شدهی خود را معرفی کنیم.
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
در این سفارشی سازی چند مورد را تغییر دادهایم:
الف) ذکر IUnitOfWork بجای ApplicationDbContext (البته این مورد، یک پارامتر اضافی است که بر اساس نیاز این سرویس سفارشی، اضافه شدهاست)
تمام پارامترهای پس از logger به دلیل نیاز این سرویس اضافه شدهاند و جزو پارامترهای سازندهی کلاس پایه نیستند.
ب) استفادهی از IApplicationUserStore بجای UserStore پیشفرض
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
UserManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار UserManager را به سرویس جدید و سفارشی سازی شدهی ApplicationUserManager خود هدایت کردهایم.
سفارشی سازی SignInManager
سرویس پایه SignInManager از سرویس UserManager جهت فراهم آوردن زیرساخت لاگین کاربران استفاده میکند.
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationSignInManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
از بین نگارشهای مختلف SignInManager، نگارشی را انتخاب کردهایم که بتوان در آن موجودیتهای سفارشی سازی شدهی خود را معرفی کنیم.
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
در این سفارشی سازی یک مورد را تغییر دادهایم:
الف) استفادهی از IApplicationUserManager بجای UserManager پیشفرض
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
SignInManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationSignInManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار مدیریت لاگین را به سرویس جدید و سفارشی سازی شدهی ApplicationSignInManager خود هدایت کردهایم.
معرفی نهایی سرویسهای سفارشی سازی شده به ASP.NET Identity Core
تا اینجا سرویسهای پایهی این فریم ورک را جهت معرفی موجودیتهای سفارشی سازی شدهی خود سفارشی سازی کردیم و همچنین آنها را به سیستم تزریق وابستگیهای ASP.NET Core نیز معرفی نمودیم. مرحلهی آخر، ثبت این سرویسها در رجیستری ASP.NET Core Identity است:
اگر منابع را مطالعه کنید، تمام آنها از AddEntityFrameworkStores و سپس معرفی ApplicationDbContext به آن استفاده میکنند. با توجه به اینکه ما همه چیز را در اینجا سفارشی سازی کردهایم، فراخوانی متد افزودن سرویسهای EF این فریم ورک، تمام آنها را بازنویسی کرده و به حالت اول و پیش فرض آن بر میگرداند. بنابراین نباید از آن استفاده شود.
در اینجا متد AddIdentity یک سری تنظیمهای پیش فرضها این فریم ورک مانند اندازهی طول کلمهی عبور، نام کوکی و غیره را در اختیار ما قرار میدهد به همراه ثبت تعدادی سرویس پایه مانند نرمال ساز نامها و ایمیلها. سپس توسط متدهای AddUserStore، AddUserManager و ... ایی که مشاهده میکنید، سبب بازنویسی سرویسهای پیش فرض این فریم ورک به سرویسهای سفارشی خود خواهیم شد.
در این مرحلهاست که اگر Migration را اجرا کنید، کار میکند و خطای تبدیل نشدن کلاسها به یکدیگر را دریافت نخواهید کرد.
تشکیل مرحله استفادهی از ASP.NET Core Identity و ثبت اولین کاربر در بانک اطلاعاتی به صورت خودکار
روال متداول کار با امکانات کتابخانههای نوشته شدهی برای ASP.NET Core، ثبت سرویسهای پایهی آنها توسط متدهای Add است که نمونهی services.AddIdentity فوق دقیقا همین کار را انجام میدهد. مرحلهی بعد به app.UseIdentity میرسیم که کار ثبت میانافزارهای این فریم ورک را انجام میدهد. متد UseCustomIdentityServices کلاس IdentityServicesRegistry اینکار را انجام میدهد که از آن در کلاس آغازین برنامه استفاده شدهاست.
در اینجا یک مرحلهی استفادهی از سرویس IIdentityDbInitializer را نیز مشاهده میکنید. کلاس IdentityDbInitializer این اهداف را برآورده میکند:
الف) متد Initialize آن، متد context.Database.Migrate را فراخوانی میکند. به همین جهت دیگر نیاز به اعمال دستی حاصل Migrations، به بانک اطلاعاتی نخواهد بود. متد Database.Migrate هر مرحلهی اعمال نشدهای را که باقی مانده باشد، به صورت خودکار اعمال میکند.
ب) متد SeedData آن، به نحو صحیحی یک Scope جدید را ایجاد کرده و توسط آن به ApplicationDbContext دسترسی پیدا میکند تا نقش Admin و کاربر Admin را به سیستم اضافه کند. همچنین به کاربر Admin، نقش Admin را نیز انتساب میدهد. تنظیمات این کاربران را نیز از فایل appsettings.json و مدخل AdminUserSeed آن دریافت میکند.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
سفارشی سازی RoleStore
ASP.NET Core Identity دو سرویس را جهت کار با نقشهای کاربران پیاده سازی کردهاست:
- سرویس RoleStore: کار آن دسترسی به ApplicationDbContext ایی است که در قسمت قبل سفارشی سازی کردیم و سپس ارائهی زیر ساختی به سرویس RoleManager جهت استفادهی از آن برای ذخیره سازی و یا تغییر و حذف نقشهای سیستم.
وجود Storeها از این جهت است که اگر علاقمند بودید، بتوانید از سایر ORMها و یا زیرساختهای ذخیره سازی اطلاعات برای کار با بانکهای اطلاعاتی استفاده کنید. در اینجا از همان پیاده سازی پیشفرض آن که مبتنی بر EF Core است استفاده میکنیم. به همین جهت است که وابستگی ذیل را در فایل project.json مشاهده میکنید:
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
برای سفارشی سازی سرویسهای پایهی ASP.NET Core Identity، ابتدا باید سورس این مجموعه را جهت یافتن نگارشی از سرویس مدنظر که کلاسهای موجودیت را به صورت آرگومانهای جنریک دریافت میکند، پیدا کرده و سپس از آن ارث بری کنیم:
public class ApplicationRoleStore : RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, IApplicationRoleStore
این ارث بری جهت تکمیل، نیاز به بازنویسی سازندهی RoleStore پایه را نیز دارد:
public ApplicationRoleStore( IUnitOfWork uow, IdentityErrorDescriber describer = null) : base((ApplicationDbContext)uow, describer)
در نگارش 1.1 این کتابخانه، بازنویسی متد CreateRoleClaim نیز اجباری است تا در آن مشخص کنیم، نحوهی تشکیل کلید خارجی و اجزای یک RoleClaim به چه نحوی است:
protected override RoleClaim CreateRoleClaim(Role role, Claim claim) { return new RoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }; }
در ادامه اگر به انتهای تعریف امضای کلاس دقت کنید، یک اینترفیس IApplicationRoleStore را نیز مشاهده میکنید. برای تشکیل آن بر روی نام کلاس سفارشی خود کلیک راست کرده و با استفاده از ابزارهای Refactoring کار Extract interface را انجام میدهیم؛ از این جهت که در سایر لایههای برنامه نمیخواهیم از تزریق مستقیم کلاس ApplicationRoleStore استفاده کنیم. بلکه میخواهیم اینترفیس IApplicationRoleStore را در موارد ضروری، به سازندههای کلاسهای تزریق نمائیم.
پس از تشکیل این اینترفیس، مرحلهی بعد، معرفی آنها به سیستم تزریق وابستگیهای ASP.NET Core است. چون تعداد تنظیمات مورد نیاز ما زیاد هستند، یک کلاس ویژه به نام IdentityServicesRegistry تشکیل شدهاست تا به عنوان رجیستری تمام سرویسهای سفارشی سازی شدهی ما عمل کند و تنها با فراخوانی متد AddCustomIdentityServices آن در کلاس آغازین برنامه، کار ثبت یکجای تمام سرویسهای ASP.NET Core Identity انجام شود و بیجهت کلاس آغازین برنامه شلوغ نگردد.
ثبت ApplicationDbContext طبق روش متداول آن در کلاس آغازین برنامه انجام شدهاست. سپس معرفی سرویس IUnitOfWork را که از ApplicationDbContext تامین میشود، در کلاس IdentityServicesRegistry مشاهده میکنید.
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IApplicationRoleStore, ApplicationRoleStore>(); services.AddScoped<RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, ApplicationRoleStore>();
سپس RoleStore توکار ASP.NET Core Identity را نیز به ApplicationRoleStore خود هدایت کردهایم. به این ترتیب هر زمانیکه در کدهای داخلی این فریم ورک وهلهای از RoleStore جنریک آن درخواست میشود، دیگر از همان پیاده سازی پیشفرض خود استفاده نخواهد کرد و به پیاده سازی ما هدایت میشود.
این روشی است که جهت سفارشی سازی تمام سرویسهای پایهی ASP.NET Core Identity بکار میگیریم:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
2) سفارشی سازی سازندهی این کلاسها با سرویسهایی که تهیه کردهایم (بجای سرویسهای پیش فرض).
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
سفارشی سازی RoleManager
در اینجا نیز همان 5 مرحلهای را که عنوان کردیم باید جهت تشکیل کلاس جدید ApplicationRoleManager پیگیری کنیم.
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationRoleManager : RoleManager<Role>, IApplicationRoleManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationRoleManager( IApplicationRoleStore store, IEnumerable<IRoleValidator<Role>> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger<ApplicationRoleManager> logger, IHttpContextAccessor contextAccessor, IUnitOfWork uow) : base((RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>)store, roleValidators, keyNormalizer, errors, logger, contextAccessor)
الف) ذکر IApplicationRoleStore بجای RoleStore آن
ب) ذکر IUnitOfWork بجای ApplicationDbContext
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
RoleManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationRoleManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationRoleManager, ApplicationRoleManager>(); services.AddScoped<RoleManager<Role>, ApplicationRoleManager>();
سفارشی سازی UserStore
در مورد مدیریت کاربران نیز دو سرویس Store و Manager را مشاهده میکنید. کار کلاس Store، کپسوله سازی لایهی دسترسی به دادهها است تا کتابخانههای ثالث، مانند وابستگی Microsoft.AspNetCore.Identity.EntityFrameworkCore بتوانند آنرا پیاده سازی کنند و کار کلاس Manager، استفادهی از این Store است جهت کار با بانک اطلاعاتی.
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationUserStore پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationUserStore : UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, IApplicationUserStore
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationUserStore( IUnitOfWork uow, IdentityErrorDescriber describer = null) : base((ApplicationDbContext)uow, describer)
الف) ذکر IUnitOfWork بجای ApplicationDbContext
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
در اینجا نیز تکمیل 4 متد از کلاس پایه UserStore جنریک انتخاب شده، جهت مشخص سازی نحوهی انتخاب کلیدهای خارجی جداول سفارشی سازی شدهی مرتبط با جدول کاربران ضروری است:
protected override UserClaim CreateUserClaim(User user, Claim claim) { var userClaim = new UserClaim { UserId = user.Id }; userClaim.InitializeFromClaim(claim); return userClaim; } protected override UserLogin CreateUserLogin(User user, UserLoginInfo login) { return new UserLogin { UserId = user.Id, ProviderKey = login.ProviderKey, LoginProvider = login.LoginProvider, ProviderDisplayName = login.ProviderDisplayName }; } protected override UserRole CreateUserRole(User user, Role role) { return new UserRole { UserId = user.Id, RoleId = role.Id }; } protected override UserToken CreateUserToken(User user, string loginProvider, string name, string value) { return new UserToken { UserId = user.Id, LoginProvider = loginProvider, Name = name, Value = value }; }
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserStore مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserStore, ApplicationUserStore>(); services.AddScoped<UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, ApplicationUserStore>();
سفارشی سازی UserManager
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationUserManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationUserManager : UserManager<User>, IApplicationUserManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationUserManager( IApplicationUserStore store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<User> passwordHasher, IEnumerable<IUserValidator<User>> userValidators, IEnumerable<IPasswordValidator<User>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<ApplicationUserManager> logger, IHttpContextAccessor contextAccessor, IUnitOfWork uow, IUsedPasswordsService usedPasswordsService) : base((UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>)store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
الف) ذکر IUnitOfWork بجای ApplicationDbContext (البته این مورد، یک پارامتر اضافی است که بر اساس نیاز این سرویس سفارشی، اضافه شدهاست)
تمام پارامترهای پس از logger به دلیل نیاز این سرویس اضافه شدهاند و جزو پارامترهای سازندهی کلاس پایه نیستند.
ب) استفادهی از IApplicationUserStore بجای UserStore پیشفرض
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
UserManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserManager, ApplicationUserManager>(); services.AddScoped<UserManager<User>, ApplicationUserManager>();
سفارشی سازی SignInManager
سرویس پایه SignInManager از سرویس UserManager جهت فراهم آوردن زیرساخت لاگین کاربران استفاده میکند.
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationSignInManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationSignInManager : SignInManager<User>, IApplicationSignInManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationSignInManager( IApplicationUserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory<User> claimsFactory, IOptions<IdentityOptions> optionsAccessor, ILogger<ApplicationSignInManager> logger) : base((UserManager<User>)userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
الف) استفادهی از IApplicationUserManager بجای UserManager پیشفرض
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
SignInManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationSignInManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationSignInManager, ApplicationSignInManager>(); services.AddScoped<SignInManager<User>, ApplicationSignInManager>();
معرفی نهایی سرویسهای سفارشی سازی شده به ASP.NET Identity Core
تا اینجا سرویسهای پایهی این فریم ورک را جهت معرفی موجودیتهای سفارشی سازی شدهی خود سفارشی سازی کردیم و همچنین آنها را به سیستم تزریق وابستگیهای ASP.NET Core نیز معرفی نمودیم. مرحلهی آخر، ثبت این سرویسها در رجیستری ASP.NET Core Identity است:
services.AddIdentity<User, Role>(identityOptions => { }).AddUserStore<ApplicationUserStore>() .AddUserManager<ApplicationUserManager>() .AddRoleStore<ApplicationRoleStore>() .AddRoleManager<ApplicationRoleManager>() .AddSignInManager<ApplicationSignInManager>() // You **cannot** use .AddEntityFrameworkStores() when you customize everything //.AddEntityFrameworkStores<ApplicationDbContext, int>() .AddDefaultTokenProviders();
در اینجا متد AddIdentity یک سری تنظیمهای پیش فرضها این فریم ورک مانند اندازهی طول کلمهی عبور، نام کوکی و غیره را در اختیار ما قرار میدهد به همراه ثبت تعدادی سرویس پایه مانند نرمال ساز نامها و ایمیلها. سپس توسط متدهای AddUserStore، AddUserManager و ... ایی که مشاهده میکنید، سبب بازنویسی سرویسهای پیش فرض این فریم ورک به سرویسهای سفارشی خود خواهیم شد.
در این مرحلهاست که اگر Migration را اجرا کنید، کار میکند و خطای تبدیل نشدن کلاسها به یکدیگر را دریافت نخواهید کرد.
تشکیل مرحله استفادهی از ASP.NET Core Identity و ثبت اولین کاربر در بانک اطلاعاتی به صورت خودکار
روال متداول کار با امکانات کتابخانههای نوشته شدهی برای ASP.NET Core، ثبت سرویسهای پایهی آنها توسط متدهای Add است که نمونهی services.AddIdentity فوق دقیقا همین کار را انجام میدهد. مرحلهی بعد به app.UseIdentity میرسیم که کار ثبت میانافزارهای این فریم ورک را انجام میدهد. متد UseCustomIdentityServices کلاس IdentityServicesRegistry اینکار را انجام میدهد که از آن در کلاس آغازین برنامه استفاده شدهاست.
public static void UseCustomIdentityServices(this IApplicationBuilder app) { app.UseIdentity(); var identityDbInitialize = app.ApplicationServices.GetService<IIdentityDbInitializer>(); identityDbInitialize.Initialize(); identityDbInitialize.SeedData(); }
الف) متد Initialize آن، متد context.Database.Migrate را فراخوانی میکند. به همین جهت دیگر نیاز به اعمال دستی حاصل Migrations، به بانک اطلاعاتی نخواهد بود. متد Database.Migrate هر مرحلهی اعمال نشدهای را که باقی مانده باشد، به صورت خودکار اعمال میکند.
ب) متد SeedData آن، به نحو صحیحی یک Scope جدید را ایجاد کرده و توسط آن به ApplicationDbContext دسترسی پیدا میکند تا نقش Admin و کاربر Admin را به سیستم اضافه کند. همچنین به کاربر Admin، نقش Admin را نیز انتساب میدهد. تنظیمات این کاربران را نیز از فایل appsettings.json و مدخل AdminUserSeed آن دریافت میکند.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
نصب: پکیجهای متنوعی از breeze وجود دارند. برای ما بستهی زیر بهترین انتخاب میباشد. با نصب پکیج زیر، breeze در سمت سرور و کلاینت، به همراه ASP.NET Web API 2.2 and Entity Framework 6 نصب میشود:
Install-Package Breeze.WebApi2.EF6
var instance = breeze.config.initializeAdapterInstance("ajax", "angular"); instance.setHttp($http);
Install-Package Breeze.Angular
قلب تپندهی breezejs در کلاینت EntityManager است که نقش data context را در کلاینت، بازی میکند. به برخی از خصوصیات آن میپردازیم:
var manager = new breeze.EntityManager({ dataService: dataService, metadataStore: metadataStore, saveOptions: new breeze.SaveOptions({ allowConcurrentSaves: true, tag: [{}] }) });
var dataService = new breeze.DataService({ serviceName: "/breeze/"+ "Automobile", hasServerMetadata: false, namingConvention: breeze.NamingConvention.camelCase }); var metadataStore = new breeze.MetadataStore({});
- serviceName: نام سرویس دهنده یا کنترلر سمت سرور میباشد. درمورد کنترلر سمت سرور کمی جلوتر بحث میکنیم.
- metadataStore: اطلاعاتی را در مورد تمام آبجکتها (جداول دیتابیس) میدهد. مثل نام فیلدها، نوع فیلدها و...
برای کار با متادیتا دو راه وجود دارد:
1- متا دیتا را خودتان در سمت کلاینت ایجاد نمایید:
var myMetadataStore = new breeze.MetadataStore(); myMetadataStore.addEntityType({...});
var customer = function () { this.City = ""; }; myMetadataStore.registerEntityTypeCtor("Customer", customer);
2- اطلاعات را از سرور دریافت نمایید. در این صورت کنترلر شما باید دارای متد Metadata باشد. بنابراین کنترلی را در سرور به نام Automobile و با محتویات زیر ایجاد نمایید. همانطور که مشاهده میکنید، این کنترلر از ApiController مشتق شده است که تفاوت خاصی با Apiهای دیگر ندارد و تنها به BreezeController مزین شده است. این attribute به NET WebApi کمک میکند که فیلترینگ و مرتب سازی با فرمت oData را فراهم کند و همچنین درک صحیح فرمت json را نیز به کنترلر میدهد.
EFContextProvider: کامپوننتی که تعامل بین کنترلر breeze با Entity Framework را سادهتر میکند و در واقع یک wrapper بر روی دیتاکانتکس یا آبجکت کانتکس میباشد. یکی از وظایف آن ارسال متا دیتا، برای کلاینتهای breeze است.
[BreezeController] public class AutomobileController : ApiController { readonly EFContextProvider<ApplicationDbContext> _contextProvider = new EFContextProvider<ApplicationDbContext>(); [HttpGet] public string Metadata() { return _contextProvider.Metadata(); } [HttpGet] public IQueryable<Customer> Customers() { return _contextProvider.Context.Customers; } [System.Web.Http.HttpPost] public SaveResult SaveChanges(JObject saveBundle) { _contextProvider.BeforeSaveEntitiesDelegate = BeforeSaveEntities; _contextProvider.AfterSaveEntitiesDelegate = afterSaveEntities; return _contextProvider.SaveChanges(saveBundle); } protected Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap) { } private void afterSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings) { } }
- saveOptions: نحوهی چگونگی برخورد با ذخیره کردن اطلاعات را مشخص میکند. با ذخیره سازی تغییرات، متد SaveChanges سمت سرور فراخوانی میشود. در breeze میتوان به قبل و بعد از ذخیره سازی اطلاعات دسترسی داشت. یکی از موارد رایج کاربرد آن، اعمال چک کردن دسترسیها، قبل از ذخیره سازی میباشد.
برای ذخیره سازی تغییرات:
manger.saveChanges().then(function success() { }, function failer(e) { });
manger.rejectChanges()
کوئری:بعد از تعریف Entity Manger میتوانیم کوئری خود را اجرا نماییم. کوئری ما شامل گرفتن اطلاعات از جدول Customer، با مرتب سازی بر روی فیلد آیدی میباشد و با اجرا کردن کوئری میتوانیم موفقیت یا عدم موفقیت آنرا بررسی نماییم.
var query = breeze.EntityQuery .from("Customer") .orderBy("Id"); var result= manager.executeQuery(query); result.then(querySucceeded) .fail(queryFailed); query = query.where("Id", "==", 1)
var predicate = new breeze.Predicate("Id", "==", false); query = query.where(predicate) var p1 = new breeze.Predicate("IsArchived", "==", false); var p2 = breeze.Predicate("IsDone", "==", false); var predicate = p1.and(p2); query = query.where(predicate).orderBy("Id")
?$filter=IsArchived eq false&IsDone eq false &$orderby=Id
اعتبارسنجی :اعتبارسنجی در breeze، هم در سمت کلاینت و هم در سمت سرور امکان پذیر میباشد که در مثالی، در قسمت بعدی، validator سفارشی خودمان را خواهیم ساخت و به entity مورد نظر اعمال خواهیم کرد.
breeze دارای یک سری Validator در سطح پراپرتیها است:
- برای انواع اقسام dataType ها مانند Int,string,..
- برای نیازهای رایجی چون: emailAddress,creditCard,maxLength,phone,regularExpression,required,url
هم چنین در breeze امکان تغییر دادن اعتبارسنجیهای پیش فرض نیز وجود دارند. برای مثال برای اینکه در فیلدهای required بتوان متن خالی هم وارد کرد، از دستور زیر میتوان استفاده کرد:
breeze.Validator.required({ allowEmptyStrings: true });
ردیابی تغییرات: هر آیتم Entity دارای EntityAspect است که وضعیت آنرا مشخص میکند و میتواند یکی از وضعیتهای Added،Modified،Deleted،Detached،Unchanged باشد. با مشخص کردن حالت هر آیتم، با فراخوانی SaveChanges تغییرات بر روی دیتابیس اعمال میگردد.
ایجاد آیتم جدید:
manager.createEntity('Customer', jsonValue);
manager.createEntity("Customer", jsonValue, breeze.EntityState.Modified, breeze.MergeStrategy.OverwriteChanges)
manager.createEntity("Customer", item, breeze.EntityState.Deleted)
برای اشنایی بیشتر با امکانات Breeze، قصد داریم یک سایت ایجاد آگهی را راه اندازی کنیم. پیش نیازهای ضروری این بخش typescript ،angularjs ،requirejs هستند. قصد داریم سایتی را برای آگهیهای خرید و فروش خودرو، مشابه با سایت باما ایجاد نماییم:
امکانات این سایت:
- ثبت نام کاربران
- ثبت آگهی توسط کاربران
- ایجاد برچسبهای آگهیها
- امتیاز دهی به آگهیها
- جستجوی آگهیها
- و....
ابتدا نصب پکیجهای زیر
Install-Package angularjs Install-Package angularjs.TypeScript.DefinitelyTyped Install-Package bootstrap Install-Package bootstrap.TypeScript.DefinitelyTyped Install-Package jQuery Install-Package jquery.TypeScript.DefinitelyTyped Install-Package RequireJS Install-Package requirejs.TypeScript.DefinitelyTyped bower install angularAMD
مدلهای برنامه:
ایجاد کلاس BaseEntity
public class BaseEntity { public int Id { get; set; } public bool Status { get; set; } public DateTime CreatedDateTime { get; set; } }
public class Ad : BaseEntity { public string Title { get; set; } public float Price { get; set; } public double Rating { get; set; } public int? RatingNumber { get; set; } public string UserId { get; set; } public DateTime ModifieDateTime { get; set; } public string Description { get; set; } public virtual ICollection<Comment> Comments { get; set; } public virtual IdentityUser User { get; set; } public virtual ICollection<AdLabel> Labels { get; set; } public virtual ICollection<AdMedia> Medias { get; set; } }
ایجاد جدول برچسب
public class Label { public int Id { get; set; } public string Title { get; set; } public int? ParentId { get; set; } public virtual Label Parent { get; set; } public virtual ICollection<Label> Items { get; set; } }
ایجاد جدول مدیا
public class Media { public int Id { get; set; } public string Name { get; set; } public string MimeType { get; set; } }
public class AdLabel { public int Id { get; set; } public virtual Ad Ad { get; set; } public virtual Label Label { get; set; } [Index("IX_AdLabel", 1, IsUnique = true)] public int AdId { get; set; } [Index("IX_AdLabel", 2, IsUnique = true)] public int LabelId { get; set; } public string Value { get; set; } }
public class AdMedia { public int Id { get; set; } public virtual Ad Ad { get; set; } public virtual Media Media { get; set; } [Index("IX_AdMedia", 1, IsUnique = true)] public int AdId { get; set; } [Index("IX_AdMedia", 2, IsUnique = true)] public int MediaId { get; set; } }
public class Comment : BaseEntity { public string Body { get; set; } public double Rating { get; set; } public int? RatingNumber { get; set; } public string EntityName { get; set; } public string UserId { get; set; } public int? ParentId { get; set; } public int? AdId { get; set; } public virtual Comment Parent { get; set; } public virtual Ad Ad { get; set; } public virtual ICollection<Comment> Items { get; set; } public virtual IdentityUser User { get; set; } }
public class Customer:BaseEntity { public string UserId { get; set; } public virtual string DisplayName { get; set; } public virtual string BirthDay { get; set; } public string City { get; set; } public string Address { get; set; } public int? MediaId { get; set; } public bool? NewsLetterSubscription { get; set; } public string PhoneNumber { get; set; } public virtual IdentityUser User { get; set; } public virtual Media Media { get; set; } }
public class Rating { public int Id { get; set; } public string UserId { get; set; } public Double Rate { get; set; } public string EntityName { get; set; } public int DestinationId { get; set; } }
اضافه کردن مدلهای برنامه به ApplicationDbContext
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public DbSet<Ad> Ads { get; set; } public DbSet<AdLabel> AdLabels { get; set; } public DbSet<AdMedia> AdMedias { get; set; } public DbSet<Comment> Comments { get; set; } public DbSet<Label> Labels { get; set; } public DbSet<Media> Medias { get; set; } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
لود کردن فایل main.js در فایل layout.cshtml ترجیحا در انتهای body
<script src="~/Scripts/require.js" data-main="/app/main"></script>
RequireJS کتابخانهی جاوااسکریپتی برای بارگزاری فایلها در صورت نیاز میباشد. تنها کاری که ما باید انجام بدهیم این است که کدهای خود را داخل moduleها قرار دهیم (در فایلهای جداگانه) و RequireJS در صورت نیاز آنها را load خواهد کرد. همچنین RequireJS وابستگی بین moduleها را نیز مدیریت میکند.
ایجاد فایل main.ts
path: مسیر فایلهای جاوا اسکریپتی
shim: وابستگیهای فایلها(ماژول ها) و export کردن آنها را مشخص میکند.
requirejs.config({ paths: { "app": "app", "angularAmd":"/Scripts/angularAmd", "angular": "/Scripts/angular", "bootstrap": "/Scripts/bootstrap", "angularRoute": "/Scripts/angular-route", "jquery": "/Scripts/jquery-2.2.2", }, waitSeconds: 0, shim: { "angular": { exports: "angular" }, "angularRoute": { deps: ["angular"] }, "bootstrap": { deps: ["jquery"] }, "app": { deps: ["bootstrap","angularRoute"] } } }); require(["app"]);
ایجاد فایل app.ts: کارهایی که در فایل app انجام دادهایم:
ایجاد کنترلر SecurityCtrl و اعمال آن به تگ body
<body ng-controller="SecurityCtrl"> ... </body>
"use strict"; module AdApps { class SecurityCtrl { private $scope: Interfaces.IAdvertismentScope; constructor($scope: Interfaces.IAdvertismentScope) { // security check this.$scope = $scope; } } define(["angularAmd", "angular"], (angularAmd, ng) => { angularAmd = angularAmd.__proto__; var app = ng.module("AngularTypeScript", ['ngRoute']); var viewPath = "app/views/"; var controllerPath = "app/controller/"; app.config(['$routeProvider', $routeProvider => { $routeProvider .when("/", angularAmd.route({ templateUrl: viewPath + "home.html", controllerUrl: controllerPath + "home .js" })) .otherwise({ redirectTo: '/' }); } ]); app.controller('SecurityCtrl', ['$scope', SecurityCtrl]); return angularAmd.bootstrap(app); })}
اشتراکها
سوالات مصاحبه ای ASP.NET Core
یک روش کار کردن با پروژههای SPA، توسعهی مجزای قسمتهای front-end و back-end است. برای مثال پروژهی React را به صورت جداگانهای توسعه میدهیم، پروژهی ASP.NET Core را نیز به همین صورت. هنگام آزمایش برنامه، در یکی دستور npm start را اجرا میکنیم تا وب سرور آزمایشی React، آنرا در آدرس http://localhost:3000 قابل دسترسی کند و در دیگری دستور dotnet watch run را صادر میکنیم تا برنامهی وب ASP.NET Core را بر روی آدرس https://localhost:5001 مهیا کند. سپس برای اینکه از پورت 3000 بتوان با پورت 5001 کار کرد، نیاز خواهد بود تا CORS را در برنامهی ASP.NET Core فعالسازی کنیم. در حین ارائهی نهایی برنامه نیز هر کدام را به صورت مجزا publish کرده و بعد هم خروجی نهایی پروژهی SPA را در پوشهی wwwroot برنامهی وب کپی میکنیم تا قابل دسترسی و استفاده شود. روش دیگری نیز برای یکی/ساده سازی این تجربه وجود دارد که در این مطلب به آن خواهیم پرداخت.
پیشنیاز: ایجاد یک برنامهی خالی React و ASP.NET Core
یک پوشهی خالی را ایجاد کرده و در آن دستور dotnet new react را صادر کنید، تا قالب خاص پروژههای React یکی سازی شدهی با پروژههای ASP.NET Core، یک پروژهی جدید را ایجاد کند.
همانطور که در تصویر فوق نیز مشاهده میکنید، این پروژه از دو برنامه تشکیل شدهاست:
الف) برنامهی SPA که در پوشهی ClientApp قرار گرفتهاست و شامل کدهای کامل یک برنامهی React است.
ب) برنامهی سمت سرور ASP.NET Core که یک برنامهی متداول وب، به همراه فایل Startup.cs و سایر فایلهای مورد نیاز آن است.
در ادامه نکات ویژهی ساختار این پروژه را بررسی خواهیم کرد.
تجربهی توسعهی برنامهها توسط این قالب ویژه
اکنون اگر این پروژهی وب را برای مثال با فشردن دکمهی F5 و یا اجرای دستور dotnet run، اجرا کنیم، چه اتفاقی رخ میدهد؟
- به صورت خلاصه برنامهی ASP.NET Core شروع به کار کرده و سبب ارائه همزمان برنامهی SPA نیز خواهد شد.
- پورتی که برنامهی وب بر روی آن قرار دارد، با پورتی که برنامهی React بر روی روی آن ارائه میشود، یکی است. یعنی نیازی به تنظیمات CORS را ندارد.
- در این حالت اگر در برنامهی React تغییری را ایجاد کنیم (در هر قسمتی از آن)، hot reloading آن هنوز هم برقرار است و سبب بارگذاری مجدد برنامهی SPA در مرورگر خواهد شد و برای اینکار نیازی به توقف و راه اندازی مجدد برنامهی ASP.NET Core نیست.
اما این تجربهی روان کاربری و توسعه، چگونه حاصل شدهاست؟
بررسی ساختار فایل Startup.cs یک پروژهی مبتنی بر dotnet new react
برای درک نحوهی عملکرد این قالب ویژه، نیاز است از فایل Startup.cs آن شروع کرد.
در ابتدا تعریف فضای نام SpaServices را مشاهده میکنید. بستهی متناظر با آن در فایل csproj برنامه به صورت زیر ثبت شدهاست:
این بسته، همان بستهی جدید SpaServices است و در NET 5x. نیز پشتیبانی خواهد شد .
در متد ConfigureServices، ثبت سرویسهای مرتبط با فایلهای استاتیک پروژهی SPA، توسط متد AddSpaStaticFiles صورت گرفتهاست. در اینجا RootPath آن، به پوشهی ClientApp/build اشاره میکند. البته این پوشه هنوز در این ساختار، قابل مشاهده نیست؛ اما زمانیکه پروژهی ASP.NET Core را برای ارائهی نهایی، publish کردیم، به صورت خودکار ایجاد شده و حاوی فایلهای قابل ارائهی برنامهی React نیز خواهد بود.
قسمت مهم دیگر کلاس آغازین برنامه، متد Configure آن است:
در اینجا ثبت سه میان افزار جدید را مشاهده میکنید:
- متد UseSpaStaticFiles، سبب ثبت میانافزاری میشود که امکان دسترسی به فایلهای استاتیک پوشهی ClientApp حاوی برنامهی React را میسر میکند؛ مسیر این پوشه را در متد ConfigureServices تنظیم کردیم.
- متد UseSpa، سبب ثبت میانافزاری میشود که دو کار مهم را انجام میدهد:
1- کار اصلی آن، ثبت مسیریابی معروف catch all است تا مسیریابیهایی را که توسط کنترلرهای برنامهی ASP.NET Core مدیریت نمیشوند، به سمت برنامهی React هدایت کند. برای مثال مسیر https://localhost:5001/api/users به یک کنترلر API برنامهی سمت سرور ختم میشود، اما سایر مسیرها مانند https://localhost:5001/login قرار است صفحهی login برنامهی سمت کلاینت SPA را نمایش دهند و متناظر با اکشن متد خاصی در کنترلرهای برنامهی وب ما نیستند. در این حالت، کار این مسیریابی catch all، نمایش صفحهی پیشفرض برنامهی SPA است.
2- بررسی میکند که آیا شرایط IsDevelopment برقرار است؟ آیا در حال توسعهی برنامه هستیم؟ اگر بله، میانافزار دیگری را به نام UseReactDevelopmentServer، اجرا و ثبت میکند.
برای درک عملکرد میانافزار ReactDevelopmentServer نیاز است به سورس آن مراجعه کرد. این میانافزار بر اساس پارامتر start ای که دریافت میکند، سبب اجرای npm run start خواهد شد. به این ترتیب دیگر نیازی به اجرای جداگانهی این دستور نخواهد بود و همچنین این اجرا، به همراه تنظیمات proxy مخصوصی نیز هست تا پورت اجرایی برنامهی React و برنامهی ASP.NET Core یکی شده و دیگر نیازی به تنظیمات CORS مخصوص برنامههای React نباشد. بنابراین hot reloading ای که از آن صحبت شد، توسط ASP.NET Core مدیریت نمیشود. در پشت صحنه همان npm run start اصلی برنامههای React، در حال اجرای وب سرور آزمایشی React است که از hot reloading پشتیبانی میکند.
یک مشکل: با این تنظیم، هربار که برنامهی ASP.NET Core اجرا میشود (به علت تغییرات در کدها و فایلهای پروژه)، سبب اجرای مجدد و پشت صحنهی react development server نیز خواهد شد که ... آغاز برنامه را در حالت توسعه، کند میکند. برای رفع این مشکل میتوان این وب سرور توسعهی برنامههای React را به صورت جداگانهای اجرا کرد و فقط تنظیمات پروکسی آنرا در اینجا ذکر نمود:
در اینجا فقط کافی است سطر UseReactDevelopmentServer را با تنظیم UseProxyToSpaDevelopmentServer که به آدرس وب سرور توسعهی برنامههای React اشاره میکند، تنظیم کنیم. بدیهی است در اینجا حالت باید از طریق خط فرمان به پوشهی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور، راه اندازی شود.
تغییرات ویژهی فایل csproj برنامه
اگر به فایل csproj برنامه دقت کنیم، دو تغییر جدید نیز در آن قابل مشاهده هستند:
الف) نصب خودکار وابستگیهای برنامهی client
در این تنظیم، در حالت build و debug، ابتدا بررسی میکند که آیا پوشهی node_modules برنامهی SPA وجود دارد؟ اگر خیر، ابتدا مطمئن میشود که node.js بر روی سیستم نصب است و سپس دستور npm install را صادر میکند تا تمام وابستگیهای برنامهی client، دریافت و نصب شوند.
ب) یکی کردن تجربهی publish برنامهی ASP.NET Core با publish پروژههای React
میانافزار ReactDevelopmentServer کار اجرا و پروکسی دستور npm run start را در حالت توسعه انجام میدهد. اما در حالت ارائهی نهایی چطور؟ در اینجا نیاز است دستور npm run build اجرا شده و فایلهای مخصوص ارائهی نهایی برنامهی React تولید و سپس به پوشهی wwwroot، کپی شوند. تنظیم فوق، دقیقا همین کار را در حین publish برنامهی ASP.NET Core، به صورت خودکار انجام میدهد و شامل این مراحل است:
- ابتدا npm install را جهت اطمینان از به روز بودن وابستگیهای برنامه مجددا اجرا میکند.
- سپس npm run build را برای تولید فایلهای قابل ارائهی برنامهی React اجرا میکند.
- در آخر تمام فایلهای پوشهی ClientApp/build تولیدی را به بستهی نهایی توزیعی برنامهی ASP.NET Core، اضافه میکند.
پیشنیاز: ایجاد یک برنامهی خالی React و ASP.NET Core
یک پوشهی خالی را ایجاد کرده و در آن دستور dotnet new react را صادر کنید، تا قالب خاص پروژههای React یکی سازی شدهی با پروژههای ASP.NET Core، یک پروژهی جدید را ایجاد کند.
همانطور که در تصویر فوق نیز مشاهده میکنید، این پروژه از دو برنامه تشکیل شدهاست:
الف) برنامهی SPA که در پوشهی ClientApp قرار گرفتهاست و شامل کدهای کامل یک برنامهی React است.
ب) برنامهی سمت سرور ASP.NET Core که یک برنامهی متداول وب، به همراه فایل Startup.cs و سایر فایلهای مورد نیاز آن است.
در ادامه نکات ویژهی ساختار این پروژه را بررسی خواهیم کرد.
تجربهی توسعهی برنامهها توسط این قالب ویژه
اکنون اگر این پروژهی وب را برای مثال با فشردن دکمهی F5 و یا اجرای دستور dotnet run، اجرا کنیم، چه اتفاقی رخ میدهد؟
- به صورت خلاصه برنامهی ASP.NET Core شروع به کار کرده و سبب ارائه همزمان برنامهی SPA نیز خواهد شد.
- پورتی که برنامهی وب بر روی آن قرار دارد، با پورتی که برنامهی React بر روی روی آن ارائه میشود، یکی است. یعنی نیازی به تنظیمات CORS را ندارد.
- در این حالت اگر در برنامهی React تغییری را ایجاد کنیم (در هر قسمتی از آن)، hot reloading آن هنوز هم برقرار است و سبب بارگذاری مجدد برنامهی SPA در مرورگر خواهد شد و برای اینکار نیازی به توقف و راه اندازی مجدد برنامهی ASP.NET Core نیست.
اما این تجربهی روان کاربری و توسعه، چگونه حاصل شدهاست؟
بررسی ساختار فایل Startup.cs یک پروژهی مبتنی بر dotnet new react
برای درک نحوهی عملکرد این قالب ویژه، نیاز است از فایل Startup.cs آن شروع کرد.
// ... using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; namespace dotnet_template_sample { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); // In production, the React files will be served from this directory services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/build"; }); }
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.2" /> </ItemGroup>
در متد ConfigureServices، ثبت سرویسهای مرتبط با فایلهای استاتیک پروژهی SPA، توسط متد AddSpaStaticFiles صورت گرفتهاست. در اینجا RootPath آن، به پوشهی ClientApp/build اشاره میکند. البته این پوشه هنوز در این ساختار، قابل مشاهده نیست؛ اما زمانیکه پروژهی ASP.NET Core را برای ارائهی نهایی، publish کردیم، به صورت خودکار ایجاد شده و حاوی فایلهای قابل ارائهی برنامهی React نیز خواهد بود.
قسمت مهم دیگر کلاس آغازین برنامه، متد Configure آن است:
// ... using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; namespace dotnet_template_sample { public class Startup { // ... public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseStaticFiles(); app.UseSpaStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); }); app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { spa.UseReactDevelopmentServer(npmScript: "start"); } }); } } }
- متد UseSpaStaticFiles، سبب ثبت میانافزاری میشود که امکان دسترسی به فایلهای استاتیک پوشهی ClientApp حاوی برنامهی React را میسر میکند؛ مسیر این پوشه را در متد ConfigureServices تنظیم کردیم.
- متد UseSpa، سبب ثبت میانافزاری میشود که دو کار مهم را انجام میدهد:
1- کار اصلی آن، ثبت مسیریابی معروف catch all است تا مسیریابیهایی را که توسط کنترلرهای برنامهی ASP.NET Core مدیریت نمیشوند، به سمت برنامهی React هدایت کند. برای مثال مسیر https://localhost:5001/api/users به یک کنترلر API برنامهی سمت سرور ختم میشود، اما سایر مسیرها مانند https://localhost:5001/login قرار است صفحهی login برنامهی سمت کلاینت SPA را نمایش دهند و متناظر با اکشن متد خاصی در کنترلرهای برنامهی وب ما نیستند. در این حالت، کار این مسیریابی catch all، نمایش صفحهی پیشفرض برنامهی SPA است.
2- بررسی میکند که آیا شرایط IsDevelopment برقرار است؟ آیا در حال توسعهی برنامه هستیم؟ اگر بله، میانافزار دیگری را به نام UseReactDevelopmentServer، اجرا و ثبت میکند.
برای درک عملکرد میانافزار ReactDevelopmentServer نیاز است به سورس آن مراجعه کرد. این میانافزار بر اساس پارامتر start ای که دریافت میکند، سبب اجرای npm run start خواهد شد. به این ترتیب دیگر نیازی به اجرای جداگانهی این دستور نخواهد بود و همچنین این اجرا، به همراه تنظیمات proxy مخصوصی نیز هست تا پورت اجرایی برنامهی React و برنامهی ASP.NET Core یکی شده و دیگر نیازی به تنظیمات CORS مخصوص برنامههای React نباشد. بنابراین hot reloading ای که از آن صحبت شد، توسط ASP.NET Core مدیریت نمیشود. در پشت صحنه همان npm run start اصلی برنامههای React، در حال اجرای وب سرور آزمایشی React است که از hot reloading پشتیبانی میکند.
یک مشکل: با این تنظیم، هربار که برنامهی ASP.NET Core اجرا میشود (به علت تغییرات در کدها و فایلهای پروژه)، سبب اجرای مجدد و پشت صحنهی react development server نیز خواهد شد که ... آغاز برنامه را در حالت توسعه، کند میکند. برای رفع این مشکل میتوان این وب سرور توسعهی برنامههای React را به صورت جداگانهای اجرا کرد و فقط تنظیمات پروکسی آنرا در اینجا ذکر نمود:
// replace spa.UseReactDevelopmentServer(npmScript: "start"); // with spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
تغییرات ویژهی فایل csproj برنامه
اگر به فایل csproj برنامه دقت کنیم، دو تغییر جدید نیز در آن قابل مشاهده هستند:
الف) نصب خودکار وابستگیهای برنامهی client
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <!-- Ensure Node.js is installed --> <Exec Command="node --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" /> </Target>
ب) یکی کردن تجربهی publish برنامهی ASP.NET Core با publish پروژههای React
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish"> <!-- As part of publishing, ensure the JS resources are freshly built in production mode --> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" /> <!-- Include the newly-built files in the publish output --> <ItemGroup> <DistFiles Include="$(SpaRoot)build\**" /> <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> <RelativePath>%(DistFiles.Identity)</RelativePath> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <ExcludeFromSingleFile>true</ExcludeFromSingleFile> </ResolvedFileToPublish> </ItemGroup> </Target>
- ابتدا npm install را جهت اطمینان از به روز بودن وابستگیهای برنامه مجددا اجرا میکند.
- سپس npm run build را برای تولید فایلهای قابل ارائهی برنامهی React اجرا میکند.
- در آخر تمام فایلهای پوشهی ClientApp/build تولیدی را به بستهی نهایی توزیعی برنامهی ASP.NET Core، اضافه میکند.
ممنون از نکاتی که ذکر کردید . از نکات ذکر شده برجسته ترینش استفاده از انواع dynamic هستش که کار رو ساده میکنه ولی خب مشکلاتی رو هم به وجود میاره مثلا اشکال زدایی رو سخت میکنه. هر چند که این نوع کار خودش رو با Reflection در زمان اجرا انجام میده و استفاده از اون رو ساده میکنه ولی خب استفاده مستقیم از روشهای سطح پایینتر مزایایی رو هم داره مثلا مثال زیر رو در نظر بگیرید
public bool sample(object query) { System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly; foreach (var item in query.GetType().GetProperties(flags)) { string test = item.GetValue(query,null).ToString(); // Do Something ... } return false; }
خب حلا فرض کنید ما میخوایم یک Table Generator بسازیم که بر اساس لیست نتایج بازگشتی توسط query بازگشت داده شده توسط Entity framework یک جدول رو برامون ایجاد میکنه . طبیعی هست که هر بار ما یک نوع داده ای بی نام رو براش ارسال میکنیم اگه ما بخوایم نوع dynamic رو به عنوان پارامتر تابع تعریف کنیم نمیتونیم به فیلدهای نوع بی نام دسترسی داشته باشیم چرا که هر بار فیلدها فرق میکنه و این تابع قراره در زمان اجرا فیلدها رو تشخیص بده ولی در انواع dynamic ما نام فیلد رو در زمان طراحی مشخص میکنیم و در زمان اجرا توسط نوع dynamic بسط داده میشه که این موضوع واسه همچین مواردی کارایی نداره ... و باید در نظر داشته که انواع dynamic فقط میتونن به فیلدهای public دسترسی داشته باشن ولی ما با reflection میتونیم محدودهی دسترسی رو مشخص کنیم...
و اما در رابطه با مطلب بالا : بر فرض ما مجموعه ای از دادهها رو توسط Entity framework واکشی کردیم و یک نوع بی نام داریم و حالا میخوایم مثلا با Table Generator فرضی دو جدول رو از همین یک بار واکشی ایجاد کنیم که هر کدوم شامل یک سری فیلدها هستن و یک سری فیلدها رو از نوع بی نام واکشی شده شامل نمیشن . ما میتونیم یک تابع داشته باشیم که لیست نوع بی نام مرجع رو براش ارسال کنیم و یک نوع بی نام هم به عنوان یک پارامتر به صورت inline براش بفرستیم که فیلدهای مورد نظرمون رو از نوع بی نام مرجع شامل میشه . حالا با کمی توسعه CreateGenericListFromAnonymous و مپ کردن نوع بی نام مرجع با نوع بی نام ارسال شده توسط پارامتر و استفاده از Reflection میتونیم فقط فیلدهایی رو که به صورت inline مشخص کردیم داشته باشیم و با یک بار واکشی اطلاعات چندین بار اون رو با شکلهای مختلف پردازش کنیم و .... این فقط یک مثال بود و مطلب بالا صرفا ایده ای بود که دوستان بتونن کارهای خلاقانه ای رو از طریق اون انجام بدن ....
Domain-Driven Refactoring - Jimmy Bogard - NDC London 2022
Books, workshops, storming and more, all build up an idealized domain model. All describe great techniques for domain-driven greenfield applications. But what about the code we have? How can we take what's already built, and move it towards a better, more cohesive design?
In this session, we'll look at anemic, procedural, boring code and examine code smells that can point us in the right direction. We'll also look at standard design patterns for more complex behaviors and models, and how to recognize when (and when not) to apply them. Finally, we'll cover how to safely apply refactoring techniques to achieve our domain-driven model nirvana.
ممنون؛ خروجی که توی برنامه نشون میده به این صورت است:
البته اگر نیاز میبینید تا کد استارت اپ را هم بفرستم شاید بتونه کمک کنه.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:45225/api/account/login application/json; charset=utf-8 Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:45225/api/account/login application/json; charset=utf-8 Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Login", controller = "Account", area = ""}. Executing action DrugExchange.Controllers.AccountController.Login (DrugExchange) Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Login", controller = "Account", area = ""}. Executing action DrugExchange.Controllers.AccountController.Login (DrugExchange) Microsoft.EntityFrameworkCore.Infrastructure:Information: Entity Framework Core 2.1.1-rtm-30846 initialized 'ApplicationDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: MaxPoolSize=128 CommandTimeout=180 Microsoft.EntityFrameworkCore.Infrastructure:Information: Entity Framework Core 2.1.1-rtm-30846 initialized 'ApplicationDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: MaxPoolSize=128 CommandTimeout=180 Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor:Information: Executing ObjectResult, writing value of type 'DrugExchange.Common.ApiError'. Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor:Information: Executing ObjectResult, writing value of type 'DrugExchange.Common.ApiError'. Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action DrugExchange.Controllers.AccountController.Login (DrugExchange) in 42.4741ms Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action DrugExchange.Controllers.AccountController.Login (DrugExchange) in 42.4741ms Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 62.5345ms 500 application/ion+json; charset=utf-8 Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 62.5345ms 500 application/ion+json; charset=utf-8 Microsoft.AspNetCore.Server.Kestrel:Information: Connection id "0HLFGFSKPQEQC", Request id "0HLFGFSKPQEQC:00000001": the application completed without reading the entire request body. Microsoft.AspNetCore.Server.Kestrel:Information: Connection id "0HLFGFSKPQEQC", Request id "0HLFGFSKPQEQC:00000001": the application completed without reading the entire request body.