نظرات مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
- خیر. یافتن یک کاربر بر اساس ID آن که کلید اصلی جدول را هم تشکیل می‌دهد و بر روی آن ایندکس وجود دارد، فوق العاده سریع است. یک چنین کوئری گرفتنی در ASP.NET Core Identity هم وجود دارد. البته ASP.NET Core Identity برای انجام اینکار، بیش از یک کوئری را صادر می‌کند.
- وجود ICookieValidatorService که سبب بروز این کوئری اضافه می‌شود، در جهت پیاده سازی logout آنی و بررسی به روز بودن محتوای کوکی دریافتی، ضروری هست. نیاز هست تا بتوان مقدار public string SerialNumber مطلب جاری را بین اطلاعات موجود در سرور و اطلاعات قبلی موجود در کوکی صادر شده‌ی در حین لاگین، مقایسه کرد. اگر این بررسی صورت نگیرد، تا زمانیکه طول عمر کوکی کاربر به پایان نرسیده، با موفقیت و بدون هیچ کوئری اضافه‌ای، اطلاعات موجود در آن، در سمت سرور اعتبارسنجی شده و استفاده می‌شود. از آن اطلاعات در جهت تامین خودکار user claims و roles کاربر جاری استفاده می‌شود (همان شیء this.User قابل دسترسی در اکشن متدها)؛ چون زیر ساخت ASP.NET Core در جهت بررسی اطلاعات کوکی رسیده، به همراه دریافت اطلاعات مجدد آن از بانک اطلاعاتی سمت سرور نیست. اگر کوکی را معتبر تشخیص دهد و هنوز طول عمر آن به پایان نرسیده باشد، از اطلاعات آن در جهت ساخت شیء this.User استفاده می‌کند. اینجا است که اهمیت وجودی ICookieValidatorService در جهت تامین یک لایه‌ی اضافی امنیتی بررسی محتوای کوکی دریافتی، مشخص می‌شود. برای مثال فرض کنید که کاربری را به دلیل خاصی غیرفعال یا ممنوع الورود کرده‌اید. اگر سرویس ICookieValidatorService را نداشته باشید، این کاربر تا زمانیکه طول عمر کوکی آن معتبر تشخیص داده شود، می‌تواند در سایت خراب کاری کند؛ چون اطلاعات کوکی او، شیء this.User معتبری را تولید می‌کند که وابسته‌ی به اطلاعات جدید بانک اطلاعاتی برنامه نیست. همچنین قرار دادن دستی بررسی دوباره‌ی اعتبار user claim حاصل از اطلاعات کوکی کاربر جاری در هر اکشن متد و مقایسه‌ی آن با اطلاعات جدید بانک اطلاعاتی، سبب شلوغ شدن و تکرار کدهای اضافه‌ای می‌شود که به صورت متمرکز و یکجا در ICookieValidatorService وجود دارد. نمونه‌ی دیگر کاربرد این سرویس، پیاده سازی خودکار مفهوم single sign out است؛ به این معنا که اگر کاربری logout کرد، بتوان با تغییر SerialNumber او در سمت سرور، سایر افرادی را که در همان لحظه از اکانت او (کوکی‌های معتبر او) در سیستم‌های دیگری استفاده می‌کنند، logout کرد.
- استفاده از کش، به همراه بحث invalidation دقیق آن هم در صورت بروز تغییرات در بانک اطلاعاتی هست. هستند کتابخانه‌هایی که این‌کارها را به صورت خودکار و بدون نیازی به تغییری در کدها برای شما انجام دهند.
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت پنجم - Redux Hooks
تا اینجا الگوی Redux را در برنامه‌های React بررسی کردیم که شامل این موارد است:
- با استفاده از 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);
اینبار اگر بخواهیم کل این container را حذف کرده و از useSelector Hook استفاده کنیم، به این ترتیب عمل خواهیم کرد:
پیشتر امضای کامپوننت تابعی Posts واقع در فایل src\components\Posts.jsx، به صورت زیر تعریف شده بود که سه خاصیت را از طریق props دریافت می‌کرد:
const Posts = ({ posts, loading, error }) => {
  return (
  // ...
و این سه خاصیت دقیقا از متد mapStateToProps فوق که ملاحظه می‌کنید، تامین می‌شود. این متد خواص شیء state.postsReducer را به صورت props به کامپوننت Posts از طریق متد connect، ارسال می‌کند. کار postsReducer، فراهم آوردن و مدیریت سه خاصیت { loading: false, posts: [], error: null } است.

اکنون فایل جدید src\components\HooksPosts.jsx را ایجاد کرده و ابتدا و امضای کامپوننت تابعی Posts را به صورت زیر تغییر می‌دهیم:
import { useSelector } from "react-redux";

// ...

const HooksPosts = () => {
  const { posts, loading, error } = useSelector(state => state.postsReducer);
  return (
  // ...
متد useSelector، امکان دسترسی به state ذخیره شده‌ی در مخزن redux را میسر می‌کند. سپس باید همانند متد mapStateToProps، خواصی را که از آن نیاز داریم، دریافت کنیم که در اینجا کل خواص postsReducer دریافت شده (کل state دریافت شده و سپس خاصیت state.postsReducer آن بازگشت داده شده‌است) و در ادامه توسط Object Destructuring، به سه متغیری که پیشتر از طریق props تامین می‌شدند، انتساب داده می‌شود.

یک نکته: خروجی تابع mapStateToProps همواره باید یک شیء باشد، اما چنین محدودیتی در مورد تابع useSelector وجود ندارد و در صورت نیاز می‌توان تنها مقدار یک خاصیت از یک شیء را نیز بازگشت داد.

این کامپوننت، هیچ تغییر دیگری را نیاز ندارد و اگر اکنون به فایل src\App.js مراجعه کنیم، می‌توان دربرگیرنده‌ی کامپوننت Posts را:
import PostsContainer from "./containers/Posts";

function App() {
  return (
    <main className="container">
      <PostsContainer />
    </main>
  );
}
با کامپوننت جدید HooksPosts جایگزین کرد و دیگر نیازی به نوشتن متد connect و ساخت یک container مخصوص آن، نیست:
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);
کار این تامین کننده، اتصال action creator ای به نام fetchPostsAsync به props کامپوننت FetchPosts است که در فایل src\components\FetchPosts.jsx به این صورت تعریف شده‌است:
const FetchPosts = ({ fetchPostsAsync }) => {
اکنون برای جایگزین کردن mapDispatchToProps با useDispatch Hook، نگارش دیگری از این کامپوننت تابعی را به نام HooksFetchPosts در فایل src\components\HooksFetchPosts.jsx ایجاد می‌کنیم:
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;
عملکر آن نیز بسیار ساده‌است. متد useDispatch، به ما امکان دسترسی به متد store.dispatch را می‌دهد (ارجاعی به آن‌را در اختیار ما قرار می‌دهد). اکنون اگر مانند رخ‌داد onClick تعریف شده، سبب dispatch یک action creator به نام fetchPostsAsync شویم (که اینبار باید به صورت صریح از ماژول مربوطه import شود؛ چون دیگر از طریق props تامین نمی‌شود)، سبب ارسال نتیجه‌ی آن به reducer متناظری می‌شود.

با این تغییر نیز می‌توان به فایل 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\components\counter.jsx با redux hooks

کامپوننت شمارشگر را در قسمت سوم این سری بررسی و تکمیل کردیم. اکنون قصد داریم فایل تامین کننده‌ی آن‌را که به صورت زیر در فایل 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);
حذف کرده و با redux hooks جایگزین کنیم. برای این منظور فایل جدید src\components\HooksCounter.jsx را ایجاد می‌کنیم و سپس در ابتدا برای جایگزین کردن قسمت دریافت اطلاعات از this.pros آن:
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 (
  // ...
- متغیر count را با استفاده از useSelector، از شیء state استخراج کرده و با نام خاصیت count بازگشت می‌دهیم.
- اینبار دو 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>
  );
}
با این تغییرات، کدهای کامل src\components\HooksCounter.jsx به صورت زیر تکمیل می‌شود که قسمت‌های استفاده از متغیر count و همچنین dispatch دو action creator دریافت شده، در آن مشخص هستند:
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 در اینجا سبب خواهد شد تا بجای مقایسه‌ی ارجاعات اشیاء (که همیشه متفاوت خواهند بود؛ چون هربار شیء جدیدی را بازگشت می‌دهیم)، مقادیر تک تک خواص آن‌ها با هم مقایسه شوند.
با اضافه کردن پارامتر shallowEqual به کامپوننت‌های HooksPosts و HooksCounter، دیگر با کلیک بر روی دکمه‌ی fetch posts، کار رندر مجدد کامپوننت شمارشگر، رخ نمی‌دهد.

یک نکته: روش دیگر مشاهده‌ی تعداد بار رندر شدن کامپوننت‌ها، استفاده از افزونه‌ی react dev tools و مراجعه به برگه‌ی profiler آن است. روی دکمه‌ی record آن کلیک کرده و سپس اندکی با برنامه کار کنید. اکنون کار ضبط را متوقف نمائید، تا نتیجه‌ی نهایی نمایش داده شود.

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-redux-mobx-part05.zip
مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core
پیشنیازها
«بررسی روش آپلود فایل‌ها در ASP.NET Core»
«ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


تدارک مقدمات مثال این قسمت

این مثال در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه می‌کنیم:
 >ng g m UploadFile -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و UploadFileModule را بجای UploadFileRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت یک درخواست پشتیبانی را اضافه خواهیم کرد:
 >ng g c UploadFile/UploadFileSimple
که اینکار سبب به روز رسانی فایل upload-file.module.ts و افزوده شدن UploadFileSimpleComponent به قسمت declarations آن می‌شود.
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف می‌کنیم:
 >ng g cl UploadFile/Ticket
با این محتوا:
export class Ticket {
  constructor(public description: string = "") {}
}
در اینجا Ticket تعریف شده دارای یک خاصیت توضیحات است و این فرم به همراه فیلد ارسال چندین فایل نیز می‌باشد که نیازی به درج آن‌ها در کلاس فوق نیست:



ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن

پس از ایجاد ساختار کلاس Ticket، یک وهله از آن‌را به نام model ایجاد کرده و در اختیار قالب آن قرار می‌دهیم:
import { Ticket } from "./../ticket";

export class UploadFileSimpleComponent implements OnInit {
  model = new Ticket();
سپس قالب این کامپوننت و یا همان فایل upload-file-simple.component.html را به صورت ذیل تکمیل می‌کنیم:
<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>
در اینجا ابتدا فیلد توضیحات درخواست جدید، ارائه و به خاصیت model.description متصل شده‌است. همچنین این فیلد با ویژگی required مزین، و اجباری بودن آن بررسی گردیده‌است.
سپس در انتها، فیلد آپلود را مشاهده می‌کنید؛ با این ویژگی‌ها:
الف) 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);
}
همانطور که مشاهده می‌کنید، event.target، امکان دسترسی مستقیم به المان متناظری را در قالب کامپوننت میسر می‌کند. سپس می‌توان به خاصیت files آن دسترسی یافت.


در اینجا ساختار شیء استاندارد FileList و اجزای آن‌را مشاهده می‌کنید. برای مثال چون دو فایل انتخاب شده‌است، این لیست به همراه یک خاصیت طول و دو شیء File است.

تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آن‌ها را شناسایی و intellisense متناظری را مهیا می‌کند:
 C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
همچنین اگر به فایل tsconfig.json پروژه نیز مراجعه کنید، یک چنین تعاریفی در آن قرار دارند:
{
    "lib": [
      "es2016",
      "dom"
    ]
  }
}
وجود و تعریف کتابخانه‌ی 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 از نوع ElementRef که در angular/core@ تعریف شده‌است، اضافه می‌کنیم. سپس برای اتصال آن به template reference variable ایی به نام screenshotInput، از ویژگی به نام ViewChild، با پارامتری مساوی نام همین متغیر، استفاده خواهیم کرد.
اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شده‌است. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه می‌کنید، می‌توان به خاصیت files این کنترل ارسال فایل‌ها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شده‌است.


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

تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون می‌خواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
 >ng g s UploadFile/UploadFileSimple -m upload-file.module
که سبب به روز رسانی خودکار قسمت providers فایل upload-file.module.ts نیز می‌شود.
در ادامه کدهای کامل این سرویس را مشاهده می‌کنید:
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]);
      }
    }
با استفاده از حلقه‌ی for می‌توان بر روی خواص یک شیء جاوا اسکریپتی حرکت کرد. به این ترتیب می‌توان نام و مقدار آن‌ها را یافت و سپس به formData به صورت key/value افزود.

ب) افزودن فایل‌ها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایل‌های فرم است:
    for (let i = 0; i < filesList.length; i++) {
      formData.append(filesList[i].name, filesList[i]);
    }
این مورد نیز به سادگی تشکیل یک حلقه، بر روی خاصیت files المان آپلود فایل است. به همین جهت بود که به دو روش سعی کردیم، به این خاصیت دسترسی پیدا کنیم.

یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمی‌توان در سمت سرور از روش 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 چنین شکلی را پیدا می‌کند:
  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);
      });
  }
در اینجا this.model حاوی اطلاعات شیء ticket است (برای مثال اطلاعات توضیحات آن) و fileInput.files امکان دسترسی به اطلاعات فایل‌های انتخابی توسط کاربر را می‌دهد. پس از آن فراخوانی متدهای this.uploadService.postTicket و subscribe، سبب ارسال این اطلاعات به سمت سرور می‌شوند.


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

کدهای کامل SimpleUpload که در سرویس فوق مشخص شده‌است، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شده‌است:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Ticket
    {
        public int Id { set; get; }
        public string Description { set; get; }
    }
}
و سپس کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
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]);
مجبور هستیم در سمت سرور بر روی Request.Form.Files یک حلقه را تشکیل داده و تمام فایل‌های رسیده را پردازش کنیم:
var files = Request.Form.Files;
foreach (var file in files)



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
نحوه ارتقاء برنامه‌های موجود MVC3 به MVC4
در ادامه، مراحل ارتقاء پروژه‌های قدیمی MVC3 را به ساختار جدید پروژه‌های MVC4 مرور خواهیم کرد.

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}
جایگزین کنید. به این ترتیب نوع پروژه به MVC4 تبدیل می‌شود.

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
و این‌ها را با نگارش چهار MVC و نگارش دو Razor جایگزین کنید:
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
این کارها را با replace in all open documents توسط notepad plus-plus به سادگی می‌توان انجام داد.

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
این‌ها را یافته و به نگارش MVC4 و Razor2 تغییر دهید:
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>
همچنین یک سطر جدید PreserveLoginUrl را نیز مطابق تنظیم فوق اضافه نمائید.

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>
اکنون فایل solution را در VS.NET گشوده و یکبار گزینه rebuild را انتخاب کنید تا پروژه مجددا بر اساس اسمبلی‌های جدید معرفی شده ساخته شود.

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 را در MVC4 منسوخ شده معرفی کرده‌اند: (^)
علت هم این است که 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);
        }
    }
}
کلاس BundleConfig فوق را به مجموعه کلاس‌های کمکی خود اضافه کنید.
چند نکته مهم در این کلاس وجود دارد:
الف) توسط 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")
پارامتر اول این متدها، سبب تعریف خودکار routing می‌شود. برای مثال اولین تعریف، آدرس خودکار زیر را تولید می‌کند:
http://site/Content/blueprint/print?v=hash
بنابراین تعریف دقیق آن مهم است. خصوصا اگر فایل‌های شما در پوشه‌ها و زیرپوشه‌های متعددی قرار گرفته نمی‌توان تمام آن‌ها را در طی یک مرحله معرفی نمود. هر سطح را باید از طریق یک بار معرفی به سیستم اضافه کرد. مثلا اگر یک زیر پوشه به نام noty دارید (Content/noty)، چون در یک سطح و زیرپوشه مجزا قرار دارد، باید نحوه تعریف آن به صورت زیر باشد:
@BundleConfig.AddStyles("~/Content/noty/css",
                                "~/Content/noty/jquery.noty.css",
                                "~/Content/noty/noty_theme_default.css")
این مورد خصوصا در مسیریابی تصاویر مرتبط با اسکریپت‌ها و شیوه نامه‌ها مؤثر است؛ وگرنه این تصاویر تعریف شده در فایل‌های CSS یافت نخواهند شد (تمام مثال‌های موجود در وب با این مساله مشکل دارند و فرض آن‌ها بر این است که کلیه فایل‌های خود را در یک پوشه، بدون هیچگونه زیرپوشه‌ای تعریف کرده‌اید).
پارامترهای بعدی، محل قرارگیری اسکریپت‌ها و CSSهای برنامه هستند و همانطور که عنوان شد اینبار با خیال راحت می‌توانید ترتیب معرفی خاصی را مدنظر داشته باشید؛ زیرا توسط AsIsBundleOrderer به صورت پیش فرض لحاظ خواهد شد.

 
مطالب
Blazor 5x - قسمت سوم - مبانی Razor
پیش از شروع به کار توسعه‌ی برنامه‌های مبتنی بر Blazor، باید با مبانی Razor آشنایی داشت. Razor امکان ترکیب کدهای #C و HTML را در یک فایل میسر می‌کند. دستور زبان آن از @ برای سوئیچ بین کدهای #C و HTML استفاده می‌کند. کدهای Razor را می‌توان در فایل‌های cshtml. نوشت که عموما مخصوص صفحات و Viewها هستند و یا در فایل‌های razor. که برای توسعه‌ی کامپوننت‌های Balzor بکار گرفته می‌شوند. در اینجا مهم نیست که پسوند فایل مورد استفاده چیست؛ چون اصول razor بکار گرفته شده در آن‌ها یکی است. البته در اینجا تاکید ما بیشتر بر روی فایل‌های razor. است که در برنامه‌های مبتنی بر Blazor بکار گرفته می‌شوند.


ایجاد یک پروژه‌ی جدید 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 "/"
این سطر، بیانگر مسیریابی منتهی به کامپوننت جاری است. یعنی با گشودن برنامه‌ی WASM در مرورگر و مراجعه به ریشه‌ی سایت، محتوای این کامپوننت را مشاهده خواهیم کرد.
در فایل‌های razor. می‌توان ترکیبی از کدهای #C و HTML را نوشت. برای مثال:
@page "/"

<p>Hello, @name</p>

@code
{
    string name = "Vahid N.";
}
در اینجا قصد داریم مقدار یک متغیر را در یک پاراگراف درج کنیم. به همین جهت برای تعریف آن و شروع به کدنویسی می‌توان با تعریف یک قطعه کد که در فایل‌های razor با code@ شروع می‌شود، اینکار را انجام داد. در این قطعه کد، نوشتن هر نوع کد #C ای مجاز است که نمونه‌ای از آن‌را در اینجا با تعریف یک متغیر مشاهده می‌کنید. اکنون برای درج مقدار این متغیر در بین کدهای HTML از حرف @ استفاده می‌کنیم؛ مانند name@ در اینجا. نمونه‌ای از خروجی تغییرات فوق را در تصویر زیر مشاهده می‌کنید:


یک نکته: با توجه به اینکه تغییرات زیادی را در فایل جاری اعمال خواهیم کرد، بهتر است برنامه را با دستور dotnet watch run اجرا کرد، تا این تغییرات را تحت نظر قرار داده و آن‌ها را به صورت خودکار کامپایل کند. به این صورت دیگر نیازی نخواهد بود به ازای هر تغییر، یکبار دستور dotnet run اجرا شود.

در زمان درج متغیرهای #C در بین کدهای HTML توسط razor، استفاده از تمام متدهای الحاقی زبان #C نیز مجاز هستند؛ مانند:
 <p>Hello, @name.ToUpper()</p>
بنابراین درج حرف @ در بین کدهای HTML به این معنا است که به کامپایلر razor اعلام می‌کنیم، پس از این حرف، هر عبارتی که قرار می‌گیرد، یک عبارت معتبر #C است.

یا حتی می‌توان یک متد جدید را مانند CustomToUpper در قطعه کد razor، تعریف کرد و از آن به صورت زیر استفاده نمود:
@page "/"

<p>Hello, @name.ToUpper()</p>
<p>Hello, @CustomToUpper(name)</p>

@code
{
    string name = "Vahid N.";

    string CustomToUpper(string value) => value.ToUpper();
}
در این مثال‌ها، ابتدای عبارت #C تعریف شده با حرف @ شروع می‌شود و انتهای آن‌را خود کامپایلر razor بر اساس بسته شدن تگ p تعریف شده، تشخیص می‌دهد. اما اگر قصد داشته باشیم برای مثال جمع دو عدد را در اینجا محاسبه کنیم چطور؟
<p>Let's add 2 + 2 : @2 + 2 </p>
در این حالت امکان تشخیص ابتدا و انتهای عبارت #C توسط کامپایلر میسر نیست. برای رفع این مشکل می‌توان از پرانتزها استفاده کرد:
<p>Let's add 2 + 2 : @(2 + 2) </p>
نمونه‌ی دیگر نیاز به تعریف ابتدا و انتهای یک قطعه کد، در حین تعریف مدیریت کنندگان رویدادها است:
<button @onclick="@(()=>Console.WriteLine("Test"))">Click me</button>
در اینجا onclick@ مشخص می‌کند که با کلیک بر روی این دکمه قرار است قطعه کد #C ای اجرا شود. سپس با استفاده از ()@ محدوده‌ی این قطعه کد، مشخص می‌شود و اکنون در داخل آن می‌توان یک anonymous function را تعریف کرد که خروجی آن را در قسمت console ابزارهای توسعه دهندگان مرورگر می‌توان مشاهده کرد:


در اینجا اگر از Console.WriteLine("Test")@ استفاده می‌شد، به معنای انتساب یک رشته‌ی محاسبه شده به رویداد onclick بود که مجاز نیست.
روش دیگر انجام اینکار به صورت زیر است:
@page "/"

<button @onclick="@WriteLog">Click me 2</button>

@code
{
    void WriteLog()
    {
        Console.WriteLine("Test");
    }
}
می‌توان یک متد void را تعریف کرد و سپس فقط نام آن‌را توسط @ به onlick انتساب داد. ذکر این نام، اشاره‌گری خواهد بود به متد اجرا نشده‌ی WriteLog. در این حالت اگر نیاز به ارسال پارامتری به متد WriteLog بود، چطور؟
@page "/"

<button @onclick="@(()=>WriteLogWithParam("Test 3"))">Click me 3</button>

@code
{
    void WriteLogWithParam(string value)
    {
        Console.WriteLine(value);
    }
}
در این حالت نیز می‌توان از روش بکارگیری anonymous function‌ها برای تعریف پارامتر استفاده کرد.

یک نکته: اگر به اشتباه بجای 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();
    }
}
برای نمونه در اینجا یک کلاس کمکی را جهت تعریف متد MyCustomToUpper، اضافه کرده‌ایم. در ادامه نحوه‌ی استفاده از این متد را در پاراگراف تعریف شده، مشاهده می‌کنید که همانند کار با کلاس و متدهای متداول #C است.
البته این کلاس را تنها می‌توان داخل همین کامپوننت استفاده کرد. برای اینکه بتوان از امکانات این کلاس، در سایر کامپوننت‌ها نیز استفاده کرد، می‌توان آن‌را در پروژه‌ی 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();
    }
}
اگر به فایل‌های csproj دو پروژه‌ی سرور و کلاینت جاری مراجعه کنیم، از پیش، مدخلی را به فایل Shared\BlazorRazorSample.Shared.csproj دارند. بنابراین جهت معرفی این اسمبلی به آن‌ها، نیاز به کار خاصی نیست و از پیش، ارجاعی به آن تعریف شده‌است.

پس از آن روش استفاده‌ی از این کلاس کمکی خارجی اشتراکی به صورت زیر است:
@page "/"

@using BlazorRazorSample.Shared

<p>Hello, @StringUtils.MyNewCustomToUpper(name)</p>
ابتدا فضای نام این کلاس را با استفاده از using@ مشخص می‌کنیم و سپس امکان دسترسی به امکانات آن میسر می‌شود.

یک نکته: می‌توان به فایل Client\_Imports.razor مراجعه و مدخل زیر را به انتهای آن اضافه کرد:
@using BlazorRazorSample.Shared
به این ترتیب دیگر نیازی به ذکر این using@ تکراری، در هیچکدام از فایل‌های razor. پروژه‌ی کلاینت نخواهد بود؛ چون تعاریف درج شده‌ی در فایل Client\_Imports.razor سراسری هستند.


کار با حلقه‌ها در فایل‌های razor.

همانطور که عنوان شد، یکی از کاربردهای پروژه‌ی Shared، امکان به اشتراک گذاشتن مدل‌ها، در برنامه‌های کلاینت و سرور است. برای مثال یک پوشه‌ی جدید Models را در این پروژه ایجاد کرده و کلاس MovieDto را به صورت زیر در آن تعریف می‌کنیم:
using System;

namespace BlazorRazorSample.Shared.Models
{
    public class MovieDto
    {
        public string Title { set; get; }

        public DateTime ReleaseDate { set; get; }
    }
}
سپس به فایل Client\_Imports.razor مراجعه کرده و فضای نام این پوشه را اضافه می‌کنیم؛ تا دیگر نیازی به تکرار آن در تمام فایل‌های razor. برنامه‌ی کلاینت نباشد:
@using BlazorRazorSample.Shared.Models
اکنون می‌خواهیم لیستی از فیلم‌ها را در فایل Client\Pages\Index.razor نمایش دهیم:
@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)
        }
    };
}
در اینجا در ابتدا لیستی از MovieDto‌ها در قسمت code@ تعریف شده و سپس روش استفاده‌ی از یک حلقه‌ی foreach سی‌شارپ را در کدهای razor نوشته شده، مشاهده می‌کنید که این خروجی را ایجاد می‌کند:


یک نکته: در حین تعریف فیلدهای 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>
    }
در اینجا روش تغییر پویای background-color هر ردیف را نیز به کمک کدهای razor، مشاهده می‌کنید. اگر شماره‌ی ردیفی زوج بود، با آبی نمایش داده می‌شود؛ در غیراینصورت با قرمز. در اینجا نیز از ()@ برای تعیین محدوده‌ی کدهای #C نوشته شده، کمک گرفته‌ایم.


نمایش شرطی عبارات در فایل‌های 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");
    }

}
در این مثال، روش کار با یک سرویس تزریق شده‌ی async که قرار است از Web API اطلاعاتی را دریافت کند، مشاهده می‌کنید. در اینجا برخلاف مثال قبلی ما، از روال رویدادگردان OnInitializedAsync برای مقدار دهی لیست یا آرایه‌ای از اطلاعات وضعیت هوا استفاده شده‌است (و نه به صورت مستقیم در یک فیلد قسمت code@). این مورد جزو life-cycle‌های کامپوننت‌های razor است که در قسمت‌های بعد بیشتر بررسی خواهد شد. متد OnInitializedAsync برای بارگذاری اطلاعات یک سرویس از راه دور استفاده می‌شود و در اولین بار اجرای کامپوننت فراخوانی خواهد شد. نکته‌ی مهمی که در اینجا وجود دارد، نال بودن فیلد forecasts در زمان رندر اولیه‌ی کامپوننت جاری است؛ از این جهت که کار دریافت اطلاعات از سرور زمان‌بر است ولی رندر کامپوننت، به صورت آنی صورت می‌گیرد. در این حالت زمانیکه نوبت به اجرای foreach (var forecast in forecasts)@ می‌رسد، برنامه با یک استثنای نال بودن forecasts، متوقف خواهد شد؛ چون هنوز کار OnInitializedAsync به پایان نرسیده‌است:


 برای رفع این مشکل، ابتدا یک if@ مشاهده می‌شود، تا نال بودن forecasts را بررسی کند:
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
و همچنین عبارت در حال بارگذاری را نمایش می‌دهد. سپس در قسمت else آن، نمایش اطلاعات دریافت شده را توسط یک حلقه‌ی foreach مشاهده می‌کنید. با مقدار دهی forecasts در متد OnInitializedAsync، مجددا کار رندر جدول انجام خواهد شد.


روش نمایش عبارات HTML در فایل‌های razor.

فرض کنید عنوان اول فیلم مثال جاری، به همراه یک تگ HTML هم هست:
new MovieDto
{
   Title = "<i>Movie 1</i>",
   ReleaseDate = DateTime.Now.AddYears(-1)
},
در این حالت اگر برنامه را اجرا کنیم، خروجی آن دقیقا به صورت <Title: <i>Movie 1</i خواهد بود. این مورد به دلایل امنیتی انجام شده‌است. اگر پیشتر تگ‌های HTML را تمیز کرده‌اید و مطمئن هستید که خطری را ایجاد نمی‌کنند، می‌توانید با استفاده از روش زیر، آن‌ها را رندر کرد:
<p>Title: <b>@((MarkupString)movie.Title)</b></p>


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-03.zip
برای اجرای آن وارد پوشه‌ی Server شده و دستور dotnet run را اجرا کنید.
نظرات مطالب
پَرباد - راهنمای اتصال و پیاده‌سازی درگاه‌های پرداخت اینترنتی (شبکه شتاب)
بله تنظیمات را در هر زمان میتوان تغییر داد. فقط کافیست مرحله تنظیمات درگاه را در هر نقطه ای که میخواهید تکرار کنید. به اینصورت، تنظیمات جدید جایگزین قبلی خواهد شد.
دو حالت جدید هم در نسخه جدید در حال پیاده‌سازی هست:
  1. تنظیم از طریق خواندن فایل
  2. تنظیم توسط "روش مورد نظر کاربر در زمان آینده"
لطفا بحث‌ها، نظر‌ها و پیشنهاد‌ها رو در صفحه پروژه عنوان کنید.
تشکر.
مطالب
تولید خودکار کدهای سمت کلاینت بر اساس OpenAPI Specification
در سری «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» با نحوه‌ی تولید OpenAPI Specification، بر اساس کنترلرها و اکشن متدهای Web API خود آشنا شدیم و سپس با استفاده از ابزار Swagger-UI، یک رابط کاربری پویا را نیز برای آن تولید و سفارشی سازی کردیم. کاربرد OpenAPI Specification صرفا به مستندسازی یک Web API خلاصه نمی‌شود. بر اساس این استاندارد، ابزارهای متعددی جهت تولید کدهای سمت سرور و سمت کلاینت نیز طراحی شده‌اند که در اینجا نمونه‌ای از آن‌ها را بررسی خواهیم کرد.


تولید خودکار کدها بر اساس 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
سپس فایل msi مخصوص NSwagStudio را نیز از لینک آن در Github دریافت، نصب و اجرا کنید.


مطابق تصویر، ابتدا آدرس 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 را نیز دارد:

در اینجا ابتدا TypeScript Client را انتخاب می‌کنیم و سپس در تنظیمات آن، قالب Angular را انتخاب کرده و نگارش RxJS آن‌را نیز، 6 انتخاب می‌کنیم. در آخر بر روی Generate outputs کلیک می‌کنیم:


نکته‌ی جالب این خروجی، دقت داشتن به 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 به این معنا است که کوئری جاری به چه موجودیت‌هایی در سیستم وابستگی دارد. از آن برای به روز رسانی کش استفاده می‌شود. برای مثال اگر یک کوئری به سه موجودیت وابستگی دارد، با تغییر یکی از آن‌ها، باید کش غیرمعتبر شده و در درخواست بعدی مجددا ساخته شود.
مطالب
ایجاد HTTP API توسط Feather HTTP
Feather HTTP یک فریم‌ورک HTTP سبک، برای ایجاد APIهای NET Core. است، در واقع یک wrapper بر روی APIهای موجود ASP.NET Core می‌باشد که به ما امکان ایجاد HTTP API را در کمترین زمان میدهد. در این مطلب نحوه ایجاد یک API را توسط این فریم‌ورک بررسی خواهیم کرد.

معرفی قالب FeatherHttp.Templates به سیستم dotnet
برای شروع می‌توانیم قالب پروژه Feather HTTP را به لیست قالب‌های از پیش نصب شده‌ی dotnet اضافه کنیم. برای اینکار کافی است در خط فرمان دستور زیر را وارد کنیم:
dotnet new -i FeatherHttp.Templates::0.1.67-alpha.g69b43bed72 --nuget-source https://f.feedz.io/featherhttp/framework/nuget/index.json
پس از نصب قالب می‌توانید Feather HTTP را در لیست قالب‌ها توسط دستور dotnet new --list مشاهده کنید:
Templates                                         Short Name               Language          Tags
----------------------------------------------------------------------------------------------------------------------------------
FeatherHttp                                       feather                  [C#]              Web/ASP.NET/FeatherHttp

نحوه‌ی ایجاد یک پروژه‌ی جدید بر اساس قالب جدید
برای ایجاد یک پروژه‌ی جدید کافی است از دستور dotnet new feather استفاده کنید، در ادامه یک پروژه جدید تحت عنوان todoAPI ایجاد خواهیم کرد:
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 نشده، سریعتر است.