اشتراکها
یکی از مواردی که به همراه NET Core 1.x. وجود دارد، کمبود کتابخانههای ثالث مخصوص آن است. برای مثال کتابخانهی log4net در اوایل ارائهی NET Core. نگارش مخصوص به آنرا نداشت (البته هم اکنون دارد). باید درنظر داشت، این مورد صرفا در حالت توزیع چندسکویی برنامههای مبتنی بر NET Core. مشکل ایجاد میکرد. از این جهت که میتوان full .NET framework را به عنوان Target Framework برنامههای NET Core. معرفی کرد و در این حالت برنامه بدون هیچگونه مشکلی تنها بر روی ویندوز و سرورهای ویندوزی اجرا میشود (و امکان دسترسی به تمامی کتابخانههای مخصوص full .NET framework را نیز دارا خواهد بود)؛ اما قابلیت توزیع بر روی لینوکس و مک را از دست خواهد داد.
در NET Core 2.0. از یک اصطلاحا «compatibility shim» مخصوص استفاده میشود که امکان افزودن ارجاعات به full framework libraryها را بدون نیاز به تغییر target framework برنامه میسر میکند. یعنی در اینجا میتوان یک کتابخانهی قدیمی دات نتی را در برنامههای مبتنی بر NET Core. بر روی لینوکس نیز اجرا کرد و در این حالت نیازی به تبدیل اجباری این کتابخانه به نسخهی NET Core. آن نیست.
NET Core 2.0. پیاده سازی کنندهی NET Standard 2.0. است
NET Standard. در حقیقت یک قرار داد است که سکوهای کاری مختلف دات نتی مانند Full .NET Framework ، Xamarin ، Mono ، UWP و غیره میتوانند آنرا پیاده سازی کنند. یک نمونهی دیگر این پیاده سازیها نیز NET Core. است. برای مثال دات نت 4.6.1، استاندارد و قرار داد شمارهی 2 دات نت را پیاده سازی میکند. به همین صورت NET Core 2.0. نیز پیاده سازی کنندهی این استاندارد شماره 2 است.
با تغییرات اخیر، اکنون NuGet میتواند کتابخانههای مبتنی بر NET Standard 2. را در برنامههای مبتنی بر سکوهای کاری که آنرا پیاده سازی میکنند، بدون مشکل اضافه کند. برای مثال میتوان اسمبلیهای دات نت 4.6.1 را به برنامههای ASP.NET Core 2.0 اضافه کرد (کاری که در نگارش 1x آن به صورت مستقیم میسر نیست) و یا میتوان اسمبلیهای کامپایل شدهی برای دات نت استاندارد 2 را به برنامههای مبتنی بر دات نت 4.6.1 اضافه کرد.
آیا واقعا کتابخانههای قدیمی دات نتی توسط برنامههای NET Core 2.0. در لینوکس نیز اجرا خواهند شد؟
دات نت استاندارد، بیش از یک قرار داد چیزی نیست و پیاده سازی کنندگان آن میتوانند سطح بیشتری را نسبت به این قرار داد نیز لحاظ کنند. برای مثال دات نت 4.6.1 شامل سطح API بیشتری از دات نت استاندارد 2 است.
به همین جهت باید درنظر داشت که امکان اضافه کردن یک بستهی نیوگت از یک کتابخانهی نوشته شدهی برای دات نت کامل در برنامههای دات نت Core به معنای تضمینی برای کار کردن آن در زمان اجرا نخواهد بود. از این جهت که دات نت کامل، به همراه قسمتهایی است که در NET Standard. وجود خارجی ندارند. بنابراین اگر کتابخانهی استفاده شده صرفا این API مشترک را هدف قرار دادهاست، هم قابلیت اتصال و هم قابلیت اجرا را خواهد داشت؛ اما اگر برای مثال کسی بستهی NServiceBus را به پروژهی ASP.NET Core 2.0 اضافه کند، بدون مشکل کامپایل خواهد شد. اما از آنجائیکه این کتابخانه از MSMQ استفاده میکند که خارج از میدان دید این استاندارد است، در زمان اجرا با شکست مواجه خواهد شد.
«compatibility shim» در NET Standard 2.0. چگونه کار میکند؟
در NET Core.، پیاده سازی Object در System.Runtime قرار دارد و کد تولید شدهی توسط آن یک چنین ارجاعی را [System.Runtime]System.Object تولید میکند. اما در دات نت کلاسیک، System.Object در mscorlib قرار دارد. به همین جهت زمانیکه سعی کنید اسمبلیهای دات نت کلاسیک را در NET Core 1.x. استفاده کنید، پیام یافتن نشدن نوعها را دریافت خواهید کرد. اما در NET Core 2.0. یک پیاده سازی صوری (facade) از mscorlib وجود دارد که کار آن هدایت نوع درخواستی، به نوع واقعی پیاده سازی شدهی در NET Core. است.
در این تصویر استفادهی از یک کتابخانهی ثالث را مشاهده میکنید که ارجاعی را به [mscorlib]Microsoft.Win32.RegistryKey دارد (مبتنی بر دات نت کلاسیک است). همچنین یک mscorlib مشخص شدهی به صورت facade را نیز مشاهده میکنید. کار آن هدایت درخواست نوع واقع شدهی در mscorlib، به نوع موجود [Microsoft.Win32.Registry] Microsoft.Win32.RegistryKey است و تنها زمانی کار خواهد کرد که Microsoft.Win32.RegistryKey.dll وجود خارجی داشته باشد. به این معنا که رجیستری، یک مفهوم ویندوزی است و این کتابخانه بر روی ویندوز بدون مشکل کار میکند. اما تحت لینوکس، این قسمت خاص با پیام PlatformNotSupportedException خاتمه خواهد یافت. اما اگر قسمتهایی از این کتابخانه را استفاده کنید که در تمام سکوهای کاری وجود داشته باشند، بدون مشکل قادر به استفادهی از آن خواهید بود.
یک مثال: استفاده از کتابخانهی رمزنگاری اطلاعات Inferno
آخرین نگارش کتابخانهی رمزنگاری اطلاعات Inferno مربوط به NET 4.5.2. است. مراحل ذیل را پس از نصب SDK جدید NET Core 2.0. در خط فرمان طی میکنیم:
الف) ایجاد پوشهی UseNET452InNetCore2 و سپس ایجاد یک پروژهی کنسول جدید
ب) افزودن بستهی نیوگت Inferno به پروژه
این بسته بدون مشکل اضافه میشود؛ البته پیام اخطار ذیل نیز صادر خواهد شد (چون مبتنی بر NET 4.6.1. که پیاده سازی کنندهی NET Standard 2.0. است، نیست):
ابتدا پیام میدهد که این بسته ممکن است با NET Core 2.0. سازگار نباشد. سپس عنوان میکند که سازگاری کاملی را با پروژهی جاری دارد و بسته را اضافه میکند.
ج) استفاده از کتابخانهی Inferno جهت تولید یک عدد تصادفی thread safe
د) اجرای برنامه
در ادامه اگر دستور dotnet run را صادر کنیم، ابتدا اخطاری را صادر میکند که این بسته ممکن است دارای قسمتهایی باشد که با NET core 2.0. سازگار نیست و سپس خروجی نهایی را بدون مشکل اجرا کرده و نمایش میدهد.
در NET Core 2.0. از یک اصطلاحا «compatibility shim» مخصوص استفاده میشود که امکان افزودن ارجاعات به full framework libraryها را بدون نیاز به تغییر target framework برنامه میسر میکند. یعنی در اینجا میتوان یک کتابخانهی قدیمی دات نتی را در برنامههای مبتنی بر NET Core. بر روی لینوکس نیز اجرا کرد و در این حالت نیازی به تبدیل اجباری این کتابخانه به نسخهی NET Core. آن نیست.
NET Core 2.0. پیاده سازی کنندهی NET Standard 2.0. است
NET Standard. در حقیقت یک قرار داد است که سکوهای کاری مختلف دات نتی مانند Full .NET Framework ، Xamarin ، Mono ، UWP و غیره میتوانند آنرا پیاده سازی کنند. یک نمونهی دیگر این پیاده سازیها نیز NET Core. است. برای مثال دات نت 4.6.1، استاندارد و قرار داد شمارهی 2 دات نت را پیاده سازی میکند. به همین صورت NET Core 2.0. نیز پیاده سازی کنندهی این استاندارد شماره 2 است.
با تغییرات اخیر، اکنون NuGet میتواند کتابخانههای مبتنی بر NET Standard 2. را در برنامههای مبتنی بر سکوهای کاری که آنرا پیاده سازی میکنند، بدون مشکل اضافه کند. برای مثال میتوان اسمبلیهای دات نت 4.6.1 را به برنامههای ASP.NET Core 2.0 اضافه کرد (کاری که در نگارش 1x آن به صورت مستقیم میسر نیست) و یا میتوان اسمبلیهای کامپایل شدهی برای دات نت استاندارد 2 را به برنامههای مبتنی بر دات نت 4.6.1 اضافه کرد.
آیا واقعا کتابخانههای قدیمی دات نتی توسط برنامههای NET Core 2.0. در لینوکس نیز اجرا خواهند شد؟
دات نت استاندارد، بیش از یک قرار داد چیزی نیست و پیاده سازی کنندگان آن میتوانند سطح بیشتری را نسبت به این قرار داد نیز لحاظ کنند. برای مثال دات نت 4.6.1 شامل سطح API بیشتری از دات نت استاندارد 2 است.
به همین جهت باید درنظر داشت که امکان اضافه کردن یک بستهی نیوگت از یک کتابخانهی نوشته شدهی برای دات نت کامل در برنامههای دات نت Core به معنای تضمینی برای کار کردن آن در زمان اجرا نخواهد بود. از این جهت که دات نت کامل، به همراه قسمتهایی است که در NET Standard. وجود خارجی ندارند. بنابراین اگر کتابخانهی استفاده شده صرفا این API مشترک را هدف قرار دادهاست، هم قابلیت اتصال و هم قابلیت اجرا را خواهد داشت؛ اما اگر برای مثال کسی بستهی NServiceBus را به پروژهی ASP.NET Core 2.0 اضافه کند، بدون مشکل کامپایل خواهد شد. اما از آنجائیکه این کتابخانه از MSMQ استفاده میکند که خارج از میدان دید این استاندارد است، در زمان اجرا با شکست مواجه خواهد شد.
«compatibility shim» در NET Standard 2.0. چگونه کار میکند؟
در NET Core.، پیاده سازی Object در System.Runtime قرار دارد و کد تولید شدهی توسط آن یک چنین ارجاعی را [System.Runtime]System.Object تولید میکند. اما در دات نت کلاسیک، System.Object در mscorlib قرار دارد. به همین جهت زمانیکه سعی کنید اسمبلیهای دات نت کلاسیک را در NET Core 1.x. استفاده کنید، پیام یافتن نشدن نوعها را دریافت خواهید کرد. اما در NET Core 2.0. یک پیاده سازی صوری (facade) از mscorlib وجود دارد که کار آن هدایت نوع درخواستی، به نوع واقعی پیاده سازی شدهی در NET Core. است.
در این تصویر استفادهی از یک کتابخانهی ثالث را مشاهده میکنید که ارجاعی را به [mscorlib]Microsoft.Win32.RegistryKey دارد (مبتنی بر دات نت کلاسیک است). همچنین یک mscorlib مشخص شدهی به صورت facade را نیز مشاهده میکنید. کار آن هدایت درخواست نوع واقع شدهی در mscorlib، به نوع موجود [Microsoft.Win32.Registry] Microsoft.Win32.RegistryKey است و تنها زمانی کار خواهد کرد که Microsoft.Win32.RegistryKey.dll وجود خارجی داشته باشد. به این معنا که رجیستری، یک مفهوم ویندوزی است و این کتابخانه بر روی ویندوز بدون مشکل کار میکند. اما تحت لینوکس، این قسمت خاص با پیام PlatformNotSupportedException خاتمه خواهد یافت. اما اگر قسمتهایی از این کتابخانه را استفاده کنید که در تمام سکوهای کاری وجود داشته باشند، بدون مشکل قادر به استفادهی از آن خواهید بود.
یک مثال: استفاده از کتابخانهی رمزنگاری اطلاعات Inferno
آخرین نگارش کتابخانهی رمزنگاری اطلاعات Inferno مربوط به NET 4.5.2. است. مراحل ذیل را پس از نصب SDK جدید NET Core 2.0. در خط فرمان طی میکنیم:
الف) ایجاد پوشهی UseNET452InNetCore2 و سپس ایجاد یک پروژهی کنسول جدید
dotnet new console
ب) افزودن بستهی نیوگت Inferno به پروژه
dotnet add package Inferno
log : Installing Inferno 1.4.0. warn : Package 'Inferno 1.4.0' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETCoreApp,Version=v2.0'. This package may not be fully compatible with your project. info : Package 'Inferno' is compatible with all the specified frameworks in project 'D:\UseNET452InNetCore2\UseNET452InNetCore2.csproj'. info : PackageReference for package 'Inferno' version '1.4.0' added to file 'D:\UseNET452InNetCore2\UseNET452InNetCore2.csproj'.
ج) استفاده از کتابخانهی Inferno جهت تولید یک عدد تصادفی thread safe
using System; using SecurityDriven.Inferno; namespace UseNET452InNetCore2 { class Program { static CryptoRandom random = new CryptoRandom(); static void Main(string[] args) { Console.WriteLine($"rnd: {random.NextLong()}"); } } }
د) اجرای برنامه
در ادامه اگر دستور dotnet run را صادر کنیم، ابتدا اخطاری را صادر میکند که این بسته ممکن است دارای قسمتهایی باشد که با NET core 2.0. سازگار نیست و سپس خروجی نهایی را بدون مشکل اجرا کرده و نمایش میدهد.
>dotnet run warning NU1701: This package may not be fully compatible with your project. rnd: 8167886599578111106
- این خروجی SQL لاگ شده مطلب جاری (با تمام توضیحات و نگاشتهای آن) توسط برنامه مطمئن SQL Server Profiler است:
منطقی هم هست. چون در ToList اول، کار با دیتابیس تمام و قطع میشود. ToList دوم سمت کلاینت اجرا میشود. یعنی تشکیل درخت نهایی توسط امکانات LINQ to Objects انجام میشود و نه هیچ کار اضافهای در سمت سرور.
- اگر اینجا join اضافی پیدا کردید ... حتما مشکلی در تنظیمات نگاشتها دارید.
- اگر duplicate reader دارید شاید بخاطر lazy loading سایر خواص راهبری است که تعریف کردید مانند User و EditByUser و غیره. اینها اگر قرار است نمایش داده شوند، پیش از ToList اول باید توسط متد الحاقی Include به صورت eager loading تعریف شوند تا lazy loading و duplicate reader نداشته باشید.
- برای فیلتر فیلدهای اضافی، پیش از ToList اول، با استفاده از Projection و نوشتن یک Select، موارد مورد نیاز را انتخاب کنید.
SELECT [Extent1].[Id] AS [Id], [Extent1].[Body] AS [Body], [Extent1].[ReplyId] AS [ReplyId] FROM [dbo].[BlogComments] AS [Extent1]
- اگر اینجا join اضافی پیدا کردید ... حتما مشکلی در تنظیمات نگاشتها دارید.
- اگر duplicate reader دارید شاید بخاطر lazy loading سایر خواص راهبری است که تعریف کردید مانند User و EditByUser و غیره. اینها اگر قرار است نمایش داده شوند، پیش از ToList اول باید توسط متد الحاقی Include به صورت eager loading تعریف شوند تا lazy loading و duplicate reader نداشته باشید.
- برای فیلتر فیلدهای اضافی، پیش از ToList اول، با استفاده از Projection و نوشتن یک Select، موارد مورد نیاز را انتخاب کنید.
در قسمت قبل در مورد حالتی که کوئری انجام شده نتیجهای را بر نگردانده است، بحث شد. در این قسمت یکی از شایعترین مشکلات حین کار با تابع Sum بررسی خواهد شد.
ابتدا جدول ساده Transactions را با دو فیلد Id و Amount مطابق تصویر زیر در نظر بگیرید:
تعدادی رکورد در این جدول ثبت شدهاند. اکنون میخواهیم جمع آنها را محاسبه کنیم:
همانطور که ملاحظه مینمائید این عملیات میسر نیست، زیرا حاصل نهایی فراتر از بازهی تعریف شدهی Int32 است.
برای رفع این مشکل باید Amount را تبدیل به BigInt (برای مثال مرتبط با نگارشهای مختلف SQL Server) کرد. مطابق توضیحات قسمت قبل، این عملیات casting باید به lambda expression تعریف شده اعمال گردد، زیرا خروجی Sum بر مبنای آن تعیین میگردد.
در این حالت خروجی SQL آن نیز به صورت زیر در خواهد آمد:
هر چند این مباحث ساده به نظر میرسند ولی در صورت عدم رعایت سبب سرخ و سفید شدن در هنگام مقتضی خواهند گردید.
پیش نویس: این مقاله ترجمه شده فصل 6 کتاب Pro Asp.Net Core MVC2 میباشد.
کار با Visual Studio
در این مقاله، یکسری توضیحاتی در مورد ویژگیهای کلیدی ویژوال استودیو به برنامه نویسهای (توسعه دهندههای) پروژههای Asp.net Core MVC ارائه میدهیم.
ایجاد یک پروژه
در ابتدا یک پروژهی وب جدید Asp.net core را به نام Working و بر اساس قالب Empty ایجاد میکنیم. سپس در کلاس startup، قابلیت MVC را فعال میکنیم (کدهای این قسمت، در فصل 5 کامل شرح داده شدهاست)
namespace Working { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //app.Run(async (context) => //{ // await context.Response.WriteAsync("Hello World!"); //}); } } }
ایجاد مدل:
یک پوشه جدید را به نام Models ایجاد میکنیم و بعد در این پوشه یک کلاس جدید را به نام Product ایجاد میکنیم و کدهای زیر را در کلاس ایجاد شده قرار میدهیم (این قسمت در فصل 5 نیز شرح داده شدهاست):
namespace Working.Models { public class Product { public string Name { get; set; } public decimal Price { get; set; } } }
namespace WorkingWithVisualStudio.Models { public class SimpleRepository { private static SimpleRepository sharedRepository = new SimpleRepository(); private Dictionary<string, Product> products = new Dictionary<string, Product>(); public static SimpleRepository SharedRepository => sharedRepository; public SimpleRepository() { var initialItems = new[] { new Product { Name = "Kayak", Price = 275M }, new Product { Name = "Lifejacket", Price = 48.95M }, new Product { Name = "Soccer ball", Price = 19.50M }, new Product { Name = "Corner flag", Price = 34.95M } }; foreach (var p in initialItems) { AddProduct(p); } } public IEnumerable<Product> Products => products.Values; public void AddProduct(Product p) => products.Add(p.Name, p); } }
نکته: من یک مشخصه (Property) استاتیک را به نام SharedRepository تعریف کردم که دسترسی به SimpleRepository را فراهم میکند و میتواند در طول برنامه از آن استفاده شود. این بهترین کار نیست، ولی میخواهم یک مشکل رایج را که در توسعه MVC روبرو میشوید، نشان دهم. من راه بهتری را برای کار با اجزای مشترک، در فصل 18 توضیح میدهم.
ایجاد Controller و View
در پوشه Controllers، یک فایل جدید را به نام HomeController.cs ایجاد میکنیم و کدهای زیر را در آن قرار میدهیم:
namespace WorkingWithVisualStudio.Controllers { public class HomeController : Controller { public IActionResult Index() => View(SimpleRepository.SharedRepository.Products); } }
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>>Working with Visual Studio</title> </head> <body> <table> <thead> <tr> <td>Name</td> <td>Price</td> </tr> </thead> <tbody> @foreach (var p in Model) {<tr> <td>@p.Name</td> <td>@p.Price</td> </tr>} </tbody> </table> </body> </html>
این View شامل یک جدول است که از حلقه foreach Razor، برای ایجاد ردیفهایی برای هر شیء مدل استفاده میکند. جایی که هر ردیف، حاوی سلولهایی برای خواص نام و قیمت است. اگر شما برنامه کاربردی را اجرا کنید، نتایج حاصل را در شکل خواهید دید:
دو نوع مختلف از بستههای نرم افزاری مورد نیاز برای Asp.Net Core MVC وجود دارند.
معرفی NuGet
ویژوال استودیو به همراه یک ابزار گرافیکی برای مدیریت بستههای NET. است که در یک پروژه گنجانده شدهاست. برای باز کردن این ابزار، گزینه Management NuGet Packages for Solution را از منوی Tools ➤ NuGet Package Manager انتخاب کنید. به این ترتیب ابزار NuGet باز میشود و لیستی از بستههایی که قبلا نصب شدهاند، نمایش داده میشود؛ همانطور که در شکل زیر نشان داده شدهاست:
برگهی Installed، خلاصهای از بستههایی را که قبلا در پروژه نصب شدهاند، نشان میدهد. از برگهی Browse، برای یافتن و نصب بستههای جدید میتوان استفاده کرد و برگهی Updates، فهرست package هایی را که نسخههای اخیر آنها منتشر شدهاند، نمایش میدهد.
معرفی بستهی MICROSOFT.ASPNETCORE.ALL
اگر شما از نسخههای قبلی Asp.Net Core استفاده کرده باشید، باید یک لیست طولانی از بستههای NuGet را به پروژه جدید خود اضافه نمایید. Asp.Net Core2 یک بستهی متفاوت را به نام Microsoft.AspNetCore.All معرفی میکند.
پکیچ Microsoft.AspNetCore.All یک meta-package است که شامل تمام بستههای Nuget مورد نیاز Asp.net Core و MVC Framework است. یعنی شما دیگر نیازی به نصب تک به تک این نوع بستهها ندارید و هنگامیکه برنامه خود را منتشر میکنید، هر بستهای از بستههای Meta-package که مورداستفاده قرار نمیگیرند، حذف خواهند شد. البته این بسته در نگارش 2.1، قسمت All آن به App تغییر نام یافتهاست.
معرفی بستههای Nuget و موقعیت ذخیره سازی آنها
ابزار NuGet لیست بستههای پروژه را در فایل projectname.csproj نگهداری میکند. در اینجا <projectname> با نام پروژه جایگزین میشود. برای مثال در پروژه فوق اطلاعات Nuget، در فایل WorkingWithVisualStudio.csproj ذخیره میشوند. ویژوال استودیو محتویات فایل csproj را در پنجرهی Solution Explorer نمایش نمیدهد. برای ویرایش این فایل، روی پروژه در پنجرهی Solution Explorer راست کلیک کنید و گزینهی Edit WorkWithVisualStudio.csproj را از منوی باز شده، انتخاب کنید. ویژوال استودیو فایل را برای ویرایش باز میکند. فایل csproj یک فایل XML است و شما در آن عنصری را مانند قطعه کد زیر در آن میبینید که Asp.net Core Meta package را به پروژه اضافه میکند:
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> </ItemGroup>
هنگامیکه از NuGet برای اضافه کردن یک بسته به پروژهی خود استفاده میکنید، به صورت خودکار به همراه هر بستهای که به آن وابستگی دارد، نصب میشود. شما میتوانید بستههای Nuget و وابستگیهای آنها را در SolutionExpolrer از طریق گزینهی Dependencies -> Nuget مشاهده کنید که هر یک از بستههای موجود در فایل csproj و وابستگیهای آنها را نشان میدهد. برای نمونه بسته Meta-Package ASP.Net Core دارای تعداد زیادی وابستگی است؛ برخی از آنها در شکل زیر دیده میشوند:
معرفی Bower
یک بسته Client-Side، شامل محتوایی است که به مشتری ارسال میشود؛ مانند فایلهای جاوا اسکریپت، Css Stylesheets و یا تصاویر. از Nuget برای مدیریت این نوع فایلها در پروژه نیز استفاده میشود. اما اکنون Asp.Net Core MVC پشتیبانی توکاری را از یک ابزار مدیریت بستههای سمت کاربر، به نام Bower نیز ارائه میدهد. Bower یک ابزار منبع باز ( Open Source ) است که در خارج از مایکروسافت و دنیای NET. توسعه داده شده و نگهداری میشود.
نکته: Bower به تازگی منسوخ شده اعلام گردیدهاست. ممکن است هشدارهایی را که ابزارهای جایگزین را پیشنهاد میکنند نیز مشاهده کنید. با این حال پشتیبانی از Bower با ویژوال استودیو یکپارچه شدهاست و در نگارش 2.1 ابزار مدیریت سمت کلاینت جدید دیگری را نیز بجای آن معرفی کردهاند.
معرفی لیست بستههای Bower
بستههای Bower از طریق فایل ویژهی bower.json مشخص میشوند. برای ایجاد این فایل در پنجره Solution Explorer روی پروژه WorkingWithVisualStudio راست کلیک کنید و Add -> New Item را از منوی باز شده انتخاب کنید. سپس قالب مورد نظر Bower Configuration File را از Asp.net Core -> Web -> General Category انتخاب نمائید؛ مانند تصویر زیر:
ویژوال استودیو نام bower.json را برای آن قرار میدهد. برروی ok کلیک میکنیم و یک فایل جدید، با محتویات پیشفرض زیر به پروژه اضافه میشود:
{ "name": "asp.net", "private": true, "dependencies": {} }
به علاوه برای فایل Bower.json، تصویر زیر بستههای Client Side وابسته به Bower را نشان میدهد. از این قسمت برای اضافه کردن وابستگیهای برنامه نیز استفاده میشود.
نکته: منبع بستههای Bower در لینک http://bower.io/search وجود دارد. شما میتوانید بستهها مورنظر را در اینجا جستجو و به پروژه اضافه کنید.
بعد از اینکه بستهها نصب شدند، محتویات فایل bower.json به صورت زیر میباشد:
{ "name": "asp.net", "private": true, "resolutions": { "jquery": "3.3.1" } }
در ادامه بسته Bootstrap CSS به پروژه اضافه شدهاست. زمانیکه شما فایل Bower.json را ویرایش میکنید، ویژوال استادیو لیستی از نام بستهها و نسخههای بستههای موجود را نمایش میدهد؛ مانند تصویر زیر:
در زمان نوشتن این مطلب، آخرین نسخهی پایدار بسته بوت استرپ، 3،3،7 است. البته اگر در دقت کنید، در اینجا سه گزینهی ارائه شدهی توسط ویژوال استودیو وجود دارند: 3.3.7 و 3.3.7^ و 3.3.7~. شماره نسخه میتواند در طیف وسیعی از روشهای مختلف در فایل bower.json مشخص شود. مفیدترین آنها در جدول زیر شرح داده شدهاند. استفاده از شماره نسخه صریح یک بسته، امنترین راه برای مشخص کردن یک بسته است. این تضمین میکند که شما همیشه با همان نسخه کار میکنید؛ مگر اینکه عمدا فایل bower.json را برای پاسخ گویی به درخواستهای دیگری به روز رسانی کنید:
فرمت | توضیحات |
3.3.7 | بیان شماره مستقیم بسته نصب شده و تطبیق دقیق آن با شمار نسخه ، e.g ، 3.3.7 |
* | با استفاده از یک ستاره به Bower اجازه نصب آخرین نسخه را میدهد |
3.3.7 =<3.3.7< | پیشوند یک شماره نسخه با < یا =< به Bower اجازه میدهد تا هر نسخه از بستهای که بزرگتر یا بزرگتر مساوی آن نسخهی معین است، نصب شود |
3.3.7 =>3.3.7> | پیشوند یک شماره نسخه با > یا => به Bower اجازه میدهد تا هر نسخه از بستهای را که کوچکتر یا کوچکتر و مساوی نسخهی معین است، نصب شود |
3.3.7~ | پیشوند یک شماره نسخه با یک tilde (با کاراکتر ~ ) به نسخههایی که دو شماره
اولیه آنها مشابه باشند، اجازه نصب میدهد؛ حتی اگر شماره آخر آن نسخه متفاوت
باشد. مانند نسخههای 3.3.9 و 3.3.8 و اجازه نصب نسخه 3.4.0 را نمیدهد؛ چون
شماره دوم آن متفاوت است. |
3.3.7^ | پیشوند یک شماره نسخه با یک قلم (کاراکتر ^) به نسخههایی که شماره اول آنها مشابه باشند، اجازه نصب میدهد؛ حتی اگر شماره دوم آنها متفاوت باشد. مانند نسخههای 3.3.1 و 3.4.1 و 3.5.1 اما نسخه 4.0.0 اجازه نصب ندارد |
نکته: برای مثال در این کتاب، من فایل bower.json را مستقیما ایجاد و ویرایش میکنم. ویرایش این فایل ساده است و به شما کمک میکند تا اطمینان حاصل کنید که نتایج مورد انتظار را در صورت پیگیری به همراه داشته باشد. همچنین ویژوال استودیو ابزار گرافیکی را نیز برای مدیریت بستههای bower فراهم میکند. شما میتوانید با کلیک راست بر روی فایل bower.json و انتخاب Manage Bower packages به منوی باز شده دسترسی داشته باشید. ویژوال استادیو فایلهای bower.json را برای تغییرات نظارت میکند و به صورت خودکار از ابزار Bower برای دانلود و نصب بستهها استفاده میکند. هنگامیکه شما تغییرات فایل را ذخیره میکنید، ویژوال استودیو بستهی BootStrap را دانلود میکند و در پوشهی wwwroot/lib ذخیره میکند.
مانند Nuget نیز Bower وابستگیهای مرتبط با بستههای اضافه شدهی به یک پروژه را مدیریت میکند. BootStrap برای دسترسی به برخی از ویژگیهای پیشرفته، به JQuery که یک کتابخانهی جاوا اسکریپتی است، تکیه میکند. به همین دلیل است که دو بسته را در شکل فوق نشان داده است. شما میتوانید لیست بستهها و وابستگیهای آنها را به صورت باز شده در بخش مورد نظر در Solution Explorer مشاهده کنید.
در ادامه کتاب، من از نسخه قبلی Bootstrap CSS framework استفاده میکنم. هنگامی که دارم این را مینویسم، تیم Bootstrap در حال توسعهی نسخهی 4 bootStrap است و چندین بار منتشر شدهاست. این نسخهها به عنوان "آلفا" برچسب گذاری شدهاند، اما کیفیت آنها بالا است و برای استفاده در نمونههای این کتاب به اندازه کافی پایدار است. با توجه به انتخاب نوشتن این کتاب با استفاده از Bootstrap 3 و نسخه پیش از نسخه بوت استرپ 4 و به زودی بایگانی شدن آن، تصمیم گرفتم از نسخه جدید استفاده کنم؛ حتی اگر برخی از نامهای کلاسها که برای شیوه نامههای عناصر HTML استفاده میشوند، احتمالا قبل از انتشار نهایی تغییر یابند. این مورد به این معنا است که شما باید همان نسخه از Bootstrap را که برای گرفتن نتایج موردنظر از خروجی نیاز دارید، استفاده کنید.
برای به روزرسانی بسته Bootstrap، شماره نسخه را در فایل bower.json تغییر دهید. مانند کد زیر:
{ "name": "asp.net", "private": true, "dependencies": { "bootstrap": "4.0.0-alpha.6" } }
زمانی که شما تغییرات فایل bower.json را ذخیره میکنید، ویژوال استودیو نسخه جدید BootStrap را دانلود میکند.
معرفی توسعه و کامپایل مداوم توسعه نرم افزار وب اغلب میتواند یک فرآیند تکراری باشد، جایی که تغییرات کوچکی را به ویووها یا کلاسها میدهید و برنامه را اجرا میکنید تا اثرات آن را آزمایش کنید. MVC و ویژوال استودیو همکاری میکنند تا از این رویکرد مداوم استفاده کنند تا تغییرات را سریعتر و آسانتر ببینید.
اعمال تغییرات در Razor Views
در زمان توسعه، تغییراتی که به Razor View اعمال میشوند، به محض رسیدن درخواستهای HTTP، از مرورگر دریافت میشوند. برای اینکه ببینید چطور کار میکند، برنامه را با انتخاب گزینه Start Debugging از منوی Debug اجرا کنید و هنگامیکه یک برگهی مرورگر باز شد و اطلاعات نمایش داده شد، تغییراتی را که در زیر نمایش میدهم در فایل Index.cshtml اعمال کنید.
@model IEnumerable<WorkingWithVisualStudio.Models.Product> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>>Working with Visual Studio</title> </head> <body> <h3>Products</h3> <table> <thead> <tr> <td>Name</td> <td>Price</td> </tr> </thead> <tbody> @foreach (var p in Model) { <tr> <td>@p.Name</td> <td>@($"{p.Price:C2}")</td> </tr>} </tbody> </table> </body> </html>
اعمال تغییرات در کلاسهای #C
برای کلاسهای #C، از جمله کنترلرها و مدلها، دو رویکرد موجود را که از طریق آیتمهای مختلف در منوی Debug انتخاب میشوند، شرح میدهم:
Start Without Debugging
تغییرات در کلاسها در پروژه به صورت خودکار زمانیکه یک درخواست HTTP دریافت میشود، برای مشاهدهی یک تجربهی توسعهی پویا، کامپایل میشوند. در این حالت برنامه بدون امکانات دیباگ و اشکالزادیی اجرا میشود.
Start Debugging
به شما اجزا میدهد صریح تغییرات را کامپایل کنید و برنامه را اجرا کنید ، بررسی مشکلات هم در زمان اجرا پروژه انجام میگیرد.به شما اجرا بررسی و تجزیه و تحلیل هر گونه مشکل در کد را میدهد.
کامپایل خودکار کلاس ها
در طول توسعه عادی، این چرخه کامپایل سریع به شما اجازه میدهد تا فورا تاثیر تغییرات خود را ببینید؛ حالا میتواند این تغییر اضافه نمودن یک اکشن جدید و یا ویرایش نمایش اطلاعات یک Model باشد. برای ارائهی این نوع از توسعه، ویژوال استودیو به محض رسیدن درخواست HTTP از مرورگر، تغییرات را دریافت و کلاسها را به صورت خودکار کامپایل میکند. برای دیدن اینکه چگونه کار میکند، گزینه Start Without Debugging را از منوی Debug در ویژوال استودیو انتخاب کنید. هنگامیکه مرورگر دادههای برنامه را نمایش میدهد، تغییرات زیر را در فایل Home controller ایجاد کنید:
namespace WorkingWithVisualStudio.Controllers { public class HomeController : Controller { public IActionResult Index() => View(SimpleRepository.SharedRepository.Products .Where(p => p.Price < 50)); } }
namespace WorkingWithVisualStudio.Models { public class SimpleRepository { private static SimpleRepository sharedRepository = new SimpleRepository(); private Dictionary<string, Product> products = new Dictionary<string, Product>(); public static SimpleRepository SharedRepository => sharedRepository; public SimpleRepository() { var initialItems = new[] { new Product { Name = "Kayak", Price = 275M }, new Product { Name = "Lifejacket", Price = 48.95M }, new Product { Name = "Soccer ball", Price = 19.50M }, new Product { Name = "Corner flag", Price = 34.95M } }; foreach (var p in initialItems) { AddProduct(p); } products.Add("Error", null); } public IEnumerable<Product> Products => products.Values; public void AddProduct(Product p) => products.Add(p.Name, p); } }
مشکلی مانند ورودی Null تا زمانیکه برنامه اجرا نشود، نمایش داده نمیشود. بارگذاری صفحه مرورگر باعث میشود کلاس SimpleRepository به صورت خودکار کامپایل شود و برنامه دوباره راه اندازی خواهد شد. هنگامیکه MVC نمونهای از کلاس Controller را برای پردازش درخواست HTTP از مرورگر ایجاد میکند، سازنده HomeController کلاس SimpleRepository را ایجاد خواهد کرد که به نوبه خود سعی میکند مقدار Null اضافه شده در لیست را پردازش کند. مقدار Null باعث بروز یک مشکل میشود، اما مشخص نیست مشکل چیست. مرورگر یک پیام مفید را نمایش نمیدهد.
توانایی نمایش صفحات خطاها
زمانیکه مشکلی در پنجرهی مرورگر ایجاد شد، میتوان یک راهنمای با اطلاعات مفید را نمایش داد. این قابلیت را میتوانید با فعال کردن نمایش صفحات انجام داد که باید در تنظیمات کلاس Startup تغییرات زیر را اعمال کنید.namespace WorkingWithVisualStudio { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage(); } } } }
استفاده از Debugger
ویژوال استادیو از اجرای یک برنامه MVC با استفاده از Debugger نیز پشتیبانی میکند که اجازه میدهد برنامه برای بررسی وضعیت نرم افزار و دنبال کردن درخواستی که به برنامه ارسال میشود، متوقف و از این طریق، پیگیری شود. این مورد نیاز به یک سبک متفاوت از توسعه را دارد. زیرا تغییراتی را در کلاسهای #C میدهیم، تا زمانیکه برنامه مجددا راه اندازی نشود، اعمال نمیشوند ( هرچند تغییرات Razor View هنوز هم به صورت خودکار اعمال میشوند). این سبک توسعه به همراه استفادهی از ویژگی کامپایل خودکار نیست؛ اما Debugger ویژوال استودیو عالی است و میتواند بینش عمیقتری را در مورد نحوهی کارکرد برنامه داشته باشد. برای اجرای برنامه با استفاده Debugger، در ویژوال استودیو از منوی Debug گزینهی Start Debugging را انتخاب کنید. ویژوال استودیو کلاسهای #C در پروژه را قبل از اجرای برنامه کامپایل میکند. اما شما همچنان میتوانید با استفاده از موارد موجود در منوی Build، کد خود را به صورت دستی نیز کامپایل کنید.
مثال فوق حاوی مقدار NULL است که سبب میشود یک NullReferenceException توسط کلاس SimpleRepository پرتاب شود. این حالت برنامه را قطع و کنترل اجرا را به توسعه دهنده منتقل میکند؛ همانطور که در شکل زیر نشان داده شده است
نکته: اگر Debugger خطا را نفهمد، گزینهی Windows ➤ exception settings را از منوی Debugger ویژوال استودیو انتخاب کنید و اطمینان حاصل کنید که تمام انواع خطاهای در لیست خطاهای زمان اجرای زبان مشترک، تایید شدهاست.
تنظیم یک Break-point Debugger عامل اصلی خطا را نمایش نمیدهد؛ تنها مکان آنرا آشکار میکند. عبارتیکه ویژوال استودیو برجسته میکند نشان میدهد که این مشکل زمانی رخ میدهد که فیلتر کردن اشیاء با استفاده از LINQ انجام شود، اما یک کار کوچک لازم است تا از جزئیات کاسته شود و به علت اصلی برسد.
Breakpoint عبارتی است که به Debugger میگوید تا برنامه را متوقف کند و کنترل دستی برنامه را به برنامه نویس میدهد. شما میتوانید وضعیت برنامه را بازبینی کنید و ببینید چه اتفاقی میافتد و به صورت اختیاری روند کاری را دوباره ادامه دهید.
برای ایجاد Breakpoint، روی عبارت راست کلیک کنید و در منوی باز شده، گزینه Breakpoint -> Insert Breakpoint را انتخاب کنید.
به عنوان مثال: یک Breakpoint به خط کد AddProduct در کلاس SimpleRepository اعمال کنید. همانطور که در شکل زیر نمایش داده میشود:
برنامه را اجرا کنید؛ با استفاده از Debug -> Start Debugging و یا با استفاده از Debug -> Restart برنامه را Restart میکنیم. در طی درخواست اولیه HTTP، برنامه اجرا میشود تا به نقطهای که Break Point دارد برسد و در آنجا برنامه متوقف میشود. در این نقطه، شما میتوانید از آیتمهای منوی Debug ویژوال استودیو یا کنترلها در بالای پنجره، برای کنترل اجرای برنامه استفاده کنید؛ یا از نمایشهای مختلف Debugger موجود از طریق Debug -> Windows برای بررسی وضعیت برنامه استفاده میکنیم.
مشاهده مقادیر داده در ویرایشگر کد
رایجترین استفاده Break Point، ردیابی مشکلات در کد شماست. قبل از اینکه بتوانید یک مشکل را رفع کنید، باید بدانید چه اتفاقی در حال رخ دادن است و یکی از ویژگیهای مفید ویژوال استودیو این است که توانایی مشاهده و کنترل ارزش متغیرها را درست در ویرایشگر کد، میدهد.اگر اشارهگر ماوس را بر روی پارامتر p به متد AddProduct که توسط Debugger برجسته شدهاست، حرکت دهید، یک فرم ظاهر خواهد شد که ارزش فعلی p را نشان میدهد؛ همانطور که در شکل زیر نشان داده شدهاست. من یک نمونه بزرگ شده از محتویات فرم ظاهر شده را نمایش میدهم تا به راحتی بتوانید متن در آن را بخوانید.
این مورد ممکن است مؤثر به نظر نرسد، چون شیء داده در یک سازنده همانند BreakPoint تعریف شدهاست. اما این ویژگیها برای هر متغیری کار میکند. شما میتوانید مقادیر را مشاهده کنید تا مقادیر خود و فیلد آنها را ببینید. هر مقدار دارای یک دکمه پین کوچک به سمت راست است. برای زمانیکه کد در حال اجراست، برای نظارت بر مقدار، از آن استفاده کنید.
اشارهگر ماوس را بر روی متغیر P قرار دهید و مرجع محصول را پین کنید. مرجع پیوست شده را باز کنید تا بتوانید نام و قیمت را نیز ببینید؛ مانند شکل زیر:
گزینه Continue را از منوی Debug در ویژوال استادیو انتخاب کنید تا برنامه ادامه پیدا کند. از آنجا که در برنامه حلقه Foreach وجود دارد، برنامه که دوباره اجرا میشود، وقتی مجددا به BreakPoint رسید، برنامه متوقف میشود. مقادیر پین شده در شکل زیر نشان میدهند که چگونه متغیر P و خواص آن تغییر میکنند.
استفاده از پنجره متغیرهای محلی ( Local Windows )
یکی از ویژگیهای مرتبط، پنجره Locals است که با انتخاب گزینهی منوی Debug ➤ Windows ➤ Locals باز میشود. پنجرهی Locals، مقدار متغیرها را به شکلی مشابه پنل پین شده نمایش میدهد، اما در اینجا تمام اشیاء محلی را نسبت به Break Point نمایش میدهد؛ همانطور که در شکل زیر نشان داده شدهاست:
هربار که Continue را انتخاب میکنید، اجرای برنامه ادامه یافته و یک شیء دیگر توسط حلقه foreach پردازش میشود.
اگر ادامه دهید، در زمان ویرایش کد، در هر دو پنجره Locals و در مقادیر پنل پین شده، شما مرجع Null را میبینید. برای کنترل اجرای برنامه، میتوانید جریان را از طریق کد خود در دیباگر دنبال کنید و احساس کنید که چه اتفاقی میافتد.
برای غیرفعال کردن BreakPoint، روی عبارت راست کلیک کنید و از منوی باز شده گزینه Delete BreakPoint را انتخاب کنید. برنامه را دوباره راه اندازی کنید و جدول داده سادهای را که در شکل نشان داده شده، مشاهده خواهید کرد.
استفاده از Browser Link
ویژگی Browser Link میتواند روند توسعه را با قرار دادن یک یا چند مرورگر تحت کنترل ویژوال استودیو، ساده سازی کند. این ویژگی مخصوصا مفید است اگر شما نیاز به دیدن اثر تغییرات را در طیف وسیعی از مرورگرها دارید. قابلیت Browser Link با و یا بدون Debugger کار میکند و به این معنا است که میتوانیم هر فایلی را در پروژه تغییر دهیم و تاثیر تغییر را بدون نیاز به تغییری در مرورگر مشاهده کنیم.
راه اندازی BrowserLink
برای فعال کردن Browser Link باید در کلاس Startup، تنظیمات را تغییر دهید. مانند کد زیر:
namespace WorkingWithVisualStudio { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); app.UseDeveloperExceptionPage(); } app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } } }
استفاده از Browser Link
برای درک اینکه Browser Link چگونه کار میکند، در ویژوال استودیو گزینه Start Without Debugging را از منوی Debug انتخاب میکنیم. ویژوال استودیو برنامه را اجرا میکند و یک برگه جدید مرورگر را برای نمایش نتیجه باز میکند. با بازبینی HTML ارسال شده به مرورگر، شما خواهید دید که حاوی بخش دیگری مانند این است:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>>Working with Visual Studio</title> </head> <body> <h3>Products</h3> <table> <thead> <tr><td>Name</td><td>Price</td></tr> </thead> <tbody> <tr><td>Lifejacket</td><td>£48.95</td></tr> <tr><td>Soccer ball</td><td>£19.50</td></tr> <tr><td>Corner flag</td><td>£34.95</td></tr> </tbody> </table> <!-- Visual Studio Browser Link --> <script type="application/json" id="__browserLink_initializationData"> {"requestId":"968949d8affc47c4a9c6326de21dfa03","requestMappingFromServer":false} </script> <script type="text/javascript" src="http://localhost:55356/d1a038413c804e178ef009a3be07b262/browserLink" async="async"></script> <!-- End Browser Link --> </body> </html>
ویژوال استادیو یک جفت عناصر اسکریپت را به HTML فرستاده شدهی به مرورگر اضافه میکند که برای بازکردن یک اتصال طولانی مدت HTTP با سرور برنامه کاربردی است؛ تا زمانیکه ویژوال استودیو مجددا برنامه را ریاستارت کند. کد زیر تغییر در فایل Index و تاثیر استفاده از Browser Link را نشان میدهد.
@model IEnumerable<WorkingWithVisualStudio.Models.Product> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>>Working with Visual Studio</title> </head> <body> <h3>Products</h3> <p>Request Time: @DateTime.Now.ToString("HH:mm:ss")</p> <table> <thead> <tr> <td>Name</td> <td>Price</td> </tr> </thead> <tbody> @foreach (var p in Model) { <tr> <td>@p.Name</td> <td>@($"{p.Price:C2}")</td> </tr>} </tbody> </table> </body> </html>
کد جاوا اسکریپتی که در HTML ارسال شده به مرورگر جاسازی شده، صحفه را دوباره بارگذاری میکند؛ برای دیدن تاثیرات کد اضافه شده که اضافه کردن یک timestamp ساده است.
نکته: عناصر اسکریپت Browser Link فقط در پاسخهای موفق جاسازی شده است. به این معنا که اگر یک خطا هنگام کامپایل در هنگام اجرا کردن یک Razor View یا مدیریت یک درخواست ایجاد شود، اتصال بین مرورگر و ویژوال استودیو از بین میرود و شما بعد از حل مشکل باید صفحه را مجدد بارگذاری کنید.
استفاده از مرورگرهای متعدد
Browser Link میتواند برای نمایش یک برنامه در مرورگرهای متعددی به طور همزمان استفاده شود و میتواند زمانی مفید باشد که شما میخواهید تفاوتهای پیاده سازی را بین مرورگرهای مختلف کنترل کنید و یا ببینید که چگونه یک برنامه بر روی ترکیبی از مرورگرهای دسکتاپ و تلفن همراه ارائه میشود.
برای انتخاب مرورگرهایی که استفاده میشوند، مرورگر را با استفاده از دکمه IIS Express در نوار ابزار ویژوال استودیو، انتخاب کنید؛ همانطور که در شکل زیر نشان داده شده است.
ویژوال استودیو لیستی از مرورگرهایی را که در مورد آنها اطلاعاتی دارد، نمایش میدهد. در عکس زیر مرورگرهایی را که من در سیستم خود نصب کردهام، نشان میدهد. برخی از آنها با ویندوز مانند Internet Explorer و Edge نصب میشوند.
ویژوال استادیو معمولا مرورگرهای رایجی را که نصب میشوند، نمایش میدهد. اما شما میتوانید با استفاده از دکمهی Add، برای اضافه کردن مرورگری که به صورت خودکار لیست نشده نیز استفاده کنید. همچنین میتوانید ابزار تست شخص ثالث مانند Browser Stack را نیز راه اندازی کنید که مرورگرها را بر روی سرویسهای ابری میزبان ( cloud-hosted ) و ماشینهای مجازی اجرا میکند.
من سه مرورگر را در شکل انتخاب کردم: Chrome ، Internet Explorer و Edge. با کلیک بر روی دکمه Browse، فعالیت هر سه مرورگر شروع میشود و باعث میشود URL مثال برنامه را بارگذاری کند؛ همانطور که در شکل نشان داده شده است.
با استفاده از منوی Browser Link Dashboard، شما میتوانید ببینید که چه مرورگرهایی در Browser Link انتخاب شدهاند. داشبورد آن نشانی اینترنتی نمایش داده شده توسط هر مرورگر را نشان میدهد و در اینجا هر مرورگر را میتوان به صورت جداگانه رفرش کرد.
آماده سازی جاوا اسکریپت و CSS برای استقرار
هنگامی که Client-Side بخشی از یک برنامه وب را ایجاد میکنید، معمولا تعدادی از فایلهای جاوا اسکریپت و CSS سفارشی را تهیه میکنید که برای تکمیل آنها، از بستههای نصب شدهی توسط Bower استفاده میشود. این فایلها نیاز به پردازش دارند تا آنها را برای تحویل در یک محیط تولید، بهینه سازی کنند تا تعداد درخواستهای HTTP و میزان پهنای باند شبکه مورد نیاز برای ارسال آنها به مشتری، به حداقل برسد. این فرآیند به عنوان بسته بندی شناخته میشود.
فعال کردن تحویل محتوای استاتیک
ASP.Net Core شامل پشتیبانی از ارائه فایلهای استاتیک از پوشه wwwroot به مشتریان است. اما این امکان به صورت پیشفرض در زمان ایجاد یک پروژهی خالی جدید فعال نیست و شما باید با قرار دادن عبارتی در فایل StartUp آن را فعال کنید؛ مانند کد زیر:
namespace WorkingWithVisualStudio { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); app.UseStaticFiles(); app.UseDeveloperExceptionPage(); } app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } } }
اضافه کردن محتوای استاتیک به پروژه
برای نشان دادن فرآیند بسته بندی، من نیاز به اضافه کردن تعدادی محتوای استاتیک به پروژه و یکی کردن آنها با برنامهی نمونه را دارم. برای این منظور ابتدا یک پوشهی جدید را به نام wwwroot/css ایجاد کنید که محل متداولی برای فایلهای سفارشی CSS است. من فایلی را به نام First.css با استفاده از قالب آیتم Style Sheet اضافه کردم؛ همانطور که در شکل زیر نشان داده شده است. قالب Style Sheet در مسیر Asp.Net Core -> Web -> Content Section وجود دارد.
فایل First.Css را ویرایش کنید و محتوای زیر را در آن قرار دهید.
h3 { } table, td { border: 2px solid black; border-collapse: collapse; padding: 5px; }
فایلهای جاوا اسکریپت معمولا در پوشه wwwroot/js قرار میگیرند. من این پوشه را ایجاد کردم. فایلهای جاوا اسکریپت را میتوانید در مسیر Asp.Net Core -> Web -> Script انتخاب کنید. همانطور که در شکل زیر نشان داده شده است.
من کد جاوا اسکریپتی ساده زیر را به این فایل جدید اضافه کردم؛ همانطور که در لیست نشان داده شده است.
document.addEventListener("DOMContentLoaded", function () { var element = document.createElement("p"); element.textContent = "This is the element from the third.js file"; document.querySelector("body").appendChild(element); });
من به بیش از یک فایل جاوا اسکریپت نیاز دارم. بنابراین فایل دیگری را به نام fourth.js نیز در پوشه wwwroot ایجاد میکنم و محتوای زیر را در آن قرار میدهم.
document.addEventListener("DOMContentLoaded", function () { var element = document.createElement("p"); element.textContent = "This is the element from the fourth.js file"; document.querySelector("body").appendChild(element); });
به روز رسانی View
گام نهایی، به روز رسانی فایل Index.cshtml برای استفاده از Css و فایل جاوا اسکریپت است. کدهای آن در زیر نشان داده شده است:
@model IEnumerable<WorkingWithVisualStudio.Models.Product> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>>Working with Visual Studio</title> <link rel="stylesheet" href="css/first.css" /> <link rel="stylesheet" href="css/second.css" /> <script src="js/third.js"></script> <script src="js/fourth.js"></script> </head> <body> <h3>Products</h3> <p>Request Time: @DateTime.Now.ToString("HH:mm:ss")</p> <table> <thead> <tr> <td>Name</td> <td>Price</td> </tr> </thead> <tbody> @foreach (var p in Model) { <tr> <td>@p.Name</td> <td>@($"{p.Price:C2}")</td> </tr>} </tbody> </table> </body> </html>
اگر برنامه کاربردی را اجرا کنید، محتویات نشان داده شدهی در شکل زیر را مشاهده خواهید کرد. محتوای موجود توسط شیوه نامههای CSS شبیه سازی شده است و کد جاوا اسکریپتی جدیدی را اضافه کرده است.
یکی کردن فایلهای سمت کلاینت در برنامههای MVC
در حال حاضر چهار فایل استاتیک وجود دارند و مرورگر باید چهار درخواست را برای دریافت فایلهای استاتیک ایجاد کند و هر یک از این فایلها نیازمند پهنای باند بیشتری است که باید به مشتری تحویل داده شود؛ زیرا آنها حاوی فضای سفید و نام متغیرها هستند که برای توسعه دهندهها معنا دار هستند؛ اما برای مرورگرها اهمیتی ندارند.
ترکیب فایلهایی هم نوع، تلفیق نامیده میشود و در آن کار ساختن فایلها به صورتی کوچکتر انجام میشود. هر دوی این کارها در برنامه Asp.Net Core MVC توسط Bundler & Minifier مخصوص ویژوال استودیو انجام میشود.
نصب افزونههای ویژوال استودیو
اولین قدم برای نصب افزونه، انتخاب از منوی Tools -> Extensions and Update و کلیک بر روی مجموعه Online است تا افزونههای ویژوال استودیو را در مجموعه نمایش بدهد. نام افزونه را در جعبه جستجوی در گوشهی سمت راست بالای پنجره وارد کنید؛ همانطور که در شکل زیر نشان داده شده است. محل نصب افزونه را مشخص میکنیم و بر روی دانلود کلیک میکنیم تا آن را به ویژوال استودیو اضافه کند. ویژوال استودیو را مجدد راه اندازی کنید تا فرآیند نصب تکمیل شود.
دسته بندی و یکی کردن فایلها
پس از نصب افزونه، ویژوال استودیو را مجددا راه اندازی کنید و پروژه نمونه را باز کنید. با افزودن افزونه، میتوانید چندین فایل هم نوع را در Solution Explorer انتخاب کنید. آنها را با یکدیگر ترکیب کرده و محتویات آنها را کوچکتر کنید. به عنوان مثال فایلهای First.css و Second.css را در Solution Explorer را انتخاب و کلیک راست کرده و سپس Bundler & Minifier -> Bundle and Minify Files را از منوی باز شده انتخاب کنید . همانطور که در شکل زیر نشان داده شده است.
فایل خروجی را با عنوان bundle.css ذخیره کنید. در Solution Explorer یک بسته جدید ایجاد میشود. اگر شما این فایل را باز کنید، خواهید دید که محتویات هر دو فایل CSS جداگانه ترکیب شدهاند و تمام فضای سفید آنها حذف شدهاست. البته شما نمیخواهید به طور مستقیم با این فایل کار کنید؛ اما این فایل کوچکتر است و فقط یک اتصال HTTP را برای ارائه CSS styles به مشتری نیاز دارد.
مراحل قبل را برای فایلهای third.js و fourth.js تکرار کنید تا فایلهای جدید bundle.js و bundle.min.js در پوشه wwwroot ایجاد شوند.
احتیاط: اطمینان حاصل کنید که فایلها را به ترتیبی که توسط مرورگر بارگیری میشوند، انتخاب کنید تا ترتیب دستورات Styleها یا دستورات کد را در فایلهای خروجی حفظ کنید. به عنوان مثال دقت کنید که فایل third.js قبل از فایل fourth.js انتخاب شده باشد تا مطمئن باشید دستورات به ترتیب و به درستی اجرا میشوند.
کد زیر، عناصر پیوند فایلهای جداگانهای را که باید در فایل Index.cshtml قرار گیرند، نمایش میدهد:
@model IEnumerable<WorkingWithVisualStudio.Models.Product> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>>Working with Visual Studio</title> <link rel="stylesheet" href="css/bundle.min.css" /> <script src="js/bundle.min.js"></script> </head> <body> <h3>Products</h3> <p>Request Time: @DateTime.Now.ToString("HH:mm:ss")</p> <table> <thead> <tr> <td>Name</td> <td>Price</td> </tr> </thead> <tbody> @foreach (var p in Model) { <tr> <td>@p.Name</td> <td>@($"{p.Price:C2}")</td> </tr>} </tbody> </table> </body> </html>
همان زمان که عملیات جمع آوری و یکی کردن را انجام میدهید، رکورد عملیات انجام شده را در فایلی به نام bundleconfig.json در پوشهی wwwroot پروژه نگهداری میکند. در اینجا یک نمونه از فایل تولیدی را مشاهده میکنید:
[ { "outputFileName": "Views/wwwroot/css/bundle.css", "inputFiles": [ "Views/wwwroot/css/First.css", "Views/wwwroot/css/second.css" ] }, { "outputFileName": "Views/wwwroot/js/bundle.js", "inputFiles": [ "Views/wwwroot/js/fourth.js", "Views/wwwroot/js/third.js" ] } ]
خلاصه
در این بخش من توضیحاتی را در مورد ویژگیهایی که ویژوال استودیو برای طراحی برنامههای وب به توسعه دهندهها ارائه میدهد، شرح دادم که شامل کامپایل خودکار کلاسها، Browser Link و یکی کردن فایلهای سمت کلاینت ( bundling and minification ) بود.
Angular 5.x به همراه پشتیبانی از RxJS 5.5.x منتشر شدهاست. RxJS 5.5 نیز به همراه تغییر قابل ملاحظهای در نحوهی import اجزای آن توسط ویژگی جدید lettable operators است. در این مطلب نحوهی ارتقاء برنامههای قبلی به این نگارش جدید و همچنین اثر آنرا بر اندازهی برنامهی نهایی تولید شده، بررسی میکنیم.
روش جدید import اجزای RxJS در نگارش 5.5 آن
تغییرات تعاریف عملگرها:
تا پیش از Angular 5 و RxJS 5.5 (و یا Angular CLI versions <1.5.0)، اگر نیاز به عملگری (operator/function) مانند map وجود داشت، روش import آن به صورت زیر بود:
اما پس از RxJS 5.5 امکان import آنها با روش مخصوص ES 6 میسر شدهاست (به نام جمع operators دقت داشته باشید؛ چون مسیر rxjs/observable نیز وجود دارد):
بنابراین در این حالت دیگر روش import یکجای این تعاریف در فایلی مانند «rxjs-operators.ts» وجود ندارد و این تعاریف باید به ازای هر فایلی که از آنها استفاده میکنند، مانند سایر importهای ES 6 یکبار دیگر مجددا ذکر شوند؛ مانند:
در حالت کلی مسیر node_modules/rxjs/operators را برای یافتن متدهای جدید بررسی کنید.
همچنین در این نگارش، Observable بجای rxjs/Rx :
از rxjs/Observable دریافت میشود:
تا بتوان از قابلیتهای جدید آن استفاده کرد.
تغییرات تعاریف statics:
برای صدور خطاها بجای throw قبلی:
خواهیم داشت:
و برای ایجاد تایمر، بجای timer پیشین:
خواهیم داشت:
و به طور کلی مسیر node_modules\rxjs\observable را برای یافتن تعاریف static قبلی جستجو کنید.
معرفی lettable operators
Lettable Operators توابعی هستند که یک observable را دریافت و یک observable را بازگشت میدهند؛ به آنها pipeable operators هم میگویند. از این جهت که در اینجا متد جدید pipe، برای ترکیب چندین تابع عملگر بر روی مقادیر observable توسط آن، ارائه شدهاست.
مزیت این روش این است که pipeable/lettable operators، یک سری تابع محض هستند و اگر مورد استفاده قرار نگیرند، به سادگی توسط سیستم و ابزار ساخت برنامه، از فایل نهایی حذف خواهند شد؛ یا اصطلاحا tree-shakable هست. اما روش پیشین تعریف این عملگرها، tree-shakable نبوده و حتی اگر توسط برنامه مورد استفاده قرار نگیرند، در بستهی نهایی تولید شده، حضور خواهند داشت. Tree-shaking به معنای پروسهی حذف کدهای مرده است. روش جدید استفادهی از importهای ES 6، امکان تشخیص عملگرهای استفاده نشده را توسط ابزارهایی مانند TS-Lint و تنظیمات کامپایلر TypeScript به سادگی میسر میکنند و به این ترتیب با حذف متدها و ماژولهای استفاده نشده، میتوان به حجم نهایی بسیار کمتری رسید.
روش قبلی تعریف عملگرهای Observable، اصطلاحا Patching نامیده میشود. به این معنا که هر متد جدید import شدهی در برنامه، به Observable.prototype اصلی اضافه و وصله میشود. اما در این روش جدید، تنها متد وصله شدهی از پیش موجود، Observable.prototype.pipe است و تمام متدهای دیگر import شده، توابع محض هستند و نه وصلهای به Observable.prototype اصلی. زمانیکه وصلهای به Observable.prototype متصل میشود، دیگر امکان حذف آن توسط ابزارهای ساخت برنامه وجود ندارد (حتی اگر استفاده نشده باشند)؛ اما اگر این متدها به صورت خالص و مجزای از Observable.prototype ارائه شوند، امکان حذف کدهای مرده و استفاده نشده، به سادگی میسر خواهد شد؛ چون توابعی خالص و متکی به خود هستند.
یک نمونه مثال استفادهی از pipeable/lettable operators را در کد زیر مشاهده میکنید:
این مثال، جمع به توان 2 اعداد را در یک بازهی مشخص، محاسبه میکند. برای این منظور ابتدا یک منبع Observable توسط متد range ایجاد شدهاست.
در اینجا روش تعریف Observableها نیز تغییر کردهاست و از متد of جهت کار با تعدادی ورودی مشخص و یا متد range برای کار با بازهای از اعداد، استفاده میشود:
سپس توسط متد pipe، ترکیبی از متدهای RxJS را مشاهده میکند که بر روی منبع Observable اصلی کار میکنند.
متد filter، اعداد فرد بازه را انتخاب میکند. متد map این اعداد انتخابی را به توان 2 میرساند و سپس متد scan آنها را با هم جمع میزند و نتیجه توسط متد pipe به صورت یک Observable دیگر بازگشت داده میشود که میتوان مشترک آن شد و برای مثال خروجی فوق را در console درج کرد.
تغییر نام عملگرهای قبلی RxJS
تا اینجا دریافتیم که هدف اصلی pipeable/lettable operators، عدم معرفی آنها به صورت یک وصلهی جدید جدانشدنی از Observable.prototype، به صورت توابع خالص است. اکنون که این عملگرها، تبدیل به متدهای خالص و متکی به خود شدهاند، نباید با متدهای اصلی جاوا اسکریپت تداخل نام پیدا کنند؛ به همین جهت برای ارتقاء کدهای قدیمی خود، به این تغییر نامها نیاز خواهید داشت: متد do به tap تغییر نام یافتهاست. متد switch شدهاست switchAll. بجای catch اینبار catchError داریم و finally شدهاست finalize.
مثالی از ارتقاء کدهای قدیمی به روش جدید RxJS 5.5
اگر مثال روش قدیمی مبتنی بر وصله کردن Observable.prototype، به صورت زیر باشد:
معادل جدید آن به این صورت تغییر میکند:
زمانیکه تعریف Observable از مسیر rxjs/Observable درخواست میشود، به همراه عملگر وصله شدهی pipe نیز هست. به همین جهت نیازی به تعریف مجدد آن نیست. پس از آن متدهای map و filter، به داخل pipe منتقل میشوند. در این بین نیاز است تغییر نام متدها را که پیشتر نیز ذکر شد، مدنظر داشته باشید.
به عنوان یک مثال تکمیلی، کدهای سری «احراز هویت و اعتبارسنجی کاربران در برنامههای Angular» جهت استفادهی از pipeable/lettable operators به روز رسانی شدهاند. لیست تغییرات آنها را در اینجا میتوانید مشاهده کنید.
روش جدید import اجزای RxJS در نگارش 5.5 آن
تغییرات تعاریف عملگرها:
تا پیش از Angular 5 و RxJS 5.5 (و یا Angular CLI versions <1.5.0)، اگر نیاز به عملگری (operator/function) مانند map وجود داشت، روش import آن به صورت زیر بود:
import 'rxjs/add/operator/map';
import { map } from 'rxjs/operators';
import { map, catchError, tap } from 'rxjs/operators';
همچنین در این نگارش، Observable بجای rxjs/Rx :
import { Observable } from 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
تغییرات تعاریف statics:
برای صدور خطاها بجای throw قبلی:
import 'rxjs/add/observable/throw'; Observable.throw('error');
import { ErrorObservable } from 'rxjs/observable/ErrorObservable'; ErrorObservable.create('error');
و برای ایجاد تایمر، بجای timer پیشین:
import "rxjs/add/observable/timer"; const timerSource$ = Observable.timer(initialDelay);
import { timer } from 'rxjs/observable/timer'; const timerSource$ = timer(initialDelay);
معرفی lettable operators
Lettable Operators توابعی هستند که یک observable را دریافت و یک observable را بازگشت میدهند؛ به آنها pipeable operators هم میگویند. از این جهت که در اینجا متد جدید pipe، برای ترکیب چندین تابع عملگر بر روی مقادیر observable توسط آن، ارائه شدهاست.
مزیت این روش این است که pipeable/lettable operators، یک سری تابع محض هستند و اگر مورد استفاده قرار نگیرند، به سادگی توسط سیستم و ابزار ساخت برنامه، از فایل نهایی حذف خواهند شد؛ یا اصطلاحا tree-shakable هست. اما روش پیشین تعریف این عملگرها، tree-shakable نبوده و حتی اگر توسط برنامه مورد استفاده قرار نگیرند، در بستهی نهایی تولید شده، حضور خواهند داشت. Tree-shaking به معنای پروسهی حذف کدهای مرده است. روش جدید استفادهی از importهای ES 6، امکان تشخیص عملگرهای استفاده نشده را توسط ابزارهایی مانند TS-Lint و تنظیمات کامپایلر TypeScript به سادگی میسر میکنند و به این ترتیب با حذف متدها و ماژولهای استفاده نشده، میتوان به حجم نهایی بسیار کمتری رسید.
روش قبلی تعریف عملگرهای Observable، اصطلاحا Patching نامیده میشود. به این معنا که هر متد جدید import شدهی در برنامه، به Observable.prototype اصلی اضافه و وصله میشود. اما در این روش جدید، تنها متد وصله شدهی از پیش موجود، Observable.prototype.pipe است و تمام متدهای دیگر import شده، توابع محض هستند و نه وصلهای به Observable.prototype اصلی. زمانیکه وصلهای به Observable.prototype متصل میشود، دیگر امکان حذف آن توسط ابزارهای ساخت برنامه وجود ندارد (حتی اگر استفاده نشده باشند)؛ اما اگر این متدها به صورت خالص و مجزای از Observable.prototype ارائه شوند، امکان حذف کدهای مرده و استفاده نشده، به سادگی میسر خواهد شد؛ چون توابعی خالص و متکی به خود هستند.
یک نمونه مثال استفادهی از pipeable/lettable operators را در کد زیر مشاهده میکنید:
import { from} from 'rxjs/observable/from'; import { map, scan, filter } from 'rxjs/operators'; const source$ = range(1,10); const sumOfSquaredOddNumbers$ = source$.pipe( filter(n => n % 2 !== 0), map(n => n * n), scan((acc,s) => acc + s, 0) ); sumOfSquaredOddNumbers$.subscribe(v => console.log(v)); /*** Output ****/ 1 10 35 84 165
در اینجا روش تعریف Observableها نیز تغییر کردهاست و از متد of جهت کار با تعدادی ورودی مشخص و یا متد range برای کار با بازهای از اعداد، استفاده میشود:
import { of } from 'rxjs/observable/of'; import { from } from 'rxjs/observable/from'; import { range } from 'rxjs/observable/range'; const source$ = of(1,2,3); const rangeSource$ = range(0,5);
متد filter، اعداد فرد بازه را انتخاب میکند. متد map این اعداد انتخابی را به توان 2 میرساند و سپس متد scan آنها را با هم جمع میزند و نتیجه توسط متد pipe به صورت یک Observable دیگر بازگشت داده میشود که میتوان مشترک آن شد و برای مثال خروجی فوق را در console درج کرد.
تغییر نام عملگرهای قبلی RxJS
تا اینجا دریافتیم که هدف اصلی pipeable/lettable operators، عدم معرفی آنها به صورت یک وصلهی جدید جدانشدنی از Observable.prototype، به صورت توابع خالص است. اکنون که این عملگرها، تبدیل به متدهای خالص و متکی به خود شدهاند، نباید با متدهای اصلی جاوا اسکریپت تداخل نام پیدا کنند؛ به همین جهت برای ارتقاء کدهای قدیمی خود، به این تغییر نامها نیاز خواهید داشت: متد do به tap تغییر نام یافتهاست. متد switch شدهاست switchAll. بجای catch اینبار catchError داریم و finally شدهاست finalize.
مثالی از ارتقاء کدهای قدیمی به روش جدید RxJS 5.5
اگر مثال روش قدیمی مبتنی بر وصله کردن Observable.prototype، به صورت زیر باشد:
import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/filter'; names = allUserData. map(user => user.name). filter(name => name);
import { Observable } from 'rxjs/Observable'; import { map, filter } from 'rxjs/operators'; names = allUserData.pipe( map(user => user.name), filter(name => name), );
به عنوان یک مثال تکمیلی، کدهای سری «احراز هویت و اعتبارسنجی کاربران در برنامههای Angular» جهت استفادهی از pipeable/lettable operators به روز رسانی شدهاند. لیست تغییرات آنها را در اینجا میتوانید مشاهده کنید.
قصد داریم اطلاعات یک فرم React را به همراه دو فایل الصاقی به آن، به سمت یک سرور ASP.NET Core ارسال کنیم؛ بطوریکه درصد پیشرفت ارسال فایلها، زمان سپری شده، زمان باقی مانده و سرعت آپلود نیز گزارش داده شوند:
پیشنیازها
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax »
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
در مورد این قالب که امکان تجربهی توسعهی یکپارچهی ASP.NET Core و React را میسر میکند، در مطلب «روش یکی کردن پروژههای React و ASP.NET Core» بیشتر بحث کردهایم.
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف و پوشهی خالی wwwroot را نیز به آن اضافه میکنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
از تنظیم زیر استفاده کردهایم تا با هر بار تغییری در کدهای پروژهی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
بدیهی است در این حالت باید از طریق خط فرمان به پوشهی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود. البته ما برنامه را به صورت یکپارچه بر روی پورت 5001 وب سرور ASP.NET Core، مرور میکنیم.
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
سپس وارد این پوشهی جدید شده و بستههای زیر را نصب میکنیم:
توضیحات:
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام میدهد، مورد استفاده قرار میگیرد.
- برای نمایش پیامهای برنامه از کامپوننت react-toastify استفاده میکنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آنرا اضافه کنیم:
همچنین نیاز است ToastContainer را به ابتدای متد render آن نیز اضافه کرد:
- برای ارسال فایلها به سمت سرور از کتابخانهی معروف axios استفاده خواهیم کرد.
ایجاد کامپوننت React فرم ارسال فایلها به سمت سرور
پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل میکنیم:
کاری که تا این مرحله انجام شده، بازگشت UI فرم برنامه توسط یک functional component است.
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شدهاند.
- مرحلهی بعد، دسترسی به فایلهای انتخابی کاربر و همچنین مقدار توضیحات وارد شدهاست. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کردهایم:
با React Hooks، بجای تعریف یک state، به صورت خاصیت، آنرا صرفا use میکنیم و یا همان useState، که یک تابع است و باید در ابتدای کامپوننت، مورد استفاده قرار گیرد. این متد برای شروع به کار، نیاز به یک state آغازین را دارد؛ مانند انتساب یک رشتهی خالی به description. سپس اولین خروجی متد useState که داخل یک آرایه مشخص شدهاست، همان متغیر description است که توسط state ردیابی خواهد شد. اینبار بجای متد this.setState قبلی که یک متد عمومی بود، متدی اختصاصی را صرفا جهت تغییر مقدار همین متغیر description به نام setDescription به عنوان دومین خروجی متد useState، تعریف میکنیم. بنابراین متد useState، یک initialState را دریافت میکند و سپس یک مقدار را به همراه یک متد، جهت تغییر state آن، بازگشت میدهد. همین کار را برای دو فیلد دیگر نیز تکرار کردهایم. بنابراین selectedFile1، فایلی است که توسط متد setSelectedFile1 تنظیم خواهد شد و این تنظیم، سبب رندر مجدد UI نیز خواهد گردید.
- پس از طراحی state این فرم، مرحلهی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، میتوان آنرا به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخداد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
در مورد فیلدهای دریافت فایلها، روش انجام اینکار به صورت زیر است:
چون المانهای دریافت فایل میتوانند بیش از یک فایل را نیز دریافت کنند (اگر ویژگی multiple، به تعریف تگ آنها اضافه شود)، به همین جهت خاصیت files بر روی آنها قابل دسترسی شدهاست. اما چون در اینجا ویژگی multiple ذکر نشدهاست، بنابراین تنها یک فایل توسط آنها قابل دریافت است و به همین جهت دسترسی به اولین فایل و یا files[0] را در اینجا مشاهده میکنید. بنابراین با فراخوانی متد setSelectedFile1، اکنون متغیر حالت selectedFile1، مقدار دهی شده و قابل استفاده است.
تشکیل مدل ارسال دادهها به سمت سرور
در فرمهای معمولی، عموما دادهها به صورت یک شیء JSON به سمت سرور ارسال میشوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرمهایی که به همراه المانهای دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه میشود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا میکند:
اما اگر قرار باشد همین فرم را توسط جاوا اسکریپت به سمت سرور ارسال کنیم، روش کار به صورت زیر است:
ابتدا به خاصیت files و اولین فایل آن دسترسی پیدا کرده و سپس شیء استاندارد FormData را بر اساس آن و تمام فیلدهای فرم تشکیل میدهیم. FormData ساختاری شبیه به یک دیکشنری را دارد و از کلیدهایی که متناظر با Id المانهای فرم و مقادیری متناظر با مقادیر آن المانها هستند، تشکیل میشود که توسط متد append آن، به این دیکشنری اضافه خواهند شد. در آخر هم شیء formData را به سمت سرور ارسال میکنیم.
در یک برنامهی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام دادهایم. در مرحلهی بعد، شیء FormData را تشکیل خواهیم داد:
به همین جهت، ابتدا کار مدیریت رخداد onSubmit فرم را انجام داده و توسط آن با استفاده از متد preventDefault، از post-back متداول فرم به سمت سرور جلوگیری میکنیم. سپس شیء FormData را بر اساس مقادیر حالت متناظر با المانهای فرم، تشکیل میدهیم. کلیدهایی که در اینجا ذکر میشوند، نام خواص مدل متناظر سمت سرور را نیز تشکیل خواهند داد.
ارسال مدل دادههای فرم React به سمت سرور
پس از تشکیل شیء FormData در متد مدیریت کنندهی handleSubmit، اکنون با استفاده از کتابخانهی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
در اینجا نحوهی ارسال شیء FormData را توسط کتابخانهی axios به سمت سرور مشاهده میکنید. با استفاده از متد post آن، به سمت مسیر api/SimpleUpload/SaveTicket که آنرا در ادامه تکمیل خواهیم کرد، شیء formData متناظر با اطلاعات فرم، به صورت async، ارسال شدهاست. همچنین headers آن نیز به همان «"enctype="multipart/form-data» که پیشتر توضیح داده شد، تنظیم شدهاست.
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده میکنید. از آن میتوان برای فعال و غیرفعال کردن دکمهی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
به این ترتیب اگر فراخوانی await axios.post هنوز به پایان نرسیده باشد، مقدار isUploading مساوی true بوده و سبب غیرفعال شدن دکمهی submit میشود.
اعتبارسنجی سمت کلاینت فایلهای ارسالی به سمت سرور
در اینجا شاید نیاز باشد نوع و یا اندازهی فایلهای انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه میکنیم:
در اینجا ابتدا بررسی میشود که آیا فایلی انتخاب شدهاست یا خیر؟ سپس فایل انتخاب شده، باید دارای یکی از MimeTypeهای تعریف شده باشد. همچنین اندازهی آن نیز نباید بیشتر از 500 کیلوبایت باشد. در هر کدام از این موارد، یک خطا توسط react-toastify به کاربر نمایش داده خواهد شد.
اکنون برای استفادهی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کنندهی submit اطلاعات:
در ابتدای متد مدیریت کنندهی handleSubmit، متد isFileValid را بر روی دو متغیر حالتی که حاوی اطلاعات فایلهای انتخابی توسط کاربر هستند، فراخوانی میکنیم.
ب) استفادهی از آن جهت غیرفعال کردن دکمهی submit:
میتوان دقیقا در همان زمانیکه کاربر فایلی را انتخاب میکند نیز به انتخاب او واکنش نشان داد. چون مقدار دهیهای متغیرهای حالت، همواره سبب رندر مجدد فرم میشوند و در این حالت مقدار ویژگی disabled نیز محاسبهی مجدد خواهد شد، بنابراین در همان زمانیکه کاربر فایلی را انتخاب میکند، متد isFileValid نیز بر روی آن فراخوانی شده و در صورت نیاز، خطایی به او نمایش داده میشود.
نمایش درصد پیشرفت آپلود فایلها
کتابخانهی axios، امکان دسترسی به میزان اطلاعات آپلود شدهی به سمت سرور را به صورت یک رخداد فراهم کردهاست که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایلها استفاده میکنیم:
هر بار که متد رویدادگردان onUploadProgress فراخوانی میشود، به همراه اطلاعات شیء progressEvent است که خواص loaded آن به معنای میزان اطلاعات آپلود شده و total هم جمع کل اندازهی اطلاعات در حال ارسال است. بر این اساس و همچنین زمان شروع عملیات، میتوان اطلاعاتی مانند درصد پیشرفت عملیات، مدت زمان باقیمانده، مدت زمان سپری شده و سرعت آپلود اطلاعات را محاسبه کرد و سپس توسط آن، شیء state ویژهای را به روز رسانی کرد که به صورت زیر تعریف میشود:
هر بار به روز رسانی state، سبب رندر مجدد UI میشود. به همین جهت متدی را برای رندر جدولی که اطلاعات شیء state فوق را نمایش میدهد، به صورت زیر تهیه میکنیم:
و این متد را به این شکل در ذیل المان fieldset فرم، اضافه میکنیم تا کار رندر نهایی را انجام دهد:
هربار که state به روز میشود، مقدار شیء uploadProgress دریافت شده و بر اساس آن، 4 سطر جدول نمایش پیشرفت آپلود، تکمیل میشوند.
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست. اگر style آنرا هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.
یک نکته: اگر میخواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگهی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایلها، 100 درصد را مشاهده خواهید کرد.
دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیرهی فایلهای آن
بر اساس نحوهی تشکیل FormData سمت کلاینت:
مدل سمت سرور معادل با آن به صورت زیر خواهد بود:
که در اینجا هر selectedFile سمت کلاینت، به یک IFormFile سمت سرور نگاشت میشود. نام این خواص نیز باید با نام کلیدهای اضافه شدهی به دیکشنری FormData، یکی باشند.
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده میکنید:
توضیحات تکمیلی:
- تزریق IWebHostEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
ویژگی FromForm نیز مرتبط است به هدر multipart/form-data ارسالی از سمت کلاینت:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
پیشنیازها
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax »
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف و پوشهی خالی wwwroot را نیز به آن اضافه میکنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios react-toastify
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای نمایش پیامهای برنامه از کامپوننت react-toastify استفاده میکنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آنرا اضافه کنیم:
import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";
render() { return ( <React.Fragment> <ToastContainer />
ایجاد کامپوننت React فرم ارسال فایلها به سمت سرور
پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل میکنیم:
import React, { useState } from "react"; import axios from "axios"; import { toast } from "react-toastify"; export default function UploadFileSimple() { const [description, setDescription] = useState(""); const [selectedFile1, setSelectedFile1] = useState(); const [selectedFile2, setSelectedFile2] = useState(); return ( <form> <fieldset className="form-group"> <legend>Support Form</legend> <div className="form-group row"> <label className="form-control-label" htmlFor="description"> Description </label> <input type="text" className="form-control" name="description" onChange={event => setDescription(event.target.value)} value={description} /> </div> <div className="form-group row"> <label className="form-control-label" htmlFor="file1"> File 1 </label> <input type="file" className="form-control" name="file1" onChange={event => setSelectedFile1(event.target.files[0])} /> </div> <div className="form-group row"> <label className="form-control-label" htmlFor="file2"> File 2 </label> <input type="file" className="form-control" name="file2" onChange={event => setSelectedFile2(event.target.files[0])} /> </div> <div className="form-group row"> <button className="btn btn-primary" type="submit" > Submit </button> </div> </fieldset> </form> ); }
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شدهاند.
- مرحلهی بعد، دسترسی به فایلهای انتخابی کاربر و همچنین مقدار توضیحات وارد شدهاست. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کردهایم:
const [description, setDescription] = useState(""); const [selectedFile1, setSelectedFile1] = useState(); const [selectedFile2, setSelectedFile2] = useState();
- پس از طراحی state این فرم، مرحلهی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، میتوان آنرا به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخداد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
<input type="text" className="form-control" name="description" onChange={event => setDescription(event.target.value)} value={description} />
<input type="file" className="form-control" name="file1" onChange={event => setSelectedFile1(event.target.files[0])} />
تشکیل مدل ارسال دادهها به سمت سرور
در فرمهای معمولی، عموما دادهها به صورت یک شیء JSON به سمت سرور ارسال میشوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرمهایی که به همراه المانهای دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه میشود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا میکند:
<form enctype="multipart/form-data" action="/upload" method="post"> <input id="file-input" type="file" /> </form>
let file = document.getElementById("file-input").files[0]; let formData = new FormData(); formData.append("file", file); fetch('/upload/image', {method: "POST", body: formData});
در یک برنامهی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام دادهایم. در مرحلهی بعد، شیء FormData را تشکیل خواهیم داد:
// ... export default function UploadFileSimple() { // ... const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2); toast.success("Form has been submitted successfully!"); setDescription(""); }; return ( <form onSubmit={handleSubmit}> </form> ); }
ارسال مدل دادههای فرم React به سمت سرور
پس از تشکیل شیء FormData در متد مدیریت کنندهی handleSubmit، اکنون با استفاده از کتابخانهی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
// ... export default function UploadFileSimple() { const apiUrl = "https://localhost:5001/api/SimpleUpload/SaveTicket"; // ... const [isUploading, setIsUploading] = useState(false); const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2); try { setIsUploading(true); const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }} }); toast.success("Form has been submitted successfully!"); console.log("uploadResult", data); setIsUploading(false); setDescription(""); } catch (error) { setIsUploading(false); toast.error(error); } }; return ( // ... ); }
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده میکنید. از آن میتوان برای فعال و غیرفعال کردن دکمهی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
<button disabled={ isUploading } className="btn btn-primary" type="submit" > Submit </button>
اعتبارسنجی سمت کلاینت فایلهای ارسالی به سمت سرور
در اینجا شاید نیاز باشد نوع و یا اندازهی فایلهای انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه میکنیم:
const isFileValid = selectedFile => { if (!selectedFile) { // toast.error("Please select a file."); return false; } const allowedMimeTypes = [ "image/png", "image/jpeg", "image/gif", "image/svg+xml" ]; if (!allowedMimeTypes.includes(selectedFile.type)) { toast.error(`Invalid file type: ${selectedFile.type}`); return false; } const maxFileSize = 1024 * 500; const fileSize = selectedFile.size; if (fileSize > maxFileSize) { toast.error( `File size ${(fileSize / 1024).toFixed( 2 )} KB must be less than ${maxFileSize / 1024} KB` ); return false; } return true; };
اکنون برای استفادهی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کنندهی submit اطلاعات:
const handleSubmit = async event => { event.preventDefault(); if (!isFileValid(selectedFile1) || !isFileValid(selectedFile2)) { return; }
ب) استفادهی از آن جهت غیرفعال کردن دکمهی submit:
<button disabled={ isUploading || !isFileValid(selectedFile1) || !isFileValid(selectedFile2) } className="btn btn-primary" type="submit" > Submit </button>
نمایش درصد پیشرفت آپلود فایلها
کتابخانهی axios، امکان دسترسی به میزان اطلاعات آپلود شدهی به سمت سرور را به صورت یک رخداد فراهم کردهاست که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایلها استفاده میکنیم:
const startTime = Date.now(); const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }, onUploadProgress: progressEvent => { const { loaded, total } = progressEvent; const timeElapsed = Date.now() - startTime; const uploadSpeed = loaded / (timeElapsed / 1000); setUploadProgress({ queueProgress: Math.round((loaded / total) * 100), uploadTimeRemaining: Math.ceil((total - loaded) / uploadSpeed), uploadTimeElapsed: Math.ceil(timeElapsed / 1000), uploadSpeed: (uploadSpeed / 1024).toFixed(2) }); } });
const [uploadProgress, setUploadProgress] = useState({ queueProgress: 0, uploadTimeRemaining: 0, uploadTimeElapsed: 0, uploadSpeed: 0 });
const showUploadProgress = () => { const { queueProgress, uploadTimeRemaining, uploadTimeElapsed, uploadSpeed } = uploadProgress; if (queueProgress <= 0) { return <></>; } return ( <table className="table"> <thead> <tr> <th width="15%">Event</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td> <strong>Elapsed time</strong> </td> <td>{uploadTimeElapsed} second(s)</td> </tr> <tr> <td> <strong>Remaining time</strong> </td> <td>{uploadTimeRemaining} second(s)</td> </tr> <tr> <td> <strong>Upload speed</strong> </td> <td>{uploadSpeed} KB/s</td> </tr> <tr> <td> <strong>Queue progress</strong> </td> <td> <div className="progress-bar progress-bar-info progress-bar-striped" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow={queueProgress} style={{ width: queueProgress + "%" }} > {queueProgress}% </div> </td> </tr> </tbody> </table> ); };
{showUploadProgress()}
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست. اگر style آنرا هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.
یک نکته: اگر میخواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگهی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایلها، 100 درصد را مشاهده خواهید کرد.
دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیرهی فایلهای آن
بر اساس نحوهی تشکیل FormData سمت کلاینت:
const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2);
using Microsoft.AspNetCore.Http; namespace UploadFilesSample.Models { public class Ticket { public int Id { set; get; } public string Description { set; get; } public IFormFile File1 { set; get; } public IFormFile File2 { set; get; } } }
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده میکنید:
using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using UploadFilesSample.Models; namespace UploadFilesSample.Controllers { [Route("api/[controller]")] [ApiController] public class SimpleUploadController : Controller { private readonly IWebHostEnvironment _environment; public SimpleUploadController(IWebHostEnvironment environment) { _environment = environment; } [HttpPost("[action]")] public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket) { var file1Path = await saveFileAsync(ticket.File1); var file2Path = await saveFileAsync(ticket.File2); //TODO: save the ticket ... get id return Created("", new { id = 1001 }); } private async Task<string> saveFileAsync(IFormFile file) { const string uploadsFolder = "uploads"; var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } //TODO: Do security checks ...! if (file == null || file.Length == 0) { return string.Empty; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream); } return $"/{uploadsFolder}/{file.Name}"; } } }
- تزریق IWebHostEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }} });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
FileTable is a great new feature in SQL Server 2012 which facilitates storing and working with unstructured blob data in SQL Server. Unfortunately is not yet supported in Entity Framework, so you cannot simply use FileTable in your entity model. But there is a workaround for this inconvenience, which I am going to show you in this post.
مطالب
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
در «قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی»، اطلاعات TestUser تنظیم شدهی در کلاس Config برنامهی IDP را به بانک اطلاعاتی منتقل کردیم که در نتیجهی آن سه جدول Users، UserClaims و UserLogins، تشکیل شدند. در اینجا میخواهیم سایر قسمتهای کلاس Config را نیز به بانک اطلاعاتی منتقل کنیم.
تنظیم مجوز امضای توکنهای IDP
تا اینجا تنظیمات کلاس آغازین برنامه چنین شکلی را دارد که AddCustomUserStore آنرا در قسمت دهم به آن افزودیم.
مرحلهی بعد، تغییر AddDeveloperSigningCredential به یک نمونهی واقعی است. استفادهی از روش فعلی آن چنین مشکلاتی را ایجاد میکند:
- اگر برنامهی IDP را در سرورهای مختلفی توزیع کنیم و این سرورها توسط یک Load balancer مدیریت شوند، هر درخواست رسیده، به سروری متفاوت هدایت خواهد شد. در این حالت هر برنامه نیز مجوز امضای توکن متفاوتی را پیدا میکند. برای مثال اگر یک توکن دسترسی توسط سرور A امضاء شود، اما در درخواست بعدی رسیده، توسط مجوز سرور B تعیین اعتبار شود، این اعتبارسنجی با شکست مواجه خواهد شد.
- حتی اگر از یک Load balancer استفاده نکنیم، به طور قطع Application pool برنامه در سرور، در زمانی خاص Recycle خواهد شد. این مورد DeveloperSigningCredential تنظیم شده را نیز ریست میکند. یعنی با ریاستارت شدن Application pool، کلیدهای مجوز امضای توکنها تغییر میکنند که در نهایت سبب شکست اعتبارسنجی توکنهای صادر شدهی توسط IDP میشوند.
بنابراین برای انتشار نهایی برنامه نمیتوان از DeveloperSigningCredential فعلی استفاده کرد و نیاز است یک signing certificate را تولید و تنظیم کنیم. برای این منظور از برنامهی makecert.exe مایکروسافت که جزئی از SDK ویندوز است، استفاده میکنیم. این فایل را از پوشهی src\IDP\DNT.IDP\MakeCert نیز میتوانید دریافت کنید.
سپس دستور زیر را با دسترسی admin اجرا کنید:
در اینجا تاریخ شروع و پایان اعتبار مجوز ذکر شدهاند. همچنین نتیجهی آن به صورت خودکار در LocalMachine certificate store ذخیره میشود. به همین جهت اجرای آن نیاز به دسترسی admin را دارد.
پس از آن در قسمت run ویندوز، دستور mmc را وارد کرده و enter کنید. سپس از منوی File گزینهی Add remove span-in را انتخاب کنید. در اینجا certificate را add کنید. در صفحهی باز شده Computer Account و سپس Local Computer را انتخاب کنید و در نهایت OK. اکنون میتوانید این مجوز جدید را در قسمت «Personal/Certificates»، مشاهده کنید:
در اینجا Thumbprint این مجوز را در حافظه کپی کنید؛ از این جهت که در ادامه از آن استفاده خواهیم کرد.
چون این مجوز از نوع self signed است، در قسمت Trusted Root Certification Authorities قرار نگرفتهاست که باید این انتقال را انجام داد. در غیراینصورت میتوان توسط آن توکنهای صادر شده را امضاء کرد اما به عنوان یک توکن معتبر به نظر نخواهند رسید.
در ادامه این مجوز جدید را انتخاب کرده و بر روی آن کلیک راست کنید. سپس گزینهی All tasks -> export را انتخاب کنید. نکتهی مهمی را که در اینجا باید رعایت کنید، انتخاب گزینهی «yes, export the private key» است. کپی و paste این مجوز از اینجا به جایی دیگر، این private key را export نمیکند. در پایان این عملیات، یک فایل pfx را خواهید داشت.
- در آخر نیاز است این فایل pfx را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید. برای اینکار بر روی نود certificate آن کلیک راست کرده و گزینهی All tasks -> import را انتخاب کنید. سپس مسیر فایل pfx خود را داده و این مجوز را import نمائید.
پس از ایجاد مجوز امضای توکنها و انتقال آن به Trusted Root Certification Authorities، نحوهی معرفی آن به IDP به صورت زیر است:
متد کمکی loadCertificateFromStore، بر اساس thumbPrint مجوز تولید شده، آنرا بارگذاری میکند. سپس این مجوز، توسط متد AddSigningCredential به IdentityServer معرفی خواهد شد و یا اگر فایل pfx ای را دارید، میتوانید از متد loadCertificateFromFile استفاده کنید. این متد برای اینکه در IIS به درستی کار کند، نیاز است در خواص Application pool سایت IDP، گزینهی Load user profile را انتخاب کرده باشید (مهم!).
پس از این تغییرات، برنامه را اجرا کنید. سپس مسیر discovery document را طی کرده و آدرس jwks_uri آنرا در مرورگر باز کنید. در اینجا خاصیت kid نمایش داده شده با thumbPrint مجوز یکی است.
انتقال سایر قسمتهای فایل Config برنامهی IDP به بانک اطلاعاتی
قسمت آخر آماده سازی برنامه برای انتشار آن، انتقال سایر دادههای فایل Config، مانند Resources و Clients برنامهی IDP، به بانک اطلاعاتی است. البته هیچ الزامی هم به انجام اینکار نیست. چون اگر تعداد برنامههای متفاوتی که در سازمان قرار است از IDP استفاده کنند، کم است، تعریف مستقیم آنها داخل فایل Config برنامهی IDP، مشکلی را ایجاد نمیکند و این تعداد رکورد الزاما نیازی به بانک اطلاعاتی ندارند. اما اگر بخواهیم امکان به روز رسانی این اطلاعات را بدون نیاز به کامپایل مجدد برنامهی IDP توسط یک صفحهی مدیریتی داشته باشیم، نیاز است آنها را به بانک اطلاعاتی منتقل کنیم. این مورد مزیت به اشتراک گذاری یک چنین اطلاعاتی را توسط Load balancers نیز میسر میکند.
البته باید درنظر داشت قسمت دیگر اطلاعات IdentityServer شامل refresh tokens و reference tokens هستند. تمام اینها اکنون در حافظه ذخیره میشوند که با ریاستارت شدن Application pool برنامه از بین خواهند رفت. بنابراین حداقل در این مورد استفادهی از بانک اطلاعاتی اجباری است.
خوشبختانه قسمت عمدهی این کار توسط خود تیم IdentityServer توسط بستهی IdentityServer4.EntityFramework انجام شدهاست که در اینجا از آن استفاده خواهیم کرد. البته در اینجا این بستهی نیوگت را مستقیما مورد استفاده قرار نمیدهیم. از این جهت که نیاز به 2 رشتهی اتصالی جداگانه و دو Context جداگانه را دارد که داخل خود این بسته تعریف شدهاست و ترجیح میدهیم که اطلاعات آنرا با ApplicationContext خود یکی کنیم.
برای این منظور آخرین سورس کد پایدار آنرا از این آدرس دریافت کنید:
https://github.com/IdentityServer/IdentityServer4.EntityFramework/releases
انتقال موجودیتها به پروژهی DNT.IDP.DomainClasses
در این بستهی دریافتی، در پوشهی src\IdentityServer4.EntityFramework\Entities آن، کلاسهای تعاریف موجودیتهای متناظر با منابع IdentityServer قرار دارند. بنابراین همین فایلها را از این پروژه استخراج کرده و به پروژهی DNT.IDP.DomainClasses در پوشهی جدید IdentityServer4Entities اضافه میکنیم.
البته در این حالت پروژهی DNT.IDP.DomainClasses نیاز به این وابستگیها را خواهد داشت:
پس از این انتقال، فضاهای نام این کلاسها را نیز اصلاح میکنیم؛ تا با پروژهی جاری تطابق پیدا کنند.
انتقال تنظیمات روابط بین موجودیتها، به پروژهی DNT.IDP.DataLayer
در فایل src\IdentityServer4.EntityFramework\Extensions\ModelBuilderExtensions.cs بستهی دریافتی، تعاریف تنظیمات این موجودیتها به همراه نحوهی برقراری ارتباطات بین آنها قرار دارد. بنابراین این اطلاعات را نیز از این فایل استخراج و به پروژهی DNT.IDP.DataLayer اضافه میکنیم. البته در اینجا از روش IEntityTypeConfiguration برای قرار هر کدام از تعاریف یک در کلاس مجزا استفاده کردهایم.
پس از این انتقال، به کلاس Context برنامه مراجعه کرده و توسط متد builder.ApplyConfiguration، این فایلهای IEntityTypeConfiguration را معرفی میکنیم.
تعاریف DbSetهای متناظر با موجودیتهای منتقل و تنظیم شده در پروژهی DNT.IDP.DataLayer
پس از انتقال موجودیتها و روابط بین آنها، دو فایل DbContext را در این بستهی دریافتی خواهید یافت:
الف) فایل src\IdentityServer4.EntityFramework\DbContexts\ConfigurationDbContext.cs
این فایل، موجودیتهای تنظیمات برنامه مانند Resources و Clients را در معرض دید EF Core قرار میدهد.
سپس فایل src\IdentityServer4.EntityFramework\Interfaces\IConfigurationDbContext.cs نیز جهت استفادهی از این DbContext در سرویسهای این بستهی دریافتی تعریف شدهاست.
ب) فایل src\IdentityServer4.EntityFramework\DbContexts\PersistedGrantDbContext.cs
این فایل، موجودیتهای ذخیره سازی اطلاعات مخصوص IDP را مانند refresh tokens و reference tokens، در معرض دید EF Core قرار میدهد.
همچنین فایل src\IdentityServer4.EntityFramework\Interfaces\IPersistedGrantDbContext.cs نیز جهت استفادهی از این DbContext در سرویسهای این بستهی دریافتی تعریف شدهاست.
ما در اینجا DbSetهای هر دوی این DbContextها را در ApplicationDbContext خود، خلاصه و ادغام میکنیم.
انتقال نگاشتهای AutoMapper بستهی دریافتی به پروژهی جدید DNT.IDP.Mappings
در پوشهی src\IdentityServer4.EntityFramework\Mappers، تعاریف نگاشتهای AutoMapper، برای تبدیلات بین موجودیتهای برنامه و IdentityServer4.Models انجام شدهاست. کل محتویات این پوشه را به یک پروژهی Class library جدید به نام DNT.IDP.Mappings منتقل و فضاهای نام آنرا نیز اصلاح میکنیم.
انتقال src\IdentityServer4.EntityFramework\Options به پروژهی DNT.IDP.Models
در پوشهی Options بستهی دریافتی سه فایل موجود هستند:
الف) Options\ConfigurationStoreOptions.cs
این فایل، به همراه تنظیمات نام جداول متناظر با ذخیره سازی اطلاعات کلاینتها است. نیازی به آن نداریم؛ چون زمانیکه موجودیتها و تنظیمات آنها را به صورت مستقیم در اختیار داریم، نیازی به فایل تنظیمات ثالثی برای انجام اینکار نیست.
ب) Options\OperationalStoreOptions.cs
این فایل، تنظیمات نام جداول مرتبط با ذخیره سازی توکنها را به همراه دارد. به این نام جداول نیز نیازی نداریم. اما این فایل به همراه سه تنظیم زیر جهت پاکسازی دورهای توکنهای قدیمی نیز هست:
از این تنظیمات در سرویس TokenCleanup استفاده میشود. به همین جهت همین سه مورد را به پروژهی DNT.IDP.Models منتقل کرده و سپس بجای اینکه این کلاس را مستقیما در سرویس TokenCleanup تزریق کنیم، آنرا از طریق سیستم Configuration و فایل appsettings.json به این سرویس تزریق میکنیم؛ به کمک سرویس توکار IOptions خود ASP.NET Core:
ج) Options\TableConfiguration.cs
کلاسی است به همراه خواص نام اسکیمای جداول که در دو کلاس تنظیمات قبلی بکار رفتهاست. نیازی به آن نداریم.
انتقال سرویسهای IdentityServer4.EntityFramework به پروژهی DNT.IDP.Services
بستهی دریافتی، شامل دو پوشهی src\IdentityServer4.EntityFramework\Services و src\IdentityServer4.EntityFramework\Stores است که سرویسهای آنرا تشکیل میدهند (جمعا 5 سرویس TokenCleanup، CorsPolicyService، ClientStore، PersistedGrantStore و ResourceStore). بنابراین این سرویسها را نیز مستقیما از این پوشهها به پروژهی DNT.IDP.Services کپی خواهیم کرد.
همانطور که عنوان شد دو فایل Interfaces\IConfigurationDbContext.cs و Interfaces\IPersistedGrantDbContext.cs برای دسترسی به دو DbContext این بستهی دریافتی در سرویسهای آن، تعریف شدهاند و چون ما در اینجا صرفا یک ApplicationDbContext را داریم که از طریق IUnitOfWork، در دسترس لایهی سرویس قرار میگیرد، ارجاعات به دو اینترفیس یاد شده را با IUnitOfWork تعویض خواهیم کرد تا مجددا قابل استفاده شوند.
انتقال متدهای الحاقی معرفی سرویسهای IdentityServer4.EntityFramework به پروژهی DNT.IDP
پس از انتقال قسمتهای مختلف IdentityServer4.EntityFramework به لایههای مختلف برنامهی جاری، اکنون نیاز است سرویسهای آنرا به برنامه معرفی کرد که در نهایت جایگزین متدهای فعلی درون حافظهای کلاس آغازین برنامهی IDP میشوند. خود این بسته در فایل زیر، به همراه متدهایی الحاقی است که این معرفی را انجام میدهند:
src\IdentityServer4.EntityFramework\Extensions\IdentityServerEntityFrameworkBuilderExtensions.cs
به همین جهت این فایل را به پروژهی وب DNT.IDP ، منتقل خواهیم کرد؛ همانجایی که در قسمت دهم AddCustomUserStore را تعریف کردیم.
این کلاس پس از انتقال، نیاز به تغییرات ذیل را دارد:
الف) چون یکسری از کلاسهای تنظیمات را حذف کردیم و نیازی به آنها نداریم، آنها را نیز به طور کامل از این فایل حذف میکنیم. تنها تنظیم مورد نیاز آن، OperationalStoreOptions است که اینبار آنرا از فایل appsettings.json دریافت میکنیم. بنابراین ذکر این مورد نیز در اینجا اضافی است.
ب) در آن، دو Context موجود در بستهی اصلی IdentityServer4.EntityFramework مورد استفاده قرار گرفتهاند. ما در اینجا آنها را نیز با تک Context برنامهی خود تعویض میکنیم.
ج) در آن سرویسی به نام TokenCleanupHost تعریف شدهاست. آنرا نیز به لایهی سرویسها منتقل میکنیم. همچنین در امضای سازندهی آن بجای تزریق مستقیم OperationalStoreOptions از <IOptions<OperationalStoreOptions استفاده خواهیم کرد.
نگارش نهایی و تمیز شدهی IdentityServerEntityFrameworkBuilderExtensions را در اینجا مشاهده میکنید.
افزودن متدهای الحاقی جدید به فایل آغازین برنامهی IDP
پس از انتقال IdentityServerEntityFrameworkBuilderExtensions به پروژهی DNT.IDP، اکنون نوبت به استفادهی از آن است:
به این ترتیب متدهای الحاقی جدید AddConfigurationStore و AddOperationalStore جهت معرفی محلهای ذخیره سازی اطلاعات کاربران، منابع و توکنهای IdentityServer مورد استفاده قرار میگیرند.
اجرای Migrations در پروژهی DNT.IDP.DataLayer
پس از پایان این نقل و انتقالات، اکنون نیاز است ساختار بانک اطلاعاتی برنامه را بر اساس موجودیتها و روابط جدید بین آنها، به روز رسانی کنیم. به همین جهت فایل add_migrations.cmd موجود در پوشهی src\IDP\DNT.IDP.DataLayer را اجرا میکنیم تا کلاسهای Migrations متناظر تولید شوند و سپس فایل update_db.cmd را اجرا میکنیم تا این تغییرات، به بانک اطلاعاتی برنامه نیز اعمال گردند.
انتقال اطلاعات فایل درون حافظهای Config، به بانک اطلاعاتی برنامه
تا اینجا اگر برنامه را اجرا کنیم، دیگر کار نمیکند. چون جداول کلاینتها و منابع آن خالی هستند. به همین جهت نیاز است اطلاعات فایل درون حافظهای Config را به بانک اطلاعاتی منتقل کنیم. برای این منظور سرویس ConfigSeedDataService را به برنامه اضافه کردهایم:
این سرویس به کمک اطلاعات Mappings مخصوص AutoMapper این پروژه، IdentityServer4.Models تعریف شدهی در کلاس Config درون حافظهای را به موجودیتهای اصلی DNT.IDP.DomainClasses.IdentityServer4Entities تبدیل و سپس در بانک اطلاعاتی ذخیره میکند.
برای استفادهی از آن، به کلاس آغازین برنامهی IDP مراجعه میکنیم:
در اینجا توسط متد seedDb، متدهای درون حافظهای کلاس Config به سرویس ConfigSeedDataService ارسال شده، توسط Mappings تعریف شده، به معادلهای موجودیتهای برنامه تبدیل و سپس در بانک اطلاعاتی ذخیره میشوند.
آزمایش برنامه
پس از اعمال این تغییرات، برنامهها را اجرا کنید. سپس به بانک اطلاعاتی آن مراجعه نمائید. در اینجا لیست جداول جدیدی را که اضافه شدهاند ملاحظه میکنید:
همچنین برای نمونه، در اینجا اطلاعات منابع منتقل شدهی به بانک اطلاعاتی، از فایل Config، قابل مشاهده هستند:
و یا قسمت ذخیره سازی خودکار توکنهای تولیدی توسط آن نیز به درستی کار میکند:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
تنظیم مجوز امضای توکنهای IDP
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddCustomUserStore() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients());
مرحلهی بعد، تغییر AddDeveloperSigningCredential به یک نمونهی واقعی است. استفادهی از روش فعلی آن چنین مشکلاتی را ایجاد میکند:
- اگر برنامهی IDP را در سرورهای مختلفی توزیع کنیم و این سرورها توسط یک Load balancer مدیریت شوند، هر درخواست رسیده، به سروری متفاوت هدایت خواهد شد. در این حالت هر برنامه نیز مجوز امضای توکن متفاوتی را پیدا میکند. برای مثال اگر یک توکن دسترسی توسط سرور A امضاء شود، اما در درخواست بعدی رسیده، توسط مجوز سرور B تعیین اعتبار شود، این اعتبارسنجی با شکست مواجه خواهد شد.
- حتی اگر از یک Load balancer استفاده نکنیم، به طور قطع Application pool برنامه در سرور، در زمانی خاص Recycle خواهد شد. این مورد DeveloperSigningCredential تنظیم شده را نیز ریست میکند. یعنی با ریاستارت شدن Application pool، کلیدهای مجوز امضای توکنها تغییر میکنند که در نهایت سبب شکست اعتبارسنجی توکنهای صادر شدهی توسط IDP میشوند.
بنابراین برای انتشار نهایی برنامه نمیتوان از DeveloperSigningCredential فعلی استفاده کرد و نیاز است یک signing certificate را تولید و تنظیم کنیم. برای این منظور از برنامهی makecert.exe مایکروسافت که جزئی از SDK ویندوز است، استفاده میکنیم. این فایل را از پوشهی src\IDP\DNT.IDP\MakeCert نیز میتوانید دریافت کنید.
سپس دستور زیر را با دسترسی admin اجرا کنید:
makecert.exe -r -pe -n "CN=DntIdpSigningCert" -b 01/01/2018 -e 01/01/2025 -eku 1.3.6.1.5.5.7.3.3 -sky signature -a sha256 -len 2048 -ss my -sr LocalMachine
پس از آن در قسمت run ویندوز، دستور mmc را وارد کرده و enter کنید. سپس از منوی File گزینهی Add remove span-in را انتخاب کنید. در اینجا certificate را add کنید. در صفحهی باز شده Computer Account و سپس Local Computer را انتخاب کنید و در نهایت OK. اکنون میتوانید این مجوز جدید را در قسمت «Personal/Certificates»، مشاهده کنید:
در اینجا Thumbprint این مجوز را در حافظه کپی کنید؛ از این جهت که در ادامه از آن استفاده خواهیم کرد.
چون این مجوز از نوع self signed است، در قسمت Trusted Root Certification Authorities قرار نگرفتهاست که باید این انتقال را انجام داد. در غیراینصورت میتوان توسط آن توکنهای صادر شده را امضاء کرد اما به عنوان یک توکن معتبر به نظر نخواهند رسید.
در ادامه این مجوز جدید را انتخاب کرده و بر روی آن کلیک راست کنید. سپس گزینهی All tasks -> export را انتخاب کنید. نکتهی مهمی را که در اینجا باید رعایت کنید، انتخاب گزینهی «yes, export the private key» است. کپی و paste این مجوز از اینجا به جایی دیگر، این private key را export نمیکند. در پایان این عملیات، یک فایل pfx را خواهید داشت.
- در آخر نیاز است این فایل pfx را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید. برای اینکار بر روی نود certificate آن کلیک راست کرده و گزینهی All tasks -> import را انتخاب کنید. سپس مسیر فایل pfx خود را داده و این مجوز را import نمائید.
پس از ایجاد مجوز امضای توکنها و انتقال آن به Trusted Root Certification Authorities، نحوهی معرفی آن به IDP به صورت زیر است:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddSigningCredential(loadCertificateFromStore()) .AddCustomUserStore() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()); } private X509Certificate2 loadCertificateFromStore() { var thumbPrint = Configuration["CertificateThumbPrint"]; using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine)) { store.Open(OpenFlags.ReadOnly); var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true); if (certCollection.Count == 0) { throw new Exception("The specified certificate wasn't found."); } return certCollection[0]; } } private X509Certificate2 loadCertificateFromFile() { // NOTE: // You should check out the identity of your application pool and make sure // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work. var certificate = new X509Certificate2( fileName: Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data", Configuration["X509Certificate:FileName"]), password: Configuration["X509Certificate:Password"], keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); return certificate; } } }
پس از این تغییرات، برنامه را اجرا کنید. سپس مسیر discovery document را طی کرده و آدرس jwks_uri آنرا در مرورگر باز کنید. در اینجا خاصیت kid نمایش داده شده با thumbPrint مجوز یکی است.
https://localhost:6001/.well-known/openid-configuration https://localhost:6001/.well-known/openid-configuration/jwks
انتقال سایر قسمتهای فایل Config برنامهی IDP به بانک اطلاعاتی
قسمت آخر آماده سازی برنامه برای انتشار آن، انتقال سایر دادههای فایل Config، مانند Resources و Clients برنامهی IDP، به بانک اطلاعاتی است. البته هیچ الزامی هم به انجام اینکار نیست. چون اگر تعداد برنامههای متفاوتی که در سازمان قرار است از IDP استفاده کنند، کم است، تعریف مستقیم آنها داخل فایل Config برنامهی IDP، مشکلی را ایجاد نمیکند و این تعداد رکورد الزاما نیازی به بانک اطلاعاتی ندارند. اما اگر بخواهیم امکان به روز رسانی این اطلاعات را بدون نیاز به کامپایل مجدد برنامهی IDP توسط یک صفحهی مدیریتی داشته باشیم، نیاز است آنها را به بانک اطلاعاتی منتقل کنیم. این مورد مزیت به اشتراک گذاری یک چنین اطلاعاتی را توسط Load balancers نیز میسر میکند.
البته باید درنظر داشت قسمت دیگر اطلاعات IdentityServer شامل refresh tokens و reference tokens هستند. تمام اینها اکنون در حافظه ذخیره میشوند که با ریاستارت شدن Application pool برنامه از بین خواهند رفت. بنابراین حداقل در این مورد استفادهی از بانک اطلاعاتی اجباری است.
خوشبختانه قسمت عمدهی این کار توسط خود تیم IdentityServer توسط بستهی IdentityServer4.EntityFramework انجام شدهاست که در اینجا از آن استفاده خواهیم کرد. البته در اینجا این بستهی نیوگت را مستقیما مورد استفاده قرار نمیدهیم. از این جهت که نیاز به 2 رشتهی اتصالی جداگانه و دو Context جداگانه را دارد که داخل خود این بسته تعریف شدهاست و ترجیح میدهیم که اطلاعات آنرا با ApplicationContext خود یکی کنیم.
برای این منظور آخرین سورس کد پایدار آنرا از این آدرس دریافت کنید:
https://github.com/IdentityServer/IdentityServer4.EntityFramework/releases
انتقال موجودیتها به پروژهی DNT.IDP.DomainClasses
در این بستهی دریافتی، در پوشهی src\IdentityServer4.EntityFramework\Entities آن، کلاسهای تعاریف موجودیتهای متناظر با منابع IdentityServer قرار دارند. بنابراین همین فایلها را از این پروژه استخراج کرده و به پروژهی DNT.IDP.DomainClasses در پوشهی جدید IdentityServer4Entities اضافه میکنیم.
البته در این حالت پروژهی DNT.IDP.DomainClasses نیاز به این وابستگیها را خواهد داشت:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="System.ComponentModel.Annotations" Version="4.3.0" /> <PackageReference Include="IdentityServer4" Version="2.2.0" /> </ItemGroup> </Project>
انتقال تنظیمات روابط بین موجودیتها، به پروژهی DNT.IDP.DataLayer
در فایل src\IdentityServer4.EntityFramework\Extensions\ModelBuilderExtensions.cs بستهی دریافتی، تعاریف تنظیمات این موجودیتها به همراه نحوهی برقراری ارتباطات بین آنها قرار دارد. بنابراین این اطلاعات را نیز از این فایل استخراج و به پروژهی DNT.IDP.DataLayer اضافه میکنیم. البته در اینجا از روش IEntityTypeConfiguration برای قرار هر کدام از تعاریف یک در کلاس مجزا استفاده کردهایم.
پس از این انتقال، به کلاس Context برنامه مراجعه کرده و توسط متد builder.ApplyConfiguration، این فایلهای IEntityTypeConfiguration را معرفی میکنیم.
تعاریف DbSetهای متناظر با موجودیتهای منتقل و تنظیم شده در پروژهی DNT.IDP.DataLayer
پس از انتقال موجودیتها و روابط بین آنها، دو فایل DbContext را در این بستهی دریافتی خواهید یافت:
الف) فایل src\IdentityServer4.EntityFramework\DbContexts\ConfigurationDbContext.cs
این فایل، موجودیتهای تنظیمات برنامه مانند Resources و Clients را در معرض دید EF Core قرار میدهد.
سپس فایل src\IdentityServer4.EntityFramework\Interfaces\IConfigurationDbContext.cs نیز جهت استفادهی از این DbContext در سرویسهای این بستهی دریافتی تعریف شدهاست.
ب) فایل src\IdentityServer4.EntityFramework\DbContexts\PersistedGrantDbContext.cs
این فایل، موجودیتهای ذخیره سازی اطلاعات مخصوص IDP را مانند refresh tokens و reference tokens، در معرض دید EF Core قرار میدهد.
همچنین فایل src\IdentityServer4.EntityFramework\Interfaces\IPersistedGrantDbContext.cs نیز جهت استفادهی از این DbContext در سرویسهای این بستهی دریافتی تعریف شدهاست.
ما در اینجا DbSetهای هر دوی این DbContextها را در ApplicationDbContext خود، خلاصه و ادغام میکنیم.
انتقال نگاشتهای AutoMapper بستهی دریافتی به پروژهی جدید DNT.IDP.Mappings
در پوشهی src\IdentityServer4.EntityFramework\Mappers، تعاریف نگاشتهای AutoMapper، برای تبدیلات بین موجودیتهای برنامه و IdentityServer4.Models انجام شدهاست. کل محتویات این پوشه را به یک پروژهی Class library جدید به نام DNT.IDP.Mappings منتقل و فضاهای نام آنرا نیز اصلاح میکنیم.
انتقال src\IdentityServer4.EntityFramework\Options به پروژهی DNT.IDP.Models
در پوشهی Options بستهی دریافتی سه فایل موجود هستند:
الف) Options\ConfigurationStoreOptions.cs
این فایل، به همراه تنظیمات نام جداول متناظر با ذخیره سازی اطلاعات کلاینتها است. نیازی به آن نداریم؛ چون زمانیکه موجودیتها و تنظیمات آنها را به صورت مستقیم در اختیار داریم، نیازی به فایل تنظیمات ثالثی برای انجام اینکار نیست.
ب) Options\OperationalStoreOptions.cs
این فایل، تنظیمات نام جداول مرتبط با ذخیره سازی توکنها را به همراه دارد. به این نام جداول نیز نیازی نداریم. اما این فایل به همراه سه تنظیم زیر جهت پاکسازی دورهای توکنهای قدیمی نیز هست:
namespace IdentityServer4.EntityFramework.Options { public class OperationalStoreOptions { public bool EnableTokenCleanup { get; set; } = false; public int TokenCleanupInterval { get; set; } = 3600; public int TokenCleanupBatchSize { get; set; } = 100; } }
public TokenCleanup( IServiceProvider serviceProvider, ILogger<TokenCleanup> logger, IOptions<OperationalStoreOptions> options)
کلاسی است به همراه خواص نام اسکیمای جداول که در دو کلاس تنظیمات قبلی بکار رفتهاست. نیازی به آن نداریم.
انتقال سرویسهای IdentityServer4.EntityFramework به پروژهی DNT.IDP.Services
بستهی دریافتی، شامل دو پوشهی src\IdentityServer4.EntityFramework\Services و src\IdentityServer4.EntityFramework\Stores است که سرویسهای آنرا تشکیل میدهند (جمعا 5 سرویس TokenCleanup، CorsPolicyService، ClientStore، PersistedGrantStore و ResourceStore). بنابراین این سرویسها را نیز مستقیما از این پوشهها به پروژهی DNT.IDP.Services کپی خواهیم کرد.
همانطور که عنوان شد دو فایل Interfaces\IConfigurationDbContext.cs و Interfaces\IPersistedGrantDbContext.cs برای دسترسی به دو DbContext این بستهی دریافتی در سرویسهای آن، تعریف شدهاند و چون ما در اینجا صرفا یک ApplicationDbContext را داریم که از طریق IUnitOfWork، در دسترس لایهی سرویس قرار میگیرد، ارجاعات به دو اینترفیس یاد شده را با IUnitOfWork تعویض خواهیم کرد تا مجددا قابل استفاده شوند.
انتقال متدهای الحاقی معرفی سرویسهای IdentityServer4.EntityFramework به پروژهی DNT.IDP
پس از انتقال قسمتهای مختلف IdentityServer4.EntityFramework به لایههای مختلف برنامهی جاری، اکنون نیاز است سرویسهای آنرا به برنامه معرفی کرد که در نهایت جایگزین متدهای فعلی درون حافظهای کلاس آغازین برنامهی IDP میشوند. خود این بسته در فایل زیر، به همراه متدهایی الحاقی است که این معرفی را انجام میدهند:
src\IdentityServer4.EntityFramework\Extensions\IdentityServerEntityFrameworkBuilderExtensions.cs
به همین جهت این فایل را به پروژهی وب DNT.IDP ، منتقل خواهیم کرد؛ همانجایی که در قسمت دهم AddCustomUserStore را تعریف کردیم.
این کلاس پس از انتقال، نیاز به تغییرات ذیل را دارد:
الف) چون یکسری از کلاسهای تنظیمات را حذف کردیم و نیازی به آنها نداریم، آنها را نیز به طور کامل از این فایل حذف میکنیم. تنها تنظیم مورد نیاز آن، OperationalStoreOptions است که اینبار آنرا از فایل appsettings.json دریافت میکنیم. بنابراین ذکر این مورد نیز در اینجا اضافی است.
ب) در آن، دو Context موجود در بستهی اصلی IdentityServer4.EntityFramework مورد استفاده قرار گرفتهاند. ما در اینجا آنها را نیز با تک Context برنامهی خود تعویض میکنیم.
ج) در آن سرویسی به نام TokenCleanupHost تعریف شدهاست. آنرا نیز به لایهی سرویسها منتقل میکنیم. همچنین در امضای سازندهی آن بجای تزریق مستقیم OperationalStoreOptions از <IOptions<OperationalStoreOptions استفاده خواهیم کرد.
نگارش نهایی و تمیز شدهی IdentityServerEntityFrameworkBuilderExtensions را در اینجا مشاهده میکنید.
افزودن متدهای الحاقی جدید به فایل آغازین برنامهی IDP
پس از انتقال IdentityServerEntityFrameworkBuilderExtensions به پروژهی DNT.IDP، اکنون نوبت به استفادهی از آن است:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddSigningCredential(loadCertificateFromStore()) .AddCustomUserStore() .AddConfigurationStore() .AddOperationalStore();
اجرای Migrations در پروژهی DNT.IDP.DataLayer
پس از پایان این نقل و انتقالات، اکنون نیاز است ساختار بانک اطلاعاتی برنامه را بر اساس موجودیتها و روابط جدید بین آنها، به روز رسانی کنیم. به همین جهت فایل add_migrations.cmd موجود در پوشهی src\IDP\DNT.IDP.DataLayer را اجرا میکنیم تا کلاسهای Migrations متناظر تولید شوند و سپس فایل update_db.cmd را اجرا میکنیم تا این تغییرات، به بانک اطلاعاتی برنامه نیز اعمال گردند.
انتقال اطلاعات فایل درون حافظهای Config، به بانک اطلاعاتی برنامه
تا اینجا اگر برنامه را اجرا کنیم، دیگر کار نمیکند. چون جداول کلاینتها و منابع آن خالی هستند. به همین جهت نیاز است اطلاعات فایل درون حافظهای Config را به بانک اطلاعاتی منتقل کنیم. برای این منظور سرویس ConfigSeedDataService را به برنامه اضافه کردهایم:
public interface IConfigSeedDataService { void EnsureSeedDataForContext( IEnumerable<IdentityServer4.Models.Client> clients, IEnumerable<IdentityServer4.Models.ApiResource> apiResources, IEnumerable<IdentityServer4.Models.IdentityResource> identityResources); }
برای استفادهی از آن، به کلاس آغازین برنامهی IDP مراجعه میکنیم:
namespace DNT.IDP { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... initializeDb(app); seedDb(app); // ... } private static void seedDb(IApplicationBuilder app) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var configSeedDataService = scope.ServiceProvider.GetService<IConfigSeedDataService>(); configSeedDataService.EnsureSeedDataForContext( Config.GetClients(), Config.GetApiResources(), Config.GetIdentityResources() ); } }
آزمایش برنامه
پس از اعمال این تغییرات، برنامهها را اجرا کنید. سپس به بانک اطلاعاتی آن مراجعه نمائید. در اینجا لیست جداول جدیدی را که اضافه شدهاند ملاحظه میکنید:
همچنین برای نمونه، در اینجا اطلاعات منابع منتقل شدهی به بانک اطلاعاتی، از فایل Config، قابل مشاهده هستند:
و یا قسمت ذخیره سازی خودکار توکنهای تولیدی توسط آن نیز به درستی کار میکند:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.