نظرات مطالب
تولید SiteMap استاندارد و ایجاد یک ActionResult اختصاصی برای Return کردن SiteMap تولید شده
کش کردن یک روش است برای کاهش ساخت خروجی پویا به ازای هر درخواست (سؤال مطرح شده) و از آن برداشت نمی‌شود که روش‌های دیگر خوب هستند یا خیر.
نظرات مطالب
اطلاع از بروز رسانی نرم افزار ساخته شده

ممنون از شما. یک روش برای اینکه مستقیما با XML Reader کار نکنیم می‌تونه استفاده از روش‌های سریالایز کردن کلاس‌ها باشه. دردسرش کمتره.

یک سؤال: این فلش‌های انحنا دار رو با چه برنامه‌ای ایجاد کردید؟

نظرات مطالب
رمزنگاری کانکشن استرینگ در ASP.Net
- این مطلب هست (البته این خیلی ساده است؛ ولی ایده‌اش مهم است).
- بله (در صورت دسترسی به سرور و یا وجود باگ LFI). البته روش‌های obfuscation می‌تونه در این حالت مفید باشه.
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت هشتم - دریافت اطلاعات از سرور
با سلام. در یک پروژه Angular 5 داده‌های ComboBox‌ها باید در سمت کلاینت کش شوند. بنده در ngOnInit مربوط به app.component عملیات دریافت این داده‌ها را با http.get انجام میدهم و داده‌ها در یک پروپرتی Observable کش می‌شوند. حالا در فرم هایی که به این داده‌ها نیاز دارم در متد ngOnInit اقدام به دریافت داده‌ها از کش میکنم اما هنوز داده‌ها دریافت نشده اند (مشکل همزمانی).
همچنین بنده lifecycle Angular رو بررسی کردم. از ngAfterContentInit هم برای دریافت داده‌ها در فرم‌ها استفاده کردم اما باز هم موقع اجرای این متد(ngAfterContentInit   ) هنور متد subscribe مربوط به http.get اجرا نشده است.
 لطفا در صورت امکان راهنمایی کنید. با تشکر
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی
در قسمت قبل، ساختار ابتدایی کلاینت Angular را تدارک دیدیم. در این قسمت قصد داریم سرویسی که زیر ساخت کامپوننت لاگین و عملیات ورود به سیستم را تامین می‌کند، تکمیل کنیم.


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

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

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

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

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

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


طراحی سرویس Auth

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

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

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


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

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

@Injectable()
export class AuthService {

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

  constructor() {
    this.updateStatusOnPageRefresh();
  }

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


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

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

export enum AuthTokenType {
  AccessToken,
  RefreshToken
}

@Injectable()
export class AuthService {

  private rememberMeToken = "rememberMe_token";

  constructor(private browserStorageService: BrowserStorageService) {  }

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

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

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

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

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


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


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

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

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


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

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

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

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


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

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

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

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


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
نظرات مطالب
مخفی کردن کوئری استرینگ‌ها در ASP.NET MVC توسط امکانات Routing
- بحث مطلب جاری در مورد ASP.NET MVC است و ساز و کار استاندارد آن؛ نه در مورد وب فرم‌ها یا روش‌های سفارشی دیگر. هنگام تعریف مسیر اسکریپت‌ها در MVC اگر از Url.Content و ~ برای ذکر ریشه سایت، استفاده شده باشد، موتور توکار MVC مسیرها را به صورت خودکار اصلاح می‌کند. مثلا:
<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
- پیشتر در مورد دیباگ اسکریپت‌های یک سایت مطلبی تهیه شده بود: (^)

الان مرورگر، مسیر اسکریپت‌های شما را از انتهای مسیر یک مطلب دریافت می‌کند نه از ریشه سایت. برای وب فرم‌ها هم روش ذیل وجود دارد:
<script language="javascript" src='<%=ResolveUrl("~/App_Themes/MainTheme/jquery.js")%>' type='text/javascript'></script>
البته در این حالت هدر صفحه باید runat server داشته باشد:
<head runat="server">
و یا از اسکریپت منیجر استفاده کنید:
<asp:ScriptManager ID="ScriptManager1" runat="server">
        <Scripts>
            <asp:ScriptReference Path="~/js/somefile.js" />
        </Scripts>
</asp:ScriptManager>
مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت چهارم
در قسمت قبل، اطلاعات نمایش داده شده، از یک سری آرایه ثابت جاوا اسکریپتی تامین شدند. در یک برنامه‌ی واقعی نیاز است داده‌ها را یا از HTML 5 local storage تامین کرد و یا از سرور به کمک Ajax. برای اینگونه اعمال، ember.js به همراه افزونه‌ای است به نام Ember Data که جزئیات کار با آن‌را در این قسمت بررسی خواهیم کرد.


استفاده از Ember Data با Local Storage

برای کار با HTML 5 local storage نیاز به Ember Data Local Storage Adapter نیز هست که در قسمت اول این سری، آدرس دریافت آن معرفی شد. این فایل‌ها نیز در پوشه‌ی Scripts\Libs برنامه کپی خواهند شد.
در ادامه به فایل Scripts\App\store.js که در قسمت قبل جهت تعریف دو آرایه ثابت مطالب و نظرات اضافه شد، مراجعه کرده و محتوای فعلی آن‌را با کدهای زیر جایگزین کنید:
Blogger.ApplicationSerializer = DS.LSSerializer.extend();
Blogger.ApplicationAdapter = DS.LSAdapter.extend();
این تعاریف سبب خواهند شد تا Ember Data از Local Storage Adapter استفاده کند.
در ادامه با توجه به حذف دو آرایه‌ی posts و comments که پیشتر در فایل store.js تعریف شده بودند، نیاز است مدل‌های متناظری را جهت تعریف خواص آن‌ها، به برنامه اضافه کنیم. این‌کار را با افزودن دو فایل جدید comment.js و post.js به پوشه‌ی Scripts\Models انجام خواهیم داد.
محتوای فایل Scripts\Models\post.js :
Blogger.Post = DS.Model.extend({
  title: DS.attr(),
  body: DS.attr()
});
محتوای فایل Scripts\Models\comment.js :
Blogger.Comment = DS.Model.extend({
  text: DS.attr()
});
سپس مداخل تعریف آن‌ها را به فایل index.html نیز اضافه خواهیم کرد:
 <script src="Scripts/Models/post.js" type="text/javascript"></script>
<script src="Scripts/Models/comment.js" type="text/javascript"></script>

برای تعاریف مدل‌ها در Ember data مرسوم است که نام مدل‌ها، اسامی جمع نباشند. سپس با ایجاد وهله‌ای از DS.Model.extend یک مدل ember data را تعریف خواهیم کرد. در این مدل، خواص هر شیء را مشخص کرده و مقدار آن‌ها همیشه ()DS.attr خواهد بود. این نکته را در دو مدل Post و Comment مشاهده می‌کنید. اگر دقت کنید به هر دو مدل، خاصیت id اضافه نشده‌است. این خاصیت به صورت خودکار توسط Ember data تنظیم می‌شود.

اکنون نیاز است برنامه را جهت استفاده از این مدل‌های جدید به روز کرد. برای این منظور فایل Scripts\Routes\posts.js را گشوده و مدل آن‌را به نحو ذیل ویرایش کنید:
Blogger.PostsRoute = Ember.Route.extend({
    //controllerName: 'posts', // مقدار پیش فرض است و نیازی به ذکر آن نیست
    //renderTemplare: function () {
    //    this.render('posts'); // مقدار پیش فرض است و نیازی به ذکر آن نیست
    //},
    model: function () {
        return this.store.find('post');
    }
});
در اینجا this.store معادل data store برنامه است که مطابق تنظیمات برنامه، همان ember data می‌باشد. سپس متد find را به همراه نام مدل، به صورت رشته‌ای در اینجا مشخص می‌کنیم.
به همین ترتیب فایل Scripts\Routes\recent-comments.js را نیز جهت استفاده از data store ویرایش خواهیم کرد:
Blogger.RecentCommentsRoute = Ember.Route.extend({
    model: function () {
        return this.store.find('comment');
    }
});
و فایل Scripts\Routes\post.js که در آن منطق یافتن یک مطلب بر اساس آدرس مختص به آن قرار دارد، به صورت ذیل بازنویسی می‌شود:
Blogger.PostRoute = Ember.Route.extend({
    model: function (params) {
        return this.store.find('post', params.post_id);
    }
});
اگر متد find بدون پارامتر ذکر شود، به معنای بازگشت تمامی عناصر موجود در آن مدل خواهد بود و اگر پارامتر دوم آن مانند این مثال تنظیم شود، تنها همان وهله‌ی درخواستی را بازگشت می‌دهد.


افزودن امکان ثبت یک مطلب جدید

تا اینجا اگر برنامه را اجرا کنید، برنامه بدون خطا بارگذاری خواهد شد اما فعلا رکوردی را برای نمایش ندارد. در ادامه، برنامه را جهت افزودن مطالب جدید توسعه خواهیم داد. برای اینکار ابتدا به فایل Scripts\App\router.js مراجعه کرده و سپس مسیریابی جدید new-post را تعریف خواهیم کرد:
Blogger.Router.map(function () {
    this.resource('posts', { path: '/' });
    this.resource('about');
    this.resource('contact', function () {
        this.resource('email');
        this.resource('phone');
    });
    this.resource('recent-comments');
    this.resource('post', { path: 'posts/:post_id' });
    this.resource('new-post');
});
اکنون در صفحه‌ی اول سایت، توسط قالب Scripts\Templates\posts.hbs، دکمه‌ای را جهت ایجاد یک مطلب جدید اضافه خواهیم کرد:
<h2>Ember.js blog</h2>
<ul>
    {{#each post in arrangedContent}}
    <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li>
    {{/each}}
</ul>
 
<a href="#" class="btn btn-primary" {{action 'sortByTitle' }}>Sort by title</a>
{{#link-to 'new-post' classNames="btn btn-success"}}New Post{{/link-to}}
در اینجا دکمه‌ی New Post به مسیریابی جدید new-post اشاره می‌کند.
برای تعریف عناصر نمایشی این مسیریابی، فایل جدید قالب Scripts\Templates\new-post.hbs را با محتوای زیر اضافه کنید:
<h1>New post</h1>
<form>
  <div class="form-group">
    <label for="title">Title</label>
    {{input value=title id="title" class="form-control"}}
  </div>

  <div class="form-group">
    <label for="body">Body</label>
    {{textarea value=body id="body" class="form-control" rows="5"}}
  </div>


  <button class="btn btn-primary" {{action 'save'}}>Save</button>
</form>
با نمونه‌ی این فرم در قسمت قبل در حین ویرایش یک مطلب، آشنا شدیم. دو المان دریافت اطلاعات در آن قرار دارند که هر کدام به خواص مدل برنامه bind شده‌اند. همچنین یک دکمه‌ی save، با اکشنی به همین نام در اینجا تعریف شده‌است.
پس از آن نیاز است نام فایل قالب new-post را به template loader برنامه در فایل index.html اضافه کرد:
<script type="text/javascript">
    EmberHandlebarsLoader.loadTemplates([
       'posts', 'about', 'application', 'contact', 'email', 'phone',
       'recent-comments', 'post', 'new-post'
    ]);
</script>
برای مدیریت دکمه‌ی save این قالب جدید نیاز است کنترلر جدیدی را در فایل جدید Scripts\Controllers\new-post.js تعریف کنیم؛ با این محتوا:
Blogger.NewPostController = Ember.Controller.extend({
    actions: {
        save: function () {
            var newPost = this.store.createRecord('post', {
                title: this.get('title'),
                body: this.get('body')
            });
            newPost.save();
            this.transitionToRoute('posts');
        }
    }
});
به همراه افزودن مدخلی از آن به فایل index.html برنامه:
 <script src="Scripts/Controllers/new-post.js" type="text/javascript"></script>

در اینجا کنترلر جدید NewPostController را مشاهده می‌کنید. از این جهت که برای دسترسی به خواص مدل تغییر کرده، از متد this.get استفاده شده‌است، نیازی نیست حتما از یک ObjectController مانند قسمت قبل استفاده کرد و Controller معمولی نیز برای اینکار کافی است.
آرگومان اول this.store.createRecord نام مدل است و آرگومان دوم آن، وهله‌ای که قرار است به آن اضافه شود. همچنین باید دقت داشت که برای تنظیم یک خاصیت، از متد this.set و برای دریافت مقدار یک خاصیت تغییر کرده از this.get به همراه نام خاصیت مورد نظر استفاده می‌شود و نباید مستقیما برای مثال از this.title استفاده کرد.
this.store.createRecord صرفا یک شیء جدید (ember data object) را ایجاد می‌کند. برای ذخیره سازی نهایی آن باید متد save آن‌را فراخوانی کرد (پیاده سازی الگوی active record است). به این ترتیب این شیء در local storage ذخیره خواهد شد.
پس از ذخیره‌ی مطلب جدید، از متد this.transitionToRoute استفاده شده‌است. این متد، برنامه را به صورت خودکار به صفحه‌ی متناظر با مسیریابی posts هدایت می‌کند.

اکنون برنامه را اجرا کنید. بر روی دکمه‌ی سبز رنگ new post در صفحه‌ی اول کلیک کرده و یک مطلب جدید را تعریف کنید. بلافاصله عنوان و لینک متناظر با این مطلب را در صفحه‌ی اول سایت مشاهده خواهید کرد.
همچنین اگر برنامه را مجددا بارگذاری کنید، این مطالب هنوز قابل مشاهده هستند؛ زیرا در local storage مرورگر ذخیره شده‌اند.


در اینجا اگر به لینک‌های تولید شده دقت کنید، id آن‌ها عددی نیست. این روشی است که local storage با آن کار می‌کند.


افزودن امکان حذف یک مطلب به سایت

برای حذف یک مطلب، دکمه‌ی حذف را به انتهای قالب Scripts\Templates\post.hbs اضافه خواهیم کرد:
<h2>{{title}}</h2>
{{#if isEditing}}
<form>
    <div class="form-group">
        <label for="title">Title</label>
        {{input value=title id="title" class="form-control"}}
    </div>
    <div class="form-group">
        <label for="body">Body</label>
        {{textarea value=body id="body" class="form-control" rows="5"}}
    </div>
    <button class="btn btn-primary" {{action 'save' }}>Save</button>
</form>
{{else}}
<p>{{body}}</p>
<button class="btn btn-primary" {{action 'edit' }}>Edit</button>
<button class="btn btn-danger" {{action 'delete' }}>Delete</button>
{{/if}}


سپس کنترلر Scripts\Controllers\post.js را جهت مدیریت اکشن جدید delete به نحو ذیل تکمیل می‌کنیم:
Blogger.PostController = Ember.ObjectController.extend({
    isEditing: false,
    actions: {
        edit: function () {
            this.set('isEditing', true);
        },
        save: function () {
            var post = this.get('model');
            post.save();

            this.set('isEditing', false);
        },
        delete: function () {
            if (confirm('Do you want to delete this post?')) {
                this.get('model').destroyRecord();
                this.transitionToRoute('posts');
            }
        }
    }
});
متد destroyRecord، مدل انتخابی را هم از حافظه و هم از data store حذف می‌کند. سپس کاربر را به صفحه‌ی اصلی سایت هدایت خواهیم کرد.
متد save نیز در اینجا بهبود یافته‌است. ابتدا مدل جاری دریافت شده و سپس متد save بر روی آن فراخوانی می‌شود. به این ترتیب اطلاعات از حافظه به local storage نیز منتقل خواهند شد.


ثبت و نمایش نظرات به همراه تنظیمات روابط اشیاء در Ember Data

در ادامه قصد داریم امکان افزودن نظرات را به مطالب، به همراه نمایش آن‌‌ها در ذیل هر مطلب، پیاده سازی کنیم. برای اینکار نیاز است رابطه‌ی بین یک مطلب و نظرات مرتبط با آن‌را در مدل ember data مشخص کنیم. به همین جهت فایل Scripts\Models\post.js را گشوده و تغییرات ذیل را به آن اعمال کنید:
Blogger.Post = DS.Model.extend({
  title: DS.attr(),
  body: DS.attr(),
  comments: DS.hasMany('comment', { async: true })
});
در اینجا خاصیت جدیدی به نام comments به مدل مطلب اضافه شده‌است و توسط آن می‌توان به تمامی نظرات یک مطلب دسترسی یافت؛ تعریف رابطه‌ی یک به چند، به کمک متد DS.hasMany که پارامتر اول آن نام مدل مرتبط است. تعریف async: true برای کار با local storage اجباری است و در نگارش‌های آتی ember data حالت پیش فرض خواهد بود.
همچنین نیاز است یک سر دیگر رابطه را نیز مشخص کرد. برای این منظور فایل Scripts\Models\comment.js را گشوده و به نحو ذیل تکمیل کنید:
Blogger.Comment = DS.Model.extend({
    text: DS.attr(),
    post: DS.belongsTo('post', { async: true })
});
در اینجا خاصیت جدید post به مدل نظر اضافه شده‌است و مقدار آن از طریق متد DS.belongsTo که مدل post را به یک نظر، مرتبط می‌کند، تامین خواهد شد. بنابراین در این حالت اگر به شیء comment مراجعه کنیم، خاصیت جدید post.id آن، به id مطلب متناظر اشاره می‌کند.

در ادامه نیاز است بتوان تعدادی نظر را ثبت کرد. به همین جهت با تعریف مسیریابی آن شروع می‌کنیم. این مسیریابی تعریف شده در فایل Scripts\App\router.js نیز باید تو در تو باشد؛ زیرا قسمت ثبت نظر (new-comment) دقیقا داخل همان صفحه‌ی نمایش یک مطلب ظاهر می‌شود:
Blogger.Router.map(function () {
    this.resource('posts', { path: '/' });
    this.resource('about');
    this.resource('contact', function () {
        this.resource('email');
        this.resource('phone');
    });
    this.resource('recent-comments');
    this.resource('post', { path: 'posts/:post_id' }, function () {
        this.resource('new-comment');
    });
    this.resource('new-post');
});
لینک آن‌را نیز به انتهای فایل Scripts\Templates\post.hbs اضافه می‌کنیم. از این جهت که این لینک به مدل جاری اشاره می‌کند، با استفاده از متغیر this، مدل جاری را به عنوان مدل مورد استفاده مشخص خواهیم کرد:
<h2>{{title}}</h2>
{{#if isEditing}}
<form>
    <div class="form-group">
        <label for="title">Title</label>
        {{input value=title id="title" class="form-control"}}
    </div>
    <div class="form-group">
        <label for="body">Body</label>
        {{textarea value=body id="body" class="form-control" rows="5"}}
    </div>
    <button class="btn btn-primary" {{action 'save' }}>Save</button>
</form>
{{else}}
<p>{{body}}</p>
<button class="btn btn-primary" {{action 'edit' }}>Edit</button>
<button class="btn btn-danger" {{action 'delete' }}>Delete</button>
{{/if}}
 
<h2>Comments</h2>
{{#each comment in comments}}
<p>
    {{comment.text}}
</p>
{{/each}}
 
<p>{{#link-to 'new-comment' this class="btn btn-success"}}New comment{{/link-to}}</p>
{{outlet}}
پس از تکمیل روابط مدل‌ها، قالب Scripts\Templates\post.hbs را جهت استفاده از این خواص به روز خواهیم کرد. در تغییرات جدید، قسمت <h2>Comments</h2> به انتهای صفحه اضافه شده‌است. سپس حلقه‌ای بر روی خاصیت جدید comments تشکیل شده و مقدار خاصیت text هر آیتم نمایش داده می‌شود.
در انتهای قالب نیز یک {{outlet}} اضافه شده‌است. کار آن نمایش قالب ارسال یک نظر جدید، پس از کلیک بر روی لینک New Comment می‌باشد. این قالب را با افزودن فایل Scripts\Templates\new-comment.hbs با محتوای ذیل ایجاد خواهیم کرد:
<h2>New comment</h2>

<form>
  <div class="form-group">
    <label for="text">Your thoughts:</label>
    {{textarea value=text id="text" class="form-control" rows="5"}}
  </div>

  <button class="btn btn-primary" {{action "save"}}>Add your comment</button>
</form>
سپس نام این قالب را به template loader فایل index.html نیز اضافه می‌کنیم؛ تا در ابتدای بارگذاری برنامه شناسایی شده و استفاده شود:
<script type="text/javascript">
    EmberHandlebarsLoader.loadTemplates([
       'posts', 'about', 'application', 'contact', 'email', 'phone',
       'recent-comments', 'post', 'new-post', 'new-comment'
    ]);
</script>
این قالب به خاصیت text یک comment متصل بوده و همچنین اکشن جدیدی به نام save دارد. بنابراین برای مدیریت اکشن save، نیاز به کنترلری متناظر خواهد بود. به همین جهت فایل جدید Scripts\Controllers\new-comment.js را با محتوای ذیل ایجاد کنید:
Blogger.NewCommentController = Ember.ObjectController.extend({
    needs: ['post'],
    actions: {
        save: function () {
            var comment = this.store.createRecord('comment', {
                text: this.get('text')
            });
            comment.save();
 
            var post = this.get('controllers.post.model');
            post.get('comments').pushObject(comment);
            post.save();
 
            this.transitionToRoute('post', post.id);
        }
    }
});
و مدخل تعریف آن‌را نیز به صفحه‌ی index.html اضافه می‌کنیم:
 <script src="Scripts/Controllers/new-comment.js" type="text/javascript"></script>

قسمت ذخیره سازی comment جدید با ذخیره سازی یک post جدید که پیشتر بررسی کردیم، تفاوتی ندارد. از متد this.store.createRecord جهت معرفی وهله‌ای جدید از comment استفاده و سپس متد save آن، برای ثبت نهایی فراخوانی شده‌است.
در ادامه باید این نظر جدید را به post متناظر با آن مرتبط کنیم. برای اینکار نیاز است تا به مدل کنترلر post دسترسی داشته باشیم. به همین جهت خاصیت needs را به تعاریف کنترلر جاری به همراه نام کنترلر مورد نیاز، اضافه کرده‌‌ایم. به این ترتیب می‌توان توسط متد this.get و پارامتر controllers.post.model در کنترلر NewComment به اطلاعات کنترلر post دسترسی یافت. سپس خاصیت comments شیء post جاری را یافته و مقدار آن‌را به comment جدیدی که ثبت کردیم، تنظیم می‌کنیم. در ادامه با فراخوانی متد save، کار تنظیم ارتباطات یک مطلب و نظرهای جدید آن به پایان می‌رسد.
در آخر با فراخوانی متد transitionToRoute به مطلبی که نظر جدیدی برای آن ارسال شده‌است باز می‌گردیم.


همانطور که در این تصویر نیز مشاهده می‌کنید، اطلاعات ذخیره شده در local storage را توسط افزونه‌ی Ember Inspector نیز می‌توان مشاهده کرد.


افزودن دکمه‌ی حذف به لیست نظرات ارسالی

برای افزودن دکمه‌ی حذف، به قالب Scripts\Templates\post.hbs مراجعه کرده و قسمتی را که لیست نظرات را نمایش می‌دهد، به نحو ذیل تکمیل می‌کنیم:
{{#each comment in comments}}
<p>
    {{comment.text}}
    <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button>
</p>
{{/each}}
همچنین برای مدیریت اکشن جدید delete، کنترلر جدید comment را در فایل Scripts\Controllers\comment.js اضافه خواهیم کرد.
Blogger.CommentController = Ember.ObjectController.extend({
    needs: ['post'],
    actions: {
        delete: function () {
            if (confirm('Do you want to delete this comment?')) {
                var comment = this.get('model');
                comment.deleteRecord();
                comment.save();
 
                var post = this.get('controllers.post.model');
                post.get('comments').removeObject(comment);
                post.save(); 
            }
        }
    }
});
به همراه تعریف مدخل آن در فایل index.html :
 <script src="Scripts/Controllers/comment.js" type="text/javascript"></script>

در این حالت اگر برنامه را اجرا کنید، پیام «Do you want to delete this post» را مشاهده خواهید کرد بجای پیام «Do you want to delete this comment». علت اینجا است که قالب post به صورت پیش فرض به کنترلر post متصل است و نه کنترلر comment. برای رفع این مشکل تنها کافی است از itemController به نحو ذیل استفاده کنیم:
{{#each comment in comments  itemController="comment"}}
<p>
    {{comment.text}}
    <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button>
</p>
{{/each}}
به این ترتیب اکشن delete به کنترلر comment ارسال خواهد شد و نه کنترلر پیش فرض post جاری.
در کنترلر Comment روش دیگری را برای حذف یک رکورد مشاهده می‌کنید. می‌توان ابتدا متد deleteRecord را بر روی مدل فراخوانی کرد و سپس آن‌را save نمود تا نهایی شود. همچنین در اینجا نیاز است نظر حذف شده را از سر دیگر رابطه نیز حذف کرد. روش دسترسی به post جاری در این حالت، همانند توضیحات NewCommentController است که پیشتر بحث شد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_04.zip
 
مطالب
بهبود Time ZoneInfo در دات نت 6
با ارائه net core.، شاهده استفاده‌ی گسترده از DateTimeOffset بجای DateTime بودیم و این استفاده به مرور در کدهای خودمان هم ورود کرد. DateTimeOffset به شما این اجازه را میدهد که دقیقا بدانید ساعت وارد شده در سیستم، دقیقا مربوطه به چه منطقه زمانی است. در پروژه‌ای که مدتی است با نسخه‌ی Asp.net core 3.1 در حال طراحی و به روزرسانی بخش‌های مختلف آن هستیم، یکی از موارد، سیستم زمانی آن است که باید در زمان‌های مناسب و مدنظر کاربر، فعالیت‌های تنظیم شده‌ی توسط کاربر را به انجام برساند.
در اینجا سامانه به صورت Paas آغاز به فعالیت کرد. در این نوع سیستم‌ها، تاریخ و ساعت سرور بر اساس منطقه‌ی زمانی صفر، یعنی گرینویچ یا نصف النهار مبدا تنظیم شده‌است. یعنی زمانی که شما ساعت سیستم را دریافت میکنید، Offset بر روی صفر تنظیم شده‌است و اگر کاربر تهران (ایران) ساعتی را بخواهد تنظیم کند، این تاریخ حتما باید یک آفست 3.5 داشته باشد تا محاسبات زمانی سیستم دقیق کار کند؛ در غیر اینصورت سه ساعت و نیم زودتر، فعالیت موردنظر آغاز میگردد و باعث خطا و بی اعتبار شدن سامانه میگردد.
 همچنین در بعضی کشورهای مانند ایران، Daylight Saving Time یا به اختصار DST نیز پیاده سازی میشود که در ین حالت در کشور مربوطه، نیمه اول سال که هوا گرم‌تر است ساعت‌ها یک ساعت به جلو کشیده شده و با آغاز روزهای سرد سال، ساعت بجای قبلی خود باز میگردد و به همین دلیل، آفست زمانی آن 4.5 میباشد که البته ممکن است بنا به دلایلی این DST اجرا نگردد که  بیش از ده سال پیش کشور ما نیز چنین تجربه‌ای را چند سالی داشت.
به همین دلیل باید این موضوع تغییر آفست را جدی گرفت. حال در سامانه‌ی ما گاها افرادی هستند که در خارج از کشور با آن کار میکنند و از آنجا که سامانه بر اساس ساعت تهران این فعالیت را انجام میدهد، باید در این قسمت به روزرسانی‌هایی را به انجام میرساندیم. بدین ترتیب در قسمت پروفایل کاربر، لیستی به نام منطقه زمانی قرار دادیم تا بر اساس آن، کاربر بتواند منطقه زمانی خود را انتخاب کند تا از این پس، زمان خود را بر اساس زمان تهران مشخص نکند. پس با استفاده از کلاس موجود در دات نت TimeZoneInfo  لیستی از TimeZone‌ها را به دست آوردم که هر timezone شامل ID، نام نمایشی، میزان آفست آن منطقه، آیا شامل DST میگردد یا خیر و ... میشود.
 در این حالت برای هر کاربر، Zone ID را ذخیره کرده و با هر بار استفاده بر اساس ZoneId، اطلاعات آن منطقه زمانی را به دست می‌آوردم. همه چیز درست کار میکرد ولی زمانیکه سامانه توسط داکر روی سرویس‌های Paas اجرا شد، در این حالت چون سیستم مذکور یک لینوکس است که توسط Nginx اجرا میگردد و اینکه TimeZoneInfo یک سیستم WindowsBased است، به مشکل برخورد کرد و نتوانست ZoneID مورد نظر را در سیستم بیابد. سیستم‌های دیگر مانند لینوکس، از ساختاری با نام IANA  میباشد که توسط ICANN حمایت میشود و دیتابیس‌های به روز آن در این آدرس قرار دارد.  به همین علت سیستم مجددا RollBack شد تا آن را اصلاح نماییم.


جهت اصلاح این مشکل سه مورد زیر را بررسی کردم:
یک. استفاده از کتابخانه مبدل منطقه زمانی: این کتابخانه تبدیل حالت‌های زمانی از ویندوز به IANA و بالعکس را دارد. همچنین این امکان را به شما میدهد که به راحتی با وارد نمودن ZoneId به هر شکلی که وارد شود، چه به صورت ویندوزی و چه به صورت IANA باشد، خروجی TimeZoneInfo مورد نظر را تامین مینماید. البته در سیستم عامل ویندوز همان شکل قبل را دارد ولی در حالت لینوکسی، نام نمایشی مناطق شکل مناسبی ندارد و فقط بیشتر به شکل اختلاف زمانی میباشد که دو مرتبه تکرار شده است.
دو. استفاده از کتابخانه NodaTime: کتابخانه NodaTime، کتابخانه‌ی خوشنامی در مورد مسائل تاریخ و ساعت میباشد و همچنین قابلیت مناطق زمانی را بر اساس سیستم IANA فراهم میکند 
سه. ارتقا پروژه به دات نت 6:  تیم دات نت، در نسخه‌ی ،6 در زمینه‌ی تاریخ و ساعت بهبود‌های فراوانی داشته‌اند که تایپ‌های جدید DateOnly و TimeOnly نیز نمونه‌هایی از آن میباشد ولی بهبودهایی هم در زمینه TimeZone‌ها نیز صورت گرفته‌است و از حالت WindowsBased به حالت CrossPlatform تغییر یافته‌است. در حال حاضر در صورتیکه در نسخه 6، شما از مناطق زمانی چه در ویندوز و چه در لینوکس استفاده کنید، یا اینکه در یک سیستم IANA از ZoneId‌های ویندوزی استفاده کنید و یا بالعکس، هیچ مشکلی وجود ندارد.
با توجه به این موضوع، ما سیستم را از نسخه‌ی 3.1 به نسخه 6 به روزرسانی کردیم و به راحتی و بدون تغییر کدها توانستیم مناطق زمانی را به راحتی بدست آوریم. البته این نکته را باید مدنظر داشته باشید که در سیستم ویندوزی، همه چیز مطابق قبل است ولی در یک سیستم مبتنی بر IANA، ممکن است بعضی از نام‌های نمایشی تکراری باشند، ولی ZONEID‌ها متفاوت هستند؛ مثلا برای ایران دو منطقه زمانی، یا برای ترکیه سه یا چهار منطقه زمانی وجود داشت؛ با نام‌های نمایشی یکسان ولی با ZoneId‌های متفاوت به شکل Iran و دیگری به شکل Asia/tehran بود و ترکیه هم با چند نام شهر متفاوت است.
مطالب
آموزش مفاهیم Data Warehouse

مفاهیم مقدماتی Data Warehouse :

OLTP   ( Online Transaction Processing ) : سیستم‌هایی می‌باشند که برای اهداف اصلی سازمان استفاده می‌شوند و این سیستم‌ها کار پردازش و ذخیره کردن داد‌ه‌ها را در OLTP Database انجام می‌دهند. مانند تمامی سیستم‌های ERP,MIS,…

OLTP Database  : پایگاه داده‌ی سیستم‌های OLTP می‌باشد. به طور معمول هر تراکنش کاربر در کمترین زمان ممکن برروی این سیستم‌ها ذخیره می‌گردد و در طول روز بار‌ها دستورات ( Insert/Update/Delete ) برروی آنها انجام می‌شود. این پایگاه‌های داده، همان Main Data ‌ها یا Source System ‌ها می‌باشند.

ETL  ( extract, transform, and load ) : مراحل انتقال داده از OLTP Database به پایگاه داده‌ی Stage می‌باشد. ETL سیستمی می‌باشد که توانایی اتصال به OLTP را دارد و اطلاعات را از OLTP واکشی می‌کند و به پایگاه داده‌ی Stage انتقال می‌دهد. سپس ETL داده‌ها را مجتمع ( integrates ) کرده و از Stage به DDS ( Dimensional Data Source ) انتقال می‌دهد .

Retrieves Data : عملیات واکشی داده‌ها طبق یک سری قوانین و قواعد می‌باشد .

برای انجام عملیات ETL دو روش وجود دارد

1. Data مجتمع ( Integrate ) و تمیز ( Data cleansing ) شود و در نهایت وارد Data Warehouse گردد.

2. Data وارد Data Warehouse گردد سپس مراحل مجتمع سازی و پاک سازی داده‌ها بر روی داده‌ها در خود Data Warehouse انجام گردد.

Consolidates Data : برخی شرکت‌ها داده‌های اصلی خودشان را در چندین پایگاه داده دارند. در این حالت برای انجام عملیات ETL باید داده‌ها تحکیم و مجتمع شوند و سپس در Data Warehouse  ذخیره شوند.

به طور کلی موارد زیر در فرایند   ETL در نظر گرفته می‌شود:

1. Data availability : برخی داده‌ها در یک سیستم وجود دارند ولی در سیستم دیگری وجود ندارند و یا تفاوت در نگهداری داده‌ها در سیستم‌های مختلف داریم. مثلا در یک سیستم آدرس در سه فیلد نگه داری می‌شود (کشور-شهر-آدرس) اما در سیستمی دیگر در دو فیلد(کشور-آدرس) نگه داری می‌شود. در این حالت باید ما در ETL راه کار هایی برای مجتمع کردن این موارد در نظر بگیریم.

2. Time ranges : در سیستم‌های مختلف امکان دارد بعد‌های زمانی مختلف باشد . مثلا در یک سیستم بررسی‌ها در بازه‌ی ساعتی و در سیستم دیگر بررسی‌ها در بازه‌ی روزانه یا ماهانه باشد . بنابر این در تجمیع داده‌ها باید این مورد مد نظر گرفته شود.

3. Definitions  : تعاریف در سیستم‌های مختلف می‌تواند متفاوت باشد. مثلا در یک سیستم، مبلغ کل فاکتور شامل مالیات می‌باشد ولی در سیستمی دیگر این مبلغ فاقد مالیات می‌باشد.

4. Conversion  : در فرآیند ETL باید باز از قواعد موجود در سیستم‌های مختلف آگاهی داشته باشیم. مثلا در یک سیستم ممکن است دما را به صورت سانتیگراد و در دیگری فارنهایت نگه داری کنند.

5. Matching : باید بررسی لازم را انجام دهیم که کدام داده مرتبط با کدام سیستم می‌باشد. به عبارت دیگر کدام سیستم مالک داده می‌باشد و دقیقا  داده‌ها در کدام سیستم معتبر‌تر می‌باشند. مثلا پرسنل، هم در سیستم حسابداری می‌باشند هم در سیستم پرسنلی؛ ولی معمولا داده‌های اصلی از سیستم پرسنلی می‌آیند.

Periodically : عملیات واکشی داده‌ها ( Retrieves Data ) و مجتمع سازی داده‌ها ( Consolidates Data ) در فرآیند   ETL فقط یکبار اتفاق نمی‌افتد و این مراحل در بازه‌های زمانی خاص تکرار می‌گردند. این واکشی و انتقال داده‌ها می‌تواند در روز چند بار تکرار شود یا می‌تواند چند روز یک بار اجرا گردد و این بستگی دارد به سیاست موجود در Data Warehouse .

DDS (Dimensional Data Source) (Data Warehouse) : یک پایگاه داده از نوع نرمال شده ( Normalized ) یا بعدی ( Dimensional ) می‌باشد. که داده‌های مجتمع شده و تمیز شده سیستم‌های OLTP را در خود جای داده است. این پایگاه داده برای واکشی‌های سیستم‌های آنالیز داده مورد استفاده قرار می‌گیرد. ورود اطلاعات در Data Warehouse به صورت Batch می‌باشد و به هیچ عنوان مانند پایگاه داده‌های OLTP ویرایش داده‌ها به صورت Online و هر زمان که داده‌ها تغییر می‌کنند، صورت نمی‌گیرد. اطلاعات در Data Warehouse معمولا به صورت تجمیع شده روزانه، ماهانه، فصلی یا سالانه می‌باشد. DDS ‌ها مجموعه ای از Dimensional Data Mart ‌ها هستند. و عمدتا به صورت denormalized می‌باشند.

Dimensional Data Mart : مجموعه ای از جداول Fact , Dimension می‌باشند که در یک بیزینس خاص باهم در ارتباط و مشترک می‌باشند.

dimensional data store schemas : طراحی‌های مختلفی از جداول Fact , Dimension در DDS وجود دارد که عبارتند از

1. Star schema : ساده‌ترین روش پیاده سازی Data Warehouse

2. Snowflake : در این روش جداول Dimension کمی نرمال سازی بیشتری دارند. سیستم‌های آنالیز داده با این روش بهتر کار می‌کنند.

3. Galaxy schemas : طراحی در این روش بسیار سخت و پیچیده می‌باشد. با این وجود فرایند ETL در این طراحی ساده‌تر انجام می‌شود.

نمونه‌ی طراحی Star به صورت زیر می‌باشد :

تفاوت‌های DDS و NDS :

1. در DDS ‌ها هیچ گونه نرمال سازی خاصی انجام نمی‌دهیم و عملا تمامی جداول را دینرمال کرده ایم، در حالی که در NDS تمامی جداول تا سطح سوم و گاهی تا سطح پنجم نرمال شده اند.

2. سرعت واکشی و پردازش کوئری‌ها روی DDS خیلی بیشتر از NDS ‌ها می‌باشد.

3. در صورتی که نیاز باشد Data Warehouse ‌های خیلی بزرگ طراحی کنیم با حجم بسیار زیاد توصیه می‌شود از NDS ‌ها استفاده شود در حالی که برای Data Warehouse ‌های کوچک و متوسط بهتر است از DDS ‌ها استفاده شود.

تصویر طراحی یک  (Enterprise Data Source = NDS) EDS در زیر آمده است :

History : جداول Data Warehouse میتوانند در طول زمان بسیار بزرگ شوند و دارای تعداد رکورد زیادی گردند. اینکه حداکثر داده‌های چند سال را در Data Warehouse نگه داری کنیم بستگی به سیاست‌های سازمانی دارد که سیستم OLAP برای آن تهیه می‌گردد. استفاده کردن از table partitioning می‌تواند در جبران افزایش تعداد رکورد کمک زیادی به ما بکند.

slowly changing dimension (SCD) : سه روش برای نگه داری سابقه‌ی تغییرات در جداول Dimension وجود دارد.

1. SCD type 1 : هیچ گونه سابقه‌ی تغییراتی را نگه داری نمی‌کنیم

2. SCD type 2 : سابقه‌ی تغییرات در ردیف‌ها نگه داری می‌شود. در این روش هر ردیف، شماره ردیف قبلی را دارد و تعداد نا محدودی از تغییرات را نگه داری می‌کنیم.

3. SCD type 3 : سابقه‌ی تغییرات در ستون‌ها نگه داری می‌شوند و فقط ردیف جاری و آخرین تغییرات را نگه داری می‌کنیم.

Query : فقط ETL حق تغییرات در Data Warehouse را دارد و کاربر نمی‌تواند Data Warehouse  را تغییر دهد. البته کاربران حق Query کردن از Data Warehouse را دارند.

دقت داشته باشید که کوئری‌های پیچیده در NDS ‌ها بسیار کندتر از همان کوئری در DDS می‌باشد.

Business Intelligence : مجموعه ای از فعالیت‌ها که در یک سازمان برای شناخت بهتر وضعیت Business آن سازمان انجام می‌شود. نتایج BI کمک بسیاری برای تصمیم گیری‌های تکنیکی و استراتژیکی درون سازمان می‌کند. همچنین کمک به بهبود فرایند‌های Business جاری می‌کند.

فعالیت‌های Business Intelligence در سه دسته بندی قرار می‌گیرند :

1. Reporting : گزارشاتی که از Data Warehouse گرفته می‌شود و به کاربر نمایش داده می‌شود و عمدتا این گزارشات به صورت tabular form می‌باشند.

2. OLAP : فعالیت‌های انجام شده روی MDB برای گرفتن گزارشات Drill-Down و ... می‌باشد.

3. Data mining : فرآیند واکشی و داده کاوی داده‌های درون سیستم می‌باشد، که منجر به کشف الگوها و رفتار‌ها و ارتباطات داده‌ها در سیستم می‌شود. توسط داده کاوی ما متوجه می‌شویم چرا برخی داده‌ها در سیستم تولید شده اند.

a. descriptive analytics : زمانی که از داده کاوی برای شرح وقایع گذشته و حال استفاده می‌شود.

b. predictive analytics : زمانی که از داده کاوی برای پیش بینی وقایع گذشته استفاده می‌شود.

Real time data warehouse  : به DW هایی گفته می‌شود که در کمترین زمان، تغییرات OLTP را در خود خواهند داشت. امروزه این نوع DW ‌ها تغییرات 5 دقیقه تا حداکثر 1 ساعت قبل را در خود دارند. برای دسترسی به چنین DW هایی دو راه زیر وجود دارد :

1. بر روی هر جدول، Trigger هایی باشد تا تغییرات را به DW انتقال دهد. (البته برای این منظور باید Business مربوط به ETL را در این تریگر‌ها نوشت)

2. سورس برنامه‌های اصلی کاربر ( OLTP ) تغییر کند تا علاوه بر OLTP Database ‌ها Data Warehouse را هم تغییر دهند.

روش‌های فوق بسیار روی سرعت و کارایی برنامه‌های اصلی تاثیر خواهند گذاشت.

NDS ( Normalize Data Source ) : در صورتی که طراحی Data Warehouse به صورت Dimensional نباشد و به صورت Normalize باشد، نوع Data Warehouse از نوع NDS می‌باشد.

روش ساخت MDB  :

OLTP Database -> ETL -> Stage Database ->  DDS (Dimensional Data Source = Data Warehouse) -> SSAS -> MDB

روش ساده‌تر ساخت Data Warehouse :

 

منظور از Source System  همان OLTP Database ‌ها می‌باشد.

به خاطر داشته باشید که Source System ‌ها جزئی از Data Warehouse نمی‌باشند.

از کاربرد‌های Data Warehouse می‌توان به موارد زیر اشاره کرد

1. Data Mining

2. استفاده در گزارشات

3. تجمیع داده ها

Data Mining کمک به درک بهتر Business جاری در سازمان می‌کند. همچنین منجر به کشف دانش از درون داده‌ها می‌شود.

برای Data Mining می‌توانید از انواع پایگاه داده‌های موجود مانند رابطه ای ، سلسله مراتبی و چند بعدی استفاده کرد . حتا می‌توان از فایل‌های XML , Excel نیز استفاده کرد.

Customer Relationship Management (CRM) :

منظور از مشتری، مصرف کننده‌ی سرویسی است که سازمان شما ارایه می‌کند. یک سیستم CRM شامل تمامی برنامه ایی می‌باشد که تمام فعالیت‌های مشتری را پشتیبانی می‌کند.

Operational Data Store (ODS) :

این پایگاه داده به صورت رابطه ای و نرمال شده می‌باشد و شامل تمامی اطلاعات پایگاه داده ای OLTP می‌باشد که در این پایگاه داده مجتمع شده اند. تفاوت ODS با Data Warehouse در این می‌باشد که داده‌ها در ODS با هر Transaction به روز می‌شوند (سرعت بروز رسانی اطلاعات در ODS بالاتر از DW می‌باشد).

Master Data Management (MDM)  :

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

1. transaction data

2. master data

transaction data : شامل داده ای transactional در سیستم‌های OLTP می‌باشد.

master data : توضیح دهنده‌ی Business جاری در سازمان می‌باشد.

برای تشخیص این دو نیاز است Business سازمان را به خوبی شناسایی نمایید. به عبارت دیگر رویداد‌های Business ی همان transaction data می‌باشند و master data شامل پاسخ‌های این سوال‌ها می‌باشد. چه کسی، چه چیزی و کجا در مورد Business transaction .

Customer data integration (CDI) : عبارت است از MDM در رابطه با مشتری داده ها. کار این قسمت عبارت است از واکشی، پاک سازی ، ذخیره سازی ، نگه داری و به اشتراک گذاشتن داده ای مشتری می‌باشد.

Unstructured Data : داده ای ذخیره شده در پایگاه داده ، structured Data می‌باشند و داده هایی مانند عکس و فیلم و صوت و ...

Service-Oriented Architecture (SOA) : یک متد ساخت برنامه می‌باشد که در این روش تمامی اجزا برنامه به صورت ماژول هایی دیده می‌شود که در آنها ارتباطات با دیگر سیستم‌ها به صورت سرویس می‌باشد و این زیر سیستم‌ها را می‌توان در پروژه‌های مختلف به کار برد.

Real-Time Data Warehouse : DW هایی که توسط ETL به روز می‌شوند در هنگامی که یک Transaction روی OLTP اتفاق می‌افتد.

مراحل انتقال داده از OLTP Database به MDB به صورت زیر می‌باشد.

Data quality : مکانیسم اطمینان بخشی از این که در DW دادهای مناسب و درست وارد می‌شوند. به عبارت دیگر DQ همان firewall برای DW در مقابل داده‌های نامناسب می‌باشد.

برای بهتر مشخص شدن مکان DQ شکل زیر را در نظر بگیرید

نحوه‌ی حرکت داده ای از OLTP به MDB اولین چیزی می‌باشد که شما باید به آن فکر کنید و برای آن روشی را انتخاب نمایید قبل از ساخت   Data Warehouse .

چهار روش برای معماری انتقال اطلاعات از OLTP به DW وجود دارد (البته به عنوان نمونه و شما می‌توانید از روش‌های دیگر و طراحی‌های مختلف و ترکیبی نیز بهره ببرید)

1. single DDS : در این روش فقط Stage , DDS وجود دارد.

2. NDS + DDS : در این روش علاوه بر Stage,DDS از NDS نیز استفاده می‌شود.

3. ODS + DDS : در این روش از Stage,ODS,DDS استفاده می‌گردد.

4. federated data warehouse (FDW ) : استفاده از چندین DW که با هم تجمیع شده اند.

تصویر Single DDS :

تصویر NDS + DDS :

تصویر ODS + DDS :

تصویر federated data warehouse (FDW ) :

منبع : Building a Data Warehouse With Examples in SQL Server  انتشارات Apress

مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک
همانطور که در قسمت قبل، با معرفی مقدماتی Middlewareها عنوان شد، تمام قابلیت‌های یک برنامه‌ی ASP.NET Core، به صورت پیش فرض غیرفعال هستند؛ مگر آنکه Middlewareهای مخصوص آن‌ها را به صورت دستی و با آگاهی کامل، به کلاس آغازین برنامه اضافه کنید. در این قسمت قصد داریم تعداد دیگری از این Middlewareهای توکار را مورد بررسی قرار دهیم.


فعال سازی پردازش فایل‌های استاتیک در برنامه‌های ASP.NET Core 1.0

در مورد پوشه‌ی جدید wwwroot در «قسمت 2 - بررسی ساختار جدید Solution» مطالبی عنوان شدند. جهت یادآوری:
اگر فایل Program.cs را بررسی کنید، یک چنین تعاریفی را مشاهده خواهید کرد:
public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();
 
        host.Run();
    }
}
در کدهای فوق، سطر UseContentRoot، پوشه‌ی خاصی را به نام content root معرفی می‌کند که در اینجا به همان پوشه‌ی اصلی برنامه اشاره می‌کند و پوشه‌ی wwwroot از مسیر content root/wwwroot خوانده می‌شود که جهت ارائه‌ی تمام فایل‌های عمومی برنامه مورد استفاده قرار می‌گیرد (مانند تصاویر، فایل‌های JS ،CSS و امثال آن). هدف این است که کدهای سمت سرور برنامه (قرار گرفته در content root) از کدهای عمومی آن (قرار گرفته در پوشه‌ی ویژه‌ی content root/wwwroot) جدا شده و به این ترتیب احتمال نشتی اطلاعات سمت سرور به حداقل برسد.

یک مثال: زمانیکه فایل استاتیک images/banner3.svg در پوشه‌ی wwwroot قرار می‌گیرد، با آدرس http://localhost:9189/images/banner3.svg توسط عموم قابل دسترسی خواهد بود.

یک نکته‌ی امنیتی مهم
در برنامه‌های ASP.NET Core، هنوز فایل web.config را نیز مشاهده می‌کنید. این فایل تنها کاربردی که در اینجا دارد، تنظیم ماژول AspNetCoreModule برای IIS است تا IIS static file handler آن، راسا اقدام به توزیع فایل‌های یک برنامه‌ی ASP.NET Core نکند. بنابراین توزیع این فایل را بر روی سرورهای IIS فراموش نکنید. همچنین بهتر است در ویندوزهای سرور، به قسمت Modules feature مراجعه کرده و StaticFileModule را از لیست ویژگی‌های موجود حذف کرد.


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

در قسمت قبل با نحوه‌ی نصب و فعال سازی middleware مخصوص WelcomePage آشنا شدیم. روال کار در اینجا نیز دقیقا به همان صورت است:
الف) نصب بسته‌ی نیوگت Microsoft.AspNetCore.StaticFiles
برای اینکار می‌توان بر روی گره‌ی references کلیک راست کرده و سپس از منوی ظاهر شده،‌گزینه‌ی manage nuget packages را انتخاب کرد. سپس ابتدا برگه‌ی browse را انتخاب کنید و در اینجا نام Microsoft.AspNetCore.StaticFiles را جستجو کرده و سپس نصب کنید.


انجام این کارها معادل افزودن یک سطر ذیل به فایل project.json است و سپس ذخیره‌ی آن که کار بازیابی بسته‌ها را به صورت خودکار آغاز می‌کند:
 "dependencies": {
   // same as before
    "Microsoft.AspNetCore.StaticFiles": "1.0.0"
},
ب) معرفی Middleware پردازش فایل‌های استاتیک
برای اینکار به فایل Startup.cs مراجعه کرده و سطر UseStaticFiles را به متد Configure اضافه کنید (به UseWelcomePage هم دیگر نیازی نداریم):
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }
 
    public void Configure(IApplicationBuilder app)
    {
        app.UseStaticFiles();
        //app.UseWelcomePage();
 
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello DNT!");
        });
    }
}

یک مثال: بر روی پوشه‌ی wwwroot کلیک راست کرده و گزینه‌ی add->new item را انتخاب کنید. سپس یک HTML page جدید را به نام index.html به این پوشه اضافه کنید.
با این محتوا:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Hello World</title>
</head>
<body>
    Hello World!
</body>
</html>
در این حالت برنامه را اجرا کنید. خروجی ذیل را مشاهده خواهید کرد:


که این خروجی دقیقا خروجی app.Run برنامه است و نه محتوای فایل index.html ایی که اضافه کردیم.
در ادامه اگر مسیر کامل این فایل را (http://localhost:7742/index.html) درخواست دهیم، آنگاه می‌توان خروجی این فایل استاتیک را مشاهده کرد:


این رفتار اندکی متفاوت است نسبت به نگارش‌های قبلی ASP.NET که فایل index.html را به عنوان فایل پیش فرض، درنظر می‌گرفت و محتوای آن‌را نمایش می‌داد. منظور از فایل پیش فرض، فایلی است که با درخواست ریشه‌ی یک مسیر، به کاربر ارائه داده می‌شود و index.html یکی از آن‌ها است.
برای رفع این مشکل، نیاز است Middleware مخصوص آن‌را به نام Default Files نیز به برنامه معرفی کرد:
public void Configure(IApplicationBuilder app)
{
   app.UseDefaultFiles();
   app.UseStaticFiles();
در این حالت است که با درخواست ریشه‌ی سایت، فایل پیش فرض آن نمایش داده خواهد شد:


فعال سازی Default Files، سبب جستجوی یکی از 4 فایل ذیل به صورت پیش فرض می‌شود (اگر تنها ریشه‌ی پوشه‌ای درخواست شود):
default.htm
default.html
index.htm
index.html

اگر خواستید فایل سفارشی خاص دیگری را معرفی کنید، نیاز است پارامتر DefaultFilesOptions آن‌را مقدار دهی نمائید:
 // Serve my app-specific default file, if present.
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(options);


ترتیب معرفی Middlewares مهم است

در قسمت قبل، در حین معرفی تفاوت‌های Middlewareها با HTTP Modules، عنوان شد که اینبار برنامه نویس می‌تواند بر روی ترتیب اجرای Middlewareها کنترل کاملی داشته باشد و این ترتیب معادل است با ترتیب معرفی آن‌ها در متد Configure، به نحوی که مشاهده می‌کنید. برای آزمایش این مطلب، متد معرفی middleware فایل‌های پیش فرض را پس از متد معرفی فایل‌های استاتیک قرار دهید:
public void Configure(IApplicationBuilder app)
{
  app.UseStaticFiles();
  app.UseDefaultFiles();
در این حالت اگر برنامه را اجرا کنید، به این خروجی خواهید رسید:


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


توزیع فایل‌های استاتیک خارج از wwwroot

همانطور که در ابتدای بحث عنوان شد، با فعال سازی UseStaticFiles به صورت پیش فرض مسیر content root/wwwroot در معرض دید دنیای خارج قرار می‌گیرد و توسط وب سرور قابل توزیع خواهد شد:
○ wwwroot
   § css
   § images
   § ...
○ MyStaticFiles
   § test.png
اما اگر قصد داشته باشیم تا تصویر test.png موجود در پوشه‌ی MyStaticFiles خارج از wwwroot را نیز عمومی کنیم چه باید کرد؟
برای این منظور می‌توان از پارامتر StaticFileOptions متد UseStaticFiles به نحو ذیل جهت معرفی پوشه‌ی MyStaticFiles استفاده کرد:
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
    RequestPath = new PathString("/StaticFiles")
});
در این حالت، مسیر دسترسی عمومی به این فایل، به صورت  http://<app>/StaticFiles/test.png خواهد بود (بر مبنای RequestPath تنظیم شده).


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


فرض کنید پوشه‌ی تصاویر را به پوشه‌ی عمومی wwwroot اضافه کرده‌ایم. برای فعال سازی مرور محتوای این پوشه می‌توان از Middleware دیگری به نام DirectoryBrowser استفاده کرد:
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
    RequestPath = new PathString("/MyImages")
});
بعد از انجام اینکار به خطای ذیل خواهید رسید:
 Unable to resolve service for type 'System.Text.Encodings.Web.HtmlEncoder' while attempting to activate 'Microsoft.AspNetCore.StaticFiles.DirectoryBrowserMiddleware'.
برای رفع آن، سرویس آن نیز باید به متد ConfigureServices اضافه شود:
public void ConfigureServices(IServiceCollection services)
{
   services.AddDirectoryBrowser();
}
در این حالت پس از اجرای برنامه، اگر مسیر http://localhost:7742/myimages را درخواست دهید (MyImages از RequestPath تنظیم شده، گرفته می‌شود)، به تصویر ذیل خواهید رسید:


مشکل! در این حالت که DirectoryBrowser را فعال کرده‌ایم، اگر بر روی لینک فایل تصویر نمایش داده شده کلیک کنیم، باز پیام Hello DNT یا اجرای app.Run را شاهد خواهیم بود.
به این دلیل که UseStaticFiles پیش فرض، مسیر درخواستی MyImages را که بر روی file system وجود ندارد، نمی‌شناسد. برای رفع این مشکل تنها کافی است مسیریابی این Request Path خاص را نیز فعال کنیم:
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
    RequestPath = new PathString("/MyImages")
});


بررسی خلاصه‌ی تنظیماتی که به فایل آغازین برنامه اضافه شدند

تا اینجا اگر توضیحات را قدم به قدم دنبال و اجرا کرده باشید، یک چنین تنظیماتی را خواهید داشت:
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
 
namespace Core1RtmEmptyTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDirectoryBrowser();
        }
 
        public void Configure(IApplicationBuilder app)
        {
            app.UseDefaultFiles();
 
            app.UseStaticFiles(); // For the wwwroot folder
 
            // For the files outside of the wwwroot
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
                RequestPath = new PathString("/StaticFiles")
            });
 
            // For DirectoryBrowser
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
                RequestPath = new PathString("/MyImages")
            });
 
            app.UseDirectoryBrowser(new DirectoryBrowserOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
                RequestPath = new PathString("/MyImages")
            });
 
            //app.UseWelcomePage();
 
            app.Run(async context =>
            {
                await context.Response.WriteAsync("Hello DNT!");
            });
        }
    }
}
services.AddDirectoryBrowser برای فعال سازی مرور پوشه‌ها اضافه شده‌است.
UseDefaultFiles کار فعال سازی شناسایی فایل‌های پیش فرضی مانند index.html را در صورت ذکر نام ریشه‌ی یک پوشه، انجام می‌دهد.
اولین UseStaticFiles تعریف شده، تمام مسیرهای فیزیکی ذیل wwwroot را عمومی می‌کند.
دومین UseStaticFiles تعریف شده، پوشه‌ی MyStaticFiles واقع در خارج از wwwroot را عمومی می‌کند.
سومین UseStaticFiles تعریف شده، پوشه‌ی فیزیکی wwwroot\images را به مسیر درخواست‌های MyImages نگاشت می‌کند (http://localhost:7742/myimages) تا توسط DirectoryBrowser تعریف شده، قابل استفاده شود.
در آخر هم DirectoryBrowser تعریف شده‌است.


یک نکته‌ی امنیتی مهم
یک چنین قابلیتی (مرور فایل‌های درون یک پوشه) به صورت پیش فرض بر روی تمام IIS‌ها به دلایل امنیتی غیرفعال است. به همین جهت بهتر است Middleware فوق را هیچگاه استفاده نکنید و به این قسمت صرفا از دیدگاه اطلاعات عمومی نگاه کنید.


ساده سازی تعاریف توزیع فایل‌های استاتیک

Middleware دیگری به نام FileServer کار تعریف توزیع فایل‌های استاتیک را ساده می‌کند. اگر آن‌را تعریف کنید:
 app.UseFileServer();
اینکار به معنای تعریف یکباره‌ی UseStaticFiles و UseDefaultFiles، با ترتیب صحیح آن‌ها است.
اگر خواستید DirectoryBrowsing آن‌را نیز فعال کنید، پارامتر ورودی آن‌را به true مقدار دهی کنید (که به صورت پیش فرض غیرفعال است):
 app.UseFileServer(enableDirectoryBrowsing: true);
همچنین در اینجا می‌توانید مسیر پوشه‌ی MyStaticFiles خارج از wwwroot را نیز با مقدار دهی پارامتر FileServerOptions آن، مشخص کنید:
app.UseFileServer(new FileServerOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
        RequestPath = new PathString("/StaticFiles"),
        EnableDirectoryBrowsing = false
    });


توزیع فایل‌های ناشناخته

اگر به سورس ASP.NET Core 1.0 دقت کنید، کلاسی را به نام FileExtensionContentTypeProvider خواهید یافت. این‌ها پسوندها و mime typeهای متناظری هستند که توسط ASP.NET Core شناخته شده و توزیع می‌شوند. برای مثال اگر فایلی را به نام test.xyz به پوشه‌ی wwwroot اضافه کنید، درخواست آن توسط کاربر، به Hello DNT ختم می‌شود؛ چون در این کلاس پایه، پسوند xyz تعریف نشده‌است.
برای رفع این مشکل و تکمیل این لیست می‌توان به نحو ذیل عمل کرد:
 // Set up custom content types -associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".xyz"] = "text/html";
 
app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
}) ; // For the wwwroot folder
در اینجا ابتدا همان کلاس پایه FileExtensionContentTypeProvider را نمونه سازی می‌کنیم و سپس به دیکشنری آن، پسوند و mime type ویژه‌ی خود را اضافه می‌کنیم. سپس این provider را می‌توان به خاصیت ContentTypeProvider پارامتر StaticFileOptions آن نسبت داد. اکنون این فایل با پسوند xyz، قابل شناسایی می‌شود:


و یا اگر خواستید کمی تمیزتر کار کنید، بهتر است از کلاس پایه FileExtensionContentTypeProvider ارث بری کرده و سپس در سازنده‌ی این کلاس، خاصیت Mappings را ویرایش نمود:
public class XyzContentTypeProvider : FileExtensionContentTypeProvider
{
    public XyzContentTypeProvider()
    {
        this.Mappings.Add(".xyz", "text/html");
    }
}
و برای استفاده‌ی از آن خواهیم داشت:
app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = new XyzContentTypeProvider()
}) ; // For the wwwroot folder

روش دیگر مدیریت این مساله، تنظیم مقدار خاصیت ServeUnknownFileTypes به true است:
app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = true,
    DefaultContentType = "image/png"
});
در اینجا هر پسوند شناخته نشده‌ای با mime type تصویر png، توزیع خواهد شد. البته از لحاظ امنیتی توصیه شده‌است که چنین کاری را انجام ندهید و از این تنظیم عمومی نیز صرفنظر کنید.