نظرات مطالب
استفاده از MVVM زمانیکه امکان Binding وجود ندارد
در برنامه های تجاری لازم است بعد از واکشی داده ها از بانک اطلاعات ، محاسباتی بر روی این داده ها انجام شده و در نهایت اطلاعات جدید حاصل شده به صورت یک گزارش ، لیست نمودار و یا مواردی از این قبیل نمایش داده شود.
سوال اینجاست که در یک برنامه سیلورلایت که با مدل MVVM توسعه یافته ، عملیاتهای محاسباتی برنامه در کدام بخش انجام میگیرد.
لازم به ذکر است که در بعضی برنامه ها نیاز است قبل از ثبت اطلاعات در بانک نیز محاسباتی بر روی آنها انجام شده و سپس  نتیجه حاصل شده در بانک قرار گیرد.حال این محاسبات کجای پروژه و در کدام لایه قرار میگیرند؟!
نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
- در سازنده‌ی کلاس ApplicationDbContext ، به connectionString1 اشاره شده که تعریف آن در فایل web.config برنامه موجود است. به عبارتی در این مثال چنین رشته‌ی اتصالی تعریف شده‌است:
<add name="connectionString1"
connectionString="Data Source=(local);Initial Catalog=TestDbIdentity;Integrated Security = true" 
providerName="System.Data.SqlClient" />
بنابراین بانک اطلاعاتی پیش فرض آن TestDbIdentity نام دارد (جهت اتصال به آن، برای مشاهده جداول و یا تغییر و ثبت اطلاعات). این رشته اتصالی هم مخصوص SQL Server تنظیم شده‌است که می‌توانید توسط management studio و یا سایر ابزارهای مشابه، همانند قبل به آن متصل شوید.
- «در پوشه App_Data حذف کردم» ... از مثال نهایی کامل شده استفاده کنید (^) و نیازی به تکرار این مراحل نیست تا خطای یافت نشدن dbcontext را دریافت نکنید.
- برای ReCreate فقط کافی هست که بانک اطلاعاتی TestDbIdentity را drop کنید. بعد برنامه را مجددا از نو اجرا کنید. چون مراحل migrations آن به حالت خودکار تنظیم شده‌است، بانک اطلاعاتی را به صورت خودکار ایجاد می‌کند یا تغییرات کلاس‌های دومین برنامه را به صورت خودکار به بانک اطلاعاتی اعمال خواهد کرد.
- برای آشنایی بیشتر با مباحث تعریف رشته اتصالی EF Code First و مباحث Migrations آن، سری EF Code First را در سایت یکبار مطالعه کنید.
مطالب
React 16x - قسمت 24 - ارتباط با سرور - بخش 3 - نکات تکمیلی کار با Axios
پس از آشنایی با مقدمات کار با Axios، در این قسمت امکانات پیشرفته‌تر آن‌را مانند خطایابی سراسری، interceptors  و ... بررسی می‌کنیم.


به روز رسانی‌های خوشبینانه‌ی UI

پیاده سازی اعمال CRUD توسط Axios در قسمت قبل، به همراه یک مشکل مهم است: اعمال کار با شبکه و سرور، زمانبر هستند و مدتی طول می‌کشد تا پاسخ عملیات از سمت سرور دریافت شود. در این بین اگر خطایی رخ دهد، مابقی کدهای نوشته شده‌ی در متدهایی مانند Update و Delete، اجرا نمی‌شوند. به این حالت «به روز رسانی بدبینانه‌ی UI» گفته می‌شود. در حالت خوشبینانه، فرض بر این است که در اکثر موارد، فراخوانی سرور با موفقیت به پایان می‌رسد. در یک چنین حالتی، ابتدا UI به روز رسانی می‌شود و سپس فراخوانی‌های سمت سرور صورت می‌گیرند. اگر این فراخوانی با شکست مواجه شد، مجددا UI را به حالت قبلی آن باز می‌گردانیم:
  handleDelete = async post => {
    const posts = this.state.posts.filter(item => item.id !== post.id);
    this.setState({ posts });

    await axios.delete(`${apiEndpoint}/${post.id}`);
  };
در کدهای فوق، ابتدا UI به روز رسانی می‌شود (که بسیار سریع است)، سپس حذف سمت سرور صورت می‌گیرد. یک چنین پیاده سازی، به کاربر حس کار با یک برنامه‌ی بسیار سریع را القاء می‌کند؛ هرچند فراخوانی سمت سرور انجام شده، ممکن است مدتی طول بکشد.
اما اگر در این بین خطایی رخ داد، چه باید کرد؟ باید آخرین تغییر انجام شده را به حالت اول باز گرداند. انجام یک چنین کاری در React ساده‌است. چون ما state را به صورت مستقیم ویرایش نمی‌کنیم، همیشه می‌توان ارجاعی را به state قبلی، ذخیره و سپس در صورت نیاز آن‌را بازیابی کرد:
  handleDelete = async post => {
    const originalPosts = this.state.posts;

    const posts = this.state.posts.filter(item => item.id !== post.id);
    this.setState({ posts }); // Optimistic Update

    try {
      await axios.delete(`${apiEndpoint}/${post.id}`);
    } catch (ex) {
      alert("An error occurred when deleting a post!");
      this.setState({ posts: originalPosts }); // Undo changes
    }
  };
در اینجا در ابتدا توسط متغیر originalPosts، ارجاعی را به وضعیت قبلی آرایه‌ی posts موجود در state (وضعیت ابتدایی UI)، نگهداری می‌کنیم. سپس کار حذف بسیار سریع آیتم درخواستی را از UI انجام می‌دهیم. اکنون کار حذف اصلی رکورد را از سرور، درون یک try/catch انجام خواهیم داد. اگر خطایی رخ دهد، پیامی را به کاربر نمایش داده و سپس مجددا state را به همان originalPosts پیشین، باز خواهیم گرداند.


مدیریت خطاهای رخ داده‌ی در حین فراخوانی سرور

تا اینجا مشاهده کردیم که یک روش مدیریت خطاها در کدهای Axios، قرار دادن آن‌ها در یک قطعه کد try/catch است. در اینجا نیز باید بتوان بین خطاهای پیش بینی شده و نشده، تفاوت قائل شد.
- خطاهای پیش بینی شده: برای مثال اگر درخواست حذف رکوردی را دادیم که در بانک اطلاعاتی موجود نیست، انتظار داریم سرور، خطای 404 یا return NotFound را بازگشت دهد و یا 400 که معادل bad request است و در حالت ارسال داده‌هایی غیرمعتبر، رخ می‌دهد. در این موارد بهتر است خطاهایی خاص را به کاربران نمایش داد؛ برای مثال رکورد درخواستی وجود ندارد یا پیشتر حذف شده‌است.
- خطاهای پیش بینی نشده: این نوع خطاها نباید و یا قرار نیست در شرایط عادی رخ دهند. برای مثال اگر شبکه در دسترس نیست، امکان ارتباط با سرور نیز میسر نخواهد بود و یا حتی ممکن است خطایی در کدهای سمت سرور، سبب بروز خطایی شده باشد. این نوع خطاها ابتدا باید لاگ شوند تا با بررسی‌های آتی آن‌ها، بتوان مشکلات پیش بینی نشده را بهتر برطرف کرد. همچنین در یک چنین مواردی، باید یک پیام خطای خیلی عمومی را به کاربر نمایش داد؛ برای مثال «یک خطای پیش بینی نشده رخ داده‌است.».

برای مدیریت این دو حالت باید به جزئیات شیء ex، در بدنه‌ی catch، دقت کرد که دارای دو خاصیت request و response است. اگر ex.response تنظیم شده بود، یعنی دریافت خروجی از سرور موفقیت آمیز بوده‌است. اگر سرور در دسترس نباشد و یا برنامه‌ی سمت سرور کرش کرده باشد، ex.response نال خواهد بود. اگر ex.request نال نبود، یعنی ارسال درخواست به سمت سرور با موفقیت انجام شده‌است. برای مثال جهت بررسی خطای مورد انتظار 404، می‌توان در قسمت catch(ex) به صورت زیر عمل کرد:
try {
  await axios.delete(`${apiEndpoint}/${post.id}`);
} catch (ex) {
  if (ex.response && ex.response.status === 404) {
     alert("This post has already been deleted!");
  } else {
     console.log("Error", ex);
     alert("An unexpected error occurred when deleting a post!");
  }

  this.setState({ posts: originalPosts }); // Undo changes
}
در اینجا ابتدا بررسی می‌شود که آیا شیء response نال است یا خیر؟ سپس خاصیت status آن‌را برای بررسی خطاهای پیش بینی شده، بررسی می‌کنیم. خطایی که در اینجا نمایش داده می‌شود، اختصاصی‌تر است. در غیراینصورت، ابتدا باید این خطا لاگ شود و سپس یک اخطار عمومی نمایش داده می‌شود. پس از بررسی هر دو حالت، باید UI را مجددا به حالت اول آن بازگشت داد.
عموما خطاهای پیش‌بینی شده را لاگ نمی‌کنیم؛ چون ممکن است کاربر، یک صفحه را در چندین برگه باز کرده باشد و در یکی، رکوردی را حذف کند. در این حال، این رکورد هنوز در برگه‌های دیگر موجود است و اگر مجددا درخواست حذف آن‌را صادر کند، مشکل خاصی از دیدگاه برنامه رخ نداده‌است و نیازی به پیگیری‌های آتی را ندارد. یعنی صرفا یک client error است.


مدیریت سراسری خطاهای رخ داده‌ی در حین فراخوانی سرور

برای مدیریت خطاها، نیاز است یک چنین try/catchهایی را در تمام قسمت‌های برنامه که با سرور کار می‌کنند، قرار دهیم. برای کاهش این کدهای تکراری، از interceptors کتابخانه‌ی Axios استفاده می‌شود. در این کتابخانه می‌توان در جاهائیکه درخواستی به سمت سرور ارسال می‌شود و یا پاسخی از سمت سرور دریافت می‌شود، قطعه کدهایی سراسری را قرار داد و بر روی درخواست و یا پاسخ، تغییراتی را اعمال کرد و یا حتی اطلاعات مربوطه را لاگ کرد؛ به این نوع قطعه کدها، interceptor گفته می‌شود و برای تعریف آن‌ها می‌توان از axios.interceptors.request و یا axios.interceptors.response، خارج از کلاس جاری استفاده کرد. برای مثال بر روی شیء axios.interceptors.response، می‌توان متد use را فراخوانی کرد که دو پارامتر را که هر کدام یک callback function هستند، می‌پذیرد. اولی در صورت موفقیت آمیز بودن response فراخوانی می‌شود و دومی در صورت شکست آن. اگر نیازی به هر کدام نبود، می‌توان آن‌را به null مقدار دهی کرد. اگر مدیریت قسمت شکست علمیات مدنظر است، نیاز خواهد بود در پایان این callback function، یک Rejected Promise را بازگشت داد تا ادامه‌ی برنامه، به درستی مدیریت شود. در این حالت اگر خطایی رخ دهد، ابتدا این interceptor فراخوانی می‌شود و سپس کنترل به بدنه‌ی catch منتقل خواهد شد:
import "./App.css";

import axios from "axios";
import React, { Component } from "react";

axios.interceptors.response.use(null, error => {
  console.log("interceptor called.");
  return Promise.reject(error);
});

const apiEndpoint = "https://localhost:5001/api/posts";
class App extends Component {
اکنون می‌خواهیم قطعه کد نمایش خطاهای عمومی پیش بینی نشده را از تمام بدنه‌های catch حذف کرده و به یک interceptor منتقل کنیم:
axios.interceptors.response.use(null, error => {
  const expectedError =
    error.response &&
    error.response.status >= 400 &&
    error.response.status < 500;

  if (!expectedError) {
    console.log("Error", error);
    alert("An unexpected error occurred when deleting a post!");
  }

  return Promise.reject(error);
});
خطاهای پیش بینی شده عموما در بازه‌ی 400 تا 500 قرار دارند. به همین جهت اگر یک چنین خطاهایی را دریافت کردیم، اخطاری را نمایش نداده و صرفا کنترل را به catch block منتقل می‌کنیم. اما اگر خطا، پیش بینی نشده بود، کار لاگ کردن خطا و همچنین نمایش اخطار را در اینجا انجام خواهیم داد.

یک نکته: استفاده از try/catchها فقط برای بازگشت UI به حالت قبلی و یا نمایش خطایی خاص به کاربر توصیه می‌شوند. اگر از روش «به روز رسانی‌های خوشبینانه‌ی UI» استفاده نمی‌کنید و همچنین خطاهای ویژه‌ای بجز خطای عمومی لاگ شده‌ی در interceptor فوق مدنظر شما نیست، نیازی هم به try/catch نخواهد بود و پس از بروز خطا، قسمت‌های بعدی کد اجرا نمی‌شوند؛ اما خطای عمومی فوق نمایش داده خواهد شد.


ایجاد یک HTTP Service با قابلیت استفاده‌ی مجدد

تا اینجا تعریف interceptor را پیش از کلاس کامپوننت جاری قرار داده‌ایم که هم سبب شلوغی این ماژول شده‌است و هم در صورت نیاز به آن در سایر برنامه‌ها، باید همین قطعه کد را مجددا در آن‌ها کپی کرد. به همین جهت پوشه‌ی جدید src\services را ایجاد کرده و سپس فایل src\services\httpService.js را در آن با محتوای زیر ایجاد می‌کنیم:
import axios from "axios";

axios.interceptors.response.use(null, error => {
  const expectedError =
    error.response &&
    error.response.status >= 400 &&
    error.response.status < 500;

  if (!expectedError) {
    console.log("Error", error);
    alert("An unexpected error occurred when deleting a post!");
  }

  return Promise.reject(error);
});

export default {
  get: axios.get,
  post: axios.post,
  put: axios.put,
  delete: axios.delete
};
در اینجا علاوه بر انتقال interceptor تعریف شده، کار export متدهای axios نیز به صورت یک شیء جدید صورت گرفته‌است.
سپس به app.js مراجعه کرده و این ماژول را با یک نام دلخواه import می‌کنیم:
import http from "./services/httpService";
در ادامه هرجائیکه ارجاعی به axios وجود دارد، آن‌را با http فوق جایگزین می‌کنیم. در این حالت می‌توان "import axios from "axios را نیز از ابتدای app.js حذف کرد. مزیت اینکار، مخفی کردن Axios، در پشت صحنه‌ی ماژول جدیدی است که ایجاد کردیم. به این ترتیب اگر در آینده خواستیم، Axios را با کتابخانه‌ی دیگری جایگزین کنیم، در کل برنامه تنها نیاز است این httpService.js جدید را تغییر دهیم.


ایجاد یک ماژول Config

بهبود دیگری را که می‌توانیم اعمال کنیم، انتقال const apiEndpoint تعریف شده، به یک ماژول مجزا است؛ تا اگر نیاز به استفاده‌ی از آن در قسمت‌های دیگری نیز وجود داشت، به سادگی بتوان آن‌را مدیریت کرد. به همین جهت فایل جدید src\config.json را با محتوای زیر ایجاد می‌کنیم:
{
   "apiEndpoint" : "https://localhost:5001/api/posts"
}
سپس به فایل app.js بازگشته و ابتدا const apiEndpoint را حذف و سپس import زیر را به ابتدای فایل، اضافه می‌کنیم:
import config from "./config.json";
اکنون هر جائی در کدهای خود که apiEndpoint را داریم، تبدیل به config.apiEndpoint می‌کنیم.


نمایش بهتر خطاها به کاربر توسط کتابخانه‌ی react-toastify

بجای alert توکار مرورگرها، می‌توان یک صفحه‌ی دیالوگ زیباتر را برای نمایش خطاها درنظر گرفت. به همین جهت ابتدا کتابخانه‌ی react-toastify را نصب می‌کنیم:
> npm i react-toastify --save
سپس به فایل app.js مراجعه کرده و importهای لازم آن‌را اضافه می‌کنیم:
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
همچنین نیاز است ToastContainer را به ابتدای متد render نیز اضافه کرد:
  render() {
    return (
      <React.Fragment>
        <ToastContainer />
اکنون به src\services\httpService.js مراجعه کرده و alert آن‌را به صورت زیر تغییر می‌دهیم:
import { toast } from "react-toastify";
// ...

axios.interceptors.response.use(null, error => {
  // ...
  if (!expectedError) {
    // ...
    toast.error("An unexpected error occurrred.");
  }
ابتدا، شیء toast آن import می‌شود و سپس توسط این شیء می‌توان از متد error آن، جهت نمایش خطاهایی شکیل‌تر استفاده کرد؛ با این خروجی:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-22-backend-part-03.zip و sample-22-frontend-part-03.zip
مطالب
پَرباد - آموزش پیاده‌سازی پرداخت آنلاین در دات نت - مقدمه
پَرباد یک کتابخانه رایگان و اوپن سورس است که امکان افزودن قابلیت پرداخت آنلاین را به وب اپلیکیشن‌ها محیا میکند.

با توجه به نسخه قدیمی و درخواست کاربران جهت ارائه نسخه‌ای برای پشتیبانی از NET Core. ، نسخه‌ی کاملا جدیدی از این پروژه آماده گردید.

مزایا و ویژگی‌ها
  • نصب آسان با استفاده از Nuget
  • بدون نیاز به هیچگونه وب سرویس و یا دانش پیاده سازی سیستم‌های پرداخت آنلاین 
  • پشتیبانی از درگاه‌های: ملت، ملی (سداد)، پارسیان، پاسارگاد، ایران کیش و سامان
  • انجام پرداخت، فقط با نوشتن ۳ خط کد
  • طراحی کاملا یکپارچه برای انجام عملیات پرداخت با تمامی بانک‌ها
  • رعایت نکات امنیتی پرداخت آنلاین
  • درگاه مجازی، برای شبیه سازی عملیات پرداخت 
  • امکان استفاده از پروکسی برای سرور‌های خارج از ایران در صورت نیاز
  • استفاده از تکنولوژی‌های مدرن و استاندارد
  • قابل نصب بر روی پروژه‌های: ASP.NET Core, ASP.NET MVC, ASP.NET WebForms

آنچه شما در این سری مقالات یاد خواهید گرفت:

نحوه نصب
  • ASP.NET WebForms
  • ASP.NET MVC
  • ASP.NET CORE

آموزش پایه
  • درخواست پرداخت
  • تایید پرداخت
  • بازگرداندن مبلغ پرداخت شده

تنظیمات
  • درگاه‌ها
  • HttpContext
  • پایگاه داده
  • پیام‌ها

آموزش پیشرفته
  • ایجاد یک صورت‌حساب پرداخت با استفاده از InvoiceBuilder
  • درگاه مجازی پرداخت
  • استفاده از پروکسی
  • توکن پرداخت
  • تزریق وابستگی
  • Logging

نمونه پروژه‌ها
نحوه نصب
مطالب
پیاده سازی ویژگی مشابه asp-append-version در برنامه‌های Blazor SSR

عموما برای درج فایل‌های ثابت اسکریپت‌ها و شیوه‌نامه‌های سایت، از روش متداول زیر استفاده می‌شود:

<link rel="stylesheet" href="/css/site.css"  />    
<script src="/js/site.js"></script>    

مشکلی که به همراه این روش وجود دارد، مطلع سازی کاربران و مرورگر، از تغییرات آن‌هاست؛ چون این فایل‌های ثابت، توسط مرورگرها کش شده و با فشردن دکمه‌هایی مانند Ctrl+F5 و به‌روز شدن کش مرورگر، به نگارش جدید،‌ ارتقاء پیدا می‌کنند. برای رفع این مشکل حداقل دو روش وجود دارد:

الف) هربار نام این فایل‌ها را تغییر دهیم. برای مثال بجای نام قدیمی site.css، از نام جدید site.v.1.1.css استفاده کنیم.

ب) یک کوئری استرینگ متغیر را به نام ثابت این فایل‌ها، اضافه کنیم.

که در این بین، روش دوم متداول‌تر و معقول‌تر است. برای این منظور، ASP.NET Core به همراه ویژگی توکاری است به نام asp-append-version که اگر آن‌‌را به تگ‌های اسکریپت و link اضافه کنیم:

<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />    
<script src="~/js/site.js" asp-append-version="true"></script>    

این کوئری استرینگ را به صورت خودکار محاسبه کرده و به آدرس فایل درج شده اضافه می‌کند؛ با خروجی‌هایی شبیه به مثال زیر:

<link rel="stylesheet" href="/css/site.css?v=AAs5qCYR2ja7e8QIduN1jQ8eMcls-cPxNYUozN3TJE0" />    
<script src="/js/site.js?v=NO2z9yI9csNxHrDHIeTBBfyARw3PX_xnFa0bz3RgnE4"></script>  

ASP.NET Core در اینجا هش فایل‌های یافت شده را با استفاده از الگوریتم SHA256 محاسبه و url encode کرده و به صورت یک کوئری استرینگ، به انتهای آدرس فایل‌ها اضافه می‌کند. به این ترتیب با تغییر محتوای این فایل‌ها، این هش نیز تغییر می‌کند و مرورگر بر این اساس، همواره آخرین نگارش ارائه شده را از سرور دریافت خواهد کرد. نتیجه‌ی این محاسبات نیز به صورت خودکار کش می‌شود و همچنین با استفاده از یک File Watcher در پشت صحنه، تغییرات این فایل‌ها هم بررسی می‌شوند. یعنی اگر فایلی تغییر کرد، نیازی به ‌ری‌استارت برنامه نیست و محاسبات جدید و کش شدن مجدد آن‌ها، به صورت خودکار انجام می‌شود.

البته این ویژگی هنوز به Blazor اضافه نشده‌است؛ اما امکان استفاده‌ی از زیر ساخت ویژگی asp-append-version با کدنویسی مهیا است که در ادامه با استفاده از آن، کامپوننتی را مخصوص Blazor SSR، تهیه می‌کنیم.

دسترسی به زیر ساخت محاسباتی ویژگی asp-append-version با کدنویسی

زیرساخت محاسباتی ویژگی asp-append-version، با استفاده از سرویس توکار IFileVersionProvider به صورت زیر قابل دسترسی است:

public static class FileVersionHashProvider
{
    private static readonly string ProcessExecutableModuleVersionId =
        Assembly.GetEntryAssembly()!.ManifestModule.ModuleVersionId.ToString("N");

    public static string GetFileVersionedPath(this HttpContext httpContext, string filePath, string? defaultHash = null)
    {
        ArgumentNullException.ThrowIfNull(httpContext);

        var fileVersionedPath = httpContext.RequestServices.GetRequiredService<IFileVersionProvider>()
            .AddFileVersionToPath(httpContext.Request.PathBase, filePath);

        return IsEmbeddedOrNotFound(fileVersionedPath, filePath)
            ? QueryHelpers.AddQueryString(filePath, new Dictionary<string, string?>(StringComparer.Ordinal)
            {
                {
                    "v", defaultHash ?? ProcessExecutableModuleVersionId
                }
            })
            : fileVersionedPath;
    }

    private static bool IsEmbeddedOrNotFound(string fileVersionedPath, string filePath)
        => string.Equals(fileVersionedPath, filePath, StringComparison.Ordinal);
} 

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

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

یک نمونه روش استفاده‌ی از متد الحاقی GetFileVersionedPath فوق را در کامپوننت DntFileVersionedJavaScriptSource.razor زیر می‌توانید مشاهده کنید:

@if (!string.IsNullOrWhiteSpace(JsFilePath))
{
    <script src="@HttpContext.GetFileVersionedPath(JsFilePath)" type="text/javascript"></script>
}

@code{
    [CascadingParameter] public HttpContext HttpContext { set; get; } = null!;

    [Parameter]
    [EditorRequired]
    public required string JsFilePath { set; get; }
}

با استفاده از HttpContext مهیای در برنامه‌های Blazor SSR، متد الحاقی GetFileVersionedPath به همراه مسیر فایل js. مدنظر، در صفحه درج می‌شود.

برای مثال یک نمونه از استفاده‌ی آن، به صورت زیر است:

<DntFileVersionedJavaScriptSource JsFilePath="/lib/quill/dist/quill.js"/>

در نهایت با اینکار، یک چنین خروجی در صفحه درج خواهد شد که با تغییر محتوای فایل quill.js، هش متناظر با آن به صورت خودکار به‌روز خواهد شد:

<scriptsrc="/lib/quill/dist/quill.js?v=5q7uUOOlr88Io5YhQk3lgYcoB_P3-5Awq1lf0rRa7-Y" type="text/javascript"></script>

شبیه به همین کار را برای شیوه‌نامه‌ها هم می‌توان تکرار کرد و کدهای آن، تفاوت آنچنانی با کامپوننت فوق ندارند.

مطالب
طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD

هدف، ارائه راه‌حلی برای نمایش جدولی اطلاعات، جستجو، مرتب سازی و صفحه بندی و همچنین انجام عملیات ثبت، ویرایش و حذف بر روی آنها به صورت Ajaxای در بخش back office نرم افزار می‌باشد.

پیش نیازها:

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


ایجاد مدل‌های پایه

همانطور که در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» مطرح شد، برای پیاده سازی متدهای GetPagedList در ApplicationService‌ها از الگوی Request/Response استفاده می‌کنیم. برای این منظور واسط و کلاس‌های زیر را خواهیم داشت:

واسط IPagedQueryModel

    public interface IPagedQueryModel
    {
        int Page { get; set; }
        int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        string SortExpression { get; set; }
    }

این واسط قراردادی می‌باشد برای نوع و نام پارامترهایی که توسط کلاینت به سرور ارسال می‌شود. پراپرتی SortExpression آن، نام و ترتیب مرتب سازی را مشخص می‌کند؛ برای این منظور FieldName_ASC و FieldName_DESC به ترتیب برای حالات مرتب سازی صعودی و نزولی براساس FieldName مقدار دهی خواهد شد.

برای جلوگیری از تکرار این خصوصیات در مدل‌های کوئری مربوط به موجودیت‌ها، میتوان کلاس پایه‌ای به شکل زیر در نظر گرفت که پیاده ساز واسط بالا می‌باشد:

  public class PagedQueryModel : IPagedQueryModel, IShouldNormalize
    {
        public int Page { get; set; }
        public int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        public string SortExpression { get; set; }

        public virtual void Normalize()
        {
            if (Page < 1)
                Page = 1;

            if (PageSize < 1)
                PageSize = 10;

            if (SortExpression.IsEmpty())
                SortExpression = "Id_DESC";
        }
    }

مدل بالا علاوه بر پیاده سازی واسط IPagedQueryModel، پیاده ساز واسط IShouldNormalize نیز می‌باشد؛ دلیل وجود چنین واسطی در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» توضیح داده شده است:

پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.

کلاس PagedQueryResult

    public class PagedQueryResult<TModel>
    {
        public PagedQueryResult()
        {
            Items = new List<TModel>();
        }
        public IEnumerable<TModel> Items { get; set; }
        public long TotalCount { get; set; }
    }

دلیل وجود کلاس بالا در مقاله «طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور» توضیح داده شده است:

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

کلاس PagedListModel

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

    public class PagedListModel<TModel>
    {
        public IPagedQueryModel Query { get; set; }

        public PagedQueryResult<TModel> Result { get; set; }
    }

پراپرتی Query در برگیرنده پارامتر ورودی اکشن متد List می‌باشد که پراپرتی‌های آن با مقادیر موجود در کوئری استرینگ درخواست جاری مقدار دهی شده‌اند؛ البته بدون وجود کلاس بالا نیز به کمک ViewBag می‌شود این اطلاعات ترکیبی را به ویو ارسال کرد که پیشنهاد نمی‌شود.


متد GetPagedListAsync موجود در CrudApplicationService

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedQueryModel, TDynamicQueryModel> : ApplicationService,
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel>
        where TEntity : Entity, new()
        where TCreateModel : class
        where TEditModel : class, IModel
        where TModel : class, IModel
        where TDeleteModel : class, IModel
        where TPagedQueryModel : PagedQueryModel, new()
        where TDynamicQueryModel : DynamicQueryModel

    {

        #region Properties

        protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking();
        public IUnitOfWork UnitOfWork { get; set; }
        public IMapper Mapper { get; set; }
        protected IDbSet<TEntity> EntitySet => UnitOfWork.Set<TEntity>();

        #endregion

        #region ICrudApplicationService Members

        #region Methods

        public virtual async Task<PagedQueryResult<TModel>> GetPagedListAsync(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var query = ApplyFiltering(model);

            var totalCount = await query.LongCountAsync().ConfigureAwait(false);

            var result = query.ProjectTo<TModel>(Mapper.ConfigurationProvider);

            result = result.ApplySorting(model);
            result = result.ApplyPaging(model);

            return new PagedQueryResult<TModel>
            {
                Items = await result.ToListAsync().ConfigureAwait(false),
                TotalCount = totalCount
            };
        }
        #endregion

        #endregion

        #region Protected Methods

        /// <summary>
        ///     Apply Filtering To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            return UnTrackedEntitySet;
        }
        #endregion
    }

در بدنه این متد، ابتدا عملیات جستجو توسط متد ApplyFiltering انجام می‌شود. این متد به صورت پیش فرض هیچ شرطی را بر روی کوئری ارسالی به منبع داده اعمال نمی‌کند؛ مگر اینکه توسط زیر کلاس‌ها بازنویسی شود و فیلترهای مورد نیاز اعمال شوند. سپس تعداد کل آیتم‌های فیلتر شده محاسبه شده و بعد از عملیات Projection، مرتب سازی و صفحه بندی انجام می‌گیرد. برای مباحث مرتب سازی و صفحه بندی از دو متد زیر کمک گرفته شده‌است:

    public static class QueryableExtensions
    {
        public static IQueryable<TModel> ApplySorting<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return query.OrderBy(request.SortExpression.Replace('_', ' '));
        }

        public static IQueryable<TModel> ApplyPaging<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return request != null
                ? query.Page((request.Page - 1) * request.PageSize, request.PageSize)
                : query;
        }
    }

به منظور مرتب سازی از کتابخانه  System.Liq.Dynamic کمک گرفته شده‌است.

نکته: مشخص است که این روش، وابستگی به وجود متد GetPagedListAsync ندارد و صرفا برای تشریح ارتباط مطالبی که قبلا منتشر شده بود، مطرح شد.


پیاده سازی اکشن متدهای Index و List

public partial class RolesController : BaseController
{
    #region Fields
        private readonly IRoleService _service;
        private readonly ILookupService _lookupService;

        #endregion

    #region Constractor
        public RolesController(IRoleService service,  ILookupService lookupService)
        {
            Guard.ArgumentNotNull(service, nameof(service));
            Guard.ArgumentNotNull(lookupService, nameof(lookupService));

            _service = service;
            _lookupService = lookupService;
        }
        #endregion

    #region Index / List
    [HttpGet]
    public virtual async Task<ActionResult> Index()
    {
        var query = new RolePagedQueryModel();
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var pagedList = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        var model = new RoleIndexViewModel
        {
            PagedListModel = pagedList,
            Permissions = _lookupService.GetPermissions()
        };
        return View(model);
    }
    [HttpGet, AjaxOnly, NoOutputCache]
    public virtual async Task<ActionResult> List(RolePagedQueryModel query)
    {
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var model = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        return PartialView(MVC.Administration.Roles.Views._List, model);
    }
    #endregion
}

به عنوان مثال در بالا کنترلر مربوط به گروه‌های کاربری را مشاهده می‌کنید. به دلیل اینکه علاوه بر مباحث صفحه بندی و مرتب سازی، امکان جستجو بر اساس نام و دسترسی‌های گروه کاربری را نیز نیاز داریم، لازم است مدل زیر را ایجاد کنیم:

    public class RolePagedQueryModel : PagedQueryModel
    {
        public string Name { get; set; }
        public string Permission { get; set; }
    }

در این مورد خاص لازم است لیست دسترسی‌های موجود درسیستم به صورت لیستی برای انتخاب در فرم جستجو مهیا باشد. فرم جستجو در ویو مربوط به اکشن Index قرار می‌گیرد و قرار نیست به همراه پارشال ویو List_ در هر درخواستی از سرور دریافت شود. لذا لازم است مدلی برای ویو Index در نظر بگیریم که به شکل زیر می‌باشد:

    public class RoleIndexViewModel
    {   
        public RoleIndexViewModel()
        {
            Permissions = new List<LookupItem>();
        }
        public IReadOnlyList<LookupItem> Permissions { get; set; }
        public PagedListModel<RoleModel> PagedListModel { get; set; }
    }

پراپرتی PagedListModel در برگیرنده اطلاعات مربوط به نمایش اولیه جدول اطلاعات می‌باشد و پراپرتی Permissions لیست دسترسی‌های موجود درسیستم را به ویو منتقل خواهد کرد. اگر ویو ایندکس شما به داده اضافه ای نیاز ندارد، از ایجاد مدل بالا صرف نظر کنید.


ویو Index.cshtml

@model RoleIndexViewModel

@{
    ViewBag.Title = L("Administration.Views.Role.Index.Title");
    ViewBag.ActiveMenu = AdministrationMenuNames.RoleManagement;
}

<div class="row">
    <div class="col-md-12">
        <div id="filterPanel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="filterPanel">
            <div class="panel panel-default margin-bottom-5">

                <div class="panel-body">
                    @using (Ajax.BeginForm(MVC.Administration.Roles.List(),
new AjaxOptions { UpdateTargetId = "RolesList", HttpMethod = "GET" }, new { id = "filterForm", data_submit_on_reset = "true" }))
                    {
                        <div class="row">
                            <div class="col-md-3">
                                <input type="text" name="Name" class="form-control" value="" placeholder="@L("Administration.Role.Fields.Name")" />
                            </div>
                            <div class="col-md-3">
                                @Html.DropDownList("Permission", Model.Permissions.ToSelectListItems(), L("Administration.Views.Role.FilterBy.Permission"),new {@class="form-control"})
                            </div>
                            <div class="col-md-3">

                                <button type="submit"
                                        role="button"
                                        class="btn btn-info">
                                    @L("Commands.Filter")
                                </button>
                                <button type="reset"
                                        role="button"
                                        class="btn btn-default">
                                    <i class="fa fa-close"></i>
                                    @L("Commands.Reset")
                                </button>
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

<div class="row">
    <div class="col-md-12" id="RolesList">
        @{Html.RenderPartial(MVC.Administration.Roles.Views._List, Model.PagedListModel);}
    </div>
</div>

فرم جستجو باید دارای ویژگی data_submit_on_reset با مقدار "true" باشد. به منظور پاکسازی فرم جستجو و ارسال درخواست جستجو با فرمی خالی از داده، برای بازگشت به حالت اولیه از تکه کد زیر استفاده خواهد شد:

  $(document).on("reset", "form[data-submit-on-reset]",
            function () {
                var form = this;
                setTimeout(function () {
                    $(form).submit();
                });
            });

در ادامه پارشال ویو List_ با داده ارسالی به ویو Index، رندر شده و کار نمایش اولیه اطلاعات به صورت جدولی به اتمام می‌رسد.


پارشال ویو List.cshtml_

@model PagedListModel<RoleModel>
@{
    Layout = null;
    var rowNumber = (Model.Query.Page - 1) * Model.Query.PageSize + 1;
    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));
}

<div class="panel panel-default margin-bottom-5">
    <table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">
        <thead>
            <tr>
                <th style="width: 5%;">
                    #
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("DisplayName", L("Administration.Role.Fields.DisplayName"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("IsDefault", L("Administration.Role.Fields.IsDefault"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
               
                <th style="width: 5%;"></th>
            </tr>
        </thead>

        <tbody>
           @foreach (var role in Model.Result.Items)
            {
                <tr>
                    <td>@(rowNumber++.ToPersianNumbers())</td>
                    <td>@role.Name</td>
                    <td>@role.DisplayName</td>
                    <td class="text-center">@Html.DisplayFor(a => role.IsDefault)</td>
                    <td class="text-center operations">
                      
                        <div class="btn-group">

                            <span class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
                            <ul class="dropdown-menu dropdown-menu-left">
                                <li>
                                    <a href="#"
                                       role="button"
                                       data-ajax="true"
                                       data-ajax-method="GET"
                                       data-ajax-update="#main-modal div.modal-content"
                                       data-ajax-url="@Url.Action(MVC.Administration.Roles.Edit(role.Id))"
                                       data-toggle="modal"
                                       data-target="#main-modal">
                                        <i class="fa fa-pencil"></i>
                                        @L("Commands.Edit")
                                    </a>
                                </li>
                                <li>
                                    <a href="#"
                                       role="button"
                                       id="delete-@role.Id"
                                       data-delete-url="@Url.Action(MVC.Administration.Roles.Delete())"
                                       data-delete-model='{"Id":"@role.Id","RowVersion":"@Convert.ToBase64String(role.RowVersion)"}'>
                                        <i class="fa fa-trash"></i>
                                        @L("Commands.Delete")
                                    </a>
                                </li>
                            </ul>
                        </div>
                    </td>
                </tr>
            }
        </tbody>
    </table>

</div>

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>

به ترتیب  فایل بالا را بررسی می‌کنیم:

    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));

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

<table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">

دو ویژگی data-ajax-refresh-url و data-ajax-refresh-update برای جدولی که لازم است عملیات CRUD را پشتیبانی کند، لازم می‌باشد. در قسمت دوم به استفاده از این دو ویژگی در هنگام عملیات ثبت، ویرایش و حذف خواهیم پرداخت.

<th class="col-md-3 sortable">
    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
</th>

ستونی که امکان مرتب سازی را دارد باید th آن، کلاس sortable را داشته باشد. همچنین باید از هلپری که پیاده سازی آن را در ادامه خواهیم دید، استفاده کنیم. این هلپر، نام فیلد، عنوان ستون، مدل Query و همچین یک urlFactory را در قالب یک ‎Func<RouteValueDictionary,string>‎ دریافت می‌کند.


پیاده سازی هلپر SortableColumn

        public static MvcHtmlString SortableColumn(this HtmlHelper html, string columnName,
            string columnDisplayName, IPagedQueryModel queryModel, string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
        {
            var dictionary = queryModel.ToDictionary();

            var routeValueDictionary = new RouteValueDictionary(dictionary)
            {
                ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
                    ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
                        ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                            ? string.Empty : $"{columnName}_DESC"
            };

            var url = urlFactory(routeValueDictionary);

            var aTag = new TagBuilder("a");
            aTag.Attributes.Add("href", "#");
            aTag.Attributes.Add("data-ajax", "true");
            aTag.Attributes.Add("data-ajax-method", "GET");
            aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
            aTag.Attributes.Add("data-ajax-url", url);
            aTag.InnerHtml = columnDisplayName;

            var iconCssClass = !queryModel.SortExpression.StartsWith(columnName)
                ? "fa-sort"
                : queryModel.SortExpression.EndsWith("DESC")
                    ? "fa-sort-down"
                    : "fa-sort-up";

            var iTag = new TagBuilder("i");
            iTag.AddCssClass($"fa {iconCssClass}");

            return new MvcHtmlString($"{aTag}\n{iTag}");
        }

ابتدا مدل Query با متد الحاقی زیر تبدیل به دیکشنری می‌شود. این کار از این جهت مهم است که پراپرتی‌های لیست موجود در مدل Query، لازم است به فرم خاصی به سرور ارسال شوند که در تکه کد زیر مشخص می‌باشد.

public static IDictionary<string, object> ToDictionary(this object source)
{
    return source.ToDictionary<object>();
}

public static IDictionary<string, T> ToDictionary<T>(this object source)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    var dictionary = new Dictionary<string, T>();

    foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source))
    {
        AddPropertyToDictionary(property, source, dictionary);
    }
    return dictionary;
}

private static void AddPropertyToDictionary<T>(PropertyDescriptor property, object source,
    IDictionary<string, T> dictionary)
{
    var value = property.GetValue(source);

    var items = value as IEnumerable;

    if (items != null && !(items is string))
    {
        var i = 0;
        foreach (var item in items)
        {
            dictionary.Add($"{property.Name}[{i++}]", (T)item);
        }
    }
    else if (value is T)
    {
        dictionary.Add(property.Name, (T)value);
    }

}

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

در ادامه پیاده سازی هلپر SortableColumn، از دیکشنری حاصل، یک وهله از RouteValueDictionary ساخته می‌شود. در زمان رندر شدن PartialView لازم است مشخص شود که برای دفعه بعدی که بر روی این ستون کلیک می‌شود، باید چه مقداری با پارامتر SortExpression موجود در کوئری استرینگ ارسال شود. از این جهت برای پشتیبانی ستون، از حالت‌های مرتب سازی صعودی، نزولی و برگشت به حالت اولیه بدون مرتب سازی، کد زیر را خواهیم داشت:

var routeValueDictionary = new RouteValueDictionary(dictionary)
{
    ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
        ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
            ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                ? string.Empty : $"{columnName}_DESC"
};

در ادامه urlFactory با routeValueDictionary حاصل، Invoke می‌شود تا url نهایی برای مرتب سازی‌های بعدی را  از طریق یک لینک تزئین شده با data اتریبیوت‌های Unobtrusive Ajax در th مربوطه قرار دهیم.

برای مباحث صفحه بندی، بارگزاری مجدد و تغییر تعداد آیتم‌ها در هر صفحه، از سه هلپر زیر کمک خواهیم گرفت:

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>


پیاده سازی هلپر Pager

public static MvcHtmlString Pager<TModel>(this HtmlHelper html, PagedListModel<TModel> model,
        string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
{
    return html.PagedListPager(
        new StaticPagedList<TModel>(model.Result.Items, model.Query.Page, model.Query.PageSize,
            (int)model.Result.TotalCount), page =>
       {
           var dictionary = model.Query.ToDictionary();
           var routeValueDictionary = new RouteValueDictionary(dictionary) { ["Page"] = page };
           return urlFactory(routeValueDictionary);
       }, PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing(
            new PagedListRenderOptions
            {
                DisplayLinkToFirstPage = PagedListDisplayMode.Always,
                DisplayLinkToLastPage = PagedListDisplayMode.Always,
                DisplayLinkToPreviousPage = PagedListDisplayMode.Always,
                DisplayLinkToNextPage = PagedListDisplayMode.Always,
                MaximumPageNumbersToDisplay = 6,
                DisplayItemSliceAndTotal = true,
                DisplayEllipsesWhenNotShowingAllPageNumbers = true,
                ItemSliceAndTotalFormat = $"تعداد کل: {model.Result.TotalCount.ToPersianNumbers()}",
                FunctionToDisplayEachPageNumber = page => page.ToPersianNumbers(),
            },
            new AjaxOptions
            {
                AllowCache = false,
                HttpMethod = "GET",
                InsertionMode = InsertionMode.Replace,
                UpdateTargetId = updateTargetId
            }));
}

در متد بالا از کتابخانه PagedList.Mvc استفاده شده‌است. یکی از overload‌های متد PagedListPager آن، یک پارامتر از نوع Func<int, string>‎ به نام generatePageUrl را دریافت می‌کند که امکان شخصی سازی فرآیند تولید لینک به صفحات بعدی و قبلی را به ما می‌دهد. ما نیز از این امکان برای افزودن اطلاعات موجود در مدل Query، به کوئری استرینگ لینک‌های تولیدی استفاده کردیم و صرفا برای لینک‌های ایجادی لازم بود مقادیر پارامتر Page موجود در کوئری استرینگ تغییر کند که در کد بالا مشخص می‌باشد.


پیاده سازی هلپر PageSize

public static MvcHtmlString PageSize(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, object htmlAttributes = null, string filterFormId = null, params int[] numbers)
{
    if (numbers.Length == 0)
        numbers = new[] { 10, 20, 30, 50, 100 };

    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary)
    {
        [nameof(IPagedQueryModel.Page)] = 1
    };
    routeValueDictionary.Remove(nameof(IPagedQueryModel.PageSize));

    var url = urlFactory(routeValueDictionary);

    var formTag = new TagBuilder("form");
    formTag.Attributes.Add("action", url);
    formTag.Attributes.Add("method", "GET");
    formTag.Attributes.Add("data-ajax", "true");
    formTag.Attributes.Add("data-ajax-method", "GET");
    formTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    formTag.Attributes.Add("data-ajax-url", url);

    if (htmlAttributes != null)
        formTag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));

    formTag.AddCssClass("form-inline inline");

    var items = numbers.Select(number =>
        new SelectListItem
        {
            Value = number.ToString(),
            Text = number.ToString().ToPersianNumbers(),
            Selected = queryModel.PageSize == number
        });

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

    if (filterFormId.IsEmpty()) return new MvcHtmlString($"{formTag}");

    // ReSharper disable once MustUseReturnValue
    var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";

    return new MvcHtmlString($"{formTag}\n{scriptBlock}");
}

ایده کار به این صورت است که یک المنت select، درون یک المنت form قرار می‌گیرد و در زمان change آن، فرم مربوطه submit می‌شود.

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

در زمان تغییر تعداد نمایشی آیتم‌ها در هر صفحه، لازم است حالت فعلی گرید حفظ شود و صرفا پارامتر Page ریست شود.


نکته مهم: در این طراحی اگر فرم جستجویی دارید، در زمان جستجو هیچیک از پارامتر‌های مربوط به صفحه بندی و مرتب سازی به سرور ارسال نخواهند شد (در واقع ریست می‌شوند) و کافیست یک درخواست GET معمولی با ارسال محتویات فرم به سرور صورت گیرد؛ ولی لازم است PageSize تنظیم شده، در زمان اعمال فیلتر نیز به سرور ارسال شود. از این جهت اسکریپتی برای ایجاد یک input مخفی در فرم جستجو نیز هنگام رندر شدن PartialView در صفحه تزریق می‌شود.

  var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";


پیاده سازی هلپر Refresh

public static MvcHtmlString Refresh(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel,
    Func<RouteValueDictionary, string> urlFactory, string label = null)
{
    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary);

    var url = urlFactory(routeValueDictionary);

    var aTag = new TagBuilder("a");
    aTag.Attributes.Add("href", "#");
    aTag.Attributes.Add("role", "button");
    aTag.Attributes.Add("data-ajax", "true");
    aTag.Attributes.Add("data-ajax-method", "GET");
    aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    aTag.Attributes.Add("data-ajax-url", url);
    aTag.AddCssClass("btn btn-default");

    var iTag = new TagBuilder("i");
    iTag.AddCssClass("fa fa-refresh");

    aTag.InnerHtml = $"{iTag} {label}";

    return new MvcHtmlString(aTag.ToString());
}

متد بالا نیز به مانند refreshUrl که پیشتر مطرح شد، برای بارگزاری مجدد حالت فعلی گرید استفاده می‌شود و از این جهت است که مقادیر مربوط به کلیدهای routeValueDictionary  را تغییر نداده‌ایم.


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

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

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


نیاز به یک Dispatcher برای تعامل با بیش از یک مخزن حالت


در اینجا برای نمونه دو مخزن حالت تعریف شده‌اند؛ اما روش تعامل با این مخازن حالت، دیگر مانند قبل نیست. برای نمونه در اثر تعامل یک کاربر با View ای خاص، رخدادی صادر شده و اینبار مدیریت این رخداد توسط یک Action (که عموما یک پیام رشته‌ای است)، به Dispatcher مرکزی ارسال می‌شود (و نه مستقیما به مخزن حالت خاصی). اکنون این Dispatcher، اکشن رسیده را به مخازن کد مشترک به آن ارسال می‌کند تا عمل متناسب با آن اکشن درخواستی را انجام دهند. مابقی آن همانند قبل است که پس از تغییر حالت در هر کدام از مخازن حالت، کار به روز رسانی UI، در کامپوننت‌های مشترک صورت خواهد گرفت. بدیهی است در اینجا مخازن حالت، مجاز به صرفنظر کردن از یک اکشن خاص هستند و الزامی به پیاده سازی آن ندارند. هدف اصلی این است که اگر اکشنی قرار بود در تمام مخازن حالت پیاده سازی شود و حالت‌های آن‌ها را تغییر دهد، روشی را برای مدیریت آن داشته باشیم.
بنابراین اگر به این الگوی جدید دقت کنید، چیزی نیست بجز یک الگوی Observer دو سطحی:
الف) Dispatcher ای (Subject) که مشترک‌هایی را مانند مخازن حالت دارد (Observers).
ب) مخازن حالتی (Subjects) که مشترک‌هایی را مانند کامپوننت‌ها دارند (Observers).

اگر پیشتر با React کار کرده باشید، این الگو را تحت عناوینی مانند Flux و یا Redux می‌شناسید و در اینجا می‌خواهیم پیاده سازی #C آن‌را بررسی کنیم:


در الگوی Flux، در اثر تعامل یک کاربر با کامپوننتی، اکشنی به سمت یک Dispatcher ارسال می‌شود. سپس Dispatcher این اکشن را به مخزن حالتی جهت مدیریت آن ارسال می‌کند که در نهایت سبب تغییر حالت آن شده و به روز رسانی UI را در پی خواهد داشت.


پیاده سازی یک Dispatcher برای تعامل با بیش از یک مخزن حالت

پیش از هر کاری نیاز است قالب اکشن‌های ارسالی را که قرار است توسط مخازن حالت مورد پردازش قرار گیرند، مشخص کنیم:
namespace BlazorStateManagement.Stores
{
    public interface IAction
    {
        public string Name { get; }
    }
}
عموما هر اکشنی با نام و یا پیامی مشخص می‌شود. بر این اساس می‌توان اکشن افزودن و یا کاهش مقادیر شمارشگر را به صورت زیر تعریف کرد:
namespace BlazorStateManagement.Stores.CounterStore
{
    public class IncrementAction : IAction
    {
        public const string Increment = nameof(Increment);

        public string Name { get; } = Increment;
    }

    public class DecrementAction : IAction
    {
        public const string Decrement = nameof(Decrement);

        public string Name { get; } = Decrement;
    }
}
مزیت تعریف و استفاده از یک کلاس در اینجا این است که اگر نیاز بود به همراه اکشنی، اطلاعات اضافه‌تری نیز به سمت مخازن کد ارسال شوند، می‌توان آن‌ها را داخل هر کدام از کلاس‌ها، بسته به نیاز برنامه تعریف کرد و صرفا محدود به Name و یا یک مقدار رشته‌ای معرف آن، نخواهند بود.

پس از تعریف ساختار یک اکشن، اکنون نوبت به پیاده سازی راه حلی برای ارسال آن به تمام مخازن حالت برنامه است:
using System;

namespace BlazorStateManagement.Stores
{
    public interface IActionDispatcher
    {
        void Dispatch(IAction action);
        void Subscribe(Action<IAction> actionHandler);
        void Unsubscribe(Action<IAction> actionHandler);
    }

    public class ActionDispatcher : IActionDispatcher
    {
        private Action<IAction> _actionHandlers;

        public void Subscribe(Action<IAction> actionHandler) => _actionHandlers += actionHandler;

        public void Unsubscribe(Action<IAction> actionHandler) => _actionHandlers -= actionHandler;

        public void Dispatch(IAction action) => _actionHandlers?.Invoke(action);
    }
}
پیاده سازی ActionDispatcher ای را که ملاحظه می‌کنید، دقیقا مشابه CounterStore قسمت قبل است و در اینجا توسط متد Subscribe، مخازن حالت برنامه مشترک آن شده و یا توسط متد Unsubscribe، قطع اشتراک می‌کنند. همچنین متد Dispatch نیز شبیه به متد BroadcastStateChange قسمت قبل عمل می‌کند و سبب می‌شود تا اکشن ارسالی به آن، به تمام مشترکین این سرویس، ارسال شود.
این سرویس را نیز با طول عمر Scoped به سیستم تزریق وابستگی‌های برنامه معرفی می‌کنیم که سبب می‌شود تا پایان عمر برنامه (بسته شدن مرورگر یا ریفرش کامل صفحه‌ی جاری)، در حافظه باقی مانده و وهله سازی مجدد نشود. به همین جهت تزریق آن در مخازن حالت مختلف برنامه، دقیقا حالت یک Dispatcher اشتراکی را پیدا خواهد کرد.
namespace BlazorStateManagement.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IActionDispatcher, ActionDispatcher>();
            // ...
        }
    }
}


استفاده از IActionDispatcher در مخازن حالت برنامه

در ادامه می‌خواهیم مخازن حالت برنامه را تحت کنترل سرویس IActionDispatcher قرار دهیم تا کاربر بتواند اکشنی را به Dispatcher ارسال کند و سپس Dispatcher این درخواست را به تمام مخازن حالت موجود، جهت بروز واکنشی (در صورت نیاز)، اطلاعات رسانی نماید.
برای این منظور سرویس ICounterStore قسمت قبل ، به صورت زیر تغییر می‌کند که اینترفیس IDisposable را پیاده سازی کرده و همچنین دیگر به همراه متدهای عمومی افزایش و یا کاهش مقدار نیست:
using System;

namespace BlazorStateManagement.Stores.CounterStore
{
    public interface ICounterStore : IDisposable
    {
        CounterState State { get; }

        void AddStateChangeListener(Action listener);
        void BroadcastStateChange();
        void RemoveStateChangeListener(Action listener);
    }
}
بر این اساس، پیاده سازی CounterStore به صورت زیر تغییر خواهد کرد:
using System;

namespace BlazorStateManagement.Stores.CounterStore
{
    public class CounterStore : ICounterStore
    {
        private readonly CounterState _state = new();
        private bool _isDisposed;
        private Action _listeners;
        private readonly IActionDispatcher _actionDispatcher;

        public CounterStore(IActionDispatcher actionDispatcher)
        {
            _actionDispatcher = actionDispatcher ?? throw new ArgumentNullException(nameof(actionDispatcher));
            _actionDispatcher.Subscribe(HandleActions);
        }

        private void HandleActions(IAction action)
        {
            switch (action)
            {
                case IncrementAction:
                    IncrementCount();
                    break;
                case DecrementAction:
                    DecrementCount();
                    break;
            }
        }

        public CounterState State => _state;

        private void IncrementCount()
        {
            _state.Count++;
            BroadcastStateChange();
        }

        private void DecrementCount()
        {
            _state.Count--;
            BroadcastStateChange();
        }

        public void AddStateChangeListener(Action listener) => _listeners += listener;

        public void RemoveStateChangeListener(Action listener) => _listeners -= listener;

        public void BroadcastStateChange() => _listeners.Invoke();

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                try
                {
                    if (disposing)
                    {
                        _actionDispatcher.Unsubscribe(HandleActions);
                    }
                }
                finally
                {
                    _isDisposed = true;
                }
            }
        }
    }
}
توضیحات:
- با توجه به اینکه CounterStore یک سرویس ثبت شده‌ی در سیستم است، می‌تواند از مزیت تزریق سایر سرویس‌ها در سازنده‌ی خودش بهره‌مند شود؛ مانند تزریق سرویس جدید IActionDispatcher.
- پس از تزریق سرویس جدید IActionDispatcher، متدهای Subscribe آن‌را در سازنده‌ی کلاس و Unsubscribe آن‌را در حین Dispose سرویس، فراخوانی می‌کنیم. البته فراخوانی و یا پیاده سازی Unsubscribe و Dispose در اینجا غیرضروری است؛ چون طول عمر این کلاس با طول عمر برنامه یکی است.
- بر اساس این الگوی جدید، هر اکشنی که به سمت Dispatcher مرکزی ارسال می‌شود، در نهایت به متد HandleActions یکی از مخازن حالت تعریف شده، خواهد رسید:
        private void HandleActions(IAction action)
        {
            switch (action)
            {
                case IncrementAction:
                    IncrementCount();
                    break;
                case DecrementAction:
                    DecrementCount();
                    break;
            }
        }
در اینجا می‌توان با استفاده از patterns matching، بر اساس نوع اکشن مدنظر، عملیات خاصی را انجام داد. فقط در اینجا دیگر متدهای IncrementCount و DecrementCount، عمومی نیستند. به همین جهت باید به کامپوننت شمارشگر مراجعه کرد و تعریف قبلی:
@inject ICounterStore CounterStore

@code {

    private void IncrementCount()
    {
        CounterStore.IncrementCount();
    }
را به صورت زیر تغییر داد:
- ابتدا در انتهای فایل Client\_Imports.razor، فضای نام سرویس جدید IActionDispatcher را اضافه می‌کنیم:
@using BlazorStateManagement.Stores
- سپس از آن جهت ارسال IncrementAction به مخازن حالت برنامه استفاده خواهیم کرد:
// ...
@inject IActionDispatcher ActionDispatcher


@code {

    private void IncrementCount()
    {
        ActionDispatcher.Dispatch(new IncrementAction());
    }
با این تغییر جدید، هربار که بر روی دکمه‌ی افزایش مقدار شمارشگر، کلیک می‌شود، در آخر یک IncrementAction به تمام مخازن حالت موجود در برنامه ارسال خواهد شد و آن‌ها بر اساس نیازشان تصمیم خواهند گرفت که آیا به آن واکنش نشان دهند یا خیر.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorStateManagement-Part-2.zip
مطالب دوره‌ها
مدیریت استثناءها در حین استفاده از واژه‌های کلیدی async و await
زمانیکه یک متد async، یک Task یا Task of T (نسخه‌ی جنریک Task) را باز می‌گرداند، کامپایلر سی‌شارپ به صورت خودکار تمام استثناءهای رخ داده درون متد را دریافت کرده و از آن برای تغییر حالت Task به اصطلاحا faulted state استفاده می‌کند. همچنین زمانیکه از واژه‌ی کلیدی await استفاده می‌شود، کدهایی که توسط کامپایلر تولید می‌شوند، عملا مباحث Continue موجود در TPL یا Task parallel library معرفی شده در دات نت 4 را پیاده سازی می‌کنند و نهایتا نتیجه‌ی Task را در صورت وجود، دریافت می‌کند. زمانیکه نتیجه‌ی یک Task مورد استفاده قرار می‌گیرد، اگر استثنایی وجود داشته باشد، مجددا صادر خواهد شد. برای مثال اگر خروجی یک متد async از نوع Task of T باشد، امکان استفاده از خاصیتی به نام Result نیز برای دسترسی به نتیجه‌ی آن وجود دارد:
using System.Threading.Tasks;

namespace Async05
{
    class Program
    {
        static void Main(string[] args)
        {
            var res = doSomethingAsync().Result;
        }

        static async Task<int> doSomethingAsync()
        {
            await Task.Delay(1);
            return 1;
        }
    }
}
در این مثال یکی از روش‌های استفاده از متدهای async را در یک برنامه‌ی کنسول مشاهده می‌کنید. هر چند خروجی متد doSomethingAsync از نوع Task of int است، اما مستقیما یک int بازگشت داده شده است. تبدیلات نهایی در اینجا توسط کامپایلر انجام می‌شود. همچنین نحوه‌ی استفاده از خاصیت Result را نیز در متد Main مشاهده می‌کنید.
البته باید دقت داشت، زمانیکه از خاصیت Result استفاده می‌شود، این متد همزمان عمل خواهد کرد و نه غیرهمزمان (ترد جاری را بلاک می‌کند؛ یکی از موارد مجاز استفاده از آن در متد Main برنامه‌های کنسول است). همچنین اگر در متد doSomethingAsync استثنایی رخ داده باشد، این استثناء زمان استفاده از Result، به صورت یک AggregateException مجددا صادر خواهد شد. وجود کلمه‌ی Aggregate در اینجا به علت امکان استفاده‌ی تجمعی و ترکیب چندین Task باهم و داشتن چندین شکست و استثنای ممکن است.
همچنین اگر از کلمه‌ی کلیدی await بر روی یک faulted task استفاده کنیم، AggregateException صادر نمی‌شود. در این حالت کامپایلر AggregateException را بررسی کرده و آن‌را تبدیل به یک Exception متداول و معمول کدهای دات نت می‌کند. به عبارتی سعی شده‌است در این حالت، رفتار کدهای async را شبیه به رفتار کدهای متداول همزمان شبیه سازی کنند.


یک مثال

در اینجا توسط متد getTitleAsync، اطلاعات یک صفحه‌ی وب به صورت async دریافت شده و سپس عنوان آن استخراج می‌شود. در متد showTitlesAsync نیز از آن استفاده شده و در طی یک حلقه، چندین وب سایت مورد بررسی قرار خواهند گرفت. چون متد getTitleAsync از نوع async تعریف شده‌است، فراخوان آن نیز باید async تعریف شود تا بتوان از واژه‌ی کلیدی  await برای کار با آن استفاده کرد.
نهایتا در متد Main برنامه، وظیفه‌ی غیرهمزمان showTitlesAsync اجرا شده و تا پایان عملیات آن صبر می‌شود. چون خروجی آن از نوع Task است و نه Task of T، در اینجا دیگر خاصیت Result قابل دسترسی نیست. متد Wait نیز ترد جاری را همانند خاصیت Result بلاک می‌کند.
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Async05
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = showTitlesAsync(new[]
            {
                "http://www.google.com",
                "https://www.dntips.ir"
            });
            task.Wait();

            Console.WriteLine();
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }

        static async Task showTitlesAsync(IEnumerable<string> urls)
        {
            foreach (var url in urls)
            {
                var title = await getTitleAsync(url);
                Console.WriteLine(title);
            }
        }

        static async Task<string> getTitleAsync(string url)
        {
            var data = await new WebClient().DownloadStringTaskAsync(url);
            return getTitle(data);
        }

        private static string getTitle(string data)
        {
            const string patternTitle = @"(?s)<title>(.+?)</title>";
            var regex = new Regex(patternTitle);
            var mc = regex.Match(data);
            return mc.Groups.Count == 2 ? mc.Groups[1].Value.Trim() : string.Empty;
        }
    }
}
کلیه عملیات مبتنی برشبکه، همیشه مستعد به بروز خطا هستند. قطعی ارتباط یا حتی کندی آن می‌توانند سبب بروز استثناء شوند.
برنامه را در حالت عدم اتصال به اینترنت اجرا کنید. استثنای صادر شده، در متد task.Wait ظاهر می‌شود (چون متدهای async ترد جاری را خالی کرده‌اند):


و اگر در اینجا بر روی لینک View details کلیک کنیم، در inner exception حاصل، خطای واقعی قابل مشاهده است:


همانطور که ملاحظه می‌کنید، استثنای صادر شده از نوع System.AggregateException است. به این معنا که می‌تواند حاوی چندین استثناء باشد که در اینجا تعداد آن‌ها با عدد یک مشخص شده‌است. بنابراین در این حالات، بررسی inner exception را فراموش نکنید.

در ادامه داخل حلقه‌ی foreach متد showTitlesAsync، یک try/catch قرار می‌دهیم:
        static async Task showTitlesAsync(IEnumerable<string> urls)
        {
            foreach (var url in urls)
            {
                try
                {
                    var title = await getTitleAsync(url);
                    Console.WriteLine(title);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
        }
اینبار اگر برنامه را اجرا کنیم، خروجی ذیل را در صفحه می‌توان مشاهده کرد:
 System.Net.WebException: The remote server returned an error: (502) Bad Gateway.
System.Net.WebException: The remote server returned an error: (502) Bad Gateway.

Press any key to exit...
در اینجا دیگر خبری از AggregateException نبوده و استثنای واقعی رخ داده در متد await شده بازگشت داده شده‌است. کار واژه‌ی کلیدی await در اینجا، بررسی استثنای رخ داده در متد async فراخوانی شده و بازگشت آن به جریان متداول متد جاری است؛ تا نتیجه‌ی عملیات همانند یک کد کامل همزمان به نظر برسد. به این ترتیب کامپایلر توانسته است رفتار بروز استثناءها را در کدهای همزمان و غیرهمزمان یک دست کند. دقیقا مانند حالتی که یک متد معمولی در این بین فراخوانی شده و استثنایی در آن رخ داده‌است.


مدیریت تمام inner exceptionهای رخ داده در پردازش‌های موازی

همانطور که عنوان شد، await تنها یک استثنای حاصل از Task در حال اجرا را به کد فراخوان بازگشت می‌دهد. در این حالت اگر این Task، چندین شکست را گزارش دهد، چطور باید برای دریافت تمام آن‌ها اقدام کرد؟ برای مثال استفاده از Task.WhenAll می‌تواند شامل چندین استثنای حاصل از چندین Task باشد، ولی await تنها اولین استثنای دریافتی را بازگشت می‌دهد. اما اگر از خاصیتی مانند Result یا متد Wait استفاده شود، یک AggregateException حاصل تمام استثناءها را دریافت خواهیم کرد. بنابراین هرچند await تنها اولین استثنای دریافتی را بازگشت می‌دهد، اما می‌توان به Taskهای مرتبط مراجعه کرد و سپس بررسی نمود که آیا استثناهای دیگری نیز وجود دارند یا خیر؟
برای نمونه در مثال فوق، حلقه‌ی foreach تشکیل شده آنچنان بهینه نیست. از این جهت که هر بار تنها یک سایت را بررسی می‌کند، بجای اینکه مانند مرورگرها چندین ترد را به یک یا چند سایت باز کرده و نتایج را دریافت کند.
البته انجام کارها به صورت موازی همیشه ایده‌ی خوبی نیست ولی حداقل در این حالت خاص که با یک یا چند سرور راه دور کار می‌کنیم، درخواست‌های همزمان دریافت اطلاعات، سبب کارآیی بهتر برنامه و بالا رفتن سرعت اجرای آن می‌شوند. اما مثلا در حالتیکه با سخت دیسک سیستم کار می‌کنیم، اجرای موازی کارها نه تنها کمکی نخواهد کرد، بلکه سبب خواهد شد تا مدام drive head در مکان‌های مختلفی مشغول به حرکت شده و در نتیجه کارآیی آن کاهش یابد.
برای ترکیب چندین Task، ویژگی خاصی به زبان سی‌شارپ اضافه نشده‌، زیرا نیازی نبوده است. برای این حالت تنها کافی است از متد Task.WhenAll، برای ساخت یک Task مرکب استفاده کرد. سپس می‌توان واژه‌ی کلیدی await را بر روی این Task مرکب فراخوانی کرد.
همچنین می‌توان از متد ContinueWith یک Task مرکب نیز برای جلوگیری از بازگشت صرفا اولین استثنای رخ داده توسط کامپایلر، استفاده کرد. در این حالت امکان دسترسی به خاصیت Result آن به سادگی میسر می‌شود که حاوی AggregateException کاملی است.


اعتبارسنجی آرگومان‌های ارسالی به یک متد async

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


از دست دادن استثناءها

فرض کنید مانند مثال قسمت قبل، دو وظیفه‌ی async آغاز شده و نتیجه‌ی آن‌ها پس از await هر یک، با هم جمع زده می‌شوند. در این حالت اگر کل عملیات را داخل یک قطعه کد try/catch قرار دهیم، اولین await ایی که یک استثناء را صادر کند، صرفنظر از وضعیت await دوم، سبب اجرای بدنه‌ی catch می‌شود. همچنین انجام این عملیات بدین شکل بهینه نیست. زیرا ابتدا باید صبر کرد تا اولین Task تمام شود و سپس دومین Task شروع گردد و به این ترتیب پردازش موازی Taskها را از دست خواهیم داد. در یک چنین حالتی بهتر است از متد await Task.WhenAll استفاده شود. در اینجا دو Task مورد نیاز، تبدیل به یک Task مرکب می‌شوند. این Task مرکب تنها زمانی خاتمه می‌یابد که هر دوی Task اضافه شده به آن، خاتمه یافته باشند. به این ترتیب علاوه بر اجرای موازی Taskها، امکان دریافت استثناءهای هر کدام را نیز به صورت تجمعی خواهیم داشت.
مشکل! همانطور که پیشتر نیز عنوان شد، استفاده از await در اینجا سبب می‌شود تا کامپایلر تنها اولین استثنای دریافتی را بازگشت دهد و نه یک AggregateException نهایی را. روش حل آن‌را نیز عنوان کردیم. در این حالت بهتر است از متد ContinueWith و سپس استفاده از خاصیت Result آن برای دریافت کلیه استثناءها کمک گرفت.
حالت دوم از دست دادن استثناءها زمانی‌است که یک متد async void را ایجاد می‌کنید. در این حالات بهتر است از یک Task بجای بازگشت void استفاده شود. تنها علت وجودی async voidها، استفاده از آن‌ها در روال‌های رویدادگردان UI است (در سایر حالات code smell درنظر گرفته می‌شود).
public async Task<double> GetSum2Async()
        {
            try
            {
                var task1 = GetNumberAsync();
                var task2 = GetNumberAsync();

                var compositeTask = Task.WhenAll(task1, task2);
                await compositeTask.ContinueWith(x => { });

                return compositeTask.Result[0] + compositeTask.Result[1];
            }
            catch (Exception ex)
            {
                //todo: log ex
                throw;
            }
        }
در مثال فوق، نحوه‌ی ترکیب دو Task را توسط Task.WhenAll جهت اجرای موازی و سپس اعمال نکته‌ی یک ContinueWith خالی و در ادامه استفاده از Result نهایی را جهت دریافت تمامی استثناءهای حاصل، مشاهده می‌کنید.
در این مثال دیگر مانند مثال قسمت قبل
        public async Task<double> GetSumAsync()
        {
            var leftOperand = await GetNumberAsync();
            var rightOperand = await GetNumberAsync();

            return leftOperand + rightOperand;
        }
هر بار صبر نشده‌است تا یک Task تمام شود و سپس Task بعدی شروع گردد.
با کمک متد Task.WhenAll ترکیب آن‌ها ایجاد و سپس با فراخوانی await، سبب اجرای موازی چندین Task با هم شده‌ایم.


مدیریت خطاهای مدیریت نشده

ابتدا مثال زیر را در نظر بگیرید:
using System;
using System.Threading.Tasks;

namespace Async01
{
    class Program
    {
        static void Main(string[] args)
        {
            Test2();
            Test();
            Console.ReadLine();

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.ReadLine();
        }

        public static async Task Test()
        {
            throw new Exception();
        }

        public static async void Test2()
        {
            throw new Exception();
        }
    }
}
در این مثال دو متد که یکی async Task و دیگری async void است، تعریف شده‌اند.
اگر برنامه را کامپایل کنید، کامپایلر بر روی سطر فراخوانی متد Test اخطار زیر را صادر می‌کند. البته برنامه بدون مشکل کامپایل خواهد شد.
 Warning  1  Because this call is not awaited, execution of the current method continues before the call is completed.
Consider applying the 'await' operator to the result of the call.
اما چنین اخطاری در مورد async void صادر نمی‌شود. بنابراین ممکن است جایی در کدها، فراخوانی await فراموش شود. اگر خروجی متد شما ازنوع Task و مشتقات آن باشد، کامپایلر حتما اخطاری را جهت رفع آن گوشزد خواهد کرد؛ اما نه در مورد متدهای void که صرفا جهت کاربردهای UI و روال‌های رخدادگردان آن طراحی شده‌اند.
همچنین اگر برنامه را اجرا کنید استثنای صادر شده در متد async void سبب کرش برنامه می‌شود؛ اما نه استثنای صادر شده در متد async Task. متدهای async void چون دارای Synchronization Context نیستند، استثنای صادره را به Thread pool برنامه صادر می‌کنند. به همین جهت در همان لحظه نیز سبب کرش برنامه خواهند شد. اما در حالت async Task به این نوع استثناءها اصطلاحا Unobserved Task Exception گفته شده و سبب بروز  faulted state در Task تعریف شده می‌گردند.
برای مدیریت آن‌ها در سطح برنامه باید در ابتدای کار و در متد Main، توسط TaskScheduler.UnobservedTaskException روال رخدادگردانی را برای مدیریت اینگونه استثناءها تدارک دید. زمانیکه GC شروع به آزاد سازی منابع می‌کند، این استثناءها نیز درنظر گرفته شده و سبب کرش برنامه خواهند شد. با استفاده از متد SetObserved همانند قطعه کد زیر، می‌توان از کرش برنامه جلوگیری کرد:
using System;
using System.Threading.Tasks;

namespace Async01
{
    class Program
    {
        static void Main(string[] args)
        {
            TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

            //Test2();
            Test();
            Console.ReadLine();

            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.ReadLine();
        }

        private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            e.SetObserved();
            Console.WriteLine(e.Exception);
        }

        public static async Task Test()
        {
            throw new Exception();
        }

        public static async void Test2()
        {
            throw new Exception();
        }
    }
}
البته لازم به ذکر است که این رفتار در دات نت 4.5 به این شکل تغییر کرده است تا کار با متدهای async ساده‌تر شود. در دات نت 4، یک چنین استثناءهای مدیریت نشده‌ای،‌بلافاصله سبب بروز استثناء و کرش برنامه می‌شدند.
به عبارتی رفتار قطعه کد زیر در دات نت 4 و 4.5 متفاوت است:
Task.Factory.StartNew(() => { throw new Exception(); });

Thread.Sleep(100);
GC.Collect();
GC.WaitForPendingFinalizers();
در دات نت 4  اگر این برنامه را خارج از VS.NET اجرا کنیم، برنامه کرش می‌کند؛ اما در دات نت 4.5 خیر و آن‌ها به UnobservedTaskException یاد شده هدایت خواهند شد. اگر می‌خواهید این رفتار را به همان حالت دات نت 4 تغییر دهید، تنظیم زیر را به فایل config برنامه اضافه کنید:
 <configuration>
    <runtime>
      <ThrowUnobservedTaskExceptions enabled="true"/>
    </runtime>
</configuration>


یک نکته‌ی تکمیلی: ممکن است عبارات lambda مورد استفاده، از نوع async void باشد.

همانطور که عنوان شد باید از async void منهای مواردی که کار مدیریت رویدادهای عناصر UI را انجام می‌دهند (مانند برنامه‌های ویندوز 8)، اجتناب کرد. چون پایان کار آن‌ها را نمی‌توان تشخیص داد و همچنین کامپایلر نیز اخطاری را در مورد استفاده ناصحیح از آن‌ها بدون await تولید نمی‌کند (چون نوع void اصطلاحا awaitable نیست). به علاوه بروز استثناء در آن‌ها، بلافاصله سبب خاتمه برنامه می‌شود. بنابراین اگر جایی در برنامه متد async void وجود دارد، قرار دادن try/catch داخل بدنه‌ی آن ضروری است.
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
    try
    {
        ClickMeButton.Tapped += async (sender, args) =>
        {
             throw new Exception();        

        };
    }
    catch (Exception ex)
    {
        // This won’t catch exceptions!
        TextBlock1.Text = ex.Message;
    }
}
در این مثال خاص ویندوز 8، شاید به نظر برسد که try/catch تعریف شده سبب مهار استثنای صادر شده می‌شود؛ اما خیر!
 public delegate void TappedEventHandler(object sender, TappedRoutedEventArgs e);
امضای متد TappedEventHandler از نوع delegate void است. بنابراین try/catch را باید داخل بدنه‌ی روال رویدادگردان تعریف شده قرار داد و نه خارج از آن.
مطالب
کوئری های پیشرفته، Error Handling و Data Loader در GraphQL
در قسمت قبل یادگرفتیم که چگونه GraphQL را با ASP.NET Core یکپارچه کنیم و اولین GraphQL query را ایجاد و داده‌ها را از سرور بازیابی کردیم. البته ما به این query ‌های ساده بسنده نخواهیم کرد. در این قسمت می‌خواهیم یاد بگیریم که چگونه query ‌های پیشرفته‌ی GraphQL را بنویسیم و در زمان انجام این کار، نمایش دهیم که چگونه خطا‌ها را  مدیریت کنیم و علاوه بر این با queries, aliases, arguments, fragments نیز کار خواهیم کرد.

Creating Complex Types for GraphQL Queries 
اگر نگاهی به owners و query (در پایان قسمت قبل)  بیندازیم، متوجه خواهیم شد که یک لیست از خصوصیات مدل Owner که در OwnerType معرفی شده‌اند، نسبت به کوئری برگشت داده می‌شود. OwnerType شامل فیلد‌های Id , Name  و Address می‌باشد. یک Owner می‌تواند چندین account مرتبط با خود را داشته باشد. هدف این است که در owners ،query لیست account ‌های مربوط به هر owner را بازگشت دهیم. 
قبل از اضافه کردن فیلد Accounts در کلاس OwnerType نیاز است کلاس AccountType را ایجاد کنیم. در ادامه یک کلاس را به نام AccountType در پوشه GraphQLTypes ایجاد می‌کنیم. 
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the account object.");
        Field(x => x.Description).Description("Description property from the account object.");
        Field(x => x.OwnerId, type: typeof(IdGraphType)).Description("OwnerId property from the account object.");
    }
}

 همانطور که مشخص است، خصوصیت Type را از کلاس Account، معرفی نکرده‌ایم (در ادامه اینکار را انجام خواهیم داد). در ادامه، واسط IAccountRepository و کلاس AccountRepository را باز کرده و آن را مطابق زیر ویرایش می‌کنیم: 
public interface IAccountRepository
{
    IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId);
}

public class AccountRepository : IAccountRepository
{
    private readonly ApplicationContext _context;
 
    public AccountRepository(ApplicationContext context)
    {
       _context = context;
    }
 
    public IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId) => _context.Accounts
        .Where(a => a.OwnerId.Equals(ownerId))
        .ToList();
}

اکنون می‌توان لیست account‌ها را به نتیجه owners ، query اضافه کنیم. پس کلاس OwnerType را باز کرده و آن را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository)
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object.");
        Field(x => x.Name).Description("Name property from the owner object.");
        Field(x => x.Address).Description("Address property from the owner object.");
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context => repository.GetAllAccountsPerOwner(context.Source.Id)
        );
    }
}

چیز خاصی در اینجا وجود ندارد که ما تا کنون ندیده باشیم. به همان روش که یک فیلد را در کلاس AppQuery  ایجاد کردیم، یک فیلد را با نام  accounts در کلاس OwnerType ایجاد می‌کنیم. همچنین متد GetAllAccountsPerOwner نیاز به پارامتر id را دارد و این پارامتر را از طریق context.Source.Id فراهم می‌کنیم. زیرا context شامل خصوصیت Source است که در این حالت مشخص نوع Owner می‌باشد.

اکنون پروژه را اجرا کنید و به آدرس زیر بروید:
https://localhost:5001/ui/playground
سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید که نتیجه آن علاوه بر owner‌ها، لیست account ‌های مربوط به هر owner هم می‌باشد:  
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      ownerId
    }
  }
}

Adding Enumerations in GraphQL Queries 
در کلاس AccountType  فیلد Type را اضافه نکرده‌ایم و این کار را عمدا انجام داده‌ایم. اکنون زمان انجام این کار می‌باشد. برای اضافه کردن گونه شمارشی به کلاس AccountType نیاز است تا در ابتدا یک کلاس تعریف شود که نسبت به type ‌های معمول در GraphQL متفاوت است. یک کلاس را به نام AccountTypeEnumType  در پوشه GraphQLTypes  ایجاد کرده و آن را مطابق زیر ویرایش می‌کنیم:  
public class AccountTypeEnumType : EnumerationGraphType<TypeOfAccount>
{
    public AccountTypeEnumType()
    {
        Name = "Type";
        Description = "Enumeration for the account type object.";
    }
}

کلاس AccountTypeEnumType باید از نوع جنریک کلاس EnumerationGraphType ارث بری کند و پارامتر جنریک آن، یک گونه شمارشی را دریافت می‌کند (که در قسمت قبل آن را ایجاد کردیم؛ TypeOfAccount).  همچنین مقدار خصوصیت Name نیز باید همان نام خصوصیت گونه شمارشی در کلاس Account باشد (نام آن در کلاس Account مساوی Type می‌باشد). سپس گونه شمارشی را در کلاس AccountType به صورت زیر اضافه می‌کنیم:
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        ...
        Field<AccountTypeEnumType>("Type", "Enumeration for the account type object.");
    }
}

اکنون پروژه را اجرا کنید و سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید:
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
}
که نتیجه آن اضافه شدن type  به هر account می‌باشد:


Implementing a Cache in the GraphQL Queries with Data Loader  
دیدم که query، نتیجه دلخواهی را برای ما بازگشت می‌دهد؛ اما این query هنوز به اندازه کافی بهینه نشده‌است. مشکل چیست؟
query  ایجاد شده به حالتی کار می‌کند که در ابتدا همه owner ‌ها را بازیابی می‌کند. سپس به ازای هر owner، یک Sql Query  را به سمت بانک اطلاعاتی ارسال می‌کند تا Account ‌های مربوط به آن Owner را بازگشت دهد که می‌توان log  آن را در Terminal مربوط به VS Code مشاهده کرد.
 


البته زمانیکه چند موجودیت owner را داشته باشیم، این مورد یک مشکل نمی‌باشد؛ ولی وقتی تعداد موجودیت‌ها زیاد باشد چطور؟
 owners ، query را می‌توان با استفاده از DataLoader که توسط GraphQL فراهم شده‌است، بهینه سازی کرد. جهت انجام اینکار در ابتدا واسط IAccountRepository و کلاس AccountRepository را همانند زیر ویرایش می‌کنیم:
public interface IAccountRepository
{
    ...
    Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds);
}
public class AccountRepository : IAccountRepository
{
    ...
    public async Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds)
    {
        var accounts = await _context.Accounts.Where(a => ownerIds.Contains(a.OwnerId)).ToListAsync();
        return accounts.ToLookup(x => x.OwnerId);
    }
}

 نیاز است که یک متد داشته باشیم که <<Task<ILookup<TKey, T  را برگشت می‌دهد؛ زیرا DataLoader نیازمند یک متد با نوع برگشتی که در امضایش عنوان شده است می‌باشد .
در ادامه کلاس OwnerType را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository, IDataLoaderContextAccessor dataLoader)
    {
       ...
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context =>
            {
                var loader = dataLoader.Context.GetOrAddCollectionBatchLoader<Guid, Account>("GetAccountsByOwnerIds", repository.GetAccountsByOwnerIds);
                return loader.LoadAsync(context.Source.Id);
            });
    }
}

در کلاس OwnerType، واسط IDataLoaderContextAccessor را در سازنده کلاس تزریق می‌کنیم و سپس متد Context.GetOrAddCollectionBatchLoader را فراخوانی می‌کنیم که در پارامتر اول آن، یک کلید و در پارامتر دوم آن، متد GetAccountsByOwnerIds را از IAccountRepository معرفی می‌کنیم.
سپس باید DataLoader را در متد ConfigureServices موجود در کلاس Startup ثبت کنیم. در ادامه services.AddGraphQL را مطابق زیر ویرایش می‌کنیم: 
services.AddGraphQL(o => { o.ExposeExceptions = false; })
        .AddGraphTypes(ServiceLifetime.Scoped)
        .AddDataLoader(); 
اکنون پروژه را با دستور زیر اجرا کنید و سپس query قبلی را در UI.Playground اجرا کنید.
 اگر log  موجود در Terminal مربوط به  VS Code را مشاهده کنید، متوجه خواهید شد که در این حالت یک query برای تمام owner ‌ها و یک query برای تمام account ‌ها داریم.


Using Arguments in Queries and Handling Errors 
تا کنون ما  یک query را اجرا می‌کردیم که نتیجه آن بازیابی تمام owner ‌ها به همراه تمام account ‌های مربوط به هر owner بود. اکنون می‌خواهیم  براساس id، یک owner  مشخص را بازیابی کنیم. برای انجام این کار نیاز است که یک آرگومان را در query شامل کنیم.
در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را همانند زیر ویرایش می‌کنیم:
public interface IOwnerRepository
{
    ...
    Owner GetById(Guid id);
}

public class OwnerRepository : IOwnerRepository
{
    ...
    Owner GetById(Guid id) => _context.Owners.SingleOrDefault(o => o.Id.Equals(id));
}
سپس کلاس AppQuery  را مطابق زیر ویرایش می‌کنیم:
public class AppQuery : ObjectGraphType
{
    public AppQuery(IOwnerRepository repository)
    {
        ...

        Field<OwnerType>(
            "owner",
            arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
            resolve: context =>
            {
                var id = context.GetArgument<Guid>("ownerId");
                return repository.GetById(id);
            }
        );
    }
}
در اینجا یک فیلد را ایجاد کرده‌ایم که مقدار برگشتی آن یک OwnerType می‌باشد. نام query را owner تعیین می‌کنیم و از بخش arguments، برای ایجاد کردن آرگومان‌های این query استفاده می‌کنیم. آرگومان این query نمی‌تواند NULL باشد و باید از نوع IdGraphType و با نام ownerId باشد و در نهایت بخش resolve است که کاملا گویا می‌باشد. 
اگر پارامتر id، از نوع Guid نباشد، بهتر است که یک پیام را به سمت کلاینت برگشت دهیم. جهت انجام این کار یک اصلاح کوچک در بخش resolve انجام میدهیم:  
Field<OwnerType>(
    "owner",
    arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
    resolve: context =>
    {
        Guid id;
        if (!Guid.TryParse(context.GetArgument<string>("ownerId"), out id))
        {
            context.Errors.Add(new ExecutionError("Wrong value for guid"));
            return null;
        }
 
         return repository.GetById(id);
     }
);

 اکنون پروژه را اجرا کنید و سپس یک query جدید را در  UI.Playground به صورت زیر ارسال  کنید: 
{
  owner(ownerId:"6f513773-be46-4001-8adc-2e7f17d52d83"){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
که نتیجه آن بازیابی یک owner  با ( Id=6f513773-be46-4001-8adc-2e7f17d52d83 ) می‌باشد.

نکته: 
در صورتیکه قصد داشته باشیم علاوه بر id، یک name را هم ارسال کنیم، در بخش resolve به صورت زیر آن را دریافت می‌کنیم: 
 string name = context.GetArgument<string>("name");
و در زمان ارسال query:
{
  owner(ownerId:"53270061-3ba1-4aa6-b937-1f6bc57d04d2", name:"ANDY") {
   ...
  }
}


Aliases, Fragments, Named Queries, Variables, Directives 
می توانیم برای query ‌های ارسال شده از سمت کلاینت با معرفی aliases، یک سری تغییرات را داشته باشیم. وقتی‌که می‌خواهیم نام نتیجه دریافتی یا هر فیلدی را در نتیجه دریافتی تغییر دهیم، بسیار کاربردی می‌باشند. اگر یک query داشته باشیم که یک آرگومان را دارد و بخواهیم دو تا از این query داشته باشیم، برای ایجاد تفاوت بین query ‌ها می‌توان از aliases  استفاده کرد. 
جهت استفاده باید نام مورد نظر را در ابتدای query یا فیلد قرار دهیم:
{
  first:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  },
  second:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  }
}
اینبار در خروجی بجای ownerId ، id و بجای ownerName ، name و ... را مشاهده خواهید کرد.


همانطور که از مثال بالا مشخص است، دو query با فیلد‌های یکسانی را داریم. اگر بجای 2  query یکسان (مانند مثال بالا) ولی با آرگومان‌های متفاوت، اینبار 10 query یکسان با آرگومان‌های متفاوتی را داشته باشیم، در این حالت خواندن query ‌ها مقداری سخت می‌باشد. در این صورت می‌توان این مشکل را با استفاده از fragment‌ها برطرف کرد. Fragment‌‌ها این اجازه را به ما می‌دهند تا فیلد‌ها را با استفاده از کاما ( ، ) از یکدیگر جدا و تبدیل به یک بخش مجزا کنیم و سپس استفاده مجدد از آن بخش را در تمام query ‌ها داشته باشیم. Syntax آن به حالت زیر می‌باشد: 
fragment SampleName on Type{
  ...
}
تعریف یک fragment به نام ownerFields  و استفاده از آن : 
{
  first:owners{
    ...ownerFields
  },
  second:owners{
    ...ownerFields
  },
  ...
}


fragment ownerFields on OwnerType{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
}

برای ایجاد کردن یک named query، مجبور هستیم از کلمه کلیدی query در آغاز کل query استفاده کنیم؛ به همراه نام query، که بعد از کلمه کلیدی query قرار میگیرد. اگر نیاز داشته باشیم می‌توان آرگومان‌ها را به query ارسال کرد.
نکته مهمی که در رابطه با named query ‌ها وجود دارد این است که اگر یک query آرگومان داشته باشد نیاز است از پنجره QUERY VARIABLES برای تخصیص مقدار به آن آرگومان استفاده کنیم. 
query OwnerQuery($ownerId:ID!)
{
  owner(ownerId:$ownerId){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type
    }
  }
}
و سپس در قسمت QUERY VARIABLES 
{
  "ownerId":"6f513773-be46-4001-8adc-2e7f17d52d83"
}
اکنون اجرا کنید و خروجی را مشاهده کنید .

در نهایت می‌توان بعضی فیلد‌ها را از نتیجه دریافتی با استفاده از directive‌ها در query حذف یا اضافه کرد. دو directive وجود دارد که می‌توان از آن‌ها استفاده کرد (include  و skip). 


در قسمت بعد در رابطه با GraphQL Mutations صحبت خواهیم کرد.  
کد‌های مربوط به این قسمت را از اینجا دریافت کنید .  ASPCoreGraphQL_2.zip  
مطالب
استفاده از Google Analytics در ASP.Net

قبل از استفاده از بلاگر، در سایت wordpress وبلاگ داشتم، که به‌دلایلی کنسل شد. تفاوت محسوسی را که اینجا مشاهده می‌کنم، نبود قسمت آمار سایت است. در سایت wordpress آمار مبسوطی را از بازدید کنندگان سایت می‌توانید در کنترل پنل مدیریتی وبلاگ مشاهده کنید، اما در اینجا خیر.
به همین جهت اولین کاری را که انجام دادم استفاده از سرویس رایگان persianstat بود که انصافا هم با کیفیت است و قابل مقایسه با آماری که wordpress ارائه می‌دهد، می‌باشد.
جالب اینجا است که هر چند هاست اینجا، گوگل است اما استفاده‌ی خودکار از ابزار Google analytics در آن مهیا نیست. احتمالا علت آن آماده نبودن API آن است که قرار است به زودی ارائه شود، بنابراین ارزش وقت گذاشتن را دارد.



برای استفاده از Google analytics ، پس از ثبت نام و ورود به آن، سایت مورد نظر را معرفی کرده (در قسمت Add Website Profile) و نهایتا یک کد جاوا اسکریپتی به شما خواهد داد که می‌توانید آنرا به صفحات مورد نظر خود در سایت اضافه نمائید تا تحت کنترل آماری قرار گیرد. محدودیتی هم در مورد تعداد سایت وجود ندارد و با یک اکانت می‌توانید چندین سایت را معرفی کرده و تحت کنترل قرار دهید.
اگر از ASP.Net استفاده می‌کنید، تنها کافی است به master page سایت مراجعه کنید و پیش از بسته شدن تگ body ، اسکریپت مربوط به Google analytics را اضافه کنید تا تمام سایت را تحت کنترل قرار دهید.
یا اگر علاقمند بودید که اینکار را به صورت "شیک‌تری" انجام دهید، می‌توان از این http module استفاده کرد. به این صورت ابتدا تگ بسته شدن body به صورت خودکار پیدا شده و سپس اسکریپت به پیش از آن اضافه می‌شود.
این روش بار بزرگ تهیه آمار سایت را حذف خواهد کرد. عموما دیتابیس جمع آوری آمار سایت خیلی زود (برای مثال پس از گذشت 6 ماه) حجیم می‌شود و تاثیر مشهودی را بر روی کارآیی سایت خواهد گذاشت. بنابراین، این سؤال مطرح می‌شود که چرا گوگل اینکار را برای ما انجام ندهد؟! هزینه بانک اس کیوال سرور بر روی هاست‌های اینترنتی بالا بوده و حجمی را هم که در اختیار قرار می‌دهند محدود است. در صورت نیاز به حجم‌های بالاتر باید هزینه بیشتری را پرداخت کرد. بنابراین هم از لحاظ قیمت و هچنین کارآیی سایت، استفاده از این سرویس واقعا مقرون به صرفه است. بعلاوه از تنوع آماری که ارائه می‌دهد نیز نمی‌توان چشم پوشی کرد. برای مثال کاربران چه واژه‌های کلیدی را در موتورهای جستجو وارد کرده‌اند تا به سایت شما رسیده‌اند؟ چند درصد کاربر وفادار دارید؟! (کاربرهای وفادار، منظور افرادی هستند که به صورت منظم به سایت سر می‌زنند) و امثال این. انصافا تهیه چنین ماژولی برای یک سایت از لحاظ برنامه نویسی شاید با برنامه نویسی کل یک سایت برابری کند.
اگر هم نیاز به یک برنامه سورس باز داشتید که هر روز به اکانت Google analytics شما سر بزند و اطلاعات آنرا استخراج کرده و در یک بانک SQL server ذخیره کند، می‌توانید به پروژه سی شارپ زیر مراجعه نمائید:
Google Analytics Data Extractor

البته باید دقت داشت که پس از ارائه API کامل Google analytics ، دیگر نیازی به این نوع روش‌های ابتکاری وجود نداشته و استخراج داده از آن بسیار ساده‌تر خواهد شد.