پروژهی ایجاد شدهی با قالب استاندارد «dotnet new blazorwasm --hosted --auth individual» از identity server استفاده میکند. ما در این سری از روش دیگری استفاده میکنیم که کاری به identity server ندارد.
آیا این فریم ورک قابلیت اضافه شدن Identity رو داره؟
میخواستم از Identity در این فریم ورک استفاده کنم نشد چون باید از dbcontext داخل فریم ورک استفاده کرد
آیا وقتی از identity استفاده میکنیم نیازی هست که از JWT برای تولید توکن و پر کردن claim استفاده کنیم ؟ اگر با identity چنین امکانی وجود داره ممنون میشم در موردش توضیحی بدید .
عموما مدلهای ASP.NET MVC یک چنین شکلی را دارند:
و ViewModel مورد استفاده برای نمونه چنین ساختاری را دارد:
مشکلی که در اینجا وجود دارد، نیاز به کپی و تکرار تک تک ویژگیهای (Data Annotations/Attributes) خاصیتهای مدل، به خواص مشابه آنها در ViewModel است؛ از این جهت که میخواهیم برچسب خواص ViewModel، از ویژگی Display دریافت شوند و همچنین اعتبارسنجیهای فیلدهای اجباری و بررسی حداقل و حداکثر طول فیلدها نیز حتما اعمال شوند (هم در سمت کاربر و هم در سمت سرور).
در ادامه قصد داریم راه حلی را به کمک جایگزین سازی Providerهای توکار ASP.NET MVC با نمونهی سازگار با AutoMapper، ارائه دهیم، به نحوی که دیگر نیازی نباشد تا این ویژگیها را در ViewModelها تکرار کرد.
قسمتهایی از ASP.NET MVC که باید جهت انتقال خودکار ویژگیها تعویض شوند
ASP.NET MVC به صورت توکار دارای یک ModelMetadataProviders.Current است که از آن جهت دریافت ویژگیهای هر خاصیت استفاده میکند. میتوان این تامین کنندهی ویژگیها را به نحو ذیل سفارشی سازی نمود.
در اینجا IConfigurationProvider همان Mapper.Engine.ConfigurationProvider مربوط به AutoMapper است. از آن جهت استخراج اطلاعات نگاشتهای AutoMapper استفاده میکنیم. برای مثال کدام خاصیت Model به کدام خاصیت ViewModel نگاشت شدهاست. اینکارها توسط متد الحاقی GetMappedAttributes انجام میشوند که در ادامهی مطلب معرفی خواهد شد.
شبیه به همین کار را باید برای ModelValidatorProviders.Providers نیز انجام داد. در اینجا یکی از تامین کنندههای ModelValidator، از نوع DataAnnotationsModelValidatorProvider است که حتما نیاز است این مورد را نیز به نحو ذیل سفارشی سازی نمود. در غیراینصورت error messages موجود در ویژگیهای تعریف شده، به صورت خودکار منتقل نخواهند شد.
و در اینجا پیاده سازی متد GetMappedAttributes را ملاحظه میکنید.
ASP.NET MVC هر زمانیکه قرار است توسط متدهای توکار خود مانند Html.TextBoxFor, Html.ValidationMessageFor، اطلاعات خاصیتها را تبدیل به المانهای HTML کند، از تامین کنندههای فوق جهت دریافت اطلاعات ویژگیهای مرتبط با هر خاصیت استفاده میکند. در اینجا فرصت داریم تا ویژگیهای مدل را از تنظیمات AutoMapper دریافت کرده و سپس بجای ویژگیهای خاصیت معادل ViewModel درخواست شده، بازگشت دهیم. به این ترتیب ASP.NET MVC تصور خواهد کرد که ViewModel ما نیز دقیقا دارای همان ویژگیهای Model است.
ثبت تامین کنندههای سفارشی سازی شده توسط AutoMapper
پس از تهیهی تامین کنندههای انتقال ویژگیها، اکنون نیاز است آنها را به ASP.NET MVC معرفی کنیم:
در اینجا ModelMetadataProviders.Current با MappedMetadataProvider جایگزین شدهاست.
در قسمت کار با ModelValidatorProviders.Providers، ابتدا صرفا همان تامین کنندهی از نوع DataAnnotationsModelValidatorProvider پیش فرض، یافت شده و حذف میشود. سپس تامین کنندهی سفارشی سازی شدهی خود را معرفی میکنیم تا جایگزین آن شود.
مثالی جهت آزمایش انتقال خودکار ویژگیهای مدل به ViewModel
کنترلر مثال برنامه به شرح زیر است. در اینجا از متد Mapper.Map جهت تبدیل خودکار مدل کاربر به ViewModel آن استفاده شدهاست:
با این View که جهت ثبت اطلاعات مورد استفاده قرار میگیرد. این View، اطلاعات مدل خود را از ViewModel معرفی شدهی در ابتدای بحث دریافت میکند:
در این حالت اگر برنامه را اجرا کنیم به شکل زیر خواهیم رسید:
در این شکل هر چند نوع مدل View مورد استفاده از ViewModel ایی تامین شدهاست که دارای هیچ ویژگی و Data Annotations/Attributes نیست، اما برچسب هر فیلد از ویژگی Display دریافت شدهاست. همچنین اعتبارسنجی سمت کاربر فعال بوده و برچسبهای آنها نیز به درستی دریافت شدهاند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
public class UserModel { public int Id { get; set; } [Required(ErrorMessage = "(*)")] [Display(Name = "نام")] [StringLength(maximumLength: 10, MinimumLength = 3, ErrorMessage = "نام باید حداقل 3 و حداکثر 10 حرف باشد")] public string FirstName { get; set; } [Required(ErrorMessage = "(*)")] [Display(Name = "نام خانوادگی")] [StringLength(maximumLength: 10, MinimumLength = 3, ErrorMessage = "نام خانوادگی باید حداقل 3 و حداکثر 10 حرف باشد")] public string LastName { get; set; } }
public class UserViewModel { public string FirstName { get; set; } public string LastName { get; set; } }
در ادامه قصد داریم راه حلی را به کمک جایگزین سازی Providerهای توکار ASP.NET MVC با نمونهی سازگار با AutoMapper، ارائه دهیم، به نحوی که دیگر نیازی نباشد تا این ویژگیها را در ViewModelها تکرار کرد.
قسمتهایی از ASP.NET MVC که باید جهت انتقال خودکار ویژگیها تعویض شوند
ASP.NET MVC به صورت توکار دارای یک ModelMetadataProviders.Current است که از آن جهت دریافت ویژگیهای هر خاصیت استفاده میکند. میتوان این تامین کنندهی ویژگیها را به نحو ذیل سفارشی سازی نمود.
در اینجا IConfigurationProvider همان Mapper.Engine.ConfigurationProvider مربوط به AutoMapper است. از آن جهت استخراج اطلاعات نگاشتهای AutoMapper استفاده میکنیم. برای مثال کدام خاصیت Model به کدام خاصیت ViewModel نگاشت شدهاست. اینکارها توسط متد الحاقی GetMappedAttributes انجام میشوند که در ادامهی مطلب معرفی خواهد شد.
public class MappedMetadataProvider : DataAnnotationsModelMetadataProvider { private readonly IConfigurationProvider _mapper; public MappedMetadataProvider(IConfigurationProvider mapper) { _mapper = mapper; } protected override ModelMetadata CreateMetadata( IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { var mappedAttributes = containerType == null ? attributes : _mapper.GetMappedAttributes(containerType, propertyName, attributes.ToList()); return base.CreateMetadata(mappedAttributes, containerType, modelAccessor, modelType, propertyName); } }
شبیه به همین کار را باید برای ModelValidatorProviders.Providers نیز انجام داد. در اینجا یکی از تامین کنندههای ModelValidator، از نوع DataAnnotationsModelValidatorProvider است که حتما نیاز است این مورد را نیز به نحو ذیل سفارشی سازی نمود. در غیراینصورت error messages موجود در ویژگیهای تعریف شده، به صورت خودکار منتقل نخواهند شد.
public class MappedValidatorProvider : DataAnnotationsModelValidatorProvider { private readonly IConfigurationProvider _mapper; public MappedValidatorProvider(IConfigurationProvider mapper) { _mapper = mapper; } protected override IEnumerable<ModelValidator> GetValidators( ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes) { var mappedAttributes = metadata.ContainerType == null ? attributes : _mapper.GetMappedAttributes(metadata.ContainerType, metadata.PropertyName, attributes.ToList()); return base.GetValidators(metadata, context, mappedAttributes); } }
و در اینجا پیاده سازی متد GetMappedAttributes را ملاحظه میکنید.
ASP.NET MVC هر زمانیکه قرار است توسط متدهای توکار خود مانند Html.TextBoxFor, Html.ValidationMessageFor، اطلاعات خاصیتها را تبدیل به المانهای HTML کند، از تامین کنندههای فوق جهت دریافت اطلاعات ویژگیهای مرتبط با هر خاصیت استفاده میکند. در اینجا فرصت داریم تا ویژگیهای مدل را از تنظیمات AutoMapper دریافت کرده و سپس بجای ویژگیهای خاصیت معادل ViewModel درخواست شده، بازگشت دهیم. به این ترتیب ASP.NET MVC تصور خواهد کرد که ViewModel ما نیز دقیقا دارای همان ویژگیهای Model است.
public static class AutoMapperExtensions { public static IEnumerable<Attribute> GetMappedAttributes( this IConfigurationProvider mapper, Type viewModelType, string viewModelPropertyName, IList<Attribute> existingAttributes) { if (viewModelType != null) { foreach (var typeMap in mapper.GetAllTypeMaps().Where(i => i.DestinationType == viewModelType)) { var propertyMaps = typeMap.GetPropertyMaps() .Where(propertyMap => !propertyMap.IsIgnored() && propertyMap.SourceMember != null) .Where(propertyMap => propertyMap.DestinationProperty.Name == viewModelPropertyName); foreach (var propertyMap in propertyMaps) { foreach (Attribute attribute in propertyMap.SourceMember.GetCustomAttributes(true)) { if (existingAttributes.All(i => i.GetType() != attribute.GetType())) { yield return attribute; } } } } } if (existingAttributes == null) { yield break; } foreach (var attribute in existingAttributes) { yield return attribute; } } }
ثبت تامین کنندههای سفارشی سازی شده توسط AutoMapper
پس از تهیهی تامین کنندههای انتقال ویژگیها، اکنون نیاز است آنها را به ASP.NET MVC معرفی کنیم:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); Mappings.RegisterMappings(); ModelMetadataProviders.Current = new MappedMetadataProvider(Mapper.Engine.ConfigurationProvider); var modelValidatorProvider = ModelValidatorProviders.Providers .Single(provider => provider is DataAnnotationsModelValidatorProvider); ModelValidatorProviders.Providers.Remove(modelValidatorProvider); ModelValidatorProviders.Providers.Add(new MappedValidatorProvider(Mapper.Engine.ConfigurationProvider)); }
در قسمت کار با ModelValidatorProviders.Providers، ابتدا صرفا همان تامین کنندهی از نوع DataAnnotationsModelValidatorProvider پیش فرض، یافت شده و حذف میشود. سپس تامین کنندهی سفارشی سازی شدهی خود را معرفی میکنیم تا جایگزین آن شود.
مثالی جهت آزمایش انتقال خودکار ویژگیهای مدل به ViewModel
کنترلر مثال برنامه به شرح زیر است. در اینجا از متد Mapper.Map جهت تبدیل خودکار مدل کاربر به ViewModel آن استفاده شدهاست:
public class HomeController : Controller { public ActionResult Index() { var model = new UserModel { FirstName = "و", Id = 1, LastName = "ن" }; var viewModel = Mapper.Map<UserViewModel>(model); return View(viewModel); } [HttpPost] public ActionResult Index(UserViewModel data) { return View(data); } }
@model Sample12.ViewModels.UserViewModel @using (Html.BeginForm("Index", "Home", FormMethod.Post, htmlAttributes: new { @class = "form-horizontal", role = "form" })) { <div class="row"> <div class="form-group"> @Html.LabelFor(d => d.FirstName, htmlAttributes: new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.TextBoxFor(d => d.FirstName) @Html.ValidationMessageFor(d => d.FirstName) </div> </div> <div class="form-group"> @Html.LabelFor(d => d.LastName, htmlAttributes: new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.TextBoxFor(d => d.LastName) @Html.ValidationMessageFor(d => d.LastName) </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="ارسال" class="btn btn-default" /> </div> </div> </div> }
در این شکل هر چند نوع مدل View مورد استفاده از ViewModel ایی تامین شدهاست که دارای هیچ ویژگی و Data Annotations/Attributes نیست، اما برچسب هر فیلد از ویژگی Display دریافت شدهاست. همچنین اعتبارسنجی سمت کاربر فعال بوده و برچسبهای آنها نیز به درستی دریافت شدهاند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
یک روش کار کردن با پروژههای 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، اضافه میکند.
دو نوع حالت کلی کارکردن با EF وجود دارند: متصل و منقطع.
در حالت متصل مانند برنامههای متداول دسکتاپ، Context مورد استفاده در طول عمر صفحهی جاری زنده نگه داشته میشود. در این حالت اگر شیءایی اضافه شود، حذف شود یا تغییر کند، توسط EF ردیابی شده و تنها با فراخوانی متد SaveChanges، تمام این تغییرات به صورت یکجا به بانک اطلاعاتی اعمال میشوند.
در حالت غیرمتصل مانند برنامههای وب، طول عمر Context در حد طول عمر یک درخواست است. پس از آن از بین خواهد رفت و دیگر فرصت ردیابی تغییرات سمت کاربر را نخواهد یافت. در این حالت به روز رسانی کلیه تغییرات انجام شده در خواص و همچنین ارتباطات اشیاء موجود، کاری مشکل و زمانبر خواهد بود.
برای حل این مشکل، کتابخانهای به نام GraphDiff طراحی شدهاست که صرفا با فراخوانی متد UpdateGraph آن، به صورت خودکار، محاسبات تغییرات صورت گرفته در اشیاء منقطع و اعمال آنها به بانک اطلاعاتی صورت خواهد گرفت. البته ذکر متد SaveChanges پس از آن نباید فراموش شود.
اصطلاحات بکار رفته در GraphDiff
برای کار با GraphDiff نیاز است با یک سری اصطلاح آشنا بود:
Aggregate root
گرافی است از اشیاء به هم وابسته که مرجع تغییرات دادهها به شمار میرود. برای مثال یک سفارش و آیتمهای آنرا درنظر بگیرید. بارگذاری آیتمهای سفارش، بدون سفارش معنایی ندارند. بنابراین در اینجا سفارش aggregate root است.
AssociatedCollection/AssociatedEntity
حالتهای Associated به GraphDiff اعلام میکنند که اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root نباید به روز رسانی شوند. در این حالت تنها ارجاعات به روز رسانی خواهند شد.
اگر خاصیت راهبری از نوع ICollection است، حالت AssociatedCollection و اگر صرفا یک شیء ساده است، از AssociatedEntity استفاده خواهد شد.
OwnedCollection/OwnedEntity
حالتهای Owned به GraphDiff اعلام میکنند که جزئیات و همچنین ارجاعات اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root باید به روز رسانی شوند.
دریافت و نصب GraphDiff
برای نصب خودکار کتابخانهی GraphDiff میتوان از دستور نیوگت ذیل استفاده کرد:
بررسی GraphDiff در طی یک مثال
مدلهای برنامه آزمایشی، از سه کلاس ذیل که روابط many-to-many و one-to-many با یکدیگر دارند، تشکیل شدهاست:
- یک مطلب میتواند چندین برچسب داشته باشد و هر برچسب میتواند به چندین مطلب انتساب داده شود.
- هر کاربر میتواند چندین مطلب ارسال کند.
در این حالت، Context برنامه چنین شکلی را خواهد یافت:
به همراه تنظیمات به روز رسانی ساختار بانک اطلاعاتی به صورت خودکار:
در متد Seed آن یک سری اطلاعات ابتدایی ثبت شدهاند؛ یک کاربر، یک برچسب و یک مطلب.
در این تصاویر به Id هر کدام از رکوردها دقت کنید. از آنها در ادامه استفاده خواهیم کرد.
در اینجا نمونهای از نحوهی استفاده از GraphDiff را جهت به روز رسانی یک Aggregate root ملاحظه میکنید:
پارامتر اول UpdateGraph، گرافی از اشیاء است که قرار است به روز رسانی شوند.
پارامتر دوم آن، همان مباحث Owned و Associated بحث شده در ابتدای مطلب را مشخص میکنند. در اینجا چون میخواهیم هم برچسبها و هم اطلاعات کاربر مطلب اول به روز شوند، نوع رابطه را Owned تعریف کردهایم.
در حین کار با متد UpdateGraph، ذکر Idهای اشیاء منقطع از Context بسیار مهم هستند. اگر دستورات فوق را اجرا کنیم به خروجی ذیل خواهیم رسید:
- همانطور که مشخص است، چون id کاربر ذکر شده و همچنین این Id در post1 نیز درج گردیده است، صرفا نام او ویرایش گردیده است. اگر یکی از موارد ذکر شده رعایت نشوند، ابتدا کاربر جدیدی ثبت شده و سپس رابطهی مطلب و کاربر به روز رسانی خواهد شد (userId آن به userId آخرین کاربر ثبت شده تنظیم میشود).
- در حین ثبت برچسبها، چون Id=1 از پیش در بانک اطلاعاتی موجود بوده، تنها نام آن ویرایش شدهاست. در سایر موارد، برچسبهای تعریف شده صرفا اضافه شدهاند (چون Id مشخصی ندارند یا Id=12 در بانک اطلاعاتی وجود خارجی ندارد).
- چون Id مطلب مشخص شدهاست، فیلدهای عنوان و محتوای آن نیز به صورت خودکار ویرایش شدهاند.
و ... تمام این کارها صرفا با فراخوانی متدهای UpdateGraph و سپس SaveChanges رخ دادهاست.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
GraphDiffTests.zip
در حالت متصل مانند برنامههای متداول دسکتاپ، Context مورد استفاده در طول عمر صفحهی جاری زنده نگه داشته میشود. در این حالت اگر شیءایی اضافه شود، حذف شود یا تغییر کند، توسط EF ردیابی شده و تنها با فراخوانی متد SaveChanges، تمام این تغییرات به صورت یکجا به بانک اطلاعاتی اعمال میشوند.
در حالت غیرمتصل مانند برنامههای وب، طول عمر Context در حد طول عمر یک درخواست است. پس از آن از بین خواهد رفت و دیگر فرصت ردیابی تغییرات سمت کاربر را نخواهد یافت. در این حالت به روز رسانی کلیه تغییرات انجام شده در خواص و همچنین ارتباطات اشیاء موجود، کاری مشکل و زمانبر خواهد بود.
برای حل این مشکل، کتابخانهای به نام GraphDiff طراحی شدهاست که صرفا با فراخوانی متد UpdateGraph آن، به صورت خودکار، محاسبات تغییرات صورت گرفته در اشیاء منقطع و اعمال آنها به بانک اطلاعاتی صورت خواهد گرفت. البته ذکر متد SaveChanges پس از آن نباید فراموش شود.
اصطلاحات بکار رفته در GraphDiff
برای کار با GraphDiff نیاز است با یک سری اصطلاح آشنا بود:
Aggregate root
گرافی است از اشیاء به هم وابسته که مرجع تغییرات دادهها به شمار میرود. برای مثال یک سفارش و آیتمهای آنرا درنظر بگیرید. بارگذاری آیتمهای سفارش، بدون سفارش معنایی ندارند. بنابراین در اینجا سفارش aggregate root است.
AssociatedCollection/AssociatedEntity
حالتهای Associated به GraphDiff اعلام میکنند که اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root نباید به روز رسانی شوند. در این حالت تنها ارجاعات به روز رسانی خواهند شد.
اگر خاصیت راهبری از نوع ICollection است، حالت AssociatedCollection و اگر صرفا یک شیء ساده است، از AssociatedEntity استفاده خواهد شد.
OwnedCollection/OwnedEntity
حالتهای Owned به GraphDiff اعلام میکنند که جزئیات و همچنین ارجاعات اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root باید به روز رسانی شوند.
دریافت و نصب GraphDiff
برای نصب خودکار کتابخانهی GraphDiff میتوان از دستور نیوگت ذیل استفاده کرد:
PM> Install-Package RefactorThis.GraphDiff
بررسی GraphDiff در طی یک مثال
مدلهای برنامه آزمایشی، از سه کلاس ذیل که روابط many-to-many و one-to-many با یکدیگر دارند، تشکیل شدهاست:
using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; namespace GraphDiffTests.Models { public class BlogPost { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public virtual ICollection<Tag> Tags { set; get; } // many-to-many [ForeignKey("UserId")] public virtual User User { get; set; } public int UserId { get; set; } public BlogPost() { Tags = new List<Tag>(); } } public class Tag { public int Id { set; get; } [StringLength(maximumLength: 450), Required] public string Name { set; get; } public virtual ICollection<BlogPost> BlogPosts { set; get; } // many-to-many public Tag() { BlogPosts = new List<BlogPost>(); } } public class User { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } // one-to-many } }
- هر کاربر میتواند چندین مطلب ارسال کند.
در این حالت، Context برنامه چنین شکلی را خواهد یافت:
using System; using System.Data.Entity; using GraphDiffTests.Models; namespace GraphDiffTests.Config { public class MyContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<BlogPost> BlogPosts { get; set; } public DbSet<Tag> Tags { get; set; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } } }
using System.Data.Entity.Migrations; using System.Linq; using GraphDiffTests.Models; namespace GraphDiffTests.Config { public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if(context.Users.Any()) return; var user1 = new User {Name = "User 1"}; context.Users.Add(user1); var tag1 = new Tag { Name = "Tag1" }; context.Tags.Add(tag1); var post1 = new BlogPost { Title = "Title...1", Content = "Content...1", User = user1}; context.BlogPosts.Add(post1); post1.Tags.Add(tag1); base.Seed(context); } } }
در این تصاویر به Id هر کدام از رکوردها دقت کنید. از آنها در ادامه استفاده خواهیم کرد.
در اینجا نمونهای از نحوهی استفاده از GraphDiff را جهت به روز رسانی یک Aggregate root ملاحظه میکنید:
using (var context = new MyContext()) { var user1 = new User { Id = 1, Name = "User 1_1_1" }; var post1 = new BlogPost { Id = 1, Title = "Title...1_1", Content = "Body...1_1", User = user1, UserId = user1.Id }; var tags = new List<Tag> { new Tag {Id = 1, Name = "Tag1_1"}, new Tag {Id=12, Name = "Tag2_1"}, new Tag {Name = "Tag3"}, new Tag {Name = "Tag4"}, }; tags.ForEach(tag => post1.Tags.Add(tag)); context.UpdateGraph(post1, map => map .OwnedEntity(p => p.User) .OwnedCollection(p => p.Tags) ); context.SaveChanges(); }
پارامتر دوم آن، همان مباحث Owned و Associated بحث شده در ابتدای مطلب را مشخص میکنند. در اینجا چون میخواهیم هم برچسبها و هم اطلاعات کاربر مطلب اول به روز شوند، نوع رابطه را Owned تعریف کردهایم.
در حین کار با متد UpdateGraph، ذکر Idهای اشیاء منقطع از Context بسیار مهم هستند. اگر دستورات فوق را اجرا کنیم به خروجی ذیل خواهیم رسید:
- همانطور که مشخص است، چون id کاربر ذکر شده و همچنین این Id در post1 نیز درج گردیده است، صرفا نام او ویرایش گردیده است. اگر یکی از موارد ذکر شده رعایت نشوند، ابتدا کاربر جدیدی ثبت شده و سپس رابطهی مطلب و کاربر به روز رسانی خواهد شد (userId آن به userId آخرین کاربر ثبت شده تنظیم میشود).
- در حین ثبت برچسبها، چون Id=1 از پیش در بانک اطلاعاتی موجود بوده، تنها نام آن ویرایش شدهاست. در سایر موارد، برچسبهای تعریف شده صرفا اضافه شدهاند (چون Id مشخصی ندارند یا Id=12 در بانک اطلاعاتی وجود خارجی ندارد).
- چون Id مطلب مشخص شدهاست، فیلدهای عنوان و محتوای آن نیز به صورت خودکار ویرایش شدهاند.
و ... تمام این کارها صرفا با فراخوانی متدهای UpdateGraph و سپس SaveChanges رخ دادهاست.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
GraphDiffTests.zip
توضیح
پروژهی CorMon یک CMS رایگان و سورس باز برپایه ی ASP.NET Core و MongoDB میباشد که سورس آن را بر روی Github میتوانید دنبال کرده و در صورت تمایل در توسعه آن مشارکت داشته باشید.
هدف
این پروژه در اصل تلاش و تمرینی است برای اینکه چگونه یک پروژه را در بستر ASP.NET Core پیاده کنیم و آن را با دیتابیسهای NoSQL از جمله MongoDB و Redis به کار بگیریم.
معماری
معماری این پروژه تا حدود زیادی برگرفته از Onion Architecture و نیز ASP.NET Boilerplate میباشد و تا حد امکان طراحی آن ساده و خوانا در نظر گرفته شده تا مشارکت در توسعه و یا استفاده از آن راحت باشد.
ویژگی ها
در اینجا بخشی از ویژگیهای این پروژه را مشاهده میکنید که به ترتیب در حال اجرا هستند :
- استفاده از دیتابیس MongoDB
- پیاده سازی Redis Cache
- استفاده از الگوی ریپازیتوری
- استفاده از ابزار DI پیش فرض
- پیاده سازی REST API
- پیاده سازی JWT
- پیاده سازی ASP.NET Identity با پروایدر Mongo
- پیاده سازی Unit Testing
- پیاده سازی Automated UI Testing با Selenium
و ...
تصاویر
بازخوردهای پروژهها
انقضای اهراز هویت کاربر پس از رفرش کردن صفحه در پروژه ای مشابه
ابتدا تشکر میکنم بابت ارائه این پروژه.
برای یادگیری من دارم پروژه ای شبیه به پروژه شما میسازم. یعنی به عبارتی مراحل شمارو یکی ، یکی طی میکنم. حالا به جایی رسیدم که هرچقدر هم تلاش کردم متاسفانه پاسخی براش پیدا نکردم.
مشکل من اینه. زمانی که کاربر دکمه ورود رو میزنه و وارد سیستم میشه ، زمانی که صفحه رو رفرش کنه یا از صفحه ای به صفحهی دیگه بره اهراز هویتیش تموم میشه. با اینکه "مرا به خاطر داشته باش" رو هم تیک میزنم ولی باز این مشکل پیش میاد. جالب اینجاست کدهای خودتون به درستی در پروژه خودتون کار میکنه ولی وقتی میارمشون تو پروژهی خودم این مشکل پیش میاد. نمیدونم کجارو اشتباه کردم ولی میدونم یک چیزی کمه.
توجه کنید زمانی که کاربر ورود میکنه مقدار @User.Identity.IsAuthenticated برابر با true هستش. پس قطعا کاربر ورود میکنه ولی زمانی که صفحه تغییر میکنه این مقدار برابر false میشه.
کدهای کلاس Startup دقیقا همون کدهای شما و من تغییری بهشون ندادم فقط اسم کوکی رو عوض کردم یعنی اینطوری :
این هم کدهای صفحه ورود :
ممنون میشم اگر میدونید کجارو اشتباه کردم بهم بگید.
با سپاس
برای یادگیری من دارم پروژه ای شبیه به پروژه شما میسازم. یعنی به عبارتی مراحل شمارو یکی ، یکی طی میکنم. حالا به جایی رسیدم که هرچقدر هم تلاش کردم متاسفانه پاسخی براش پیدا نکردم.
مشکل من اینه. زمانی که کاربر دکمه ورود رو میزنه و وارد سیستم میشه ، زمانی که صفحه رو رفرش کنه یا از صفحه ای به صفحهی دیگه بره اهراز هویتیش تموم میشه. با اینکه "مرا به خاطر داشته باش" رو هم تیک میزنم ولی باز این مشکل پیش میاد. جالب اینجاست کدهای خودتون به درستی در پروژه خودتون کار میکنه ولی وقتی میارمشون تو پروژهی خودم این مشکل پیش میاد. نمیدونم کجارو اشتباه کردم ولی میدونم یک چیزی کمه.
توجه کنید زمانی که کاربر ورود میکنه مقدار @User.Identity.IsAuthenticated برابر با true هستش. پس قطعا کاربر ورود میکنه ولی زمانی که صفحه تغییر میکنه این مقدار برابر false میشه.
کدهای کلاس Startup دقیقا همون کدهای شما و من تغییری بهشون ندادم فقط اسم کوکی رو عوض کردم یعنی اینطوری :
public class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); app.MapSignalR(); } private static void ConfigureAuth(IAppBuilder appBuilder) { const int twoWeeks = 14; ProjectObjectFactory.Container.Configure(config => config.For<IDataProtectionProvider>() .HybridHttpOrThreadLocalScoped() .Use(() => appBuilder.GetDataProtectionProvider())); appBuilder.CreatePerOwinContext( () => ProjectObjectFactory.Container.GetInstance<ApplicationUserManager>()); appBuilder.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), ExpireTimeSpan = TimeSpan.FromDays(twoWeeks), SlidingExpiration = true, CookieName = "MyFirstCms", Provider = new CookieAuthenticationProvider { OnValidateIdentity = ProjectObjectFactory.Container.GetInstance<IApplicationUserManager>().OnValidateIdentity() } }); ProjectObjectFactory.Container.GetInstance<IApplicationRoleManager>() .SeedDatabase(); ProjectObjectFactory.Container.GetInstance<IApplicationUserManager>() .SeedDatabase(); appBuilder.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process. //appBuilder.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); // Enables the application to remember the second login verification factor such as phone or email. // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from. // This is similar to the RememberMe option when you log in. // appBuilder.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); appBuilder.UseFacebookAuthentication( appId: "fdsfdsfs", appSecret: "fdfsfs"); appBuilder.UseGoogleAuthentication( clientId: "fdsfsdfs", clientSecret: "fdsfsf"); } }
این هم کدهای صفحه ورود :
[HttpPost] [AllowAnonymous] //[CheckReferrer] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> Login(LoginViewModel model, string returnUrl) { if (!ModelState.IsValid) { return View(model); } if (!_userManager.CheckUserNameExist(model.UserName,null)) { this.AddErrors("UserName", "نام کاربری یا کلمه عبور وارد شده نادرست است"); return View(model); } if (_userManager.CheckIsUserBannedOrDelete(model.UserName)) { this.AddErrors("UserName", "حساب کاربری شما مسدود شده است"); return View(model); } if (!_userManager.IsEmailConfirmedByUserNameAsync(model.UserName)) { this.NotyWarning("برای ورود به سایت لازم است حساب خود را فعال کنید"); return RedirectToAction(MVC.Account.ActionNames.ReceiveActivatorEmail, MVC.Account.Name); } var result = await _signInManager.PasswordSignInAsync (model.UserName.ToLower(), model.Password, model.RememberMe, shouldLockout: true); switch (result) { case SignInStatus.Success: this.NotySuccess("شما با موفقیت وارد سیستم شدید"); return RedirectToLocal(returnUrl); case SignInStatus.LockedOut: this.NotyError( $"دقیقه دوباره امتحان کنید {_userManager.DefaultAccountLockoutTimeSpan} حساب شما قفل شد ! لطفا بعد از "); return View(model); case SignInStatus.Failure: this.NotyError(ModelState.GetListOfErrors()); return View(model); default: this.NotyError( "در این لحظه امکان ورود به سابت وجود ندارد . مراتب را با مسئولان سایت در میان بگذارید"); return View(model); } }
ممنون میشم اگر میدونید کجارو اشتباه کردم بهم بگید.
با سپاس
اشتراکها
توابع سیستمی در Sql server
در قسمت قبل یادگرفتیم که چگونه GraphQL را با ASP.NET Core یکپارچه کنیم و اولین GraphQL query را ایجاد و دادهها را از سرور بازیابی کردیم. البته ما به این query های ساده بسنده نخواهیم کرد. در این قسمت میخواهیم یاد بگیریم که چگونه query های پیشرفتهی GraphQL را بنویسیم و در زمان انجام این کار، نمایش دهیم که چگونه خطاها را مدیریت کنیم و علاوه بر این با queries, aliases, arguments, fragments نیز کار خواهیم کرد.
Creating Complex Types for GraphQL Queries
اگر نگاهی به owners و query (در پایان قسمت قبل) بیندازیم، متوجه خواهیم شد که یک لیست از خصوصیات مدل Owner که در OwnerType معرفی شدهاند، نسبت به کوئری برگشت داده میشود. OwnerType شامل فیلدهای Id , Name و Address میباشد. یک Owner میتواند چندین account مرتبط با خود را داشته باشد. هدف این است که در owners ،query لیست account های مربوط به هر owner را بازگشت دهیم.
قبل از اضافه کردن فیلد Accounts در کلاس OwnerType نیاز است کلاس AccountType را ایجاد کنیم. در ادامه یک کلاس را به نام AccountType در پوشه GraphQLTypes ایجاد میکنیم.
public class AccountType : ObjectGraphType<Account> { public AccountType() { Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the account object."); Field(x => x.Description).Description("Description property from the account object."); Field(x => x.OwnerId, type: typeof(IdGraphType)).Description("OwnerId property from the account object."); } }
همانطور که مشخص است، خصوصیت Type را از کلاس Account، معرفی نکردهایم (در ادامه اینکار را انجام خواهیم داد). در ادامه، واسط IAccountRepository و کلاس AccountRepository را باز کرده و آن را مطابق زیر ویرایش میکنیم:
public interface IAccountRepository { IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId); } public class AccountRepository : IAccountRepository { private readonly ApplicationContext _context; public AccountRepository(ApplicationContext context) { _context = context; } public IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId) => _context.Accounts .Where(a => a.OwnerId.Equals(ownerId)) .ToList(); }
اکنون میتوان لیست accountها را به نتیجه owners ، query اضافه کنیم. پس کلاس OwnerType را باز کرده و آن را مطابق زیر ویرایش میکنیم:
public class OwnerType : ObjectGraphType<Owner> { public OwnerType(IAccountRepository repository) { Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object."); Field(x => x.Name).Description("Name property from the owner object."); Field(x => x.Address).Description("Address property from the owner object."); Field<ListGraphType<AccountType>>( "accounts", resolve: context => repository.GetAllAccountsPerOwner(context.Source.Id) ); } }
چیز خاصی در اینجا وجود ندارد که ما تا کنون ندیده باشیم. به همان روش که یک فیلد را در کلاس AppQuery ایجاد کردیم، یک فیلد را با نام accounts در کلاس OwnerType ایجاد میکنیم. همچنین متد GetAllAccountsPerOwner نیاز به پارامتر id را دارد و این پارامتر را از طریق context.Source.Id فراهم میکنیم. زیرا context شامل خصوصیت Source است که در این حالت مشخص نوع Owner میباشد.
اکنون پروژه را اجرا کنید و به آدرس زیر بروید:
https://localhost:5001/ui/playground
{ owners{ id, name, address, accounts{ id, description, ownerId } } }
Adding Enumerations in GraphQL Queries
در کلاس AccountType فیلد Type را اضافه نکردهایم و این کار را عمدا انجام دادهایم. اکنون زمان انجام این کار میباشد. برای اضافه کردن گونه شمارشی به کلاس AccountType نیاز است تا در ابتدا یک کلاس تعریف شود که نسبت به type های معمول در GraphQL متفاوت است. یک کلاس را به نام AccountTypeEnumType در پوشه GraphQLTypes ایجاد کرده و آن را مطابق زیر ویرایش میکنیم:
public class AccountTypeEnumType : EnumerationGraphType<TypeOfAccount> { public AccountTypeEnumType() { Name = "Type"; Description = "Enumeration for the account type object."; } }
کلاس AccountTypeEnumType باید از نوع جنریک کلاس EnumerationGraphType ارث بری کند و پارامتر جنریک آن، یک گونه شمارشی را دریافت میکند (که در قسمت قبل آن را ایجاد کردیم؛ TypeOfAccount). همچنین مقدار خصوصیت Name نیز باید همان نام خصوصیت گونه شمارشی در کلاس Account باشد (نام آن در کلاس Account مساوی Type میباشد). سپس گونه شمارشی را در کلاس AccountType به صورت زیر اضافه میکنیم:
public class AccountType : ObjectGraphType<Account> { public AccountType() { ... Field<AccountTypeEnumType>("Type", "Enumeration for the account type object."); } }
اکنون پروژه را اجرا کنید و سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید:
{ owners{ id, name, address, accounts{ id, description, type, ownerId } } }
که نتیجه آن اضافه شدن type به هر account میباشد:
Implementing a Cache in the GraphQL Queries with Data Loader
دیدم که query، نتیجه دلخواهی را برای ما بازگشت میدهد؛ اما این query هنوز به اندازه کافی بهینه نشدهاست. مشکل چیست؟
query ایجاد شده به حالتی کار میکند که در ابتدا همه owner ها را بازیابی میکند. سپس به ازای هر owner، یک Sql Query را به سمت بانک اطلاعاتی ارسال میکند تا Account های مربوط به آن Owner را بازگشت دهد که میتوان log آن را در Terminal مربوط به VS Code مشاهده کرد.
البته زمانیکه چند موجودیت owner را داشته باشیم، این مورد یک مشکل نمیباشد؛ ولی وقتی تعداد موجودیتها زیاد باشد چطور؟
owners ، query را میتوان با استفاده از DataLoader که توسط GraphQL فراهم شدهاست، بهینه سازی کرد. جهت انجام اینکار در ابتدا واسط IAccountRepository و کلاس AccountRepository را همانند زیر ویرایش میکنیم:
public interface IAccountRepository { ... Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds); }
public class AccountRepository : IAccountRepository { ... public async Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds) { var accounts = await _context.Accounts.Where(a => ownerIds.Contains(a.OwnerId)).ToListAsync(); return accounts.ToLookup(x => x.OwnerId); } }
نیاز است که یک متد داشته باشیم که <<Task<ILookup<TKey, T را برگشت میدهد؛ زیرا DataLoader نیازمند یک متد با نوع برگشتی که در امضایش عنوان شده است میباشد .
در ادامه کلاس OwnerType را مطابق زیر ویرایش میکنیم:
public class OwnerType : ObjectGraphType<Owner> { public OwnerType(IAccountRepository repository, IDataLoaderContextAccessor dataLoader) { ... Field<ListGraphType<AccountType>>( "accounts", resolve: context => { var loader = dataLoader.Context.GetOrAddCollectionBatchLoader<Guid, Account>("GetAccountsByOwnerIds", repository.GetAccountsByOwnerIds); return loader.LoadAsync(context.Source.Id); }); } }
در کلاس OwnerType، واسط IDataLoaderContextAccessor را در سازنده کلاس تزریق میکنیم و سپس متد Context.GetOrAddCollectionBatchLoader را فراخوانی میکنیم که در پارامتر اول آن، یک کلید و در پارامتر دوم آن، متد GetAccountsByOwnerIds را از IAccountRepository معرفی میکنیم.
سپس باید DataLoader را در متد ConfigureServices موجود در کلاس Startup ثبت کنیم. در ادامه services.AddGraphQL را مطابق زیر ویرایش میکنیم:
services.AddGraphQL(o => { o.ExposeExceptions = false; }) .AddGraphTypes(ServiceLifetime.Scoped) .AddDataLoader();
اکنون پروژه را با دستور زیر اجرا کنید و سپس query قبلی را در UI.Playground اجرا کنید.
اگر log موجود در Terminal مربوط به VS Code را مشاهده کنید، متوجه خواهید شد که در این حالت یک query برای تمام owner ها و یک query برای تمام account ها داریم.
Using Arguments in Queries and Handling Errors
تا کنون ما یک query را اجرا میکردیم که نتیجه آن بازیابی تمام owner ها به همراه تمام account های مربوط به هر owner بود. اکنون میخواهیم براساس id، یک owner مشخص را بازیابی کنیم. برای انجام این کار نیاز است که یک آرگومان را در query شامل کنیم.
در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را همانند زیر ویرایش میکنیم:
public interface IOwnerRepository { ... Owner GetById(Guid id); } public class OwnerRepository : IOwnerRepository { ... Owner GetById(Guid id) => _context.Owners.SingleOrDefault(o => o.Id.Equals(id)); }
public class AppQuery : ObjectGraphType { public AppQuery(IOwnerRepository repository) { ... Field<OwnerType>( "owner", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }), resolve: context => { var id = context.GetArgument<Guid>("ownerId"); return repository.GetById(id); } ); } }
در اینجا یک فیلد را ایجاد کردهایم که مقدار برگشتی آن یک OwnerType میباشد. نام query را owner تعیین میکنیم و از بخش arguments، برای ایجاد کردن آرگومانهای این query استفاده میکنیم. آرگومان این query نمیتواند NULL باشد و باید از نوع IdGraphType و با نام ownerId باشد و در نهایت بخش resolve است که کاملا گویا میباشد.
اگر پارامتر id، از نوع Guid نباشد، بهتر است که یک پیام را به سمت کلاینت برگشت دهیم. جهت انجام این کار یک اصلاح کوچک در بخش resolve انجام میدهیم:
Field<OwnerType>( "owner", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }), resolve: context => { Guid id; if (!Guid.TryParse(context.GetArgument<string>("ownerId"), out id)) { context.Errors.Add(new ExecutionError("Wrong value for guid")); return null; } return repository.GetById(id); } );
اکنون پروژه را اجرا کنید و سپس یک query جدید را در UI.Playground به صورت زیر ارسال کنید:
{ owner(ownerId:"6f513773-be46-4001-8adc-2e7f17d52d83"){ id, name, address, accounts{ id, description, type, ownerId } }
نکته:
در صورتیکه قصد داشته باشیم علاوه بر id، یک name را هم ارسال کنیم، در بخش resolve به صورت زیر آن را دریافت میکنیم:
string name = context.GetArgument<string>("name");
{ owner(ownerId:"53270061-3ba1-4aa6-b937-1f6bc57d04d2", name:"ANDY") { ... } }
Aliases, Fragments, Named Queries, Variables, Directives
می توانیم برای query های ارسال شده از سمت کلاینت با معرفی aliases، یک سری تغییرات را داشته باشیم. وقتیکه میخواهیم نام نتیجه دریافتی یا هر فیلدی را در نتیجه دریافتی تغییر دهیم، بسیار کاربردی میباشند. اگر یک query داشته باشیم که یک آرگومان را دارد و بخواهیم دو تا از این query داشته باشیم، برای ایجاد تفاوت بین query ها میتوان از aliases استفاده کرد.
جهت استفاده باید نام مورد نظر را در ابتدای query یا فیلد قرار دهیم:
{ first:owners{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } }, second:owners{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } } }
همانطور که از مثال بالا مشخص است، دو query با فیلدهای یکسانی را داریم. اگر بجای 2 query یکسان (مانند مثال بالا) ولی با آرگومانهای متفاوت، اینبار 10 query یکسان با آرگومانهای متفاوتی را داشته باشیم، در این حالت خواندن query ها مقداری سخت میباشد. در این صورت میتوان این مشکل را با استفاده از fragmentها برطرف کرد. Fragmentها این اجازه را به ما میدهند تا فیلدها را با استفاده از کاما ( ، ) از یکدیگر جدا و تبدیل به یک بخش مجزا کنیم و سپس استفاده مجدد از آن بخش را در تمام query ها داشته باشیم. Syntax آن به حالت زیر میباشد:
fragment SampleName on Type{ ... }
تعریف یک fragment به نام ownerFields و استفاده از آن :
{ first:owners{ ...ownerFields }, second:owners{ ...ownerFields }, ... } fragment ownerFields on OwnerType{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } }
برای ایجاد کردن یک named query، مجبور هستیم از کلمه کلیدی query در آغاز کل query استفاده کنیم؛ به همراه نام query، که بعد از کلمه کلیدی query قرار میگیرد. اگر نیاز داشته باشیم میتوان آرگومانها را به query ارسال کرد.
نکته مهمی که در رابطه با named query ها وجود دارد این است که اگر یک query آرگومان داشته باشد نیاز است از پنجره QUERY VARIABLES برای تخصیص مقدار به آن آرگومان استفاده کنیم.
query OwnerQuery($ownerId:ID!) { owner(ownerId:$ownerId){ id, name, address, accounts{ id, description, type } } }
{ "ownerId":"6f513773-be46-4001-8adc-2e7f17d52d83" }
اکنون اجرا کنید و خروجی را مشاهده کنید .
در نهایت میتوان بعضی فیلدها را از نتیجه دریافتی با استفاده از directiveها در query حذف یا اضافه کرد. دو directive وجود دارد که میتوان از آنها استفاده کرد (include و skip).
در قسمت بعد در رابطه با GraphQL Mutations صحبت خواهیم کرد.
کدهای مربوط به این قسمت را از اینجا دریافت کنید . ASPCoreGraphQL_2.zip