مطالب
مسیریابی در Angular - قسمت نهم - محافظ‌های مسیرها
جهت مقاصد امنیتی، اعتبارسنجی کاربران و یا تحت نظر قرار دادن مسیرها، نیاز است بتوان بررسی کرد که آیا پیمایش یک مسیر، مجاز است یا خیر؟ برای پیاده سازی یک چنین ویژگی‌هایی در Angular، مفهوم Route Guards یا محافظ‌های مسیرها پیش بینی شده‌است که شامل چندین نوع محافظ می‌شوند:
 - canActivate : جهت محافظت دسترسی به یک مسیر
 -  canActivateChild: برای محافظت دسترسی به یک Child Route
 - canDeactivate : برای جلوگیری کردن از ترک مسیر جاری و هدایت به مسیری دیگر (برای مثال جهت نمایش پیام «هنوز اطلاع تغییر یافته را ذخیره نکرده‌اید»)
 - canLoad : برای جلوگیری از مسیریابی غیرهمزمان (async routing) که در قسمت بعدی بررسی خواهد شد
 - resolve: برای پیش واکشی اطلاعات، پیش از نمایش مسیر (که آن‌را در قسمت چهارم این سری بررسی کردیم)


لزوم استفاده‌ی از محافظ‌های مسیرها


گاهی از اوقات می‌خواهیم دسترسی به یک مسیر را محدود به کاربران وارد شده‌ی به سیستم کنیم و یا مسیرهایی را داشته باشیم که تنها توسط گروه خاصی از کاربران قابل دسترسی باشند. همچنین در بسیاری از اوقات نیاز است به کاربران اخطارهایی را پیش از ترک یک مسیر نمایش دهیم. برای مثال پیش از ترک صفحه‌ی ویرایش اطلاعاتی که دارای اطلاعات ذخیره نشده‌است، بهتر است پیامی را جهت یادآوری این مساله نمایش دهیم. برای پیاده سازی هر کدام از این قابلیت‌ها از یک محافظ مسیر ویژه استفاده می‌شود.


ترتیب اجرای محافظ‌های مسیرها

مسیریاب سیستم، ابتدا محافظ canDeactivate را اجرا می‌کند تا مشخص شود که آیا کاربر می‌تواند مسیر جاری را ترک کند یا خیر؟ سپس اگر مسیریابی تعریف شده غیرهمزمان باشد، محافظ canLoad اجرا می‌شود. پس از آن محافظ canActivateChild بررسی می‌شود. در ادامه محافظ canActivate اجرا می‌گردد. در پایان کار بررسی محافظ‌های موجود، کار بررسی محافظ resolve‌، جهت پیش واکشی اطلاعات مسیر درخواستی، انجام خواهد شد.
در اینجا اگر یکی از محافظ‌ها مقدار false را برگرداند، پردازش مابقی آن‌ها لغو خواهد شد و کار هدایت کاربر به مسیر درخواستی، خاتمه می‌یابد.


مراحل ساخت و اعمال یک محافظ مسیر

ساخت و اعمال یک محافظ مسیر شامل سه مرحله است:
الف) یک محافظ مسیر عموما به صورت یک سرویس جدید پیاده سازی می‌شود:
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {

    }
}
در اینجا برای اینکه این سرویس به صورت یک محافظ مسیر عمل کند، نیاز است نوع محافظ مدنظر را نیز پیاده سازی نماید؛ مانند CanActivate در اینجا. پس از آن باید متد مرتبط با این اینترفیس که در اینجا canActivate است، پیاده سازی شود. اگر این متد false را برگرداند، سبب لغو هدایت کاربر به آن مسیر خواهد شد و این متد می‌تواند خروجی پیچیده‌تری مانند یک Observable را نیز داشته باشد. اگر یک چنین نوع خروجی درنظر گرفته شود، فراخوان آن، تا پایان کار این Observable صبر خواهد کرد.

ب) از آنجائیکه محافظ‌ها، سرویس هستند، نیاز است تعریف کلاس آن‌ها را در قسمت providers ماژول مرتبط نیز ذکر کنیم تا در برنامه قابل دسترسی شوند. باید دقت داشت که برخلاف سایر سرویس‌ها، امکان تعریف محافظ‌ها صرفا در سطح یک ماژول مسیر است و نه در سطح یک کامپوننت. به این ترتیب مسیریاب می‌تواند به آن، در طی هدایت کاربر به مسیر درخواستی، دسترسی پیدا کند.

ج) پس از آن برای فعالسازی یک محافظ مسیر، آن‌را به عنوان یک خاصیت جدید، به تنظیمات مسیریابی اضافه خواهیم کرد. نام این خاصیت دقیقا مساوی با نوع محافظی است که تعریف شده‌است. برای مثال اگر محافظ تعریف شده از نوع CanActivate است، نام خاصیتی که ذکر خواهد شد، canActivate می‌باشد. مقدار آن نیز می‌تواند آرایه‌ای از سرویس‌هایی از این نوع باشد.

امکان به اشتراک گذاشتن یک محافظ بین چندین مسیر نیز وجود دارد. فرض کنید می‌خواهیم تمام مسیرهای مربوط به محصولات را محافظت کنیم. در این حالت می‌توان محافظ را به تک تک Child routes موجود اعمال کرد و یا می‌توان محافظ را به والد آن‌ها نیز اعمال کنیم تا به صورت خودکار سبب محافظت از فرزندان آن نیز شویم.


یک مثال: ساخت محافظ canActivate‌

جهت بررسی شرط یا شرایطی پیش از فعال سازی یک مسیر درخواستی، از محافظ‌هایی از نوع canActivate می‌توان استفاده کرد. این نوع محافظ‌ها عموما جهت اعتبارسنجی کاربران و محدود سازی دسترسی آن‌ها به قسمت‌های مختلف برنامه استفاده می‌شوند. این نوع محافظ‌ها حتی با تغییر پارامترهای مسیریابی نیز فعال شده و بررسی می‌شوند.

در ادامه‌ی مثال این سری می‌خواهیم کاربران را پیش از دسترسی به قسمت‌های مختلف مرتبط با محصولات، وادار به لاگین کنیم. برای این منظور دستور ذیل را اجرا کنید:
 >ng g guard user/auth -m user/user.module
به این ترتیب تغییرات ذیل در ماژول کاربران رخ خواهند داد:
 installing guard
  create src\app\user\auth.guard.spec.ts
  create src\app\user\auth.guard.ts
  update src\app\user\user.module.ts
در اینجا قالب ابتدایی کلاس سرویس AuthGuard ایجاد می‌شود (در فایل auth.guard.ts) و همچنین اگر به سطر آخر آن دقت کنید، این سرویس را به قسمت providers ماژول کاربران (در فایل user.module.ts) نیز افزوده‌است.

در ادامه کدهای این محافظ را به صورت ذیل تکمیل کنید:
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router } from '@angular/router';

import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService,
    private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.checkLoggedIn(state.url);
  }

  checkLoggedIn(url: string): boolean {
    if (this.authService.isLoggedIn()) {
      return true;
    }
    this.authService.redirectUrl = url;
    this.router.navigate(['/login']);
    return false;
  }
}
خاصیت redirectUrl نیز به کلاس سرویسAuthService ، جهت به اشتراک گذاری اطلاعات، اضافه شده‌است:
export class AuthService {
   currentUser: IUser;
   redirectUrl: string;

توضیحات:

این سرویس چون از نوع CanActivate است، این اینترفیس را پیاده سازی کرده‌است و همچنین متد canActivate آن‌را نیز به همراه دارد:
export class AuthGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
در اینجا از ActivatedRouteSnapshot می‌توان اطلاعات مسیرجاری، مانند پارامترهای آن‌را بدست آورد. پارامتر RouterStateSnapshot نیز وضعیت مسیریابی را بازگشت می‌دهد. برای مثال state.url، حاوی آدرس کامل مسیر درخواستی به صورت یک رشته است که از آن در اینجا جهت حفظ و به اشتراک گذاری مسیر اولیه‌ی درخواستی استفاده شده‌است. خاصیت route.url حاوی آرایه‌ای از URL segments است.

یک نکته: هرچند در اینجا می‌توان به پارامتر id مسیر، مانند route.params['id'] در صورت نیاز دسترسی یافت، اما امکان دسترسی به اطلاعات از پیش واکشی شده مانند route.data['product'] وجود ندارد. علت آن‌را نیز در قسمت «ترتیب اجرای محافظ‌های مسیرها» ابتدای بحث جاری، بررسی کردیم: محافظ resolve در انتهای کار پردازش تمام محافظ‌های موجود فراخوانی می‌شود.

در متد canActivate می‌خواهیم بررسی کنیم که آیا کاربر، لاگین کرده‌است یا خیر؟ اگر بله، تنها کافی است true را بازگشت دهیم تا کار این محافظ پایان یابد. در غیراینصورت false را بازگشت داده و همچنین سبب هدایت کاربر به صفحه‌ی لاگین می‌شویم.
به همین منظور سرویس AuthService را به سازنده‌ی این کلاس تزریق کرده‌ایم تا بتوانیم به متد isLoggedIn آن دسترسی پیدا کنیم (این سرویس را در قسمت دوم این سری تکمیل کردیم).
این متد نیز به صورت ذیل تعریف شده‌است:
isLoggedIn(): boolean {
   return !this.currentUser;
}
در اینجا استفاده‌ی از ! سبب بازگشت true، در صورت نال نبودن شیء کاربر جاری وارد شده‌ی به سیستم می‌شود.

در ادامه برای استفاده‌ی از این محافظ مسیر، به فایل src\app\product\product-routing.module.ts مراجعه کرده و آن‌را به نحو ذیل اعمال خواهیم کرد:
import { AuthGuard } from './../user/auth.guard';

const routes: Routes = [
  {
    path: 'products',
    canActivate: [ AuthGuard ],
    children: [    ]
  }
];
در قسمت ششم، کار گروه بندی مسیرها را انجام دادیم. اکنون در اینجا نمونه‌ای از استفاده‌ی از آن‌را مشاهده می‌کنید. بجای اینکه AuthGuard  را به تک تک مسیرهای فرزند تعریف شده‌ی محصولات، اعمال کنیم، آن‌را به والد این مسیر اعمال کرده‌ایم تا به صورت خودکار به تمام فرزندان آن نیز اعمال شود.

اکنون برنامه را با دستور ng s -o ساخته و اجرا کنید. سپس بر روی لینک لیست محصولات و یا افزودن یک محصول جدید کلیک کنید. بلافاصله صفحه‌ی لاگین را مشاهده خواهید کرد.


به خاطر سپاری و بازیابی مسیر درخواستی کاربر پس از لاگین

در اینجا اگر کاربر بر روی لینک افزودن یک محصول جدید کلیک کند، صفحه‌ی لاگین را مشاهده خواهد کرد. اما پس از لاگین، همواره به مسیر لیست محصولات هدایت می‌شود و در این حالت مسیر درخواستی اولیه فراموش خواهد شد. برای رفع این مشکل نیاز است آدرس درخواستی کاربر را نیز ذخیره و بازیابی کرد. به همین جهت خاصیت this.authService.redirectUrl = url را در متد checkLoggedIn محافظ تعریف شده مقدار دهی کردیم. در اینجا از سرویس Auth، برای به اشتراک گذاری اطلاعات با محافظ‌های مسیر استفاده کرده‌ایم. طول عمر یک سرویس، singleton است. بنابراین تنها یک وهله از آن در طول عمر برنامه وجود خواهد داشت. به این ترتیب با ذخیره‌ی اطلاعاتی در آن، این اطلاعات در تمام برنامه قابل دسترسی خواهد شد.
با توجه به این نکته، اکنون به فایل src\app\user\login\login.component.ts مراجعه کرده و قسمت this.router.navigate آن‌را به صورت ذیل بهبود خواهیم بخشید:
      if (this.authService.login(userName, password)) {
        if (this.authService.redirectUrl) {
          this.router.navigateByUrl(this.authService.redirectUrl);
        } else {
          this.router.navigate(['/products']);
        }
      }
در اینجا بررسی می‌شود که آیا پیشتر خاصیت redirectUrl پس از لاگین مقدار دهی شده‌است یا خیر؟ اگر بله، از متد navigateByUrl جهت هدایت به آن مسیر استفاده خواهد شد.

در ادامه برای آزمایش آن، پس از اجرای برنامه، صفحه‌ی افزودن یک محصول جدید را درخواست دهید. سپس لاگین کنید. اکنون مشاهده خواهید کرد که برنامه مسیر درخواستی پیش از لاگین را به خاطر سپرده‌است.


بررسی محافظ canActivateChild

این محافظ نیز شبیه به محافظ canActivate است؛ با این تفاوت که تنها زمانی فعالسازی خواهد شد که فرزند یک مسیر قرار است نمایش داده شود و نه خود مسیر اصلی.
محافظ canActivateChild با تغییر قسمت child یک مسیر فعالسازی می‌شود؛ حتی اگر این تغییر در حد تغییر پارامترهای آن مسیر باشد. اما باید درنظر داشت که اگر تنها قسمت child یک مسیر تغییر کند، دیگر محافظ canActivate مجددا اجرا نخواهد شد.

یک مثال: اگر کاربر در حال مشاهده‌ی صفحه‌ی لیست محصولات باشد و بر روی لینک مشاهده‌ی یک محصول کلیک کند، تنها قسمت child مسیر تغییر می‌کند. در این حالت canActivate مسیر اصلی دیگر اجرا نخواهد شد؛ اما تمام محافظ‌های canActivateChild مرتبط مجددا اجرا خواهند شد.


بررسی محافظ canDeactivate

محافظ canDeactivate پیش از ترک یک مسیر، فعالسازی و بررسی می‌شود. عموما از آن جهت بررسی وضعیت اطلاعات ذخیره نشده و اطلاع رسانی به کاربر، پیش از ترک مسیر جاری استفاده استفاده می‌گردد. این محافظ با هر تغییری در آدرس جاری مسیر، بررسی می‌شود. بدیهی است این تغییر صرفا درون یک برنامه‌ی Angular معنا پیدا می‌کند و نه هدایت به سایتی دیگر.
در حال حاضر در مثال جاری این سری، اگر کاربر، تغییری را در صفحه‌ی ویرایش اطلاعات ایجاد کند و بدون کلیک بر روی دکمه‌ی Save به صفحه‌ی دیگری مراجعه کند، این اطلاعات تغییر یافته، از دست خواهند رفت. برای رفع این مشکل می‌توان محافظ canDeactivate ایی را برای آن طراحی کرد. به همین جهت دستور ذیل را اجرا کنید:
 >ng g guard product/ProductEdit -m product/product.module
تا سبب انجام تغییرات ذیل در ماژول محصولات شود:
 installing guard
  create src\app\product\product-edit.guard.spec.ts
  create src\app\product\product-edit.guard.ts
  update src\app\product\product.module.ts
در اینجا علاوه بر ایجاد قالب ابتدایی محافظ ProductEdit، سبب به روز رسانی قسمت providers ماژول محصولات نیز شده‌است.

امضای ابتدایی یک محافظ CanDeactivate به صورت ذیل است:
export  class ProductEditGuard implements CanDeactivate<ProductEditComponent> {
    canDeactivate(component: ProductEditComponent): boolean {
اینترفیس CanDeactivate جنریک بوده و پارامتر جنریک آن نوع کامپوننتی را که قرار است از این محافظ استفاده کند، مشخص می‌کند. سپس نوع پارامتر متد canDeactivate آن بر اساس نوع پارامتر جنریک، تعیین می‌گردد.
اکنون این محافظ نیاز دارد تا بداند که آیا کامپوننت ویرایش محصولات، دارای اطلاعات ذخیره نشده‌ای هست یا خیر؟ چون کامپوننت ویرایش محصولات، به عنوان پارامتر به متد canDeactivate آن ارسال شده‌است، بنابراین می‌تواند به خواص و متد‌های عمومی آن کلاس نیز دسترسی پیدا کند. به همین جهت تغییرات ذیل را به کامپوننت ویرایش محصولات در فایل src\app\product\product-edit\product-edit.component.ts اعمال می‌کنیم:
  get product(): IProduct {
    return this.currentProduct;
  }
  set product(value: IProduct) {
    this.currentProduct = value;
    // Clone the object to retain a copy
    this.originalProduct = Object.assign({}, value);
  }

  get isDirty(): boolean {
    return JSON.stringify(this.originalProduct) !== JSON.stringify(this.currentProduct);
  }
در اینجا یک کپی از اصل محصول در حال ویرایش، برای مقایسه‌ی آن با محصول جاری در حال ویرایش، نگهداری می‌شود. به این ترتیب خاصیت isDirty می‌تواند مشخص کند که آیا تغییری بر روی خواص این شیء صورت گرفته‌است یا خیر؟ استفاده از متد JSON.stringify، یکی از ساده‌ترین روش‌هایی است که از آن می‌توان جهت مقایسه‌ی تمام خواص دو شیء استفاده کرد. البته چون در اینجا ترتیب خواص این دو شیء یکی است، این روش کار می‌کند.
برای اینکه این امر میسر شود، خاصیت product به حالت get/set دار تغییر یافته‌است تا بتوان کپی اولیه‌ی محصول را جهت مقایسه، نگهداری کرد. استفاده از متد Object.assign سبب ایجاد یک کپی از شیء اولیه شده و به این صورت دو وهله‌ی غیرمشترک را خواهیم داشت. اگر value مستقیما به originalProduct  انتساب داده می‌شد، در این حالت هر دوی currentProduct و originalProduct به یک شیء اشاره می‌کردند.

اکنون می‌توان از این خاصیت جدید کامپوننت ویرایش محصولات، در محافظ ترک صفحه‌ی آن استفاده کرد:
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';

import { ProductEditComponent } from './product-edit/product-edit.component';

@Injectable()
export class ProductEditGuard implements CanDeactivate<ProductEditComponent> {

  canDeactivate(component: ProductEditComponent): boolean {
    if (component.isDirty) {
      let productName = component.product.productName || 'New Product';
      return confirm(`Navigate away and lose all changes to ${productName}?`);
    }
    return true;
  }
}
در اینجا اگر فرم، تغییر یافته و هنوز ذخیره نشده باشد، خاصیت isDirty برقرار شده و سبب نمایش یک دیالوگ confirm می‌شود. اگر کاربر آن‌را تائید کند، آنگاه مسیر درخواستی جدید فعال می‌شود. در غیراینصورت، هدایت به مسیر جدید لغو خواهد شد.

در آخر برای استفاده‌ی از این محافظ جدید، باید آن‌را به تنظیمات مسیریابی برنامه اضافه کنیم. به همین جهت به فایل src\app\product\product-routing.module.ts مراجعه کرده و این محافظ را به والد مسیریابی ویرایش یک محصول اضافه می‌کنیم:
import { ProductEditGuard } from './product-edit.guard';

const routes: Routes = [
  {
    path: 'products',
    canActivate: [ AuthGuard ],    
    children: [
      {
        path: '',
        component: ProductListComponent
      },
      {
        path: ':id',
        component: ProductDetailComponent,
        resolve: { product: ProductResolverService }
      },
      {
        path: ':id/edit',
        component: ProductEditComponent,
        resolve: { product: ProductResolverService },
        canDeactivate: [ ProductEditGuard ],
        children: [
          { path: '', redirectTo: 'info', pathMatch: 'full' },
          { path: 'info', component: ProductEditInfoComponent },
          { path: 'tags', component: ProductEditTagsComponent }
        ]
      }
    ]
  }
];
با افزودن canDeactivate به والد ویرایش محصولات، از هر دو child route تعریف شده محافظت می‌کند.


برای آزمایش آن، به صفحه‌ی ویرایش یکی از محصولات مراجعه کرده و تغییری را ایجاد کنید. سپس درخواست مشاهده‌ی صفحه‌ی دیگری را با کلیک بر روی یکی از لینک‌های منوی برنامه ارائه دهید. بلافاصله دیالوگ confirm ظاهر خواهد شد (تصویر فوق).

مشکل! در همین حالت بر روی دکمه‌ی Ok کلیک کنید تا اطلاعات ذخیره نشده را از دست داده و به مسیر دیگری هدایت شویم. مجددا همین پروسه را تکرار کنید. اینبار اگر بر روی دکمه‌ی Save کلیک کنید، باز هم دیالوگ confirm ظاهر می‌شود. علت اینجا است که شیء محصول اصلی و جاری، پس از ذخیره سازی به حالت اولیه بازگشت داده نشده‌اند. برای این منظور متد reset را به کامپوننت ویرایش اطلاعات اضافه کرده:
reset(): void {
    this.dataIsValid = null;
    this.currentProduct = null;
    this.originalProduct = null;
  }
و سپس آن‌را به متد onSaveComplete، اضافه می‌کنیم:
  onSaveComplete(message?: string): void {
    if (message) {
      this.messageService.addMessage(message);
    }
    this.reset();

    // Navigate back to the product list
    this.router.navigate(['/products']);
  }


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نظرات مطالب
نوشتن Middleware سفارشی در ASP.NET Core

یک نکته‌ی تکمیلی: کار با اینترفیس IMiddleware جهت تعریف میان‌افزارهای سفارشی

اگر امروز قصد تعریف میان‌افزارهای سفارشی را دارید، بهتر است از روش باز public async Task Invoke(HttpContext context) که در این مطلب معرفی شد، دیگر استفاده نکنید؛ چون مهم‌ترین محدودیت‌های آن، داشتن طول عمر Singleton غیرقابل تغییر و همچنین عدم امکان پیاده سازی اینترفیس IDisposable در آن جهت پاکسازی خودکار منابع است. امروز روش توصیه شده‌، استفاده از اینترفیس IMiddleware است. در این حالت متد Task Invoke فوق، به متد مشخص و ثابت Task InvokeAsync(HttpContext context, RequestDelegate next) تغییر می‌کند. چون در این حالت دیگر نمی‌توان پارامترهای این متد مشخص را مانند قبل که اینترفیسی را پیاده سازی نمی‌کرد، به صورت پویا کم و زیاد کرد، می‌توان سرویس‌های مدنظر را به سازنده‌ی کلاس، تزریق کرد. به همین جهت نیاز است، آن‌را به نحو زیر به سیستم تزریق وابستگی‌ها معرفی کرد:

builder.Services.AddTransient<MyNewMiddleware>();

این الزام به تعریف آن به صورت یک سرویس رسمی، مزیت‌های زیر را به همراه دارد:

الف) می‌توان طول عمری، غیر از Singleton را هم در صورت نیاز، تعریف کرد (و مشکل کار با سرویس‌هایی با طول عمرهای غیر از Singleton کمتر می‌شود).

ب) چون طول عمر این میان‌افزار اکنون توسط سیستم تزریق وابستگی‌ها مدیریت می‌شود، اگر این میان‌افزار اینترفیس IDisposable را پیاده سازی کند، کار پاکسازی منابع آن خودکار خواهد شد.

نکته 1: روش معرفی آن به سیستم تزریق وابستگی‌ها، به صورت Concrete type است؛ یعنی اصل کلاس باید معرفی شود (مانند سطر فوق) و نه اینکه به صورت متداول زیر به همراه ذکر اینترفیس IMiddleware باشد:

builder.Services.AddTransient<IMiddleware, MyNewMiddleware>();

مابقی کار با آن، با میان‌افزارهای متداول، تفاوتی ندارد. یعنی قسمت UseMiddleware آن یکی است:

app.UseMiddleware<MyNewMiddleware>();

بنابراین این روش نسبت به روش متداول قبلی، دو تفاوت پیاده سازی اینترفیس مشخص IMiddleware و ثبت کلاس آن به صورت یک سرویس رسمی را دارد؛ مابقی نکات آن، مانند قبل است.

نکته 2: اگر از Scrutor برای ثبت خودکار سرویس‌های برنامه استفاده می‌کنید، روش ثبت خودکار اینگونه سرویس‌ها به صورت زیر و با استفاده از متد ()AsSelf است:

services.Scan(scan => scan.FromAssembliesOf(typeof(IDataSeedersRunner))
            .AddClasses(classes => classes.Where(type =>
            {
                var allInterfaces = type.GetInterfaces();

                return allInterfaces.Contains(typeof(IMiddleware)) && 
                       allInterfaces.Contains(typeof(ISingletonService));
            }))
            .AsSelf()
            .WithSingletonLifetime());

در این مثال، تمام IMiddleware هایی که با نشانگر ISingletonService هم مزین شده‌اند، یافت شده و به صورت Concrete type هایی، با طول عمر Singleton، به سیستم تزریق وابستگی‌ها اضافه می‌شوند.

نظرات مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
- خیر. یافتن یک کاربر بر اساس ID آن که کلید اصلی جدول را هم تشکیل می‌دهد و بر روی آن ایندکس وجود دارد، فوق العاده سریع است. یک چنین کوئری گرفتنی در ASP.NET Core Identity هم وجود دارد. البته ASP.NET Core Identity برای انجام اینکار، بیش از یک کوئری را صادر می‌کند.
- وجود ICookieValidatorService که سبب بروز این کوئری اضافه می‌شود، در جهت پیاده سازی logout آنی و بررسی به روز بودن محتوای کوکی دریافتی، ضروری هست. نیاز هست تا بتوان مقدار public string SerialNumber مطلب جاری را بین اطلاعات موجود در سرور و اطلاعات قبلی موجود در کوکی صادر شده‌ی در حین لاگین، مقایسه کرد. اگر این بررسی صورت نگیرد، تا زمانیکه طول عمر کوکی کاربر به پایان نرسیده، با موفقیت و بدون هیچ کوئری اضافه‌ای، اطلاعات موجود در آن، در سمت سرور اعتبارسنجی شده و استفاده می‌شود. از آن اطلاعات در جهت تامین خودکار user claims و roles کاربر جاری استفاده می‌شود (همان شیء this.User قابل دسترسی در اکشن متدها)؛ چون زیر ساخت ASP.NET Core در جهت بررسی اطلاعات کوکی رسیده، به همراه دریافت اطلاعات مجدد آن از بانک اطلاعاتی سمت سرور نیست. اگر کوکی را معتبر تشخیص دهد و هنوز طول عمر آن به پایان نرسیده باشد، از اطلاعات آن در جهت ساخت شیء this.User استفاده می‌کند. اینجا است که اهمیت وجودی ICookieValidatorService در جهت تامین یک لایه‌ی اضافی امنیتی بررسی محتوای کوکی دریافتی، مشخص می‌شود. برای مثال فرض کنید که کاربری را به دلیل خاصی غیرفعال یا ممنوع الورود کرده‌اید. اگر سرویس ICookieValidatorService را نداشته باشید، این کاربر تا زمانیکه طول عمر کوکی آن معتبر تشخیص داده شود، می‌تواند در سایت خراب کاری کند؛ چون اطلاعات کوکی او، شیء this.User معتبری را تولید می‌کند که وابسته‌ی به اطلاعات جدید بانک اطلاعاتی برنامه نیست. همچنین قرار دادن دستی بررسی دوباره‌ی اعتبار user claim حاصل از اطلاعات کوکی کاربر جاری در هر اکشن متد و مقایسه‌ی آن با اطلاعات جدید بانک اطلاعاتی، سبب شلوغ شدن و تکرار کدهای اضافه‌ای می‌شود که به صورت متمرکز و یکجا در ICookieValidatorService وجود دارد. نمونه‌ی دیگر کاربرد این سرویس، پیاده سازی خودکار مفهوم single sign out است؛ به این معنا که اگر کاربری logout کرد، بتوان با تغییر SerialNumber او در سمت سرور، سایر افرادی را که در همان لحظه از اکانت او (کوکی‌های معتبر او) در سیستم‌های دیگری استفاده می‌کنند، logout کرد.
- استفاده از کش، به همراه بحث invalidation دقیق آن هم در صورت بروز تغییرات در بانک اطلاعاتی هست. هستند کتابخانه‌هایی که این‌کارها را به صورت خودکار و بدون نیازی به تغییری در کدها برای شما انجام دهند.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهنده‌ها
یک نکته‌ی تکمیلی
شبیه سازی customErrors در نگارش‌های دیگر ASP.NET که در فایل web.config قابل تنظیم است:
<customErrors mode="On" defaultRedirect="error">
        <error statusCode="404" redirect="error/notfound" />
        <error statusCode="403" redirect="error/forbidden" />
</customErrors>
در ASP.NET Core چنین شکلی را پیدا می‌کند. ابتدا در متد Configure کلاس آغازین برنامه، میان افزارهای مطلب فوق را اضافه می‌کنیم:
        public void Configure(IApplicationBuilder app)
        {
            if (env.IsDevelopment())
            {
                app.UseDatabaseErrorPage();
                app.UseDeveloperExceptionPage();
            }
            app.UseExceptionHandler("/error/index/500");
            app.UseStatusCodePagesWithReExecute("/error/index/{0}");
در اینجا ذکر مسیر کامل اکشن متد Index و کنترلر Error ضروری هستند. سپس این کنترلر چنین محتوایی را خواهد داشت:
    public class ErrorController : Controller
    {
        private readonly ILogger<ErrorController> _logger;

        public ErrorController(ILogger<ErrorController> logger)
        {
            _logger = logger;
        }

        public IActionResult Index(int? id)
        {
            var logBuilder = new StringBuilder();

            var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
            logBuilder.AppendLine($"Error {id} for {Request.Method} {statusCodeReExecuteFeature?.OriginalPath ?? Request.Path.Value}{Request.QueryString.Value}\n");

            var exceptionHandlerFeature = this.HttpContext.Features.Get<IExceptionHandlerFeature>();
            if (exceptionHandlerFeature?.Error != null)
            {
                var exception = exceptionHandlerFeature.Error;
                logBuilder.AppendLine($"<h1>Exception: {exception.Message}</h1>{exception.StackTrace}");
            }

            foreach (var header in Request.Headers)
            {
                var headerValues = string.Join(",", value: header.Value);
                logBuilder.AppendLine($"{header.Key}: {headerValues}");
            }
            _logger.LogError(logBuilder.ToString());

            if (id == null)
            {
                return View("Error");
            }

            switch (id.Value)
            {
                case 401:
                case 403:
                    return View("AccessDenied");
                case 404:
                    return View("NotFound");

                default:
                    return View("Error");
            }
        }
    }
- در اینجا اگر UseExceptionHandler فعال شده باشد، امکان دسترسی به سرویس IExceptionHandlerFeature خواهد بود.
- و اگر UseStatusCodePagesWithReExecute فعال شده باشد، سرویس IStatusCodeReExecuteFeature اطلاعات مسیر اصلی درخواستی را ارائه می‌دهد.
- سپس بر اساس id ارسالی به این اکشن متد می‌توان برای مثال صفحه‌ی 404 (یافت نشد) و یا سایر صفحات دلخواه دیگری را به صورت انتخابی نمایش داد.
مطالب
مراحل ارتقاء پروژه‌های Angular از نسخه‌ی 6.0 به 7.0
مراحل ارتقاء پروژه‌های Angular از نگارش 6 به 7 آن به شرح زیر هستند:
1- به روز رسانی Angular CLI
ابتدا نیاز است نگارش قبلی را حذف و سپس نگارش جدید را نصب کنید:
npm uninstall -g @angular/cli
npm cache verify
# if npm version is < 5 then use `npm cache clean` 
npm install -g @angular/cli@latest
البته Angular 7 پشتیبانی از  Node 10 را اضافه کرده است (بیشتر؛ دانلود Node). بنابراین پیش از اجرای دستورات فوق بهتر است NodeJS خود را نیز به روز کنید:
npm i -g npm

2- به روز رسانی RxJS (اگر در نگارش 6 آن‌را تکمیل نکرده‌اید)
1-تعویض کردن HttpModule با HttpClientModule و سرویس Http با سرویس HttpClient
2-حذف کردن ویژگی‌های منسوخ شده از RxJS 6 با اجرای دستور زیر:
npm install -g rxjs-tslint
rxjs-5-to-6-migrate -p src/tsconfig.app.json
3-حذف rxjs-compat بعد از بروزرسانی RxJS 6
 
3- به روز رسانی TypeScript
Angular 7 از تایپ اسکریپت 3.1 استفاده می‌کند (بیشتر). به همین جهت نیاز است وابستگی‌های سراسری سیستم خود را مانند TypeScript، پس از نصب CLI جدید، به نحو زیر به روز کنید:
npm update -g

4- به روز رسانی وابستگی‌های اصلی پروژه
برای به‌‌روز رسانی به نسخه 7 (core framework و CLI)، دستورات زیر را اجرا کنید:
ng update @angular/cli
ng update @angular/core
ng update rxjs
و اگر از Angular Material نیز استفاده می‌کنید، نیاز به اجرای دستور زیر را نیز خواهید داشت:
ng update @angular/material
اگر در اینجا خطای  peer dependency را مشاهده کردید، از سوئیچ force-- در انتهای دستورات، استفاده کنید.
و یا به صورت خلاصه دستور زیر تمام مراحل فوق را به صورت یکجا انجام می‌دهد:
 ng update --all --force
 
5- به روز رسانی Service worker 

اگر شما از Service worker  مربوط به Angular استفاده می‌کنید، بجای versionedFiles از files استفاده کنید. رفتار همان است و تغییر نکرده‌است.
 
6- به روز رسانی وابستگی‌های ثالث پروژه
برای به روز رسانی سایر وابستگی‌های پروژه‌، می‌توان از بسته‌ی npm-check-updates استفاده کرد:
npm install npm-check-updates -g
ncu -u
npm install
دستور دوم تمام شماره نگارش‌های بسته‌های موجود در فایل package.json را به صورت خودکار به آخرین نگارش آن‌ها روز رسانی می‌کند و دستور سوم این بسته‌های جدید را دریافت و نصب خواهد کرد.  
مطالب
3 نکته کاربردی TypeScript برای Angular
اگر چه من  این نکات را در حین کار کردن بر روی پروژه‌های انگیولار یافتم، اما همه آنها مشخصه‌های انگیولار نیستند؛ فقط کد‌های تایپ اسکریپت می‌باشند.


Eliminating the need to import interfaces 

من interface ‌ها را دوست دارم با این حال نمی‌خواهم هر بار آنها را import  کنم. تمایلی ندارم فایل‌های من بخاطر import ‌های چند خطی کثیف شوند (فقط به منظور strong typing )، اگر چه Visual Studio Code ویژگی auto-import را دارا می‌باشد. در حالت معمول به صورت زیر کار می‌کنیم :
// api.model.ts
export interface Customer {
    id: number;
    name: string;
}

export interface User {
    id: number;
    isActive: boolean;
}
و استفاده از آن  :
// using the interfaces
import { Customer, User } from './api.model'; 

export class MyComponent {
    cust: Customer; 
}
اگر به خط شماره 2 دقت کنید ، در صورتی که interface ‌های بیشتری استفاده شود، این خط بزرگتر می‌شود و کمی به‌هم ریختگی ایجاد می‌کند.

راه حل شماره 1 : استفاده از namespace 

با استفاده از namespaceها می‌توان نیاز به import فایل‌های interface را حذف کرد: 
// api.model.ts
namespace ApiModel {
    export interface Customer {
        id: number;
        name: string;
    }

    export interface User {
        id: number;
        isActive: boolean;
    }
}
و استفاده از آن :
// using the interfaces
export class MyComponent {
    cust: ApiModel.Customer; 
}
هم چنین با استفاده از namespaceها می‌توان خیلی بهتر interface‌ها را سازماندهی و گروه بندی کرد.

اجازه بدهید بگویم اگر شما فایل دیگری به نام مثلا api.v2.model.ts را داشته باشید و بخواهید interface ‌های جدیدی را به این فایل اضافه کنید، می‌توانید از همان نام namespace مشخص شده قبلی استفاده کنید:
// api.v2.model.ts
namespace ApiModel {
    export interface Order {
        id: number;
        total: number;
    }
}

برای استفاده از interface  جدیدا ایجاد شده، می‌توانید همانند مثال قبل کار کنید: ( استفاده از interface ‌ها با namespace های یکسان و فایل‌های متفاوت)
export class MyComponent {
    cust: ApiModel.Customer; 
    order: ApiModel.Order;
}

راه حل شماره 2 : استفاده از فایل‌های d

راه دیگر حذف کردن import ها، ایجاد یک فایل typescript  با پسوند d.ts. می‌باشد ( d مخفف declaration file در typescript می‌باشد). در ضمن نیازی به export کردن interface ‌ها در فایل‌های d نمی‌باشد:
// api.model.d.ts
interface Customer {
    id: number;
    name: string;
}
در زیر از Customer  بدون نیاز به import کردن استفاده شده است :
// using the interfaces of d file
export class MyComponent {
    cust: Customer; 
}

من راه حل شماره 1 را در مقابل راه حل شماره 2 پیشنهاد می‌کنم زیرا :
از فایل d معمولا برای کتابخانه‌های ثالث استفاده می‌شود.
namespace ‌ها این اجازه را به ما میدهند تا فایل‌ها را خیلی بهتر سازماندهی کنیم.
 
Making all interface properties optional  

این موضوع خیلی رایج است که شما از یک interface برای عملیات CRUD استفاده کنید. مثلا شما یک interface را به نام customer دارید که تمامی فیلد‌های آن در زمان ایجاد اجباری می‌باشند؛ اما در زمان به‌روز رسانی، همه آنها اختیاری هستند. آیا شما نیاز به دو interface برای مدیریت کردن این سناریو دارید؟ 

راه حل : استفاده از Partial 

Partial یک type است که خصوصیات یک شیء را اختیاری می‌کند. تعریف آن در فایل d پیش فرضی به نام lib.es5.d.ts قرار داده شده‌است:
// lib.es5.d.ts
type Partial<T> = {
    [P in keyof T]?: T[P];
};
چگونه می‌توان از آن استفاده کرد؟ به کدهای زیر توجه کنید: 
import { Customer } from './api.model';

export class MyComponent {
    cust: Partial<Customer>;  /

    ngOninit() {
        this.cust = { name: 'jane' };
    }
}

در کد بالا هیچ خطایی پرتاب نمی‌شود؛ به دلیل اینکه همه‌ی فیلد‌ها اختیاری شده‌اند (اگر Partial را حذف کنیم با خطا مواجه می‌شویم). 
اگر شما تعریف Partial را پیدا نکردید، می‌توانید یک فایل d را ایجاد کنید؛ مثلا با نام util.d.ts، سپس قطعه کد تعریف Partial را در آن قرار دهید.

Stop throwing me error, I know what I'm doing 

در بعضی از سناریو‌ها شما می‌خواهید به Typescript  بگویید که  "من می‌دانم دارم چکار می‌کنم لطفا اینجا بی خیال من شو".

راه حل : استفاده از ts-ignore comment@ 

از نگارش 2.6 به بعد Typescript، شما می‌توانید این کار را به منظور مانع شدن از ایجاد خطا  با استفاده از کامنت ts-ignore@  انجام دهید. 
برای مثال در کد زیر، Typescript خطای Unreachable code detected را پرتاب خواهد کرد:
if (false) {
    console.log('x');
}

شما می‌توانید با استفاده از کامنت ts-ignore@ مانع از پرتاب خطا شوید :
if (false) {
    // @ts-ignore
    console.log('x');
}

البته من به شما پیشنهاد می‌کنم که همیشه خطاهاتون را برطرف کنید؛ قبل از اینکه آن‌ها را ignore کنید. 
مطالب
CoffeeScript #12

بخش‌های بد

جاوااسکریپت یک زبان پیچیده است که شما برای کار با آن، نیاز است قسمت‌هایی را که باید از آن‌ها دوری کنید و قسمت‌های مهمی را که باید استفاده کنید، بشناسید. همانطور که Sun Tzu گفته "دشمن خود را بشناس"، ما نیز در این قسمت می‌خواهیم برای شناخت بیشتر قسمت‌های تاریک و روشن جاوااسکریپت به آن بپردازیم.

همانطور که در قسمت‌های قبل گفته شد، CoffeeScript تنها به یک syntax محدود نمی‌شود و توانایی برطرف کردن برخی از مشکلات جاوااسکریپت را نیز دارد. با این حال، با توجه به این واقعیت که کدهای CoffeeScript به صورت مستقیم به جاوااسکریپت تبدیل می‌شوند و نمی‌توانند تمامی مشکلاتی را که در جاوااسکریپت وجود دارند، حل کنند، پس برخی از مسائل وجود دارند که شما باید از آنها آگاهی داشته باشید.

اول از قسمت‌هایی که توسط CoffeeScript حل شده‌اند شروع می‌کنیم.

A JavaScript Subset

with یک دستور بسیار زمانبر است و مضر شناخته شده است و نباید از آن استفاده کنید. with با ایجاد یک ساختار خلاصه نویسی، برای جستجو بر روی خصوصیات اشیاء در نظر گرفته شده بود. برای نمونه به جای نوشتن:

dataObj.users.vahid.email = "info@vmt.ir";
می‌توانید به این صورت این کار را انجام دهید:
with(dataObj.users.vahid) {
  email = "info@vmt.ir";
}
مفسر جاوااسکریپت دقیقا نمی‌داند که شما می‌خواهید چه کاری را با with انجام دهید، و به شیء مشخص شده فشار می‌آورد تا اول اسم همه مراجعه شده‌ها را جستجو کند. این عمل واقعا به عملکرد و کارآیی لطمه می‌زند. یعنی مترجم، تمام انواع بهینه سازی‌های JIT را خاموش می‌کند. همچنین پیشنهادهایی مبنی بر حذف کامل آن از نسخه‌های بعدی جاوااسکریپت نیز مطرح شده است.
همه چیز برای عدم استفاده از with در نظر گرفته شده است. CoffeeScript یک قدم جلوتر از همه برداشته و with را از syntax خود حذف کرده است. به عبارت دیگر در صورتیکه شما از آن استفاده کنید، کامپایلر CoffeeScript خطا صادر می‌کند.

Global variables

به طور پیش فرض تمامی برنامه‌های جاوااسکریپت در دامنه global اجرا می‌شوند و تمامی متغیرهایی که ساخته می‌شوند به طور پیش فرض در ناحیه‌ی global قرار می‌گیرند. اگر شما بخواهید متغیری را در ناحیه‌ی local ایجاد کنید، باید از کلمه کلیدی var استفاده کنید.

usersCount = 1;        // Global
var groupsCount = 2;   // Global

(function(){              
  pagesCount = 3;      // Global
  var postsCount = 4;  // Local
})()
اکثر اوقات شما می‌خواهید متغیر local ایی را ایجاد کنید و نه global. توسعه دهندگان باید همیشه به یاد داشته باشند که قبل از مقداردهی اولیه‌ی هر متغیری، کلمه‌ی کلیدی var را قرار دهند یا با انواع و اقسام مشکلات، هنگامی که متغیرها به طور تصادفی با یکدیگر برخورد و یا بازنویسی بر روی یکدیگر انجام می‌دهند، روبرو شوند.
خوشبختانه CoffeeScript به کمک شما می‌آید و به طور کامل انتساب متغیرهای global را به طور ضمنی از بین می‌برد. به عبارت دیگر کلمه کلیدی var در CoffeeScript رزرو شده است و در صورت استفاده خطا صادر می‌شود.
به صورت پیش فرض به طور ضمنی متغیرها local ایجاد می‌شوند و خیلی سخت می‌شود متغیر global ایی را بدون انتساب آن به عنوان خصوصیتی از شیء window ایجاد کرد.
outerScope = true
do ->
  innerScope = true
نتیجه‌ی کامپایل آن می‌شود:
var outerScope;
outerScope = true;
(function() {
  var innerScope;
  return innerScope = true;
})();
همانطور که مشاهده می‌کنید CoffeeScript مقداردهی اولیه متغیر را (با استفاده از var) به صورت خودکار در context ایی که برای اولین بار استفاده شده است انجام می‌دهد. باید مواظب باشید تا از نام متغیر خارجی مجددا استفاده نکنید که این اتفاق ممکن است در کلاس یا تابع با عمق زیاد ایجاد شود. برای مثال، در اینجا به صورت تصادفی متغیر package در یک تابع کلاس بازنویسی شده است:
package = require('./package')

class Test
  build: ->
    # Overwrites outer variable!
    package = @testPackage.compile()

  testPackage: ->
    package.create()
برای ایجاد متغیرهای global باید از انتساب آنها به عنوان خصوصیتی از شیء window استفاده کرد.
  class window.Asset
    constructor: ->
با تضمین متغیرهای global به صورت صریح و روشن به جای به طور ضمنی بودن آنها، CoffeeScript یکی از منابع اصلی ایجاد مشکلات در جاوااسکریپت را حذف کرد‌ه‌است.

Semicolons

جاوااسکریپت اجباری برای نوشتن ";" ندارد، بنابراین ممکن است یک سری از دستورات از قلم بیافتند. با این حال در پشت صحنه‌ی کامپایلر جاوااسکریپت به ";" احتیاج دارد. به طوری که parser جاوااسکریپت به صورت خودکار هر زمانی که نتواند ارزیابی از دستورات داشته باشد، یک بار دیگر با ";" این کار را انجام می‌دهد و درصورت موفقیت، پیام خطایی مبنی بر نبود ";" را صادر می‌کند.
متاسفانه این یک ایده بد است. چرا که ممکن است تغییر رفتاری در کد نوشته شده به وجود آید. به مثال زیر توجه کنید. به نظر کد نوشته شده صحیح است؛ درسته؟
function() {}
(window.options || {}).property
اشتباه است، حداقل با توجه به parser، یک خطای syntax صادر می‌شود. در مورد دوم نیز parser، ";" اضافه نمی‌کند و کد نوشته شده به کد یک خطی تبدیل می‌شوند.
function() {}(window.options || {}).property
حالا شما می‌توانید این موضوع را ببینید که چرا parser خطا داده‌است. وقتی شما در حال نوشتن کد جاوااسکریپتی هستید، باید بعد از هر دستور از ";" استفاده کنید. خوشبختانه در تمام زمانیکه درحال نوشتن کد CoffeeScript هستید، نیازی به نوشتن ";" ندارید. در زمانیکه کد CoffeeScript نوشته شده کامپایل می‌شود، به صورت خودکار ";" را در جای مناسبی قرار می‌دهد.

مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی
در قسمت قبل، ساختار ابتدایی کلاینت Angular را تدارک دیدیم. در این قسمت قصد داریم سرویسی که زیر ساخت کامپوننت لاگین و عملیات ورود به سیستم را تامین می‌کند، تکمیل کنیم.


تعریف تزریق وابستگی تنظیمات برنامه

در مطلب «تزریق وابستگی‌ها فراتر از کلاس‌ها در برنامه‌های Angular» با روش تزریق ثوابت برنامه آشنا شدیم. در این مثال، برنامه‌ی کلاینت بر روی پورت 4200 اجرا می‌شود و برنامه‌ی سمت سرور وب، بر روی پورت 5000. به همین جهت نیاز است این آدرس پایه سمت سرور را در تمام قسمت‌های برنامه که با سرور کار می‌کنند، در دسترس داشته باشیم و روش مناسب برای پیاده سازی آن همان قسمت «تزریق تنظیمات برنامه توسط تامین کننده‌ی مقادیر» مطلب یاد شده‌است. به همین جهت فایل جدید src\app\core\services\app.config.ts را در پوشه‌ی core\services برنامه ایجاد می‌کنیم:
import { InjectionToken } from "@angular/core";

export let APP_CONFIG = new InjectionToken<string>("app.config");

export interface IAppConfig {
  apiEndpoint: string;
  loginPath: string;
  logoutPath: string;
  refreshTokenPath: string;
  accessTokenObjectKey: string;
  refreshTokenObjectKey: string;
}

export const AppConfig: IAppConfig = {
  apiEndpoint: "http://localhost:5000/api",
  loginPath: "account/login",
  logoutPath: "account/logout",
  refreshTokenPath: "account/RefreshToken",
  accessTokenObjectKey: "access_token",
  refreshTokenObjectKey: "refresh_token"
};
در اینجا APP_CONFIG یک توکن منحصربفرد است که از آن جهت یافتن مقدار AppConfig که از نوع اینترفیس IAppConfig تعریف شده‌است، در سراسر برنامه استفاده خواهیم کرد.
سپس تنظیمات ابتدایی تزریق وابستگی‌های IAppConfig را در فایل src\app\core\core.module.ts به صورت ذیل انجام می‌دهیم:
import { AppConfig, APP_CONFIG } from "./app.config";

@NgModule({
  providers: [
    { provide: APP_CONFIG, useValue: AppConfig }
  ]
})
export class CoreModule {}
اکنون هر سرویس و یا کامپوننتی در سراسر برنامه که نیاز به تنظیمات AppConfig را داشته باشد، کافی است با استفاده از ویژگی Inject(APP_CONFIG)@ آن‌را درخواست کند.


طراحی سرویس Auth

پس از لاگین باید بتوان به اطلاعات اطلاعات کاربر وارد شده‌ی به سیستم، در تمام قسمت‌های برنامه دسترسی پیدا کرد. به همین جهت نیاز است این اطلاعات را در یک سرویس سراسری singleton قرار داد تا همواره یک وهله‌ی از آن در کل برنامه قابل استفاده باشد. مرسوم است این سرویس را AuthService بنامند. بنابراین محل قرارگیری این سرویس سراسری در پوشه‌ی Core\services و محل تعریف آن در قسمت providers آن خواهد بود. به همین جهت ابتدا ساختار این سرویس را با دستور ذیل ایجاد می‌کنیم:
 ng g s Core/services/Auth
با این خروجی:
   create src/app/Core/services/auth.service.ts (110 bytes)
و سپس تعریف آن‌را به مدخل providers ماژول Core اضافه می‌کنیم:
import { AuthService } from "./services/auth.service";

@NgModule({
  providers: [
    // global singleton services of the whole app will be listed here.
    BrowserStorageService,
    AuthService,
    { provide: APP_CONFIG, useValue: AppConfig }
  ]
})
export class CoreModule {}

در ادامه به تکمیل AuthService خواهیم پرداخت و قسمت‌های مختلف آن‌را مرور می‌کنیم.


اطلاع رسانی به کامپوننت Header در مورد وضعیت لاگین

در مطلب «صدور رخدادها از سرویس‌ها به کامپوننت‌ها در برنامه‌های Angular» با نحوه‌ی کار با BehaviorSubject آشنا شدیم. در اینجا می‌خواهیم توسط آن، پس از لاگین موفق، وضعیت لاگین را به کامپوننت هدر صادر کنیم، تا لینک لاگین را مخفی کرده و لینک خروج از سیستم را نمایش دهد:
import { BehaviorSubject } from "rxjs/BehaviorSubject";

@Injectable()
export class AuthService {

  private authStatusSource = new BehaviorSubject<boolean>(false);
  authStatus$ = this.authStatusSource.asObservable();

  constructor() {
    this.updateStatusOnPageRefresh();
  }

  private updateStatusOnPageRefresh(): void {
    this.authStatusSource.next(this.isLoggedIn());
  }
اکنون تمام کامپوننت‌های برنامه می‌توانند مشترک $authStatus شده و همواره آخرین وضعیت لاگین را دریافت کنند و نسبت به تغییرات آن عکس العمل نشان دهند (برای مثال قسمتی را نمایش دهند و یا قسمتی را مخفی کنند).
در اینجا در سازنده‌ی کلاس، بر اساس خروجی متد وضعیت لاگین شخص، برای اولین بار، متد next این BehaviorSubject فراخوانی می‌شود. علت قرار دادن این متد در سازنده‌ی کلاس سرویس، عکس العمل نشان دادن به refresh کامل صفحه، توسط کاربر است و یا عکس العمل نشان دادن به وضعیت به‌خاطر سپاری کلمه‌ی عبور، در اولین بار مشاهده‌ی سایت و برنامه. در این حالت متد isLoggedIn، کش مرورگر را بررسی کرده و با واکشی توکن‌ها و اعتبارسنجی آن‌ها، گزارش وضعیت لاگین را ارائه می‌دهد. پس از آن، خروجی آن (true/false) به مشترکین اطلاع رسانی می‌شود.
در ادامه، متد next این  BehaviorSubject را در متدهای login و logout نیز فراخوانی خواهیم کرد.


تدارک ذخیره سازی توکن‌ها در کش مرورگر

از طرف سرور، دو نوع توکن access_token و refresh_token را دریافت می‌کنیم. به همین جهت یک enum را جهت مشخص سازی آن‌ها تعریف خواهیم کرد:
export enum AuthTokenType {
   AccessToken,
   RefreshToken
}
سپس باید این توکن‌ها را پس از لاگین موفق در کش مرورگر ذخیره کنیم که با مقدمات آن در مطلب «ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular» پیشتر آشنا شدیم. از همان سرویس BrowserStorageService مطلب یاد شده، در اینجا نیز استفاده خواهیم کرد:
import { BrowserStorageService } from "./browser-storage.service";

export enum AuthTokenType {
  AccessToken,
  RefreshToken
}

@Injectable()
export class AuthService {

  private rememberMeToken = "rememberMe_token";

  constructor(private browserStorageService: BrowserStorageService) {  }

  rememberMe(): boolean {
    return this.browserStorageService.getLocal(this.rememberMeToken) === true;
  }

  getRawAuthToken(tokenType: AuthTokenType): string {
    if (this.rememberMe()) {
      return this.browserStorageService.getLocal(AuthTokenType[tokenType]);
    } else {
      return this.browserStorageService.getSession(AuthTokenType[tokenType]);
    }
  }

  deleteAuthTokens() {
    if (this.rememberMe()) {
      this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.AccessToken]);
      this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.RefreshToken]);
    } else {
      this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.AccessToken]);
      this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.RefreshToken]);
    }
    this.browserStorageService.removeLocal(this.rememberMeToken);
  }

  private setLoginSession(response: any): void {
    this.setToken(AuthTokenType.AccessToken, response[this.appConfig.accessTokenObjectKey]);
    this.setToken(AuthTokenType.RefreshToken, response[this.appConfig.refreshTokenObjectKey]);
  }

  private setToken(tokenType: AuthTokenType, tokenValue: string): void {
    if (this.rememberMe()) {
      this.browserStorageService.setLocal(AuthTokenType[tokenType], tokenValue);
    } else {
      this.browserStorageService.setSession(AuthTokenType[tokenType], tokenValue);
    }
  }
}
ابتدا سرویس BrowserStorageService به سازنده‌ی کلاس تزریق شده‌است و سپس نیاز است بر اساس گزینه‌ی «به‌خاطر سپاری کلمه‌ی عبور»، نسبت به انتخاب محل ذخیره سازی توکن‌ها اقدام کنیم. اگر گزینه‌ی rememberMe توسط کاربر در حین لاگین انتخاب شود، از local storage ماندگار و اگر خیر، از session storage فرار مرورگر برای ذخیره سازی توکن‌ها و سایر اطلاعات مرتبط استفاده خواهیم کرد.


- متد rememberMe مشخص می‌کند که آیا وضعیت به‌خاطر سپاری کلمه‌ی عبور توسط کاربر انتخاب شده‌است یا خیر؟ این مقدار را نیز در local storage ماندگار ذخیره می‌کنیم تا در صورت بستن مرورگر و مراجعه‌ی مجدد به آن، در دسترس باشد و به صورت خودکار پاک نشود.
- متد setToken، بر اساس وضعیت rememberMe، مقادیر توکن‌های دریافتی از سرور را در local storage و یا session storage ذخیره می‌کند.
- متد getRawAuthToken بر اساس یکی از مقادیر enum ارسالی به آن، مقدار خام access_token و یا refresh_token ذخیره شده را بازگشت می‌دهد.
- متد deleteAuthTokens جهت حذف تمام توکن‌های ذخیره شده‌ی توسط برنامه استفاده خواهد شد. نمونه‌ی کاربرد آن در متد logout است.
- متد setLoginSession پس از لاگین موفق فراخوانی می‌شود. کار آن ذخیره سازی توکن‌های دریافتی از سرور است. فرض آن نیز بر این است که خروجی json از طرف سرور، توکن‌ها را با کلیدهایی دقیقا مساوی access_token و refresh_token بازگشت می‌دهد:
 {"access_token":"...","refresh_token":"..."}
اگر این کلیدها در برنامه‌ی شما نام دیگری را دارند، محل تغییر آن‌ها در فایل app.config.ts است.


تکمیل متد ورود به سیستم

در صفحه‌ی لاگین، کاربر نام کاربری، کلمه‌ی عبور و همچنین گزینه‌ی «به‌خاطر سپاری ورود» را باید تکمیل کند. به همین جهت اینترفیسی را برای این کار به نام Credentials در محل src\app\core\models\credentials.ts ایجاد می‌کنیم:
export interface Credentials {
   username: string;
   password: string;
   rememberMe: boolean;
}
پس از آن در متد لاگین از این اطلاعات جهت دریافت توکن‌های دسترسی و به روز رسانی، استفاده خواهیم کرد:
@Injectable()
export class AuthService {
  constructor(
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
    private http: HttpClient,
    private browserStorageService: BrowserStorageService   
  ) {
    this.updateStatusOnPageRefresh();
  }

  login(credentials: Credentials): Observable<boolean> {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .post(`${this.appConfig.apiEndpoint}/${this.appConfig.loginPath}`, credentials, { headers: headers })
      .map((response: any) => {
        this.browserStorageService.setLocal(this.rememberMeToken, credentials.rememberMe);
        if (!response) {
          this.authStatusSource.next(false);
          return false;
        }
        this.setLoginSession(response);
        this.authStatusSource.next(true);
        return true;
      })
      .catch((error: HttpErrorResponse) => Observable.throw(error));
  }
}
متد login یک Observable از نوع boolean را بازگشت می‌دهد. به این ترتیب می‌توان مشترک آن شد و در صورت دریافت true یا اعلام لاگین موفق، کاربر را به صفحه‌ای مشخص هدایت کرد.
در اینجا نیاز است اطلاعات شیء Credentials را به مسیر http://localhost:5000/api/account/login ارسال کنیم. به همین جهت نیاز به سرویس IAppConfig تزریق شده‌ی در سازنده‌ی کلاس وجود دارد تا با دسترسی به this.appConfig.apiEndpoint، مسیر تنظیم شده‌ی در فایل src\app\core\services\app.config.ts را دریافت کنیم.
پس از لاگین موفق:
- ابتدا وضعیت rememberMe انتخاب شده‌ی توسط کاربر را در local storage مرورگر جهت مراجعات آتی ذخیره می‌کنیم.
- سپس متد setLoginSession، توکن‌های دریافتی از شیء response را بر اساس وضعیت rememberMe در local storage ماندگار و یا session storage فرار، ذخیره می‌کند.
- در آخر با فراخوانی متد next مربوط به authStatusSource با پارامتر true، به تمام کامپوننت‌های مشترک به این سرویس اعلام می‌کنیم که وضعیت لاگین موفق بوده‌است و اکنون می‌توانید نسبت به آن عکس العمل نشان دهید.


تکمیل متد خروج از سیستم

کار خروج، با فراخوانی متد logout صورت می‌گیرد:
@Injectable()
export class AuthService {

  constructor(
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
    private http: HttpClient,
    private router: Router
  ) {
    this.updateStatusOnPageRefresh();
  }

  logout(navigateToHome: boolean): void {
    this.http
      .get(`${this.appConfig.apiEndpoint}/${this.appConfig.logoutPath}`)
      .finally(() => {
        this.deleteAuthTokens();
        this.unscheduleRefreshToken();
        this.authStatusSource.next(false);
        if (navigateToHome) {
          this.router.navigate(["/"]);
        }
      })
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        console.log("logout", result);
      });
  }
}
در اینجا در ابتدا متد logout سمت سرور که در مسیر http://localhost:5000/api/account/logout قرار دارد فراخوانی می‌شود. پس از آن در پایان کار در متد finally (چه عملیات فراخوانی logout سمت سرور موفق باشد یا خیر)، ابتدا توسط متد deleteAuthTokens تمام توکن‌ها و اطلاعات ذخیره شده‌ی در مرورگر حذف می‌شوند. در ادامه با فراخوانی متد next مربوط به authStatusSource با مقدار false، به تمام مشترکین سرویس جاری اعلام می‌کنیم که اکنون وقت عکس العمل نشان دادن به خروجی سیستم و به روز رسانی رابط کاربری است. همچنین اگر پارامتر navigateToHome نیز مقدار دهی شده بود، کاربر را به صفحه‌ی اصلی برنامه هدایت می‌کنیم.


اعتبارسنجی وضعیت لاگین و توکن‌های ذخیره شده‌ی در مرورگر

برای اعتبارسنجی access token دریافتی از طرف سرور، نیاز به بسته‌ی jwt-decode است. به همین جهت دستور ذیل را در خط فرمان صادر کنید تا بسته‌ی آن به پروژه اضافه شود:
 > npm install jwt-decode --save
در ادامه برای استفاده‌ی از آن، ابتدا بسته‌ی آن‌را import می‌کنیم:
 import * as jwt_decode from "jwt-decode";
و سپس توسط متد jwt_decode آن می‌توان به اصل اطلاعات توکن دریافتی از طرف سرور، دسترسی یافت:
  getDecodedAccessToken(): any {
    return jwt_decode(this.getRawAuthToken(AuthTokenType.AccessToken));
  }
این توکن خام، پس از decode، یک چنین فرمت نمونه‌ای را دارد که در آن، شماره‌ی کاربری (nameidentifier)، نام کاربری (name)، نام نمایشی کاربر (DisplayName)، نقش‌های او (قسمت role) و اطلاعات تاریخ انقضای توکن (خاصیت exp)، مشخص هستند:
{
  "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a",
  "iss": "http://localhost/",
  "iat": 1513070340,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid",
  "DisplayName": "وحید",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
    "Admin",
    "User"
  ],
  "nbf": 1513070340,
  "exp": 1513070460,
  "aud": "Any"
}
برای مثال اگر خواستیم به خاصیت DisplayName این شیء decode شده دسترسی پیدا کنیم، می‌توان به صورت ذیل عمل کرد:
  getDisplayName(): string {
    return this.getDecodedAccessToken().DisplayName;
  }
و یا خاصیت exp آن، بیانگر تاریخ انقضای توکن است. برای تبدیل آن به نوع Date، ابتدا باید به این خاصیت در توکن decode شده دسترسی یافت و سپس توسط متد setUTCSeconds آن‌را تبدیل به نوع Date کرد:
  getAccessTokenExpirationDateUtc(): Date {
    const decoded = this.getDecodedAccessToken();
    if (decoded.exp === undefined) {
      return null;
    }
    const date = new Date(0); // The 0 sets the date to the epoch
    date.setUTCSeconds(decoded.exp);
    return date;
  }
اکنون که به این تاریخ انقضای توکن دسترسی یافتیم، می‌توان از آن جهت تعیین اعتبار توکن ذخیره شده‌ی در مرورگر، استفاده کرد:
  isAccessTokenTokenExpired(): boolean {
    const expirationDateUtc = this.getAccessTokenExpirationDateUtc();
    if (!expirationDateUtc) {
      return true;
    }
    return !(expirationDateUtc.valueOf() > new Date().valueOf());
  }
و در آخر متد isLoggedIn که وضعیت لاگین بودن کاربر جاری را مشخص می‌کند، به صورت ذیل تعریف می‌شود:
  isLoggedIn(): boolean {
    const accessToken = this.getRawAuthToken(AuthTokenType.AccessToken);
    const refreshToken = this.getRawAuthToken(AuthTokenType.RefreshToken);
    const hasTokens = !this.isEmptyString(accessToken) && !this.isEmptyString(refreshToken);
    return hasTokens && !this.isAccessTokenTokenExpired();
  }

  private isEmptyString(value: string): boolean {
    return !value || 0 === value.length;
  }
ابتدا بررسی می‌کنیم که آیا توکن‌های درخواست شده‌ی از کش مرورگر، وجود خارجی دارند یا خیر؟ پس از آن تاریخ انقضای access token را نیز بررسی می‌کنیم. تا همین اندازه جهت تعیین اعتبار این توکن‌ها در سمت کاربر کفایت می‌کنند. در سمت سرور نیز این توکن‌ها به صورت خودکار توسط برنامه تعیین اعتبار شده و امضای دیجیتال آن‌ها بررسی می‌شوند.

در قسمت بعد، از این سرویس اعتبارسنجی تکمیل شده جهت ورود به سیستم و تکمیل کامپوننت header استفاده خواهیم کرد.


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
مطالب
ساختن code snippet ( قطعه کد ) در ویژوال استودیو 2010
ویژوال استودیو به توسعه دهندگان این امکان را می‌دهد تا کدهایی را که تکراری بوده و به  دفعات در متن برنامه مورد استفاده هستند به شکل یک قطعه کد آماده (در صورت نیاز با مقادیر پیش فرض ) ذخیره کنند ، سپس در مواقع نیاز بدون اینکه مجبور باشند آن را دوباره و دوباره بنویسند ، تنها با تایپ کردن نام قطعه کد ذخیره شده  و دو بار فشردن  کلید Tab ، کد  تعریف شده توسط ویژوال استودیو  در محل تعیین شده اضافه می‌گردد. به این  قطعه کدهای آماده code snippet گفته می‌شود .
خود ویژوال استودیو تعدادی code snippet آماده  دارد که آشنایی با آنها می‌تواند سرعت کدنویسی را افزایش دهد . برای دیدن لیست کامل و مدیریت آنها به مسیر

Tools -> Code Snippets Manager (Ctrl+K,Ctrl+B)
بروید .

در ویژوال استودیو 2010 دو نوع snippet وجود دارد :

 1- Expansion snippets : که در محل کرسر (Cursor) اضافه می‌شوند . مثل cw و enum که به ترتیب دستور writeLine و ساختار یک enum را ایجاد می‌کنند .

 2- SurroundsWith snippets : که  می‌توانند یک تکه کد انتخاب شده را در بر بگیرند مثل for و یا do که کد انتخاب شده را در یک حلقه for و do-while محصور می‌کنند  .

نکته ای که باید توجه داشت این است که یک snippet می‌تواند از هر دو نوع باشد . برای مثال  for و do و یا if ، در صورتی که  کدی انتخاب شده باشد آن را محصور می‌کنند و گرنه  ساختار خالی مرتبط را در محل cursor اضافه می‌کنند .

همانطور که در ابتدا هم ذکر شد ، علاوه بر snippet‌های آماده‌ی موجود ، توسعه دهنده می‌تواند قطعه کدهایی را خود ایجاد کرده و مورد استفاده قرار دهد .

در اینجا یک expansion snippet  خواهیم ساخت تا  کار اضافه کردن بلاک try-catch-finally  را برای ما انجام دهد .

- ابتدا یک فایل xml به پروژه اضافه می‌کنیم و آنرا TryCatchFinally.snippet می‌نامیم . توجه کنید که نام فایل باید به .snippet ختم شود .

- فایل را باز  و درون آن راست کلیک کرده و گزینه Insert snippet > Snippet را انتخاب می‌کنیم . با اینکار یک قالب پایه snippet ( که یک ساختار xml ) است به فایل اضافه می‌شود .  هر فایل snippet از دو بخش اصلی header و  snippet تشکیل شده که بخش header اطلاعاتی کلی درباره قطعه کد را نگهداری می‌کند و بخش snippet مربوط به تعریف محتوای قطعه کد است .

<codesnippet format="1.0.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <header>
    <title>title</title>
    <author>author</author>
    <shortcut>shortcut</shortcut>
    <description>description</description>
    <snippettypes>
      <snippettype>SurroundsWith</snippettype>
      <snippettype>Expansion</snippettype>
    </snippettypes>
  </header>
  <snippet>
    <declarations>
      <literal>
        <id>name</id>
        <default>value</default>
      </literal>
    </declarations>
    <code language="XML">
      <!--[CDATA[<test-->
      <name>$name$</name>
      $selected$ $end$]]>
    </code>
  </snippet>
</codesnippet>
 

- قالب پیش فرض شامل هر دو نوع snippet است .


<codesnippet format="1.0.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <header>
...
    <snippettypes>
      <snippettype>SurroundsWith</snippettype>
      <snippettype>Expansion</snippettype>
    </snippettypes>
  </header>
...
</codesnippet>

از آنجا که قصد داریم یک Expansion snippet بسازیم پس تگ SurroundsWith را حذف می‌کنیم . 


    <snippettypes>
      <snippettype>Expansion</snippettype>
    </snippettypes>

- در بخش header مقدار تگ Title را به “Try Catch Finally”و  مقدار تگ Shortcut را به “trycf” و Description را به “Adds a try-catch-finally block ” تغییر می‌دهیم . Title عنوان snippet است و وجود آن ضروری است . اضافه کردن shortcut  اختیاری است  و به عنوان یک متن میانبر برای اضافه کردن snippet استفاده می‌شود .


    <Header>
      <Title>Try Catch Finally</Title>
      <Author>mohsen.d</Author>
      <Shortcut>trycf</Shortcut>
      <Description>Adds a try-catch-finally block</Description>

- تگ Literal برای تعریف جایگزین برای  بخشی از کد  درون snippet  که احتمال دارد پس از اضافه شدن ، توسط  برنامه نویس و یا در صورت استفاده از  function توسط خود ویژوال استودیو تغییر کند استفاده می‌شود . در قطعه کد try-catch-finally ، ما می‌خواهیم به کاربر اجازه بدهیم که نوع استثنائی را که catch می‌شود تغییر دهد .
تگ id نامی برای بخش قابل ویرایش تعریف می‌کند ( که از آن در  ادامه در تعریف خود قطعه کد استفاده می‌کنیم ) . آنرا به “ExceptionName” تغییر می‌دهیم . تگ default هم مقدار پیش فرضی را  برای آن بخش مشخص می‌کند . ما می‌خواهیم تمام استثناها را Catch کنیم پس مقدار پیش فرض را برابر "Exception" قرار می‌دهیم .


.....
    <declarations>
      <literal>
        <id>ExceptionName</id>
        <default>Exception</default>
      </literal>
    </declarations>
...

- و در تگ Code ، خود قطعه کدی که ویژوال استودیو باید آن را اضافه کند ، تعریف می‌شود . مقدار مشخصه Language آن را به CSharp تغییر می‌دهیم و محتویات داخل آنرا به شکل زیر اضافه می‌کنیم . 


<code language="CSharp">
<!--[CDATA[
try
{
    $end$
}
catch($ExceptionName$)
{
  
}
finally
{
  
}
      ]]-->
</code>

به نحوه استفاده از ExceptionName که در قسمت Literal تعریف کردیم توجه کنید  . عبارت $end$ هم یک کلمه رزرو شده است که محل قرار گرفتن cursor را بعد از اضافه شدن قطعه کد مشخص می‌کند .

- در نهایت snippet ما به شکل زیر خواهد بود : 


<codesnippet format="1.0.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <header>
    <title>Try Catch Finally</title>
    <author>mohsen.d</author>
    <shortcut>trycf</shortcut>
    <description>Adds a try-catch-finally block</description>
    <snippettypes>
      <snippettype>Expansion</snippettype>
    </snippettypes>
  </header>
  <snippet>
    <declarations>
      <literal>
        <id>ExceptionName</id>
        <default>Exception</default>
      </literal>
    </declarations>
    <code language="CSharp">
      <!--[CDATA[
try
{
    $end$
}
catch($ExceptionName$)
{
  
}
finally
{
  
}
      ]]-->
    </code>
  </snippet>
</codesnippet>

اضافه کردن snippet ساخته شده به visual studio


دو راه برای اضافه کردن snippet تعریف شده به ویژوال استودیو وجود دارد :
روش اول قرار دادن فایل .snippet در پوشه code snippets ویژوال استودیو است که مسیر پیش فرض آن 

C:\Users\<UserName>\Documents\Visual Studio 2010\Code Snippets\
است . این پوشه به ازای هر زبان دارای یک زیر پوشه است . این snippet را باید در پوشه C# قرار دهیم . همین که فایل را در پوشه مناسب قرار دهیم ویژوال استودیو بدون نیاز به restart شدن آن را خواهد شناخت .

گزینه دوم import کردن فایل .snippet به داخل ویژوال استودیو است . در ویژوال استودیو به مسیر

Tools -> Code Snippets Manager (Ctrl+K,Ctrl+B)
می‌رویم . در پنجره Code Snippets Manager ، بر روی کلید import کلیک و فایل موردنظر را یافته و انتخاب کرده و پوشه‌ی محل ذخیره شدن آن را تعیین می‌کنیم .

استفاده از snippet ساخته شده

برای استفاده از snippet می‌توانیم متن میانبر تعریف شده را تایپ کنیم و با دو بار فشردن کلید tab قطعه کد تعریف شده به محل کرسر اضافه می‌شود 



همینطور با فشردن کلیدهای Ctrl+K و Ctrl+X  به صورت پشت سر هم منوی “Insert Snippet” ظاهر می‌شود که از طریق آن می‌توانیم Snippet موردنظر را یافته  ( بدنبال Title تعریف شده برای snippet  در پوشه ای که آنرا ذخیره کرده اید بگردید ) و با انتخاب آن کد تعریف شده اضافه خواهد شد .



 برای آشنایی با روش‌های مختلف دسترسی به snippet‌ها اینجا را بررسی کنید .

ابزارها

دستکاری خود فایل xml چندان جالب و خالی از خطا نیست . روش‌های بهتری برای ساخت و ویرایش snippet‌ها وجود دارد . Snippet Editor ابزاری برای ویرایش و ساخت snippet هاست و Snippet Designer هم یک پلاگین برای ویژوال استودیوست که کار مشابهی را انجام می‌دهد . یکی از کارهای جالبی که با این ابزار می‌توانید انجام دهید انتخاب یک قطعه از کد ( مثل یک تابع ) و سپس ساختن یک snippet از روی آن است .

در این پروژه هم مجموعه snippet‌های موجود ویژوال استودیو 2010 برای زبان سی شارپ ، جهت سازگاری با stylecop ویرایش و refactor شده اند ( در کنار تعریف snippet‌های دیگر ).