مطالب دوره‌ها
تزریق وابستگی‌ها در فیلترهای ASP.NET MVC
فرض کنید فیلتر سفارشی لاگ کردن را که از سرویس ILogActionService استفاده می‌کند، به نحو ذیل تعریف کرده‌اید:
public interface ILogActionService
{
    void Log(string data);
}

public class LogAttribute : ActionFilterAttribute
{
    public ILogActionService LogActionService { get; set; }
 
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        LogActionService.Log("......data......");
        base.OnActionExecuted(filterContext);
    }
}
با استفاده‌ای مانند:
 [Log]
public ActionResult Index()
{}

روش متداول تنظیمات تزریق وابستگی‌ها در ASP.NET MVC، بیشتر به بحث کنترلرها مرتبط است و سایر قسمت‌ها را پوشش نمی‌دهد. برای این مورد خاص ابتدا نیاز است یک FilterProvider سفارشی را به نحو ذیل تدارک دید:
using StructureMap;
using System.Collections.Generic;
using System.Web.Mvc;
 
namespace DI06.CustomFilters
{
    public class StructureMapFilterProvider : FilterAttributeFilterProvider
    {
        private readonly IContainer _container;
        public StructureMapFilterProvider(IContainer container)
        {
            _container = container;
        }
 
        public override IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
        {
            var filters = base.GetFilters(controllerContext, actionDescriptor);
            foreach (var filter in filters)
            {
                _container.BuildUp(filter.Instance);
                yield return filter;
            }
        }
    }
}
نکته‌ی مهم آن، استفاده از متد BuildUp استراکچرمپ است. نمونه‌ی آن‌را در تنظیمات تزریق وابستگی‌ها در وب فرم‌ها پیشتر ملاحظه کرده‌اید. در این مثال کار آن وهله سازی وابستگی‌های فیلترهای تعریف شده در برنامه است.
پس از اینکه FilterProvider سفارشی مخصوص کار با استراکچرمپ را تهیه کردیم، اکنون نوبت به جایگزین کردن آن با FilterProvider پیش فرض ASP.NET MVC در فایل global.asax.cs به نحو ذیل است:
 //Using the custom StructureMapFilterProvider
var filterProvider = FilterProviders.Providers.Single(provider => provider is FilterAttributeFilterProvider);
FilterProviders.Providers.Remove(filterProvider);
FilterProviders.Providers.Add(SmObjectFactory.Container.GetInstance<StructureMapFilterProvider>());
استفاده از SmObjectFactory.Container.GetInstance سبب خواهد شد تا به صورت خودکار، وابستگی تزریق شده‌ی در سازنده‌ی کلاس StructureMapFilterProvider وهله سازی و تامین شود.
همچنین در این مثال چون تزریق وابستگی در کلاس LogAttribute از نوع setter injection است، نیاز است در تنظیمات ابتدایی Container مورد استفاده، Policies.SetAllProperties نیز قید شود:
namespace DI06.IocConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        private static Container defaultContainer()
        {
            return new Container(x =>
            {
                x.For<ILogActionService>().Use<LogActionService>();
 
                x.Policies.SetAllProperties(y =>
                {
                    y.OfType<ILogActionService>();
                });
            });
        }
    }
}


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
DI06
مطالب دوره‌ها
تزریق وابستگی‌ها
خوب! بالاخره بعد از دو قسمت قبل، به بحث اصلی تزریق وابستگی‌ها رسیدیم. بحثی که بسیاری از برنامه نویس‌های تصور می‌کنند همان IoC است که پیشتر در مورد آن بحث شد.

تزریق وابستگی‌ها یا DI چیست؟

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


در تصویر فوق یک طراحی مبتنی بر معکوس سازی کنترل‌ها را مشاهده می‌کنید. وابستگی‌های یک کلاس توسط اینترفیسی مشخص شده و سپس کلاسی وجود دارد که این وابستگی را پیاده سازی کرده است.
همچنین در این تصویر یک خط منقطع از کلاس MyClass به کلاس Dependency رسم شده است. این خط، مربوط به حالتی است که خود کلاس، مستقیما کار وهله سازی وابستگی مورد نیاز خود را انجام دهد؛ هر چند اینترفیسی نیز در این بین تعریف شده باشد. بنابراین اگر در بین کدهای این کلاس، چنین کدی مشاهده شد:
 IDependency dependency= new Dependency();
تعریف dependency از نوع IDependency به معنای معکوس سازی کنترل نبوده و عملا همان معماری سابق و متداول بکارگرفته شده است. برای بهبود این وضعیت، از تزریق وابستگی‌ها کمک خواهیم گرفت:


در اینجا یک کلاس دیگر به نام Injector اضافه شده است که قابلیت تزریق وابستگی‌ها را به کلاس نیازمند آن به روش‌های مختلفی دارا است. کار کلاس Injector، وهله سازی MyClass و همچنین وابستگی‌های آن می‌باشد؛ سپس وابستگی را دراختیار MyClass قرار می‌دهد.


تزریق وابستگی‌ها در سازنده کلاس

یکی از انواع روش‌های تزریق وابستگی‌ها، Constructor Injection و یا تزریق وابستگی‌ها در سازنده کلاس می‌باشد که متداول‌ترین نوع آن‌ها نیز به‌شمار می‌رود. در این حالت، وابستگی پس از وهله سازی، از طریق پارامترهای سازنده یک کلاس، در اختیار آن قرار می‌گیرند.
یک مثال:
public class Shopper
{
   private readonly ICreditCard _creditCard;
   public Shopper(ICreditCard creditCard)
   {
      _creditCard = creditCard;
   }
}
در اینجا شما یک کلاس خریدار را مشاهده می‌کنید که وابستگی مورد نیاز خود را به شکل یک اینترفیس از طریق سازنده کلاس دریافت می‌کند.
 ICreditCard creditCard = new MasterCard();
Shopper shopper = new Shopper(creditCard);
برای نمونه، وهله‌ای از مستر کارتی که  ICreditCard را پیاده سازی کرده باشد، می‌توان به سازنده این کلاس ارسال کرد. کار وهله سازی وابستگی و واژه کلیدی new به خارج از کلاس استفاده کننده از وابستگی منتقل شده است. بنابراین همانطور که ملاحظه می‌کنید، این مفاهیم آنچنان پیچیده نبوده و به همین سادگی قابل تعریف و اعمال هستند.


تزریق در خواص عمومی کلاس

Setter Injection و یا تزریق در خواص عمومی یک کلاس، یکی دیگر از روش‌های تزریق وابستگی‌ها است (setter در اینجا منظور همان get و set یک خاصیت می‌باشد).
در حالت تزریق وابستگی‌ها در سازنده کلاس، امکان وهله سازی آن کلاس بدون ارسال وابستگی‌ها به سازنده آن ممکن نیست؛ اما در اینجا خیر و امکان وهله سازی و استفاده از یک کلاس، پیش از اعمال وابستگی‌ها نیز وجود دارد. بنابراین امکان تغییر و تعویض وابستگی‌ها پس از وهله سازی کلاس نیز میسر است.
public class Shopper
{
   public ICreditCard CreditCard { get; set; }
}

ICreditCard creditCard = new MasterCard();
Shopper shopper = new Shopper();
shopper.CreditCard = creditCard;
نمونه‌ای از این روش را در مثال فوق مشاهده می‌کنید. در این کلاس، وابستگی مورد نیاز از طریق یک خاصیت عمومی دریافت شده است.


تزریق اینترفیس‌ها

تزریق اینترفیس‌ها نیز یکی دیگر از روش‌های تزریق وابستگی‌ها است؛ اما آنچنان استفاده گسترده‌ای برخلاف دو روش قبل نیافته است.
در این روش نه از سازنده کلاس استفاده می‌شود و نه از یک خاصیت عمومی. ابتدا یک اینترفیس که بیانگر ساختار کلاسی که قرار است تزریق شود ایجاد می‌گردد. سپس این اینترفیس را در کلاس وابستگی مورد نظر پیاده سازی خواهیم کرد. در این اینترفیس نیاز است متد خاصی تعریف شود تا کار تزریق وابستگی را انجام دهد.
یک مثال:
public interface IDependOnCreditCard
{
   void Inject(ICreditCard creditCard);
}

public class Shopper : IDependOnCreditCard
{
  private ICreditCard creditCard;
  public void Inject(ICreditCard creditCard)
  {
     this.creditCard = creditCard;
  }
}  

ICreditCard creditCard = new MasterCard();
Shopper shopper = new Shopper();
((IDependOnCreditCard)shopper).Inject(creditCard);
در اینجا یک اینترفیس به نام IDependOnCreditCard  تعریف گردیده و متد خاصی را به نام Inject تدارک دیده است که نوعی از کردیت کارد را دریافت می‌کند.
در ادامه کلاس خریدار، اینترفیس IDependOnCreditCard را پیاده سازی کرده است. به این ترتیب کلاس Injector تنها کافی است بداند تا این متد خاص را باید جهت تنظیم و تزریق وابستگی‌ها فراخوانی نماید. این روش نسبتا شبیه به روش Setter injection است.
این روش بیشتر می‌تواند جهت فریم ورک‌هایی که قابلیت یافتن کلیه کلاس‌های مشتق شده از IDependOnCreditCard  را داشته و سپس می‌دانند که باید متد Inject آن‌ها را فراخوانی کنند، مناسب است.


نکاتی که باید حین کار با تزریق وابستگی‌ها درنظر داشت

الف) حین استفاده از تزریق وابستگی‌ها، وهله سازی به تاخیر افتاده وابستگی‌ها میسر نیست. برای مثال زمانیکه یک وابستگی قرار است در سازنده کلاسی تزریق شود، وهله سازی آن باید پیش از نیاز واقعی به آن صورت گیرد. البته امکان استفاده از اشیاء Lazy دات نت 4 برای مدیریت این مساله وجود دارد اما در حالت کلی، دیگر همانند قبل و روش‌های متداول، وهله سازی تنها زمانیکه نیاز به آن وابستگی خاص باشد، میسر نیست. به همین جهت باید به تعداد وابستگی‌هایی که قرار است در یک کلاس استفاده شوند نیز جهت کاهش مصرف حافظه دقت داشت.
ب) یکی از مزایای دیگر تزریق وابستگی‌ها، ساده‌تر شدن نوشتن آزمون‌های واحد است. زیرا تهیه Mocks در این حالت کار با اینترفیس‌ها بسیار ساده‌تر است. اما باید دقت داشت، کلاسی که در سازنده آن حداقل 10 اینترفیس را به عنوان وابستگی دریافت می‌کند، احتمالا دچار مشکلاتی در طراحی و همچنین مصرف حافظه می‌باشد.
 
مطالب
Angular Material 6x - قسمت هفتم - کار با انواع قالب‌ها
در این قسمت می‌خواهیم روش تغییر رنگ‌های قالب‌های پیش‌فرض Angular Material را به همراه تغییر پویای آن‌ها در زمان اجرا، بررسی کنیم. همچنین Angular Material از راست به چپ نیز به خوبی پشتیبانی می‌کند که مثالی از آن‌را در ادامه بررسی خواهیم کرد.


بررسی ساختار یک قالب Angular Material

قالب، مجموعه‌ای از رنگ‌ها است که به کامپوننت‌های Angular Material اعمال می‌شود. هر قالب از چندین جعبه‌رنگ یا palette تشکیل می‌شود:
- primary palette: به صورت گسترده‌ای در تمام کامپوننت‌ها مورد استفاده‌است.
- accent palette: به المان‌های تعاملی انتساب داده می‌شود.
- warn palette: برای نمایش خطاها و اخطارها بکار می‌رود.
- foreground palette: برای متون و آیکن‌ها استفاده می‌شود.
- background palette: برای پس‌زمینه‌ی المان‌ها بکار می‌رود.

روش انتخاب این جعبه رنگ‌ها نیز به صورت زیر است:
<mat-card>
  Main Theme:
  <button mat-raised-button color="primary">
    Primary
  </button>
  <button mat-raised-button color="accent">
    Accent
  </button>
  <button mat-raised-button color="warn">
    Warning
  </button>
</mat-card>
در Angular Material تمام قالب‌ها استاتیک بوده و در زمان کامپایل برنامه به صورت خودکار به آن اضافه می‌شوند. به همین جهت برنامه نیازی به تشکیل این اجزا و کامپایل یک قالب را در زمان آغاز آن ندارد.
همانطور که در قسمت اول این سری نیز بررسی کردیم، بسته‌ی Angular Material به همراه چندین قالب از پیش طراحی شده‌است (قالب‌های از پیش آماده‌ی متریال را در پوشه‌ی node_modules\@angular\material\prebuilt-themes می‌توانید مشاهده کنید) و در حین اجرای برنامه تنها یکی از آن‌ها که در فایل styles.css ذکر شده‌است، مورد استفاده قرار می‌گیرد.
اگر نیاز به سفارشی سازی بیشتری وجود داشته باشد، می‌توان قالب‌های ویژه‌ی خود را نیز طراحی کرد. این قالب جدید باید mat-core() sass mixin را import کند که حاوی تمام شیوه‌نامه‌های مشترک بین کامپوننت‌ها است. این مورد باید تنها یکبار به کل برنامه الحاق شود تا حجم آن‌را بیش از اندازه زیاد نکند. سپس این قالب سفارشی، جعبه رنگ‌های خاص خودش را معرفی می‌کند. در ادامه این جعبه رنگ‌ها توسط توابع mat-light-theme و یا mat-dark-theme ترکیب شده و مورد استفاده قرار می‌گیرند. سپس این قالب را include خواهیم کرد. به این ترتیب یک قالب سفارشی Angular Material، چنین طرحی را دارد:
@import '~@angular/material/theming';
@include mat-core();

$candy-app-primary: mat-palette($mat-indigo);
$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);
$candy-app-warn: mat-palette($mat-red);

$candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn);

@include angular-material-theme($candy-app-theme);
اعداد و ارقامی را که در اینجا ملاحظه می‌کنید، در سیستم رنگ‌های طراحی متریال به darker hue و lighter hue تفسیر می‌شوند. همچنین امکان سفارشی سازی تایپوگرافی آن نیز وجود دارد.
ذکر جعبه رنگ اخطار در اینجا اختیاری است و اگر ذکر نشود به قرمز تنظیم خواهد شد.

ایجاد یک قالب سفارشی جدید Angular Material

برای ایجاد یک قالب سفارشی نیاز است از فایل‌های sass استفاده کرد. بنابراین بهترین روش ایجاد برنامه‌های Angular Material در ابتدای کار، ذکر صریح نوع style مورد استفاده به sass است:
 ng new MyProjectName --style=sass
اگر اینکار را انجام نداده‌ایم و حالت پیش‌فرض پروژه همان css است، مهم نیست. می‌توان فایل قالب سفارشی را در یک فایل با پسوند custom.theme.scss نیز در پوشه‌ی src قرار داد و سپس آن‌را در فایل angular.json مشخص کرد تا به صورت css کامپایل شده و مورد استفاده قرار گیرد:
"styles": [
   "node_modules/material-design-icons/iconfont/material-icons.css",
   "src/styles.css",
   "src/custom.theme.scss"
],
بدیهی است اگر از ابتدا style=sass را تنظیم کرده بودیم، نیازی به ایجاد این فایل اضافی نبود و همان styles.scss اصلی را می‌شد ویرایش کرد و در این حالت فایل angular.json بدون تغییر باقی می‌ماند.
پس از افزودن و تنظیم فایل custom.theme.scss، به فایل styles.css مراجعه کرده و قالب فعلی را به صورت comment در می‌آوریم:
/* @import "~@angular/material/prebuilt-themes/indigo-pink.css"; */

body {
  margin: 0;
}
سپس فایل src\custom.theme.scss را به صورت زیر تکمیل می‌کنیم:
@import '~@angular/material/theming';
@include mat-core();

$my-app-primary: mat-palette($mat-blue-grey);
$my-app-accent:  mat-palette($mat-pink, 500, 900, A100);
$my-app-warn:    mat-palette($mat-deep-orange);

$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);

@include angular-material-theme($my-app-theme);

.alternate-theme {
  $alternate-primary: mat-palette($mat-light-blue);
  $alternate-accent:  mat-palette($mat-yellow, 400);

  $alternate-theme: mat-light-theme($alternate-primary, $alternate-accent);

  @include angular-material-theme($alternate-theme);
}
که در اینجا شامل مراحل import فایل‌های پایه‌ی Angular Material، تعریف جعبه رنگ جدید، ترکیب آن‌ها و در نهایت include آن‌ها می‌باشد.

در اینجا روش تعریف یک قالب دوم (alternate-theme) را نیز مشاهده می‌کنید. علت تعریف قالب دوم در همین فایل جاری، کاهش حجم نهایی برنامه است. از این جهت که اگر alternate-themeها را در فایل‌های scss دیگری قرار دهیم، مجبور به import تعاریف اولیه‌ی قالب‌های Angular Material در هرکدام به صورت جداگانه‌ای خواهیم بود که حجم قابل ملاحظه‌ای را به خود اختصاص می‌دهند. به همین جهت قالب‌های دیگر را نیز در همینجا به صورت کلاس‌های ثانویه تعریف خواهیم کرد.
 در این حالت روش استفاده‌ی از این قالب ثانویه به صورت زیر می‌باشد:
<mat-card class="alternate-theme">
  Alternate Theme:
  <button mat-raised-button color="primary">
    Primary
  </button>
  <button mat-raised-button color="accent">
    Accent
  </button>
  <button mat-raised-button color="warn">
    Warning
  </button>
</mat-card>

پس از افزودن فایل src\custom.theme.scss به برنامه، اگر آن‌را اجرا کنیم به خروجی زیر خواهیم رسید:




افزودن امکان انتخاب پویای قالب‌ها به برنامه

قصد داریم به منوی برنامه که اکنون گزینه‌ی new contact را به همراه دارد، گزینه‌ی toggle theme را هم جهت تغییر پویای قالب اصلی برنامه اضافه کنیم. به همین جهت فایل toolbar.component.html را گشوده و به صورت زیر تغییر می‌دهیم:
  <mat-menu #menu="matMenu">
    <button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
    <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button>
  </mat-menu>
در اینجا دکمه‌ای به منو اضافه شده‌است که سبب صدور رخدادی به والد آن یا همان sidenav خواهد شد. علت اینجا است که تغییر قالب را در sidenav، که در برگیرنده‌ی router-outlet است، می‌توان به کل برنامه اعمال کرد.
بنابراین جهت تبادل اطلاعات بین toolbar و sidenav از یک رخ‌داد استفاده خواهیم کرد. برای این منظور فایل toolbar.component.ts را گشوده و این رخ‌داد را به آن اضافه می‌کنیم:
export class ToolbarComponent implements OnInit {

  @Output() toggleTheme = new EventEmitter<void>();
پس از این تعریف، به sidenav.component.html مراجعه کرده و به این رخ‌داد گوش فرا می‌دهیم:
<app-toolbar (toggleTheme)="toggleTheme()" (toggleSidenav)="sidenav.toggle()"></app-toolbar>
متد toggleTheme را نیز به صورت زیر به sidenav.component.ts اضافه می‌کنیم:
export class SidenavComponent {

  isAlternateTheme = false;

  toggleTheme() {
    this.isAlternateTheme = !this.isAlternateTheme;
  }
}
در اینجا یک خاصیت عمومی boolean را با کلیک بر روی گزینه‌ی منوی Toggle theme، به true و یا false تنظیم می‌کنیم. اکنون از این مقدار جهت تغییر css قالب sidenav استفاده خواهیم کرد:
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill 
   [class.alternate-theme]="isAlternateTheme">
به این ترتیب اگر مقدار isAlternateTheme مساوی true باشد، کلاس alternate-theme به قالب sidenav به صورت پویا اعمال خواهد شد و برعکس. در تصویر زیر نمونه‌ای از تغییر پویای قالب برنامه را مشاهده می‌کنید:




افزودن پشتیبانی از راست به چپ به قالب برنامه

اگر به mat-sidenav-container ویژگی dir=rtl را اضافه کنیم، قالب برنامه راست به چپ خواهد شد. در ادامه می‌خواهیم شبیه به حالت تغییر پویای قالب سایت، گزینه‌ای را به منوی برنامه جهت تغییر جهت برنامه نیز اضافه کنیم. برای این منظور به قالب toolbar.component.html مراجعه کرده و گزینه‌ی Toggle dir را به آن اضافه می‌کنیم:
  <mat-menu #menu="matMenu">
    <button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
    <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button>
    <button mat-menu-item (click)="toggleDir.emit()">Toggle dir</button>
  </mat-menu>
سپس این رخ‌داد را که قرار است در نهایت به sidenav منتقل شود، به صورت زیر به toolbar.component.ts اضافه می‌کنیم:
export class ToolbarComponent implements OnInit {

  @Output() toggleDir = new EventEmitter<void>();
اکنون در sidenav.component.html به این ر‌خ‌داد گوش فرا خواهیم داد:
<app-toolbar (toggleDir)="toggleDir()" 
(toggleTheme)="toggleTheme()" 
(toggleSidenav)="sidenav.toggle()"></app-toolbar>
متد toggleDir در sidenav.component.ts به صورت زیر پیاده سازی می‌شود:
export class SidenavComponent implements OnInit, OnDestroy {

  dir = "ltr";

  toggleDir() {
    this.dir = this.dir === "ltr" ? "rtl" : "ltr";
  }
}
و در نهایت این جهت را به mat-sidenav-container در فایل sidenav.component.html اعمال می‌کنیم:
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill [dir]="dir"
  [class.alternate-theme]="isAlternateTheme">
در تصویر زیر نمونه‌ای از تغییر پویای جهت برنامه را مشاهده می‌کنید:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-06.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
مطالب
پیاده سازی PWA در Angular
امروزه طراحی اپلیکیشن‌های موبایل بخش زیادی از جامعه را در برگرفته است و روز به روز در حال توسعه میباشند. موازی با رشد روز افزون و نیاز بیشتر به این اپلیکیشن‌ها فریمورک‌های زیادی نیز  ابداع شده اند. از جمله این فریم ورک‌ها می‌توان به موارد زیر اشاره کرد:

Ionic  , react native , flutter , xamarin ….

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

 
مزایای نوشتن یک اپلیکیشن با  فریم ورک‌ها:

1-  نوشتن کد به مراتب آسانتر است.
۲- چون اکثر فریم ورک‌ها، فریم ورک‌های جاوا اسکریپتی هستند، سرعت بالایی هنگام run کردن برنامه دارند ولی در build آخر و خروجی نهایی بر روی پلتفرم، این سرعت کندتر می‌شود.
۳- cossplatform بودن. با طراحی یک اپلیکیشن برای یک پلتفرم می‌توان همزمان برای پلتفرم‌های دیگر خروجی گرفت.


معایب:

۱- برنامه نویس را محدود  میکند.
۲- سرعت اجرای پایینی دارد.
۳- حجم برنامه به مراتب بالاتر میباشد.
۴- از منابع سخت افزاری بیشتری استفاده می‌کند.

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


 pwa چیست؟

pwa مخفف Progressive Web Apps است و یک تکنولوژی برای طراحی وب اپلیکیشن‌های تحت مرورگر میباشد. شما با pwa میتوانید اپلیکیشن خود را بر روی پلتفرم‌های مختلفی اجرا کنید، طوری که کاربران متوجه نشوند با یک صفحه‌ی وبی دارند کار میکنند. شاید شما هم فکر کنید این کار را هم میتوان با html ,css و responsive کردن صفحه انجام داد! ولی اگر بخواهید کاربر متوجه استفاده‌ی از یک صفحه‌ی وبی نشود، باید حتما از pwa استفاده کنید! برای مثال یک صفحه‌ی وبی معمولی حتما باید در بستر اینترنت اجرا شود، ولی با pwa با یکبار وصل شدن به اینترنت و کش کردن داده‌ها، برای بار دوم دیگر نیازی به اینترنت ندارد و میتواند به صورت offline  کار کند. شما حتی با pwa می‌توانید اپلیکیشن را در background اجرا کنید و notification ارسال کنید.


مزیت pwa

۱- یکی از مزیت‌های مهم pwa، حالت offline آن میباشد که حتی با قطع اینترنت، شما میتوانید همچنان با اپلیکیشن کار کنید.
۲- با توجه به اینکه شما در حقیقت با یک صفحه‌ی وبی کار میکنید، دیگر نیازی به دانلود و نصب ندارید.
۳- امکان به‌روز رسانی کردن، بدون اعلام کردن نسخه جدید.
۴- از سرعت بسیار زیادی برخوردار است.
۵- چون pwa از پروتکل https استفاده می‌کند، دارای امنیت بالایی میباشد.


معایب

۱- محدود به مرورگر می‌باشد.
۲- هرچند امروزه اکثر مرورگر‌ها pwa را پشتیبانی میکنند، ولی در بعضی از مرورگر‌ها و مرورگر‌های با ورژن پایین، pwa پشتیبانی نمیشود.
3- نمیتوان یک برنامه‌ی مبتنی بر os را نوشت و محدود به مرورگر میباشد


چند نکته درباره pwa

1- برای pwa  لزومی ندارد حتما از فریم ورک‌های spa استفاده کنید. شما از هر فریم ورک Client Side ای می‌توانید استفاده کنید.
۲- چون صفحات شما در پلتفرم‌های مختلف و با صفحه نمایش‌های مختلفی اجرا می‌شود، باید صفحات به صورت کاملا responsive شده طراحی شوند.
۳- باید از پروتکل https استفاده کنید.

ما در این مقاله از فریم ورک angular  استفاده  خواهیم کرد.

قبل از شروع، با شیوه کار pwa آشنا خواهیم شد. یکی از قسمت‌های مهم  Service Worker ،pwa میباشد که از جمله کش کردن، notification فرستادن و اجرای پردازش‌ها در پس زمینه را بر عهده دارد.


با توجه به شکل بالا، Service Worker مانند یک لایه‌ی نرم افزاری مابین کلاینت و سرور قرار دارد که درخواست‌های داده شده از کلاینت را در صورت اتصال به اینترنت، ارسال کرده و مجددا response را برگشت میدهد‌. اگر به هر دلیلی اینترنت قطع باشد، درخواست به صورت آفلاین به کش مرورگر که توسط proxy ساخته شده است، فرستاده میشود و response را برگشت میدهد.


چند نکته  در رابطه با Service Worker

- نباید برای  نگهداری داده global از Service Worker استفاد کرد. برای استفاده از داده‌های Global میتوان از Local Storage یا IndexedDB استفاده کرد.
- service worker  به dom دسترسی ندارند.


 سناریو

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


 قبل از شروع لازم است به چند نکته زیر توجه کرد

1-  سرویس crud را به صورت کامل در پروژه قرار خواهیم داد، ولی چون از حوصله‌ی مقاله خارج است، فقط ثبت کاربران و نمایش کاربران را پیاده سازی خواهیم کرد.
2 - هر اپلیکیشنی قطعا نیاز به یک وب سرویس دارد که واسط بین دیتابیس و اپلیکیشن باشد. ولی ما چون در این مثال به عنوان front end کار قصد طراحی  اپلیکیشن را داریم، برای حذف back end از firebase استفاده خواهیم کرد و مستقیما از انگولار به دیتابیس firebase کوئری خواهیم زد.


 شروع به کار

پیش نیاز‌های یک پروژه‌ی انگیولاری را بر روی سیستم خود فراهم کنید. ما در این مثال از یک template آماده انگیولاری استفاده خواهیم کرد. پس برای اینکه با جزئیات و طراحی ui درگیر نشویم، از لینک github پروژه را دریافت کنید.
سپس وارد root پروژه شوید و با دستور زیر پکیج‌های پروژه را نصب کنید:
 npm install
پس از نصب پکیج‌ها، با دستور ng serve، پروژه را اجرا کنید.

پس از اجرا باید خروجی بالا را داشته باشید. می‌خواهیم یکی از قابلیت‌های pwa را بررسی کنیم. بر روی صفحه کلیک راست کرده و وارد inspect شوید. وارد تب Application  شوید و Service Worker را انتخاب کنید.
 


قبل از اینکه کاری را انجام دهید، چند بار صفحه را refresh کنید! صفحه بدون هیچ تغییری refresh میشود. اینبار گزینه‌ی offline را فعال کنید و مجددا صفحه را refresh کنید.
این بار صفحه No internet به معنی قطع بودن اینترنت نمایش داده میشود و شما دیگر نمی‌توانید بر روی اپلیکیشن خود فعالیتی داشته باشید! 


نصب pwa  بر روی پروژه

برای اضافه کردن pwa به پروژه وارد ریشه‌ی پروژه شوید و دستور زیر را وارد کنید:
 ng add @angular/PWA
پس از  اجرای دستور بالا، تغییراتی در پروژه لحاظ میشود از جمله در فایل‌هایindex.html,app-module,angular.json، همچنین یک پوشه‌ی جدید به نام icons در assets و دو فایل جیسونی زیر به روت پروژه اضافه شده‌اند:
 Manifest.json : اگر محتویات فایل را مشاهده کرده باشید، شامل تنظیمات فنی وب اپلیکیشن می‌باشد؛ از جمله Home Screen Icon و نام وب اپلیکیشن و سایر تنظیمات دیگر.
 Nsgw-config.json : این فایل نسبت به فایل manifest فنی‌تر میباشد و بیشتر به کانفیگ مد آفلاین و کش کردن مرتبط میشود. در ادامه با این فایل بیشتر کار داریم.

بعد از نصب pwa  بر روی پروژه، همه تنظیمات به صورت خودکار انجام می‌شود. البته می‌توانید تنظیمات مربوط به کش کردن داده‌ها را به صورت پیشرفته‌تر کانفیگ کنید.


  اجرا کردن وب اپلیکیشن

 برای اجرا کردن و نمایش خروجی از وب اپلیکیشن، ابتدا باید از پروژه build گرفت. با استفاده از دستور زیر از پروژه خود build بگیرید:
 ng build -- prod
سپس  با دستور زیر وارد پوشه‌ای که فایل‌های Build  شده در آن قرار دارند، شوید:
 cd dist/Web Application  Pwa
بعد با دستور زیر برنامه را اجرا کنید:
 Http-server -o
اگر چنانچه با دستور بالا به خطا برخوردید، با دستور زیر پکیج npm آن را نصب کنید:
 npm i http-server
اگر تا به اینجای کار درست پیش رفته باشید، پروژه را بدون هیچ تغییری مشاهده خواهیم کرد. inspect گرفته و وارد تب Application شوید. اینبار گزینه‌ی offline را فعال کنید و مجددا صفحه را refresh کنید! اینبار برنامه بدون هیچ مشکلی کار میکند. حتی شما میتوانید در کنسول، برنامه را به صورت کامل stop کنید.


برسی فایل Nsgw-config.json 

وارد فایل Nsgw-config.json شوید: 

"$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }

    }
  ],
  "dataGroups": [
    {
      "name": "api-performance",
      "urls": [
        "https://api/**"
      ],
      "cacheConfig": {
        "strategy": "performance",
        "maxSize": 100,
        "maxAge": "3d"
      }
    }
  ]
}
 این فایل مربوط به کش کردن داده‌ها میباشند و میتواند شامل چند object زیر باشد. مهمترین آنها را بررسی خواهیم کرد:
  ۱- assetGroups : کش کردن اطلاعات مربوط به اپلیکیشن
  ۲- Index : کش کردن  فایل مربوط به index.html
  ۳-  assetGroups : کش کردن فایل‌های مربوط به asset، شامل فایل‌های js، css  و  غیره
  ۴- dataGroups : این object مربوط به وقتی است که برنامه در حال اجرا است. میتوان داده‌های در حال اجرای اپلیکیشن را کش کرد. داده‌ی در حال اجرا میتواند شامل فراخوانی apiها باشد. برای مثال فرض کنید شما در حالت کار کردن online با اپلیکیشن، لیستی از اسامی کاربران را از api گرفته و نمایش میدهید. وقتی دفعه‌ی بعد در حالت offline  اپلیکیشن را باز کنیم، اگر api را کش کرده باشیم، اپلیکیشن داده‌ها را از کش فراخوانی میکند. این عمل درباره post کردن داده‌ها هم صدق میکند.

خود dataGroups شامل چند object زیر میباشد:
  ۱- name :  یک نام انتخابی برای Groups  میباشد.
 ۲- urls  : شامل آرایه‌ای از آدرس‌ها میباشد. میتوان آدرس یک دومین را همراه با کل apiها به صورت زیر کش کرد:
"https://api/**"
۳- cacheConfig  : که شامل
  ۱- maxSize  : حداکثر تعداد کش‌های مربوط به Groups .
  ۲- maxAge  : حداکثر  lifetime مربوط به کش.
  ۳- strategy  : که میتواند یکی از مقادیر freshness به معنی Network-First یا performance  به معنی Cache-First باشد.


 پیاده سازی پیغام نمایش به‌روزرسانی جدید وب اپلیکیشن

در اپلیکیشن‌های native  وقتی به‌روزرسانی جدیدی برای app اعلام میشود، در فروشگاهای اینترنتی پیغامی مبنی بر به‌روزرسانی جدید app برای کاربران ارسال میشود که کاربران میتوانند app خود را به‌روزرسانی کنند. ولی در pwa تنها با یک رفرش صفحه میتوان اپلیکیشن را به جدیدترین امکانات به‌روزرسانی کرد! برای اینکه بتوانیم با هر تغییر، پیغامی را جهت به‌روزرسانی نسخه یا هر  پیغامی دیگری را نمایش دهیم، از کد زیر استفاده میکنم:
if(this.swUpdate.isEnabled)
    {
      this.swUpdate.available.subscribe(()=> {

        if(confirm("New Version available.Load New Version?")){
          window.location.reload();
        }
      })
    }


وصل شدن به دیتابیس

برای اینکه بتوانیم یک وب اپلیکیشن کامل را طراحی کنیم، قطعا نیاز به دیتابیس داریم. برای دیتابیس از firebase استفاده میکنیم. قبل از شروع، وارد سایت firebase  شوید. پس از لاگین، بر روی Add Project کلیک کنید. بعد از انتخاب نام مناسب، create Project را انتخاب کنید. ساختار دیتابیس‌های firebase  شبیه nosqlها می‌باشد و بر پایه‌ی json کار میکنند. پس از ساخت پروژه و دریافت کد جاواسکریپتی زیر شروع به طراحی فیلد‌های دیتابیس خواهیم کرد.


در منوی سمت چپ، بر روی database کلیک کنید و یک دیتابیس را در حالت test mode  ایجاد نماید. سپس یک collection را به نام user ایجاد کرده و فیلد‌های زیر را به آن اضافه کنید:
Age :number
Fullname :string
Mobile : string
مرحله‌ی بعد، config پروژه‌ی انگیولاری میباشد. وارد  vscode شوید و با دستور زیر پکیج firebase را اضافه کنید:
  npm install --save firebase @angular/fire
سپس وارد appmodule شوید و آن‌را به صورت زیر کانفیگ کنید:

@NgModule({
  declarations: [AppComponent, RegisterComponent,AboutComponent, UserListComponent],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    HttpClientModule,
    SharedModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(environmentFirebase.firebase),
    AngularFireDatabaseModule,
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
  ],
  providers: [FirebaseService],
  bootstrap: [AppComponent]
})
export class AppModule {}
خط ۱۰ مربوط به pwa است که هنگام نصب pwa اضافه شده‌است و خط ۸ و مربوط به apiKey فایربیس میباشد. می‌شود گفت environmentFirebase.firebase شبیه  connection string می‌باشد که به صورت زیر کانفیگ شده است:
export const environment ={
   production: true,
   firebase: {
     apiKey: "AIz×××××××××××××××××××××××××××××××××8",
     authDomain: "pwaangular-6c041.firebaseapp.com",
     databaseURL: "https://pwaangular-6c041.firebaseio.com",
     projectId: "pwaangular-6c041",
     storageBucket: "pwaangular-6c041.appspot.com",
     messagingSenderId: "545522081966"
   }

}

FirebaseService در قسمت providers مربوط به سرویس crud میباشد. اگر وارد فایل مربوطه شوید، چند عمل اصلی به صورت زیر در آن پیاده سازی شده است:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import * as firebase from 'firebase';
import { AngularFireDatabase } from '@angular/fire/database';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { ThrowStmt } from '@angular/compiler';
@Injectable({
  providedIn: 'root'
})
export class FirebaseService {

  ref = firebase.firestore().collection('users');
  constructor(public db: AngularFireDatabase,public _http:HttpClient) { 
 
  }

  getUsers(): Observable<any> {
    return new Observable((observer) => {
      this.ref.onSnapshot((querySnapshot) => {
        let User = [];
        querySnapshot.forEach((doc) => {
          let data = doc.data();
          User.push({
            key: doc.id,
            fullname: data.fullname,
            age: data.age,
            mobile: data.mobile
          });
        });
        observer.next(User);
      });
    });
  }

  getUser(id: string): Observable<any> {
    return new Observable((observer) => {
      this.ref.doc(id).get().then((doc) => {
        let data = doc.data();
        observer.next({
          key: doc.id,
          title: data.title,
          description: data.description,
          author: data.author
        });
      });
    });
  }

  postUser(user): Observable<any> {
    return new Observable((observer) => {
      
      this.ref.add(user).then((doc) => {
        observer.next({
          key: doc.id,
        });
      });
    });
  }

  updateUser(id: string, data): Observable<any> {
    return new Observable((observer) => {
      this.ref.doc(id).set(data).then(() => {
        observer.next();
      });
    });
  }

  deleteUser(id: string): Observable<{}> {
    return new Observable((observer) => {
      this.ref.doc(id).delete().then(() => {
        observer.next();
      });
    });
  }

getDataOnApi(){
  return this._http.get('https://site.com/api/General/Getprovince')
  .pipe(
      map((res: Response) => {
return res;

      })
  );
}

getOnApi(){
  return  this._http.get("https://site.com/api/General/Getprovince",).pipe(
    map((response:any) => {
       
return  response
    } )
    );
}
}
اگر دقت کرده باشید، خیلی شبیه به کد نویسی سمت سرور میباشد.

با دستورات زیر میتوانید مجددا پروژه را اجرا کنید:
ng build --prod
cd dist/Pwa-WepApp
Http-server -o


تست وب اپلیکیشن بر روی پلتفرم‌های مختلف

برای اینکه بتوانیم خروجی وب اپلیکیشن را بر روی پلتفرم‌های مختلفی تست کنیم، میتوانیم آن را آپلود کرده و مثل یک سایت اینترنتی، با وارد کردن دومین، وارده پروژه شد. ولی کم هزینه‌ترین راه، استفاده از ابزار ngrok میباشد. میتوانید توسط این مقاله پروژه خودتان در لوکال بر روی https سوار کنید.

نکته! توجه کنید apiهای مربوط به firebase را نمیتوان کش کرد.


کد‌های مربوط به این قسمت را میتوانید از این repository دریافت کنید .
مطالب
Angular Material 6x - قسمت سوم - طرحبندی برنامه
پس از نصب بسته‌ی Angular Material و آشنایی با سیستم Angular Flex Layout برای پوشش کمبود سیستم طرحبندی آن، در این قسمت طرح ابتدایی دفترچه تلفن این سری را پیگیری می‌کنیم تا به طراحی زیر برای حالت‌های دسکتاپ و موبایل برسیم:



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


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

در قسمت اول، برنامه را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد کردیم که نتیجه‌ی آن تولید فایل src\app\app-routing.module.ts می‌باشد:
 ng new MaterialAngularClient --routing
در ادامه ماژول مخصوص مدیریت تماس‌ها را ایجاد می‌کنیم که به آن feature module هم گفته می‌شود. برای این منظور دستور زیر را اجرا کنید:
 ng g m ContactManager -m app.module --routing


این دستور ماژول جدید contact-manager را به همراه تنظیمات ابتدایی مسیریابی و همچنین به روز رسانی app.module، برای درج آن، ایجاد می‌کند. البته در این حالت نیاز است به app.module.ts مراجعه کرد و محل درج آن‌را تغییر داد:
import { ContactManagerModule } from "./contact-manager/contact-manager.module";

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    CoreModule,
    SharedModule.forRoot(),
    ContactManagerModule,
    AppRoutingModule
  ],
})
export class AppModule { }
به صورت پیش‌فرض ContactManagerModule، پس از AppRoutingModule ذکر می‌شود و چون مسیر catch all را در ادامه در AppRoutingModule قرار می‌دهیم، دیگر هیچگاه مسیریابی‌های ContactManagerModule پردازش نخواهند شد. به همین جهت باید آن‌را پیش از AppRoutingModule قرار داد.
سپس دستور زیر را اجرا می‌کنیم تا کامپوننت contact-manager-app در ماژول contact-manager ایجاد شود:
 ng g c contact-manager/ContactManagerApp --no-spec
با این خروجی:
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.html (38 bytes)
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.ts (319 bytes)
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.css (0 bytes)
UPDATE src/app/contact-manager/contact-manager.module.ts (436 bytes)
همانطور که ملاحظه می‌کنید این دستور کار به روز رسانی contact-manager.module را نیز جهت معرفی این کامپوننت جدید انجام داده‌است.
این کامپوننت به عنوان میزبان سایر کامپوننت‌هایی که در مقدمه‌ی بحث عنوان شدند، عمل می‌کند. این کامپوننت‌ها را به صورت زیر در پوشه‌ی components ایجاد می‌کنیم:
ng g c contact-manager/components/toolbar --no-spec
ng g c contact-manager/components/main-content --no-spec
ng g c contact-manager/components/sidenav --no-spec
با این خروجی:



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

در ادامه به src\app\app-routing.module.ts مراجعه کرده و این ماژول جدید را به صورت lazy load معرفی می‌کنیم:
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";

const routes: Routes = [
  { path: "contactmanager", loadChildren: "./contact-manager/contact-manager.module#ContactManagerModule" },
  { path: "", redirectTo: "contactmanager", pathMatch: "full" },
  { path: "**", redirectTo: "contactmanager" }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا تنظیمات صفحه‌ی پیش‌فرض برنامه و همچنین not found و یا catch all را نیز مشاهده می‌کنید که هر دو به contactmanager تنظیم شده‌اند.

سپس تنظیمات مسیریابی ماژول مدیریت تماس‌ها را در فایل src\app\contact-manager\contact-manager-routing.module.ts به صورت زیر تغییر می‌دهیم:
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";

import { MainContentComponent } from "./components/main-content/main-content.component";
import { ContactManagerAppComponent } from "./contact-manager-app/contact-manager-app.component";

const routes: Routes = [
  {
    path: "", component: ContactManagerAppComponent,
    children: [
      { path: "", component: MainContentComponent }
    ]
  },
  { path: "**", redirectTo: "" }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ContactManagerRoutingModule { }
در اولین بار بارگذاری این ماژول، کامپوننت ContactManagerApp بارگذاری می‌شود. همچنین مسیر not found نیز به همان مسیر ریشه‌ی این کامپوننت تنظیم شده‌است.
کامپوننت ContactManagerApp که کار هاست سایر کامپوننت‌های این ماژول را بر عهده دارد، دارای router-outlet خاص خود خواهد بود. به همین جهت برای آن children تعریف شده‌است که مسیر پیش‌فرض آن، بارگذاری کامپوننت MainContent است.
بنابراین نیاز است به فایل contact-manager-app\contact-manager-app.component.html مراجعه و ابتدا منوی کنار صفحه را به آن افزود:
 <app-sidenav></app-sidenav>
app-sidenav همان selector کامپوننت sidenav است که در فایل sidenav\sidenav.component.ts قابل مشاهده‌است.
سپس در قالب sidenav\sidenav.component.html، کار تعریف toolbar و همچنین router-outlet را انجام می‌دهیم:
<app-toolbar></app-toolbar>
<router-outlet></router-outlet>
بر اساس مسیریابی که تعریف کردیم، router-outlet کار نمایش Main Content را انجام می‌دهد.


معرفی Angular Material به ماژول جدید مدیریت تماس‌ها

در قسمت اول، یک فایل material.module.ts را ایجاد کردیم که به همراه تعریف تمامی کامپوننت‌های Angular Material بود. سپس آن‌را به shared.module.ts افزودیم که حاوی تعریف ماژول فرم‌ها و همچنین Flex Layout نیز هست. به همین جهت برای معرفی این‌ها به این ماژول جدید تنها کافی است در فایل src\app\contact-manager\contact-manager.module.ts در قسمت imports، کار معرفی SharedModule صورت گیرد:
import { SharedModule } from "../shared/shared.module";

@NgModule({
  imports: [
    CommonModule,
    SharedModule,
    ContactManagerRoutingModule
  ]
})
export class ContactManagerModule { }
تا اینجا پیش از ادامه‌ی کار، فرمان ng serve -o را صادر کنید تا مطمئن شویم همه چیز به درستی قابل دریافت و کامپایل است.


پس از اجرای برنامه مشاهده می‌کنید که ابتدا ماژول مدیریت تماس‌ها بارگذاری شده‌است و سپس contact-manager-app عمل و sidenav را بارگذاری کرده و آن نیز سبب نمایش کامپوننت toolbar و سپس main-content شده‌است.


تنظیم طرحبندی برنامه توسط کامپوننت‌های Angular Material و همچنین Flex Layout


پس از این تنظیمات اکنون نوبت به تنظیم طرحبندی برنامه‌است و آن‌را با قراردادن کامپوننت Sidenav بسته‌ی Angular Material شروع می‌کنیم:
<mat-sidenav-container *ngIf="shouldRun">
   <mat-sidenav mode="side" opened>
      Sidenav content
   </mat-sidenav>
   Primary content
</mat-sidenav-container>
از کامپوننت Sidenav عموما برای طراحی منوی راهبری سایت استفاده می‌شود. این کامپوننت در سه حالت قابل تنظیم است که بر روی نحوه‌ی نمایش Sidenav content و Primary content تاثیرگذار است:
- Over: قسمت Sidenav content بر روی Primary content قرار می‌گیرد.
- Push: قسمت Sidenav content قسمت Primary content را از سر راه خود بر می‌دارد.
- Side:  قسمت Sidenav content در کنار Primary content قرار می‌گیرد.

در اینجا از حالت Side، در صفحات نمایشی بزرگ (اولین تصویر این قسمت) و از حالت Over، در صفحات نمایشی موبایل (مانند تصویر زیر) استفاده خواهیم کرد.



در ابتدا کدهای کامل هر سه کامپوننت و سپس توضیحات آن‌ها را مشاهده خواهید کرد:


تنظیم margin در CSS اصلی برنامه

زمانیکه sidenav و toolbar را بر روی صفحه قرار می‌دهیم، فاصله‌ای بین آن‌ها و لبه‌های صفحه مشاهده می‌شود. برای اینکه این فاصله را به صفر برسانیم، به فایل src\styles.css مراجعه کرده و margin بدنه‌ی صفحه را به صفر تنظیم می‌کنیم:
@import "~@angular/material/prebuilt-themes/indigo-pink.css";

body {
  margin: 0;
}


طراحی قالب main content

<mat-card>
  <h1>Main content</h1>
</mat-card>
برای نمایش main-content از کامپوننت ساده‌ی card استفاده شده‌است که به فایل main-content\main-content.component.html اضافه خواهد شد. این قسمت در نهایت توسط router-outlet نمایش داده می‌شود.


طرای منوی راهبری واکنشگرا

sidenav\sidenav.component.css 
 sidenav\sidenav.component.html 
.app-sidenav-container {
  position: fixed;
}

.app-sidenav {
  width: 240px;
}

.wrapper {
  margin: 50px;
}
<mat-sidenav-container fxLayout="row"
         fxFill>
  <mat-sidenav #sidenav 
    fxFlex="1 1 100%" [opened]="!isScreenSmall"
    [mode]="isScreenSmall ? 'over' : 'side'">
    <mat-toolbar color="primary">
      Contacts
    </mat-toolbar>
    <mat-list>
      <mat-list-item>Item 1</mat-list-item>
      <mat-list-item>Item 2</mat-list-item>
      <mat-list-item>Item 3</mat-list-item>
    </mat-list>
  </mat-sidenav>

  <mat-sidenav-content fxLayout="column" fxFlex="1 1 100%" fxFill>
    <app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
    <div>
      <router-outlet></router-outlet>
    </div>
  </mat-sidenav-content>
</mat-sidenav-container>

- نمای کلی صفحه در این قسمت طراحی شده‌است. sidenav-container که در برگیرنده‌ی اصلی است، به fxLayout از نوع row تنظیم شده‌است. یعنی mat-sidenav و mat-sidenav-content دو ستون آن‌را از چپ به راست تشکیل می‌دهند و درون یک ردیف، سیلان خواهند یافت. همچنین می‌خواهیم این container کل صفحه را پر کند، به همین جهت از fxFill استفاده شده‌است. این fxFill اعمال شده‌ی به container، زمانی عمل خواهد کرد که position آن در css، به fixed تنظیم شود که اینکار در css این قالب و در کلاس app-sidenav-container آن انجام شده‌است.
- سپس toolbar و همچنین router-outlet که main content را نمایش می‌دهند، داخل sidenav-content قرار گرفته‌اند و هر دو با هم، ستون دوم این طرحبندی را تشکیل می‌دهند. به همین جهت fxLayout آن به column تنظیم شده‌است (ستون اول آن، لیست تماس‌ها است و ستون دوم آن، از دو ردیف toolbar و main-content تشکیل می‌شود).
- اگر دقت کنید یک template reference variable به نام sidenav# به container اعمال شده‌است. از آن، جهت باز و بسته کردن sidenav استفاده می‌شود:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
زمانیکه در toolbar بر روی دکمه‌ی منوی همبرگری کلیک شود، متد sidenav.toggle فراخوانی شده و این مورد سبب نمایان شدن مجدد sidenav خواهد شد. در این مورد در ادامه بیشتر بحث می‌کنیم.
- mat-sidenav از دو قسمت تشکیل شده‌است. بالای آن توسط mat-toolbar صرفا کلمه‌ی Contacts نمایش داده می‌شود و سپس ذیل آن، لیست فرضی تماس‌ها توسط کامپوننت mat-list قرار گرفته‌اند (تا فعلا خالی نباشد. در قسمت‌های بعدی آن‌را پویا خواهیم کرد). رنگ تولبار آن‌را ("color="primary) نیز به primary تنظیم کرده‌ایم تا خاکستری پیش‌فرض آن نباشد.
- کار کلاس mat-elevation-z10 این است که بین sidenav و main-content یک سایه‌ی سه بعدی را ایجاد کند که آن‌را در تصاویر مشاهده می‌کنید. عددی که پس از z قرار می‌گیرد، میزان عمق سایه را مشخص می‌کند.
- این قسمت از sidenav به همراه دو خاصیت opened و همچنین mode است که به مقدار isScreenSmall عکس العمل نشان می‌دهند:
<mat-sidenav  [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'">
در اینجا می‌خواهیم اگر اندازه‌ی صفحه xs شد، حالت over بجای حالت پیش‌فرض side تنظیم شود. یعنی در حالت موبایل و اندازه‌ی صفحه‌ی کوچک، sidenav در صورت فراخوانی متد sidenav.toggle در toolbar، بر روی قسمتی از صفحه ظاهر شود و نه در کنار آن که مخصوص حالت تمام صفحه است. همچنین می‌خواهیم اگر اندازه‌ی صفحه کوچک بود، sidenav بسته شود و نمایان نباشد. به همین جهت خاصیت opened آن به isScreenSmall تنظیم شده‌است. مدیریت خاصیت isScreenSmall در کدهای این کامپوننت به صورت زیر انجام می‌شود:

محتویات فایل sidenav\sidenav.component.ts:
import { Component, OnDestroy, OnInit } from "@angular/core";
import { MediaChange, ObservableMedia } from "@angular/flex-layout";
import { Subscription } from "rxjs";

@Component({
  selector: "app-sidenav",
  templateUrl: "./sidenav.component.html",
  styleUrls: ["./sidenav.component.css"]
})
export class SidenavComponent implements OnInit, OnDestroy {

  isScreenSmall = false;
  watcher: Subscription;

  constructor(private media: ObservableMedia) {
    this.watcher = media.subscribe((change: MediaChange) => {
      this.isScreenSmall = change.mqAlias === "xs";
    });
  }

  ngOnInit() {
  }

  ngOnDestroy() {
    this.watcher.unsubscribe();
  }
}
ObservableMedia را در انتهای قسمت دوم این سری بررسی کردیم. کار آن گوش فرادادن به تغییرات اندازه‌ی صفحه‌است. زمانیکه mqAlias آن برای مثال مساوی xs شد، یعنی در حالت موبایل قرار داریم. در این حالت مقدار خاصیت isScreenSmall به true تنظیم می‌شود و برعکس. با توجه به اینکه این media یک Observable است، نیاز است کار unsubscribe از آن نیز همواره در کدها وجود داشته باشد که نمونه‌ای از آن در متد ngOnDestroy صورت گرفته‌است.
تاثیر خاصیت isScreenSmall بر روی دو خاصیت opened و mode کامپوننت sidenav را در دو تصویر زیر مشاهده می‌کنید. اگر اندازه‌ی صفحه کوچک شود، ابتدا sidenav مخفی می‌شود. اگر کاربر بر روی دکمه‌ی منوی همبرگری کلیک کند، سبب نمایش مجدد sidenav، اینبار با حالت over و بر روی محتوای زیرین آن خواهد شد:




طراحی نوار ابزار واکنشگرا

کدهای قالب و css تولبار (ستون دوم طرحبندی کلی صفحه) را در ادامه مشاهده می‌کنید:
  toolbar\toolbar.component.css    toolbar\toolbar.component.html 
 
.sidenav-toggle {
  padding: 0;
  margin: 8px;
  min-width:56px;
}
 
<mat-toolbar color="primary">
  <button mat-button fxHide fxHide.xs="false" 
              class="sidenav-toggle" (click)="toggleSidenav.emit()">
    <mat-icon>menu</mat-icon>
  </button>

  <span>Contact Manager</span>
</mat-toolbar>

با توجه به استفاده‌ی از fxHide، یعنی دکمه‌ی نمایش منوی همبرگری در تمام حالات مخفی خواهد بود. برای لغو آن و نمایش آن در حالت موبایل، از حالت واکنشگرای آن یعنی fxHide.xs استفاده می‌کنیم (قسمت «کار با API واکنشگرای Angular Flex layout» در مطلب قبلی این سری). به این ترتیب زمانیکه کاربر اندازه‌ی صفحه را کوچک می‌کند و یا اندازه‌ی واقعی صفحه‌ی نمایش او کوچک است، این دکمه نمایان خواهد شد.
همچنین در sidenav یک چنین تعریفی را داریم:
 <app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
بروز رخ‌داد toggleSidenav سبب خواهد شد که متد sidenav.toggle فراخوانی شود و سبب نمایش sidenav در اندازه‌های کوچک صفحه‌ی نمایشی گردد. این رخ‌داد سفارشی را نیز به رخ‌داد click دکمه‌ی همبرگری تولبار متصل کرده‌ایم که با کلیک بر روی آن، کار emit آن صورت می‌گیرد. این emit نیز سبب خواهد شد تا sidenav.toggle متصل به سمتی دیگر، فعال شود. نحوه‌ی تعریف این رخ‌داد سفارشی را در کدهای کامپوننت تولبار، در ادامه مشاهده می‌کنید:

محتویات فایل toolbar\toolbar.component.ts:
import { Component, EventEmitter, OnInit, Output } from "@angular/core";

@Component({
  selector: "app-toolbar",
  templateUrl: "./toolbar.component.html",
  styleUrls: ["./toolbar.component.css"]
})
export class ToolbarComponent implements OnInit {

  @Output() toggleSidenav = new EventEmitter<void>();

  constructor() { }

  ngOnInit() {
  }
}


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-02.zip
برای اجرای آن نیز ابتدا فایل restore.bat و سپس فایل ng-serve.bat را اجرا کنید. پس از اجرای برنامه، یکبار آن‌را در حالت تمام صفحه و بار دیگر با کوچک‌تر کردن اندازه‌ی مرورگر آزمایش کنید. در حالتیکه به اندازه‌ی موبایل رسیدید، بر روی دکمه‌ی همبرگری نمایان شده کلیک کنید تا عکس العمل آن و نمایش مجدد sidenav را در حالت over، مشاهده نمائید.
مطالب
پیاده سازی CQRS توسط MediatR - قسمت اول
در مطالب قبلی (1 , 2) الگوی CQRS معرفی شد. همانطور که می‌بینید، پیاده سازی این الگو هرچند با فریمورک آماده‌ای همچون SimpleCQRS، دارای پیچیدگی زیادی است و باعث نوشتن حجم زیادی کد می‌شود.

فریمورک MediatR توسط توسعه دهنده کتابخانه‌ی محبوب AutoMapper ایجاد شده‌است. این فریمورک پیاده سازی کاملی از الگوی طراحی Mediator در NET. است که داخل خود، تمام پیچیدگی‌های پیاده سازی CQRS را Abstract کرده و با حداقل کد ممکن، می‌توانید به‌راحتی CQRS را داخل پروژه‌ی خود پیاده سازی کنید.

در این سری مطالب به بررسی کامل الگوی CQRS و مزایا و معایب استفاده از آن می‌پردازیم و سپس با استفاده از کتابخانه‌ی Mediatr، این الگو را داخل یک پروژه پیاده سازی می‌کنیم.

CQRS

در CQRS متد‌های برنامه به 2 بخش Read و Write تقسیم می‌شوند. بخش‌هایی که State کلی برنامه ( شامل Database, Cookie, Session, LocalStorage, Memory و ... ) را تغییر می‌دهند، Command و بخش‌هایی که صرفا جنبه خواندنی دارند و وضعیت سیستم را تغییر نمی‌دهند مثل خواندن و نشان دادن اطلاعات از دیتابیس، Query می‌نامند.

* نکته : Naming Convention مورد استفاده برای Command‌‌ها به صورت دستوری است و کار Command در نام آن مشخص است؛ مثال : RegisterUser, SendForgottenPasswordEmail, PlaceOrder

مزایا:
1- شما می‌توانید تکنولوژی‌های مورد استفاده‌ی در بخش‌های Command و Query برنامه‌ی خود را به‌راحتی از هم جدا سازید. به‌عنوان مثال Apache Cassandra در ذخیره سازی داده‌ها ( Write Side ) به عنوان یک دیتابیس قابل اعتنا شناخته میشود و از طرفی دیگر ElasticSearch بدلیل سرعت فوق العاده‌ی خود، برای خواندن داده‌ها استفاده میشود. در این روش، دیتابیس‌ها باید Sync باشند تا داده‌های به‌روز به کاربر نمایش داده شود که این موضوع چالش‌های خود همچون Eventual Consistency و Strong Consistency را دارد که در مقالات بعدی آن‌ها را بررسی خواهیم کرد.

2- در برنامه‌های معمول، اکثرا بخش Read Side، بیشتر از Write Side استفاده می‌شود و کاربران معمولا اطلاعات را دریافت و می‌بینند تا اینکه در آن تغییری ایجاد کنند؛ در این صورت شما می‌توانید بخش Read برنامه‌ی خود را Scale کرده و تعداد سیستم یا منابع بیشتری را به این قسمت از برنامه‌ی خود اختصاص دهید ( Horizontal Scaling, Vertical Scaling ). 

3- این جداسازی باعث تمرکز بیشتر شما بر روی قسمت‌های مختلف برنامه می‌شود؛ بخش‌هایی که وضعیت سیستم را تغییر می‌دهند از بخش‌هایی که صرفا داده‌هایی را خوانده و نمایش می‌دهند، بطور کامل جدا شده‌اند و به‌راحتی قابلیت تغییر هرکدام از این بخش‌ها را خواهید داشت.

معایب : معمولا از معایب این الگو، از پیچیدگی پیاده سازی آن یاد می‌شود که در این آموزش با استفاده از Mediatr سعی بر از بین بردن این پیچیدگی را داریم.

Events

Event‌ها رویدادهایی هستند که خبر انجام کاری را که قبلا داخل سیستم انجامش به پایان رسیده است، به Consumer‌های خود می‌دهند. بعنوان مثال می‌خواهیم بعد از ثبت نام موفق یک کاربر داخل سیستم، Notification و یا ایمیلی را به او ارسال کنیم. بعد از ثبت نام کاربر میتوانیم Event ای به نام UserRegistered را که شامل Username و Email کاربر در بدنه خود است، Raise کنیم.

Event‌ها می‌توانند چندین Consumer داشته باشند؛ بنابراین می‌توانیم یک EventHandler را برای UserRegistered بنویسیم که Email ارسال کند و EventHandler دیگری ایجاد کنیم که Notification ای را برای کاربر بفرستد.

* نکته : Naming Convention مورد استفاده برای Event‌ها به صورت گذشته‌است و خبر یک کار، که قبلا انجام شده است را می‌دهد؛ مثال : UserRegistered, OrderPlaced

Event Sourcing

Event Sourcing به معنای ذخیره‌ی تمام Event‌های رخ داده در برنامه داخل یک دیتابیس Append-Only است. در این نوع دیتابیس‌ها فقط میتوانیم Event‌های جدیدی به آن اضافه کنیم و قادر به ویرایش و حذف Event‌ها نیستیم؛ چون منطق Event، کارهایی است که در گذشته اتفاق افتاده‌اند و ما قادر به تغییر چیزی که در گذشته رخ داده‌است، نیستیم.

مزیت Event Sourcing این است که State برنامه را در زمان‌های مختلفی نگه داشته‌ایم و می‌توانیم وضعیت سیستم را در تاریخی مشخص، پیدا کنیم و در صورت به‌وجود آمدن مشکلی در سیستم، وضعیت آن را تا قبل از به مشکل خوردن، بررسی کنیم.

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

روش دیگری وجود دارد که بجای به‌روزرسانی مداوم state جاری، تمام Event هایی که در آن تراکنشی داخل سیستم رخ داده و این تراکنش State برنامه را تحت تاثیر خود قرار داده‌است، داخل یک دیتابیس اضافه نماییم. در این صورت بدلیل داشتن تمام رویدادهای اتفاق افتاده‌ی در برنامه، می‌توان وضعیت جاری سیستم را شبیه سازی و متوجه شد.

* در این سری آموزشی از دیتابیس  Event Store برای پیاده سازی Event Sourcing استفاده خواهیم کرد.

در مقاله‌ی بعدی، امکانات فریمورک MediatR را بررسی خواهیم کرد.
مطالب
طراحی و پیاده سازی مکانیزم مدیریت Transactionها در ServiceLayer

هدف ارائه راه حلی برای مدیریت Transactionها به عنوان یک Cross Cutting Concern، توسط ApplicationServiceها می‌باشد. 

پیش نیازها:

پیش فرض ما این است که شما از EF به عنوان OR-Mapper استفاده می‌کنید و الگوی Context Per Request را پیاده سازی کرده اید یا از طریق پیاده سازی الگوی Container Per Request به داشتن Context یکتا برای هر درخواست رسیده اید.
کتابخانه StructureMap.Mvc5 پیاده سازی از الگوی Container Per Request را با استفاده از امکانات Nested Container مربوط به StructureMap ارائه می‌دهد. اشیاء موجود در Nested Container طول عمر Singleton دارند.

ایده کار به این صورت هست که توسط یک Interceptor، به هنگام ورود به متد موجود در یک ApplicationService که با TransactionalAttribute تزئین شده است، یک تراکنش ایجاد شده و بعد از اتمام کار متد مورد نظر، در صورت عدم بروز استثناء، Commit و در غیر این صورت Rollback شود. حال اگر یک متد تراکنشی، داخل خود از متد تراکنشی دیگری استفاده کند، صرفا متد بیرونی تراکنش را ایجاد می‌کند و متد داخلی از همان تراکنش استفاده خواهد کرد و اصطلاحا  این تراکنش به صورت Ambient Transaction می باشد. 


واسط ITransaction

    public interface ITransaction : IDisposable
    {
        void Commit();
        void Rollback();
    }

واسط بالا 3 متد را که برای مدیریت تراکنش لازم می‌باشد، در اختیار استفاده کننده قرار می‌دهد.


واسط IUnitOfWork

    public interface IUnitOfWork : IDisposable
    {
        ...

        ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot);
        ITransaction Transaction { get; }
        IDbConnection Connection { get; }
        bool HasTransaction { get; }
    }

اعضای جدید واسط IUnitOfWork کاملا مشخص هستند.

پیاده سازی واسط ITransaction، توسط یک Nested Type در دل کلاس DbContextBase انجام می‌گیرد.

 public abstract class DbContextBase : DbContext
    {
        ...

        #region Fields
        
        private ITransaction _currenTransaction;

        #endregion

        #region NestedTypes

        private class DbContextTransactionAdapter : ITransaction
        {
            private DbContextTransaction _transaction;

            public DbContextTransactionAdapter(DbContextTransaction transaction)
            {
                Guard.NotNull(transaction, nameof(transaction));

                _transaction = transaction;
            }

            public void Commit()
            {
                _transaction?.Commit();
            }

            public void Rollback()
            {
                if (_transaction?.UnderlyingTransaction.Connection != null)
                    _transaction.Rollback();
            }

            public void Dispose()
            {
                _transaction?.Dispose();
                _transaction = null;
            }
        }

        #endregion

        #region Public Methods
        ...

        public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
        {
            if (_currenTransaction != null) return _currenTransaction;

            return _currenTransaction = new DbContextTransactionAdapter(Database.BeginTransaction(isolationLevel));
        }
        #endregion

        #region Properties
        ...

        public ITransaction Transaction => _currenTransaction;
        public IDbConnection Connection => Database.Connection;
        public bool HasTransaction => _currenTransaction != null;

        #endregion
}

public class ApplicationDbContext : DbContextBase, IUnitOfWork, ITransientDependency
{
     
}

کلاس DbContextTransactionAdapter همانطور که از نام آن مشخص می‌باشد، پیاده سازی از الگوی Adapter برای وفق دادن DbContextTransaction با واسط ITransaction، می‌باشد. متد BeginTransaction در صورتی که تراکنشی برای وهله جاری DbContext ایجاد نشده باشد، تراکنشی را ایجاد کرده و فیلد currentTransaction_ را نیز مقدار دهی می‌کند. 


TransactionalAttribute

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
    public sealed class TransactionalAttribute : Attribute
    {
        public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.ReadCommitted;
        public TimeSpan? Timeout { get; set; }
    }

TransactionInterceptor

    public class TransactionInterceptor : ISyncInterceptionBehavior
    {
        private readonly IUnitOfWork _unitOfWork;

        public TransactionInterceptor(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }
        public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation)
        {
            var transactionAttribute = GetTransactionaAttributeOrNull(methodInvocation.InstanceMethodInfo);

            if (transactionAttribute == null || _unitOfWork.HasTransaction) return methodInvocation.InvokeNext();

            using (var transaction = _unitOfWork.BeginTransaction(transactionAttribute.IsolationLevel))
            {
                var result = methodInvocation.InvokeNext();

                if (result.Successful)
                    transaction.Commit();
                else
                {
                    transaction.Rollback();
                }

                return result;
            }
        }

        private static TransactionalAttribute GetTransactionaAttributeOrNull(MemberInfo methodInfo)
        {
            var transactionalAttribute =
                ReflectionHelper.GetAttributesOfMemberAndDeclaringType<TransactionalAttribute>(
                    methodInfo
                ).FirstOrDefault();

            return transactionalAttribute;
        }
    }

واسط ISyncInterceptionBehavior، مربوط می‌شود به کتابخانه جانبی دیگری که برای AOP توسط تیم StructureMap به نام StructureMap.DynamicInterception ارائه شده‌است. در متد Intercept، ابتدا چک می‌شود که که آیا این متد با TransactionAttribute تزئین شده و طی درخواست جاری برای Context جاری تراکنشی ایجاد نشده باشد؛ سپس تراکنش جدیدی ایجاد شده و بدنه اصلی متد اجرا می‌شود و نهایتا در صورت موفقیت آمیز بودن عملیات، تراکنش مورد نظر Commit می‌شود.

در آخر لازم است این Interceptor در تنظیمات اولیه StructureMap به شکل زیر معرفی شود:

Policies.Interceptors(new DynamicProxyInterceptorPolicy(
                type => typeof(IApplicationService).IsAssignableFrom(type), typeof(AuthorizationInterceptor), typeof(TransactionInterceptor), typeof(ValidationInterceptor)));



نکته: فرض کنید در بدنه اکشن متد یک کنترلر ASP.NET MVC یا ASP.NET Core، دو متد تراکنشی فراخوانی شود؛ در این صورت شاید لازم باشد که این دو متد طی یک تراکنش واحد به جای تراکنش‌های مجزا، اجرا شوند؛ بنابراین نیاز است از الگوی Transaction Per Request استفاده شود. برای این کار می‌توان یک ActionFilterAttribute سفارشی ایجاد کرد که ایجاد کننده تراکنش باشد و متدهای داخلی که هر کدام جدا تراکنشی بودند، نیز از تراکنش ایجاد شده استفاده کنند.

مطالب
#Defensive Code in C - قسمت دوم

تعریف متد‌ها در برنامه نویسی:

متدها جزء اولین چیزهایی هستند که در هنگام شروع برنامه نویسی در هر یک از زبان‌های برنامه نویسی، برنامه نویس با آنها آشنا می‌شود. بنابراین متد‌ها به عنوان اصلی‌ترین Building Block  ها در زبان‌های برنامه نویسی دارای اهمیت بسیار زیادی می‌باشند. متد‌ها اولین جاهایی هستند که ما می‌توانیم کار خودمان را از آنها شروع کنیم و به سوی هدف خود حرکت کنیم.

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

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

متد‌ها علاوه بر ویژگی‌های ذکر شده‌ی در بالا که بیشتر بر ویژگی‌های ذاتی و عملکردی آن تمرکز داشت، باید داری یکسری ویژگی‌های دیگر نیز باشند، متد‌ها باید Clean ، Testable و Predictable باشند. هر کدام از ویژگی‌های مذکور توسط این پارامتر‌ها تشریح می‌شوند. 

در ذیل ویژگی‌های مذکور در شکل بالا را تشریح خواهیم کرد.

Clear Purpose :

· یک متد یک کار را انجام می‌دهد و همچنین آن کار را نیز به خوبی انجام می‌دهد.

· متد به راحتی قابل درک می‌باشد.

· میزان خطا‌ها را به شدت کاهش می‌دهد.

· دیباگ کردن را در صورت وجود هر خطایی ساده‌تر می‌کند.

· قابلیت توسعه را افزایش می‌دهد.

· نوشتن تست برای این نوع متد‌ها به دلیل اینکه فقط بر روی یک هدف خاص تمرکز دارند ساده است.

Good Name :

· نام متد عمکرد آن را به روشنی بیان می‌کند

Focused Code :

· تمام کد نوشته شده‌ی در متد فقط بر روی یک هدف تمرکز دارند.

· خوانایی کد بالا است و میزان توضیحاتی که برای کد نوشته می‌شود در کمترین حد ممکن است.

· متد دارای تاثیرات ناخواسته‌ای بر سایر قسمت‌های نرم افزار نمی‌باشد. این مسئله به معنی است که این نوع متد‌ها شامل کدهایی که کارهای ناخواسته ای را انجام می‌دهند نمی‌باشد. برای مثال متدی که برای واکشی اطلاعات مشتریان استفاده می‌شود هیچگونه عملیاتی را که برای ثبت اطلاعات مشتریان انجام می‌شود، انجام نمی‌دهد.

Short Length :

· تعداد خطوط کد مربوط به متد کم می‌باشد. این مسئله خود باعث کاهش باگ‌های احتمالی در یک متد می‌شود.

Automated Code Test :

· متد این قابلیت را دارد که توسط زیر ساخت‌های تست، تست شود که این مسئله خود باعث افزایش کیفیت کد می‌شود.

Predictable Result :

· متد دارای یک نتیجه‌ی قابل پیش بینی می‌باشد.


در ادامه سعی می‌کنیم با ذکر یک مثال، مواردی را که ذکر شد بیشتر توضیح دهیم و دیدگاه کاربردی آن را بررسی کنیم.

مثالی از دنیای واقعی:

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

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

Clear Purpose :

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

Clear Name :

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

Focused Code :

 متد باید کاری را انجام دهد که نام آن بیان می‌کند و تمام کد‌های متد باید حتما بر روی آن هدف تمرکز کنند.

Short Length :

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

Automated Code Test :

 آیا این متد به وسیله‌ی  Automated Code Test  می تواند تست شود؟ چند نکته در مورد این کد وجود دارد که توانایی Automated Code Test را از این کد می‌گیرد. اولین مسئله این است که در این مثال منطق یا همان Business برنامه با UI تلفیق شده‌است. برای رفع این مشکل باید منطق برنامه را در یک پروژه‌ی مجزا از نوع Class Library قرار داد. مسئله‌ی دیگر این است که این متد برای تست شدن بسیار طولانی می‌باشد و باید به یکسری اجزای کوچکتر و منطقی‌تر شکسته شود و هر متد باید یک هدف و عملکرد روشن را داشته باشد. 

در قسمت بعدی راهکارهایی برای Refactor کردن کد بر اساس اصول ذکر شده ارائه خواهد شد.

مطالب
API Versioning
فرض کنید امروز یک API را برای استفاده عموم ارائه میدهید. آیا با یک breaking change در منابع شما که باعث تغییر در داده‌های ورودی یا خروجی API شود، باید استفاده کنندگان این API در سیستمی که از آن استفاده کرده‌اند، تغییراتی را اعمال کنند یا خیر؟ جواب خیر می‌باشد؛ اصلی‌ترین استفاده از API Versioning دقیقا برای این منظور است که بدون نگرانی از توسعه‌های بعدی، از ورژن‌های قدیمی API بتوانیم استفاده کنیم. 
در این مقاله با روش‌های مختلف ورژن بندی API آشنا خواهید شد.
سه روش اصلی زیر را میتوان برای این منظور در نظر گرفت:
  1.  URI-based versioning 
  2.  Header-based versioning 
  3.  Media type-based versioning 

پیاده سازی URI-based versioning
حداقل به 3 طریق میتوان این مکانیسم را پیاده کرد:
راه حل اول: اگر از Attribute Routing استفاده نمی‌کنید، با تغییر جزئی در تعاریف مسیریابی خود میتوانید به نتیجه مورد نظر برسید. برای ادامه کار دو ویوومدل و دو کنترلر را که هر کدام مربوط به ورژن خاصی از API ما هستند، پیاده سازی میکنیم:
public class ItemViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Country { get; set; }
}
public class ItemV2ViewModel : ItemViewModel
{
    public double Price { get; set; }
}
ItemViewModel مربوط به ورژن 1 میباشد.
 public class ItemsController : ApiController   
 {
        [ResponseType(typeof(ItemViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
 public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
ItemsController مربوط به ورژن 1 میباشد.
اگر قرار باشد از مسیرهای متمرکز استفاده کنیم، کافی است که تغییرات زیر را اعمال کنیم:
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new
{
    controller = "ItemsV2",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute("Items", "api/items/{id}", new
{
    controller = "Items",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
در کد بالا به صراحت برای تک تک کنترلرها مسیریابی متناسب با ورژنی که آن را پشتیبانی میکند، معرفی کرده‌ایم.
نکته: این تنظیمات خاص باید قبل از تنظیمات پیش فرض قرار گیرند.

روش بالا به خوبی کار خواهد کرد ولی آنچنان مطلوب نمیباشد. به همین دلیل یک مسیریابی عمومی و کلی را در نظر میگیریم که شامل ورژن مورد نظر، در قالب یک پارامتر میباشد. در روش جایگزین باید به شکل زیر عمل کنید:
config.Routes.MapHttpRoute(
"defaultVersioned",
"api/v{version}/{controller}/{id}",
new { id = RouteParameter.Optional }, new { version = @"\d+" });

config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional }
);

با این تنظیمات فعلا به مسیریابی ورژن بندی شده‌ای دست نیافته‌ایم. زیرا فعلا به هیچ طریق به Web API اشاره نکرده‌ایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.

IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده می‌باشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:

 به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.

public class VersionFinder
    {
        private static bool NeedsUriVersioning(HttpRequestMessage request, out string version)
        {
            var routeData = request.GetRouteData();
            if (routeData != null)
            {
                object versionFromRoute;
                if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute))
                {
                    version = versionFromRoute as string;
                    if (!string.IsNullOrWhiteSpace(version))
                    {
                        return true;
                    }
                }
            }
            version = null;
            return false;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
        public static int GetVersionFromRequest(HttpRequestMessage request)
        {
            string version;

            return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0;
        }
    }

کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری  مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت می‌گیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار می‌گیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.

 public class VersionAwareControllerSelector : DefaultHttpControllerSelector
    {
        public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { }
        public override string GetControllerName(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);
            var version = VersionFinder.GetVersionFromRequest(request);
                
        return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName;
        }
        private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName,
        int version)
        {
            var versionControllerName = $"{baseControllerName}v{version}";
            HttpControllerDescriptor descriptor;
            if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor))
            {
                return versionControllerName;
            }

            throw new HttpResponseException(request.CreateErrorResponse(
            HttpStatusCode.NotFound,
                $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}"));
        }
    }

متد  GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از  IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند. 

config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));

حال با اجرای برنامه به نتیجه زیر خواهیم رسید: 

راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالب‌های مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.

[RoutePrefix("api/v1/Items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country =        "Japan" };
            return Ok(viewModel);
        }
    }


 [RoutePrefix("api/V2/Items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

اگر توجه کرده باشید در مثال ما، نام‌های کنترلر‌ها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نام‌های مختلف برای یک کنترلر در ورژن‌های مختلف، آن را با یک نام یکسان درون namespace‌های مختلف احاطه کنیم یا حتی آنها را درون Class Library‌های جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید. 

راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر می‌باشد:

کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:

public class VersionConstraint : IHttpRouteConstraint
{
    public VersionConstraint(string allowedVersion)
    {
        AllowedVersion = allowedVersion.ToLowerInvariant();
    }
    public string AllowedVersion { get; private set; }

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
    IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
        }
        return false;
    }
}

کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.

متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کرده‌ایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.

 به روش استفاده از این Constraint توجه کنید:

namespace UriBasedVersioning.Namespace.Controllers.V1
{
    using Models.V1;

RoutePrefix("api/{version:VersionConstraint(v1)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
}
namespace UriBasedVersioning.Namespace.Controllers.V2
{
    using Models.V2;


RoutePrefix("api/{version:VersionConstraint(v2)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
}

با توجه به کد بالا می‌توان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Route‌ها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از API‌های بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:

var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));
config.MapHttpAttributeRoutes(constraintsResolver);

مرحله بعدی کار، پیاده سازی IHttpControllerSelector می‌باشد:

  public class NamespaceControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
        public NamespaceControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string,
                HttpControllerDescriptor>>(InitializeControllerDictionary);
        }
        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var version = GetVersion(routeData);
            if (version == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                version, controllerName);
            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }
        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string,
                HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);
                var controllerName =
                    controllerType.Name.Remove(controllerType.Name.Length -
                                               DefaultHttpControllerSelector.ControllerSuffix.Length);
                var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                    segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration,
                        controllerType.Name,
                        controllerType);
                }
            }
            return dictionary;
        }
        private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
        private static string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            var dataTokenValue = subroute?.Route.DataTokens.First().Value;
            var controllerName =
                ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
            return controllerName;
        }
        private static string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();
            return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version");
        }
    }

سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکته‌ی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing می‌باشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامتر‌های controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلر‌های موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.

کارکرد متد InitializeControllerDictionary :

همانطور که میدانید به صورت پیش‌فرض Web API توجهی به فضای نام مرتبط با کنترلر‌ها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:

public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()

لذا در کلاس پیاده سازی شده، خصوصیتی به نام ‎‎_controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا می‌باشد، تعریف کرده‌ایم. متد InitializeControllerDictionary کار آماده سازی داده‌های مورد نیاز خصوصیت ‎‎_controllers می‌باشد. به این صورت که تمام کنترلر‌های موجود در برنامه را واکشی کرده و این بار کلید‌های مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کرده‌ایم.

حال تنظیمات زیر را اعمال کنید:

 public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));

            config.MapHttpAttributeRoutes(constraintsResolver);

            config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config));

        }
    }

این بار برنامه را اجرا کرده و API‌های مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.

نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم  فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.

پیاده سازی Header-based versioning  

در این روش کافی است هدری را برای دریافت ورژن API درخواستی مشخص کرده باشید. برای مثال هدری به نام api-version، که استفاده کننده از این طریق، ورژن درخواستی خود را اعلام میکند. همانطور که قبلا اشاره کردیم، با استفاده از Constraint‌ها دست شما خیلی باز خواهد بود. برای مثال این بار به جای واکشی پارامتر version از RouteData، کد همان قسمت را برای واکشی آن از هدر درخواستی تغییر خواهیم داد:
public class HeaderVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionHeaderName = "api-version";

        public HeaderVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
            IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Contains(VersionHeaderName)) return false;

            var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault();

            return VersionToInt(version) == AllowedVersion;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
    }
این بار در متد Match، به دنبال هدر خاصی به نام api-version می‌گردیم و مقدار آن را با AllowdVersion مقایسه میکنیم تا مشخص کنیم که آیا این Route نوشته شده برای ادامه کار واکشی کنترلر مورد نظر استفاده شود یا خیر. در ادامه یک RouteFactoryAttribute شخصی سازی شده را به منظور معرفی این محدودیت تعریف شده، در نظر میگیریم:
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute
    {
        public HeaderVersionedRouteAttribute(string template) : base(template)
        {
            Order = -1;
        }

        public int Version { get; set; }

        public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary
        {
            {"", new HeaderVersionConstraint(Version)}
        };
    }

با استفاده از خصوصیت Constraints، توانستیم محدودیت پیاده سازی شده خود را به سیستم مسیریابی معرفی کنیم. توجه داشته باشید که این بار هیچ پارامتری در Uri تحت عنوان version را نداریم و از این جهت به صراحت کلیدی برای آن مشخص نکرده ایم (‎‎‎‎‏‏‎‎{"", new HeaderVersionConstraint(Version)} ‎‎ ).
کنترلر‌های ما به شکل زیر خواهند بود:
    [RoutePrefix("api/items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [HeaderVersionedRoute("{id}", Version = 1)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
  [RoutePrefix("api/items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [HeaderVersionedRoute("{id}", Version = 2)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

این بار از VersionedRoute برای اعمال مسیر یابی Attribute-based استفاده کرده ایم و به صراحت ورژن هر کدام را با استفاده از خصوصیت Version آن مشخص کرده‌ایم. نتیجه:


پیاده سازی Media type-based versioning
قرار است ورژن API مورد نظر را که استفاده کننده درخواست میدهد، از دل درخواست‌هایی به فرم زیر واکشی کنیم:
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v1+json
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v2+json
در این روش دست ما کمی بازتر است چرا که میتوانیم بر اساس فرمت درخواستی نیز تصمیماتی را برای ارائه ورژن خاصی از API  داشته باشیم. روال کار شبیه به روش قبلی با پیاده سازی یک Constraint و یک RouteFactoryAttribute انجام خواهد گرفت.
 public class MediaTypeVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionMediaType = "vnd.mediatype.versioning";

        public MediaTypeVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Accept.Any()) return false;

            var acceptHeaderVersion =
                request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType));

            //Accept: application/vnd.mediatype.versioning-v1+json
            if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") ||
                !acceptHeaderVersion.MediaType.Contains("+"))
                return false;

            var version = acceptHeaderVersion.MediaType.Between("-v", "+");
            return VersionToInt(version)==AllowedVersion;
        }
}
در متد Match کلاس بالا به دنبال مدیا تایپ مشخصی هستیم که با هدر Accept برای ما ارسال میشود. برای آشنایی با فرمت ارسال Media Type‌ها میتوانید به اینجا مراجعه کنید. کاری که در اینجا انجام شده‌است، یافتن ورژن ارسالی توسط استفاده کننده می‌باشد که آن را با فرمت خاصی که نشان داده شد و مابین (v-) و (+) قرار داده، ارسال میشود. ادامه کار به مانند روش Header-based میباشد و از مطرح کردن آن در مقاله خودداری میکنیم.
نتیجه به شکل زیر خواهد بود:


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.


مطالب
مدیریت اطلاعات وابسته به زمان در بانک‌های اطلاعاتی رابطه‌ای
در یک برنامه فروشگاه، جداول مشتری و خریدهای او را درنظر بگیرید. خرید 3 سال قبل مشتری خاصی به آدرس قبلی او ارسال شده‌است. خرید امروز او به آدرس جدید او ارسال خواهد شد. سؤال: آیا با وارد کردن و به روز رسانی آدرس جدید مشتری، باید سابقه اطلاعاتی قبلی او حذف شود؟ اجناس ارسالی پیشین او، واقعا به آدرس دیگری ارسال شده‌اند و نه به آدرس جدید او. چگونه باید اینگونه اطلاعاتی را که در طول زمان تغییر می‌کنند، در بانک‌های اطلاعاتی رابطه‌ای نرمال شده مدیریت کرد؟ از این نمونه‌ها در دنیای کاری واقعی بسیارند. برای مثال قیمت اجناس نیز چنین وضعی را دارند. یک بستنی مگنوم، سال قبل 300 تومان بود؛ امسال شده است 1500 تومان. یک سطل ماست 2500 تومان بود؛ امروز همان سطل ماست 6500 تومان است. چطور باید سابقه فروش این اجناس را نگهداری کرد؟


منابع مطالعاتی مرتبط

این موضوع اینقدر مهم است که تابحال چندین کتاب در مورد آن تالیف شده است:

Temporal Data & the Relational Model
Trees and Hierarchies in SQL
Developing Time-Oriented Database Applications in SQL
Temporal Data: Time and Relational Databases
Temporal Database Entries for the Springer Encyclopedia of Database Systems
Temporal Database Management

نکته مهمی که در این مآخذ وجود دارند، واژه کلیدی «Temporal data » است که می‌تواند در جستجوهای اینترنتی بسیار مفید واقع شود.


بررسی ابعاد زمان

فرض کنید کارمندی را استخدام کرده‌اید که ساعتی 2000 تومان از ابتدای فروردین ماه حقوق دریافت می‌کند. حقوق این شخص از ابتدای مهرماه قرار است به ساعتی 2400 تومان افزایش یابد. اگر مامور مالیات در بهمن ماه در مورد حقوق این شخص سؤال پرسید، ما چه پاسخی را باید ارائه دهیم؟ قطعا در بهمن ماه عنوان می‌کنیم که حقوقش ساعتی 2400 تومان است؛ اما واقعیت این است که این عدد از ابتدای استخدام او ثابت نبوده است و باید تاریخچه تغییرات آن، در نحوه محاسبه مالیات سال جاری لحاظ شود.
بنابراین در مدل سازی این سیستم به دو زمان نیاز داریم:
الف) actual time یا زمان رخ دادن واقعه‌ای. برای مثال حقوق شخصی در تاریخ ابتدای مهر ماه تغییر کرده است. به این تاریخ در منابع مختلف Valid time نیز گفته می‌شود.
ب) record time یا زمان ثبت یک واقعه؛ مثلا زمان پرداخت حقوق. به آن Transaction time هم گفته شده است.
یک مثال:
 record date  actual date  حقوق دریافتی
1392/01/01  1392/01/01  2000/روز
1392/02/01  1392/01/01  2000/روز
...
1392/07/01  1392/07/01  2400/روز
...
1392/17/01  1392/07/01  2400/روز
در این لیست، ریز حقوق پرداختی به یک شخص را ملاحظه می‌کنید. actual dateها، زمان‌هایی هستند که حقوق پایه شخص در آن‌ها تغییر کرده و record dateها زمان‌هایی هستند که به شخص حقوق داده شده‌است.
به ترکیب Valid Time  و  Transaction Time، اصطلاحا Bitemporal data می‌گویند.


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

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


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

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

ب) نگهداری اطلاعات تغییرات حقوقی در یک جداول جداگانه
یک جدول ثانویه حقوق جاری کارمندان مرتبط با جدول اصلی کارمندان باید ایجاد شود. در این جدول هر رکورد آن باید دارای بازه زمانی (valid_start_time و valid_end_time) مشخصی باشد. مثلا از تاریخ X تا تاریخ Y، حقوق کارمند شماره 11 ، 2000 تومان در ساعت بوده است. از تاریخ H تا تاریخ Z اطلاعات دیگری ثبت خواهند شد. به این ترتیب با گزارشگیری از جدول لیست حقو‌ق‌های پرداخت شده، سابقه گذشته اشخاص محو نشده و هر رکورد بر اساس قرارگیری در یک بازه زمانی ثبت شده در جدول ثانویه حقوق جاری کارمندان تفسیر می‌شود.
در این حالت باید دقت داشت که بازه‌های زمانی تعریف شده، با هم تداخل نکنند و برنامه ثبت کننده اطلاعات باید این مساله را به ازای هر کارمند کنترل کند و یا با ثبت record_date، اجازه ثبت بازه‌های تکراری را نیز بدهد (توضیحات در قسمت بعد).
به این جدول، یک Temporal table نیز گفته می‌شود. نمونه دیگر آن، نگهداری قیمت یک کالا است از یک تاریخ تا تاریخی مشخص. به این ترتیب می‌توان کوئری گرفت که بستنی مگنوم فروخته شده در ماه آبان سال قبل، بر مبنای قیمت آن زمان، دقیقا چقدر فروش کرده است و نه اینکه صرفا بر اساس آخرین قیمت روز این کالا گزارشگیری کنیم که در این حالت اطلاعات نهایی استخراج شده صحیح نیستند.
حال اگر به این طراحی در جدولی دیگر Transaction time یا زمان ثبت یک رکورد یا زمان ثبت یک فروش را هم اضافه کنیم، به جداول حاصل Bitemporal Tables می‌گویند.


مدیریت به روز رسانی‌ها در جداول Temporal
در جداول Temporal، حذف فیزیکی اطلاعات مطلقا ممنوع است؛ چون سابقه سیستم را تخریب می‌کند. اگر اطلاعاتی در این جداول دیگر معتبر نیست باید تنها تاریخ پایان دوره آن به روز شوند یا یک رکورد جدید بر اساس بازه‌ای جدید ثبت گردد.
همچنین به روز رسانی‌ها در این جداول نیز معادل هستند با یک Insert جدید به همراه فیلد record_date و نه به روز رسانی واقعی یک رکورد قبلی  (شبیه به سیستم‌های حسابداری باید عمل کرد).
یک مثال:
فرض کنید حقوق کارمندی که مثال زده شد، در مهرماه به ساعتی 2400 تومان افزایش یافته است و حقوق نهایی نیز پرداخته شده است. بعد از یک ماه مشخص می‌شود که مدیر عامل سیستم گفته بوده است که ساعتی 2500 تومان و نه ساعتی 2400 تومان! (از این نوع مسایل در دنیای واقعی زیاد رخ می‌دهند!) خوب؛ اکنون چه باید کرد؟ آیا باید رفت و رکورد ساعتی 2400 تومان را به روز کرد؟ خیر. چون سابقه پرداخت واقعی صورت گرفته را تخریب می‌کند. به روز رسانی شما ابدا به این معنا نخواهد بود که دریافتی واقعی شخص در آن تاریخ خاص، ساعتی 2500 بوده است.
بنابراین در جداول Temporal، تنها «تغییرات افزودنی» مجاز هستند و این تغییرات همواره به عنوان آخرین رکورد جدول ثبت می‌شوند. به این ترتیب می‌توان اصطلاحا «مابه التفاوت» حقوق پرداخت نشده را به شخص خاصی، محاسبه و پرداخت کرد (می‌دانیم در یک بازه زمانی خاص به او چقد حقوق داده‌ایم. همچنین می‌دانیم که این بازه در یک record_date دیگر لغو و با عددی دیگر، جایگزین شده‌است).


برای مطالعه بیشتر
Bitemporal Database Table Design - The Basics
Temporal Data Techniques in SQL
Database Design: A Point in Time Architecture
Temporal database
Temporal Patterns



راه حلی دیگر؛ استفاده از بانک‌های اطلاعاتی NoSQL
بانک‌های اطلاعاتی NoSQL برخلاف بانک‌های اطلاعاتی رابطه‌ای برای اعمال Read بهینه سازی می‌شوند و نه برای Write. در چند دهه قبل که بانک‌های اطلاعاتی رابطه‌ای پدیدار شدند، یک سخت دیسک 10 مگابایتی حدود 4000 دلار قیمت داشته است. به همین جهت مباحث نرمال سازی اطلاعات و ذخیره نکردن اطلاعات تکراری تا این حد در این نوع بانک‌های اطلاعاتی مهم بوده است. اما در بانک‌های اطلاعاتی NoSQL امروزی، اگر قرار است فیش حقوقی شخصی ثبت شود، می‌توان کل اطلاعات جاری او را یکجا داخل یک سند ثبت کرد (از اطلاعات شخص در آن تاریخ تا اطلاعات تمام اجزای فیش حقوقی در قالب یک شیء تو در توی JSON). به همین جهت بسیار سریع هستند برای اعمال Read و گزارشگیری. همچنین این نوع سیستم‌ها برای نگهداری نگارش‌های مختلف یک سند بهینه سازی شده‌اند و جزو ساختار توکار آن‌ها است. بنابراین در این نوع سیستم‌ها اگر نیاز است از یک سند خاصی گزارش بگیریم، دقیقا اطلاعات همان تاریخ خاص را دارا است و اگر اطلاعات پایه سیستم را به روز کنیم، از امروز به بعد در سندهای جدید ثبت خواهد شد. این نوع سیستم‌ها رابطه‌ای نیستند و بسیاری از مباحث نرمال سازی اطلاعات در آن‌ها ضرورتی ندارد. قرار است یک فیش حقوقی شخص را نمایش دهیم؟ خوب، چرا تمام اطلاعات مورد نیاز او را در قالب یک شیء JSON تو در توی حاضر و آماده نداشته باشیم؟