CREATE TABLE [dbo].[Tree]( [Id] [int] IDENTITY(1,1) NOT NULL, [Name] [nchar](10) NOT NULL, [ParentId] [int] NOT NULL, CONSTRAINT [PK_Tree] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[Tree] WITH CHECK ADD CONSTRAINT [FK_Tree_Tree] FOREIGN KEY([ParentId]) REFERENCES [dbo].[Tree] ([Id]) GO ALTER TABLE [dbo].[Tree] CHECK CONSTRAINT [FK_Tree_Tree] GO
حالتهای مختلف ذخیره سازی اطلاعات در مرورگر کاربر
Web Storage و یا Client-side storage در دو حالت کلی session storage و local storage قابل دسترسی است:
الف) session storage
در این حالت اطلاعات ذخیره شدهی در session storage، پس از بسته شدن مرورگر، به صورت خودکار حذف خواهند شد.
ب) local storage
اطلاعات ذخیره شدهی در local storage پس از بسته شدن مرورگر نیز باقی مانده و قابل دسترسی و بازیابی مجدد هستند. تاریخ انقضای آنها صرفا بر اساس خالی شدن دستی کش مرورگر توسط کاربر و یا حذف دستی اطلاعات آن توسط کدهای برنامه تعیین میشود.
هر دو حالت فوق به صورت ایزوله ارائه میشوند؛ با محدودیت حجم 10 مگابایت (جمع حجم نهایی هر دو حالت با هم، محدود به 10 مگابایت است). به این معنا که برنامههای هر دومین، تنها به محل ذخیره سازی خاص همان دومین دسترسی خواهند داشت.
همچنین API دسترسی به آنها synchronous است و کار کردن با آنها سادهاست.
البته Client-side storage به دو مورد فوق خلاصه نمیشود و شامل File Storage ،WebSQL ،IndexedDB و کوکیهای مرورگر نیز هست.
- File Storage هنوز مراحل آزمایشی خودش را طی میکند و مناسب برنامههای دنیای واقعی نیست.
- WebSQL قرار بود بر اساس بانک اطلاعاتی معروف SQLite ارائه شود؛ اما W3C در سال 2010 این استاندارد را منسوخ شده اعلام کرد و با IndexedDB جایگزین شد. دسترسی به آن async است و میتواند موضوع بحثی مجزا باشد.
- کوکیهای مرورگرها نیز یکی دیگر از روشهای ذخیره سازی اطلاعات در مرورگرها هستند و تنها به ذخیره سازی حداکثر 4096 بایت اطلاعات محدود هستند. کوکیها نیز همانند local storage پس از بسته شدن مرورگر باقی میمانند؛ اما برخلاف آن، دارای تاریخ انقضاء و همچنین قابلیت ارسال بین دومینها را نیز دارا میباشند. اگر تاریخ انقضای یک کوکی تعیین نشود، همانند session storage، در پایان کار مرورگر و بسته شدن آن، حذف خواهد شد.
تهیه یک سرویس Angular برای کار با Web Storage
جهت کپسوله سازی نحوهی کار با session storage و local storage میتوان سرویسی را برای اینکار تهیه کرد:
import { Injectable } from "@angular/core"; @Injectable() export class BrowserStorageService { getSession(key: string): any { const data = window.sessionStorage.getItem(key); return JSON.parse(data); } setSession(key: string, value: any): void { const data = value === undefined ? null : JSON.stringify(value); window.sessionStorage.setItem(key, data); } removeSession(key: string): void { window.sessionStorage.removeItem(key); } removeAllSessions(): void { for (const key in window.sessionStorage) { if (window.sessionStorage.hasOwnProperty(key)) { this.removeSession(key); } } } getLocal(key: string): any { const data = window.localStorage.getItem(key); return JSON.parse(data); } setLocal(key: string, value: any): void { const data = value === undefined ? null : JSON.stringify(value); window.localStorage.setItem(key, data); } removeLocal(key: string): void { window.localStorage.removeItem(key); } removeAllLocals(): void { for (const key in window.localStorage) { if (window.localStorage.hasOwnProperty(key)) { this.removeLocal(key); } } } }
در حالت setItem اطلاعاتی را که مرورگرها ذخیره میکنند باید رشتهای باشد. به همین جهت توسط متد JSON.stringify میتوان یک شیء را تبدیل به رشته کرد و ذخیره نمود و در حالت getItem توسط متد JSON.parse، میتوان این رشته را مجددا به همان شیء پیشین خود تبدیل کرد و بازگشت داد.
محل صحیح تعریف BrowserStorageService
همانطور که در مطلب «سازماندهی برنامههای Angular توسط ماژولها» بررسی شد، محل صحیح تعریف این سرویس سراسری مشترک در بین کامپوننتها و ماژولهای برنامه، در CoreModule و پوشهی src\app\core\browser-storage.service.ts است:
import { BrowserStorageService } from "./browser-storage.service"; import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterModule } from "@angular/router"; @NgModule({ imports: [CommonModule, RouterModule], exports: [], // components that are used in app.component.ts will be listed here. declarations: [], // components that are used in app.component.ts will be listed here. providers: [BrowserStorageService] // singleton services of the whole app will be listed here. }) export class CoreModule { };
و CoreModule نیز به AppModule اضافه میشود:
import { CoreModule } from "./core/core.module"; @NgModule({ imports: [ //... CoreModule, //... RouterModule.forRoot(appRoutes) ], //... }) export class AppModule { }
بنابراین یکی دیگر از روشهای به اشتراک گذاری اطلاعات در بین قسمتهای مختلف برنامه، ذخیره سازی آنها در session/local storage و سپس بازیابی آنها بر اساس کلیدهای مشخص آنها است.
مثالی از نحوهی کاربرد BrowserStorageService
برای آزمایش سرویس تهیه شده، از کامپوننت و قالب ذیل استفاده خواهیم کرد. در اینجا سرویس BrowserStorageService به سازندهی کلاس تزریق شدهاست و سپس دو حالت session storage و local storage مورد بررسی قرار گرفتهاند:
import { BrowserStorageService } from "./../../core/browser-storage.service"; import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-browser-storage-sample-test", templateUrl: "./browser-storage-sample-test.component.html", styleUrls: ["./browser-storage-sample-test.component.css"] }) export class BrowserStorageSampleTestComponent implements OnInit { fromSessionStorage = ""; fromLocalStorage = "" sessionStorageKey = "sessionStorageKey1"; localStorageKey = "localStorageKey1" constructor(private browserStorage: BrowserStorageService) { } ngOnInit() { } sessionStorageSetItem() { this.browserStorage.setSession(this.sessionStorageKey, "Val1"); } sessionStorageGetItem() { this.fromSessionStorage = this.browserStorage.getSession(this.sessionStorageKey); } localStorageSetItem() { this.browserStorage.setLocal(this.localStorageKey, { key1: "val1", key2: 2 }); } localStorageGetItem() { this.fromLocalStorage = JSON.stringify(this.browserStorage.getLocal(this.localStorageKey)); } }
<h1>Browser storage sample</h1> <div class="panel"> <button class="btn btn-primary" (click)="sessionStorageSetItem()" type="button">sessionStorage -> Set Item</button> <button class="btn btn-success" (click)="sessionStorageGetItem()" type="button">sessionStorage -> Get Item</button> <div class="alert alert-info" *ngIf="fromSessionStorage"> {{fromSessionStorage}} </div> </div> <div class="panel"> <button class="btn btn-warning" (click)="localStorageSetItem()" type="button">localStorage -> Set Item</button> <button class="btn btn-success" (click)="localStorageGetItem()" type="button">localStorage -> Get Item</button> <div class="alert alert-info" *ngIf="fromLocalStorage"> {{fromLocalStorage}} </div> </div>
در این حالت اگر برنامه را اجرا کنیم، یک چنین خروجی قابل مشاهده خواهد بود:
و اگر به برگهی Application کنسول ابزارهای توسعه دهندههای مرورگرها نیز مراجعه کنیم، این مقادیر ثبت شده را در دو حالت استفادهی از session storage و local storage، میتوان مشاهده کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
تصمیم گرفتم در طی چندین پست در حد توانم به آموزش jQuery بپردازم. (مطالب نوشته شده برداشت ازادی از کتاب jQuery in action است)
جی کوئری (jQuery) چیست؟
نکته: برای استفاده از جی کوئری باید HTML و CSS و جاوا اسکریپت آشنایی داشته باشید.
چگونه از جی کوئری استفاده کنیم؟
برای استفاده از جی کوئری باید ابتدا فایل آن را از سایت آن دانلود کرده و در پروژه خود استفاده نمایید. البته روشهای دیگری برای استفاده از این فایل وجود دارد که در آینده بیشتر با آن آشنا خواهیم شد. برای استفاده از این فایل در پروژه باید به شکل زیر آن را به صفحه HTML خود معرفی کنیم.
<html> <head> <script type="text/javascript" src="jquery-1.9.1.min.js"></script> </head> <body> </body> </html>
کوتاه کردن کد: هر زمان شما خواسته باشید کارکرد یک صفحه وب را پویاتر کنید، در اکثر مواقع به ناچار این کار از طریق عناصری بروی صفحه انجام داده اید که با توجه به انتخاب شدن آنها، صفحه کارکردی خاص خواهد داشت. مثلا در جاوا اسکریپت اگر بخواهیم عنصری را که در یک radioGroup انتخاب شده است را برگردانیم باید کدهای زیر را بنویسیم:
var checkedValue; var elements = document.getElementByTagName ('input'); for (var n = 0; n < elements.length; n++) { if (elements[n].type == 'radio' && elements[n].name == 'myRadioGroup' && elements[n].checked) { checkedValue = elements[n].value; } }
var checkedValue = $ ('[name="myRadioGroup"]:checked').val();
قدرت اصلی جی کوئری برگفته از انتخابکنندهها (Selector) هاست، انتخابکننده ، یک عبارت است که دسترسی به عنصری خاص بر روی صفحه را موجب میشود؛ انتخابکننده این امکان را فراهم میسازد تا به سادگی عنصر مورد نظر را مشخص و به آن دسترسی پیدا کنیم که در مثال فوق، عنصر مورد نظر ما گزینه انتخاب شده از myRadioGroup بود.
Unobtrusive JavaScript: اگر پیش از پیدایش CSS در کار ایجاد صفحههای اینترنتی بودهاید حتما مشکلات و مشقات آن دوران را به خاطر میآورید. در آن زمان برای فرمتدهی به اجزای مختلف صفحه ، به ناچار علائم فرمتدهی را به همراه دستورات خود اجزا، در صفحههای HTML استفاده میکردیم. اکنون بسیار بعید به نظر میرسد کسی ترجیح دهد فرمتدهی اجزا را به همراه دستورهای HTML آن انجام دهد. اگر چه هنوز دستوری مانند زیر بسیار عادی به نظر میآید:
<button type="button" onclick="document.getElementById('xyz').style.color='red';"> Click Me </button>
مجموعه عناصر در جی کوئری:
زمانی که CSS به عنوان یک تکنولوژی به منظور جداسازی طراحی از ساختار به دنیای صفحههای اینترنتی معرفی شد، میبایست راهی برای اشاره به اجزای صفحات از طرف فایل CSS نیز معرفی میشد. این امر از طریق انتخابکنندهها (Selector) صورت پذیرفت.
برای مثال انتخابکننده زیر، به تمام عناصر <a> اشاره دارد که در یک عنصر <p> قرار گرفتهاند:
p a
برای انتخاب مجموعهای از عناصر از یکی از دو Syntax زیر استفاده میکنیم.
$(Selector) یا jQuery(Selector)
مثال زیر نمونهای دیگر است که در آن مجموعهای از تمام لینکهایی که درون تگ <p> قرار دارند را انتخاب میکند:
$("p a")
در اصطلاح برنامه نویسی به چنین توابعی که گروهی از عناصر را جمع میکنند، Wrapper میگویند زیرا تمام عناصر مطلوب را تحت یک شی بستهبندی میکند. در جیکوئری به آنها Wrapped Set یا jQuery Wrapper میگویند و به متدهایی که قابل اعمال بروی اینها به نام jQuery Wrapper Methodes شناخته میشوند.
در مثال زیر میخواهیم تمام عناصر <div> در صورتی که دارای کلاس notLongForThisWorldباشند را مخفی (با فید شدن) کنیم.
$("div.notLongForThisWorld").fadeOut();
فرض کنید در مثال بالا بخواهیم پس از مخفی کردن هر <div> بخواهیم یک کلاس به نام removedبه آن بیافزاییم. به این منظور میتوان کدی مانند زیر نوشت:
$("div.notLongForThisWorld").fadeOut().addClass("removed");
چند نمونه انتخاب کننده:
نتیجه | انتخاب کننده | |
تمام <p>های زوج را انتخاب میکند | $('p:even') | |
سطر اول هر جدول را انتخاب میکند | $("tr:nth-child(1)"); | |
<div>هایی که مستقیما در <body> تعریف شده باشند را انتخاب میکند. | $("body > div"); | |
لینک هایی که به یک فایل pdf اشاره دارند را انتخاب میکند. | $("a[href$=pdf]"); | |
تمام <div> هایی که مستقیما در <body> معرفی شده اند و دارای لینک میباشند را انتخاب میکند. | $("body > div:has(a)") | |
ادامه مطالب در پستهای بعدی تشریح خواهد شد.
جهت مطالعه بیشتر میتوانید از این منابع ^ و ^ و ^ و ^ و ^ استفاده کنید.
موفق و موید باشید
sudo apt-get update -y sudo apt-get upgrade -y
sudo java -version
sudo add-apt-repository -y ppa:webupd8team/java
gpg: keyring `/tmp/tmpkjrm4mnm/secring.gpg' created gpg: keyring `/tmp/tmpkjrm4mnm/pubring.gpg' created gpg: requesting key EEA14886 from hkp server keyserver.ubuntu.com gpg: /tmp/tmpkjrm4mnm/trustdb.gpg: trustdb created gpg: key EEA14886: public key "Launchpad VLC" imported gpg: no ultimately trusted keys found gpg: Total number processed: 1 gpg: imported: 1 (RSA: 1) OK
sudo apt-get update
sudo apt-get install oracle-java8-installer -y
sudo java -version
java version "1.8.0_66" Java(TM) SE Runtime Environment (build 1.8.0_66-b17) Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)
پس از دریافت Apache Kafka، از طریق یک terminal جدید وارد پوشه Downloads شوید. سپس فایل مورد نظر را از حالت فشرده خارج کنید و وارد پوشه اصلی آن شوید. برای این کار از دستورات زیر استفاده کنید:
tar -xzf kafka_2.11-1.0.0.tgz cd kafka_2.11-1.0.0
سپس با استفاده از دستور "ls " لیست تمامی فایلها و فولدرهای موجود در فولدر kafka_2.11-1.0.0 و را نمایش دهید:
در لیست نمایش داده شده دو فولد اصلی bin و config وجود دارند که به ترتیب فایلهای اجرایی و کانفیگهای پیشفرض و مورد نیاز، در آنها قرار دارند.
اجرای Apache ZooKeeper:
همانطور که در بخش قبل توضیح داده شد، Apache Kafka هیچ Stateی را در خود ذخیره و مدیریت نمیکند و اصطلاحا Stateless میباشد و مدیریت تمامی این Stateها را بر عهده Apache ZooKeeper قرار میدهد. بنابراین قبل از اینکه بخواهیم Kafka را اجرا کنیم، ابتدا باید Apache ZooKeeper را اجرا کنیم. برای اجرای Apache ZooKeeper در terminalی که قبلا باز کردهاید، دستور زیر را اجرا کنید:
bin/zookeeper-server-start.sh config/zookeeper.properties
با اجرای فایل zookeeper-server-start.sh و ارسال کانفیگ پیشفرضش در فولدر config با نام zookeeper.properties، قسمت مدیریت کننده Stateهای Kafka اجرا میشود.
اجرای Apache Kafka:
پس از اجرای Apache ZooKeeper، باید یک terminal جدید را باز کنید و با استفاده از دستور زیر kafka-server را با استفاده از کانفیگ پیشفرضش اجرا کنید:
bin/kafka-server-start.sh config/server.properties
به همین راحتی Kafka Server اجرا شده و آمادهی استفاده است. برای ادامه و نمایش دادن سایر قابلیتهای Kafka باید یک Topic جدید را ایجاد کنیم.
ایجاد Topic:
همانطور که میدانید تمامی پیامهای شما در Partitionهای Topic ذخیره میشوند؛ پس قبل از اینکه بخواهیم توسط یک Producer پیامی را ارسال کنیم یا اینکه بخواهیم توسط Consumer پیامی را دریافت کنیم، ابتدا باید Topic مربوطه را ایجاد کنیم. بهترین و عمومیترین مثال برای نمایش قابلیتهای Publish-Subscribe در Kafka، مثال چت بین کاربران است که در آن یک کاربر به عنوان Producer عمل میکند و پیامهایی را برای سایر کاربران که نقش Consumer را دارند ارسال میکند. kafka-topics
.sh
در Kafka ابزاریست برای مدیریت Topic ها که با استفاده از آن میتوانید Topicهای مورد نیاز را تعریف، ویرایش و یا حذف کنید.
یک terminal جدید را آغاز و با استفاده از دستور زیر یک Topic را با نام userChat ایجاد کنید:
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic userChat
با این دستور یک Topic در Kafka با نام userChat ایجاد میشود و مشخصات آن (منظور stateهای مرتبط با آن است)، در zookeeper نیز ثبت میشود.
توضیحات Optionها و آرگومانهایی که در دستور ایجاد Topic استفاده شدند به شرح زیر است:
create-- :
مشخص کننده این است که شما میخواهید یک Topic جدید را ایجاد کنید.
zookeeper localhost:2181--:
مشخص کننده آدرس سرور zookeeper است (سرور zookeeper بصورت پیشفرض از پورت 2181 استفاده میکند).
replication-factor 1--:
مشخص کننده این است که تنها یک کپی از این Topic در سرور ایجاد شود. البته در این مثال به دلیل اینکه تنها یک سرور از Kafka را اجرا کردهایم در صورتیکه این مقدار را بیش از عدد 1 در نظر بگیریم، با خطا مواجه میشویم.
partitions 1--
تعداد Partitionهای این Topic را مشخص میکند. مقدار Partition در حالتیکه حتی یک سرور را نیز اجرا کردهایم، میتواند بیش از 1 باشد؛ اما در این حالت Primary Broker برای تمام Partitionهای همین سرور در نظر گرفته میشود.
topic userChat--
نام Topic را مشخص میکند.
پس از اینکه Topic مورد نظر ایجاد شد، با استفاده از دستور زیر میتوانیم لیست Topicهای ایجاد شده را مشاهده کنیم:
bin/kafka-topics.sh --list --zookeeper localhost:2181
توضیحات Optionها و آرگومانهایی که در دستور نمایش لیست Topicها استفاده شدند به شرح زیر است:
list--:
شما میخواهید لیست topicها را مشاهده کنید.
zookeeper localhost:2181--:
همانند دستور ایجاد، مشخص کننده سرور zookeeper میباشد.
تا اینجا برای ارسال و دریافت پیام در بین Applicationهای مختلف همه چیز آمادهاست. البته همانطور که در بخشهای بعدی این سری مقالات میبینیم، در عمل Producerها و Consumerها زیرسیستمهایی هستند که خود ما با استفاده از هر زبان برنامه نویسی پیاده سازی میکنیم. اما در این قسمت برای نمایش و تکمیل مثالمان، از ابزارهایی که خود Kafka در اختیار ما قرار داده استفاده میکنیم.
اجرای یک Producer و ارسال پیام به Kafka:
kafka-console-producer.sh ابزاریست که با استفاده از آن میتوانید به راحتی یک Producer را ایجاد کنید. در terminalی که قبلا برای مدیریت Topicها باز کردهاید با استفاده از دستور زیر، Producer را اجرا میکنیم:
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic userChat
توضیحات Optionها و آرگومانهایی که در دستور ایجاد Producer استفاده شدند به شرح زیر است:
broker-list localhost:9092--:
نشان دهنده لیست Brokerها میباشد و در صورتیکه تعداد آنها بیش از یک باشد، میتوان آنها را با "," از هم جدا کرد (پورت پیشفرض Kafka server برای دریافت دستورات، 9092 میباشد).
topic userChat--:
Topicی از Kafka server را مشخص میکند که این Producer میخواهد پیامهای خود را برایش ارسال کند.
پس از اجرای دستور فوق، با تایپ کردن و زدن کلید Enter، پیام مورد نظر به Broker موردنظر یعنی localhost:9092 ارسال و در تنها Partition تاپیک userChat ذخیره میشود.
اجرای یک Consumer و دریافت پیامهای موجود در Topic مورد نظر:
kafka-console-consumer.sh ابزاریست که خود Kafka در اختیار ما قرار داده است و با استفاده از آن میتوانیم به راحتی یک Consumer برای userChat ،Topic ایجاد کنیم.
در یک terminal جدید با استفاده از دستور زیر یک Consumer را ایجاد کنید که userChat ،Topic را Subscribe میکند:
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic userChat --from-beginning
توضیحات Optionها و آرگومانهایی که در دستور ایجاد یک Consumer استفاده شدند به شرح زیر است:
bootstrap-server localhost:9092-- :
مشخص کننده Kafka serverی است که میخواهیم از طریق آن به تمامی اعضای Cluster دسترسی داشته باشیم. در صورتیکه بیش از یک سرور را بخواهیم وارد کنیم، باید آنها را با "," از هم جدا کنیم.
topic userChat --:
مشخص کننده Topicی است که این Consumer میخواهد روی آن Subscribe کند.
from-beginning--:
مشخص کننده این است که Consumer ایجاد شده میخواهد تمامی پیامهای موجود در userChat ، Topic را از اولین offset تا آخرین offset دریافت کند.
پس از اجرای کد فوق مشاهده میکنید که پیامهایی که قبلا Producer در تاپیک userChat ثبت کردهاست، برای این Consumer ارسال میشوند.
از اینجا به بعد هر لحظه که در terminal ارسال کننده یا Producer پیامی تایپ کنید و کلید Enter را بزنید، بلافاصله پیام مورد نظر برای Consumer ارسال میشود و در terminal آن نمایش داده میشود. حتی میتوانیم چندین Consumer را در terminal های مختلفی اجرا کنیم. در این صورت با ارسال هر پیام از طرف Producer، تمامی Consumerها آن را نمایش میدهند.
با استفاده از سادگی راه اندازی و قابلیتهای بسیار زیادی که Apache Kafka در مدیریت جریان دادهای بین سیستمهایمان به ما میدهد، میتوانیم به سادگی در سیستمهایمان قابلیتهای مقیاس پذیری افقی، تحمل در برابر خطا، در دسترس بودن، کارآیی بالا و سادگی مدیریت ارتباطات بخشهای مختلف را اضافه کنیم. در بخشهای بعدی به نحوه ایجاد یک Cluster و اینکه چگونه میتوان از این بستر در Net. استفاده کرد، میپردازیم.
public static class ShadowProperty { public static readonly string CreatedAt = nameof(CreatedAt); } public class Student { public int Id { get; set; } public string Name { get; set; } } public class StudentDto { public int Id { get; set; } public string Name { get; set; } public DateTimeOffset Created { get; set; } } public class StudentMappingProfile : Profile { public StudentMappingProfile() { CreateMap<Student, StudentDto>() .ForMember( dest => dest.Created, opt => opt.MapFrom(src => EF.Property<DateTimeOffset>(src, ShadowProperty.CreatedAt)) ); } }
var students = context.Students.ProjectTo<StudentDto>(configuration: mapper.ConfigurationProvider).ToList();
- ایجاد صورت حساب پرداخت با استفاده از InvoiceBuilder
- درگاه مجازی
- استفاده از پروکسی
- توکن پرداخت
- تزریق وابستگی
- Logging
var result = _onlinePayment.Request(Gateways.Mellat, 123, 25000, "http://www.mywebsite.com/foo/bar/");
var result = _onlinePayment.Request(invoice => { invoice .SetTrackingNumber(123) .SetAmount(25000) .SetCallbackUrl("http://www.mywebsite.com/foo/bar/") .UseGateway(Gateways.Mellat); });
- تولید اتوماتیک کد رهگیری به صورت افزایشی
- تولید اتوماتیک کد رهگیری به صورت تصادفی
- ایجاد یک تولید کننده کد رهگیری توسط شما
- صورت حساب سفارشی برای امکانات اختصاصی درگاههای بانکی
var result = _onlinePayment.Request(invoice => { invoice .UseAutoIncrementTrackingNumber() .SetAmount(25000) .SetCallbackUrl("http://www.mywebsite.com/foo/bar/") .UseGateway(Gateways.Mellat); });
var result = _onlinePayment.Request(invoice => { invoice .UseAutoRandomTrackingNumber() .SetAmount(25000) .SetCallbackUrl("http://www.mywebsite.com/foo/bar/") .UseGateway(Gateways.Mellat); });
public class MyTrackingNumberProvider : ITrackingNumberProvider { public Task<long> ProvideAsync(CancellationToken cancellationToken = new CancellationToken()) { // تولید و برگشت کد در اینجا } }
var result = _onlinePayment.Request(invoice => { invoice .UseTrackingNumberProvider<MyTrackingNumberProvider>() // یا .UseTrackingNumberProvider(new MyTrackingNumberProvider()) // یا .UseTrackingNumberProvider(services => new MyTrackingNumberProvider()) });
services.AddParbad() .ConfigureGateways(gateways => { gateways .AddParbadVirtual() .WithOptions(options => options.GatewayPath = "/MyVirtualGateway"); });
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); // ثبت درگاه مجازی app.UseParbadVirtualGateway(); }
public void Configuration(IAppBuilder app) { var parbad = ParbadBuilder.CreateDefaultBuilder() .ConfigureGateways(gateways => { gateways .AddParbadVirtual() .WithOptions(options => options.GatewayPath = "/MyVirtualGateway"); }) .Build(); app.UseParbadVirtualGateway(parbad.Services); }
var result = _onlinePayment.Request(Gateways.ParbadVirtualGateway, 123, 25000, "http://www.mywebsite.com/foo/bar/");
services.AddParbad() .ConfigureGateways(gateways => { gateways .AddMellat() .WithOptions(options => { options.TerminalId = 123; options.UserName = "abc"; options.UserPassword = "xyz; ) .WithProxy(new Uri("Proxy Server URL"), "UserName", "Password"); });
public class MyTokenProvider : IPaymentTokenProvider { public Task<string> ProvideTokenAsync(Invoice invoice, CancellationToken cancellationToken = new CancellationToken()) { // تولید و برگرداندن توکن در اینجا } public Task<string> RetrieveTokenAsync(CancellationToken cancellationToken = new CancellationToken()) { // خواندن و برگرداندن توکن در اینجا } }
services.AddParbad() .ConfigurePaymentToken(builder => builder.AddPaymentTokenProvider<MyTokenProvider>(ServiceLifetime.Transient));
معرفی برنامهی Blazor WASM این مطلب
در این مطلب قصد داریم دقیقا قسمت جزیرهی تعاملی Blazor Server همان برنامهی مطلب قبل را توسط یک جزیرهی تعاملی Blazor WASM بازنویسی کنیم و با نکات و تفاوتهای ویژهی آن آشنا شویم. یعنی زمانیکه صفحهی SSR نمایش جزئیات یک محصول ظاهر میشود، نحوهی رندر و پردازش کامپوننت نمایش محصولات مرتبط و مشابه، اینبار یک جزیرهی تعاملی Blazor WASM باشد. بنابراین قسمت عمدهای از کدهای این دو قسمت یکی است؛ فقط نحوهی دسترسی به سرویسها و محل قرارگیری تعدادی از فایلها، متفاوت خواهد بود.
ایجاد یک پروژهی جدید Blazor WASM تعاملی در دات نت 8
بنابراین در ادامه، در ابتدای کار نیاز است یک پوشهی جدید را برای این پروژه، ایجاد کرده و بجای انتخاب interactivity از نوع Server:
dotnet new blazor --interactivity Server
dotnet new blazor --interactivity WebAssembly
الف) یک پروژهی سمت سرور (برای تامین backend و API و سرویسهای مرتبط)
ب) یک پروژهی سمت کلاینت (برای اجرای مستقیم درون مرورگر کاربر؛ بدون داشتن وابستگی مستقیمی به اجزای برنامهی سمت سرور)
این ساختار، خیلی شبیه به ساختار پروژههای نگارش قبلی Blazor از نوع Hosted Blazor WASM است که در آن، یک پروژهی ASP.NET Core هاست کنندهی پروژهی Blazor WASM وجود دارد و یکی از کارهای اصلی آن، فراهم ساختن Web API مورد استفادهی در پروژهی WASM است.
در حالتیکه نوع تعاملی بودن پروژه را Server انتخاب کنیم (مانند مثال قسمت پنجم)، فایل Program.cs آن به همراه دو تعریف مهم زیر است که امکان تعریف کامپوننتهای تعاملی سمت سرور را میسر میکنند:
// ... builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); // ... app.MapRazorComponents<App>() .AddInteractiveServerRenderMode();
این تعاریف در فایل Program.cs (پروژهی سمت سرور) قالب جدید Blazor WASM به صورت زیر تغییر میکنند تا امکان تعریف کامپوننتهای تعاملی سمت کلاینت از نوع وباسمبلی، میسر شود:
// ... builder.Services.AddRazorComponents() .AddInteractiveWebAssemblyComponents(); // ... app.MapRazorComponents<App>() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(Counter).Assembly);
نیاز به تغییر معماری برنامه جهت کار با جزایر Blazor WASM
همانطور که در قسمت پنجم مشاهده کردیم، تبدیل کردن یک کامپوننت Blazor، به کامپوننتی تعاملی برای اجرای در سمت سرور، بسیار سادهاست؛ فقط کافی است rendermode@ آنرا به InteractiveServer تغییر دهیم تا ... کار کند. اما تبدیل همان کامپوننت نمایش محصولات مرتبط، به یک جزیرهی وباسمبلی، نیاز به تغییرات قابل ملاحظهای را دارد؛ از این لحاظ که اینبار این قسمت قرار است بر روی مرورگر کاربر اجرا شود و نه بر روی سرور. در این حالت دیگر کامپوننت ما دسترسی مستقیمی را به سرویسهای سمت سرور ندارد و برای رسیدن به این مقصود باید از یک Web API در سمت سرور کمک بگیرد و برای کار کردن با آن API در سمت کلاینت، از سرویس HttpClient استفاده کند. به همین جهت، پیاده سازی معماری این روش، نیاز به کار بیشتری را دارد:
همانطور که ملاحظه میکنید، برای فعالسازی یک جزیرهی تعاملی وباسمبلی، نمیتوان کامپوننت RelatedProducts آنرا مستقیما در پروژهی سمت سرور قرار داد و باید آنرا به پروژهی سمت کلاینت منتقل کرد. در ادامه پیاده سازی کامل این پروژه را با توجه به این تغییرات بررسی میکنیم.
مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول
از این جهت که مدل برنامه (که در قسمت پنجم معرفی شد) در دو پروژهی Client و سرور قابل استفادهاست، به همین جهت مرسوم است یک پروژهی سوم Shared را نیز به جمع دو پروژهی جاری solution اضافه کرد و فایل این مدل را در آن قرار داد. بنابراین این فایل را از پوشهی Models پروژهی سرور به پوشهی Models پروژهی جدید BlazorDemoApp.Shared در مسیر جدید BlazorDemoApp.Shared\Models\Product.cs منتقل میکنیم. مابقی کدهای آن با قسمت پنجم تفاوتی ندارد.
سپس به فایل csproj. پروژهی کلاینت مراجعه کرده و ارجاعی را به پروژهی جدید BlazorDemoApp.Shared اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\BlazorDemoApp.Shared\BlazorDemoApp.Shared.csproj" /> </ItemGroup> </Project>
سرویس برنامه: سرویسی برای بازگشت لیست محصولات
چون Blazor Server و صفحات SSR آن هر دو بر روی سرور اجرا میشوند، از لحاظ دسترسی به اطلاعات و کار با سرویسها، هماهنگی کاملی وجود داشته و میتوان کدهای یکسان و یکدستی را در اینجا بکار گرفت. یعنی هنوز هم همان مسیر قبلی سرویس Services\ProductStore.cs در این پروژهی سمت سرور نیز برقرار است و نیازی به تغییر مسیر آن نیست. البته بدیهی است چون این پروژه جدید است، باید این سرویس را در فایل Program.cs برنامهی سمت سرور به صورت زیر معرفی کرد تا در فایل razor برنامهی آن قابل دسترسی شود:
builder.Services.AddScoped<IProductStore, ProductStore>();
تکمیل فایل Imports.razor_ پروژهی سمت سرور
جهت سهولت کار با برنامه، یک سری مسیر و using را نیاز است به فایل Imports.razor_ پروژهی سمت سرور اضافه کرد:
@using static Microsoft.AspNetCore.Components.Web.RenderMode // ... @using BlazorDemoApp.Client.Components.Store @using BlazorDemoApp.Client.Components
تکمیل صفحهی نمایش لیست محصولات
کدها و مسیر کامپوننت ProductsList.razor، با قسمت پنجم دقیقا یکی است. این صفحه، یک صفحهی SSR بوده و در همان سمت سرور اجرا میشود و دسترسی آن به سرویسهای سمت سرور نیز ساده بوده و همانند قبل است.
تکمیل صفحهی نمایش جزئیات یک محصول
کدها و مسیر کامپوننت ProductDetails.razor، با قسمت پنجم دقیقا یکی است. این صفحه، یک صفحهی SSR بوده و در همان سمت سرور اجرا میشود و دسترسی آن به سرویسهای سمت سرور نیز ساده بوده و همانند قبل است ... البته بجز یک تغییر کوچک:
<RelatedProducts ProductId="ProductId" @rendermode="@InteractiveWebAssembly"/>
تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط
پس از این توضیحات، به اصل موضوع این قسمت رسیدیم! کامپوننت سمت سرور RelatedProducts.razor قسمت پنجم ، از آنجا cut شده و به مسیر جدید BlazorDemoApp.Client\Components\Store\RelatedProducts.razor منتقل میشود. یعنی کاملا به پروژهی وباسمبلی منتقل میشود. بنابراین کدهای آن دیگر دسترسی مستقیمی به سرویس دریافت اطلاعات محصولات ندارند و برای اینکار نیاز است در سمت سرور، یک Web API Controller را تدارک ببینیم:
using BlazorDemoApp.Services; using Microsoft.AspNetCore.Mvc; namespace BlazorDemoApp.Controllers; [ApiController] [Route("/api/[controller]")] public class ProductsController : ControllerBase { private readonly IProductStore _store; public ProductsController(IProductStore store) => _store = store; [HttpGet("[action]")] public IActionResult Related([FromQuery] int productId) => Ok(_store.GetRelatedProducts(productId)); }
برای اینکه مسیریابی این کنترلر کار کند، باید به فایل Program.cs برنامه، مراجعه و سطرهای زیر را اضافه کرد:
builder.Services.AddControllers(); // ... app.MapControllers();
یک نکته: همانطور که مشاهده میکنید، در Blazor 8x، امکان استفاده از دو نوع مسیریابی یکپارچه، در یک پروژه وجود دارد؛ یعنی Blazor routing و ASP.NET Core endpoint routing. بنابراین در این پروژهی سمت سرور، هم میتوان صفحات SSR و یا Blazor Server ای داشت که مسیریابی آنها با page@ مشخص میشوند و همزمان کنترلرهای Web API ای را داشت که بر اساس سیستم مسیریابی ASP.NET Core کار میکنند.
بر این اساس در پروژهی سمت کلاینت، کامپوننت RelatedProducts.razor باید با استفاده از سرویس HttpClient، اطلاعات درخواستی را از Web API فوق دریافت و همانند قبل نمایش دهد که تغییرات آن به صورت زیر است:
@using BlazorDemoApp.Shared.Models @inject HttpClient Http <button class="btn btn-outline-secondary" @onclick="LoadRelatedProducts">Related products</button> @if (_loadRelatedProducts) { @if (_relatedProducts == null) { <p>Loading...</p> } else { <div class="mt-3"> @foreach (var item in _relatedProducts) { <a href="/ProductDetails/@item.Id"> <div class="col-sm"> <h5 class="mt-0">@item.Title (@item.Price.ToString("C"))</h5> </div> </a> } </div> } } @code{ private IList<Product>? _relatedProducts; private bool _loadRelatedProducts; [Parameter] public int ProductId { get; set; } private async Task LoadRelatedProducts() { _loadRelatedProducts = true; var uri = $"/api/products/related?productId={ProductId}"; _relatedProducts = await Http.GetFromJsonAsync<IList<Product>>(uri); } }
در ادامه یکبار برنامه را اجرا میکنیم و ... بلافاصله پس از انتخاب صفحهی نمایش جزئیات یک محصول، با خطای زیر مواجه خواهیم شد!
System.InvalidOperationException: Cannot provide a value for property 'Http' on type 'RelatedProducts'. There is no registered service of type 'System.Net.Http.HttpClient'.
اهمیت درنظر داشتن pre-rendering در حالت جزیرههای وباسمبلی
استثنائی را که مشاهده میکنید، به علت pre-rendering سمت سرور این کامپوننت، رخدادهاست.
زمانیکه کامپوننتی را به این نحو رندر میکنیم:
<RelatedProducts ProductId="ProductId" @rendermode="@InteractiveWebAssembly"/>
الف) یکبار در سمت سرور تا HTML حداقل قالب آن، به همراه سایر قسمتهای صفحهی SSR جاری به سمت مرورگر کاربر ارسال شود.
ب) یکبار هم در سمت کلاینت، زمانیکه Blazor WASM بارگذاری شده و فعال میشود.
استثنائی را که مشاهده میکنیم، مربوط به حالت الف است. یعنی زمانیکه برنامهی ASP.NET Core هاست برنامه، سعی میکند کامپوننت RelatedProducts را در سمت سرور رندر کند، اما ... ما سرویس HttpClient را در آن ثبت و فعالسازی نکردهایم. به همین جهت است که عنوان میکند این سرویس را پیدا نکردهاست. برای رفع این مشکل، چندین راهحل وجود دارند که در ادامه آنها را بررسی میکنیم.
راهحل اول: ثبت سرویس HttpClient در سمت سرور
یک راهحل مواجه شدن با مشکل فوق، ثبت سرویس HttpClient در فایل Program.cs برنامهی سمت سرور به صورت زیر است:
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("http://localhost/") });
راهحل دوم: استفاده از polymorphism یا چندریختی
برای اینکار اینترفیسی را طراحی میکنیم که قرارداد نحوهی تامین اطلاعات مورد نیاز کامپوننت RelatedProducts را ارائه میکند. سپس یک پیاده سازی سمت سرور را از آن خواهیم داشت که مستقیما به بانک اطلاعاتی رجوع میکند و همچنین یک پیاده سازی سمت کلاینت را که از HttpClient جهت کار با Web API استفاده خواهد کرد.
از آنجائیکه این قرارداد نیاز است توسط هر دو پروژهی سمت سرور و سمت کلاینت استفاده شود، باید آنرا در پروژهی Shared قرار داد تا بتوان ارجاعاتی از آنرا به هر دو پروژه اضافه کرد؛ برای مثال در فایل BlazorDemoApp.Shared\Data\IProductStore.cs به صورت زیر:
using BlazorDemoApp.Shared.Models; namespace BlazorDemoApp.Shared.Data; public interface IProductStore { IList<Product> GetAllProducts(); Product GetProduct(int id); Task<IList<Product>?> GetRelatedProducts(int productId); }
پیاده سازی سمت سرور این اینترفیس، کاملا مهیا است و فقط نیاز به تغییر زیر را دارد تا با خروجی Task دار هماهنگ شود:
public Task<IList<Product>?> GetRelatedProducts(int productId) { var product = ProductsDataSource.Single(x => x.Id == productId); return Task.FromResult<IList<Product>?>(ProductsDataSource.Where(p => product.Related.Contains(p.Id)) .ToList()); }
[HttpGet("[action]")] public async Task<IActionResult> Related([FromQuery] int productId) => Ok(await _store.GetRelatedProducts(productId));
در ادامه نیاز است یک پیاده سازی سمت کلاینت را نیز از آن تهیه کنیم که در فایل BlazorDemoApp.Client\Data\ClientProductStore.cs درج خواهد شد:
public class ClientProductStore : IProductStore { private readonly HttpClient _httpClient; public ClientProductStore(HttpClient httpClient) => _httpClient = httpClient; public IList<Product> GetAllProducts() => throw new NotImplementedException(); public Product GetProduct(int id) => throw new NotImplementedException(); public Task<IList<Product>?> GetRelatedProducts(int productId) => _httpClient.GetFromJsonAsync<IList<Product>>($"/api/products/related?productId={productId}"); }
builder.Services.AddScoped<IProductStore, ClientProductStore>(); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
الف) معرفی سرویس IProductStore بجای HttpClient قبلی
@inject IProductStore ProductStore
private async Task LoadRelatedProducts() { _loadRelatedProducts = true; _relatedProducts = await ProductStore.GetRelatedProducts(ProductId); }
اکنون اگر برنامه را اجرا کنیم، پس از مشاهدهی جزئیات یک محصول، بارگذاری کامپوننت Blazor WASM آن در developer tools مرورگر کاملا مشخص است:
راهحل سوم: استفاده از سرویس PersistentComponentState
با استفاده از سرویس PersistentComponentState میتوان اطلاعات دریافتی از بانکاطلاعاتی را در حین pre-rendering در سمت سرور، به جزایر تعاملی انتقال داد و این روشی است که مایکروسافت برای پیاده سازی مباحث اعتبارسنجی و احراز هویت در Blazor 8x در پیشگرفتهاست. این راهحل را در قسمت بعد بررسی میکنیم.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: Blazor8x-WebAssembly-Normal.zip
ابزارهای پیش نیاز:
در اولین قدم، برنامهی متن باز Visual Studio Code را از اینجا دانلود و نصب کنید. برنامهی Visual Studio Code که در ادامهی فعالیتهای جدید متن باز مایکروسافت به بازار عرضه شده است، سریع، سبک و کاملا قابل توسعه و سفارشی سازی است و از اکثر زبانهای معروف پشتیبانی میکند.
در قدم بعدی، شما باید NET Core. را از اینجا (64 بیتی) دانلود و نصب کنید.
.NET Core چیست؟
NET Core. در واقع پیاده سازی بخشی از NET. اصلی است که به صورت متن باز در حال توسعه میباشد و بر روی لینوکس و مکینتاش هم قابل اجراست. موتور اجرای دات نت کامل CLR نام دارد و NET Core. نیز دارای موتور اجرایی CoreCLR است و شامل فریمورک CoreFX میباشد.
در حال حاضر شما میتوانید با استفاده از NET Core. برنامههای کنسولی و تحت وب با ASP.NET 5 بنویسید و احتمالا در آینده میتوان امیدوار بود که از ساختارهای پیچیدهتری مثل WPF نیز پشتیبانی کند.
پس از آنکه NET Core. را دانلود و نصب کردید، جهت شروع پروژه، یک پوشه را در یکی از درایوها ساخته (در این مثال E:\Projects\EF7-SQLite-NETCore) و Command prompt را در آنجا باز کنید. سپس دستورات زیر را به ترتیب اجرا کنید:
dotnet restore
dotnet run
- NuGet.Config (این فایل، تنظیمات مربوط به نیوگت را جهت کشف و دریافت وابستگیهای پروژه، شامل میشود)
- Program.cs (این فایل سی شارپ حاوی کد برنامه است)
- project.json (این فایل حاوی اطلاعات پلتفرم هدف و لیست وابستگیهای پروژه است)
دستور dotnet restore بر اساس لیست وابستگیها و پلتفرم هدف، وابستگیهای لازم را از مخزن نیوگت دریافت میکند. (در صورتی که در هنگام اجرای این دستور با خطای NullReferenceException مواجه شدید از دستور dnu restore استفاده کنید. این خطا در گیت هاب در حال بررسی است)
دستور dotnet run هم سورس برنامه را کامپایل و اجرا میکند. در صورتی که پیام Hello World را مشاهده کردید، یعنی برنامهی شما تحت NET Core. با موفقیت اجرا شده است.
توسعهی پروژه با Visual Studio Code
در ادامه، قصد داریم پروژهی HelloWorld را تحت Visual Studio Code باز کرده و تغییرات بعدی را در آنجا اعمال کنیم. پس از باز کردن Visual Studio Code از منوی File گزینهی Open Folder را انتخاب کنید و پوشهی حاوی پروژه (EF7-SQLite-NETCore) را انتخاب کنید. اکنون پروژهی شما تحت VS Code باز شده و قابل ویرایش است.
سپس از لیست فایلهای پروژه، فایل project.json را باز کرده و در بخش "dependencies" یک ردیف را برای EntityFramework.SQLite به صورت زیر اضافه کنید. به محض افزودن این خط در project.json و ذخیرهی آن، در صورتیکه قبلا این وابستگی دریافت نشده باشد، Visual Studio Code با نمایش یک هشدار در بالای برنامه به شما امکان دریافت اتوماتیک این وابستگی را میدهد. در نتیجه کافیست دکمهی Restore را زده و منتظر شوید تا وابستگی EntityFramework.SQLite از مخزن ناگت دانلود و برای پروژهی شما تنظیم شود.
"EntityFramework.SQLite": "7.0.0-rc1-final"
پس از کامل شدن این مرحله، در پروژههای بعدی تمام ارجاعات به وابستگیهای دریافت شده، از طریق مخزن موجود در سیستم خود شما، برطرف خواهد شد و نیاز به دانلود مجدد وابستگیها نیست.
اکنون همهی موارد، جهت توسعهی پروژه آماده است. ماوس خود را بر روی ریشهی پروژه در VS Code قرار داده و New Folder را انتخاب کنید و نام Models را برای آن تایپ کنید. این پوشه قرار است مدل کلاسهای پروژه را شامل شود. در اینجا ما یک مدل به نام Book داریم و نام کانتکست اصلی پروژه را هم LibraryContext گذاشتهایم.
بر روی پوشهی Models راست کلیک کرده و گزینهی New File را انتخاب کنید. سپس فایلهای Book.cs و LibraryContext.cs را ایجاد کرده و کدهای زیر را برای مدل و کانتکست، در درون این دو فایل قرار دهید.
Book.cs
namespace Models { public class Book { public int ID { get; set; } public string Title { get; set; } public string Author{get;set;} public int PublishYear { get; set; } } }
using Microsoft.Data.Entity; using Microsoft.Data.Sqlite; namespace Models { public class LibraryContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = "test.db" }; var connectionString = connectionStringBuilder.ToString(); var connection = new SqliteConnection(connectionString); optionsBuilder.UseSqlite(connection); } public DbSet<Book> Books { get; set; } } }
در قدم آخر هم کافیست که فایل Program.cs را تغییر دهید و مقادیری را در دیتابیس ذخیره و بازخوانی کنید.
using System; using Models; namespace ConsoleApplication { public class Program { public static void Main(string[] args) { Console.WriteLine("EF7 + Sqlite with the taste of .NET Core"); try { using (var context = new LibraryContext()) { context.Database.EnsureCreated(); var book1 = new Book() { Title = "Adaptive Code via C#: Agile coding with design patterns and SOLID principles ", Author = "Gary McLean Hall", PublishYear = 2014 }; var book2 = new Book() { Title = "CLR via C# (4th Edition)", Author = "Jefrey Ritcher", PublishYear = 2012 }; context.Books.Add(book1); context.Books.Add(book2); context.SaveChanges(); ReadData(context); } Console.WriteLine("Press any key to exit ..."); Console.ReadKey(); } catch (Exception ex) { Console.WriteLine($"An exception occured: {ex.Message}\n{ex.StackTrace}"); } } private static void ReadData(LibraryContext context) { Console.WriteLine("Books in database:"); foreach (var b in context.Books) { Console.WriteLine($"Book {b.ID}"); Console.WriteLine($"\tName: {b.Title}"); Console.WriteLine($"\tAuthor: {b.Author}"); Console.WriteLine($"\tPublish Year: {b.PublishYear}"); Console.WriteLine(); } } } }
جهت اجرای برنامه کافیست Command prompt را در آدرس پروژه باز کرده و دستور dotnet run را اجرا کنید. پروژهی شما کامپایل و اجرا میشود و خروجی مشابه زیر را مشاهده خواهید کرد. اگر برنامه را مجددا اجرا کنید، به جای دو کتاب اطلاعات چهار کتاب نمایش داده خواهد شد؛ چرا که در هر مرحله اطلاعات دو کتاب در دیتابیس درج میشود.
اگر به پوشهی bin که در پوشهی پروژه ایجاد شده است، نگاهی بیندازید، خبری از فایل باینری نیست. چرا که در لحظه، تولید و اجرا شده است. جهت build کردن پروژه و تولید فایل باینری کافیست دستور dotnet build را اجرا کنید، تا فایل باینری در پوشهی bin ایجاد شود.
جهت انتشار برنامه میتوانید دستور dotnet publish را اجرا کنید. این دستور نه تنها برنامه، که تمام وابستگیهای مورد نیاز آن را برای اجرای در یک پلتفرم خاص تولید میکند. برای مثال بعد از اجرای این دستور یک پوشهی win7-x64 حاوی 211 فایل در مجموع تولید شده است که تمامی وابستگیهای این پروژه را شامل میشود.
در واقع این پوشه تمام وابستگیهای مورد نیاز پروژه را همراه خود دارد و در نتیجه جهت اجرای این برنامه برخلاف برنامههای معمولی دات نت، دیگر نیازی به نصب هیچ وابستگی مجزایی نیست و حتی پروژههای نوشته شده تحت NET Core. را میتوانید در سیستمهای عاملهای دیگری مثل لینوکس و مکینتاش و یا Windows IoT بر روی سخت افزار Raspberry Pi 2 هم اجرا کنید.
جهت مطالعهی بیشتر:
- اصل Fork در اینجا
- تاریخچهی این Fork غیر رسمی در اینجا
- بستهی نیوگت آن در اینجا
چون تیم EF در نگارش فعلی این کتابخانه حاضر به افزودن این نوع جدید نشدهاست، بنابراین بجای بستهی اصلی Entity framework نیاز است بستهی EntityFrameworkWithHierarchyId را نصب کنید.
PM> install-package EntityFrameworkWithHierarchyId
یک تذکر مهم:
چون امضای دیجیتال این بسته، با امضای دیجیتال بستهی اصلی EF یکی نیست، اگر پروژهی شما صرفا از EF استفاده میکند، مشکلی نخواهید داشت. اما اگر برای مثال از ASP.NET Identity کامپایل شدهی برای کار با EF اصلی استفاده کنید، پیام یافت نشدن DLL مرتبط را دریافت خواهید کرد.
تعریفی مدلی با خاصیتی از نوع جدید HierarchyId
public class Employee { public int Id { get; set; } [Required, MaxLength(100)] public string Name { get; set; } [Required] public HierarchyId Node { get; set; } // نوع داده جدید }
تعریف Context و مقدار دهی اولیهی آن
در این حالت Context برنامه به همراه تنظیمات اولیهی Migrations آن یک چنین شکلی را پیدا خواهد کرد:
public class MyContext : DbContext { public DbSet<Employee> Employees { get; set; } public MyContext() : base("Connection1") { this.Database.Log = log => Console.WriteLine(log); } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if (context.Employees.Any()) return; context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD NodePath as Node.ToString() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD Level AS Node.GetLevel() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD ManagerNode as Node.GetAncestor(1) persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD ManagerNodePath as Node.GetAncestor(1).ToString() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD CONSTRAINT [UK_EmployeeNode] UNIQUE NONCLUSTERED (Node)"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] WITH CHECK ADD CONSTRAINT [EmployeeManagerNodeNodeFK] " + "FOREIGN KEY([ManagerNode]) REFERENCES [dbo].[Employees] ([Node])"); context.Employees.Add(new Employee { Name = "Root", Node = new HierarchyId("/") }); context.Employees.Add(new Employee { Name = "Emp1", Node = new HierarchyId("/1/") }); context.Employees.Add(new Employee { Name = "Emp2", Node = new HierarchyId("/2/") }); context.Employees.Add(new Employee { Name = "Emp3", Node = new HierarchyId("/1/1/") }); context.Employees.Add(new Employee { Name = "Emp4", Node = new HierarchyId("/1/1/1/") }); context.Employees.Add(new Employee { Name = "Emp5", Node = new HierarchyId("/2/1/") }); context.Employees.Add(new Employee { Name = "Emp6", Node = new HierarchyId("/1/2/") }); base.Seed(context); } }
همچنین چند فیلد محاسباتی نیز بر اساس امکانات توکار SQL Server اضافه شدهاند. متدهایی مانند ToString، GetLevel، GetAncestor و امثال آن جزئی از پیاده سازی توکار SQL Server هستند. همچنین این متدها توسط کتابخانهی EntityFrameworkWithHierarchyId نیز ارائه شدهاند.
کوئری نویسی
مرتب سازی رکوردها بر اساس HierarchyId آنها
using (var context = new MyContext()) { Console.WriteLine("\ngetItems OrderByDescending(employee => employee.Node)"); var employees = context.Employees.OrderByDescending(employee => employee.Node).ToList(); foreach (var employee in employees) { Console.WriteLine("{0} {1}", employee.Id, employee.Node); } }
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] ORDER BY [Extent1].[Node] DESC 6 /2/1/ 3 /2/ 7 /1/2/ 5 /1/1/1/ 4 /1/1/ 2 /1/ 1 /
یافتن یک HierarchyId خاص و سپس یافتن کلیهی فرزندان آن در یک سطح پایینتر
using (var context = new MyContext()) { Console.WriteLine("\nGetAncestor(1) of /1/"); var firstItem = context.Employees.Single(employee => employee.Node == new HierarchyId("/1/")); foreach (var item in context.Employees.Where(employee => firstItem.Node == employee.Node.GetAncestor(1))) { Console.WriteLine("{0} {1}", item.Id, item.Name); } }
با این خروجی:
SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE cast('/1/' as hierarchyid) = [Extent1].[Node] SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE (@p__linq__0 = ([Extent1].[Node].GetAncestor(1))) OR ((@p__linq__0 IS NULL) AND ([Extent1].[Node].GetAncestor(1) IS NULL)) -- p__linq__0: '/1/' (Type = Object) 4 Emp3 7 Emp6
کوئریهای فوق را میتوان بجای استفاده از متد GetAncestor، با استفاده از متد IsDescendantOf به شکل زیر نیز نوشت:
var list = context.Employees.Where( employee => employee.Node.IsDescendantOf(new HierarchyId("/1/")) && employee.Node.GetLevel() == 2).ToList();
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE (([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1) AND (2 = ([Extent1].[Node].GetLevel()))
جابجا کردن نودها توسط متد GetReparentedValue
در کوئری ذیل، تمامی فرزندان ریشهی /1/ یافت شده و سپس والد آنها به صورت پویا تغییر داده میشود:
var items = context.Employees.Where(employee => employee.Node.IsDescendantOf(new HierarchyId("/1/"))) .Select(employee => new { Id = employee.Id, OrigPath = employee.Node, ReparentedValue = employee.Node.GetReparentedValue(new HierarchyId("/1/"), HierarchyId.GetRoot()), Level = employee.Node.GetLevel() }).ToList(); foreach (var item in items) { Console.WriteLine("Id:{0}; OrigPath:{1}; ReparentedValue:{2}; Level:{3}", item.Id, item.OrigPath, item.ReparentedValue, item.Level); }
SELECT [Extent1].[Id] AS [Id], [Extent1].[Node] AS [Node], [Extent1].[Node].GetReparentedValue(cast('/1/' as hierarchyid), hierarchyid::GetRoot()) AS [C1], [Extent1].[Node].GetLevel() AS [C2] FROM [dbo].[Employees] AS [Extent1] WHERE ([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1 Id:2; OrigPath:/1/; ReparentedValue:/; Level:1 Id:4; OrigPath:/1/1/; ReparentedValue:/1/; Level:2 Id:5; OrigPath:/1/1/1/; ReparentedValue:/1/1/; Level:3 Id:7; OrigPath:/1/2/; ReparentedValue:/2/; Level:2
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
HierarcyIdTests.zip
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; } } }
البته باید دقت داشت، زمانیکه از خاصیت 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...
مدیریت تمام 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; } }
در این مثال دیگر مانند مثال قسمت قبل
public async Task<double> GetSumAsync() { var leftOperand = await GetNumberAsync(); var rightOperand = await GetNumberAsync(); return leftOperand + rightOperand; }
با کمک متد 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(); } } }
اگر برنامه را کامپایل کنید، کامپایلر بر روی سطر فراخوانی متد 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 سبب کرش برنامه میشود؛ اما نه استثنای صادر شده در متد 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 و 4.5 متفاوت است:
Task.Factory.StartNew(() => { throw new Exception(); }); Thread.Sleep(100); GC.Collect(); GC.WaitForPendingFinalizers();
<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; } }
public delegate void TappedEventHandler(object sender, TappedRoutedEventArgs e);