مطالب
بررسی فرمت کوکی‌های ASP.NET Identity
فرمت کوکی‌های ASP.NET Identity از پروژه‌ی سورس باز Katana دریافت شده‌است و تولید آن پس از لاگین کاربر، شامل مراحل زیر می‌باشد:
1- با استفاده از کلاس ApplicationUser، شیء ClaimsPrincipal را تولید می‌کند.
2- به این ClaimsPrincipal اطلاعاتی مانند ApplicationUser.Id و SecurityStamp اضافه می‌شوند.
3- در ادامه، ClaimsPrincipal به OWIN و کلاس CookieAuthenticationHandler آن ارسال می‌شود.
4- کار کلاس CookieAuthenticationHandler، تولید و تنظیم اطلاعاتی مانند تاریخ صدور کوکی، تاریخ انقضای آن، نوع کوکی، مانند ماندگار بودن یا امن بودن (HTTPS) و امثال آن است. حاصل این مراحله، تولید یک AuthenticationTicket است.
5- در آخر، AuthenticationTicket و  ClaimsPrincipal به کلاس SecureDataFormat، برای ابتدا، serialize شدن اشیاء، رمزنگاری و در نهایت تبدیل آن‌ها به فرمت base64، ارسال می‌شوند.

جزئیات تکمیلی مرحله‌ی آخر آن نیز به این ترتیب است:
AuthenticationTicket با استفاده از کلاس TicketSerializer سریالایز می‌شود. پس از آن یک memory stream تشکیل شده و اطلاعات ClaimsIdentity و AuthenticationTicket سریالایز شده به آن ارسال می‌شوند. این memory stream با استفاده از الگوریتم GZip فشرده شده و برای پردازش بیشتر بازگشت داده می‌شود. مرحله‌ی بعد، رمزنگاری اطلاعات فشرده سازی شده‌است. برای این منظور از کلاس DpapiDataProtector دات نت استفاده می‌کنند. پس از رمزنگاری، استریم نهایی با فرمت base64 برای درج در HTTP Response آماده خواهد شد.

سؤال: چرا کوکی‌‌های یک کاربر معین لاگین شده‌ی توسط ASP.NET Identity، در مرورگرهای مختلف متفاوت است؟
هرچند اطلاعاتی مانند ApplicationUser.Id و SecurityStamp برای یک کاربر، در مرورگرهای مختلف یکسان هستند، اما در مرحله‌ی چهارم، ذکر شد که AuthenticationTicket دارای اطلاعات بیشتری مانند زمان تولید کوکی نیز هست. بنابراین اطلاعات نهایی رمزنگاری شده‌ی در این حالت که در زمان‌های مختلفی تولید شده‌اند، یکسان نخواهند بود.

سؤال: در ساب دومین‌های مختلف دومین مشخصی، چندین برنامه‌ی مختلف نصب شده‌اند. چگونه می‌توان از یک سیستم لاگین ASP.NET Identity برای تمام آن‌ها استفاده کرد؟
برای این منظور نیاز هست خاصیت CookieDomain را به صورت صریح مقدار دهی کرد. برای اینکار فایل Startup.Auth.cs را گشوده و  CookieAuthenticationOptions را تنظیم کنید:
var cookieAuthenticationOptions = new CookieAuthenticationOptions
{
    AuthenticationType  = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath           = new PathString("/Account/Login"),
    CookieDomain        = ".mydomain.com"
};
البته کار به همینجا ختم نمی‌شود. پس از آن نیاز است به ازای تمام دومین‌های موجود، یک machine key مشخص تنظیم شود. از این جهت که در مرحله‌ی پنجم تولید کوکی، کلاس DpapiDataProtector دات نت، از machine key موجود، برای رمزنگاری اطلاعات استفاده می‌کند و اگر این machine key، به ازای برنامه‌های مختلف متفاوت باشد، کوکی تولید شده، قابل رمزگشایی و استفاده نخواهد بود.
برای اینکار به کنسول IIS مراجعه کرده و گزینه‌ی machine key آن‌را بیابید. در این قسمت بر روی generate keys کلیک کرده و اطلاعات تولیدی را باید به تمام web.config‌های موجود کپی کنید:
 <machineKey
  validationKey="DAD9E2B0F9..."
  decryptionKey="ADD1C39C02..."
  validation="SHA1"
  decryption="AES"
/>

سؤال: برنامه‌های مختلفی بر روی یک دومین نصب هستند، اما قصد نداریم از سیستم اعتبارسنجی یکپارچه‌ای برای تمام آن‌ها استفاده کنیم. اما اگر در یکی لاگین کنیم، بلافاصله لاگین در برنامه‌ی دوم منقضی می‌شود، چرا؟
شبیه به همین مساله با Forms Authentication هم وجود دارد. برای رفع آن باید نام کوکی‌های هر برنامه را منحصربفرد کنید و از نام پیش فرض کوکی‌ها استفاده نکنید تا بر روی یکدیگر بازنویسی نشوند. برای اینکار خاصیت CookieName شیء CookieAuthenticationOptions را جداگانه مقدار دهی کنید:
 CookieName = "my-very-own-cookie-name"

سؤال: لاگین انجام شده‌ی در برنامه‌ای که از ASP.NET Identity استفاده می‌کند، زود منقضی می‌شود؛ چرا؟
برای تنظیم صریح زمان انقضای کوکی ASP.NET Identity نیاز است خاصیت ExpireTimeSpan آن‌را مقدار دهی کنید:
 app.UseCookieAuthentication(new CookieAuthenticationOptions
{
     ExpireTimeSpan = TimeSpan.FromHours(24.0),
});

سؤال: کاربر سیستم ASP.NET Identity از سیستم خارج شده‌است (log off کرده) ولی هنوز می‌توان از کوکی پیشین او برای اعتبارسنجی مجدد استفاده کرد. چطور می‌توان این نقیصه‌ی امنیتی را برطرف کرد؟
مشکل از اینجا است:
   public ActionResult LogOff()
  {
      AuthenticationManager.SignOut();
      return RedirectToAction("Index", "Home");
  }
در مثال رسمی ASP.NET Identity یک چنین کدی برای خروج از سیستم ارائه شده‌است. نمونه‌ی امن‌تر آن به صورت زیر است:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> LogOff()
{
    var user = await UserManager.FindByNameAsync(User.Identity.Name);
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    await UserManager.UpdateSecurityStampAsync(user.Id);
    return RedirectToAction("Login", "Account");
}
در اینجا علاوه بر عدم استفاده‌ی از متد بدون پارامتر SignOut (با توجه به خاصیت AuthenticationType ذکر شده‌ی در CookieAuthenticationOptions)، کار به روز رسانی مجدد SecurityStamp کوکی نیز انجام شده‌است. با این تغییر، کوکی موجود بلا استفاده خواهد شد؛ چون دیگر قابل رمزگشایی نیست.
همچنین بهتر است مقدار validateInterval مربوط به SecurityStampValidator.OnValidateIdentity که به صورت پیش فرض 30 دقیقه است را به مقدار کمتری مانند 5 دقیقه تغییر دهید (تنظیمات OnValidateIdentity مربوط به CookieAuthenticationOptions فایل آغارین برنامه). کار این تنظیم، بررسی اعتبار کوکی، در بازه‌های زمانی مشخص شده‌است.
مطالب
اجرای SSIS Package از طریق برنامه کاربردی

مقدمه

در اکثر موارد در یک Landscape عملیاتی، چنانچه به تجمیع و انتقال داده‌ها از بانک‌های اطلاعاتی مختلف نیاز باشد، از SSIS Package اختصار (SQL Server Integration Service) استفاده می‌شود و معمولاً با تعریف یک Job در سطح SQL Server به اجرای Package در زمانهای مشخص می‌پردازند. چنانچه در موقعیتی لازم باشد که از طریق برنامه کاربردی توسعه یافته، به اجرای Package مبادرت ورزیده شود و البته نخواهیم Job تعریف شده را از طریق کد برنامه، اجرا کنیم و در واقع این امکان را داشته باشیم که همانند یک رویه ذخیره شده تعریف شده در سطح بانک اطلاعاتی به اجرای عمل فوق بپردازیم، یک راه حل می‌تواند تعریف یک CLR Stored Procedures باشد. در این مقاله به بررسی این موضوع پرداخته می‌شود، در ابتدا لازم است به بیان تئوری موضوع پرداخته شود (قسمت‌های 1 الی 5) در ادامه به ذکر پیاده سازی روش پیشنهادی پرداخته می‌شود.

1- اجرای Integration Service Package 

جهت اجرای یک Package از ابزارهای زیر می‌توان استفاده کرد:
• command-line ابزار خط فرمان dtexec.exe
• ابزار اجرائی پکیج dtexecui.exe
• استفاده از SQL Server Agent job
توجه: همچنین یک Package را در زمان طراحی در  Business Intelligence Development Studio) BIDS)  می‌توان اجرا نمود.

2- استفاده از dtexec جهت اجرای Package

با استفاده از ابزار dtexec می‌توان Package‌های ذخیره شده در فایل سیستم، یک SQL Instance و یا Package‌های ذخیره شده در Integration Service را اجرا نمود.

توجه:
در سیستم عامل‌های 64 بیتی، ابزار dtexec موجود در Integration Service با نسخه 64 بیتی نصب می‌شود. چنانچه بایست Package‌های معینی را در حالت 32 بیتی اجرا کنید، لازم است ابزار dtexec نسخه 32 بیتی نصب شود. ابزار dtexec دستیابی به تمامی ویژگی‌های پیکربندی و اجرای Package از قبیل اتصالات، مشخصات(Properties)، متغیرها، logging و شاخص‌های پردازشی را فراهم می‌کند.
توجه: زمانی که از نسخه‌ی ابزار dtexec که با SQL Server 2008 ارائه شده استفاده می‌کنید برای اجرای یک SSIS Package نسخه 2005، Integration Service به صورت موقت Package را به نسخه 2008 ارتقا می‌دهد، اما نمی‌توان از ابزار dtexec برای ذخیره این تغییرات استفاده کرد.

2-1- ملاحظات نصب dtexec روی سیستم‌های 64 بیتی

به صورت پیش فرض، یک سیستم عامل 64 بیتی که هر دو نسخه 64 بیتی و 32 بیتی ابزار خط فرمان Integration Service را دارد، نسخه 32 بیتی نصب شده را در خط فرمان اجرا خواهد کرد. نسخه 32 بیتی بدین دلیل اجرا می‌شود که در متغیر محیطی (Path (Path environment variable مسیر directory نسخه 32 بیتی قرار گرفته است.به طور معمول:
(<drive>:\Program Files(x86)\Microsoft SQL Server\100\DTS\Binn)
توجه: اگر از SQL Server Agent برای اجرای Package استفاده می‌کنید، SQL Server Agent به طور خودکار از ابزار نسخه 64 بیتی استفاده می‌کند. SQL Server Agent از Registry و نه از متغیر محیطی Path استفاده می‌کند. برای اطمینان از اینکه نسخه 64 بیتی این ابزار را در خط فرمان اجرا می‌کنید، directory را به directory ای تغییر دهید که شامل نسخه 64 بیتی این ابزار است(<drive>:\Program Files\Microsoft SQL Server\100\DTS\Binn) و ابزار را از این مسیر اجرا کنید و یا برای همیشه مسیر قرار گرفته در متغیر محیطی path را با مسیری که نسخه 64 بیتی قرار دارد، جایگزین کنید.

2-2- تفسیر کدهای خروجی

هنگامی که یک Package اجرا می‌شود، dtexec یک کد خروجی (Return Code) بر می‌گرداند:

 مقدار توصیف
 0  Package با موفقیت اجرا شده است.
 1  Package با خطا مواجه شده است.
 3 Package در حال اجرا توسط کاربر لغو شده است.
 4  Package پیدا نشده است.
 5  Package بارگذاری نشده است.
 6  ابزار با یک خطای نحوی یا خطای معنایی در خط فرمان برخورد کرده است.

2-3- قوانین نحوی dtexec

تمامی گزینه‌ها (Options) باید با یک علامت Slash (/)  و یا Minus (-)  شروع شوند.
یک آرگومان باید در یک quotation mark محصور شود چنانچه شامل یک فاصله خالی باشد.
گزینه‌ها و آرگومان‌ها بجز رمزعبور حساس به حروف کوچک و بزرگ نیستند.

2-3-1- Syntax

 dtexec /option [value] [/option [value]]…


2-3-2- Parameters

نکته: در Integration Service، ابزار خط فرمان dtsrun که برایData Transformation Service) DTS)‌های نسخه SQL Server 2000 استفاده می‌شد، با ابزار خط فرمان dtexec جایگزین شده است.
• تعدادی از گزینه‌های خط فرمان dtsrun به طور مستقیم در dtexec معادل دارند برای مثال نام Server و نام Package.
• تعدادی از گزینه‌های dtsrun به طور مستقیم در dtexec معادل ندارند.
• تعدادی گزینه‌های خط فرمان جدید dtsexec وجود دارد که در ویژگی‌های جدید Integration Service پشتیبانی می‌شود.

2-3-3- مثال

1) به منظور اجرای یک SSIS Package که در SQL Server ذخیره شده است، با استفاده از Windows Authentication :
 dtexec /sq <Package Name> /ser <Server Name>

2) به منظور اجرای یک SSIS Package که در پوشه File System در SSIS Package Store ذخیره شده است :
 dtexec /dts “\File System\<Package File Name>”

3) به منظور اجرای یک SSIS Package که در سیستم فایل ذخیره شده است و مشخص کردن گزینه logging:
 dtexec /f “c:\<Package File Name>” /l “DTS.LogProviderTextFile; <Log File Name>”

4) به منظور اجرای یک SSIS Package که در SQL Server ذخیره شده با استفاده از SQL Server Authentication برای نمونه(user:ssis;pwd:ssis@ssis)و رمز (Package(123:
 dtexec  /server “<Server Name>”  /sql “<Package Name>”  / user “ssis” /Password “ssis@ssis” /De “123”


3- تنظیمات سطح حفاظتی یک Package

به منظور حفاظت از داده‌ها در Package‌های Integration Service می‌توانید یک سطح حفاظتی (protection level) را تنظیم کنید که به حفاظت از داده‌های صرفاً حساس یا تمامی داده‌های یک Package کمک نماید. به  علاوه می‌توانید این داده‌ها را با یک Password یا یک User Key رمزگذاری نمائید یا به رمزگذاری داده‌ها در بانک اطلاعاتی اعتماد کنید. همچنین سطح حفاظتی که برای یک Package استفاده می‌کنید، الزاماً ایستا (static) نیست و در طول چرخه حیات یک Package می‌تواند تغییر کند. اغلب سطح حفاظتی در طول توسعه یا به محض (deploy) استقرار Package تنظیم می‌شود.
توجه: علاوه بر سطوح حفاظتی که توصیف شد، Package‌ها در بانک اطلاعاتی msdb ذخیره می‌شوند که همچنین می‌توانند توسط نقش‌های ثابت در سطح بانک اطلاعاتی (fixed database-level roles) حفاظت شوند. Integration Service شامل 3 نقش ثابت بانک اطلاعاتی برای نسبت دادن مجوزها به Package است که عبارتند از db_ssisadmin  ،db_ssisltduser و db_ssisoperator

3-1- درک سطوح حفاظتی

در یک Package اطلاعات زیر به عنوان حساس تعریف می‌شوند:
• بخش password در یک connection string. گرچه، اگر گزینه ای را که همه چیز را رمزگذاری کند، انتخاب کنید تمامی connection string حساس در نظر گرفته می‌شود.
• گره‌های task-generated XML که برچسب (tagged) هایی حساس هستند.
• هر متغییری که به عنوان حساس نشان گذاری شود.

3-1-1- Do not save sensitive

هنگامی که Package ذخیره می‌شود از ذخیره مقادیر ویژگی‌های حساس در Package جلوگیری می‌کند. این سطح حفاظتی رمزگذاری نمی‌کند اما در عوض از ذخیره شدن ویژگی هایی که حساس نشان گذاری شده اند به همراه Package جلوگیری می‌کند.

3-1-2- Encrypt all with password

به منظور رمزگذاری تمامی Package از یک Password استفاده می‌شود. Package توسط Password ای رمزگذاری می‌شود  که کاربر هنگامی که Package را ایجاد یا Export می‌کند، ارائه می‌دهد. به منظور باز کردن Package در SSIS Designer یا اجرای Package توسط ابزار خط فرمان dtexec کاربر بایست رمز Package را ارائه نماید. بدون رمز کاربر قادر به دستیابی و اجرای Package نیست.

3-1-3- Encrypt all with user key

به منظور رمزگذاری تمامی Package از یک کلید که مبتنی بر Profile کاربر جاری می‌باشد، استفاده می‌شود. تنها کاربری که Package را ایجاد یا Export می‌کند، می‌تواند Package را در SSIS Designer باز کند و یا Package را توسط ابزار خط فرمان dtexec اجرا کند.

3-1-4- Encrypt sensitive with password

به منظور رمزگذاری تنها مقادیر ویژگی‌های حساس در Package از یک Password استفاده می‌شود. برای رمزگذاری از DPAPI استفاده می‌شود. داده‌های حساس به عنوان بخشی از Package ذخیره می‌شوند اما آن داده‌ها با استفاده از Password رمزگذاری می‌شوند. به منظور باز نمودن Package در SSIS Designer کاربر باید رمز Package را ارائه دهد. اگر رمز ارائه نشود، Package بدون داده‌های حساس باز می‌شود و کاربر باید مقادیر جدیدی برای داده‌های حساس فراهم کند. اگر کاربر سعی نماید Package را بدون ارائه رمز اجرا کند، اجرای Package با خطا مواجه می‌شود.

3-1-5- Encrypt sensitive with user key

به منظور رمزگذاری تنها مقادیر ویژگی‌های حساس در Package از یک کلید که مبتنی بر Profile کاربر جاری می‌باشد، استفاده می‌شود. تنها کاربری که از همان Profile استفاده می‌کند، Package را می‌تواند بارگذاری (load) کند. اگر کاربر متفاوتی Package را باز نماید، اطلاعات حساس با مقادیر پوچی جایگزین می‌شود و کاربر باید مقادیر جدیدی برای داده‌های حساس فراهم کند. اگر کاربر سعی نماید Package را بدون ارائه رمز اجرا کند، اجرای Package با خطا مواجه می‌شود. برای رمزگذاری از DPAPI استفاده می‌شود.

3-1-6- (Rely on server storage for encryption (ServerStorage

با استفاده از نقش‌های بانک اطلاعاتی، SQL Server تمامی Package را حفاظت می‌کند. این گزینه تنها زمانی پشتیبانی می‌شود که Package در بانک اطلاعاتی msdb ذخیره شده است.

4- استفاده از نقش‌های Integration Service

برای کنترل کردن دستیابی به Package، SSIS شامل 3 نقش ثابت در سطح بانک اطلاعاتی است. نقش‌ها می‌توانند تنها روی Package هایی که در بانک اطلاعاتی msdb ذخیره شده اند، بکار روند. با استفاده از SSMS می‌توانید نقش‌ها را به Package‌ها نسبت دهید، این انتساب نقش‌ها در بانک اطلاعاتی msdb ذخیره می‌شود.

 Write action  Read action Role
 Import packages
Delete own packages
Delete all packages
Change own package roles
Change all package roles

* به نکته رجوع شود

 Enumerate own packages
Enumerate all packages
View own packages
View all packages
Execute own packages
Execute all packages
Export own packages
Export all packages
Execute all packages in SQL Server Agent
 db_ssisadmin
or
sysadmin

Import packages
Delete own packages
Change own package roles

Enumerate own packages
Enumerate all packages
View own packages
Execute own packages
Export own packages
db_ssisltduser
None
Enumerate all packages
View all packages
Execute all packages
Export all packages
Execute all packages in SQL Server Agent
db_ssisoperator
Stop all currently running packages
View execution details of all running packages
Windows administrators
* نکته: اعضای نقش‌های db_ssisadmin و dc_admin ممکن است قادر باشند مجوزهای خودشان را تا سطح sysadmin ارتقا دهند براساس این ترفیع مجوز امکان اصلاح و اجرای Package‌ها از طریق SQL Server Agent میسر می‌شود. برای محافظت در برابر این ارتقا، با استفاده از یک (account) حساب Proxy  با دسترسی محدود، Job هایی که این Package‌ها را اجرا می‌کنند، پیکربندی شوند یا  تنها اعضای نقش sysadmin به نقش‌های db_ssisadmin و dc_admin افزوده شوند.

همچنین جدول sysssispackages در بانک اطلاعاتی msdb شامل Package هایی است که در SQL Server ذخیره می‌شوند. این جدول شامل ستون هایی که اطلاعاتی درباره نقش هایی که به Package‌ها نسبت داده شده است، می‌باشد.
به صورت پیش فرض، مجوزهای نقش‌های ثابت بانک اطلاعاتی db_ssisadmin و db_ssisoperator و شناسه منحصر به فرد کاربری (unique security identifier) که Package را ایجاد کرده برای خواندن Package بکار می‌رود، و مجوزهای نقش db_ssisadmin و شناسه منحصر به فرد کاربری که Package را ایجاد کرده برای نوشتن Package به کار می‌رود. یک User باید عضو نقش db_ssisadmin و db_ssisltduser یا db_ssisoperator برای داشتن دسترسی خواندن Package باشد. یک User باید عضو نقش db_ssisadmin برای داشتن دسترسی نوشتن Package باشد.

5- اتصال به صورت Remote به Integration Service

زمانی که یک کاربر بدون داشتن دسترسی کافی تلاش کند به یک Integration Service به صورت Remote متصل شود، با پیغام خطای "Access is denied" مواجه می‌شود. برای اجتناب از این پیغام خطا می‌توان تضمین کرد که کاربر مجوز مورد نیاز DCOM را دارد.
به منظور پیکربندی کردن دسترسی کاربر به صورت Remote به سرویس Integration  مراحل زیر را دنبال کنید:
- Component Service را باز نمایید ( در Run عبارت dcomcnfg را تایپ کنید).
- گره Component Service را باز کنید، گره Computer و سپس My Computer را باز نمایید و روی DCOM Config کلیک نمایید.
- گره DCOM Config را باز کنید و از لیست برنامه هایی که می‌توانند پیکربندی شوند MsDtsServer را انتخاب کنید.
- روی Properties برنامه MsDtsServer رفته و قسمت Security را انتخاب کنید.
- در قسمت Lunch and Activation Permissions، مورد Customize را انتخاب و سپس روی Edit کلیک نمایید تا پنجره Lunch Permission باز شود.
- در پنجره Lunch Permission، کاربران را اضافه و یا حذف کنید و مجوزهای مناسب را به کاربران یا گروه‌های مناسب نسبت دهید. مجوزهای موجود عبارتند از Local Lunch، Remote Lunch، Local Activation و Remote Activation .
- در قسمت Access Permission مراحل فوق را به منظور نسبت دادن مجوزهای مناسب به کاربران یا گروه‌های مناسب انجام دهید.
- سرویس Integration  را Restart کنید.
مجوز دسترسی  Lunch به منظور شروع و خاتمه سرویس، اعطا  یا رد  می‌شود و مجوز دسترسیActivation به منظور متصل شدن به سرویس، اعطا (grant) یا رد (deny) می‌شود.

6- پیاده سازی

در ابتدا به ایجاد یک CLR Stored Procedures پرداخته می‌شود نام اسمبلی ساخته شده به این نام RunningPackage.dll می‌باشد و حاوی کد زیر است:
Partial Public Class StoredProcedures
    '------------------------------------------------
    'exec dbo.Spc_NtDtexec 'Package','ssis','ssis@ssis','1234512345'
    '------------------------------------------------
    <Microsoft.SqlServer.Server.SqlProcedure()> _
    Public Shared Sub Spc_NtDtexec(ByVal PackageName As String, _
                                   ByVal UserName As String, _
                                   ByVal Password As String, _
                                   ByVal Decrypt As String)
        Dim p As New System.Diagnostics.Process()
        p.StartInfo.FileName = "C:\Program Files\Microsoft SQL Server\100\DTS\Binn\DTExec.exe"
        p.StartInfo.RedirectStandardOutput = True
        p.StartInfo.Arguments = "/sql " & PackageName & " /User " & UserName & " /Password " & Password & " /De " & Decrypt
        p.StartInfo.UseShellExecute = False
        p.Start()
        p.WaitForExit()
        Dim output As String
        output = p.StandardOutput.ReadToEnd()
        Microsoft.SqlServer.Server.SqlContext.Pipe.Send(output)
    End Sub
End Class
در حقیقت توسط این رویه به اجرای برنامه dtexec.exe و ارسال پارامترهای مورد نیاز جهت اجرا پرداخته می‌شود. با توجه به توضیحات تئوری بیان شده، سطح حفاظتی Package ایجاد شده Encrypt all with password توصیه می‌شود که رمز مذکور در قالب یکی از پارامتر ارسالی به رویه ساخته شده موسوم به Spc_NtDtexec ارسال می‌گردد.

در قدم بعدی نیاز به Register کردن dll ساخته شده در سطح بانک اطلاعاتی SQL Server است، این گام‌ها پس از اتصال به SQL Server Management Studio به شرح زیر است:
1- فعال کردن CLR در سرویس SQL Server
SP_CONFIGURE 'clr enabled',1
GO
RECONFIGURE

2- فعال کردن ویژگی TRUSTWORTHY در بانک اطلاعاتی مورد نظر
 ALTER DATABASE <Database Name> SET TRUSTWORTHY ON
GO
RECONFIGURE

3- ایجاد Assembly و Stored Procedure در بانک اطلاعاتی مورد نظر
Assembly ساخته شده با نام RunningPacakge.dll در ریشه :C کپی شود. بعد از ثبت نمودن این Assembly لزومی به وجود آن نمی‌باشد.
USE <Database Name>
GO
CREATE ASSEMBLY [RunningPackage]
AUTHORIZATION [dbo]
FROM 'C:\RunningPackage.dll'
WITH PERMISSION_SET = UNSAFE
Go
CREATE PROCEDURE [dbo].[Spc_NtDtexec]
@PackageName [nvarchar](50),
@UserName [nvarchar](50),
@Password [nvarchar](50),
@Decrypt [nvarchar](50)
WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [RunningPackage].[RunningPackage.StoredProcedures].[Spc_NtDtexec]
GO
توجه: Application User برنامه بایست دسترسی اجرای رویه ذخیره شده Spc_NtDtexec را در بانک اطلاعاتی مورد نظر داشته باشد همچنین بایست عضو نقش db_ssisoperator در بانک اطلاعاتی msdb باشد.( منظور از Application User، لاگین است که در Connection string برنامه قرار داده اید.)

در برنامه کاربردی تان کافی است متدی به شکل زیر ایجاد و با توجه به نیازتان در برنامه به فراخوانی آن و اجرای Package بپردازید.
    Private Sub ExecutePackage()
        Dim oSqlConnection As SqlClient.SqlConnection
        Dim oSqlCommand As SqlClient.SqlCommand
        Dim strCnt As String = String.Empty
        strCnt = "Data Source=" & txtServer.Text & ";User ID=" & txtUsername.Text & ";Password=" & txtPassword.Text & ";Initial Catalog=" & cmbDatabaseName.SelectedValue.ToString() & ";"
        Try
            oSqlConnection = New SqlClient.SqlConnection(strCnt)
            oSqlCommand = New SqlClient.SqlCommand
            With oSqlCommand
                .Connection = oSqlConnection
                .CommandType = System.Data.CommandType.StoredProcedure
                .CommandText = "dbo.Spc_NtDtexec"
                .Parameters.Clear()
                .Parameters.Add("@PackageName", System.Data.SqlDbType.VarChar, 50)
                .Parameters.Add("@UserName", System.Data.SqlDbType.VarChar, 50)
                .Parameters.Add("@Password", System.Data.SqlDbType.VarChar, 50)
                .Parameters.Add("@Decrypt", System.Data.SqlDbType.VarChar, 50)
                .Parameters("@PackageName").Value = txtPackageName.Text.Trim()
                .Parameters("@UserName").Value = txtUsername.Text.Trim()
                .Parameters("@Password").Value = txtPassword.Text.Trim()
                .Parameters("@Decrypt").Value = txtDecrypt.Text.Trim()
            End With
            If (oSqlCommand.Connection.State <> System.Data.ConnectionState.Open) Then
                oSqlCommand.Connection.Open()
                oSqlCommand.ExecuteNonQuery()
                System.Windows.Forms.MessageBox.Show("Success")
            End If
            If (oSqlCommand.Connection.State = System.Data.ConnectionState.Open) Then
                oSqlCommand.Connection.Close()
            End If
        Catch ex As Exception
            MessageBox.Show(ex.Message, "Error")
        End Try
    End Sub 'ExecutePackage
مطالب
ذخیره سازی تنظیمات برنامه‌های ASP.NET Core در بانک اطلاعاتی به کمک Entity Framework Core
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config » با مقدمات کار با فایل‌های تنظیمات برنامه و تامین کننده‌های مختلف آن‌ها آشنا شدیم. در این مطلب قصد داریم یک نمونه‌ی سفارشی تامین کننده‌های تنظیمات برنامه را بر اساس دریافت و ذخیره سازی اطلاعات در بانک اطلاعاتی، تهیه کنیم.


ساختار موجودیت تنظیمات برنامه

تنظیمات برنامه با هر قالبی که تهیه شوند، دست آخر به صورت یک <Dictionary<string,string در برنامه پردازش شده و قابل دسترسی می‌شوند. بنابراین موجودیت معادل این Dictionary را به صورت زیر تعریف می‌کنیم:
namespace DbConfig.Web.DomainClasses
{
    public class ConfigurationValue
    {
        public int Id { get; set; }
        public string Key { get; set; }
        public string Value { get; set; }
    }
}


ساختار Context برنامه و مقدار دهی اولیه‌ی آن

پس از تعریف موجودیت تنظیمات برنامه، آن‌را به صورت زیر به Context برنامه معرفی می‌کنیم:
    public class MyAppContext : DbContext, IUnitOfWork
    {
        public MyAppContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<ConfigurationValue> Configurations { set; get; }
همچنین، برای مقدار دهی مقادیر اولیه‌ی تنظیمات برنامه نیز اینبار می‌توان به کمک متد HasData، به صورت زیر عمل کرد:
        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<ConfigurationValue>(entity =>
            {
                entity.Property(e => e.Key).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Key).IsUnique();
                entity.Property(e => e.Value).IsRequired();
                entity.HasData(new ConfigurationValue
                {
                    Id = 1,
                    Key = "key-1",
                    Value = "value_from_ef_1"
                });
                entity.HasData(new ConfigurationValue
                {
                    Id = 2,
                    Key = "key-2",
                    Value = "value_from_ef_2"
                });
            });
        }

ایجاد یک IConfigurationSource سفارشی مبتنی بر بانک اطلاعاتی

انواع و اقسام تامین کننده‌های تنظیمات برنامه در پروژه‌های ASP.NET Core، در حقیقت یک پیاده سازی سفارشی از اینترفیس IConfigurationSource هستند. به همین جهت در ادامه یک نمونه‌ی مبتنی بر EF Core آن را تهیه می‌کنیم:
    public class EFConfigurationSource : IConfigurationSource
    {
        private readonly IServiceProvider _serviceProvider;

        public EFConfigurationSource(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new EFConfigurationProvider(_serviceProvider);
        }
    }
در اینجا چون می‌خواهیم به IUnitOfWork دسترسی پیدا کنیم، IServiceProvider را به سازنده‌ی این تامین کننده تزریق کرده‌ایم. کار اصلی ساخت آن نیز در متد Build، با ارائه‌ی یک IConfigurationProvider سفارشی انجام می‌شود. اینجا است که اطلاعات را از بانک اطلاعاتی خوانده و در اختیار سیستم تنظیمات برنامه قرار می‌دهیم:
    public class EFConfigurationProvider : ConfigurationProvider
    {
        private readonly IServiceProvider _serviceProvider;

        public EFConfigurationProvider(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
            ensureDatabaseIsCreated();
        }

        public override void Load()
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
                this.Data?.Clear();
                this.Data = uow.Set<ConfigurationValue>()
                               .AsNoTracking()
                               .ToList()
                               .ToDictionary(c => c.Key, c => c.Value);
            }
        }

        private void ensureDatabaseIsCreated()
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
                uow.Migrate();
            }
        }
    }
در ConfigurationProvider فوق، متد Load، در آغاز برنامه فراخوانی شده و در اینجا فرصت داریم تا خاصیت this.Data آن‌را که از نوع <Dictionary<string,string است، مقدار دهی کنیم. بنابراین از serviceProvider تزریق شده‌ی در سازنده‌ی کلاس استفاده کرده و به وهله‌ای از IUnitOfWork دسترسی پیدا می‌کنیم. سپس بر این اساس تمام رکوردهای جدول متناظر با ConfigurationValue را دریافت و توسط متد ToDictionary، تبدیل به ساختار مدنظر خاصیت this.Data می‌کنیم.
در اینجا فراخوانی متد ensureDatabaseIsCreated را نیز مشاهده می‌کنید. کلاس EFConfigurationProvider در آغاز برنامه و پیش از هر عمل دیگری وهله سازی شده و سپس متد Load آن فراخوانی می‌شود. به همین جهت نیاز است یا پیشتر، بانک اطلاعاتی را توسط دستورات Migration ایجاد کرده باشید و یا متد ensureDatabaseIsCreated، اطلاعات Migration موجود را به بانک اطلاعاتی برنامه اعمال می‌کند.


معرفی EFConfigurationSource به برنامه

جهت معرفی ساده‌تر EFConfigurationSource تهیه شده، ابتدا یک متد الحاقی را بر اساس آن تهیه می‌کنیم:
    public static class EFExtensions
    {
        public static IConfigurationBuilder AddEFConfig(this IConfigurationBuilder builder,
            IServiceProvider serviceProvider)
        {
            return builder.Add(new EFConfigurationSource(serviceProvider));
        }
    }
سپس می‌توان این متد AddEFConfig را به صورت زیر به تنظیمات برنامه در کلاس Startup اضافه و معرفی کرد:
namespace DbConfig.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, MyAppContext>();
            services.AddScoped<IConfigurationValuesService, ConfigurationValuesService>();

            var connectionString = Configuration.GetConnectionString("SqlServerConnection")
                     .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data"));
            services.AddDbContext<MyAppContext>(options =>
                    {
                        options.UseSqlServer(
                            connectionString,
                            dbOptions =>
                                {
                                    var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                                    dbOptions.CommandTimeout(minutes);
                                    dbOptions.EnableRetryOnFailure();
                                });
                    });

            var serviceProvider = services.BuildServiceProvider();
            var configuration = new ConfigurationBuilder()
                                       .AddConfiguration(Configuration) // Adds all of the existing configurations
                                       .AddEFConfig(serviceProvider)
                                       .Build();
            services.AddSingleton<IConfigurationRoot>(sp => configuration); // Replace
            services.AddSingleton<IConfiguration>(sp => configuration); // Replace
در اینجا ابتدا نیاز است یک ConfigurationBuilder جدید را ایجاد کنیم تا بتوان AddEFConfig را بر روی آن فراخوانی کرد. در این بین، خود برنامه نیز تعدادی تامین کننده‌ی تنظیمات پیش‌فرض را نیز دارد که قصد نداریم سبب پاک شدن آن‌ها شویم. به همین جهت آن‌ها را توسط متد AddConfiguration، افزوده‌ایم. پس از تعریف این ConfigurationBuilder جدید، نیاز است آن‌را جایگزین IConfiguration و IConfigurationRoot پیش‌فرض برنامه کنیم که روش آن‌را در دو متد services.AddSingleton ملاحظه می‌کنید.
همچنین روش دسترسی به serviceProvider مورد نیاز AddEFConfig، توسط متد services.BuildServiceProvider نیز در کدهای فوق مشخص است. به همین جهت مجبور شدیم این تعریف را در اینجا قرار دهیم و گرنه می‌شد از کلاس Program و یا حتی سازنده‌ی کلاس Startup نیز استفاده کرد. مشکل این دو مکان عدم دسترسی به سرویس IUnitOfWork و سایر تنظیمات برنامه است.


آزمایش برنامه

اگر به قسمت «ساختار Context برنامه و مقدار دهی اولیه‌ی آن» مطلب جاری دقت کرده باشید، دو کلید پیش‌فرض در اینجا ثبت شده‌اند. به همین جهت در ادامه با تزریق سرویس IConfiguration به سازنده‌ی یک کنترلر، سعی در خواندن مقادیر آن‌ها خواهیم کرد:
namespace DbConfig.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IConfiguration _configuration;

        public HomeController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public IActionResult Index()
        {
            return Json(
                new
                {
                    key1 = _configuration["key-1"],
                    key2 = _configuration["key-2"]
                });
        }
با این خروجی:



به روز رسانی بانک اطلاعاتی برنامه و بارگذاری مجدد اطلاعات IConfiguration

فرض کنید توسط سرویسی، اطلاعات جدول ConfigurationValue را تغییر داده‌اید. نکته‌ی مهم اینجا است که اینکار سبب فراخوانی مجدد متد Load کلاس EFConfigurationProvider نخواهد شد و عملا این تغییرات در سراسر برنامه توسط تزریق اینترفیس IConfiguration قابل دسترسی نخواهند بود (مگر اینکه برنامه مجددا ری‌استارت شود). نکته‌ی به روز رسانی این اطلاعات به صورت زیر است:
    public class ConfigurationValuesService : IConfigurationValuesService
    {
        private readonly IConfiguration _configuration;

        public ConfigurationValuesService(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        private void reloadEFConfigurationProvider()
        {
            ((IConfigurationRoot)_configuration).Reload();
        }
در جائیکه نیاز است پس از به روز رسانی بانک اطلاعاتی، تنظیمات برنامه را نیز بارگذاری مجدد کنید، ابتدا اینترفیس IConfiguration را به سازنده‌ی آن تزریق کرده و سپس به نحو فوق، متد Reload را فراخوانی کنید. اینکار سبب می‌شود تا یکبار دیگری متد Load کلاس EFConfigurationProvider نیز فراخوانی شود که باعث بارگذاری مجدد تنظیمات برنامه خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreDbConfig.zip
مطالب
مسیریابی در Angular - قسمت اول - معرفی
مسیریابی در +Angular 2 به همراه قابلیت‌های فراوان و ویژه‌ای است که تعدادی از آن‌ها را تابحال در این سایت بررسی کرده‌ایم. مورد مقدماتی اول، نیاز به بازنویسی کامل دارد، مورد دوم جهت آشنایی با ساختار Angular CLI مفید است و مورد سوم یکی از مباحث تکمیلی آن است. به همین جهت در طی یک سری، ویژگی‌های متعدد سیستم مسیریابی +Angular 2 را از ابتدا بررسی خواهیم کرد.


مسیریابی در +Angular 2

عموما از مسیریابی جهت حرکت بین Viewهای مختلف برنامه استفاده می‌شود، اما کارهای بیشتری را نیز می‌توان با آن انجام داد؛ مانند ارسال اطلاعات، به مسیریابی‌ها، پیش بارگذاری اطلاعات، جهت نمایش در Viewها، گروه بندی و محافظت از مسیریابی‌ها، پویانمایی و انیمیشن و همچنین بهبود کارآیی، با بارگذاری async مسیرهای مختلف.
کار سیستم مسیریاب +Angular 2 زمانی شروع می‌شود که تغییری را در آدرس درخواستی از برنامه مشاهده می‌کند؛ یا از طریق درخواست آدرسی توسط مرورگر و یا هدایت به قسمتی خاص، از طریق کدنویسی. سپس مسیریاب به آرایه‌ی تنظیم شده‌ی مسیرهای سیستم مراجعه می‌کند تا بتواند تطابقی را بین آدرس درخواستی و یکی از کلیدهای تنظیم شده‌ی در آن پیدا کند. در این حالت اگر تطابقی یافت نشود، کارمسیریابی خاتمه خواهد یافت. در غیراینصورت کار ادامه یافته و سپس مسیریاب، محافظ‌های مسیر درخواستی را بررسی می‌کند تا مشخص شود که آیا کاربر مجاز به هدایت به این قسمت خاص از برنامه هست یا خیر؟ در صورت مثبت بودن پاسخ، مرحله‌ی بعد، پیش بارگذاری اطلاعات درخواستی جهت نمایش View مرتبط است. در ادامه کامپوننت متناظر با مسیریابی فعالسازی می‌شود. سپس قالب این کامپوننت را در قسمتی که توسط router-outlet مشخص می‌گردد، جایگذاری کرده و نمایش می‌دهد.


تعریف مسیر پایه یا Base path

اولین مرحله‌ی کار با سیستم مسیریابی +Angular 2، تعریف یک base path است. مسیرپایه، به زیرپوشه‌ای اشاره می‌کند که برنامه‌ی ما در آن قرار گرفته‌است:
 www.mysite.com/myapp
در این مثال، مسیرپایه برنامه، /myapp/ است.
مسیریاب از این مسیرپایه جهت ساخت آدرس‌های مسیریابی استفاده می‌کند. مقدار آن نیز به صورت ذیل در فایل index.html برنامه، درست پس از تگ head تعیین می‌گردد:
<!DOCTYPE html>
<html>
<head>
    <base href="/">
در اینجا با ذکر / ، به عنوان مسیرپایه، به ریشه‌ی سایت اشاره شده‌است. اگر برنامه‌ی شما قرار است در یک زیرپوشه قرار بگیرد، در این حالت نحوه‌ی تعیین این زیر پوشه به صورت ذیل خواهد بود:
<base href="/myapp/">


تعیین مسیرپایه جهت ارائه‌ی نهایی

استفاده از مسیر پایه / برای حالت توسعه و همچنین زمانیکه برنامه‌ی نهایی شما در ریشه‌ی سایت توزیع می‌شود، بسیار مناسب است. اما اگر برای حالت توسعه از مقدار / و برای حالت توزیع از مقدار /myapp/ بخواهید استفاده کنید، مدام نیاز خواهید داشت تا فایل index.html نهایی سایت را ویرایش کنید. برای این منظور Angular CLI دارای پرچمی است به نام base-href:
 > ng build --base-href /myapp/
به این ترتیب در زمان ساخت و توزیع نهایی برنامه توسط Angular CLI، کار تنظیم خودکار مسیرپایه نیز صورت خواهد گرفت.
حالت پیش فرض تولید برنامه‌های Angular توسط Angular CLI، تنظیم مسیرپایه در فایل src\index.html به صورت خودکار به / می‌باشد.


تعریف مسیریاب Angular

مسیریاب Angular در ماژولی به نام RouterModule قرار گرفته‌است و باید در ابتدای کار import شود. این ماژول شامل سرویسی است جهت هدایت کاربران به صفحات دیگر و مدیریت URLها، تنظیماتی برای تعریف جزئیات مسیریابی‌ها و تعدادی دایرکتیو که برای فعالسازی و نمایش مسیرها از آن‌ها استفاده می‌شود. برای مثال دایرکتیو RouterLink آن یک المان قابل کلیک HTML را به مسیر و کامپوننتی خاص در برنامه متصل می‌کند. RouterLinkActive، شیوه‌نامه‌ها را به لینک فعال انتساب می‌دهد و RouterOutlet محل نمایش قالب کامپوننت فعال شده را مشخص می‌کند.

یک مثال: در ادامه، یک پروژه‌ی جدید مبتنی بر Angular CLI را به نام angular-routing-lab به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
 > ng new angular-routing-lab --routing
به علاوه قصد استفاده‌ی از بوت استرپ را نیز داریم. به همین جهت ابتدا به ریشه‌ی پروژه وارد شده و سپس دستور ذیل را صادر کنید تا بوت استرپ نیز نصب شود و پرچم save آن سبب به روز رسانی فایل package.json نیز گردد:
 > npm install bootstrap --save
پس از آن نیاز است به فایل angular-cli.json. مراجعه کرده و شیوه‌نامه‌ی بوت استرپ را تعریف کنیم:
  "apps": [
    {
      "styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
      ],
به این ترتیب به صورت خودکار این شیوه نامه به همراه توزیع برنامه حضور خواهد داشت و نیازی به تعریف مستقیم آن در فایل index.html نیست.

در ادامه اگر به فایل src\app\app-routing.module.ts مراجعه کنید، یک چنین محتوایی را خواهید یافت:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    children: []
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا برای شروع به کار با سیستم مسیریابی، RouterModule معرفی شده‌است. ابتدا import آن‌را مشاهده می‌کنید و سپس این ماژول به قسمت imports مربوط به NgModule اضافه شده‌است.
می‌توان این قسمت را خلاصه کرد و فایل app-routing.module.ts را نیز حذف کرد و سپس import لازم و تعریف ماژول آن‌را به ماژول آغازین برنامه یا همان src\app\app.module.ts نیز منتقل کرد. اما پس از مدتی تنظیمات مسیریابی آن، فایل ماژول اصلی برنامه را بیش از اندازه شلوغ خواهند کرد. بنابراین Angular-CLI تصمیم به ایجاد یک ماژول مستقل را برای تعریف تنظیمات مسیریابی برنامه گرفته‌است. سپس تعریف آن را به فایل src\app\app.module.ts به صورت خودکار اضافه می‌کند:
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  imports: [
    AppRoutingModule
  ],

اگر به قسمت import مربوط به NgModule فایل src\app\app-routing.module.ts دقت کنید، این ماژول به همراه متد forRoot معرفی شده‌است.
@NgModule({
  imports: [RouterModule.forRoot(routes)],
ماژول مسیریابی به همراه دو متد ذیل است:
الف) forRoot
- کار آن تعریف دایرکتیوهای مسیریابی، مدیریت تنظیمات مسیریابی و ثبت سرویس مسیریابی است.
- نکته‌ی مهم اینجا است که متد forRoot تنها یکبار باید در طول عمر یک برنامه تعریف شود.
- این متد آرایه‌ای از تنظیمات مسیریابی‌های تعریف شده را دریافت می‌کند.
ب) forChild
- کار آن تعریف دایرکتیوهای مسیریابی و مدیریت تنظیمات مسیریابی است؛ اما سرویس مسیریابی را مجددا ثبت نمی‌کند.
- از این متد در جهت تعریف مسیریابی‌های ماژول‌های ویژگی‌های مختلف برنامه و نظم بخشیدن به آن‌ها استفاده می‌شود.

بنابراین زمانیکه از forRoot استفاده می‌شود، سرویس مسیریابی تنها یکبار ثبت خواهد شد و تنها یک وهله از آن موجود خواهد بود. در ادامه هر کدام از ماژول‌های دیگر برنامه می‌توانند forChild خاص خودشان را داشته باشند.

اکنون تمام کامپوننت‌های قید شده‌ی در قسمت declaration، امکان دسترسی به دایرکتیوهای مسیریابی را پیدا می‌کنند. همچنین از آنجائیکه AppRoutingModule به همراه متد forRoot است، سرویس مسیریابی نیز در کل برنامه قابل دسترسی است.


تنظیمات اولیه مسیریابی برنامه

آرایه‌ی const routes: Routes فایل src\app\app-routing.module.ts در ابتدای کار خالی است. در اینجا کار تعریف URL segments و سپس اتصال آن‌ها به کامپوننت‌های متناظری جهت فعالسازی و نمایش قالب آن‌ها صورت می‌گیرد. این نمایش نیز در محل router-outlet تعریف شده‌ی در فایل src\app\app.component.html انجام می‌شود:
<h1>
  {{title}}
</h1>
<router-outlet></router-outlet>
به ازای هر router-outlet، تنها یک مسیر را در هر زمان می‌توان نمایش داد. بنابراین با فعال شدن هر کامپوننت، قالب آن، قالب کامپوننت پیشین را در router-outlet جایگزین خواهد کرد. امکان تعریف router-outletهای دیگری نیز وجود دارند که به آن‌ها secondary routes گفته می‌شود.

در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوش‌آمد گویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه می‌کنیم:
>ng g c welcome
>ng g c PageNotFound
که سبب ایجاد کامپوننت‌های src\app\welcome\welcome.component.ts و src\app\page-not-found\page-not-found.component.ts خواهند شد، به همراه به روز رسانی خودکار فایل src\app\app.module.ts جهت تکمیل قسمت declarations آن:
@NgModule({
  declarations: [
    AppComponent,
    WelcomeComponent,
    PageNotFoundComponent
  ],

سپس فایل src\app\app-routing.module.ts را به نحو ذیل تکمیل نمائید:
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { WelcomeComponent } from './welcome/welcome.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: 'welcome', component: WelcomeComponent },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا در ابتدا دو کامپوننت جدید تعریف شده، import شده‌اند.

یک نکته: افزونه‌ی auto import، کار تعریف کامپوننت‌ها را در VSCode بسیار ساده می‌کند و امکان تشکیل خودکار قسمت import را با ارائه‌ی یک intellisense به همراه دارد.

سپس کار تکمیل آرایه‌ی Routes انجام شده‌است. همانطور که مشاهده می‌کنید، این آرایه متشکل است از اشیایی که به همراه خاصیت path و سایر پارامترهای مورد نیاز هستند.
کار خاصیت path، تعیین URL segment متناظری است که این مسیریابی را فعال می‌کند. برای مثال اولین شیء تعریف شده با آدرس‌هایی مانند www.mysite.com/welcome متناظر است.
 { path: 'welcome', component: WelcomeComponent },
در اغلب حالات به همراه path، خاصیت کامپوننت متناظر با آن مسیر نیز ذکر می‌شود؛ مانندWelcomeComponent ایی که در اینجا مقدار دهی شده‌است. هنگامیکه این مسیریابی فعال می‌شود، قالب این کامپوننت‌است که در router-outlet نمایش داده خواهد شد (فایل src\app\welcome\welcome.component.html).

چند نکته:
- در حین تعریف مقدار خاصیت path، هیچ / آغاز کننده‌ای تعریف نشده‌است.
- مقدار خاصیت path، حساس به کوچکی و بزرگی حروف است.
- WelcomeComponent تعریف شده، یک رشته نیست و ارجاعی را به کامپوننت مرتبط دارد. به همین جهت نیاز به import statement ابتدایی را دارد و وجود آن توسط کامپایلر بررسی می‌شود.


تعیین مسیریابی پیش فرض سایت

اما زمانیکه برنامه برای بار اول بارگذاری می‌شود، چطور؟ در این حالت هیچ URL segment ایی وجود ندارد. بنابراین برای تنظیم مسیرپیش فرض سایت، خاصیت path، به یک رشته‌ی خالی همانند دومین شیء تنظیمات مسیریابی، تنظیم می‌شود:
   { path: '', redirectTo: 'welcome', pathMatch: 'full' },
در اینجا زمانیکه کاربر، ریشه‌ی سایت را درخواست می‌کند، به کامپوننت welcome هدایت خواهد شد. در این تنظیم خاصیت pathMatch نحوه‌ی تطابق با خاصیت path را مشخص می‌کند. مقدار full نیز به این معنا است که اگر تمام URL segment دریافتی خالی بود، از این تنظیم استفاده کن.


مدیریت مسیریابی آدرس‌های ناموجود در سایت

تنظیم سومی را نیز در اینجا مشاهده می‌کنید:
 { path: '**', component: PageNotFoundComponent },
** به این معنا است که اگر مسیریاب موفق به تطابق URL segment دریافتی، با هیچکدام از pathهای تعریف شده نبود، از این تنظیم استفاده کند (catch all). برای مثال اگر کاربر آدرس www.mysite.com/someAddr را درخواست کند، چون URL segment آن در آرایه‌ی تنظیمات، تعریف نشده‌است، می‌خواهیم یک صفحه‌ی یافت نشد را نمایش دهیم.

یک نکته: ترتیب مسیریابی‌ها در آرایه‌ی تعریف آن‌ها اهمیت دارد. در اینجا از استراتژی «اولین تطابق یافته‌، برنده خواهد بود» استفاده می‌شود. بنابراین تنظیم ** باید در انتهای لیست ذکر شود؛ در غیراینصورت هیچکدام از مسیریابی‌های تعریف شده‌ی پس از آن پردازش نخواهند شد.


مدیریت تغییرات آدرس‌های برنامه

در طول عمر برنامه ممکن است نیاز به تغییر آدرس‌های برنامه باشد. برای مثال بجای مسیر welcome مسیر home نمایش داده شود:
const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
در این تنظیمات جدید برنامه، مسیر home به WelcomeComponent اشاره می‌کند. اما جهت هدایت آدرس‌های قدیمی welcome، به این آدرس جدید home می‌توان از redirectTo، مانند دومین شیء مسیریابی اضافه شده‌ی به آرایه‌ی تنظیمات استفاده کرد. به این ترتیب هم تغییرات آرایه‌ی Routes به حداقل می‌رسد و هم آدرس‌های ذخیره شده‌ی توسط کاربران هنوز هم معتبر خواهند بود.

نکته: redirectToها قابلیت تعریف زنجیره‌ای را ندارند. به این معنا که اگر ریشه‌ی سایت درخواست شود، ابتدا به مسیر welcome هدایت خواهیم شد. مسیر welcome هم یک redirectTo دیگر به مسیر home را دارد. اما در اینجا کار به این redirectTo دوم نخواهد رسید و این پردازش، زنجیره‌ای نیست. بنابراین مسیریابی پیش‌فرض را نیز باید ویرایش کرد و به home تغییر داد:
const routes: Routes = [
  { path: 'home', component: WelcomeComponent },
  { path: 'welcome', redirectTo: 'home', pathMatch: 'full' },
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
به این ترتیب دیگر درخواست زنجیره‌وار redirectToها رخ نخواهد داد.

نکته: redirectToها می‌توانند local و یا absolute باشند. تعریف محلی آن‌ها مانند ذکر home و welcome در اینجا است و تنها سبب تغییر یک URL segment می‌شود. اما اگر در ابتدای مقادیر redirectToها یک / قرار دهیم، به معنای تعریف یک مسیر مطلق است و کل URL را جایگزین می‌کند.


تعیین محل نمایش قالب‌های کامپوننت‌ها

زمانیکه یک کامپوننت فعالسازی می‌شود، قالب آن در router-outlet نمایش داده خواهد شد. برای این منظور فایل src\app\app.component.html را گشوده و به نحو ذیل تغییر دهید:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <a class="navbar-brand">{{title}}</a>
    <ul class="nav navbar-nav">
      <li>
        <a [routerLink]="['/home']">Home</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
در اینجا دایرکتیو router-outlet محلی است که قالب کامپوننت فعالسازی شده، نمایش داده می‌شود. برای مثال اگر کاربر بر روی لینک Home کلیک کند، کامپوننت متناظر با آن از طریق تنظیمات مسیریابی یافت شده و فعالسازی می‌شود و سپس قالب آن در محل router-outlet رندر خواهد شد.

یک نکته: چون کامپوننت welcome از طریق مسیریابی نمایش داده می‌شود و دیگر به صورت مستقیم با درج تگ selector آن در صفحه فعالسازی نخواهد شد، می‌توان به تعریف کامپوننت آن مراجعه کرده و selector آن‌را حذف کرد.
 @Component({
//selector: 'app-welcome',
templateUrl: './welcome.component.html',
styleUrls: ['./welcome.component.css']
})

تا اینجا اگر دستور ng serve -o را صادر کنیم (کار build درون حافظه‌ای جهت محیط توسعه و نمایش خودکار برنامه در مرورگر)، چنین خروجی در مرورگر نمایان خواهد شد:


اگر به آدرس تنظیم شده‌ی در مرورگر دقت کنید، http://localhost:4200/home آدرسی است که در ابتدای نمایش سایت نمایان خواهد شد. علت آن نیز به تنظیم مسیریابی پیش فرض سایت برمی‌گردد.
و اگر یک مسیر غیرموجود را درخواست دهیم، قالب کامپوننت PageNotFound ظاهر می‌شود:



هدایت کاربران به قسمت‌های مختلف برنامه

کاربران را می‌توان به روش‌های مختلفی به قسمت‌های گوناگون برنامه هدایت کرد؛ برای مثال با کلیک بر روی المان‌های قابل کلیک HTML و سپس اتصال آن‌ها به کامپوننت‌های برنامه. استفاده‌ی کاربر از bookmark مرورگر و یا ورود مستقیم و دستی آدرس قسمتی از برنامه و یا کلیک بر روی دکمه‌های forward و back مرورگر. تنها مورد اول است که نیاز به تنظیم دارد و سایر قسمت‌ها به صورت خودکار مدیریت خواهند شد. نمونه‌ی آن‌را نیز با تعریف لینک Home پیشتر مشاهده کردید:
  <a [routerLink]="['/home']">Home</a>
- در اینجا با استفاده از یک دایرکتیو ویژه به نام routerLink، المان‌های قابل کلیک HTML را به خاصیت‌های path تعریف شده‌ی در تنظیمات مسیریابی برنامه متصل می‌کنیم. routerLink یک attribute directive است؛ بنابراین می‌توان آن‌را به یک المان قابل کلیک anchor tag، مانند مثال فوق نسبت داد.
- زمانیکه کاربر بر روی این لینک کلیک می‌کند، اولین path متناظر با routerLink یافت شده و فعالسازی خواهد شد.
- علت تعریف مقدار routerLink به صورت [] این است که آرایه‌ی پارامترهای لینک را مشخص می‌کند. بنابراین چون آرایه‌است، نیاز به [] دارد. اولین پارامتر این آرایه مفهوم root URL segment را دارد. در اینجا حتما نیاز است URL segment را با یک / شروع کرد. به علاوه باید دقت داشت که خاصیت path تنظیمات مسیریابی، حساس به حروف کوچک و بزرگ است. بنابراین این مورد را باید در اینجا نیز مدنظر داشت.
- پارامترهای دیگر routerLink می‌توانند مفهوم پارامترهای این segment و یا حتی segments دیگری باشند.

یک نکته: چون در مثال فوق، آرایه‌ی تعریف شده تنها دارای یک عضو است، آن‌را می‌توان به صورت ذیل نیز خلاصه نویسی کرد (one-time binding):
 <a routerLink="/home">Home</a>


تفاوت بین آدرس‌های HTML 5 و Hash-based

زمانیکه مسیریاب Angular کار پردازش آدرس‌های رسیده را انجام می‌دهد، اینکار در سمت کلاینت صورت می‌گیرد و تنها URL segment مدنظر را تغییر داده و این درخواست را به سمت سرور ارسال نمی‌کند. به همین جهت سبب reload صفحه نمی‌شود. دو روش در اینجا جهت مدیریت سمت کلاینت آدرس‌ها قابل استفاده است:
الف) HTML 5 Style
- آدرسی مانند http://localhost:4200/home، یک آدرس به شیوه‌ی HTML 5 است. در اینجا مسیریاب Angular با استفاده از HTML 5 history pushState سبب به روز رسانی History مرورگر شده و آدرس‌ها را بدون ارسال درخواستی به سمت سرور، در همان سمت کلاینت تغییر می‌دهد.
- این روش حالت پیش فرض Angular است و نحوه‌ی نمایش آن بسیار طبیعی به نظر می‌رسد.
- در اینجا URL rewriting سمت سرور نیز جهت هدایت آدرس‌ها، به برنامه‌ی Angular ضروری است. برای مثال زمانیکه کاربری آدرس http://localhost:4200/home را مستقیما در مرورگر وارد می‌کند، این درخواست ابتدا به سمت سرور ارسال خواهد شد و چون چنین صفحه‌ای در سمت سرور وجود ندارد، پیغام خطای 404 را دریافت می‌کند. اینجا است که URL rewriting سمت سرور به فایل index.html برنامه، جهت مدیریت یک چنین حالت‌هایی ضروری است.
برای نمونه اگر از وب سرور IIS استفاده می‌کنید، تنظیم ذیل را به فایل web.config در قسمت system.webServer اضافه کنید (کار کرد آن هم وابسته‌است به نصب و فعالسازی ماژول URL Rewrite بر روی IIS):
<rewrite>
    <rules>
        <rule name="Angular 2+ pushState routing" stopProcessing="true">
            <match url=".*" />
                <conditions logicalGrouping="MatchAll">
                    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                    <add input="{REQUEST_FILENAME}" pattern=".*\.[\d\w]+$" negate="true" />
                    <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
                </conditions>
                <action type="Rewrite" url="/index.html" />
        </rule>
    </rules>
</rewrite>

ب) Hash-based
- آدرسی مانند http://localhost:4200/#/home یک آدرس به شیوه‌ی Hash-based بوده و مخصوص مرورگرهایی است بسیار قدیمی که از HTML 5 پشتیبانی نمی‌کنند. اینبار قطعات قرار گرفته‌ی پس از علامت # دارای نام URL fragments بوده و قابلیت پردازش در سمت کلاینت را دارا می‌باشند.
- اگر علاقمند به استفاده‌ی از این روش هستید، نیاز است خاصیت useHash را به true تنظیم کنید:
@NgModule({
   imports: [RouterModule.forRoot(routes, { useHash: true })],
-  این روش نیازی به URL rewriting سمت سرور ندارد. از آنجائیکه سرور هرچیزی را که پس # باشد ندید خواهد گرفت و سعی در یافتن آن‌، در سمت سرور نخواهد کرد.
مطالب دوره‌ها
مثال - نمایش بلادرنگ تعداد کاربران آنلاین توسط SignalR
راه حل‌های زیادی برای محاسبه و نمایش تعداد کاربران آنلاین یک برنامه وب وجود دارند و عموما مبتنی بر کار با متغیرهای سشن یا Application و امثال آن هستند. این روش‌ها عموما دقیق نبوده و خصوصا قسمت قطع اتصال کاربر را نمی‌توانند دقیقا تشخیص دهند. به همین جهت نیاز به یک تایمر دارند که مثلا اگر در 5 دقیقه قبل، کاربری درخواست مشاهده آدرسی را به سرور ارسال نکرده بود، از لیست کاربران آنلاین حذف شود.
در ادامه بجای این روش‌ها، از SignalR برای محاسبه تعداد کاربران آنلاین و همچنین به روز رسانی بلادرنگ این عدد در سمت کاربر، استفاده خواهیم کرد.

تشخیص اتصال و قطع اتصال کاربران در SignalR

زیر ساخت‌های کلاس Hub موجود در SignalR، دارای متدهای ردیابی اتصال (OnConnected)، قطع اتصال (OnDisconnected) و یا برقراری مجدد اتصال کاربران (OnReconnected) هستند. با بازنویسی این متدها می‌توان به تخمین بسیار دقیقی از تعداد کاربران آنلاین یک سایت رسید.


پیشنیازهای بحث
پیشنیازهای این بحث با مطلب «مثال - نمایش درصد پیشرفت عملیات توسط SignalR» یکی است. برای مثال نحوه دریافت وابستگی‌ها، تنظیمات فایل global.asax و افزودن اسکریپت‌ها، تفاوتی با مثال یاد شده ندارند.


تعریف هاب کاربران آنلاین برنامه

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;

namespace SignalR05.Common
{
    public class OnlineUsersHub : Hub
    {
        public static readonly ConcurrentDictionary<string, string> OnlineUsers = new ConcurrentDictionary<string, string>();

        public void UpdateUsersOnlineCount()
        {
            // آی پی معرف یک کاربر است
            // اما کانکشن آی دی معرف یک برگه جدید در مرورگر او است
            // هر کاربر می‌تواند چندین برگه را به یک سایت گشوده یا ببندد
            var ipsCount = OnlineUsers.Select(x => x.Value).Distinct().Count();
            this.Clients.All.updateUsersOnlineCount(ipsCount);
        }

        /// <summary>
        /// اگر کاربران اعتبار سنجی شده‌اند بهتر است از
        /// this.Context.User.Identity.Name
        /// بجای آی پی استفاده شود
        /// </summary>        
        protected string GetUserIpAddress()
        {
            object environment;
            if (!Context.Request.Items.TryGetValue("owin.environment", out environment))
                return null;

            object serverRemoteIpAddress;
            if (!((IDictionary<string, object>)environment).TryGetValue("server.RemoteIpAddress", out serverRemoteIpAddress))
                return null;

            return serverRemoteIpAddress.ToString();
        }

        public override Task OnConnected()
        {
            var ip = GetUserIpAddress();
            OnlineUsers.TryAdd(this.Context.ConnectionId, ip);
            UpdateUsersOnlineCount();

            return base.OnConnected();
        }

        public override Task OnReconnected()
        {
            var ip = GetUserIpAddress();
            OnlineUsers.TryAdd(this.Context.ConnectionId, ip);
            UpdateUsersOnlineCount();

            return base.OnReconnected();
        }

        public override Task OnDisconnected()
        {
            // در این حالت ممکن است مرورگر کاملا بسته شده باشد
            // یا حتی صرفا یک برگه مرورگر از چندین برگه متصل به سایت بسته شده باشند
            string ip;
            OnlineUsers.TryRemove(this.Context.ConnectionId, out ip);
            UpdateUsersOnlineCount();

            return base.OnDisconnected();
        }
    }
}
کدهای کامل هاب شمارش کاربران آنلاین را در اینجا ملاحظه می‌کنید؛ به همراه نکته‌ی نحوه‌ی دریافت IP کاربر متصل شده به سایت، در یک هاب. کار افزودن یا حذف این کاربران به ConcurrentDictionary تعریف شده، در روال‌های بازنویسی شده اتصال، قطع اتصال و اتصال مجدد یک کاربر، انجام شده است.
در اینجا، هم به IP کاربر و هم به ConnectionId او نیاز است. از این جهت که هر ConnectionId، معرف یک برگه جدید باز شده در مرورگر کاربر است. اگر صرفا IPها را پردازش کنیم، با بسته شدن یکی از چندین برگه مرورگر او که اکنون به سایت متصل هستند، آمار او را از دست خواهیم داد. این کاربر هنوز چندین برگه باز دیگر را دارد که با سایت در ارتباط هستند، اما چون IP او را از لیست حذف کرده‌ایم (در نتیجه بسته شدن یکی از برگه‌ها)، آمار کلی شخص را نیز از دست خواهیم داد. بنابراین هر دوی IP و ConnectionIdها باید پردازش شوند.
اگر برنامه شما دارای اعتبارسنجی است (یک صفحه لاگین دارد)، بهتر است بجای IP از this.Context.User.Identity.Name استفاده کنید.


کدهای سمت کلاینت نمایش آمار کاربران

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script>
    <script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>
</head>
<body>
    <form id="form1" runat="server">
    online users count: <span id="usersCount"></span>
    </form>
    <script type="text/javascript">
        $(function () {
            $.connection.hub.logging = true;
            var onlineUsersHub = $.connection.onlineUsersHub;
            onlineUsersHub.client.updateUsersOnlineCount = function (count) {
                $('#usersCount').text(count);
            };
            $.connection.hub.start();
        });
    </script>
</body>
</html>
با توجه به اینکه در هاب تعریف شده، متد پویای updateUsersOnlineCount، آمار تعداد کاربران متصل را (تعداد آی پی‌های منحصربفرد متصل را) به کلاینت‌ها ارسال می‌کند، بنابراین در سمت کلاینت نیز با تعریف callback ایی به همین نام، می‌توان این آمار دریافتی را به کاربران سایت نمایش داد. آماری که به صورت خودکار با کم و زیاد شدن کاربران به روز شده و نیازی نیست کاربر به صورت دستی، صفحه را به روز کند.


کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت کنید:
SignalR05.zip
 
مطالب
بررسی Source Generators در #C - قسمت سوم - بهبود کارآیی برنامه با تبدیل عملیات Reflection به تولید کد خودکار
همانطور که در قسمت اول این سری نیز عنوان شد، انجام عملیات Reflection عموما به همراه سربار محاسبه‌ی هرباره‌ی اطلاعات مورد نیاز آن است و اکنون می‌توان یک چنین محاسباتی را توسط Source generators، در زمان کامپایل برنامه، تامین و جزئی از خروجی نهایی کامپل شده‌ی آن کرد تا کارآیی برنامه به شدت افزایش یابد. یک نمونه مثال آن، استفاده از ویژگی Display بر روی عناصر یک enum است تا بتوان توضیحات بیشتری را جهت نمایش در UI، ارائه داد:
using System.ComponentModel.DataAnnotations;

namespace NotifyPropertyChangedGenerator.Demo;

public enum Gender
{
    NotSpecified,
    [Display(Name = "مرد")] Male,
    [Display(Name = "زن")] Female
}
روش متداول جهت دسترسی به اطلاعات ویژگی Display، استفاده از Reflection به صورت زیر است:
public static class Extensions
{
    public static string GetDisplayName(this Enum value)
    {
        if (value is null)
            throw new ArgumentNullException(nameof(value));

        var attribute = value.GetType().GetField(value.ToString())?
            .GetCustomAttributes<DisplayAttribute>(false).FirstOrDefault();

        if (attribute is null)
            return value.ToString();

        return attribute.GetType().GetProperty("Name")?.GetValue(attribute, null)?.ToString();
    }
}
یعنی هرجائی که در برنامه نیاز باشد تا برای مثال نام نمایشی Gender.Female محاسبه شود، باید یکبار عملیات فوق در زمان اجرا، تکرار گردد با محاسبه‌ی تمام ویژگی‌های یک عنصر enum، بررسی وجود DisplayAttribute در این بین و در صورت وجود، محاسبه‌ی مقدار خاصیت Name آن.
یعنی در اصل متد کمکی که برای اینکار نیاز داریم، چنین خروجی را دارد:
namespace NotifyPropertyChangedGenerator.Demo
{
  public static class GenderExtensions
  {
      public static string GetDisplayName(this Gender @enum)
      {
          return @enum switch
            {
                Gender.NotSpecified => "NotSpecified",
                Gender.Male => "مرد",
                Gender.Female => "زن",
                _ => throw new ArgumentOutOfRangeException(nameof(@enum))
            };
      }
  }
}
مزیت این روش نسبت به Reflection، از پیش محاسبه شده بودن و سرعت بالای کار با آن است؛ اما ... باید به ازای هر enum نوشته شده، یکبار به صورت اختصاصی، تکرار شود و همچنین اگر اطلاعات enum متناظر با آن تغییر کرد، نیاز است تا این کلاس‌ها و متدهای کمکی نیز اصلاح شوند. به همین جهت است که عموما کار با Reflection ترجیح داده می‌شود؛ چون حجم کدنویسی کمتری را به همراه دارد و همچنین می‌تواند انواع و اقسام enum را پوشش دهد و عمومی است.
با ارائه‌ی Source Generators، مشکلات یاد شده دیگر وجود ندارند. یعنی کار تولید متدهای اختصاصی برای هر enum، خودکار است و همچنین به روز رسانی آنی آن‌ها با هر تغییری در enum‌ها نیز پیش‌بینی شده‌است.


تهیه‌ی تولید کننده‌ی خودکار کدی که نام نمایشی enumها را به صورت از پیش محاسبه شده ارائه می‌دهد

در قسمت قبل، با روش تهیه و استفاده از Source Generators آشنا شدیم. در اینجا نیز از همان قالب، در جهت تولید کد متد الحاقی GetDisplayName فوق، استفاده خواهیم کرد. یعنی هدف رسیدن به کلاس GenderExtensions فوق و متد GetDisplayName آن، در زمان کامپایل برنامه و به صورت خودکار است:
[Generator]
public class EnumExtensionsGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {}

    public void Execute(GeneratorExecutionContext context)
    {
        var compilation = context.Compilation;
        foreach (var syntaxTree in compilation.SyntaxTrees)
        {
            var semanticModel = compilation.GetSemanticModel(syntaxTree);
            var immutableHashSet = syntaxTree.GetRoot()
                .DescendantNodesAndSelf()
                .OfType<EnumDeclarationSyntax>()
                .Select(enumDeclarationSyntax => semanticModel.GetDeclaredSymbol(enumDeclarationSyntax))
                .OfType<ITypeSymbol>()
                /*.Where(typeSymbol => typeSymbol.GetAttributes().Any(
                    attributeData => string.Equals(attributeData.AttributeClass?.Name, "GenerateExtensions",
                        StringComparison.Ordinal)
                ))*/
                .ToImmutableHashSet();

            foreach (var typeSymbol in immutableHashSet)
            {
                var source = GenerateEnumExtensions(typeSymbol);
                context.AddSource($"{typeSymbol.Name}Extensions.cs", source);
            }
        }
    }
کار را با ایجاد یک کلاس عمومی جدید که پیاده سازی کننده‌ی اینترفیس ISourceGenerator و مزین به ویژگی Generator است، شروع می‌کنیم. در مورد وابستگی‌های مورد نیاز یک چنین پروژه‌ای، در قسمت قبل توضیحات کافی ارائه شد.
در اینجا در متد Execute، دسترسی کاملی را به اطلاعات تهیه شده‌ی توسط کامپایلر داریم. توسط آن تمام Enumهای برنامه را یا همان EnumDeclarationSyntax را در اینجا، یافته و سپس حلقه‌ای را بر روی اطلاعات آن‌ها تشکیل داده و برای تک تک آن‌ها، توسط متد GenerateEnumExtensions، کد معادل کلاس GenderExtensions را که در این مطلب معرفی شد، تولید می‌کنیم. در پایان کار نیز این کد را توسط متد AddSource، به کامپایلر معرفی خواهیم کرد تا بلافاصله در IDE ظاهر شده و قابلیت استفاده را پیدا کند.

یک نکته: اگر می‌خواهید صرفا enumهای خاصی در این بین بررسی شوند، می‌توانید کدهای یک Attribute سفارشی را مثلا با نام فرضی [GenerateExtensions] در همینجا توسط متد context.AddSource به مجموعه سورس‌ها اضافه کنید و سپس بر اساس نام آن، در قسمت Where ایی که کامنت شده‌است، تنها اطلاعات مدنظر را فیلتر و پردازش کنید.

متدی هم که ابتدا کلاس Extensions را بر اساس نام هر Enum موجود تولید و سپس بدنه‌ی متد GetDisplayName اختصاصی آن‌را تکمیل می‌کند، به صورت زیر است:
    private string GenerateEnumExtensions(ITypeSymbol typeSymbol)
    {
        return $@"namespace {typeSymbol.ContainingNamespace}
{{
  public static class {typeSymbol.Name}Extensions
  {{
      public static string GetDisplayName(this {typeSymbol.Name} @enum)
      {{
          {GenerateExtensionMethodBody(typeSymbol)}
      }}
  }}
}}";
    }

    private static string GenerateExtensionMethodBody(ITypeSymbol typeSymbol)
    {
        var sb = new StringBuilder();
        sb.Append(@"return @enum switch
            {
");

        foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>())
        {
            var displayAttribute = fieldSymbol.GetAttributes()
                .FirstOrDefault(attributeData =>
                    string.Equals(attributeData.AttributeClass?.Name, "DisplayAttribute", StringComparison.Ordinal));
            if (displayAttribute is null)
            {                
                sb.AppendLine(
                    $@"                {typeSymbol.Name}.{fieldSymbol.Name} => ""{fieldSymbol.Name}"",");
            }
            else
            {
                var displayAttributeName = displayAttribute.NamedArguments
                    .FirstOrDefault(x => string.Equals(x.Key, "Name", StringComparison.Ordinal))
                    .Value;
                sb.AppendLine(
                    $@"                {typeSymbol.Name}.{fieldSymbol.Name} => ""{displayAttributeName.Value}"",");
            }
        }

        sb.Append(
            @"                _ => throw new ArgumentOutOfRangeException(nameof(@enum))
            };");

        return sb.ToString();
    }
در مورد روش استفاده‌ی از این source generator نیز در قسمت قبل بحث شد و فقط کافی است ارجاعی را به اسمبلی آن به پروژه‌ی مدنظر افزود و OutputItemType را به آنالایزر تنظیم کرد.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: SourceGeneratorTests-part3.zip


سؤال: چگونه می‌توان کدهای تولید شده‌ی توسط یک Source Generator را ذخیره کرد؟

Source Generators به صورت پیش‌فرض هیچ فایلی را بر روی دیسک سخت ذخیره نمی‌کنند و تمام عملیات آن‌ها در حافظه انجام می‌شود. اگر علاقمند به مطالعه‌ی این خروجی‌های خودکار، به صورت فایل‌های واقعی هستید، نیاز به انجام تغییرات زیر در فایل csproj پروژه‌ی مصرف کننده‌ی Source Generator است:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
  </PropertyGroup>
  
  <Target Name="CleanSourceGeneratedFiles" BeforeTargets="BeforeBuild" DependsOnTargets="$(BeforeBuildDependsOn)">
    <RemoveDir Directories="$(CompilerGeneratedFilesOutputPath)" />
  </Target>  
  <ItemGroup>
    <!-- Exclude the output of source generators from the compilation -->
    <Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />
<Content Include="$(CompilerGeneratedFilesOutputPath)/**" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj" 
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>
توضیحات:
- EmitCompilerGeneratedFiles سبب ثبت فایل‌های خودکار تولید شده، بر روی دیسک سخت می‌شود که قالب مسیر پیش‌فرض ذخیره سازی آن به صورت زیر است:
{BaseIntermediateOutpath}/generated/{Assembly}/{SourceGeneratorName}/{GeneratedFile}
- اگر می‌خواهید نام پوشه‌ی generated را تغییر دهید، می‌توان از ویژگی CompilerGeneratedFilesOutputPath استفاده کرد.
- چون این فایل‌های cs. جدید ثبت شده‌ی بر روی دیسک سخت، مجددا وارد پروسه‌ی کامپایل می‌شوند و خود Source Generator هم یک نمونه‌ی از آن‌ها‌را پیش‌تر به کامپایلر معرفی کرده‌است، برنامه دیگر به علت وجود اطلاعات تکراری، کامپایل نخواهد شد. به همین جهت نیاز است تا قسمت Compile Remove فوق را نیز معرفی کرد تا کامپایلر از پوشه‌ی Generated تنظیمی، صرفنظر کند.
- اطلاعات موجود در پوشه‌ی Generated، فقط یکبار تولید می‌شوند؛ صرفنظر از اطلاعات موجود در حافظه که همیشه به روز است. به همین جهت اگر می‌خواهید نمونه‌های به روز شده‌ی آن‌ها را نیز بر روی دیسک سخت داشته باشید، نیاز به قسمت RemoveDir تنظیمی وجود دارد.
مطالب
Blazor 5x - قسمت 13 - کار با فرم‌ها - بخش 1 - کار با EF Core در برنامه‌های Blazor Server
در ادامه قصد داریم یک پروژه‌ی مدیریت هتل را پیاده سازی کنیم. این پروژه، دو قسمتی است. قسمت اول آن یک پروژه‌ی Blazor Server، برای مدیریت هتل مانند تعاریف اتاق‌ها است و پروژه‌ی دوم آن از نوع Blazor WASM، برای مراجعه‌ی کاربران عمومی و رزرو اتاق‌ها است. هدف، بررسی نحوه‌ی کار با هر دو نوع فناوری است. وگرنه می‌توان کل پروژه را با Blazor Server و یا کل آن‌را با Blazor WASM هم پیاده سازی کرد. در مورد نحوه‌ی انتخاب و مزایا و معایب هرکدام از این فناوری‌ها، در قسمت‌های اول و دوم این سری بیشتر بحث شده‌است.


ساختار پوشه‌ها و پروژه‌های قسمت Blazor Server


قسمت Blazor Server مدیریت هتل ما از 7 پروژه و پوشه‌ی زیر تشکیل می‌شود:
- BlazorServer.App: پروژه‌ی اصلی Blazor Server است که با اجرای دستور dotnet new blazorserver در پوشه‌ی خالی آن آغاز می‌شود.
- BlazorServer.Common: پروژه‌ای از نوع classlib، جهت قرارگیری کدهای مشترک بین پروژه‌ها است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.DataAccess: پروژه‌ای از نوع classlib، برای تعریف DbContext برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Entities: پروژه‌ای از نوع classlib، جهت تعریف کلاس‌های متناظر با جداول بانک اطلاعاتی برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Models: پروژه‌ای از نوع classlib، برای تعریف کلاس‌های data transfer objects برنامه (DTO's) است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Models.Mappings: پروژه‌ای از نوع classlib، برای تعریف نگاشت‌های بین DTO's و مجودیت‌های برنامه و برعکس است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Services: پروژه‌ای از نوع classlib، جهت تعریف کدهایی که منطق تجاری تعامل با بانک اطلاعاتی را از طریق BlazorServer.DataAccess میسر می‌کند که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.


اصلاح پروژه‌ی BlazorServer.App جهت استفاده از LibMan

قالب پیش‌فرض BlazorServer.App، به همراه پوشه‌ی wwwroot\css است که در آن بوت استرپ و open-iconic به همراه فایل site.css قرار دارند. چون این پروژه به همراه هیچ نوع روشی برای مدیریت نگهداری بسته‌های سمت کلاینت خود نیست، دو پوشه‌ی بوت استرپ و open-iconic آن‌را حذف کرده و از روش مطرح شده‌ی در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» استفاده خواهیم کرد:
dotnet tool update -g Microsoft.Web.LibraryManager.Cli
libman init
libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap
libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic
در پوشه‌ی ریشه‌ی پروژه‌ی BlazorServer.App، دستورات فوق را اجرا می‌کنیم تا بسته‌های bootstrap و open-iconic را در پوشه‌ی wwwroot/lib نصب کند و همچنین فایل libman.json متناظری را نیز جهت اجرای دستور libman restore برای دفعات آتی، تولید کند.

بعد از نصب بسته‌های ذکر شده، ابتدا سطر زیر را از ابتدای فایل پیش‌فرض wwwroot\css\site.css حذف می‌کنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
سپس فایل Pages\_Host.cshtml را به صورت زیر به روز رسانی می‌کنیم تا به مسیرهای جدید بسته‌های CSS، اشاره کند:
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorServer.App</title>
    <base href="~/" />
    <link href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet" />
    <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="BlazorServer.App.styles.css" rel="stylesheet" />
</head>

برای bundling & minification این فایل‌ها می‌توان از «Bundler Minifier» استفاده کرد.


پروژه‌ی موجودیت‌های مدیریت هتل

فایل BlazorServer.Entities.csproj وابستگی خاصی را نداشته و به صورت زیر تعریف شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
</Project>
در این پروژه، کلاس جدید HotelRoom را که بیانگر ساختار جدول متناظری در بانک اطلاعاتی است، به صورت زیر تعریف می‌کنیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Entities
{
    public class HotelRoom
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public int Occupancy { get; set; }

        [Required]
        public decimal RegularRate { get; set; }

        public string Details { get; set; }

        public string SqFt { get; set; }

        public string CreatedBy { get; set; }

        public DateTime CreatedDate { get; set; } = DateTime.Now;

        public string UpdatedBy { get; set; }

        public DateTime UpdatedDate { get; set; }
    }
}
که شامل فیلدهایی مانند نام، ظرفیت، مساحت و ... یک اتاق هتل است.


پروژه‌ی تعریف DbContext برنامه‌ی مدیریت هتل

فایل BlazorServer.DataAccess.csproj به این صورت تعریف شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
- در اینجا چون نیاز است موجودیت HotelRoom را به صورت یک DbSet معرفی کنیم، ارجاعی را به پروژه‌ی BlazorServer.Entities.csproj تعریف کرده‌ایم.
- همچنین دو وابستگی مورد نیاز جهت کار با EntityFrameworkCore و اجرای مهاجرت‌ها را نیز به آن افزوده‌ایم.

پس از تامین این وابستگی‌ها، اکنون می‌توان DbContext ابتدایی برنامه را به صورت زیر تعریف کرد که کار آن، در معرض دید قرار دادن HotelRoom به صورت یک DbSet است:
using BlazorServer.Entities;
using Microsoft.EntityFrameworkCore;

namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<HotelRoom> HotelRooms { get; set; }

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        { }
    }
}
پس از این مرحله، نیاز است این DbContext را به سیستم تزریق وابستگی‌های برنامه‌ی اصلی معرفی کرد. بنابراین فایل BlazorServer.App.csproj پروژه‌ی اصلی Blazor Server را گشوده و تغییرات زیر را اعمال می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
- چون می‌خواهیم ApplicationDbContext را به سیستم تزریق وابستگی‌ها معرفی کنیم، بنابراین باید بتوان به کلاس آن نیز دسترسی داشت که اینکار، با تعریف ارجاعی به BlazorServer.DataAccess.csproj میسر شده‌است.
- سپس چون می‌خواهیم از تامین کننده‌ی بانک اطلاعاتی SQL Server نیز استفاده کنیم، وابستگی‌های آن‌را نیز افزوده‌ایم.

با این تنظیمات، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و کار افزودن AddDbContext و UseSqlServer را انجام می‌دهیم تا DbContext برنامه از طریق تزریق وابستگی‌ها قابل دسترسی شود و همچنین رشته‌ی اتصالی مشخص شده نیز به تامین کننده‌ی SQL Server ارسال شود:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...
 
        public void ConfigureServices(IServiceCollection services)
        {
            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));

            // ...
این رشته‌ی اتصالی را به صورت زیر در فایل BlazorServer\BlazorServer.App\appsettings.json تعریف کرده‌ایم:
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
که در حقیقت یک رشته‌ی اتصالی جهت کار با LocalDB است.


اجرای مهاجرت‌ها و تشکیل ساختار بانک اطلاعاتی

پس از این تنظیمات، اکنون می‌توانیم به پوشه‌ی BlazorServer\BlazorServer.DataAccess مراجعه کرده و از طریق خط فرمان، دستورات زیر را صادر کنیم:
dotnet tool update --global dotnet-ef --version 5.0.3
dotnet build
dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext
dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
- در ابتدا نیاز است ابزارهای مهاجرت EF-Core را نصب کنیم که سطر اول، اینکار را انجام می‌دهد.
- همیشه بهتر است پیش از اجرای عملیات Migration، یکبار dotnet build را اجرا کرد؛ تا اگر خطایی وجود دارد، بتوان جزئیات دقیق آن‌را مشاهده کرد. چون عموما این جزئیات در حین اجرای دستورات بعدی، با پیام مختصر «عملیات شکست خورد»، نمایش داده نمی‌شوند.
- دستور سوم، کار تشکیل پوشه‌ی BlazorServer\BlazorServer.DataAccess\Migrations و تولید خودکار دستورات تشکیل بانک اطلاعاتی را بر اساس ساختار DbContext برنامه انجام می‌دهد.
- دستور چهارم، بر اساس اطلاعات موجود در پوشه‌ی BlazorServer\BlazorServer.DataAccess\Migrations، بانک اطلاعاتی واقعی را تولید می‌کند.
در این دستورات ذکر پروژه‌ی آغازین برنامه جهت یافتن وابستگی‌های پروژه ضروری است.



تکمیل پروژه‌ی DTO‌های برنامه

همواره توصیه شده‌است که موجودیت‌های برنامه را مستقیما در معرض دید UI قرار ندهید. حداقل مشکلی را که در اینجا ممکن است مشاهده کنید، حملات از نوع mass assignment هستند. برای مثال قرار است از کاربر، کلمه‌ی عبور جدید آن‌را دریافت کنید، ولی چون اطلاعات دریافتی، به اصل موجودیت متناظر با بانک اطلاعاتی نگاشت می‌شود، کاربر می‌تواند فیلد IsAdmin را هم خودش مقدار دهی کند! و چون سیستم Binding بسیار پیشرفته عمل می‌کند، این ورودی را معتبر یافته و در اینجا علاوه بر به روز رسانی کلمه‌ی عبور، خواص دیگری را هم که نباید به روز رسانی شوند، به روز رسانی می‌کند و یا در بسیاری از موارد نیاز است data annotations خاصی را برای فیلدها تعریف کرد که ربطی به موجودیت اصلی ندارند و یا نیاز است فیلدهایی را در UI قرار داد که باز هم تناظر یک به یکی با موجودیت اصلی ندارند (گاهی کمتر و گاهی بیشتر هستند و باید بر روی آن‌ها محاسباتی صورت گیرد تا قابلیت ذخیره سازی در بانک اطلاعاتی را پیدا کنند). به همین جهت کار مدل سازی UI و یا بازگشت اطلاعات نهایی از سرویس‌ها را توسط DTO‌ها که یک سری کلاس ساده‌ی C# 9.0 از نوع record هستند، انجام می‌دهیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public record HotelRoomDTO
    {
        public int Id { get; init; }

        [Required(ErrorMessage = "Please enter the room's name")]
        public string Name { get; init; }

        [Required(ErrorMessage = "Please enter the occupancy")]
        public int Occupancy { get; init; }

        [Range(1, 3000, ErrorMessage = "Regular rate must be between 1 and 3000")]
        public decimal RegularRate { get; init; }

        public string Details { get; init; }

        public string SqFt { get; init; }
    }
}
Record‌های C# 9.0، انتخاب بسیار مناسبی برای تعریف DTO‌ها هستند. از این لحاظ که قرار نیست اطلاعات دریافتی از کاربر، در این بین و پس از مقدار دهی اولیه، تغییر کنند.
در اینجا فیلدهای UI برنامه را که در قسمت بعد تکمیل خواهیم کرد، مشاهده می‌کنید؛ به همراه یک سری data annotation برای تعریف اجباری و یا بازه‌ی مورد قبول، به همراه پیام‌های خطای مرتبط.


نگاشت DTO‌های برنامه به موجودیت‌ها و بر عکس

یا می‌توان خواص DTO تعریف شده را یکی یکی به موجودیتی متناظر با آن انتساب داد و یا می‌توان از AutoMapper برای اینکار استفاده کرد. به همین جهت به BlazorServer.Models.Mappings.csproj مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" />
    <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
  </ItemGroup>
</Project>
- پروژه‌ای که کار تعریف نگاشت‌ها را انجام می‌دهد، نیاز به اطلاعات موجودیت‌ها و مدل‌ها (DTO ها)ی متناظر را دارد. به همین جهت ارجاعاتی را به این دو پروژه، تعریف کرده‌ایم.
- همچنین بسته‌ی مخصوص AutoMapper را که به همراه امکانات تزریق وابستگی‌های آن نیز هست، در اینجا افزوده‌ایم.

پس از افزودن این ارجاعات، نگاشت دو طرفه‌ی بین مدل و موجودیت تعریف شده را به صورت زیر تعریف می‌کنیم:
using AutoMapper;
using BlazorServer.Entities;

namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            CreateMap<HotelRoomDTO, HotelRoom>().ReverseMap(); // two-way mapping
        }
    }
}
اکنون برای شناسایی پروفایل فوق و معرفی آن به AutoMapper، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و تزریق وابستگی و ردیابی خودکار آن‌را اضافه می‌کنیم که شامل اسکن تمام اسمبلی‌های موجود، جهت یافتن Profile‌های AutoMapper است:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

            // ...


تعریف سرویس مدیریت اتاق‌های هتل

پس از راه اندازی برنامه و تعریف موجودیت‌ها، DbContext و غیره، اکنون می‌توانیم از آن‌ها جهت ارائه‌ی منطق مدیریتی برنامه استفاده کنیم:
using System.Collections.Generic;
using System.Threading.Tasks;
using BlazorServer.Models;

namespace BlazorServer.Services
{
    public interface IHotelRoomService
    {
        Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO);

        Task<int> DeleteHotelRoomAsync(int roomId);

        IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync();

        Task<HotelRoomDTO> GetHotelRoomAsync(int roomId);

        Task<HotelRoomDTO> IsRoomUniqueAsync(string name);

        Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO);
    }
}

که پیاده سازی ابتدایی آن به صورت زیر است:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using BlazorServer.DataAccess;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public HotelRoomService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO)
        {
            var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
            hotelRoom.CreatedDate = DateTime.Now;
            hotelRoom.CreatedBy = "";
            var addedHotelRoom = await _dbContext.HotelRooms.AddAsync(hotelRoom);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<HotelRoomDTO>(addedHotelRoom.Entity);
        }

        public async Task<int> DeleteHotelRoomAsync(int roomId)
        {
            var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId);
            if (roomDetails == null)
            {
                return 0;
            }

            _dbContext.HotelRooms.Remove(roomDetails);
            return await _dbContext.SaveChangesAsync();
        }

        public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync()
        {
            return _dbContext.HotelRooms
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .AsAsyncEnumerable();
        }

        public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId)
        {
            return _dbContext.HotelRooms
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
        }

        public Task<HotelRoomDTO> IsRoomUniqueAsync(string name)
        {
            return _dbContext.HotelRooms
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Name == name);
        }

        public async Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO)
        {
            if (roomId != hotelRoomDTO.Id)
            {
                return null;
            }

            var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId);
            var room = _mapper.Map(hotelRoomDTO, roomDetails);
            room.UpdatedBy = "";
            room.UpdatedDate = DateTime.Now;
            var updatedRoom = _dbContext.HotelRooms.Update(room);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<HotelRoomDTO>(updatedRoom.Entity);
        }
    }
}
- در اینجا DbContext برنامه و همچنین نگاشت‌گر AutoMapper، به سازنده‌ی سرویس، تزریق شده و توسط آن‌ها، ابتدا اطلاعات DTOها به موجودیت‌ها تبدیل شده‌اند (و یا برعکس) و سپس با استفاده از dbContext برنامه، کوئری‌هایی را بر روی بانک اطلاعاتی اجرا کرده‌ایم.
- در این کدها استفاده از متد ProjectTo را هم مشاهده می‌کنید. استفاده از این متد، بسیار بهینه‌تر از کار با متد Map درون حافظه‌ای است. از این جهت که بر روی SQL نهایی ارسالی به سمت سرور تاثیرگذار است و تعداد فیلدهای بازگشت داده شده را بر اساس DTO تعیین شده، کاهش می‌دهد. درغیراینصورت باید تمام ستون‌های جدول را بازگشت داد و سپس با استفاده از متد Map درون حافظه‌‌ای، کار نگاشت نهایی را انجام داد که آنچنان بهینه نیست.

در آخر نیاز است این سرویس را نیز به سیستم تزریق وابستگی‌های برنامه معرفی کنیم. به همین جهت در فایل BlazorServer\BlazorServer.App\Startup.cs، تغییر زیر را اعمال خواهیم کرد:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IHotelRoomService, HotelRoomService>();

         // ...


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-13.zip
نظرات مطالب
ایجاد سیستم وضعیت آب و هوا مانند گوگل (بخش اول)
قسمت switch ایی را که در View نوشتید، تبدیل کنید به یک متد کمکی در کلاسی خارج از View: (مهم نیست متد الحاقی باشد یا خیر؛ فقط داخل View نباشد)
public static string GetFileName(int code)
{
    switch (code)
    {
       case 0: return "/Images/User/Weather/Tornado.png";
  //...
}
بعد یک خاصیت محاسباتی به نام FileName به مدل مورد استفاده اضافه کنید:
    public class YahooWeatherRssItem
    {
        public int Code { get; set; } 
        //...
        public string FileName 
        {
          get { return Util.GetFileName(Code); }
        }
    }
به این صورت View از کدهای محاسبات یافتن FileName خالی می‌شود.
نظرات مطالب
EF Code First #2
لطف کنید سؤالی رو که مطرح می‌کنید در حیطه مطلب جاری عنوان شده باشد و خارج از آن نباشد.
سؤال شما هم بحث کلاینت سروری است و نه بحث کلاینت تنها که EF روی آن مشغول به کار است.
- می‌شود در متد Seed ایی که در بالا توضیح دادم در SQL Server تریگر درست کرد. (که مثلا اگر کاربر دیگری به شرط اینکه این کاربر جزو کاربران تعریف شده در خود SQL Server باشد نه در برنامه شما، اتفاق خاصی رخ دهد. برنامه شما هم بدیهی است باید سرور را مدام چک کند تا از این مساله مطلع شود)- SQL Server مبحثی دارد به نام Service Broker : (^). توسط آن می‌توان از طریق سرور به کلاینت اطلاع رسانی کرد. بازهم خارج است از بحث یک ORM. یا تمام ORMهای موجود. - EF مبحثی دارد به نام Concurrency check که اگر شخصی در شبکه بر روی رکوردی که همین الان شما مشغول به کار هستید، تغییری را ایجاد کرد، به شما اطلاع رسانی کند. (در قسمت‌های بعدی بحث خواهد شد). البته این هم خودکار نیست. لازم است یک رفت و برگشت به سرور انجام شود.- entity framework auditing هم میسر است. خودکار نیست. در همان کلاس Context فوق که از DbContext مشتق می‌شود می‌توان متد تحریف شده public override int SaveChanges را تعریف کرد. در اینجا می‌توان به تمام تغییراتی که قرار است اعمال شوند دسترسی داشت. مثلا آن‌ها را در یک جدول مجزا ثبت کرد. بدیهی است برنامه بعدا نیاز خواهد داشت از این جدول گزارشگیری کند.