آقای نصیری بسیار جالب و آموزنده بود،ممنون
ساخت یک دیتابیس ترکیبی از SQL و فایل های XML
اگر مشکل در ذخیره و نمایش داده ها به صورت آنی ندارید، پشنهاد میکنم یک message broker در سر راه این معماری قرار بدید تا مشکل که بیان کردید در مورد (تایم 10 دقیقه ) درج داده و مشکل دیتابیس رو به حداقل برسونید.
ایجاد برنامههای ابتدایی مورد نیاز
در ابتدا دو پوشهی جدید BlazorServerApp و WinFormsApp را ایجاد میکنیم. سپس از طریق خط فرمان در اولی دستور dotnet new blazorserver و در دومی دستور dotnet new winforms را اجرا میکنیم تا دو برنامهی خالی Blazor Server و همچنین Windows Forms، ایجاد شوند. برنامهی WinForms ایجاد شده مبتنی بر NET Core. و یا همان NET 6x. است؛ بجای اینکه مبتنی بر دات نت فریمورک 4x باشد.
ایجاد یک پروژهی کتابخانهی Razor
چون میخواهیم کدهای برنامهی BlazorServerApp ما در برنامهی WinForms قابل استفاده باشد، نیاز است فایلهای اصلی آنرا به یک پروژهی razor class library منتقل کنیم. به همین جهت برای این پروژه، یک پوشهی جدید را به نام BlazorClassLibrary ایجاد کرده و درون آن دستور dotnet new razorclasslib را اجرا میکنیم.
انتقال فایلهای پروژهی Blazor به پروژهی کتابخانهی Razor
در ادامه این فایلها را از پروژهی BlazorServerApp به پروژهی BlazorClassLibrary منتقل میکنیم:
- کل پوشهی Data
- کل پوشهی Pages
- کل پوشهی Shared
- فایل App.razor
- فایل Imports.razor_
- کل پوشهی wwwroot
پس از اینکار، نیاز است فایل csproj کتابخانهی class lib را اندکی ویرایش کرد تا بتواند فایلهای اضافه شده را کامپایل کند:
<Project Sdk="Microsoft.NET.Sdk.Razor"> <PropertyGroup> <AddRazorSupportForMvc>true</AddRazorSupportForMvc> </PropertyGroup> <ItemGroup> <FrameworkReference Include="Microsoft.AspNetCore.App" /> </ItemGroup> </Project>
- به علاوه فایل Error.cshtml.cs انتقالی، نیاز به افزودن فضای نام using Microsoft.Extensions.Logging را خواهد داشت.
- در فایل Imports.razor_ انتقالی نیاز است دو using آخر آنرا که به BlazorServerApp قبلی اشاره میکنند، به BlazorClassLibrary جدید ویرایش کنیم:
@using BlazorClassLibrary @using BlazorClassLibrary.Shared
@namespace BlazorClassLibrary.Pages
<link rel="stylesheet" href="_content/BlazorClassLibrary/css/bootstrap/bootstrap.min.css" /> <link href="_content/BlazorClassLibrary/css/site.css" rel="stylesheet" />
پس از این تغییرات، برای اینکه برنامهی BlazorServerApp موجود، به کار خود ادامه دهد، نیاز است ارجاعی از پروژهی class lib را به فایل csproj آن اضافه کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> </Project>
ویرایش برنامهی WinForms جهت اجرای کدهای Blazor
تا اینجا برنامهی Blazor Server ما تمام فایلهای مورد نیاز خود را از BlazorClassLibrary دریافت میکند و بدون مشکل اجرا میشود. در ادامه میخواهیم کار هاست این class lib را در برنامهی WinForms نیز انجام دهیم. به همین جهت در ابتدا ارجاعی را به class lib به آن اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> </Project>
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.101-preview.11.2349" /> </ItemGroup> </Project>
در ادامه نیاز است فایل Form1.Designer.cs را به صورت دستی جهت افزودن این WebView اضافه شده، تغییر داد:
namespace WinFormsApp; partial class Form1 { private void InitializeComponent() { this.blazorWebView1 = new Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView(); this.SuspendLayout(); this.blazorWebView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.blazorWebView1.Location = new System.Drawing.Point(13, 181); this.blazorWebView1.Name = "blazorWebView1"; this.blazorWebView1.Size = new System.Drawing.Size(775, 257); this.blazorWebView1.TabIndex = 20; this.Controls.Add(this.blazorWebView1); this.components = new System.ComponentModel.Container(); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(800, 450); this.Text = "Form1"; this.ResumeLayout(false); } private Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView blazorWebView1; }
هاست برنامهی Blazor در برنامهی WinForm
پس از تغییرات فوق، نیاز است فایلهای wwwroot را از پروژهی class lib به پروژهی WinForms کپی کرد. از این جهت که این فایلها از طریق index.html جدیدی خوانده خواهند شد. پس از کپی کردن این پوشه، نیاز است فایل csproj پروژهی WinForm را به صورت زیر اصلاح کرد:
<Project Sdk="Microsoft.NET.Sdk.Razor"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.101-preview.11.2349" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> <ItemGroup> <Content Update="wwwroot\**"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> </ItemGroup> </Project>
در ادامه داخل این پوشهی wwwroot که از پروژهی class lib کپی کردیم، نیاز است فایل index.html جدیدی را که قرار است blazor.webview.js را اجرا کند، به صورت زیر ایجاد کنیم:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>Blazor WinForms app</title> <base href="/" /> <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> <link href="css/site.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" /> <link href="WinFormsApp.styles.css" rel="stylesheet" /> </head> <body> <div id="app"></div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="">Reload</a> <a>🗙</a> </div> <script src="_framework/blazor.webview.js"></script> </body> </html>
- همچنین در این فایل باید مداخل css.های مورد نیاز را هم مجددا ذکر کرد.
مرحلهی آخر کار، استفاده از کامپوننت webview جهت نمایش فایل index.html فوق است:
using System; using System.Windows.Forms; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebView.WindowsForms; using Microsoft.Extensions.DependencyInjection; using BlazorServerApp.Data; using BlazorClassLibrary; namespace WinFormsApp; public partial class Form1 : Form { private readonly AppState _appState = new(); public Form1() { var serviceCollection = new ServiceCollection(); serviceCollection.AddBlazorWebView(); serviceCollection.AddSingleton<AppState>(_appState); serviceCollection.AddSingleton<WeatherForecastService>(); InitializeComponent(); blazorWebView1.HostPage = @"wwwroot\index.html"; blazorWebView1.Services = serviceCollection.BuildServiceProvider(); blazorWebView1.RootComponents.Add<App>("#app"); //blazorWebView1.Dock = DockStyle.Fill; } }
نکتهی مهم! حتما نیاز است WebView2 Runtime را جداگانه دریافت و نصب کرد. در غیر اینصورت در حین اجرای برنامه، با خطای نامفهوم زیر مواجه خواهید شد:
System.IO.FileNotFoundException: The system cannot find the file specified. (0x80070002)
در اینجا یک ServiceCollection را ایجاد کرده و توسط آن سرویسهای مورد نیاز کامپوننت WebView را تامین میکنیم. همچنین مسیر فایل index.html نیز توسط آن مشخص شدهاست. این تنظیمات شبیه به فایل Program.cs برنامهی Blazor هستند.
تا اینجا اگر برنامه را اجرا کنیم، چنین خروجی قابل مشاهدهاست:
اکنون برنامهی کامل Blazor Server ما توسط یک WinForms هاست شدهاست و کاربر برای کار با آن، نیاز به نصب IIS یا هیچ وب سرور خاصی ندارد.
تعامل بین برنامهی WinForm و برنامهی Blazor
میخواهیم یک دکمه را بر روی WinForm قرار داده و با کلیک بر روی آن، مقدار شمارشگر حاصل در برنامهی Blazor را نمایش دهیم؛ مانند تصویر فوق.
برای اینکار در کدهای فوق، ثبت سرویس جدید AppState را هم مشاهده میکنید:
serviceCollection.AddSingleton<AppState>(_appState);
namespace BlazorServerApp.Data; public class AppState { public int Counter { get; set; } }
builder.Services.AddSingleton<AppState>();
@inject BlazorServerApp.Data.AppState AppState // ... @code { private void IncrementCount() { // ... AppState.Counter++; } }
private void button1_Click(object sender, EventArgs e) { MessageBox.Show( owner: this, text: $"Current counter value is: {_appState.Counter}", caption: "Counter"); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorDesktopHybrid.zip
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت سوم - بررسی مفاهیم OpenID Connect
پروتکل OpenID Connect چگونه کار میکند؟
در انتهای قسمت اول این سری، پروتکل OpenID Connect معرفی شد. در ادامه جزئیات بیشتری از این پروتکل را بررسی میکنیم.
هر برنامهی کلاینت متصل به WebAPI مثال قسمت قبل، نیاز به دانستن هویت کاربر وارد شدهی به آنرا دارد. در اینجا به این برنامهی کلاینت، اصطلاحا relying party هم گفته میشود؛ از این جهت که این برنامهی کلاینت، به برنامهی Identity provider و یا به اختصار IDP، جهت دریافت نتیجهی اعتبارسنجی کاربر، وابستهاست. برنامهی کلاینت یک درخواست Authentication را به سمت IDP ارسال میکند. به این ترتیب کاربر به صورت موقت از برنامهی جاری خارج شده و به برنامهی IDP منتقل میشود. در برنامهی IDP است که کاربر مشخص میکند کیست؛ برای مثال با ارائهی نام کاربری و کلمهی عبور. پس از این مرحله، در صورت تائید هویت کاربر، برنامهی IDP یک Identity Token را تولید و امضاء میکند. سپس برنامهی IDP کاربر را مجددا به برنامهی کلاینت اصلی هدایت میکند و Identity Token را در اختیار آن کلاینت قرار میدهد. در اینجا برنامهی کلاینت، این توکن هویت را دریافت و اعتبارسنجی میکند. اگر این اعتبارسنجی با موفقیت انجام شود، اکنون کاربر تعیین اعتبار شده و هویت او جهت استفادهی از قسمتهای مختلف برنامه مشخص میشود. در برنامههای ASP.NET Core، این توکن هویت، پردازش و بر اساس آن یکسری Claims تولید میشوند که در اغلب موارد به صورت یک کوکی رمزنگاری شده در سمت کلاینت ذخیره میشوند.
به این ترتیب مرورگر با هر درخواستی از سمت کاربر، این کوکی را به صورت خودکار به سمت برنامهی کلاینت ارسال میکند و از طریق آن، هویت کاربر بدون نیاز به مراجعهی مجدد به IDP، استخراج و استفاده میشود.
مراحل انتقال کاربر به IDP، صدور توکن هویت، بازگشت مجدد به برنامهی کلاینت، اعتبارسنجی، استخراج Claims و ذخیرهی آن به صورت یک کوکی رمزنگاری شده را در تصویر فوق ملاحظه میکنید. بنابراین در حین کار با یک IDP، مرحلهی لاگین به سیستم، دیگر در برنامه یا برنامههای کلاینت قرار ندارد. در اینجا دو فلش به سمت IDP و سپس به سمت کلاینت را بخاطر بسپارید. در ادامه از آنها برای توضیح Flow و Endpoints استفاده خواهیم کرد.
البته OpenID Connect برای کار همزمان با انواع و اقسام برنامههای کلاینت طراحی شدهاست؛ مانند برنامهی سمت سرور MVC، برنامههای سمت کلاینت جاوا اسکریپتی مانند Angular و برنامههای موبایل. برای این منظور باید در IDP نوع کلاینت و یکسری از تنظیمات مرتبط با آنرا مشخص کرد.
کلاینتهای عمومی و محرمانه
زمانیکه قرار است با یک IDP کار کنیم، این IDP باید بتواند بین یک برنامهی حسابداری و یک برنامهی پرسنلی که از آن برای احراز هویت استفاده میکنند، تفاوت قائل شود و آنها را شناسایی کند.
- کلاینت محرمانه (Confidential Client)
هر کلاینت با یک client-id و یک client-secret شناخته میشود. کلاینتی که بتواند محرمانگی این اطلاعات را حفظ کند، کلاینت محرمانه نامیده میشود.
در اینجا هر کاربر، اطلاعات هویت خود را در IDP وارد میکند. اما اطلاعات تعیین هویت کلاینتها در سمت کلاینتها ذخیره میشوند. برای مثال برنامههای وب ASP.NET Core میتوانند هویت کلاینت خود را به نحو امنی در سمت سرور ذخیره کنند و این اطلاعات، قابل دسترسی توسط کاربران آن برنامه نیستند.
- کلاینت عمومی (Public Client)
این نوع کلاینتها نمیتوانند محرمانگی هویت خود را حفظ و تضمین کنند؛ مانند برنامههای جاوا اسکریپتی Angular و یا برنامههای موبایل که بر روی وسایل الکترونیکی کاربران اجرا میشوند. در این حالت هرچقدر هم سعی کنیم، چون کاربران به اصل این برنامهها دسترسی دارند، نمیتوان محرمانگی اطلاعات ذخیره شدهی در آنها را تضمین کرد.
مفهوم OpenID Connect Endpoints
در تصویر ابتدای بحث، دو فلش را مشاهده میکنید؛ برای مثال چگونه میتوان به Identity token دسترسی یافت (Authentication) و همچنین زمانیکه صحبت از Authorization میشود، چگونه میتوان Access tokens را دریافت کرد. اینکه این مراحله چگونه کار میکنند، توسط Flow مشخص میشود. Flow مشخص میکند که چگونه باید توکنها از سمت IDP به سمت کلاینت بازگشت داده شوند. بسته به نوع کلاینتها که در مورد آنها بحث شد و نیازمندیهای برنامه، باید از Flow مناسبی استفاده کرد.
هر Flow با Endpoint متفاوتی ارتباط برقرار میکند. این Endpointها در حقیقت جایگزین راهحلهای خانگی تولید برنامههای IDP هستند.
- در ابتدا یک Authorization Endpoint وجود دارد که در سطح IDP عمل میکند. این مورد همان انتهای فلش اول ارسال درخواست به سمت IDP است؛ در تصویر ابتدای بحث. کار این Endpoint، بازگشت Identity token جهت انجام عملیات Authentication و بازگشت Access token برای تکمیل عملیات Authorization است. این عملیات نیز توسط Redirection کلاینت انجام میشود (هدایت کاربر به سمت برنامهی IDP، دریافت توکنها و سپس هدایت مجدد به سمت برنامهی کلاینت اصلی).
نکتهی مهم: استفادهی از TLS و یا همان پروتکل HTTPS برای کار با OpenID Connect Endpoints اجباری است و بدون آن نمیتوانید با این سیستم کار کنید. به عبارتی در اینجا ترافیک بین کلاینت و IDP، همواره باید رمزنگاری شده باشد.
البته مزیت کار با ASP.NET Core 2.1، یکپارچگی بهتر و پیشفرض آن با پروتکل HTTPS است؛ تا حدی که مثال پیشفرض local آن به همراه یک مجوز موقتی SSL نصب شدهی توسط SDK آن کار میکند.
- پس از Authorization Endpoint که در مورد آن توضیح داده شد، یک Redirection Endpoint وجود دارد. در ابتدای کار، کلاینت با یک Redirect به سمت IDP هدایت میشود و پس از احراز هویت، مجددا کاربر به سمت کلاینت Redirect خواهد شد. به آدرسی که IDP کاربر را به سمت کلاینت Redirect میکند، Redirection Endpoint میگویند و در سطح کلاینت تعریف میشود. برنامهی IDP، اطلاعات تولیدی خود را مانند انواع توکنها، به سمت این Endpoint که در سمت کلاینت قرار دارد، ارسال میکند.
- پس از آن یک Token Endpoint نیز وجود دارد که در سطح IDP تعریف میشود. این Endpoint، آدرسی است در سمت IDP، که برنامهی کلاینت میتواند با برنامه نویسی، توکنهایی را از آن درخواست کند. این درخواست عموما از نوع HTTP Post بدون Redirection است.
مفهوم OpenID Connect Flow
- اولین Flow موجود، Authorization Code Flow است. کار آن بازگشت کدهای Authorization از Authorization Endpoint و همچنین توکنها از طریق Token Endpoint میباشد. در ایجا منظور از «کدهای Authorization»، اطلاعات دسترسی با طول عمر کوتاه است. هدف Authorization Code این است که مشخص کند، کاربری که به IDP لاگین کردهاست، همانی است که Flow را از طریق برنامهی وب کلاینت، شروع کردهاست. انتخاب این نوع Flow، برای کلاینتهای محرمانه مناسب است. در این حالت میتوان مباحث Refresh token و داشتن توکنهایی با طول عمر بالا را نیز پیاده سازی کرد.
- Implicit Flow، تنها توکنهای تولیدی را توسط Authorization Endpoint بازگشت میدهد و در اینجا خبری از بازگشت «کدهای Authorization» نیست. بنابراین از Token Endpoint استفاده نمیکند. این نوع Flow، برای کلاینتهای عمومی مناسب است. در اینجا کار client authentication انجام نمیشود؛ از این جهت که کلاینتهای عمومی، مناسب ذخیره سازی client-secret نیستند. به همین جهت در اینجا امکان دسترسی به Refresh token و توکنهایی با طول عمر بالا میسر نیست. این نوع از Flow، ممکن است توسط کلاینتهای محرمانه نیز استفاده شود.
- Hybrid Flow، تعدادی از توکنها را توسط Authorization Endpoint و تعدادی دیگر را توسط Token Endpoint بازگشت میدهد؛ بنابراین ترکیبی از دو Flow قبلی است. انتخاب این نوع Flow، برای کلاینتهای محرمانه مناسب است. در این حالت میتوان مباحث Refresh token و داشتن توکنهایی با طول عمر بالا را نیز پیاده سازی کرد. از این نوع Flow ممکن است برای native mobile apps نیز استفاده شود.
آگاهی از انواع Flowها، انتخاب نوع صحیح آنها را میسر میکند که در نتیجه منتهی به مشکلات امنیتی نخواهند شد. برای مثال Hybrid Flow توسط پشتیبانی از Refresh token امکان تمدید توکن جاری و بالا بردن طول عمر آنرا دارد و این طول عمر بالا بهتر است به کلاینتهای اعتبارسنجی شده ارائه شود. برای اعتبارسنجی یک کلاینت، نیاز به client-secret داریم و برای مثال برنامههای جاوا اسکریپتی نمیتوانند محل مناسبی برای ذخیره سازی client-secret باشند؛ چون از نوع کلاینتهای عمومی محسوب میشوند. بنابراین نباید از Hybrid Flow برای برنامههای Angular استفاده کرد. هرچند انتخاب این مساله صرفا به شما بر میگردد و چیزی نمیتواند مانع آن شود. برای مثال میتوان Hybrid Flow را با برنامههای Angular هم بکار برد؛ هرچند ایدهی خوبی نیست.
انتخاب OpenID Connect Flow مناسب برای یک برنامهی کلاینت از نوع ASP.NET Core
برنامههای ASP.NET Core، از نوع کلاینتهای محرمانه بهشمار میروند. بنابراین در اینجا میتوان تمام Flowهای یاد شده را انتخاب کرد. در برنامههای سمت سرور وب، به ویژگی به روز رسانی توکن نیاز است. بنابراین باید دسترسی به Refresh token را نیز داشت که توسط Implicit Flow پشتیبانی نمیشود. به همین جهت از Implicit Flow در اینجا استفاده نمیکنیم. پیش از ارائهی OpenID Connect، تنها Flow مورد استفادهی در برنامههای سمت سرور وب، همان Authorization Code Flow بود. در این Flow تمام توکنها توسط Token Endpoint بازگشت داده میشوند. اما Hybrid Flow نسبت به آن این مزیتها را دارد:
- ابتدا اجازه میدهد تا Identity token را از IDP دریافت کنیم. سپس میتوان آنرا بدون دریافت توکن دسترسی، تعیین اعتبار کرد.
- در ادامه OpenID Connect این Identity token را به یک توکن دسترسی، متصل میکند.
به همین جهت OpenID Connect نسبت به OAuth 2 ارجحیت بیشتری پیدا میکند.
پس از آشنایی با این مقدمات، در قسمت بعدی، کار نصب و راه اندازی IdentityServer را انجام خواهیم داد.
استفاده از وب سوکت در Asp.Net Core
سازماندهی برنامههای Angular توسط ماژولها
- جاوا اسکریپت توانایی خواندن، نوشتن، کپی و اجرای فایلها را بر روی هارد دیسک ندارد و همچنین دسترسی مستقیمی به سیستم عامل ندارد.
- مرورگرهای مدرن اجازه کار با فایلها را به جاوا اسکریپت میدهند؛ بشرطی که آن فایل از طریق input درون صفحه وب و با اجازه کاربر انتخاب شود و فقط محدود به دسترسی به همان فایل است.
- وقتی یک تَب جدید را در مرورگر باز میکنید، جاوااسکریپتِ این دو تَب (دو صفحهی جداگانه) هیچ دسترسی و اطلاعاتی بهم ندارند.
- جاوااسکریپت به کوکیها دسترسی دارد (کوکی، فایلهای متنی کوچکی برای ذخیره اطلاعات کاربر از یک سایت است؛ مانند نام کاربری و کلمه عبور). از کوکیها برای ذخیرهی توکن نشستها استفاده میشود. لذا اگه هکر بتواند کوکی نشست یک کاربر را بدست بیاورد، میتواند از هویت او استفاده کند و خود را بجای کاربر مورد نظر قرار دهد.
- جاوااسکریپت امکان تغییرات در DOM مرورگر را دارد (داخل صفحهای که جاوااسکریپت در آن اجرا شده است).
- جاوا اسکریپت با استفاده از شیء XMLHttpRequest میتواند درخواستهای HTTP را با محتوای دلخواه، به مقصد مورد نظر ارسال نماید.
- در مرورگرهای مدرن امروزی امکان دسترسی به دوربین، میکروفن، GPS وفایلهای خاصی از طریق APIهای HTML5 امکانپذیر هست.
انواع روشهای XSS
- روش بازتابی Reflected XSS Attack
- روش ذخیره شده Stored XSS Attack
- روش مبتنی بر DOM-based XSS
روش بازتابی Reflected XSS Attack
برپایی ساختار ابتدایی پروژهی تمرین
ابتدا یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app sample-05 > cd sample-05 > npm start
> npm install --save bootstrap > npm install --save font-awesome
در ادامه نیاز است فایلهای CSS این کتابخانهها و قلمهای وب را import کنیم. به همین جهت ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css"; import "font-awesome/css/font-awesome.css";
همچنین به فایل index.css هم مراجعه کرده و یک padding را به بالای صفحه اضافه میکنیم؛ تا اطلاعات نمایش داده شده، با کمی فاصله از لبهی مرورگر رندر شوند:
body { margin: 0; padding: 20px 0 0 0; font-family: sans-serif; }
پس از نصب و import این کتابخانههای ثالث، به فایل App.js مراجعه کرده و کلاس container اصلی بوت استرپ را در آن تعریف میکنیم تا در برگیرندهی محتوای برنامه شود:
return ( <main className="container"> <h1>Hello world!</h1> </main> );
معرفی سرویسهای دادهی برنامه
کدهای نهایی این قسمت را از فایل پیوست شدهی در انتهای مطلب، میتوانید دریافت کنید. در اینجا یک پوشهی src\services تعریف شدهاست که داخل آن دو فایل fakeGenreService.js و fakeMovieService.js قرار دارند. این فایلها، منبع دادهی درون حافظهای مثال تمرین ما هستند.
سرویس fakeGenre چنین ساختاری را دارد و ژانرهای سینمایی، مانند اکشن، کمدی و غیره در آن لیست شدهاند:
export const genres = [ { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, // ... ];
و سرویس fakeMovie که دارای ساختار کلی زیر است، لیست 9 فیلم سینمایی را به همراه دارد:
const movies = [ { _id: "5b21ca3eeb7f6fbccd471815", title: "Terminator", genre: { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, numberInStock: 6, dailyRentalRate: 2.5, publishDate: "2018-01-03T19:04:28.809Z" }, //... ];
ایجاد کامپوننت Movies برای نمایش لیست فیلمها در برنامه
اکنون میخواهیم یک کامپوننت جدید را به نام Movies در فایل جدید src\components\movies.jsx ایجاد کنیم، تا لیست فیلمهای سرویس fakeMovieService را نمایش دهد. برای اینکار مراحل زیر را طی خواهیم کرد:
- نمایش سادهی لیست فیلمها توسط یک جدول. برای دریافت لیست اشیاء موجود در fakeMovieService، از متد ()getMovies آن میتوان استفاده کرد.
- اضافه کردن یک دکمهی حذف، به هر ردیف، به نحوی که با کلیک بر روی آن، آن ردیف حذف شود.
- نمایش یک پیام بالای جدول که تعداد فیلمهای موجود در سرویس درون حافظهای را نمایش میدهد. همچنین پس از حذف تمام ردیفها، باید پیام «فیلمی موجود نیست» را نمایش دهد.
پس از ایجاد فایل خالی جدید movies.jsx در پوشهی جدید components، با استفاده از «simple react snippets» نصب شدهی در VSCode، یکبار imrc را تایپ کرده (مخفف import react component است) و سپس دکمهی tab را فشار میدهیم، در آخر اینکار را برای cc نیز تکرار میکنیم (مخفف create class است) تا importها و سپس ساختار ابتدایی کامپوننت React ما تشکیل شوند. نام این کامپوننت را هم Movies که با حرف بزرگ شروع میشود، وارد میکنیم.
اکنون مجددا به App.js مراجعه میکنیم و بجای Hello world ای که نمایش دادیم، کامپوننت Movies را اضافه میکنیم. برای این منظور ابتدا import آنرا به ابتدای فایل اضافه میکنیم:
import Movies from "./components/movies";
return ( <main className="container"> <Movies /> </main> );
دریافت لیست اشیاء فیلمها از سرویس fakeMovieService
برای دریافت لیست اشیاء فیلمها، ابتدا تعریف سرویس آنرا به ابتدای کامپوننت Movies اضافه میکنیم:
import { getMovies } from "../services/fakeMovieService";
سپس خاصیت state را جهت تعریف خاصیت movies که با متد ()getMovies سرویس fakeMovieService مقدار دهی میشود، به نحو زیر تکمیل میکنیم:
state = { movies: getMovies() };
نمایش لیست فیلمها، به همراه مدیریت حذف هر ردیف
در ادامه، کدهای کامل و تکمیل شدهی این کامپوننت را ملاحظه میکنید:
import React, { Component } from "react"; import { getMovies } from "../services/fakeMovieService"; class Movies extends Component { state = { movies: getMovies() }; handleDelete = movie => { const movies = this.state.movies.filter(m => m._id !== movie._id); this.setState({ movies }); }; render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; return ( <React.Fragment> <p>Showing {count} movies in the database.</p> <table className="table"> <thead> <tr> <th>Title</th> <th>Genre</th> <th>Stock</th> <th>Rate</th> <th /> </tr> </thead> <tbody> {this.state.movies.map(movie => ( <tr key={movie._id}> <td>{movie.title}</td> <td>{movie.genre.name}</td> <td>{movie.numberInStock}</td> <td>{movie.dailyRentalRate}</td> <td> <button onClick={() => this.handleDelete(movie)} className="btn btn-danger btn-sm" > Delete </button> </td> </tr> ))} </tbody> </table> </React.Fragment> ); } } export default Movies;
همانطور که در ابتدای بحث نیز ذکر شد، هدف از این تمرین، مرور قسمتهای قبل است و تمام نکات زیر را در قسمتهای پیشین، با جزئیات بیشتری بررسی کردهایم:
- ابتدا خاصیت state و سپس خاصیت movies شیء منتسب به آن، با لیست فیلمهای موجود در سرویس مرتبط، مقدار دهی شدهاند.
- سپس در ابتدای متد render، کار رندر شرطی انجام شدهاست. اگر تعداد فیلمهای دریافتی صفر بود، پیام «فیلمی در بانک اطلاعاتی موجود نیست» نمایش داده میشود و در غیراینصورت، جدول اصلی بوت استرپی برنامه رندر خواهد شد.
در اینجا چون از خاصیت طول آرایهی فیلمها در چندین قسمت قرار است استفاده شود، آنرا توسط Object Destructuring به یک متغیر نسبت دادهایم. همچنین توسط یک نام مستعار هم خاصیت length را با نام جدید count استفاده میکنیم.
- در ادامه بازگشت React.Fragment را مشاهده میکنید. علت اینجا است که نمیخواهیم div اضافهتری را در UI رندر کنیم. React.Fragment سبب میشود تا بتوانیم چندین فرزند را به المان جاری تبدیل شدهی به کدهای جاوا اسکریپتی اضافه کنیم، بدون اینکه خودش به المانی ترجمه شود.
- پس از return، یک () قابل مشاهدهاست. چون خروجی return ما چند سطری است، اگر در سطری که return قرار میگیرد، اطلاعاتی درج نشود، موتور جاوا اسکریپت آنرا با یک سمیکالن خاتمه خواهد داد! و دیگر سطرهای بعدی دیده نمیشوند و پردازش نخواهند شد. به همین جهت از روش ذکر یک () پس از return در فایلهای jsx زیاد استفاده میشود.
- در ابتدای return، همان خاصیت count را نمایش میدهیم.
- سپس کار رندر جدول اصلی برنامه که با کلاسهای جداول بوت استرپ نیز مزین شده، انجام شدهاست. در React برای عدم تداخل ویژگی class با نام از پیش رزرو شدهی class، از خاصیت className برای ذکر کلاسهای CSSای استفاده میشود.
- قسمت thead این جدول مشخص است و سرستونهای جدول را مشخص میکند.
- پس از آن نیاز است ردیفهای جدول را رندر کنیم. اینکار را توسط متد Array.map، با نگاشت هر آیتم آرایهی this.state.movies، به یک tr جدول انجام دادهایم.
- React برای اینکه بتواند DOM مجازی خودش را کنترل کند، نیاز دارد عناصر موجود در آنرا به صورت منحصربفردی تشخص دهد. به همین جهت در اینجا ذکر key را بر روی المان tr که با movie._id مقدار دهی شدهاست، مشاهده میکنید.
- رندر مقادیر سلولهای ردیفها توسط درج {} و سپس ذکر مقداری از شیء movie دریافتی توسط متد Array.map انجام میشود.
- در اینجا ستون رندر دکمهی Delete را نیز مشاهده میکنید. برای مدیریت this در آن و دسترسی به شیء movie جاری (ارسال پارامتر به رویداد گردان آن) و همچنین دسترسی به شیء this کلاس جاری برای کار با آرایهی this.state.movies، از روش arrow functions برای تعریف رویدادگردان onClick استفاده کردهایم.
- در متد handleDelete، یک آرایهی جدید را که id ردیفهای آن با id شیء ردیف انتخابی یکی نیست، بازگشت میدهیم. انتساب این آرایهی جدید به آرایهی this.state.movies، تغییری را در برنامههای React ایجاد نمیکند. در اینجا باید توسط متد this.setState که از کلاس پایهی extends Component دریافت میشود، خاصیت movies را بازنویسی کرد تا React از تغییرات مطلع شده و DOM مجازی جدیدی را با مقایسهی با نمونه جدید، محاسبه کرده و به DOM اصلی، جهت به روز رسانی UI اعمال کند.
- البته در اینجا this.setState({ movies }) را بجای this.setState({ movies: movies }) مشاهده میکنید. علت اینجا است که اگر عبارات key و value یکی باشند، میتوان تنها همان عبارت key را جهت حذف تکرار واژهها، ذکر کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-05.zip
Service Layer
نقش لایهی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایهی Facade نیز میشناسند. این لایه، دادهها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایهی Presentation فراهم میکند. کلاس View Model یک Strongly Typed محسوب میشود که نماهای خاصی از دادهها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه مینماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.
الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interfaceها و زیر سیستمهای پیچیده ارائه میکند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.
کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductViewModel { Public int ProductId {get; set;} public string Name { get; set; } public string Rrp { get; set; } public string SellingPrice { get; set; } public string Discount { get; set; } public string Savings { get; set; } }
برای اینکه کلاینت با لایهی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه میشود و پارامترهای مورد نیاز را فراهم میکند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Service { public class ProductListRequest { public CustomerType CustomerType { get; set; } } }
در شی Response نیز بررسی میکنیم که درخواست به درستی انجام شده باشد، دادههای مورد نیاز را برای کلاینت فراهم میکنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال مینماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductListResponse { public bool Success { get; set; } public string Message { get; set; } public IList<ProductViewModel> Products { get; set; } }
به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما میتوانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمیباشند. بنابراین بهترین انتخاب، استفاده از Extension Methodها میباشد که باید برای کلاس Product و در لایهی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public static class ProductMapperExtensionMethods { public static ProductViewModel ConvertToProductViewModel(this Model.Product product) { ProductViewModel productViewModel = new ProductViewModel(); productViewModel.ProductId = product.Id; productViewModel.Name = product.Name; productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP); productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice); if (product.Price.Discount > 0) productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount); if (product.Price.Savings < 1 && product.Price.Savings > 0) productViewModel.Savings = product.Price.Savings.ToString(“#%”); return productViewModel; } public static IList<ProductViewModel> ConvertToProductListViewModel( this IList<Model.Product> products) { IList<ProductViewModel> productViewModels = new List<ProductViewModel>(); foreach(Model.Product p in products) { productViewModels.Add(p.ConvertToProductViewModel()); } return productViewModels; } }
حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد مینماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductService { private Model.ProductService _productService; public ProductService(Model.ProductService ProductService) { _productService = ProductService; } public ProductListResponse GetAllProductsFor( ProductListRequest productListRequest) { ProductListResponse productListResponse = new ProductListResponse(); try { IList<Model.Product> productEntities = _productService.GetAllProductsFor(productListRequest.CustomerType); productListResponse.Products = productEntities.ConvertToProductListViewModel(); productListResponse.Success = true; } catch (Exception ex) { // Log the exception… productListResponse.Success = false; // Return a friendly error message productListResponse.Message = ex.Message; } return productListResponse; } }
کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال میکند. همچنین این لایه محل مناسبی برای Log کردن خطاها میباشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer میپردازیم.
Data Layer
برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:
برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده میکنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه میکنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.
از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.
اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Repository { public class ProductRepository : IProductRepository { public IList<Model.Product> FindAll() { var products = from p in new ShopDataContext().Products select new Model.Product { Id = p.ProductId, Name = p.ProductName, Price = new Model.Price(p.Rrp, p.SellingPrice) }; return products.ToList(); } } }
در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایهی Data هم به پایان رسید و در ادامه به کدنویسی لایهی Presentation و UI میپردازیم.
Presentation Layer
به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده میکنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; public interface IProductListView { void Display(IList<ProductViewModel> Products); Model.CustomerType CustomerType { get; } string ErrorMessage { set; } }
این Interface توسط Web Formهای ASP.NET و یا Win Formها باید پیاده سازی شوند. کار با Interfaceها موجب میشود تا تست Viewها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; namespace SoCPatterns.Layered.Presentation { public class ProductListPresenter { private IProductListView _productListView; private Service.ProductService _productService; public ProductListPresenter(IProductListView ProductListView, Service.ProductService ProductService) { _productService = ProductService; _productListView = ProductListView; } public void Display() { ProductListRequest productListRequest = new ProductListRequest(); productListRequest.CustomerType = _productListView.CustomerType; ProductListResponse productResponse = _productService.GetAllProductsFor(productListRequest); if (productResponse.Success) { _productListView.Display(productResponse.Products); } else { _productListView.ErrorMessage = productResponse.Message; } } } }
کلاس Presenter وظیفهی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایهی Presentation به پایان رسیده است. از مزایای وجود لایهی Presentation این است که تست نویسی مربوط به نمایش دادهها و تعامل بین کاربر و سیستم به سهولت انجام میشود بدون آنکه نگران دشواری Unit Test نویسی Web Formها باشید. حال میتوانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت.