- با استفاده از Redux، یک شیء سراسری state، کار مدیریت state تمام برنامه را به عهده میگیرد که به آن «single source of truth» نیز گفته میشود. البته هرچند میتوان کامپوننتهایی را هم در این بین داشت که state خاص خودشان را داشته باشند و آنرا در این شیء سراسری ذخیره نکنند.
- در حین کار با Redux، تنها راه تغییر شیء سراسری state آن، صدور رخدادهایی هستند که در اینجا اکشن نامیده میشوند. یک اکشن شیءای است که بیان میکند چه چیزی قرار است تغییر کند.
- برای ساده سازی ساخت این اشیاء میتوان متدهایی را به نام action creators ایجاد کرد.
- اگر این متدهای action creator را توسط متد store.dispatch فراخوانی کنیم، سبب dispatch شیء اکشن، به یک تابع Reducer متناظری خواهند شد. این تابع Reducer است که قسمتی از state را که متناظر با نوع اکشن رسیدهاست، تغییر میدهد. در این حالت اگر اکشن رسیده، نوع مدنظری را نداشته باشد، خروجی تابع Reducer، همان state اصلی و بدون تغییر خواهد بود.
- Reducerها توابعی خالص هستند و نباید به همراه اثرات جانبی باشند (هر نوع تعاملی با دنیای خارج از تابع جاری) و همچنین نباید شیء state را نیز مستقیما تغییر دهند. این توابع باید یک کپی تغییر یافتهی از state را در صورت نیاز بازگشت دهند.
- برای مدیریت بهتر برنامه میتوان چندین تابع Reducer را بر اساس نوعهای اکشنهای ویژهای، پیاده سازی کرد. سپس با ترکیب آنها، یک شیء rootReducer ایجاد میشود.
- در نهایت در الگوی Redux، یک مخزن یا store تعریف خواهد شد که تمام این اجزاء را مانند rootReducer و میانافزارهای تعریف شده مانند Thunk، در کنار هم قرار میدهد و امکان dispatch اکشنها را میسر میکند.
- اکنون برای استفادهی از Redux در یک برنامهی React، نیاز است کامپوننت ریشهی برنامه را توسط کامپوننت Provider آن محصور کرد تا قسمتهای مختلف برنامه بتوانند با امکانات مخزن Redux، کار کرده و با آن ارتباط برقرار کنند.
- قسمت آخر این اتصال جائی است که کامپوننتهای اصلی برنامه، توسط یک کامپوننت دربرگیرنده که Container نامیده میشود، توسط متد connect کتابخانهی react-redux محصور میشوند. به این ترتیب این کامپوننتها میتوانند state و خواص مورد نیاز خود را از طریق props دریافت کرده (mapStateToProps) و یا رویدادها را به سمت store، ارسال کنند (mapDispatchToProps).
از زمان React 16.8، مفهوم جدیدی به نام React Hooks معرفی شد که تعدادی از مهمترینهای آنها را در سری «React 16x» بررسی کردیم. توسط Hooks، کامپوننتهای تابعی React اکنون میتوانند به local state خود دسترسی پیدا کنند و یا با دنیای خارج ارتباط برقرار کنند. پس از آن سایر کتابخانههای نوشته شدهی برای React نیز شروع به انطباق خود با این الگوی جدید کردهاند؛ برای مثال کتابخانهی react-redux v1.7 نیز به همراه تعدادی Hook، جهت ساده سازی آخرین قسمتی است که در اینجا بیان شد، تا بتوانند راه حل دومی برای اتصال کامپوننتها و دربرگیری آنها باشند که در ادامه جزئیات آنها را بررسی خواهیم کرد.
بررسی useSelector Hook
useSelector Hook که توسط کتابخانهی react-redux ارائه میشود، معادل بسیار نزدیک تابع mapStateToProps مورد استفادهی در متد connect است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت Posts در فایل src\containers\Posts.js، یک چنین محتوایی را دارد:
import { connect } from "react-redux"; import Posts from "../components/Posts"; const mapStateToProps = state => { console.log("PostsContainer->mapStateToProps", state); return { ...state.postsReducer }; }; export default connect(mapStateToProps)(Posts);
پیشتر امضای کامپوننت تابعی Posts واقع در فایل src\components\Posts.jsx، به صورت زیر تعریف شده بود که سه خاصیت را از طریق props دریافت میکرد:
const Posts = ({ posts, loading, error }) => { return ( // ...
اکنون فایل جدید src\components\HooksPosts.jsx را ایجاد کرده و ابتدا و امضای کامپوننت تابعی Posts را به صورت زیر تغییر میدهیم:
import { useSelector } from "react-redux"; // ... const HooksPosts = () => { const { posts, loading, error } = useSelector(state => state.postsReducer); return ( // ...
یک نکته: خروجی تابع mapStateToProps همواره باید یک شیء باشد، اما چنین محدودیتی در مورد تابع useSelector وجود ندارد و در صورت نیاز میتوان تنها مقدار یک خاصیت از یک شیء را نیز بازگشت داد.
این کامپوننت، هیچ تغییر دیگری را نیاز ندارد و اگر اکنون به فایل src\App.js مراجعه کنیم، میتوان دربرگیرندهی کامپوننت Posts را:
import PostsContainer from "./containers/Posts"; function App() { return ( <main className="container"> <PostsContainer /> </main> ); }
import HooksPosts from "./components/HooksPosts"; function App() { return ( <main className="container"> <HooksPosts /> </main> ); }
بررسی useDispatch Hook
تا اینجا موفق شدیم متد mapStateToProps را با useSelector Hook جایگزین کنیم. مرحلهی بعد، جایگزین کردن mapDispatchToProps با هوک دیگری به نام useDispatch است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت FetchPosts در فایل src\containers\FetchPosts.js، چنین تعریفی را دارد:
import { connect } from "react-redux"; import { fetchPostsAsync } from "../actions"; import FetchPosts from "../components/FetchPosts"; const mapDispatchToProps = { fetchPostsAsync }; export default connect(null, mapDispatchToProps)(FetchPosts);
const FetchPosts = ({ fetchPostsAsync }) => {
import React from "react"; import { useDispatch } from "react-redux"; import { fetchPostsAsync } from "../actions"; const HooksFetchPosts = () => { const dispatch = useDispatch(); return ( <section className="card mt-5"> <div className="card-header text-center"> <button className="btn btn-primary" onClick={() => dispatch(fetchPostsAsync())} > Fetch Posts </button> </div> </section> ); }; export default HooksFetchPosts;
با این تغییر نیز میتوان به فایل src\App.js مراجعه کرد و المان قبلی FetchPostsContainer را که از ماژول containers/FetchPosts تامین میشد، به نحو متداولی با همان کامپوننت جدید HooksFetchPosts، تعویض کرد:
import HooksFetchPosts from "./components/HooksFetchPosts"; import HooksPosts from "./components/HooksPosts"; // ... function App() { return ( <main className="container"> <HooksFetchPosts /> <HooksPosts /> </main> ); }
کامپوننت شمارشگر را در قسمت سوم این سری بررسی و تکمیل کردیم. اکنون قصد داریم فایل تامین کنندهی آنرا که به صورت زیر در فایل src\containers\Counter.js تعریف شده:
import { connect } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; import Counter from "../components/counter"; const mapStateToProps = (state, ownProps) => { console.log("CounterContainer->mapStateToProps", { state, ownProps }); return { count: state.counterReducer.count }; }; const mapDispatchToProps = { incrementValue, decrementValue }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);
class Counter extends Component { render() { console.log("Counter->props", this.props); const { //counterReducer: { count }, count, incrementValue, decrementValue } = this.props;
import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; const HooksCounter = ({ prop1 }) => { const { count } = useSelector(state => { console.log("HooksCounter->useSelector", { state, prop1 }); return { count: state.counterReducer.count }; }); const dispatch = useDispatch(); return ( // ...
- اینبار دو action creator مورد استفادهی در متدهای + و - را از ماژول action دریافت کردهایم تا توسط useDispatch مورد استفاده قرار گیرند.
- همچنین دیگر نیازی به ذکر (state, ownProps) نیست. مقدار ownProps، همان props معمولی است که به کامپوننت ارسال میشود که برای مثال اینبار نام prop1 را دارد؛ چون هنگامیکه المان کامپوننت HooksCounter را درج و معرفی میکنیم، توسط کامپوننت دیگری محصور نشدهاست. تامین آن نیز در فایل src\App.js با درج متداول نام المان کامپوننت HooksCounter و ذکر ویژگی سفارشی prop1 صورت میگیرد:
import HooksCounter from "./components/HooksCounter"; //... function App() { const prop1 = 123; return ( <main className="container"> <HooksCounter prop1={prop1} /> </main> ); }
import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; const HooksCounter = ({ prop1 }) => { const { count } = useSelector(state => { console.log("HooksCounter->useSelector", { state, prop1 }); return { count: state.counterReducer.count }; }); const dispatch = useDispatch(); return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">{count}</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm" onClick={() => dispatch(incrementValue())} > + </button> <button className="btn btn-secondary btn-sm m-2" onClick={() => dispatch(decrementValue())} > - </button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); }; export default HooksCounter;
مشکل! با استفاده از useSelector، تعداد رندرهای مجدد کامپوننتهای برنامه افزایش یافتهاست!
برنامهی جاری را پس از این تغییرات اجرا کنید. با هر بار کلیک بر روی دکمهی fetch posts، حتی کامپوننت شمارشگر درج شدهی در صفحه که ربطی به آن ندارد نیز رندر مجدد میشود! چرا؟ (این مورد را با مشاهدهی کنسول توسعه دهندگان مرورگر میتوانید مشاهده کنید. در ابتدای متد رندر هر کدام از کامپوننتها، یک console.log قرار داده شدهاست)
زمانیکه اکشنی dispatch میشود، useSelector hook با استفاده از مقایسهی ارجاعات اشیاء (strict === reference check)، کار مقایسهی مقدار قبلی و مقدار جدید را انجام میدهد. اگر اینها متفاوت باشند، کامپوننت را مجبور به رندر مجدد میکند. این مورد مهمترین تفاوت بین useSelector hook و متد connect است. متد connect از روش shallow equality checks برای مقایسهی نتایج حاصل از mapStateToProps و تصمیم در مورد رندر مجدد استفاده میکند. اما این مقایسهها چه تفاوتی با هم دارند؟
در حالت mapStateToProps، مهم نیست که شیء بازگشت داده شده، دارای یک ارجاع جدید است یا خیر؟ shallow equality checks فقط به معنای مقایسهی خاصیت به خاصیت شیء بازگشت داده شده، با نمونهی قبلی است. اما زمانیکه از useSelector hook استفاده میکنیم، با بازگشت یک شیء جدید، یعنی یک ارجاع جدید را خواهیم داشت و ... این یعنی اجبار به رندر مجدد کامپوننتها. به همین جهت در این حالت تعداد بار رندر کامپوننتها افزایش یافتهاست، چون خروجی reducerهای تعریف شدهی در برنامه، همیشه یک شیء جدید را بازگشت میدهند.
برای رفع این مشکل میتوان از پارامتر دوم متد useSelector که روش مقایسهی اشیاء را مشخص میکند، استفاده کرد:
import React from "react"; import { shallowEqual, useSelector } from "react-redux"; import Post from "./Post"; const HooksPosts = () => { const { posts, loading, error } = useSelector( state => state.postsReducer, shallowEqual ); console.log("render HooksPosts"); return ( // ...
با اضافه کردن پارامتر shallowEqual به کامپوننتهای HooksPosts و HooksCounter، دیگر با کلیک بر روی دکمهی fetch posts، کار رندر مجدد کامپوننت شمارشگر، رخ نمیدهد.
یک نکته: روش دیگر مشاهدهی تعداد بار رندر شدن کامپوننتها، استفاده از افزونهی react dev tools و مراجعه به برگهی profiler آن است. روی دکمهی record آن کلیک کرده و سپس اندکی با برنامه کار کنید. اکنون کار ضبط را متوقف نمائید، تا نتیجهی نهایی نمایش داده شود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part05.zip
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
تدارک مقدمات مثال این قسمت
این مثال در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه میکنیم:
>ng g m UploadFile -m app.module --routing
>ng g c UploadFile/UploadFileSimple
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف میکنیم:
>ng g cl UploadFile/Ticket
export class Ticket { constructor(public description: string = "") {} }
ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن
پس از ایجاد ساختار کلاس Ticket، یک وهله از آنرا به نام model ایجاد کرده و در اختیار قالب آن قرار میدهیم:
import { Ticket } from "./../ticket"; export class UploadFileSimpleComponent implements OnInit { model = new Ticket();
<div class="container"> <h3>Support Form</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="description.invalid && description.touched"> <label class="control-label">Description</label> <input #description="ngModel" required type="text" class="form-control" name="description" [(ngModel)]="model.description"> <div *ngIf="description.invalid && description.touched"> <div class="alert alert-danger" *ngIf="description.errors.required"> description is required. </div> </div> </div> <div class="form-group"> <label class="control-label">Screenshot(s)</label> <input #screenshotInput required type="file" multiple (change)="fileChange($event)" class="form-control" name="screenshot"> </div> <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button> </form> </div>
سپس در انتها، فیلد آپلود را مشاهده میکنید؛ با این ویژگیها:
الف) ngModel ایی به آن متصل نشدهاست؛ چون روش کار با آن متفاوت است.
ب) یک template reference variable به نام screenshotInput# در آن تعریف شدهاست. از این متغیر، در کامپوننت قالب استفاده خواهیم کرد.
ج) به رخداد change این کنترل، متد fileChange متصل شدهاست که رخداد جاری را نیز دریافت میکند.
د) ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده میکنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.
دسترسی به المان ارسال فایل در کامپوننت متناظر
تا اینجا یک المان ارسال فایل را به فرم، اضافه کردهایم. اما چگونه باید به فایلهای آن برای ارسال به سرور دسترسی پیدا کنیم؟
برای این منظور در ادامه دو روش را بررسی خواهیم کرد:
1) دسترسی به المان ارسال فایل از طریق رخداد change
در تعریف فیلد ارسال فایل، اتصال به رخداد change تعریف شدهاست:
(change)="fileChange($event)"
fileChange(event) { const filesList: FileList = event.target.files; console.log("fileChange() -> filesList", filesList); }
در اینجا ساختار شیء استاندارد FileList و اجزای آنرا مشاهده میکنید. برای مثال چون دو فایل انتخاب شدهاست، این لیست به همراه یک خاصیت طول و دو شیء File است.
تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آنها را شناسایی و intellisense متناظری را مهیا میکند:
C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
{ "lib": [ "es2016", "dom" ] } }
2) دسترسی به المان آپلود فایل از طریق یک template reference variable
در حین تعریف المان فایل در فرم برنامه، متغیر screenshotInput# نیز ذکر شدهاست. میتوان به یک چنین متغیرهایی در کامپوننت متناظر به روش ذیل دسترسی یافت:
import { Component, OnInit, ViewChild, ElementRef } from "@angular/core"; export class UploadFileSimpleComponent implements OnInit { @ViewChild("screenshotInput") screenshotInput: ElementRef; submitForm(form: NgForm) { const fileInput: HTMLInputElement = this.screenshotInput.nativeElement; console.log("fileInput.files", fileInput.files); }
اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شدهاست. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه میکنید، میتوان به خاصیت files این کنترل ارسال فایلها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شدهاست.
ارسال فرم درخواست پشتیبانی به سرور
تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون میخواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
>ng g s UploadFile/UploadFileSimple -m upload-file.module
در ادامه کدهای کامل این سرویس را مشاهده میکنید:
import { Http, RequestOptions, Response, Headers } from "@angular/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import "rxjs/add/operator/do"; import "rxjs/add/operator/catch"; import "rxjs/add/observable/throw"; import "rxjs/add/operator/map"; import "rxjs/add/observable/of"; import { Ticket } from "./ticket"; @Injectable() export class UploadFileSimpleService { private baseUrl = "api/SimpleUpload"; constructor(private http: Http) {} private extractData(res: Response) { const body = res.json(); return body || {}; } private handleError(error: Response): Observable<any> { console.error("observable error: ", error); return Observable.throw(error.statusText); } postTicket(ticket: Ticket, filesList: FileList): Observable<any> { if (!filesList || filesList.length === 0) { return Observable.throw("Please select a file."); } const formData: FormData = new FormData(); for (const key in ticket) { if (ticket.hasOwnProperty(key)) { formData.append(key, ticket[key]); } } for (let i = 0; i < filesList.length; i++) { formData.append(filesList[i].name, filesList[i]); } const headers = new Headers(); headers.append("Accept", "application/json"); const options = new RequestOptions({ headers: headers }); return this.http .post(`${this.baseUrl}/SaveTicket`, formData, options) .map(this.extractData) .catch(this.handleError); } }
روش کار با فرمهایی که فیلدهای ارسال فایل را به همراه دارند، متفاوت است با روش کار با فرمهای معمولی. در فرمهای معمولی، اصل شیء Ticket را به متد this.http.post واگذار میکنیم. مابقی آن خودکار است. در اینجا باید شیء استاندارد FormData را تشکیل داده و سپس اطلاعات را از طریق آن ارسال کنیم:
الف) افزودن مقادیر خواص شیء Ticket به FormData
postTicket(ticket: Ticket, filesList: FileList): Observable<any> { const formData: FormData = new FormData(); for (const key in ticket) { if (ticket.hasOwnProperty(key)) { formData.append(key, ticket[key]); } }
ب) افزودن فایلها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایلهای فرم است:
for (let i = 0; i < filesList.length; i++) { formData.append(filesList[i].name, filesList[i]); }
یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمیتوان در سمت سرور از روش model binding استفاده کرد. چون این نام دیگر ثابت نیست و هربار میتواند متغیر باشد (در حالت model binding دقیقا مشخص است که کلید مشخصی قرار است به سرور ارسال شود و بر همین اساس، نام خاصیت یا پارامتر سمت سرور تعیین میگردد). به همین جهت در سمت سرور برای دسترسی به این مجموعه، از روش Request.Form.Files استفاده میکنیم.
ج) ارسال اطلاعات نهایی به سرور
اکنون که formData را بر اساس اطلاعات اضافی ticket و فایلهای متصل به آن تشکیل دادیم، روش ارسال آن به سرور همانند قبل است:
const headers = new Headers(); headers.append("Accept", "application/json"); const options = new RequestOptions({ headers: headers }); return this.http .post(`${this.baseUrl}/SaveTicket`, formData, options) .map(this.extractData) .catch(this.handleError);
یک نکته: در اینجا در روش استفاده از formData نباید Content-Type را به multipart/form-data تنظیم کرد. در غیراینصورت خطای Missing content-type boundary error را دریافت میکنید.
تکمیل کامپوننت ارسال درخواست پشتیبانی
پس از تکمیل سرویس ارسال اطلاعات به سمت سرور، اکنون نوبت به استفادهی از آن در کامپوننت ارسال فرم درخواست پشتیبانی است. بنابراین ابتدا این سرویس جدید را به سازندهی UploadFileSimpleComponent تزریق میکنیم:
import { UploadFileSimpleService } from "./../upload-file-simple.service"; export class UploadFileSimpleComponent implements OnInit { constructor(private uploadService: UploadFileSimpleService ) {}
submitForm(form: NgForm) { const fileInput: HTMLInputElement = this.screenshotInput.nativeElement; console.log("fileInput.files", fileInput.files); this.uploadService .postTicket(this.model, fileInput.files) .subscribe(data => { console.log("success: ", data); }); }
دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیرهی فایلهای آن
کدهای کامل SimpleUpload که در سرویس فوق مشخص شدهاست، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شدهاست:
namespace AngularTemplateDrivenFormsLab.Models { public class Ticket { public int Id { set; get; } public string Description { set; get; } } }
using System.IO; using System.Threading.Tasks; using AngularTemplateDrivenFormsLab.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class SimpleUploadController : Controller { private readonly IHostingEnvironment _environment; public SimpleUploadController(IHostingEnvironment environment) { _environment = environment; } [HttpPost("[action]")] public async Task<IActionResult> SaveTicket(Ticket ticket) { //TODO: save the ticket ... get id ticket.Id = 1001; var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } var files = Request.Form.Files; foreach (var file in files) { //TODO: do security checks ...! if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream).ConfigureAwait(false); } } return Created("", ticket); } } }
- تزریق IHostingEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به مسیر wwwroot سایت دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
SaveTicket(Ticket ticket)
formData.append(filesList[i].name, filesList[i]);
var files = Request.Form.Files; foreach (var file in files)
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
1) نصب پیشنیاز
الف) نصب VS 2012
و یا
ب) نصب بسته MVC4 مخصوص VS 2010 (این مورد جهت سرورهای وب نیز توصیه میشود)
پس از نصب باید به این نکته دقت داشت که پوشههای زیر حاوی اسمبلیهای جدید MVC4 هستند و نیازی نیست الزاما این موارد را از NuGet دریافت و نصب کرد:
C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies
2) نیاز است نوع پروژه ارتقاء یابد
به پوشه پروژه MVC3 خود مراجعه کرده و تمام فایلهای csproj و web.config موجود را با یک ادیتور متنی باز کنید (از خود ویژوال استودیو استفاده نکنید، زیرا نیاز است محتوای فایلهای پروژه نیز دستی ویرایش شوند).
در فایلهای csproj (یا همان فایل پروژه؛ که vbproj هم میتواند باشد) عبارت
{E53F8FEA-EAE0-44A6-8774-FFD645390401}
{E3E379DF-F4C6-4180-9B81-6769533ABE47}
3) به روز رسانی شماره نگارشهای قدیمی
سپس تعاریف اسمبلیهای قدیمی نگارش سه MVC و نگارش یک Razor را یافته (در تمام فایلها، چه فایلهای پروژه و چه تنظیمات):
System.Web.Mvc, Version=3.0.0.0 System.Web.WebPages, Version=1.0.0.0 System.Web.Helpers, Version=1.0.0.0 System.Web.WebPages.Razor, Version=1.0.0.0
System.Web.Mvc, Version=4.0.0.0 System.Web.WebPages, Version=2.0.0.0 System.Web.Helpers, Version=2.0.0.0 System.Web.WebPages.Razor, Version=2.0.0.0
4) به روز رسانی مسیرهای قدیمی
به علاوه اگر در پروژههای خود از اسمبلیهای قدیمی به صورت مستقیم استفاده شده:
C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v1.0\Assemblies C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 3\Assemblies
C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies
5) به روز رسانی قسمت appSettings فایلهای کانفیگ
در کلیه فایلهای web.config برنامه، webpages:Version را یافته و شماره نگارش آنرا از یک به دو تغییر دهید:
<appSettings> <add key="webpages:Version" value="2.0.0.0" /> <add key="PreserveLoginUrl" value="true" /> </appSettings>
6) رسیدگی به وضعیت اسمبلیهای شرکتهای ثالث
ممکن است در این زمان از تعدادی کامپوننت و اسمبلی MVC3 تهیه شده توسط شرکتهای ثالث نیز استفاده نمائید. برای اینکه این اسمبلیها را وادار نمائید تا از نگارشهای MVC4 و Razor2 استفاده کنند، نیاز است bindingRedirectهای زیر را به فایلهای web.config برنامه اضافه کنید (در فایل کانفیگ ریشه پروژه):
<configuration> <!--... elements deleted for clarity ...--> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="4.0.0.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
7) استفاده از NuGet برای به روز رسانی بستههای نصب شده
یک سری از بستههای تشکیل دهنده MVC3 مانند موارد ذیل نیز به روز شدهاند که لازم است از طریق NuGet دریافت و جایگزین شوند:
Unobtrusive.Ajax.2
Unobtrusive.Validation.2
Web.Optimization.1.0.0
و ....
برای اینکار در solution explorer روی references کلیک راست کرده و گزینه Manage NuGet Packages را انتخاب کنید. در صفحه باز شده گزینه updates/all را انتخاب کرده و مواردی را که لیست میکند به روز نمائید (شامل جی کوئری، EF، structureMap و غیره خواهد بود).
8) اضافه کردن یک فضای نام جدید
بسته Web Optimization را از طریق NuGet دریافت کنید (برای یافتن آن bundling را جستجو کنید؛ نام کامل آن Microsoft ASP.NET Web Optimization Framework 1.0.0 است). این مورد به همراه پوشه MVC4 نیست و باید از طریق NuGet دریافت و نصب شود. (البته پروژههای جدید MVC4 شامل این مورد هستند)
در فایل وب کانفیگ، فضای نام System.Web.Optimization را نیز اضافه نمائید:
<pages> <namespaces> <add namespace="System.Web.Optimization" /> </namespaces> </pages>
پس از ارتقاء
اولین مشکلی که مشاهده شد:
بعد از rebuild به مقدار پارامتر salt که به نحو زیر در MVC3 تعریف شده بود، ایراد خواهد گرفت:
[ValidateAntiForgeryToken(Salt = "data123")]
علت هم این است که salt را اینبار به نحو صحیحی خودشان در پشت صحنه تولید و اعمال میکنند. بنابراین این یک مورد را کلا از کدهای خود حذف کنید که نیازی نیست.
مشکل بعدی:
در EF 5 جای یک سری از کلاسها تغییر کرده. مثلا ویژگیهای ForeignKey، ComplexType و ... به فضای نام System.ComponentModel.DataAnnotations.Schema منتقل شدهاند. در همین حد تغییر جهت کامپایل مجدد کدها کفایت میکند.
همچنین فایلهای پروژه موجود را باز کرده و EntityFramework, Version=4.1.0.0 را جستجو کنید. نگارش جدید 4.4.0.0 است که باید اصلاح شود (این موارد را بهتر است توسط یک ادیتور معمولی خارج از VS.NET ویرایش کنید).
در زمان نگارش این مطلب EF Mini Profiler با EF 5 سازگار نیست. بنابراین اگر از آن استفاده میکنید نیاز است غیرفعالش کنید.
اولین استفاده از امکانات جدید MVC4:
استفاده از امکانات System.Web.Optimization که ذکر گردید، میتواند اولین تغییر مفید محسوب شود.
برای اینکه با نحوه کار آن بهتر آشنا شوید، یک پروژه جدید MVC4 را در VS.NET (از نوع basic) آغاز کنید. به صورت خودکار یک پوشه جدید را به نام App_Start به ریشه پروژه اضافه میکند. داخل آن فایل مثال BundleConfig قرار دارد. این کلاس در فایل global.asax برنامه نیز ثبت شدهاست. باید دقت داشت در حالت دیباگ (compilation debug=true در وب کانفیگ) تغییر خاصی را ملاحظه نخواهید کرد.
تمام اینها خوب؛ اما من به نحو زیر از این امکان جدید استفاده میکنم:
using System.Collections.Generic; using System.IO; using System.Web; using System.Web.Optimization; namespace Common.WebToolkit { /// <summary> /// A custom bundle orderer (IBundleOrderer) that will ensure bundles are /// included in the order you register them. /// </summary> public class AsIsBundleOrderer : IBundleOrderer { public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files) { return files; } } public static class BundleConfig { private static void addBundle(string virtualPath, bool isCss, params string[] files) { BundleTable.EnableOptimizations = true; var existing = BundleTable.Bundles.GetBundleFor(virtualPath); if (existing != null) return; var newBundle = isCss ? new Bundle(virtualPath, new CssMinify()) : new Bundle(virtualPath, new JsMinify()); newBundle.Orderer = new AsIsBundleOrderer(); foreach (var file in files) newBundle.Include(file); BundleTable.Bundles.Add(newBundle); } public static IHtmlString AddScripts(string virtualPath, params string[] files) { addBundle(virtualPath, false, files); return Scripts.Render(virtualPath); } public static IHtmlString AddStyles(string virtualPath, params string[] files) { addBundle(virtualPath, true, files); return Styles.Render(virtualPath); } public static IHtmlString AddScriptUrl(string virtualPath, params string[] files) { addBundle(virtualPath, false, files); return Scripts.Url(virtualPath); } public static IHtmlString AddStyleUrl(string virtualPath, params string[] files) { addBundle(virtualPath, true, files); return Styles.Url(virtualPath); } } }
چند نکته مهم در این کلاس وجود دارد:
الف) توسط AsIsBundleOrderer فایلها به همان ترتیبی که به سیستم اضافه میشوند، در حاصل نهایی ظاهر خواهند شد. حالت پیش فرض مرتب سازی، بر اساس حروف الفباء است و ... خصوصا برای اسکریپتهایی که ترتیب معرفی آنها مهم است، مساله ساز خواهد بود.
ب)BundleTable.EnableOptimizations سبب میشود تا حتی در حالت debug نیز فشرده سازی را مشاهده کنید.
ج) متدهای کمکی تعریف شده این امکان را میدهند تا بدون نیاز به کامپایل مجدد پروژه، به سادگی در کدهای Razor بتوانید اسکریپتها را اضافه کنید.
سپس نحوه جایگزینی تعاریف قبلی موجود در فایلهای Razor با سیستم جدید، به نحو زیر است:
@using Common.WebToolkit <link href="@BundleConfig.AddStyleUrl("~/Content/blueprint/print", "~/Content/blueprint/print.css")" rel="stylesheet" type="text/css" media="print"/> @BundleConfig.AddScripts("~/Scripts/js", "~/Scripts/jquery-1.8.0.min.js", "~/Scripts/jquery.unobtrusive-ajax.min.js", "~/Scripts/jquery.validate.min.js") @BundleConfig.AddStyles("~/Content/css", "~/Content/Site.css", "~/Content/buttons.css")
http://site/Content/blueprint/print?v=hash
@BundleConfig.AddStyles("~/Content/noty/css", "~/Content/noty/jquery.noty.css", "~/Content/noty/noty_theme_default.css")
پارامترهای بعدی، محل قرارگیری اسکریپتها و CSSهای برنامه هستند و همانطور که عنوان شد اینبار با خیال راحت میتوانید ترتیب معرفی خاصی را مدنظر داشته باشید؛ زیرا توسط AsIsBundleOrderer به صورت پیش فرض لحاظ خواهد شد.
ایجاد یک پروژهی جدید Blazor WASM
برای پیاده سازی و اجرای مثالهای این قسمت، نیاز به یک پروژهی جدید Blazor WASM را داریم که میتوان آنرا با اجرای دستور dotnet new blazorwasm --hosted در یک پوشهی خالی، ایجاد کرد.
یک نکته: دستور فوق به همراه یک سری پارامتر اختیاری مانند hosted-- نیز هست. برای مشاهدهی لیست آنها دستور dotnet new blazorwasm --help را صادر کنید. برای مثال ذکر پارامتر hosted-- سبب میشود تا یک ASP.NET Core host نیز برای Blazor WebAssembly app ایجاد شده تولید شود.
حالت hosted-- آن یک چنین ساختاری را دارد که از سه پروژه و پوشهی Client ،Server و Shared تشکیل میشود:
در اینجا یک پروژهی خالی WASM ایجاد شده که برخلاف حالت معمولی dotnet new blazorwasm که در قسمت قبل آنرا بررسی کردیم، دیگر از فایل استاتیک wwwroot\sample-data\weather.json در آن خبری نیست. بجای آن، یک پروژهی استاندارد ASP.NET Core Web API را در پوشهی جدید Server ایجاد کرده که کار ارائهی اطلاعات این سرویس آب و هوا را انجام میدهد و برنامهی WASM ایجاد شده، این اطلاعات را توسط HTTP Client خود، از سرور Web API دریافت میکند.
بنابراین اگر مدل برنامهای که قصد دارید تهیه کنید، ترکیبی از یک Web API و WASM است، روش hosted--، آغاز آنرا بسیار ساده میکند.
نکته: روش اجرای این نوع برنامهها با اجرای دستور dotnet run در داخل پوشهی Server پروژه، انجام میشود. با اینکار هم سرور ASP.NET Core آغاز میشود و هم برنامهی WASM توسط آن ارائه میگردد. در این حالت اگر آدرس https://localhost:5001 را در مرورگر باز کنیم، هم قسمتهای بدون نیاز به سرور پروژهی WASM قابل دسترسی است (مانند کار با شمارشگر آن) و هم قسمت دریافت اطلاعات از سرور آن، در منوی Fetch Data.
شروع به کار با Razor
پس از ایجاد یک پروژهی جدید WASM، به فایل Client\Pages\Index.razor آن مراجعه کرده و محتوای پیشفرض آنرا بجز سطر اول زیر، حذف میکنیم:
@page "/"
در فایلهای razor. میتوان ترکیبی از کدهای #C و HTML را نوشت. برای مثال:
@page "/" <p>Hello, @name</p> @code { string name = "Vahid N."; }
یک نکته: با توجه به اینکه تغییرات زیادی را در فایل جاری اعمال خواهیم کرد، بهتر است برنامه را با دستور dotnet watch run اجرا کرد، تا این تغییرات را تحت نظر قرار داده و آنها را به صورت خودکار کامپایل کند. به این صورت دیگر نیازی نخواهد بود به ازای هر تغییر، یکبار دستور dotnet run اجرا شود.
در زمان درج متغیرهای #C در بین کدهای HTML توسط razor، استفاده از تمام متدهای الحاقی زبان #C نیز مجاز هستند؛ مانند:
<p>Hello, @name.ToUpper()</p>
یا حتی میتوان یک متد جدید را مانند CustomToUpper در قطعه کد razor، تعریف کرد و از آن به صورت زیر استفاده نمود:
@page "/" <p>Hello, @name.ToUpper()</p> <p>Hello, @CustomToUpper(name)</p> @code { string name = "Vahid N."; string CustomToUpper(string value) => value.ToUpper(); }
<p>Let's add 2 + 2 : @2 + 2 </p>
<p>Let's add 2 + 2 : @(2 + 2) </p>
<button @onclick="@(()=>Console.WriteLine("Test"))">Click me</button>
در اینجا اگر از Console.WriteLine("Test")@ استفاده میشد، به معنای انتساب یک رشتهی محاسبه شده به رویداد onclick بود که مجاز نیست.
روش دیگر انجام اینکار به صورت زیر است:
@page "/" <button @onclick="@WriteLog">Click me 2</button> @code { void WriteLog() { Console.WriteLine("Test"); } }
@page "/" <button @onclick="@(()=>WriteLogWithParam("Test 3"))">Click me 3</button> @code { void WriteLogWithParam(string value) { Console.WriteLine(value); } }
یک نکته: اگر به اشتباه بجای WriteLogWithParam، همان WriteLog قبلی را بنویسیم، کامپایلر (در حال اجرای توسط دستور dotnet watch run) خطای زیر را نمایش میدهد؛ پیش از اینکه برنامه در مرورگر اجرا شود:
BlazorRazorSample\Client\Pages\Index.razor(12,25): error CS1501: No overload for method 'WriteLog' takes 1 arguments
امکان تعریف کلاسها در فایلهای razor.
در فایلهای razor.، محدود به تعریف یک سری متدها و متغیرهای ساده نیستیم. در اینجا امکان تعریف کلاسها نیز وجود دارد و همچنین میتوان از کلاسهای خارجی (کلاسهایی که خارج از فایل razor جاری تعریف شدهاند) نیز استفاده کرد.
@page "/" <p>Hello, @StringUtils.MyCustomToUpper(name)</p> @code { public class StringUtils { public static string MyCustomToUpper(string value) => value.ToUpper(); } }
البته این کلاس را تنها میتوان داخل همین کامپوننت استفاده کرد. برای اینکه بتوان از امکانات این کلاس، در سایر کامپوننتها نیز استفاده کرد، میتوان آنرا در پروژهی Shared قرار داد. اگر به تصویر ابتدای مطلب جاری دقت کنید، سه پروژه ایجاد شدهاست:
الف) پروژهی کلاینت: که همان WASM است.
ب) پروژهی سرور: که یک پروژهی ASP.NET Core Web API ارائه کنندهی سرویس و API آب و هوا است و همچنین هاست کنندهی WASM ما.
ج) پروژهی Shared: کدهای این پروژه، بین هر دو پروژه به اشتراک گذاشته میشوند و برای مثال محل مناسبی است برای تعریف DTO ها. برای نمونه WeatherForecast.cs قرار گرفتهی در آن، DTO یا data transfer object سرویس API برنامه است که قرار است به کلاینت بازگشت داده شود. به این ترتیب دیگر نیازی نخواهد بود تا این تعاریف را در پروژههای سرور و کلاینت تکرار کنیم و میتوان کدهای اینگونه را به اشتراک گذاشت.
کاربرد دیگر آن تعریف کلاسهای کمکی است؛ مانند StringUtils فوق. به همین به پروژهی Shared مراجعه کرده و کلاس StringUtils را به صورت زیر در آن تعریف میکنیم (و یا حتی میتوان این قطعه کد را داخل یک پوشهی جدید، در همان پروژهی WASM نیز قرار داد):
namespace BlazorRazorSample.Shared { public class StringUtils { public static string MyNewCustomToUpper(string value) => value.ToUpper(); } }
پس از آن روش استفادهی از این کلاس کمکی خارجی اشتراکی به صورت زیر است:
@page "/" @using BlazorRazorSample.Shared <p>Hello, @StringUtils.MyNewCustomToUpper(name)</p>
یک نکته: میتوان به فایل Client\_Imports.razor مراجعه و مدخل زیر را به انتهای آن اضافه کرد:
@using BlazorRazorSample.Shared
کار با حلقهها در فایلهای razor.
همانطور که عنوان شد، یکی از کاربردهای پروژهی Shared، امکان به اشتراک گذاشتن مدلها، در برنامههای کلاینت و سرور است. برای مثال یک پوشهی جدید Models را در این پروژه ایجاد کرده و کلاس MovieDto را به صورت زیر در آن تعریف میکنیم:
using System; namespace BlazorRazorSample.Shared.Models { public class MovieDto { public string Title { set; get; } public DateTime ReleaseDate { set; get; } } }
@using BlazorRazorSample.Shared.Models
@page "/" <div> <h3>Movies</h3> @foreach(var movie in movies) { <p>Title: <b>@movie.Title</b></p> <p>ReleaseDate: @movie.ReleaseDate.ToString("dd MMM yyyy")</p> } </div> @code { List<MovieDto> movies = new List<MovieDto> { new MovieDto { Title = "Movie 1", ReleaseDate = DateTime.Now.AddYears(-1) }, new MovieDto { Title = "Movie 2", ReleaseDate = DateTime.Now.AddYears(-2) }, new MovieDto { Title = "Movie 3", ReleaseDate = DateTime.Now.AddYears(-3) } }; }
یک نکته: در حین تعریف فیلدهای code@، امکان استفادهی از var وجود ندارد؛ مگر اینکه از آن بخواهیم در داخل بدنهی یک متد استفاده کنیم.
و یا نمونهی دیگری از حلقههای #C مانند for را میتوان به صورت زیر تعریف کرد:
@for(var i = 0; i < movies.Count; i++) { <div style="background-color: @(i % 2 == 0 ? "blue" : "red")"> <p>Title: <b>@movies[i].Title</b></p> <p>ReleaseDate: @movies[i].ReleaseDate.ToString("dd MMM yyyy")</p> </div> }
نمایش شرطی عبارات در فایلهای razor.
اگر به مثال توکار Client\Pages\FetchData.razor مراجعه کنیم (مربوط به حالت host-- که در ابتدای مطلب عنوان شد)، کدهای زیر قابل مشاهده هستند:
@page "/fetchdata" @using BlazorRazorSample.Shared @inject HttpClient Http <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } }
برای رفع این مشکل، ابتدا یک if@ مشاهده میشود، تا نال بودن forecasts را بررسی کند:
@if (forecasts == null) { <p><em>Loading...</em></p> }
روش نمایش عبارات HTML در فایلهای razor.
فرض کنید عنوان اول فیلم مثال جاری، به همراه یک تگ HTML هم هست:
new MovieDto { Title = "<i>Movie 1</i>", ReleaseDate = DateTime.Now.AddYears(-1) },
<p>Title: <b>@((MarkupString)movie.Title)</b></p>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-03.zip
برای اجرای آن وارد پوشهی Server شده و دستور dotnet run را اجرا کنید.
- تنظیم از طریق خواندن فایل
- تنظیم توسط "روش مورد نظر کاربر در زمان آینده"
تولید خودکار کدها بر اساس OpenAPI Specification
فرض کنید در حال توسعهی برنامهی سمت کلاینت Angular و یا سمت سرور ASP.NET Core ای هستید که هر دوی اینها از یک Web API استفاده میکنند. همچنین فرض کنید که این Web API را نیز خودتان توسعه میدهید. بنابراین حداقل کدی که باید در اینجا به اشتراک گذاشته شود، کدهای کلاسهای DTO یا Data transfer objects هستند تا این کلاینتها بتوانند اطلاعات Web API را به نحو صحیحی Deserialize کنند و یا برعکس، بتوانند اطلاعات را با فرمت صحیحی به سمت Web API ارسال کنند.
برای مدیریت این مساله میتوان از دو روش استفاده کرد:
الف) استفاده از یک پروژهی اشتراکی
اگر کدهای مدنظر، سمت سرور باشند، میتوان یک پروژهی اشتراکی را برای این منظور ایجاد کرد و کدهای DTO را درون آن قرار داد و سپس ارجاعی به آن را در پروژههای مختلف، استفاده نمود. به این ترتیب تکرار کدها، کاهش یافته و همچنین تغییرات آن نیز به تمام پروژههای استفاده کننده به نحو یکسانی اعمال میشوند. در این حالت یک اسمبلی اشتراکی تولید شده و به صورت مستقلی توزیع میشود.
ب) استفاده از روش لینک کردن فایلها
در این روش پروژههای استفاده کننده از کلاسهای DTO، فایلهای آنرا به پروژهی خود لینک میکنند. در این حالت باز هم شاهد کاهش تکرار کدها و همچنین اعمال یک دست تغییرات خواهیم بود. اما در این روش دیگر یک اسمبلی اشتراکی وجود نداشته و کلاسهای DTO هم اکنون با اسمبلی پروژههای استفاده کننده، یکی و کامپایل شدهاند.
بدیهی است در هر دو روش، نیاز است بر روی کلاینت و API، کنترل کاملی وجود داشته باشد و بتوان به کدهای آنها دسترسی داشت. به علاوه فایلهای اشتراکی نیز باید بر اساس Target platform یکسانی تولید شده باشند. در این حالت دیگر نیازی به OpenAPI Specification برای تولید کدهای کلاینت دات نتی خود، نیست.
اما اگر کدهای API مدنظر در دسترس نباشند و یا بر اساس پلتفرم دیگری مانند node.js تولید شده باشد، کار یکپارچه سازی با آن دیگر با به اشتراک گذاری فایلهای آن میسر نیست. در این حالت اگر این API به همراه یک OpenAPI Specification باشد، میتوان از آن برای تولید خودکار کدهای کلاینتهای آن استفاده کرد.
معرفی تعدادی از ابزارهایی که قادرند بر اساس OpenAPI Specification، کد تولید کنند
برای تولید کد از روی OpenAPI Specification، گزینههای متعددی در دسترس هستند:
الف) Swagger CodeGen
این ابزار را که جزئی از مجموعه ابزارهای تولید شدهی برفراز OpenAPI است، میتوانید از آدرس swagger-codegen دریافت کنید. البته برای اجرای آن نیاز به Java Runtime است و یا نگارش آنلاین آن نیز در دسترس است: swagger.io
در ابزار آنلاین آن، در منوی generate بالای صفحه، گزینهی تولید کد برای #C نیز موجود است.
ب) AutoRest
محل دریافت: https://github.com/Azure/autorest
بر اساس node.js کار میکند و از طریق خط فرمان، قابل دسترسی است. همچنین این مورد ابزار تامین کنندهی گزینهی Add REST client در ویژوال استودیو نیز میباشد. اما در کل، امکان تنظیمات آنچنانی را به همراه ندارد.
ج) NSwagStudio
محل دریافت: https://github.com/RSuter/NSwag/wiki/NSwagStudio
همانطور که در مطلب «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت اول - معرفی» نیز عنوان شد، NSwag یکی دیگر از تولید کنندههای OpenAPI Specification مخصوص پروژههای دات نت است. NSwagStudio نیز جزئی از این مجموعه است که به کمک آن میتوان کدهای کلاینتها و DTOها را بر اساس OpenAPI Spec تولید کرد. همچنین امکان تنظیمات قابل توجهی را در مورد نحوهی تولید کدهای نهایی به همراه دارد.
استفاده از NSwagStudio برای تولید کدهای DTOها
در اینجا از همان برنامهای که در سری «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» بررسی کردیم، استفاده خواهیم کرد. بنابراین این برنامه، از پیش تنظیم شدهاست و هم اکنون به همراه یک تولید کنندهی OpenAPI Specification نیز میباشد. آنرا اجرا کنید تا بتوان به OpenAPI Specification تولیدی آن در آدرس زیر دسترسی یافت:
https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json
مطابق تصویر، ابتدا آدرس Swagger Specification URL یا همان آدرس فوق را وارد کنید. سپس فضای نام دلخواهی را وارد کرده و گزینهی تولید کلاسهای کلاینت را فعلا انتخاب نکنید. در لیست تنظیمات آن، گزینهی Class Style نیز مهم است. برای مثال برای پروژههای ASP.NET Core حالت POCO را انتخاب کنید (plain old clr objects) و برای پروژههای مبتنی بر XAML، گزینهی Inpc مناسبتر است چون RaisePropertyChangedها را هم تولید میکند. در آخر بر روی دکمهی Generate Outputs کلیک کنید تا خروجی ذیل حاصل شود:
یا میتوان این خروجی را copy/paste کرد و یا میتوان در برگهی Settings، در انتهای لیست آن، مقدار output file path را مشخص کرد و سپس بر روی دکمهی Generate files کلیک نمود تا فایل معادل آن تولید شود.
استفاده از NSwagStudio برای تولید کدهای کلاینت Angular استفاده کنندهی از API
NSwagStudio امکان تولید یک TypeScript Client را نیز دارد:
نکتهی جالب این خروجی، دقت داشتن به status codes درج شدهی در OpenAPI Spec است که در قسمتهای چهارم و پنجم سری «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» آنها را بررسی کردیم.
در اینجا نه تنها سرویسی جهت تعامل با API ما تولید شدهاست، بلکه معادل تایپاسکریپتی DTOهای برنامه را نیز تولید کردهاست:
بازنویسی سطح دوم کش برای Entity framework 6
- چون از روش LINQ to Objects برای تهیه معادل رشتهای کوئری درخواستی استفاده میکند (دقیقا این روش: ^) قادر نیست Includeها و جوینهای EF را پردازش کند و در این حالت برای تمام جوینها یک هش مساوی را در سیستم خواهید داشت.
- چون قادر نیست cache dependencies را از کوئری به صورت خودکار استخراج کند، شما نیاز خواهید داشت تا پارامتر تگهای آنرا به صورت دستی به ازای هر کوئری تنظیم کنید. اینکار به صورت خودکار در پروژهی جاری انجام میشود. cache dependencies به این معنا است که کوئری جاری به چه موجودیتهایی در سیستم وابستگی دارد. از آن برای به روز رسانی کش استفاده میشود. برای مثال اگر یک کوئری به سه موجودیت وابستگی دارد، با تغییر یکی از آنها، باید کش غیرمعتبر شده و در درخواست بعدی مجددا ساخته شود.
dotnet new -i FeatherHttp.Templates::0.1.67-alpha.g69b43bed72 --nuget-source https://f.feedz.io/featherhttp/framework/nuget/index.json
Templates Short Name Language Tags ---------------------------------------------------------------------------------------------------------------------------------- FeatherHttp feather [C#] Web/ASP.NET/FeatherHttp
dotnet new feather --name todoAPI
همانطور که مشاهده میکنید پروژهی فوق تنها شامل دو فایل .csproj و Program.cs است. درون Program.cs و متد Main کار initialize کردن سرور HTTP صورت گرفته است. WebApplication.Create دقیقا همانند Host.CreateDefaultBuilder پروژههای ASP.NET Core عمل میکند؛ یعنی پیکربندی pipeline از قبیل اضافه کردن متغیرهای محیطی، خواندن از فایل JSON و ... را انجام میدهد اما با کد boilerplate کمتر. بنابراین خروجی WebApplication.Create یک ASP.NET Core Pipeline با قابلیت اضافه کردن تنظیمات دلخواه است. در ادامه جهت بررسی بیشتر Feather HTTP، یک مدل را به همراه یک سری دیتای In-memory به پروژه اضافه خواهیم کرد:
using System.Collections.Generic; using System.Text.Json.Serialization; using System.Linq; namespace todoAPI.Models { public class Todo { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("title")] public string Title { get; set; } [JsonPropertyName("completed")] public bool Completed { get; set; } } public class TodoData { private readonly IList<Todo> _db = new List<Todo> { new Todo { Id = 1, Title = "Read book" }, new Todo { Id = 2, Title = "Watch an episode of Dark" }, new Todo { Id = 3, Title = "Publish a post on dotnettips" }, new Todo { Id = 4, Title = "Skype with my friend" }, }; public IList<Todo> GetAllToDoItmes() { return _db; } public void AddTodo(Todo item) { _db.Add(item); } public void ToggleTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); todo.Completed = !todo.Completed; } public void DeleteTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); _db.Remove(todo); } } }
در مثال فوق برای نگاشت نام خواص، از System.Text.Json توکار NET Core 3.0. استفاده شدهاست. در ادامه نیز از یک کلاس برای شبیهسازی CRUD یک Todo استفاده شدهاست. سپس برای داشتن اندپوینتهای موردنظر به ازای هر کدام از متدهای فوق درون متد Main، از app.Map... استفاده کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using todoAPI.Models; namespace todoAPI { class Program { private static readonly TodoData db = new TodoData(); static async Task Main(string[] args) { var app = WebApplication.Create(args); app.MapGet("/", GetTodos); app.MapPost("/api/todos", CreateTodo); app.MapPost("/api/todos/{id}", ToggleTodo); app.MapDelete("/api/todos/{id}", DeleteTodo); await app.RunAsync(); } static async Task GetTodos(HttpContext http) { var todos = db.GetAllToDoItmes(); await http.Response.WriteJsonAsync(todos); } static async Task CreateTodo(HttpContext http) { var todo = await http.Request.ReadJsonAsync<Todo>(); db.AddTodo(todo); http.Response.StatusCode = 204; } static async Task ToggleTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } db.ToggleTodo(id); http.Response.StatusCode = 204; } static async Task DeleteTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } db.DeleteTodo(id); http.Response.StatusCode = 204; } } }
هر کدام از اندپوینتهای فوق، یک ورودی HttpContext دریافت خواهند کرد. توسط این شیء میتوانیم به درخواست جاری و همچنین به پاسخ درخواست، دسترسی داشته باشیم.
استفاده از سیستم DI توکار NET Core.
همانطور که در ابتدای مطلب نیز عنوان شد، Feather HTTP یک wrapper بر روی APIهای موجود ASP.NET Core است، بنابراین میتوانیم از همان سرویس DI که درون پروژههای ASP.NET Core در اختیار داریم در اینجا نیز استفاده کنیم. در ادامه یک پوشهی جدید را به مثال قبل، با نام Controllers اضافه خواهیم کرد و درون آن یک فایل TodoController را با محتویات زیر ایجاد خواهیم کرد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using todoAPI.Models; using todoAPI.Services; namespace todoAPI.Controllers { public class TodoController { private readonly ITodoService _todoService; public TodoController(ITodoService todoService) { _todoService = todoService; } public async Task GetTodos(HttpContext http) { var todos = _todoService.GetAllToDoItmes(); await http.Response.WriteJsonAsync(todos); } public async Task CreateTodo(HttpContext http) { var todo = await http.Request.ReadJsonAsync<Todo>(); _todoService.AddTodo(todo); http.Response.StatusCode = 204; } public async Task ToggleTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } _todoService.ToggleTodo(id); http.Response.StatusCode = 204; } public async Task DeleteTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } _todoService.DeleteTodo(id); http.Response.StatusCode = 204; } } }
کاری که انجام شده است، انتقال تمامی متدهای static به کلاس فوق و سپس جایگزین کردن کلمهی کلیدی static با public است. همچنین یه ارجاع به اینترفیس جدید با عنوان ITodoService اضافه شده است؛ درون پیادهسازی این اینترفیس همان متدهای کلاس TodoData را اضافه کردهایم:
using System.Collections.Generic; using todoAPI.Models; using System.Linq; namespace todoAPI.Services { public interface ITodoService { void AddTodo(Todo item); void DeleteTodo(int id); IList<Todo> GetAllToDoItmes(); void ToggleTodo(int id); } public class TodoService : ITodoService { private readonly IList<Todo> _db = new List<Todo> { new Todo { Id = 1, Title = "Read book" }, new Todo { Id = 2, Title = "Watch an episode of Dark" }, new Todo { Id = 3, Title = "Publish a post on dotnettips" }, new Todo { Id = 4, Title = "Skype with my friend" }, }; public IList<Todo> GetAllToDoItmes() { return _db; } public void AddTodo(Todo item) { _db.Add(item); } public void ToggleTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); todo.Completed = !todo.Completed; } public void DeleteTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); _db.Remove(todo); } } }
نکته: برای ایجاد اینترفیس از روی یک کلاس درون VS Code میتوانیم اینگونه عمل کنیم:
تغییرات فایل Program.cs
ابتدا باید using مربوط به DI را در ابتدای فایل اضافه کنیم:
using Microsoft.Extensions.DependencyInjection;
سپس توسط ServiceProvider یک وهله از کلاس موردنظر را ایجاد کردهایم و همچنین سرویسهای موردنظر را درون DI Container اضافه کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using todoAPI.Controllers; using todoAPI.Services; namespace todoAPI { class Program { static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddTransient<TodoController>(); builder.Services.AddTransient<ITodoService, TodoService>(); var serviceProvider = builder.Services.BuildServiceProvider(); var todoController = serviceProvider.GetService<TodoController>(); var app = WebApplication.Create(args); app.MapGet("/", todoController.GetTodos); app.MapPost("/api/todos", todoController.CreateTodo); app.MapPost("/api/todos/{id}", todoController.ToggleTodo); app.MapDelete("/api/todos/{id}", todoController.DeleteTodo); await app.RunAsync(); } } }
Convention Over Configuration
در کد قبلی به صورت دستی TodoController را توسط Service Location از DI درخواست کردهایم. اینکار را در ادامه میتوانیم به Feather HTTP سپرده تا کار وهلهسازی را براساس قواعد توکار برایمان انجام دهد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using todoAPI.Services; namespace todoAPI { class Program { static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); builder.Services.AddControllers(); builder.Services.AddSingleton<ITodoService, TodoService>(); var serviceProvider = builder.Services.BuildServiceProvider(); var app = builder.Build(); app.MapControllers(); await app.RunAsync(); } } }
سپس در ادامه برای دسترسی به HTTP Context درون TodoController از IHttpContextAccessor استفاده کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using todoAPI.Models; using todoAPI.Services; namespace todoAPI.Controllers { public class TodoController { private readonly ITodoService _todoService; private readonly IHttpContextAccessor _accessor; public TodoController(ITodoService todoService, IHttpContextAccessor accessor) { _todoService = todoService; _accessor = accessor; } [HttpGet("/todos")] public async Task GetTodos() { var todos = _todoService.GetAllToDoItmes(); await _accessor.HttpContext.Response.WriteJsonAsync(todos); } [HttpPost("/todos")] public async Task CreateTodo() { var todo = await _accessor.HttpContext.Request.ReadJsonAsync<Todo>(); _todoService.AddTodo(todo); _accessor.HttpContext.Response.StatusCode = 204; } [HttpPost("/todos/{id}")] public async Task ToggleTodo(int id) { _todoService.ToggleTodo(id); _accessor.HttpContext.Response.StatusCode = 204; } [HttpDelete("/todos/{id}")] public async Task DeleteTodo(int id) { _todoService.DeleteTodo(id); _accessor.HttpContext.Response.StatusCode = 204; } } }
کدهای کامل مطلب را میتوانید از اینجا دریافت کنید.
استفاده از AOP Interceptors برای حذف کدهای تکراری کش کردن اطلاعات در لایه سرویس برنامه
«هر Aspect صرفا یک محصور کننده قابلیتی خاص و تکراری در برنامه است. از این جهت که کدهای تکراری برنامه، به Aspects منتقل شدهاند و دیگر نیازی نیست برای تغییر آنها، کدهای قسمتهای مختلف را تغییر داد (کدهای برنامه باز خواهند بود برای توسعه و بسته برای تغییر). بنابراین با استفاده از Aspects، به یک طراحی شیءگرای بهتر نیز دست خواهیم یافت.»
بنابراین فرق مهمش با روش کار با Expressions این است که شما در اینجا به یک Attribute جدید رسیدید که منطق پیاده سازی آن جایی در لابلای کدهای شما قرار نگرفته. هر زمان که نیازی به آن نبود، فقط کافی است که قسمت EnrichAllWith تنظیمات IoC Container یاده شده را حذف کرد. این روش یک دید دیگر طراحی شیءگرا است.
- از دیدگاه صرفا کاربردی:
الف) روش AOP یاد شده با هر نوع ORM ایی سازگار است. اصلا مهم نیست که الزاما EF باشد یا NH.
ب) چون درگیر بسیاری از جزئیات ریز تفسیر Expressions نشده، سریعتر است.