اطلاع از بروز رسانی نرم افزار ساخته شده
ممنون از شما. یک روش برای اینکه مستقیما با XML Reader کار نکنیم میتونه استفاده از روشهای سریالایز کردن کلاسها باشه. دردسرش کمتره.
یک سؤال: این فلشهای انحنا دار رو با چه برنامهای ایجاد کردید؟
رمزنگاری کانکشن استرینگ در ASP.Net
- بله (در صورت دسترسی به سرور و یا وجود باگ LFI). البته روشهای obfuscation میتونه در این حالت مفید باشه.
تعریف تزریق وابستگی تنظیمات برنامه
در مطلب «تزریق وابستگیها فراتر از کلاسها در برنامههای 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" };
سپس تنظیمات ابتدایی تزریق وابستگیهای IAppConfig را در فایل src\app\core\core.module.ts به صورت ذیل انجام میدهیم:
import { AppConfig, APP_CONFIG } from "./app.config"; @NgModule({ providers: [ { provide: APP_CONFIG, useValue: AppConfig } ] }) export class CoreModule {}
طراحی سرویس Auth
پس از لاگین باید بتوان به اطلاعات اطلاعات کاربر وارد شدهی به سیستم، در تمام قسمتهای برنامه دسترسی پیدا کرد. به همین جهت نیاز است این اطلاعات را در یک سرویس سراسری singleton قرار داد تا همواره یک وهلهی از آن در کل برنامه قابل استفاده باشد. مرسوم است این سرویس را AuthService بنامند. بنابراین محل قرارگیری این سرویس سراسری در پوشهی Core\services و محل تعریف آن در قسمت providers آن خواهد بود. به همین جهت ابتدا ساختار این سرویس را با دستور ذیل ایجاد میکنیم:
ng g s Core/services/Auth
create src/app/Core/services/auth.service.ts (110 bytes)
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()); }
در اینجا در سازندهی کلاس، بر اساس خروجی متد وضعیت لاگین شخص، برای اولین بار، متد next این BehaviorSubject فراخوانی میشود. علت قرار دادن این متد در سازندهی کلاس سرویس، عکس العمل نشان دادن به refresh کامل صفحه، توسط کاربر است و یا عکس العمل نشان دادن به وضعیت بهخاطر سپاری کلمهی عبور، در اولین بار مشاهدهی سایت و برنامه. در این حالت متد isLoggedIn، کش مرورگر را بررسی کرده و با واکشی توکنها و اعتبارسنجی آنها، گزارش وضعیت لاگین را ارائه میدهد. پس از آن، خروجی آن (true/false) به مشترکین اطلاع رسانی میشود.
در ادامه، متد next این BehaviorSubject را در متدهای login و logout نیز فراخوانی خواهیم کرد.
تدارک ذخیره سازی توکنها در کش مرورگر
از طرف سرور، دو نوع توکن access_token و refresh_token را دریافت میکنیم. به همین جهت یک enum را جهت مشخص سازی آنها تعریف خواهیم کرد:
export enum AuthTokenType { AccessToken, RefreshToken }
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); } } }
- متد 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":"..."}
تکمیل متد ورود به سیستم
در صفحهی لاگین، کاربر نام کاربری، کلمهی عبور و همچنین گزینهی «بهخاطر سپاری ورود» را باید تکمیل کند. به همین جهت اینترفیسی را برای این کار به نام 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)); } }
در اینجا نیاز است اطلاعات شیء 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); }); } }
اعتبارسنجی وضعیت لاگین و توکنهای ذخیره شدهی در مرورگر
برای اعتبارسنجی access token دریافتی از طرف سرور، نیاز به بستهی jwt-decode است. به همین جهت دستور ذیل را در خط فرمان صادر کنید تا بستهی آن به پروژه اضافه شود:
> npm install jwt-decode --save
import * as jwt_decode from "jwt-decode";
getDecodedAccessToken(): any { return jwt_decode(this.getRawAuthToken(AuthTokenType.AccessToken)); }
{ "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" }
getDisplayName(): string { return this.getDecodedAccessToken().DisplayName; }
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(): 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; }
در قسمت بعد، از این سرویس اعتبارسنجی تکمیل شده جهت ورود به سیستم و تکمیل کامپوننت header استفاده خواهیم کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
<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>
<head runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server"> <Scripts> <asp:ScriptReference Path="~/js/somefile.js" /> </Scripts> </asp:ScriptManager>
استفاده از 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();
در ادامه با توجه به حذف دو آرایهی 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() });
Blogger.Comment = DS.Model.extend({ text: DS.attr() });
<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'); } });
به همین ترتیب فایل Scripts\Routes\recent-comments.js را نیز جهت استفاده از data store ویرایش خواهیم کرد:
Blogger.RecentCommentsRoute = Ember.Route.extend({ model: function () { return this.store.find('comment'); } });
Blogger.PostRoute = Ember.Route.extend({ model: function (params) { return this.store.find('post', params.post_id); } });
افزودن امکان ثبت یک مطلب جدید
تا اینجا اگر برنامه را اجرا کنید، برنامه بدون خطا بارگذاری خواهد شد اما فعلا رکوردی را برای نمایش ندارد. در ادامه، برنامه را جهت افزودن مطالب جدید توسعه خواهیم داد. برای اینکار ابتدا به فایل 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'); });
<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}}
برای تعریف عناصر نمایشی این مسیریابی، فایل جدید قالب 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>
پس از آن نیاز است نام فایل قالب new-post را به template loader برنامه در فایل index.html اضافه کرد:
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post', 'new-post' ]); </script>
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'); } } });
<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'); } } } });
متد 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 }) });
همچنین نیاز است یک سر دیگر رابطه را نیز مشخص کرد. برای این منظور فایل Scripts\Models\comment.js را گشوده و به نحو ذیل تکمیل کنید:
Blogger.Comment = DS.Model.extend({ text: DS.attr(), post: DS.belongsTo('post', { async: true }) });
در ادامه نیاز است بتوان تعدادی نظر را ثبت کرد. به همین جهت با تعریف مسیریابی آن شروع میکنیم. این مسیریابی تعریف شده در فایل 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'); });
<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}}
در انتهای قالب نیز یک {{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>
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post', 'new-post', 'new-comment' ]); </script>
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); } } });
<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}}
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(); } } } });
<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}}
در کنترلر Comment روش دیگری را برای حذف یک رکورد مشاهده میکنید. میتوان ابتدا متد deleteRecord را بر روی مدل فراخوانی کرد و سپس آنرا save نمود تا نهایی شود. همچنین در اینجا نیاز است نظر حذف شده را از سر دیگر رابطه نیز حذف کرد. روش دسترسی به post جاری در این حالت، همانند توضیحات NewCommentController است که پیشتر بحث شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_04.zip
مفاهیم مقدماتی 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
در مورد پوشهی جدید 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(); } }
یک مثال: زمانیکه فایل استاتیک 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" },
برای اینکار به فایل 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
برای این منظور میتوان از پارامتر StaticFileOptions متد UseStaticFiles به نحو ذیل جهت معرفی پوشهی MyStaticFiles استفاده کرد:
app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")), RequestPath = new PathString("/StaticFiles") });
فعال سازی مشاهدهی مرور فایلهای استاتیک بر روی سرور
فرض کنید پوشهی تصاویر را به پوشهی عمومی 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'.
public void ConfigureServices(IServiceCollection services) { services.AddDirectoryBrowser(); }
مشکل! در این حالت که 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!"); }); } } }
UseDefaultFiles کار فعال سازی شناسایی فایلهای پیش فرضی مانند index.html را در صورت ذکر نام ریشهی یک پوشه، انجام میدهد.
اولین UseStaticFiles تعریف شده، تمام مسیرهای فیزیکی ذیل wwwroot را عمومی میکند.
دومین UseStaticFiles تعریف شده، پوشهی MyStaticFiles واقع در خارج از wwwroot را عمومی میکند.
سومین UseStaticFiles تعریف شده، پوشهی فیزیکی wwwroot\images را به مسیر درخواستهای MyImages نگاشت میکند (http://localhost:7742/myimages) تا توسط DirectoryBrowser تعریف شده، قابل استفاده شود.
در آخر هم DirectoryBrowser تعریف شدهاست.
یک نکتهی امنیتی مهم
یک چنین قابلیتی (مرور فایلهای درون یک پوشه) به صورت پیش فرض بر روی تمام IISها به دلایل امنیتی غیرفعال است. به همین جهت بهتر است Middleware فوق را هیچگاه استفاده نکنید و به این قسمت صرفا از دیدگاه اطلاعات عمومی نگاه کنید.
ساده سازی تعاریف توزیع فایلهای استاتیک
Middleware دیگری به نام FileServer کار تعریف توزیع فایلهای استاتیک را ساده میکند. اگر آنرا تعریف کنید:
app.UseFileServer();
اگر خواستید DirectoryBrowsing آنرا نیز فعال کنید، پارامتر ورودی آنرا به true مقدار دهی کنید (که به صورت پیش فرض غیرفعال است):
app.UseFileServer(enableDirectoryBrowsing: true);
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 ارث بری کرده و سپس در سازندهی این کلاس، خاصیت 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" });