مطالب
نحوه تعریف Linked Server و دریافت اطلاعات از سروری دیگر

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

به عنوان مثال:

من سروری با آدرس 192.168.0.1 دارم که دارای پایگاه داده‌ای با نام Salary می باشد. نام این سرور را A می‌گذارم.

همچنین من سرور دیگری با آدرس 192.168.1.100 دارم که دارای پایگاه داده ای با نام Accounting است. نام این سرور را B می‌گذارم.

حالا می‌خواهم در سرور A یک Query بنویسم که جدول Payment را با اتصال به سرور B به جدول Document متصل نموده و نتیجه ی JOIN این دو جدول را نمایش دهد. به عنوان مثال:

SELECT * FROM Payment AS pay JOIN Document AS doc
ON pay.DocumentId = doc.Id
این Query به هیچ عنوان اجرا نخواهد شد. زیرا نمی‌تواند جدول Document را پیدا کند. برای این منظور باید سرور B را به سرور A معرفی کنیم که این کار از طریق Linked Server انجام خواهد شد.


نحوه‌ی ایجاد یک  Linked Server

بر روی سیستم من دو نسخه از SQL نصب شده است. یکی Standard Edition و دیگری Express Edition. من می‌خواهم در نسخه Standard یک Linked Server به نسخه‌ی Express ایجاد کنم. بنابراین با اتصال به نسخه Standard مراحل زیر را طی می‌کنم:

1.  یک New query ایجاد می‌کنم.

2.  دستورات زیر را در Query ایجاد شده می‌نویسم:

sp_addlinkedserver 'MyServer', '', 'SQLNCLI', '.\sqlexpress'
توضیحات:

sp_addlinkedserver نام رویه ای است که یک Linked Server را ایجاد می‌نماید.
پارامتر اول نام Linked Server را مشخص می‌نماید که جهت دسترسی به سرور دیگر مورد استفاده قرار می‌گیرد.
پارامتر دوم Product Name می‌باشد که من خالی گذاشتم.
پارامتر سوم Provider Name یا نام فراهم کننده داده‌ای است. چون من میخواهم به یک سرور SQL متصل شوم SQLNCLI (SQL Native Client) را انتخاب کردم. اگر به منبع داده‌ای دیگری مثل Access،Oracle، MySql و ... متصل می‌شوید باید Provider Name دیگری را نتخاب کنید.
پارامتر چهارم نام یا IP سروری است که می‌خواهیم به آن لینک شویم.

3.  با فشردن F5 یا منوی Execute این Query را اجرا کنید.

با اجرای موفقیت آمیز مراحل فوق باید عنوان MyServer را در مسیر Server Objects > Linked Server مشاهده کنید. در نسخه Express پایگاه داده‌ای با نام test دارم که شامل جدولی به نام tbl می باشد. با نوشتن Query زیر می‌توانم محتویات این جدول را مشاهده کنم:

SELECT * FROM MyServer.test.dbo.tbl
ممکن است جهت اتصال به سرور لینک شده نیاز به نام کاربری و رمز عبور داشته باشید. جهت تعریف نام کاربری و رمز عبور برای سرور لینک شده از دستورات زیر استفاده کنید:
sp_addlinkedsrvlogin 'MyServer',@rmtuser='user1', @rmtpassword='abc123'
توضیحات:

sp_addlinkedsrvlogin نام رویه ای است که نام کاربری و رمز عبور را به یک Linked Server اضافه می‌کند.
پارامتر اول نام Linked Server می باشد.
پارامتر دوم نام کاربری جهت اتصال به سرور لینک شده می‌باشد.
پارامتر سوم رمز عبور جهت اتصال به سرور لینک شده می‌باشد. 

مطالب
EF Code First #8

ادامه بحث بررسی جزئیات نحوه نگاشت کلاس‌ها به جداول، توسط EF Code first


استفاده از Viewهای SQL Server در EF Code first

از Viewها عموما همانند یک جدول فقط خواندنی استفاده می‌شود. بنابراین نحوه نگاشت اطلاعات یک کلاس به یک View دقیقا همانند نحوه نگاشت اطلاعات یک کلاس به یک جدول است و تمام نکاتی که تا کنون بررسی شدند، در اینجا نیز صادق است. اما ...
الف) بر اساس تنظیمات توکار EF Code first، نام مفرد کلاس‌ها، حین نگاشت به جداول، تبدیل به اسم جمع می‌شوند. بنابراین اگر View ما در سمت بانک اطلاعاتی چنین تعریفی دارد:
Create VIEW EmployeesView
AS
SELECT id,
FirstName
FROM Employees

در سمت کدهای برنامه نیاز است به این شکل تعریف شود:

using System.ComponentModel.DataAnnotations;

namespace EF_Sample04.Models
{
[Table("EmployeesView")]
public class EmployeesView
{
public int Id { set; get; }
public string FirstName { set; get; }
}
}

در اینجا به کمک ویژگی Table، نام دقیق این View را در بانک اطلاعاتی مشخص کرده‌ایم. به این ترتیب تنظیمات توکار EF بازنویسی خواهد شد و دیگر به دنبال EmployeesViews نخواهد گشت؛ یا جدول متناظر با آن‌را به صورت خودکار ایجاد نخواهد کرد.
ب) View شما نیاز است دارای یک فیلد Primary key نیز باشد.
ج) اگر از مهاجرت خودکار توسط MigrateDatabaseToLatestVersion استفاده کنیم، پیغام خطای زیر را دریافت خواهیم کرد:

There is already an object named 'EmployeesView' in the database.

علت این است که هنوز جدول dbo.__MigrationHistory از وجود آن مطلع نشده است، زیرا یک View، خارج از برنامه و در سمت بانک اطلاعاتی اضافه می‌شود.
برای حل این مشکل می‌توان همانطور که در قسمت‌های قبل نیز عنوان شد، EF را طوری تنظیم کرد تا کاری با بانک اطلاعاتی نداشته باشد:

Database.SetInitializer<Sample04Context>(null);

به این ترتیب EmployeesView در همین لحظه قابل استفاده است.
و یا به حالت امن مهاجرت دستی سوئیچ کنید:
Add-Migration Init -IgnoreChanges
Update-Database

پارامتر IgnoreChanges سبب می‌شود تا متدهای Up و Down کلاس مهاجرت تولید شده، خالی باشد. یعنی زمانیکه دستور Update-Database انجام می‌شود، نه Viewایی دراپ خواهد شد و نه جدول اضافه‌ای ایجاد می‌گردد. فقط جدول dbo.__MigrationHistory به روز می‌شود که هدف اصلی ما نیز همین است.
همچنین در این حالت کنترل کاملی بر روی کلاس‌های Up و Down وجود دارد. می‌توان CreateTable اضافی را به سادگی از این کلاس‌ها حذف کرد.

ضمن اینکه باید دقت داشت یکی از اهداف کار با ORMs، فراهم شدن امکان استفاده از بانک‌های اطلاعاتی مختلف، بدون اعمال تغییری در کدهای برنامه می‌باشد (فقط تغییر کانکشن استرینگ، به علاوه تعیین Provider جدید، باید جهت این مهاجرت کفایت کند). بنابراین اگر از View استفاده می‌کنید، این برنامه به SQL Server گره خواهد خورد و دیگر از سایر بانک‌های اطلاعاتی که از این مفهوم پشتیبانی نمی‌کنند، نمی‌توان به سادگی استفاده کرد.



استفاده از فیلدهای XML اس کیوال سرور

در حال حاضر پشتیبانی توکاری توسط EF Code first از فیلدهای ویژه XML اس کیوال سرور وجود ندارد؛ اما استفاده از آن‌ها با رعایت چند نکته ساده، به نحو زیر است:

using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;

namespace EF_Sample04.Models
{
public class MyXMLTable
{
public int Id { get; set; }

[Column(TypeName = "xml")]
public string XmlValue { get; set; }

[NotMapped]
public XElement XmlValueWrapper
{
get { return XElement.Parse(XmlValue); }
set { XmlValue = value.ToString(); }
}
}
}


در اینجا توسط TypeName ویژگی Column، نوع توکار xml مشخص شده است. این فیلد در طرف کدهای کلاس‌های برنامه، به صورت string تعریف می‌شود. سپس اگر نیاز بود به این خاصیت توسط LINQ to XML دسترسی یافت، می‌توان یک فیلد محاسباتی را همانند خاصیت XmlValueWrapper فوق تعریف کرد. نکته‌ دیگری را که باید به آن دقت داشت، استفاده از ویژگی NotMapped می‌باشد، تا EF سعی نکند خاصیتی از نوع XElement را (یک CLR Property) به بانک اطلاعاتی نگاشت کند.

و همچنین اگر علاقمند هستید که این قابلیت به صورت توکار اضافه شود، می‌توانید اینجا رای دهید!



نحوه تعریف Composite keys در EF Code first

کلاس نوع فعالیت زیر را درنظر بگیرید:

namespace EF_Sample04.Models
{
public class ActivityType
{
public int UserId { set; get; }
public int ActivityID { get; set; }
}
}

در جدول متناظر با این کلاس، نباید دو رکورد تکراری حاوی شماره کاربری و شماره فعالیت یکسانی باهم وجود داشته باشند. بنابراین بهتر است بر روی این دو فیلد، یک کلید ترکیبی تعریف کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;

namespace EF_Sample04.Mappings
{
public class ActivityTypeConfig : EntityTypeConfiguration<ActivityType>
{
public ActivityTypeConfig()
{
this.HasKey(x => new { x.ActivityID, x.UserId });
}
}
}

در اینجا نحوه معرفی بیش از یک کلید را در متد HasKey ملاحظه می‌کنید.

یک نکته:
اینبار اگر سعی کنیم مثلا از متد db.ActivityTypes.Find با یک پارامتر استفاده کنیم، پیغام خطای «The number of primary key values passed must match number of primary key values defined on the entity» را دریافت خواهیم کرد. برای رفع آن باید هر دو کلید، در این متد قید شوند:

var activity1 = db.ActivityTypes.Find(4, 1);

ترتیب آن‌ها هم بر اساس ترتیبی که در کلاس ActivityTypeConfig، ذکر شده است، مشخص می‌گردد. بنابراین در این مثال، اولین پارامتر متد Find، به ActivityID اشاره می‌کند و دومین پارامتر به UserId.


بررسی نحوه تعریف نگاشت جداول خود ارجاع دهنده (Self Referencing Entity)

سناریوهای کاربردی بسیاری را جهت جداول خود ارجاع دهنده می‌توان متصور شد و عموما تمام آن‌ها برای مدل سازی اطلاعات چند سطحی کاربرد دارند. برای مثال یک کارمند را درنظر بگیرید. مدیر این شخص هم یک کارمند است. مسئول این مدیر هم یک کارمند است و الی آخر. نمونه دیگر آن، طراحی منوهای چند سطحی هستند و یا یک مشتری را درنظر بگیرید. مشتری دیگری که توسط این مشتری معرفی شده است نیز یک مشتری است. این مشتری نیز می‌تواند یک مشتری دیگر را به شما معرفی کند و این سلسله مراتب به همین ترتیب می‌تواند ادامه پیدا کند.
در طراحی بانک‌های اطلاعاتی، برای ایجاد یک چنین جداولی، یک کلید خارجی را که به کلید اصلی همان جدول اشاره می‌کند، ایجاد خواهند کرد؛ اما در EF Code first چطور؟

using System.Collections.Generic;

namespace EF_Sample04.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }

//public int? ManagerID { get; set; }
public virtual Employee Manager { get; set; }
}
}

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

using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;

namespace EF_Sample04.Mappings
{
public class EmployeeConfig : EntityTypeConfiguration<Employee>
{
public EmployeeConfig()
{
this.HasOptional(x => x.Manager)
.WithMany()
//.HasForeignKey(x => x.ManagerID)
.WillCascadeOnDelete(false);
}
}
}

با توجه به اینکه یک کارمند می‌تواند مسئولی نداشته باشد (خودش مدیر ارشد است)، به کمک متد HasOptional مشخص کرده‌ایم که فیلد Manager_Id را که می‌خواهی به این کلاس اضافه کنی باید نال پذیر باشد. توسط متد WithMany طرف دیگر رابطه مشخص شده است.
اگر نیاز بود فیلد Manager_Id اضافه شده نام دیگری داشته باشد، یک خاصیت nullable مانند ManagerID را که در کلاس Employee مشاهده می‌کنید،‌ اضافه نمائید. سپس در طرف تعاریف نگاشت‌ها به کمک متد HasForeignKey، باید صریحا عنوان کرد که این خاصیت، همان کلید خارجی است. از این نکته در سایر حالات تعاریف نگاشت‌ها نیز می‌توان استفاده کرد، خصوصا اگر از یک بانک اطلاعاتی موجود قرار است استفاده شود و از نام‌های دیگری بجز نام‌های پیش فرض EF استفاده کرده است.




مثال‌های این سری رو از این آدرس هم می‌تونید دریافت کنید: (^)

مطالب
مسیریابی در Angular - قسمت سوم - پارامترهای مسیریابی
گاهی از اوقات نیاز است به همراه مسیریابی، اطلاعاتی را نیز به آن‌ها ارسال کنیم. برای مثال در حین نمایش لیست محصولات، برای هدایت به صفحه‌ی نمایش جزئیات هر محصول، نیاز است Id هر محصول نیز به همراه مسیریابی، به کامپوننت مقصد ارسال شود. اینکار توسط route parameters قابل مدیریت است.

تنظیم مسیریابی‌ها جهت درج پارامترها

پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابی‌ها مشخص کرد.
در ادامه‌ی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه می‌کنیم:
>ng g c product/ProductDetail
>ng g c product/ProductEdit
این دستورات سبب به روز رسانی خودکار قسمت declarations فایل src\app\product\product.module.ts نیز خواهند شد.

در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آن‌را به شکل ذیل تکمیل خواهیم کرد:
import { ProductEditComponent } from './product-edit/product-edit.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
import { ProductListComponent } from './product-list/product-list.component';

const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  { path: 'products/:id', component: ProductDetailComponent },
  { path: 'products/:id/edit', component: ProductEditComponent }
];
اولین شیء مسیریابی تعریف شده را در قسمت‌های پیشین بررسی کردیم که امکان نمایش کامپوننت لیست محصولات را توسط یک routerLink در منوی سایت میسر می‌کند.
دومین شیء مسیریابی، از مسیر ریشه‌ی یکسانی استفاده می‌کند (products) که علت آن‌را در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص می‌کند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری می‌خواهیم id محصولات را انتقال دهیم، Id نام‌گرفته‌است؛ وگرنه هر نام دیگری نیز می‌تواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشه‌ی یکسانی استفاده می‌کند و تفاوت آن‌را با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کرده‌ایم.

در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نام‌های پس از : در یک مسیریابی، باید منحصربفرد باشند.


تکمیل کامپوننت نمایش لیست محصولات

پیش از ادامه‌ی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمت‌های قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینک‌هایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه می‌کنیم:
 > ng g i product/IProduct
و آن‌را به نحو ذیل تکمیل خواهیم کرد:
export interface IProduct {
    id: number;
    productName: string;
    productCode: string;
}


تشکیل یک منبع اطلاعات درون حافظه‌ای

یکی از بسته‌های Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائه‌ی مطلب).
به همین جهت ابتدا بسته‌ی مرتبط با آن‌را نصب کنید:
 >npm install angular-in-memory-web-api --save
ذکر پارامتر save در اینجا، سبب به روز رسانی فایل package.json نیز خواهد شد:
"dependencies": {
   "angular-in-memory-web-api": "^0.3.1"
},

سپس کلاس ProductData را به ماژول محصولات اضافه می‌کنیم:
 > ng g cl product/ProductData
این کلاس را در ادامه به صورت ذیل تکمیل خواهیم کرد:
import { IProduct } from './iproduct';
import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api';

export class ProductData implements InMemoryDbService, InMemoryBackendConfig {
    createDb() {
        let products: IProduct[] = [
            {
                'id': 1,
                'productName': 'Product 1',
                'productCode': '0011'
            },
            {
                'id': 2,
                'productName': 'Product 2',
                'productCode': '0023'
            },
            {
                'id': 5,
                'productName': 'Product 5',
                'productCode': '0048'
            },
            {
                'id': 8,
                'productName': 'Product 8',
                'productCode': '0022'
            },
            {
                'id': 10,
                'productName': 'Product 10',
                'productCode': '0042'
            }
        ];
        return { products };
    }
}
همانطور که ملاحظه می‌کنید، کلاسی که قرار است  به عنوان منبع داده‌ی بسته‌ی angular-in-memory-web-api بکار رود باید InMemoryDbService, InMemoryBackendConfig را نیز پیاده سازی کند که نمونه‌ای از آن‌را در اینجا برای بازگشت یک لیست درون حافظه‌ای محصولات، مشاهده می‌کنید.

در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
import { ProductData } from './product/product-data';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),

    ProductModule,
    UserModule,

    AppRoutingModule
  ],
- ابتدا مکان یافت شدن ماژول‌های مورد نیاز ProductData و InMemoryWebApiModule تعریف شده‌اند و سپس InMemoryWebApiModule.forRoot را جهت تشکیل یک Web API آزمایشی، برای ارائه‌ی اطلاعات کلاس ProductData، به لیست imports اضافه کرده‌ایم. باید دقت داشت که نیاز است تعریف InMemoryWebApiModule پس از تعریف HttpModule باشد تا بتواند تعدادی از پیش فرض‌های آن را بازنویسی کند.
- در اینجا یک delay را هم در تنظیمات آن مشاهده می‌کنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.


تعریف سرویس HTTP کار با منبع اطلاعات درون حافظه‌ای

پس از تعریف REST Web API درون حافظه‌ای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
 >ng g s product/product -m product/product.module
که سبب افزوده شدن سرویس product.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول product.module.ts نیز می‌شود:
 installing service
  create src\app\product\product.service.spec.ts
  create src\app\product\product.service.ts
  update src\app\product\product.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمی‌شود.

پس از ایجاد قالب ابتدایی فایل product.service.ts، آن‌را به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/of';

import { IProduct } from './iproduct';

@Injectable()
export class ProductService {
  private baseUrl = 'api/products';

  constructor(private http: Http) { }

  getProducts(): Observable<IProduct[]> {
    return this.http.get(this.baseUrl)
      .map(this.extractData)
      .do(data => console.log('getProducts: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  getProduct(id: number): Observable<IProduct> {
    if (id === 0) {
      return Observable.of(this.initializeProduct());
    };
    const url = `${this.baseUrl}/${id}`;
    return this.http.get(url)
      .map(this.extractData)
      .do(data => console.log('getProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  deleteProduct(id: number): Observable<Response> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    const url = `${this.baseUrl}/${id}`;
    return this.http.delete(url, options)
      .do(data => console.log('deleteProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  saveProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    if (product.id === 0) {
      return this.createProduct(product, options);
    }
    return this.updateProduct(product, options);
  }

  private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> {
    product.id = undefined;
    return this.http.post(this.baseUrl, product, options)
      .map(this.extractData)
      .do(data => console.log('createProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> {
    const url = `${this.baseUrl}/${product.id}`;
    return this.http.put(url, product, options)
      .map(() => product)
      .do(data => console.log('updateProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  private extractData(response: Response) {
    let body = response.json();
    return body.data || {};
  }

  private handleError(error: Response): Observable<any> {
    console.error(error);
    return Observable.throw(error.json().error || 'Server error');
  }

  initializeProduct(): IProduct {
    // Return an initialized object
    return {
      id: 0,
      productName: null,
      productCode: null
    };
  }
}
این سرویس HTTP، به سرویس Web API آزمایشی واقع در آدرس  baseUrl، متصل خواهد شد:
   private baseUrl = 'api/products';
از آن برای دریافت لیست محصولات (getProducts)، دریافت جزئیات یک محصول (getProduct)، حذف یک محصول (deleteProduct)، ثبت و یا به روز رسانی یک محصول (saveProduct) استفاده خواهیم کرد.


نمایش لیست محصولات

اکنون پس از این مقدمات می‌توانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آن‌را به نحو ذیل تکمیل کنید:
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit } from '@angular/core';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  pageTitle = 'Product List';
  errorMessage: string;

  products: IProduct[];

  constructor(private productService: ProductService,
    private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.productService.getProducts()
      .subscribe(products => this.products = products,
      error => this.errorMessage = <any>error);
  }
}
در اینجا با استفاده از سرویس محصولاتی که پیشتر ایجاد کردیم، کار دریافت لیست محصولات انجام شده و سپس به خاصیت عمومی products نسبت داده می‌شود. این خاصیت را در قالب این کامپوننت نمایش خواهیم داد. به همین جهت فایل src\app\product\product-list\product-list.component.html را گشوده و آن‌را به نحو ذیل تکمیل کنید:
<div class="panel panel-default">
  <div class="panel-heading">
    {{pageTitle}}
  </div>

  <div class="panel-body">
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>

    <div class="table-responsive">
      <table class="table" *ngIf="products && products.length">
        <thead>
          <tr>
            <th>Product</th>
            <th>Code</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let product of products">
            <td><a [routerLink]="['/products', product.id]">
                  {{product.productName}}
                </a>
            </td>
            <td>{{ product.productCode}}</td>
            <td>
              <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']">
                Edit
              </a>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>
اکنون اگر برنامه را توسط دستور ng serve -o ساخته و اجرا کنید، چنین صفحه‌ای قابل مشاهده خواهد بود:


توضیحات:

پس از تعریف مسیریابی‌های صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آن‌ها به لینک‌هایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحه‌ی مشاهده‌ی جزئیات آن متصل کرده‌ایم:
<a [routerLink]="['/products', product.id]">
                  {{product.productName}}
</a>
برای مشاهده‌ی جزئیات هر محصول نیاز است Id آن محصول را به عنوان پارامتر مسیریابی ارسال کنیم. به همین جهت این Id را به عنوان پارامتری جدید، به routerLink انتساب داده‌ایم.
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کرده‌ایم.
<a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']">
     Edit
</a>
در اینجا ابتدا root URL segment ذکر می‌شود. سپس پارامترهای متغیر مسیریابی و همچنین ثوابت آن مسیر خاص نیز باید ذکر شوند. اگر URL segment ثابت edit‌، در انتها ذکر نشود، این مسیر با تنظیم 'path: 'products/:id تطابق داده خواهد شد و نه با حالت 'path: 'products/:id/edit.

به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه می‌کنیم:
<li>
      <a [routerLink]="['/products', 0, 'edit']">Add Product</a>
</li>
در اینجا عدد صفر را به عنوان پارامتر یا Id محصول جدید، به همان صفحه‌ی ویرایش اطلاعات یک محصول، ارسال کرده‌ایم. اگر به سرویس محصولات دقت کنید،
  saveProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    if (product.id === 0) {
      return this.createProduct(product, options);
    }
    return this.updateProduct(product, options);
  }
اگر id مساوی صفر باشد، یک محصول جدید ایجاد خواهد شد و اگر غیر صفر باشد، این محصول از پیش موجود، به روز رسانی می‌گردد.

همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز می‌توان بکار برد. برای مثال:
 this.router.navigate(['products', this.product.id]);


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

تا اینجا لیست محصولات را نمایش دادیم و همچنین لینک‌هایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحله‌ی بعد، پیاده سازی این کامپوننت‌ها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننت‌ها قرار می‌دهد. بنابراین برای دسترسی به آن‌ها تنها کافی است این سرویس را به سازنده‌ی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم می‌شود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id می‌توان به صورت ذیل عمل کرد:
 let id = +this.route.snapshot.params['id'];

بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف می‌شود:
 { path: 'products/:id', component: ProductDetailComponent }
سپس این مسیریابی توسط لینک ذیل فعال می‌شود:
<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
اکنون برای دریافت مقدار این پارامتر از URL جاری، می‌توان از this.route.snapshot.params['id'] استفاده کرد. این id دقیقا نام همان متغیری است که در تعریف مسیریابی ذکر شده‌است و حساس به کوچکی و بزرگی حروف می‌باشد.

ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز می‌باشد:
this.route.params.subscribe(
         params => {
            let id = +params['id'];
            this.getProduct(id);
         }
      );
هر زمان که پارامترهای مسیریابی تغییر کنند، این Observable به آن‌ها گوش فرا داده و برنامه را از این تغییرات مطلع می‌سازد.

یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشته‌ی دریافتی، به عدد می‌گردد.


تکمیل کامپوننت نمایش جزئیات یک محصول

در ادامه قالب ابتدایی مشاهده‌ی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.component.html',
  styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
  pageTitle = 'Product Detail';
  product: IProduct;
  errorMessage: string;

  constructor(private productService: ProductService,
    private route: ActivatedRoute) { }

  ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }

  getProduct(id: number) {
    this.productService.getProduct(id).subscribe(
      product => this.product = product,
      error => this.errorMessage = <any>error);
  }
}
در این حالت اگر آدرس http://localhost:4200/products/1 توسط کاربر درخواست شود، نیاز است بتوان id=1 آن‌را از مسیرجاری استخراج کرد. به همین جهت سرویس ActivatedRoute به سازنده‌ی کلاس کامپوننت جزئیات محصول تزریق شده‌است. هرچند می‌توان از این سرویس در همان سازنده‌ی کلاس نیز استفاده کرد، اما انجام اعمال async آغازین یک کامپوننت بهتر است به ngOnInit منتقل شوند تا سبب تاخیری در آغاز و نمایش کامپوننت نگردند. این life cycle hook، پس از آغاز کامپوننت فراخوانی می‌گردد. به همین جهت ذکر implements OnInit را در قسمت تعریف کلاس مشاهده می‌کنید.
در متد OnInit، مقدار id، از snapshot دریافت می‌گردد. سپس این id به متد getProduct ارسال می‌شود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.


برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
<div class="panel panel-primary" *ngIf="product">
  <div class="panel-heading" style="font-size:large">
    {{pageTitle + ": " + product.productName}}
  </div>

  <div class="panel-body">
    <div>
      Name: {{product.productName}}
    </div>
    <div>
      Code: {{product.productCode}}
    </div>
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
  </div>

  <div class="panel-footer">
    <a class="btn btn-default" [routerLink]="['/products']">
      <i class="glyphicon glyphicon-chevron-left"></i> Back
    </a>
    <a class="btn btn-primary" style="width:80px;margin-left:10px" 
       [routerLink]="['/products', product.id, 'edit']">
       Edit
    </a>
  </div>
</div>
در اینجا علاوه بر استفاده از شیء product در جهت نمایش جزئیات محصول انتخابی، دو دکمه‌ی back و edit نیز اضافه شده‌اند که اولی صفحه‌ی لیست محصولات را مجددا نمایش می‌دهد و دومی کار هدایت به صفحه‌ی ویرایش جزئیات این محصول را میسر می‌کند.


تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول

از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ProductRoutingModule
  ]
کد کامل کامپوننت ویرایش و افزودن جزئیات یک محصول به شرح ذیل است (فایل src\app\product\product-edit\product-edit.component.ts):
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-edit',
  templateUrl: './product-edit.component.html',
  styleUrls: ['./product-edit.component.css']
})
export class ProductEditComponent implements OnInit {
  pageTitle = 'Product Edit';
  product: IProduct;
  errorMessage: string;

  constructor(private productService: ProductService,
    private route: ActivatedRoute,
    private router: Router) { }

  ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }

  getProduct(id: number): void {
    this.productService.getProduct(id)
      .subscribe(
      (product: IProduct) => this.onProductRetrieved(product),
      (error: any) => this.errorMessage = <any>error
      );
  }

  onProductRetrieved(product: IProduct): void {
    this.product = product;

    if (this.product.id === 0) {
      this.pageTitle = 'Add Product';
    } else {
      this.pageTitle = `Edit Product: ${this.product.productName}`;
    }
  }

  deleteProduct(): void {
    if (this.product.id === 0) {
      // Don't delete, it was never saved.
      this.onSaveComplete();
    } else {
      if (confirm(`Really delete the product: ${this.product.productName}?`)) {
        this.productService.deleteProduct(this.product.id)
          .subscribe(
          () => this.onSaveComplete(`${this.product.productName} was deleted`),
          (error: any) => this.errorMessage = <any>error
          );
      }
    }
  }

  saveProduct(): void {
    if (true === true) {
      this.productService.saveProduct(this.product)
        .subscribe(
        () => this.onSaveComplete(`${this.product.productName} was saved`),
        (error: any) => this.errorMessage = <any>error
        );
    } else {
      this.errorMessage = 'Please correct the validation errors.';
    }
  }

  onSaveComplete(message?: string): void {
    if (message) {
      // TODO: show msg
    }

    // Navigate back to the product list
    this.router.navigate(['/products']);
  }
}
به همراه کد کامل قالب آن (فایل src\app\product\product-edit\product-edit.component.html):
<div class="panel panel-primary">
  <div class="panel-heading">
    {{pageTitle}}
  </div>

  <div class="panel-body" *ngIf="product">
    <form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm">
      <fieldset>
        <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || 
                                               productNameVar.dirty) && 
                                               !productNameVar.valid }">
          <label class="col-md-2 control-label" for="productNameId">Product Name</label>

          <div class="col-md-8">
            <input class="form-control" id="productNameId" type="text" placeholder="Name (required)" 
                   required minlength="3" [(ngModel)]=product.productName 
                   name="productName" #productNameVar="ngModel" />
            <span class="help-block" *ngIf="(productNameVar.touched ||
                                                         productNameVar.dirty) &&
                                                         productNameVar.errors">
                <span *ngIf="productNameVar.errors.required">
                    Product name is required.
                </span>
                <span *ngIf="productNameVar.errors.minlength">
                    Product name must be at least three characters.
                </span>
            </span>
          </div>
        </div>

        <div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || 
                                               productCodeVar.dirty) && 
                                               !productCodeVar.valid }">
          <label class="col-md-2 control-label" for="productCodeId">Product Code</label>

          <div class="col-md-8">
            <input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" 
                   required [(ngModel)]=product.productCode
                   name="productCode" #productCodeVar="ngModel" />
            <span class="help-block" *ngIf="(productCodeVar.touched ||
                                                         productCodeVar.dirty) &&
                                                         productCodeVar.errors">
                <span *ngIf="productCodeVar.errors.required">
                     Product code is required.
                </span>
            </span>
          </div>
        </div>

        <div class="form-group">
          <div class="col-md-4 col-md-offset-2">
            <span>
                 <button class="btn btn-primary"
                         type="submit"
                         style="width:80px;margin-right:10px"
                         [disabled]="!productForm.valid"
                         (click)="saveProduct()">
                         Save
                 </button>
             </span>
             <span>
                 <a class="btn btn-default"
                    [routerLink]="['/products']">
                       Cancel
                 </a>
             </span>
             <span>
                 <a class="btn btn-default"
                    (click)="deleteProduct()">
                     Delete
                  </a>
             </span>
          </div>
        </div>
      </fieldset>
    </form>
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
  </div>
</div>

توضیحات:

نکته‌ی مهمی را که در این کدها می‌خواهیم بررسی کنیم، متد ngOnInit آن‌است:
ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }
برنامه را یکبار توسط دستور ng server -o ساخته و اجرا کنید.
 - ابتدا لیست محصولات را مشاهده کنید.
 - سپس بر روی دکمه‌ی edit محصول شماره یک کلیک نمائید:


تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شده‌است. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیده‌است.
 - در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:


همانطور که مشاهده می‌کنید، هرچند URL صفحه تغییر کرده‌است، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشده‌اند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کرده‌ایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمی‌شود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر  root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمی‌شود. به همین جهت است که فیلدهای این فرم تغییر نکرده‌اند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آن‌ها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe می‌توان انجام داد:
  ngOnInit(): void {
    this.route.params.subscribe(
      params => {
        let id = +params['id'];
        this.getProduct(id);
      }
    );
  }
در اینجا چون کامپوننت به علت نحوه‌ی تعریف مسیریابی آن مجددا آغاز نمی‌شود، شیء route.snapshot برای دسترسی به پارامترهای تغییر کرده‌ی مسیریابی، کارآیی لازم را نداشته و باید از روش دوم دسترسی به آن مقادیر که یک Observable است و به تغییرات پارامترها گوش فرا می‌دهد، استفاده کرد.

یک نکته: هر زمانیکه از Observable‌ها استفاده می‌شود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت می‌شود.


روش خواندن پارامترهای مسیریابی در +Angular 4

روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کرده‌اند که توسط شیء paramMap مدیریت می‌شود:
    // For Angular 4+
    let id = +this.route.snapshot.paramMap.get('id');
    this.getProduct(id);

    // OR
    this.route.paramMap.subscribe(params => {
          let id = +params['id'];
          this.getProduct(id);
        });
در اینجا دو روش دسترسی به پارامتر id را مشاهده می‌کنید. در حالت کار با snapshot متد paramMap.get اینبار یک رشته را قبول می‌کند و یا بجای params می‌توان از paramMap استفاده کرد.


تعریف پارامترهای اختیاری مسیریابی

فرض کنید یک صفحه‌ی جستجو را طراحی کرده‌اید که در آن می‌توان نام و کد محصول را جستجو کرد. سپس می‌خواهیم این پارامترها را به صفحه‌ی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی می‌توان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
 { path: 'products/:name/:code', component: ProductListComponent }
و سپس می‌توان یک چنین لینکی را جهت فعالسازی آن اضافه کرد:
 [routerLink]="['/products', productName, productCode]"
این روش به همراه URLهایی ناخوانا خواهد بود که قسمت‌های مختلف آن مشخص نیستند و هر بار که قرار باشد گزینه‌ی دیگری را به جستجو اضافه کرد، نیاز است این پارامترها را نیز تغییر داد. همچنین در حین جستجو ممکن است تعدادی از فیلدها اختیاری باشند و نه اجباری. برای حل این مشکل امکان تعریف پارامترهای اختیاری مسیریابی نیز پیش بینی شده‌است. دراین حالت تعریف مسیریابی صفحه‌ی نمایش لیست محصولات به صورت ذیل خواهد بود (بدون ذکر هیچ پارامتری):
 { path: 'products', component: ProductListComponent },
و سپس لینکی که به آن تعریف می‌شود، نحوه‌ی تعریف خاصی را خواهد داشت:
 [routerLink]="['/products', {name: productName, code: productCode}]"
در اینجا پارامترهای اختیاری به صورت یک سری key/value در آرایه‌ی پارامترهای مسیریابی مشخص می‌شوند و هربار که نیاز به تغییر آن‌ها بود، نیازی نیست تا تعریف مسیریابی اصلی مرتبط را تغییر داد. باید دقت داشت که پارامترهای اختیاری باید همواره پس از پارامترهای اجباری در این آرایه، ذکر شوند.
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
 http://localhost:4200/products;name=Controller;code=gmg
نحوه‌ی خواندن این پارامترها، دقیقا همانند نحوه‌ی خواندن پارامترهای اجباری هستند و در اینجا از نام key‌ها برای اشاره‌ی به آن‌ها استفاده می‌شود:
let name = this.route.snapshot.params['name'];
let code = this.route.snapshot.params['code'];

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


تعریف پارامترهای کوئری در مسیریابی

فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کرده‌اید. اکنون اگر بر روی لینک مشاهده‌ی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده می‌شود. در یک چنین حالتی برای بهبود تجربه‌ی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمت‌های مختلف حفظ کرد:
 http://localhost:42000/products/5?filterBy=product1
در این لینک جزئیات محصول 5 نمایش داده خواهد شد. پس از عدد 5، پارامترهای کوئری ذکر شده‌اند و برخلاف پارامترهای اختیاری مسیریابی، در بین مسیریابی و هدایت کاربران به صفحات مختلف، حفظ خواهند شد.
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
 { path: 'products', component: ProductListComponent}
اعمال پارامترهای کوئری مختلف، به لینک‌های تعریف شده، توسط دایرکتیو queryParams صورت می‌گیرد و در اینجا یک مجموعه‌ی از key/valueها ذکر خواهند شد:
<a [routerLink] = "['/products', product.id]"
     [queryParams] = "{ filterBy: 'er', showImage: true }">
{{product.productName}}
</a>
در این مثال یک ثابت دلخواه er مشخص شده‌است. بدیهی است می‌توان متغیری را نیز بجای این ثابت تعریف کرد (یک خاصیت عمومی تعریف شده‌ی در سطح کامپوننت که به تکست‌باکس جستجو متصل است).

و یا با کدنویسی به صورت ذیل است:
this.router.navigate(['/products'],
   {
       queryParams: { filterBy: 'er', showImage: true}
   }
);
باید دقت داشت که چون این پارامترهای کوئری در بین مسیریابی به صفحات مختلف حفظ می‌شوند، نباید کلیدهای انتخاب شده‌ی در اینجا با سایر کلیدهای موجود در صفحات دیگر تداخل پیدا کنند.

یک مشکل: اگر در صفحه‌ی نمایش جزئیات یک محصول، دکمه‌ی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمی‌شوند. مرحله‌ی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
 [preserveQueryParams] = "true"
یعنی دکمه‌ی back به این شکل تغییر می‌کند:
<a class="btn btn-default"
           [routerLink]="['/products']"
           [preserveQueryParams]="true">
            <i class="glyphicon glyphicon-chevron-left"></i> Back
</a>
و یا استفاده از { preserveQueryParams: true} در حین کدنویسی.
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافته‌است و حالت قبلی منسوخ شده اعلام گردیده‌است.
this.router.navigate(['/products'],
   { queryParamsHandling: 'preserve'}
);

پس از بازگشت به صفحه‌ی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شده‌اند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آن‌ها در متد ngOnInit باید به صورت ذیل عمل کرد:
var filter = this.route.snapshot.queryParams['filterBy'] || '';
var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
علت تعریف '' || این است که ممکن است filterBy تعریف نشده باشد (برای حالتی که صفحه برای بار اول نمایش داده می‌شود) و دلیل تعریف 'true' === این است که مقادیر دریافتی در اینجا رشته‌ای هستند و نه boolean. به همین جهت باید با رشته‌ی true مقایسه شوند.

در مثال تکمیل شده‌ی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شده‌است:
 >ng g p product/ProductFilter
فایل src\app\product\product-filter.pipe.ts با این محتوا:
import { PipeTransform, Pipe } from '@angular/core';
import { IProduct } from './iproduct';

@Pipe({
  name: 'productFilter'
})
export class ProductFilterPipe implements PipeTransform {
  transform(value: IProduct[], filterBy: string): IProduct[] {
    filterBy = filterBy ? filterBy.toLocaleLowerCase() : null;
    return filterBy ? value.filter((product: IProduct) =>
      product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value;
  }
}
و سپس تعریف تکست باکس فیلتر کردن در ابتدای قالب src\app\product\product-list\product-list.component.html :
  <div class="panel-body">
    <div class="row">
            <div class="col-md-2">Filter by:</div>
            <div class="col-md-4">
                <input type="text" [(ngModel)]="listFilter" />
            </div>
    </div>
    <div class="row" *ngIf="listFilter">
            <div class="col-md-6">
                <h3>Filtered by: {{listFilter}} </h3>
            </div>
    </div>
و اعمال این فیلتر به حلقه‌ی نمایش ردیف‌های جدول؛ به همراه تعریف پارامتر کوئری:
<tr *ngFor="let product of products | productFilter:listFilter">
            <td><a [routerLink]="['/products', product.id]"
                   [queryParams]="{filterBy: listFilter}">
                  {{product.productName}}
                </a>
            </td>


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


و در این حالت اگر بر روی دکمه‌ی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده می‌شود:


که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت می‌شود:
@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  listFilter: string;

  ngOnInit(): void {
    this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
بازخوردهای دوره
طراحی روابط و ارجاعات در RavenDB
Timeline یک جدول نیست؛ یک گزارش هست از اطلاعات موجود (گزارش از اینکه یک کاربر به چه بلاگ‌هایی علاقمند است). به ازای هر View جدید مورد نیاز از بانک اطلاعاتی، یک جدول جدید ایجاد نمی‌کنند. همچنین در اینجا چیزی به نام جدول نداریم. این بانک اطلاعاتی، سندگرا است. هر رکورد آن یک سند JSON است و مجموعه‌ای از آن‌ها تا حدودی شبیه به یک جدول بانک اطلاعاتی رابطه‌ای است (تا حدودی از این جهت که هر سند JSON آن می‌تواند ساختار متفاوتی با قبلی داشته باشد، یا نداشته باشد؛ بسته به انتخاب و طراحی). در مورد «denormalized references» در متن بحث شده: «... بنابراین بهترین حالت استفاده از روش denormalized references محدود خواهد شد به موارد ذیل ... ». یعنی همه جا قرار نیست کار رفع نرمال سازی در بانک‌های اطلاعاتی NoSQL سندگرا انجام شود. سه مورد مهم دارد که در بحث ذکر شده‌است.
در اینجا برای طراحی حالت بلاگ‌های مورد علاقه یک شخص در RavenDB فقط کافی است از مفهوم Includes آن استفاده کنید (نمونه آن «Includeهای یک به چند» در بحث). داخل کلاس User، یک آرایه شبیه به SupplierIds (مثال زده شده) به نام FavoriteBlogIds خواهید داشت. بارگذاری و گزارشگیری از آن برای نمایش لیست این بلاگ‌ها و سپس مطالب آن‌ها، مانند مثال‌های Include و Load ایی است که ارائه شد.
بنابراین در اینجا به چیزی مانند دو جدول مجزای کاربران و جدول ذخیره سازی لیست بلاگ‌های محبوب آن‌ها نیازی نیست. لیست و آرایه Idهای بلاگ‌های مورد علاقه‌ی یک کاربر، داخل سند JSON همان کاربر قرار می‌گیرد.
مطالب
#Defensive Code in C - قسمت دوم

تعریف متد‌ها در برنامه نویسی:

متدها جزء اولین چیزهایی هستند که در هنگام شروع برنامه نویسی در هر یک از زبان‌های برنامه نویسی، برنامه نویس با آنها آشنا می‌شود. بنابراین متد‌ها به عنوان اصلی‌ترین Building Block  ها در زبان‌های برنامه نویسی دارای اهمیت بسیار زیادی می‌باشند. متد‌ها اولین جاهایی هستند که ما می‌توانیم کار خودمان را از آنها شروع کنیم و به سوی هدف خود حرکت کنیم.

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

بسیاری از برنامه نویسان انتظاری که از متد‌ها دارند فقط در همین حد می‌باشد و بیشتر از این درگیر هیچ مسئله‌ی دیگری نمی‌شوند. اما آیا فقط در نظر گرفتن این مسایل در رسیدن به یک کد خوش ساخت، قابل توسعه و بدون پیچیدگی کافی است؟

متد‌ها علاوه بر ویژگی‌های ذکر شده‌ی در بالا که بیشتر بر ویژگی‌های ذاتی و عملکردی آن تمرکز داشت، باید داری یکسری ویژگی‌های دیگر نیز باشند، متد‌ها باید Clean ، Testable و Predictable باشند. هر کدام از ویژگی‌های مذکور توسط این پارامتر‌ها تشریح می‌شوند. 

در ذیل ویژگی‌های مذکور در شکل بالا را تشریح خواهیم کرد.

Clear Purpose :

· یک متد یک کار را انجام می‌دهد و همچنین آن کار را نیز به خوبی انجام می‌دهد.

· متد به راحتی قابل درک می‌باشد.

· میزان خطا‌ها را به شدت کاهش می‌دهد.

· دیباگ کردن را در صورت وجود هر خطایی ساده‌تر می‌کند.

· قابلیت توسعه را افزایش می‌دهد.

· نوشتن تست برای این نوع متد‌ها به دلیل اینکه فقط بر روی یک هدف خاص تمرکز دارند ساده است.

Good Name :

· نام متد عمکرد آن را به روشنی بیان می‌کند

Focused Code :

· تمام کد نوشته شده‌ی در متد فقط بر روی یک هدف تمرکز دارند.

· خوانایی کد بالا است و میزان توضیحاتی که برای کد نوشته می‌شود در کمترین حد ممکن است.

· متد دارای تاثیرات ناخواسته‌ای بر سایر قسمت‌های نرم افزار نمی‌باشد. این مسئله به معنی است که این نوع متد‌ها شامل کدهایی که کارهای ناخواسته ای را انجام می‌دهند نمی‌باشد. برای مثال متدی که برای واکشی اطلاعات مشتریان استفاده می‌شود هیچگونه عملیاتی را که برای ثبت اطلاعات مشتریان انجام می‌شود، انجام نمی‌دهد.

Short Length :

· تعداد خطوط کد مربوط به متد کم می‌باشد. این مسئله خود باعث کاهش باگ‌های احتمالی در یک متد می‌شود.

Automated Code Test :

· متد این قابلیت را دارد که توسط زیر ساخت‌های تست، تست شود که این مسئله خود باعث افزایش کیفیت کد می‌شود.

Predictable Result :

· متد دارای یک نتیجه‌ی قابل پیش بینی می‌باشد.


در ادامه سعی می‌کنیم با ذکر یک مثال، مواردی را که ذکر شد بیشتر توضیح دهیم و دیدگاه کاربردی آن را بررسی کنیم.

مثالی از دنیای واقعی:

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

این روش کد نویسی روشی است که اکثر برنامه نویسان با آن آشنایی دارند. اولین مشکلی که این روش دارد این است که این کد Clean نمی‌باشد. قابلیت توسعه و نگهداری این کد بسیار پایین می‌باشد و به اصطلاح یک کد کاملا باگ خیز می‌باشد. حال نوبت این رسیده که این کد را از نظر پارامترهایی که در بالا ذکر شد بررسی کنیم.

Clear Purpose :

 آیا این متد دارای یک هدف مشخص و معین است؟ بیایید بررسی کنیم، ایجاد مشتری، ایجاد سفارش، ارسال درخواست به انبار، انجام عملیات پرداخت و ارسال رسید. همه‌ی این کارها توسط این متد انجام می‌شود. خودتان در مورد تبعات این روش کد نویسی قضاوت کنید.

Clear Name :

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

Focused Code :

 متد باید کاری را انجام دهد که نام آن بیان می‌کند و تمام کد‌های متد باید حتما بر روی آن هدف تمرکز کنند.

Short Length :

متد باید دارای طول کمی باشد. برای مثال طول کد نباید از اندازه‌ی صفحه نمایش بیشتر باشد. به عبارتی دیگر، کد متد نباید اسکرول بخورد. بنابراین باید سعی شود کد‌های اضافی از متد حذف شوند تا با این کار پیچیدگی کد هم به کمترین میزان برسد.

Automated Code Test :

 آیا این متد به وسیله‌ی  Automated Code Test  می تواند تست شود؟ چند نکته در مورد این کد وجود دارد که توانایی Automated Code Test را از این کد می‌گیرد. اولین مسئله این است که در این مثال منطق یا همان Business برنامه با UI تلفیق شده‌است. برای رفع این مشکل باید منطق برنامه را در یک پروژه‌ی مجزا از نوع Class Library قرار داد. مسئله‌ی دیگر این است که این متد برای تست شدن بسیار طولانی می‌باشد و باید به یکسری اجزای کوچکتر و منطقی‌تر شکسته شود و هر متد باید یک هدف و عملکرد روشن را داشته باشد. 

در قسمت بعدی راهکارهایی برای Refactor کردن کد بر اساس اصول ذکر شده ارائه خواهد شد.

مطالب
مقدمه‌ای بر یادگیری ماشین در #C با استفاده از ML.NET

هنگامی که درباره‌ی علم و یادگیری ماشینی فکر می‌کنیم، دو زبان برنامه‌نویسی بلافاصله به ذهن متبادر می‌شوند: پایتون و R. این دو زبان به شکل عمومی از بسیاری از الگوریتم‌های یادگیری ماشین رایج، تکنیکهای پیش‌پردازش داده‌ها و خیلی بیشتر از اینها پشتیبانی می‌کنند؛ بنابراین برای -تقریباً- هر مساله‌ی یادگیری ماشینی مورد استفاده قرار می‌گیرند. 
 با این‌حال، گاهی فرد یا شرکتی نمی‌تواند از پایتون یا R استفاده کند که می‌تواند به یکی از دلایل متعدد، از جمله وجود کد منبع در زبان دیگر یا نداشتن هیچ تجربه‌ای در پایتون یا R باشد. یکی از محبوب‌ترین زبان‌های امروزی، #C است که برای بسیاری از کاربردها مورد استفاده قرار می‌گیرد. مایکروسافت برای استفاده از قدرت یادگیری ماشین در #C، یک بسته را به نام ML.NET ایجاد کرده که همه‌ی قابلیت‌های یادگیری ماشین پایه را فراهم می‌کند. 
در این مقاله، به شما نشان خواهم داد که چگونه از ML.NET برای ایجاد یک مدل دسته‌بندی دوتایی بهره ببرید؛ قابلیت‌های AutoML را مورد استفاده قرار داده و از یک مدل Tensorflow با ML.NET استفاده کنید. کد کامل مخصوص مدل دسته‌بندی دوتایی را می‌توانید در GitHub بیابید.

افزودن ML.NET به پروژه‌ی #C
اضافه کردن ML.NET به یک پروژه‌ی #C یا #F آسان است. تنها کار لازم، اضافه کردن بسته‌ی Microsoft.ML یا در برخی موارد، -بسته به نیازمندی‌های پروژه- بسته‌های اضافی مانند: Microsoft.ML.ImageAnalytics, Microsoft.ML.TensorFlow یا Microsoft.ML.OnnxTransformer است. 


بارگذاری در یک دیتاست و ایجاد pipeline داده‌ها
بارگذاری و پیش‌پردازش یک مجموعه داده در ML.NET کاملا ً متفاوت از زمانی است که با دیگر بسته‌ها / چارچوب‌های یادگیری ماشین کار می‌کنیم. چون ما نیاز داریم به طور واضح، ساختار داده‌ها را بیان کنیم. برای انجام این کار، فایلی به نام ModelInput.cs را درون یک پوشه به نام DataModels ایجاد کرده و داخل این فایل، همه‌ی ستون‌های مجموعه داده‌های خود را ثبت خواهیم کرد. برای این مقاله، ما از مجموعه داده‌های ردیابی کلاه‌برداری کارت اعتباری استفاده می‌کنیم که می‌تواند آزادانه از Kaggle بارگیری شود. این مجموعه‌داده‌ها شامل ۳۱ ستون است. کلاس تراکنش (۰ یا ۱)، مقدار تراکنش، زمان تراکنش و نیز ۲۸ ویژگی بی‌نام (anonymous). 


using Microsoft.ML.Data;

namespace CreditCardFraudDetection.DataModels
{
    public class ModelInput
    {
        [ColumnName("Time"), LoadColumn(0)]
        public float Time { get; set; }

        [ColumnName("V1"), LoadColumn(1)]
        public float V1 { get; set; }

        [ColumnName("V2"), LoadColumn(2)]
        public float V2 { get; set; }

        [ColumnName("V3"), LoadColumn(3)]
        public float V3 { get; set; }

        [ColumnName("V4"), LoadColumn(4)]
        public float V4 { get; set; }

        [ColumnName("V5"), LoadColumn(5)]
        public float V5 { get; set; }

        [ColumnName("V6"), LoadColumn(6)]
        public float V6 { get; set; }

        [ColumnName("V7"), LoadColumn(7)]
        public float V7 { get; set; }

        [ColumnName("V8"), LoadColumn(8)]
        public float V8 { get; set; }

        [ColumnName("V9"), LoadColumn(9)]
        public float V9 { get; set; }

        [ColumnName("V10"), LoadColumn(10)]
        public float V10 { get; set; }

        [ColumnName("V11"), LoadColumn(11)]
        public float V11 { get; set; }

        [ColumnName("V12"), LoadColumn(12)]
        public float V12 { get; set; }

        [ColumnName("V13"), LoadColumn(13)]
        public float V13 { get; set; }

        [ColumnName("V14"), LoadColumn(14)]
        public float V14 { get; set; }

        [ColumnName("V15"), LoadColumn(15)]
        public float V15 { get; set; }

        [ColumnName("V16"), LoadColumn(16)]
        public float V16 { get; set; }

        [ColumnName("V17"), LoadColumn(17)]
        public float V17 { get; set; }

        [ColumnName("V18"), LoadColumn(18)]
        public float V18 { get; set; }

        [ColumnName("V19"), LoadColumn(19)]
        public float V19 { get; set; }

        [ColumnName("V20"), LoadColumn(20)]
        public float V20 { get; set; }

        [ColumnName("V21"), LoadColumn(21)]
        public float V21 { get; set; }

        [ColumnName("V22"), LoadColumn(22)]
        public float V22 { get; set; }

        [ColumnName("V23"), LoadColumn(23)]
        public float V23 { get; set; }

        [ColumnName("V24"), LoadColumn(24)]
        public float V24 { get; set; }

        [ColumnName("V25"), LoadColumn(25)]
        public float V25 { get; set; }

        [ColumnName("V26"), LoadColumn(26)]
        public float V26 { get; set; }

        [ColumnName("V27"), LoadColumn(27)]
        public float V27 { get; set; }

        [ColumnName("V28"), LoadColumn(28)]
        public float V28 { get; set; }

        [ColumnName("Amount"), LoadColumn(29)]
        public float Amount { get; set; }

        [ColumnName("Class"), LoadColumn(30)]
        public bool Class { get; set; }
    }
} 
در اینجا یک فیلد را برای هر یک از ستون‌های داخل مجموعه داده‌مان ایجاد می‌کنیم. نکته‌ی مهم، تعیین شاخص (Index)، نوع و ستون، به شکل صحیح است. حالا که داده‌های ما مدل‌سازی شده‌اند، باید قالب و شکل داده‌های خروجی خود را مدل کنیم. این کار می‌تواند به روشی مشابه با کدهای بالا انجام شود. 
 using Microsoft.ML.Data;

namespace CreditCardFraudDetection.DataModels
{
    public class ModelOutput
    {
        [ColumnName("PredictedLabel")]
        public bool Prediction { get; set; }

        public float Score { get; set; }
    }
}  
ما در این‌جا ۲ فیلد داریم. فیلد score نشان‌دهنده‌ی خروجی به شکل درصد است؛ در حالیکه فیلد prediction از نوع بولی است. اکنون که هر دو داده ورودی و خروجی را مدل‌سازی کرده‌ایم، می‌توانیم داده‌های واقعی خود را با استفاده از روش مونت‌کارلو بارگذاری کنیم.
IDataView trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
                                            path: dataFilePath,
                                            hasHeader: true,
                                            separatorChar: ',',
                                            allowQuoting: true,
                                            allowSparse: false);

ساخت و آموزش مدل
برای ایجاد و آموزش مدل، نیاز به ایجاد یک pipeline داریم که شامل پیش‌پردازش داده‌های مورد نیاز و الگوریتم آموزش است. برای این مجموعه داده‌ی خاص، انجام هر پیش‌پردازش بسیار دشوار است زیرا ۲۸ ویژگی بی‌نام دارد. بنابراین تصمیم گرفتم که آن را ساده نگه دارم و تنها همه‌ی ویژگی‌ها را الحاق کنم (این کار باید در ML.NET انجام شود).
var dataProcessPipeline = mlContext.Transforms.Concatenate("Features", new[] { "Time", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "V12", "V13", "V14", "V15", "V16", "V17", "V18", "V19", "V20", "V21", "V22", "V23", "V24", "V25", "V26", "V27", "V28", "Amount" });
برای مدل، الگوریتم LightGBM را انتخاب می‌کنم. این الگوریتم در واقع در Microsoft.ML از ابتدا وجود ندارد؛ بنابراین شما باید Microsoft.ML.LightGbm را نصب کنید تا قادر باشید از آن استفاده کنید.
// Choosing algorithm
var trainer = mlContext.BinaryClassification.Trainers.LightGbm(labelColumnName: "Class", featureColumnName: "Features");

// Appending algorithm to pipeline
var trainingPipeline = dataProcessPipeline.Append(trainer);
اکنون می‌توانیم مدل را با متد Fit، آموزش داده سپس با استفاده از mlContext.model.save ذخیره کنیم:
ITransformer model = trainingPipeline.Fit(trainingDataView);
mlContext.Model.Save(model , trainingDataView.Schema, <path>);

ارزیابی مدل
حالا که مدل ما آموزش دیده است، باید عملکرد آن را بررسی کنیم. ساده‌ترین راه برای انجام این کار، استفاده از اعتبارسنجی متقاطع (cross-validation) است. ML.Net به ما روش‌های اعتبارسنجی متقاطع را برای انواع مختلف داده‌های مختلف، ارایه می‌دهد. از آنجا که مجموعه داده‌های ما یک مجموعه داده دسته‌بندی دودویی است، ما از روش mlContext.BinaryClassification.CrossValidateNonCalibrated برای امتیازدهی به مدل خود استفاده خواهیم کرد:
var crossValidationResults = mlContext.BinaryClassification.CrossValidateNonCalibrated(trainingDataView, trainingPipeline, numberOfFolds: 5, labelColumnName: "Class");

انجام پیش‌بینی
پیش بینی داده‌های جدید با استفاده از ML.NET واقعاً سرراست و راحت است. ما فقط باید یک PredictionEngine، نمایشی دیگر را از مدل خود که به طور خاص، برای استنباط ساخته شده است، ایجاد کنیم و متد Predict آن را به عنوان یک شی ModelInput فراخوانی کنیم. 
var predEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(mlModel);

ModelInput sampleData = new ModelInput() {
    time = 0,
    V1 = -1.3598071336738,
    ...
};

ModelOutput predictionResult = predEngine.Predict(sampleData);

Console.WriteLine($"Actual value: {sampleData.Class} | Predicted value: {predictionResult.Prediction}"); 

Auto-ML 
نکته جالب دیگر در مورد ML.NET اجرای عالی Auto ML است. با استفاده از  Auto ML  فقط با مشخص کردن اینکه روی چه مشکلی کار می‌کنیم و ارائه داده‌های خود، می‌توانیم راه‌حل‌های اساسی و پایه‌ی یادگیری ماشین را بسازیم.
برای شروع کار با ML خودکار در ML.NET، باید  Visual Studio Extension - ML.NET Model Builder (Preview)  را بارگیری کنیم. این کار را می‌توان از طریق تب extensions انجام داد.
پس از نصب موفقیت آمیز افزونه، با کلیک راست روی پروژه‌ی خود در داخل Solution Ex می‌توانیم از Auto ML استفاده کنیم.

با این کار پنجره Model Builder باز می‌شود. سازنده‌ی مدل، ما را در روند ساخت یک مدل یادگیری ماشین راهنمایی می‌کند.
   
برای کسب اطلاعات در مورد چگونگی گذراندن مراحل مختلف، حتماً آموزش رسمی شروع کار را در سایت مایکروسافت، بررسی کنید. بعد از تمام مراحل، Model Builder به طور خودکار کد را تولید می‌کند.
 
استفاده از یک مدل پیش‌آموزش‌داده‌شده‌ی تنسورفلو (pre-trained) 
نکته‌ی جالب دیگر در مورد ML.NET این است که به ما امکان استفاده از مدل‌های Tensorflow و ONNX را برای استنباط ( inference ) می‌دهد. برای استفاده از مدل Tensorflow باید Microsoft.ML.TensorFlow را با استفاده از NuGet نصب کنیم. پس از نصب بسته‌های لازم، می‌توانیم با فراخوانی متد Model.LoadTensorFlowModel، یک مدل Tensorflow را بارگذاری کنیم. پس از آن، باید متد ScoreTensorFlowModel را فراخوانی کرده و نام لایه‌ی ورودی و خروجی را به آن ارسال کنیم.  
private ITransformer SetupMlnetModel(string tensorFlowModelFilePath)
{
    var pipeline = _mlContext.<preprocess-data>
           .Append(_mlContext.Model.LoadTensorFlowModel(tensorFlowModelFilePath)
                                               .ScoreTensorFlowModel(
                                                      outputColumnNames: new[]{TensorFlowModelSettings.outputTensorName },
                                                      inputColumnNames: new[] { TensorFlowModelSettings.inputTensorName },
                                                      addBatchDimensionInput: false));
 
    ITransformer mlModel = pipeline.Fit(CreateEmptyDataView());
 
    return mlModel;
}

اطلاعات بیشتر در مورد نحوه استفاده از مدل Tensorflow در ML.NET:
مطالب
آشنایی با NHibernate - قسمت سوم

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

کلاس Product تعریف شده در فایل جدید Product.cs در پوشه domain برنامه:

namespace NHSample1.Domain
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public bool Discontinued { get; set; }
}
}
کلاس ProductMapping تعریف شده در فایل جدید ProductMapping.cs (توصیه شده است که به ازای هر کلاس یک فایل جداگانه در نظر گرفته شود)، در پوشه Mappings برنامه:

using FluentNHibernate.Mapping;
using NHSample1.Domain;

namespace NHSample1.Mappings
{
public class ProductMapping : ClassMap<Product>
{
public ProductMapping()
{
Not.LazyLoad();
Id(p => p.Id).GeneratedBy.HiLo("1000");
Map(p => p.Name).Length(50).Not.Nullable();
Map(p => p.UnitPrice).Not.Nullable();
Map(p => p.Discontinued).Not.Nullable();
}
}
}
همانطور که ملاحظه می‌کنید، روش تعریف آن‌ها همانند شیء Customer است که در قسمت‌های قبل بررسی شد و نکته جدیدی ندارد.
آزمون واحد بررسی این نگاشت نیز همانند مثال قبلی است.
کلاس ProductMapping_Fixture را در فایل جدید ProductMapping_Fixture.cs به پروژه UnitTests خود (که ارجاعات آن‌را در قسمت قبل مشخص کردیم) خواهیم افزود:

using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;

namespace UnitTests
{
[TestFixture]
public class ProductMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_product()
{
new PersistenceSpecification<Product>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.Name, "Apples")
.CheckProperty(p => p.UnitPrice, 10.45m)
.CheckProperty(p => p.Discontinued, true)
.VerifyTheMappings();
}
}
}
و پس از اجرای این آزمون واحد، عبارات SQL ایی که به صورت خودکار توسط این ORM جهت بررسی عملیات نگاشت صورت خواهند گرفت به صورت زیر می‌باشند:

ProductMapping_Fixture.can_correctly_map_product : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 10.45, @p2 = True, @p3 = 1001
NHibernate: SELECT product0_.Id as Id1_0_, product0_.Name as Name1_0_, product0_.UnitPrice as UnitPrice1_0_, product0_.Discontinued as Disconti4_1_0_ FROM "Product" product0_ WHERE product0_.Id=@p0;@p0 = 1001

در ادامه تعریف کلاس کارمند، نگاشت و آزمون واحد آن به صورت زیر خواهند بود:

using System;
namespace NHSample1.Domain
{
public class Employee
{
public int Id { set; get; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
}


using NHSample1.Domain;
using FluentNHibernate.Mapping;

namespace NHSample1.Mappings
{
public class EmployeeMapping : ClassMap<Employee>
{
public EmployeeMapping()
{
Not.LazyLoad();
Id(e => e.Id).GeneratedBy.Assigned();
Map(e => e.LastName).Length(50);
Map(e => e.FirstName).Length(50);
}
}
}


using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class EmployeeMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_employee()
{
new PersistenceSpecification<Employee>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.FirstName, "name1")
.CheckProperty(p => p.LastName, "lname1")
.VerifyTheMappings();
}
}
}
خروجی SQL حاصل از موفقیت آزمون واحد آن:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 1001
NHibernate: SELECT employee0_.Id as Id4_0_, employee0_.LastName as LastName4_0_, employee0_.FirstName as FirstName4_0_ FROM "Employee" employee0_ WHERE employee0_.Id=@p0;@p0 = 1001

همانطور که ملاحظه می‌کنید، این آزمون‌های واحد 4 مرحله را در یک سطر انجام می‌دهند:
الف) ایجاد یک وهله از کلاس Employee
ب) ثبت اطلاعات کارمند در دیتابیس
ج) دریافت اطلاعات کارمند در وهله‌ای جدید از شیء Employee
د) و در پایان بررسی می‌کند که آیا شیء جدید ایجاد شده با شیء اولیه مطابقت دارد یا خیر

اکنون در ادامه پیاده سازی سیستم ثبت سفارشات، به قسمت جالب این مدل می‌رسیم. قسمتی که در آن ارتباطات اشیاء و روابط one-to-many تعریف خواهند شد. تعاریف کلاس‌های OrderItem و OrderItemMapping را به صورت زیر در نظر بگیرید:

کلاس OrderItem تعریف شده در فایل جدید OrderItem.cs واقع شده در پوشه domain پروژه:
که در آن هر سفارش (order) دقیقا از یک محصول (product) تشکیل می‌شود و هر محصول می‌تواند در سفارشات متعدد و مختلفی درخواست شود.

namespace NHSample1.Domain
{
public class OrderItem
{
public int Id { get; set; }
public int Quantity { get; set; }
public Product Product { get; set; }
}
}
کلاس OrderItemMapping تعریف شده در فایل جدید OrderItemMapping.cs :

using FluentNHibernate.Mapping;
using NHSample1.Domain;

namespace NHSample1.Mappings
{
public class OrderItemMapping : ClassMap<OrderItem>
{
public OrderItemMapping()
{
Not.LazyLoad();
Id(oi => oi.Id).GeneratedBy.Assigned();
Map(oi => oi.Quantity).Not.Nullable();
References(oi => oi.Product).Not.Nullable();
}
}
}
نکته جدیدی که در این کلاس نگاشت مطرح شده است، واژه کلیدی References می‌باشد که جهت بیان این ارجاعات و وابستگی‌ها بکار می‌رود. این ارجاع بیانگر یک رابطه many-to-one بین سفارشات و محصولات است. همچنین در ادامه آن Not.Nullable ذکر شده است تا این ارجاع را اجباری نمائید (در غیر اینصورت سفارش غیر معتبر خواهد بود).
نکته‌ی دیگر مهم آن این مورد است که Id در اینجا به صورت یک کلید تعریف نشده است. یک آیتم سفارش داده شده، موجودیت به حساب نیامده و فقط یک شیء مقداری (value object) است و به خودی خود امکان وجود ندارد. هر وهله از آن تنها توسط یک سفارش قابل تعریف است. بنابراین id در اینجا فقط به عنوان یک index می‌تواند مورد استفاده قرار گیرد و فقط توسط شیء Order زمانیکه یک OrderItem به آن اضافه می‌شود، مقدار دهی خواهد شد.

اگر برای این نگاشت نیز آزمون واحد تهیه کنیم، به صورت زیر خواهد بود:

using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class OrderItemMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_order_item()
{
var product = new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};

new PersistenceSpecification<OrderItem>(Session)
.CheckProperty(p => p.Id, 1)
.CheckProperty(p => p.Quantity, 5)
.CheckReference(p => p.Product, product)
.VerifyTheMappings();
}
}
}

مشکل! این آزمون واحد با شکست مواجه خواهد شد، زیرا هنوز مشخص نکرده‌ایم که دو شیء Product را که در قسمت CheckReference فوق برای این منظور معرفی کرده‌ایم، چگونه باید با هم مقایسه کرد. در مورد مقایسه نوع‌های اولیه و اصلی مانند int و string و امثال آن مشکلی نیست، اما باید منطق مقایسه سایر اشیاء سفارشی خود را با پیاده سازی اینترفیس IEqualityComparer دقیقا مشخص سازیم:

using System.Collections;
using NHSample1.Domain;

namespace UnitTests
{
public class CustomEqualityComparer : IEqualityComparer
{
public bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;

if (x is Product && y is Product)
return (x as Product).Id == (y as Product).Id;

if (x is Customer && y is Customer)
return (x as Customer).Id == (y as Customer).Id;

if (x is Employee && y is Employee)
return (x as Employee).Id == (y as Employee).Id;

if (x is OrderItem && y is OrderItem)
return (x as OrderItem).Id == (y as OrderItem).Id;


return x.Equals(y);
}

public int GetHashCode(object obj)
{
//شاید وقتی دیگر
return obj.GetHashCode();
}
}
}
در اینجا فقط Id این اشیاء با هم مقایسه شده است. در صورت نیاز تمامی خاصیت‌های این اشیاء را نیز می‌توان با هم مقایسه کرد (یک سری از اشیاء بکار گرفته شده در این کلاس در ادامه بحث معرفی خواهند شد).
سپس برای بکار گیری این کلاس جدید، سطر مربوط به استفاده از PersistenceSpecification به صورت زیر تغییر خواهد کرد:

new PersistenceSpecification<OrderItem>(Session, new CustomEqualityComparer())

پس از این تغییرات و مشخص سازی نحوه‌ی مقایسه دو شیء سفارشی، آزمون واحد ما پاس شده و خروجی SQL تولید شده آن به صورت زیر می‌باشد:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 5, @p1 = 1001, @p2 = 1
NHibernate: SELECT orderitem0_.Id as Id0_1_, orderitem0_.Quantity as Quantity0_1_, orderitem0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitem0_ inner join "Product" product1_ on orderitem0_.Product_id=product1_.Id WHERE orderitem0_.Id=@p0;@p0 = 1

قسمت پایانی کار تعاریف کلاس‌های نگاشت، مربوط به کلاس Order است که در ادامه بررسی خواهد شد.

using System;
using System.Collections.Generic;

namespace NHSample1.Domain
{
public class Order
{
public int Id { set; get; }
public DateTime OrderDate { get; set; }
public Employee Employee { get; set; }
public Customer Customer { get; set; }
public IList<OrderItem> OrderItems { get; set; }
}
}
نکته‌ی مهمی که در این کلاس وجود دارد استفاده از IList جهت معرفی مجموعه‌ای از آیتم‌های سفارشی است (بجای List و یا IEnumerable که در صورت استفاده خطای type cast exception در حین نگاشت حاصل می‌شد).

using NHSample1.Domain;
using FluentNHibernate.Mapping;

namespace NHSample1.Mappings
{
public class OrderMapping : ClassMap<Order>
{
public OrderMapping()
{
Not.LazyLoad();
Id(o => o.Id).GeneratedBy.GuidComb();
Map(o => o.OrderDate).Not.Nullable();
References(o => o.Employee).Not.Nullable();
References(o => o.Customer).Not.Nullable();
HasMany(o => o.OrderItems)
.AsList(index => index.Column("ListIndex").Type<int>());
}
}
}
در تعاریف نگاشت این کلاس نیز دو ارجاع به اشیاء کارمند و مشتری وجود دارد که با References مشخص شده‌اند.
قسمت جدید آن HasMany است که جهت تعریف رابطه one-to-many بکار گرفته شده است. یک سفارش رابطه many-to-one با یک مشتری و همچنین کارمندی که این رکورد را ثبت می‌کند، دارد. در اینجا مجموعه آیتم‌های یک سفارش به صورت یک لیست بازگشت داده می‌شود و ایندکس آن به ستونی به نام ListIndex در یک جدول دیتابیس نگاشت خواهد شد. نوع این ستون، int می‌باشد.

using System;
using System.Collections.Generic;
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class OrderMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_an_order()
{
{
var product1 =
new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};
var product2 =
new Product
{
Name = "Pears",
UnitPrice = 3.5m,
Discontinued = false
};

Session.Save(product1);
Session.Save(product2);

var items = new List<OrderItem>
{
new OrderItem
{
Id = 1,
Quantity = 100,
Product = product1
},
new OrderItem
{
Id = 2,
Quantity = 200,
Product = product2
}
};

var customer = new Customer
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

var employee =
new Employee
{
FirstName = "name1",
LastName = "lname1"
};



var order = new Order
{
Customer = customer,
Employee = employee,
OrderDate = DateTime.Today,
OrderItems = items
};

new PersistenceSpecification<Order>(Session, new CustomEqualityComparer())
.CheckProperty(o => o.OrderDate, order.OrderDate)
.CheckReference(o => o.Customer, order.Customer)
.CheckReference(o => o.Employee, order.Employee)
.CheckList(o => o.OrderItems, order.OrderItems)
.VerifyTheMappings();
}
}
}
}
همانطور که ملاحظه می‌کنید در این متد آزمون واحد، نیاز به مشخص سازی منطق مقایسه اشیاء سفارش، مشتری و آیتم‌های سفارش داده شده نیز وجود دارد که پیشتر در کلاس CustomEqualityComparer معرفی شدند؛ درغیر اینصورت این آزمون واحد با شکست مواجه می‌شد.
متد آزمون واحد فوق کمی طولانی است؛ زیرا در آن باید تعاریف انواع و اقسام اشیاء مورد استفاده را مشخص نمود (و ارزش کار نیز دقیقا در همینجا مشخص می‌شود که بجای SQL نوشتن، با اشیایی که توسط کامپایلر تحت نظر هستند سر و کار داریم).
تنها نکته جدید آن استفاده از CheckList برای بررسی IList تعریف شده در قسمت قبل است.

خروجی SQL این آزمون واحد پس از اجرا و موفقیت آن به صورت زیر است:

OrderMapping_Fixture.can_correctly_map_an_order : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 3, @p1 = 2
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Pears', @p1 = 3.5, @p2 = False, @p3 = 1002
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 2002
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 4, @p1 = 3
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 3003
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 100, @p1 = 1001, @p2 = 1
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 200, @p1 = 1002, @p2 = 2
NHibernate: INSERT INTO "Order" (OrderDate, Employee_id, Customer_id, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 2009/10/10 12:00:00 ق.ظ, @p1 = 3003, @p2 = 2002, @p3 = 0
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 0, @p2 = 1
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 1, @p2 = 2
NHibernate: SELECT order0_.Id as Id1_2_, order0_.OrderDate as OrderDate1_2_, order0_.Employee_id as Employee3_1_2_, order0_.Customer_id as Customer4_1_2_, employee1_.Id as Id4_0_, employee1_.LastName as LastName4_0_, employee1_.FirstName as FirstName4_0_, customer2_.Id as Id2_1_, customer2_.FirstName as FirstName2_1_, customer2_.LastName as LastName2_1_, customer2_.AddressLine1 as AddressL4_2_1_, customer2_.AddressLine2 as AddressL5_2_1_, customer2_.PostalCode as PostalCode2_1_, customer2_.City as City2_1_, customer2_.CountryCode as CountryC8_2_1_ FROM "Order" order0_ inner join "Employee" employee1_ on order0_.Employee_id=employee1_.Id inner join "Customer" customer2_ on order0_.Customer_id=customer2_.Id WHERE order0_.Id=@p0;@p0 = 0
NHibernate: SELECT orderitems0_.Order_id as Order4_2_, orderitems0_.Id as Id2_, orderitems0_.ListIndex as ListIndex2_, orderitems0_.Id as Id0_1_, orderitems0_.Quantity as Quantity0_1_, orderitems0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitems0_ inner join "Product" product1_ on orderitems0_.Product_id=product1_.Id WHERE orderitems0_.Order_id=@p0;@p0 = 0

تا اینجای کار تعاریف اشیاء ، نگاشت آن‌ها و همچنین بررسی صحت این نگاشت‌ها به پایان می‌رسد.

نکته:
دیتابیس برنامه را جهت آزمون‌های واحد برنامه، از نوع SQLite ساخته شده در حافظه مشخص کردیم. اگر علاقمند باشید که database schema تولید شده توسط NHibernate را مشاهده نمائید، در متد SetupContext کلاس FixtureBase که در قسمت قبل معرفی شد، سطر آخر را به صورت زیر تغییر دهید، تا اسکریپت دیتابیس نیز به صورت خودکار در خروجی اس کیوال آزمون واحد لحاظ شود (پارامتر دوم آن مشخص می‌کند که schema ساخته شده، نمایش داده شود یا خیر):

SessionSource.BuildSchema(Session, true);
پس از این تغییر و انجام مجدد آزمون واحد، اسکریپت دیتابیس ما به صورت زیر خواهد بود (که جهت ایجاد یک دیتابیس SQLite می‌تواند مورد استفاده قرار گیرد):

drop table if exists "OrderItem"

drop table if exists "Order"

drop table if exists "Customer"

drop table if exists "Product"

drop table if exists "Employee"

drop table if exists hibernate_unique_key

create table "OrderItem" (
Id INTEGER not null,
Quantity INTEGER not null,
Product_id INTEGER not null,
Order_id INTEGER,
ListIndex INTEGER,
primary key (Id)
)

create table "Order" (
Id INTEGER not null,
OrderDate DATETIME not null,
Employee_id INTEGER not null,
Customer_id INTEGER not null,
primary key (Id)
)

create table "Customer" (
Id INTEGER not null,
FirstName TEXT not null,
LastName TEXT not null,
AddressLine1 TEXT not null,
AddressLine2 TEXT,
PostalCode TEXT not null,
City TEXT not null,
CountryCode TEXT not null,
primary key (Id)
)

create table "Product" (
Id INTEGER not null,
Name TEXT not null,
UnitPrice NUMERIC not null,
Discontinued INTEGER not null,
primary key (Id)
)

create table "Employee" (
Id INTEGER not null,
LastName TEXT,
FirstName TEXT,
primary key (Id)
)

create table hibernate_unique_key (
next_hi INTEGER
)
البته اگر مستندات SQLite را مطالعه کرده باشید می‌دانید که مفهوم کلید خارجی در این دیتابیس وجود دارد اما اعمال نمی‌شود! (برای اعمال آن باید تریگر نوشت) به همین جهت در این اسکریپت تولیدی خبری از کلید خارجی نیست.

برای اینکه از دیتابیس اس کیوال سرور استفاده کنیم، در همان متد SetupContext کلاس مذکور، سطر اول را به صورت زیر تغییر دهید (نوع دیتابیس اس کیوال سرور 2008 مشخص شده و سپس رشته اتصالی به دیتابیس ذکر گردیده است):

var cfg = Fluently.Configure().Database(
// SQLiteConfiguration.Standard.ShowSql().InMemory
MsSqlConfiguration
.MsSql2008
.ShowSql()
.ConnectionString("Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true")
);

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

if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF88858466CFBF7]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF88858466CFBF7


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF888589F32DE52]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF888589F32DE52


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099B1EBA72BC]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099B1EBA72BC


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099BB2F9593A]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099BB2F9593A


if exists (select * from dbo.sysobjects where id = object_id(N'[OrderItem]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [OrderItem]

if exists (select * from dbo.sysobjects where id = object_id(N'[Order]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Order]

if exists (select * from dbo.sysobjects where id = object_id(N'[Customer]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Customer]

if exists (select * from dbo.sysobjects where id = object_id(N'[Product]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Product]

if exists (select * from dbo.sysobjects where id = object_id(N'[Employee]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Employee]

if exists (select * from dbo.sysobjects where id = object_id(N'hibernate_unique_key') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table hibernate_unique_key

create table [OrderItem] (
Id INT not null,
Quantity INT not null,
Product_id INT not null,
Order_id INT null,
ListIndex INT null,
primary key (Id)
)

create table [Order] (
Id INT not null,
OrderDate DATETIME not null,
Employee_id INT not null,
Customer_id INT not null,
primary key (Id)
)

create table [Customer] (
Id INT not null,
FirstName NVARCHAR(50) not null,
LastName NVARCHAR(50) not null,
AddressLine1 NVARCHAR(50) not null,
AddressLine2 NVARCHAR(50) null,
PostalCode NVARCHAR(10) not null,
City NVARCHAR(50) not null,
CountryCode NVARCHAR(2) not null,
primary key (Id)
)

create table [Product] (
Id INT not null,
Name NVARCHAR(50) not null,
UnitPrice DECIMAL(19,5) not null,
Discontinued BIT not null,
primary key (Id)
)

create table [Employee] (
Id INT not null,
LastName NVARCHAR(50) null,
FirstName NVARCHAR(50) null,
primary key (Id)
)

alter table [OrderItem]
add constraint FK3EF88858466CFBF7
foreign key (Product_id)
references [Product]

alter table [OrderItem]
add constraint FK3EF888589F32DE52
foreign key (Order_id)
references [Order]

alter table [Order]
add constraint FK3117099B1EBA72BC
foreign key (Employee_id)
references [Employee]

alter table [Order]
add constraint FK3117099BB2F9593A
foreign key (Customer_id)
references [Customer]

create table hibernate_unique_key (
next_hi INT
)
که نکات ذیل در مورد آن جالب توجه است:
الف) جداول مطابق نام کلاس‌های ما تولید شده‌اند.
ب) نام فیلدها دقیقا مطابق نام خواص کلاس‌های ما تشکیل شده‌اند.
ج) Id ها به صورت primary key تعریف شده‌اند (از آنجائیکه ما در هنگام تعریف نگاشت‌ها، آن‌ها را از نوع identity مشخص کرده بودیم).
د) رشته‌ها به نوع nvarchar با اندازه 50 نگاشت شده‌اند.
ه) کلیدهای خارجی بر اساس نام جدول با پسوند _id تشکیل شده‌اند.




ادامه دارد ...


مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای IRIS به ASP.NET Identity – بخش اول
سیستم مدیریت محتوای IRIS از سیستم‌های اعتبار سنجی و مدیریت کاربران رایج نظیر ASP.NET Membership و یا ASP.NET Simple Membership استفاده نمی‌کند و از یک سیستم احراز هویت سفارشی شده مبتنی بر FormsAuthentication بهره می‌برد. زمانیکه در حال نوشتن پروژه‌ی IRIS بودم هنوز ASP.NET Identity معرفی نشده بود و به دلیل مشکلاتی که سیستم‌های قدیمی ذکر شده داشت، یک سیستم اعتبار سنجی کاربران سفارشی شده را در پروژه پیاده سازی کردم.
برای اینکه با معایب سیستم‌های مدیریت کاربران پیشین و مزایای ASP.NET Identity آشنا شوید، مقاله زیر می‌تواند شروع خیلی خوبی باشد:


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

رفع باگ ثبت کاربرهای تکراری نسخه‌ی کنونی

قبل از این که سراغ پیاده سازی Identity برویم، ابتدا باید یک باگ مهم را در نسخه‌ی قبلی، برطرف نماییم. نسخه‌ی کنونی مدیریت کاربران اجازه‌ی ثبت کاربر با ایمیل و یا نام کاربری تکراری را نمی‌دهد. جلوگیری از ثبت نام کاربر جدید با ایمیل یا نام کاربری تکراری از طریق کدهای زیر صورت گرفته است؛ اما در عمل، همیشه هم درست کار نمی‌کند.
        public AddUserStatus Add(User user)
        {
            if (ExistsByEmail(user.Email))
                return AddUserStatus.EmailExist;
            if (ExistsByUserName(user.UserName))
                return AddUserStatus.UserNameExist;

            _users.Add(user);
            return AddUserStatus.AddingUserSuccessfully;
        }
شاید الان با خود بگویید که چرا برای فیلدهای UserName و Email ایندکس منحصر به فرد تعریف نشده است؟ دلیلش این بوده که در زمان نگارش پروژه، Entity Framework پشتیبانی پیش فرضی از تعریف ایندکس نداشت و نوشتن همین شرط‌ها کافی به نظر می‌رسید. باز هم ممکن است بگویید که مسائل همزمانی چگونه مدیریت شده است و اگر دو کاربر مختلف در یک لحظه، نام کاربری یکسانی را انتخاب کنند، سیستم چگونه از ثبت دو کاربر مختلف با نام کاربری یکسان ممانعت می‌کند؟ 
جواب این است که ممانعتی نمی‌کند و دو کاربر با نام‌های کاربری یکسان ثبت می‌شوند؛ اما من برای وبسایت خودم که تعداد کاربرانش محدود بود این سناریو را محتمل نمی‌دانستم و کد خاصی برای جلوگیری از این اتفاق پیاده سازی نکرده بودم.
با این حال، در حال حاضر نزدیک به 20 کاربر تکراری در دیتابیسی که این سیستم استفاده می‌کند ثبت شده است. اما واقعا آیا دو کاربر مختلف اطلاعات یکسانی وارد کرده‌اند؟
 دلیل رخ دادن این اتفاق این است که کاربری که در حال ثبت نام در سایت است، وقتی که بر روی دکمه‌ی ثبت نام کلیک می‌کند و اطلاعات به سرور ارسال می‌شوند، در سمت سرور بعد از رد شدن از شرطهای تکراری نبودن UserName و Email، قبل از رسیدن به متد SaveChanges برای ذخیره شدن اطلاعات کاربر جدید در دیتابیس، وقفه ای در ترد این درخواست به وجود می‌آید. کاربر که احساس می‌کند اتفاقی رخ نداده است، دوباره بر روی دکمه‌ی ثبت نام کلیک می‌کند و  همان اطلاعات قبلی به سرور ارسال می‌شود و این درخواست نیز دوباره شرط‌های تکراری نبودن اطلاعات را با موفقیت رد می‌کند( چون هنوز SaveChanges درخواست اول فراخوانی نشده است) و این بار SaveChanges درخواست دوم با موفقیت فراخوانی می‌شود و کاربر ثبت می‌شود. در نهایت هم ترد درخواست اول به ادامه‌ی کار خود می‌پردازد و SaveChanges درخواست اول نیز فراخوانی می‌شود و خیلی راحت دو کاربر با اطلاعات یکسان ثبت می‌شود. این سناریو را در ویژوال استادیو با قرار دادن یک break point قبل از فراخوانی متد SaveChanges می‌توانید شبیه سازی کنید.
احتمالا این سناریو با مباحث همزمانی در سیستم عامل و context switch‌های بین ترد‌ها مرتبط است و این context switch‌ها  بین درخواست‌ها و atomic نبودن روند چک کردن اطلاعات و ثبت آن ها، سبب بروز چنین مشکلی میشود.
برای رفع این مشکل می‌توان از غیر فعال کردن یک دکمه در حین انجام پردازش‌های سمت سرور استفاده کرد تا کاربر بی حوصله، نتواند چندین بار بر روی یک دکمه کلیک کند و یا راه حل اصولی‌تر این است که ایندکس منحصر به فرد برای فیلدهای مورد نظر تعریف کنیم.
به طور پیش فرض در ASP.NET Identity برای فیلدهای UserName و Email ایندکس منحصر به فرد تعریف شده است. اما مشکل این است که به دلیل وجود کاربرانی با Email و UserName تکراری در دیتابیس کنونی، امکان تعریف Index منحصر به فرد وجود ندارد و پیش از انجام هر کاری باید این ناهنجاری را در دیتابیس برطرف نماییم.
   
به شخصه معمولا برای انجام کارهایی از این دست، یک کنترلر در برنامه خود تعریف می‌کنم و در آنجا کارهای لازم را انجام می‌دهم.
در اینجا من برای حذف کاربران با اطلاعات تکراری، یک کنترلر به نام Migration و اکشن متدی به نام RemoveDuplicateUsers تدارک دیدم.
using System.Linq;
using System.Web.Mvc;
using Iris.Datalayer.Context;

namespace Iris.Web.Controllers
{
    public class MigrationController : Controller
    {
        public ActionResult RemoveDuplicateUsers()
        {
            var db = new IrisDbContext();

            var lstDuplicateUserGroup = db.Users
                                               .GroupBy(u => u.UserName)
                                               .Where(g => g.Count() > 1)
                                               .ToList();

            foreach (var duplicateUserGroup in lstDuplicateUserGroup)
            {
                foreach (var user in duplicateUserGroup.Skip(1).Where(user => user.UserMetaData != null))
                {
                    db.UserMetaDatas.Remove(user.UserMetaData);
                }
                
                db.Users.RemoveRange(duplicateUserGroup.Skip(1));
            }

            db.SaveChanges();

            return new EmptyResult();
        }
    }
}
در اینجا کاربران بر اساس نام کاربری گروه بندی می‌شوند و گروه‌هایی که بیش از یک عضو داشته باشند، یعنی کاربران آن گروه دارای نام کاربری یکسان هستند و به غیر از کاربر اول گروه، بقیه باید حذف شوند. البته این را متذکر شوم که منطق وبسایت من به این شکل بوده است و اگر منطق کدهای شما فرق می‌کند، مطابق با منطق خودتان این کدها را تغییر دهید.

تذکر: اینجا شاید بگویید که چرا cascade delete برای UserMetaData فعال نیست و بخواهید که آن را اصلاح کنید. پیشنهاد من این است که اکنون از هدف اصلی منحرف نشوید و تمام تمرکز خود را بر روی انتقال به ASP Identity با حداقل تغییرات بگذارید. این گونه نباشد که در اواسط کار با خود بگویید که بد نیست حالا فلان کتابخانه را آپدیت کنم یا این تغییر را بدهم بهتر می‌شود. سعی کنید با حداقل تغییرات رو به جلو حرکت کنید؛ آپدیت کردن کتابخانه‌ها را هم بعدا می‌شود انجام داد.
   

 مقایسه ساختار جداول دیتابیس کاربران IRIS با ASP.NET Identity

ساختار جداول ASP.NET Identity به شکل زیر است:

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

همان طور که مشخص است در هر دو سیستم، بین ساختار جداول و رابطه‌ی بین آن‌ها شباهت‌ها و تفاوت هایی وجود دارد. سیستم Identity دو جدول بیشتر از IRIS دارد و برای جداولی که در سیستم کنونی وجود ندارند نیاز به انجام کاری نیست و به هنگام پیاده سازی Identity، این جداول به صورت خودکار به دیتابیس اضافه خواهند شد.

دو جدول مشترک در این دو سیستم، جداول Users و Roles هستندکه نحوه‌ی ارتباطشان با یکدیگر متفاوت است. در Iris بین User و Role رابطه‌ی یک به چند وجود دارد ولی در Identity، رابطه‌ی بین این دو جدول چند به چند است و جدول واسط بین آن‌ها نیز UserRoles نام دارد.

از آن جایی که من قصد دارم در سیستم جدید هم رابطه‌ی بین کاربر و نقش چند به چند باشد، به پیش فرض‌های Identity کاری ندارم. به رابطه‌ی کنونی یک به چند کاربر و نقشش نیز دست نمی‌گذارم تا در انتها با یک کوئری از دیتابیس، اطلاعات نقش‌های کاربران را به جدول جدیدش منتقل کنم.

جدولی که در هر دو سیستم مشترک است و هسته‌ی آن‌ها را تشکیل می‌دهد، جدول Users است. اگر دقت کنید می‌بینید که این جدول در هر دو سیستم، دارای یک سری فیلد مشترک است که دقیقا هم نام هستند مثل Id، UserName و Email؛ پس این فیلد‌ها از نظر کاربرد در هر دو سیستم یکسان هستند و مشکلی ایجاد نمی‌کنند.

یک سری فیلد هم در جدول User در سیستم IRIS هست که در Identity نیست و بلعکس. با این فیلد‌ها نیز کاری نداریم چون در هر دو سیستم کار مخصوص به خود را انجام می‌دهند و تداخلی در کار یکدیگر ایجاد نمی‌کنند.

اما فیلدی که برای ذخیره سازی پسورد در هر دو سیستم استفاده می‌شود دارای نام‌های متفاوتی است. در Iris این فیلد Password نام دارد و در Identity نامش PasswordHash است.

برای اینکه در سیستم کنونی، نام  فیلد Password جدول User را به PasswordHash تغییر دهیم قدم‌های زیر را بر می‌داریم:

وارد پروژه‌ی DomainClasses شده و کلاس User را باز کنید. سپس نام خاصیت Password را به PasswordHash تغییر دهید.  پس از این تغییر بلافاصله یک گزینه زیر آن نمایان می‌شود که می‌خواهد در تمام جاهایی که از این نام استفاده شده است را به نام جدید تغییر دهد؛ آن را انتخاب کرده تا همه جا Password به PasswordHash تغییر کند.

برای این که این تغییر نام بر روی دیتابیس نیز اعمال شود باید از Migration استفاده کرد. در اینجا من از مهاجرت دستی که بر اساس کد هست استفاده می‌کنم تا هم بتوانم کدهای مهاجرت را پیش از اعمال بررسی و هم تاریخچه‌ای از تغییرات را ثبت کنم.

برای این کار، Package Manager Console را باز کرده و از نوار بالایی آن، پروژه پیش فرض را بر روی DataLayer قرار دهید. سپس در کنسول، دستور زیر را وارد کنید:

Add-Migration Rename_PasswordToPasswordHash_User

اگر وارد پوشه Migrations پروژه DataLayer خود شوید، باید کلاسی با نامی شبیه به 201510090808056_Rename_PasswordToPasswordHash_User ببینید. اگر آن را باز کنید کدهای زیر را خواهید دید: 

  public partial class Rename_PasswordToPasswordHash_User : DbMigration
    {
        public override void Up()
        {
            AddColumn("dbo.Users", "PasswordHash", c => c.String(nullable: false, maxLength: 200));
            DropColumn("dbo.Users", "Password");
        }
        
        public override void Down()
        {
            AddColumn("dbo.Users", "Password", c => c.String(nullable: false, maxLength: 200));
            DropColumn("dbo.Users", "PasswordHash");
        }
    }

بدیهی هست که این کدها عمل حذف ستون Password را انجام میدهند که سبب از دست رفتن اطلاعات می‌شود. کد‌های فوق را به شکل زیر ویرایش کنید تا تنها سبب تغییر نام ستون Password به PasswordHash شود.

    public partial class Rename_PasswordToPasswordHash_User : DbMigration
    {
        public override void Up()
        {
            RenameColumn("dbo.Users", "Password", "PasswordHash");
        }
        
        public override void Down()
        {
            RenameColumn("dbo.Users", "PasswordHash", "Password");
        }
    }

سپس باز در کنسول دستور Update-Database  را وارد کنید تا تغییرات بر روی دیتابیس اعمال شود.

دلیل اینکه این قسمت را مفصل بیان کردم این بود که می‌خواستم در مهاجرت از سیستم اعتبارسنجی خودتان به ASP.NET Identity دید بهتری داشته باشید.

تا به این جای کار فقط پایگاه داده سیستم کنونی را برای مهاجرت آماده کردیم و هنوز ASP.NET Identity را وارد پروژه نکردیم. در بخش‌های بعدی Identity را نصب کرده و تغییرات لازم را هم انجام می‌دهیم.

نظرات مطالب
معرفی کتابخانه Postal برای ASP.NET MVC
با کمی جستجو مشکلم حل شد.
برای باز کردن پورت باید به control panel > windows firewall > advanced setting > advanced settings > inbound roles  رفته و سپس در پنل سمت راست new role > port > tcp آدرس پورت رو در قسمت specific local port وارد میکنیم و و در مرحله بعد allow connection رو انتخاب میکنیم و در نهایت با انتخاب یه نام برای role ایجاد شده کار رو به پایان میرسونیم
ضمن این نکته که در صورتی که باز هم ارسال ایمیل در وب سرور صورت نگرفت به inbox آدرس gmail که میخواهیم از ان ارسال ایمیل انجام بدیم رفته و ایمیلی که سرویس جمیل برای اعتبارسنجی و اعمال مجوز براتون میفرسته رو باز می‌کنید دقت کنید که در اینجا بهتون میگه که باید پسورد جمیل رو باید تغییر بدید. اینکار رو انجام بدید و تغییرات صورت گرفته رو در تنظیمات smtp در فایل webconfig تون ست کنید با این کار مشکل حل خواهد شد.
نظرات مطالب
راه اندازی StimulSoft Report در ASP.NET MVC
سلام؛ من گزارشی طراحی کردم که از فونت b nazanin استفاده کرده. گزارش در زمان debug به خوبی با فونت b nazanin نشان داده میشه ولی وقتی روی وب publish میشه فونت اون به microsoft san serif تغییر کرده و به هم میریزه. میشه راهنمایی بفرمائید ؟