مطالب
معرفی یک ابزار گزارشگیری رایگان مخصوص WPF

تا صحبت از گزارشگیری به میان بیاید احتمالا معرفی ابزارهای تجاری مانند Reporting services ، کریستال ریپورت، stimulsoft.com ، fast-report.com و امثال آن درصدر لیست توصیه کنندگان و مشاوران قرار خواهند داشت. اما خوب برای ایجاد یک گزارشگیری ساده حتما نیازی نیست تا به این نوع ابزارهای تجاری مراجعه کرد. ابزار رایگان و سورس باز جالبی هم در این باره جهت پروژه‌های WPF در دسترس است:



در ادامه در طی یک مثال قصد داریم از این کتابخانه استفاده کنیم:

1) تنظیم وابستگی‌ها
پس از دریافت کتابخانه فوق، ارجاعات زیر باید به پروژه شما اضافه شوند:
CodeReason.Reports.dll (از پروژه فوق) و ReachFramework.dll (جزو اسمبلی‌های استاندارد دات نت است)

2) تهیه منبع داده‌ گزارش
کتابخانه‌ی فوق به صورت پیش فرض با DataTable‌ کار می‌کند. بنابراین کوئری‌های شما یا باید خروجی DataTable داشته باشد یا باید از یک سری extension methods برای تبدیل IEnumerable به DataTable استفاده کرد (در پروژه پیوست شده در پایان مطلب، این موارد موجود است).
برای مثال فرض کنید می‌خواهیم رکوردهایی را از نوع کلاس Product زیر در گزارش نمایش دهیم:

namespace WpfRptTests.Model
{
public class Product
{
public string Name { set; get; }
public int Price { set; get; }
}
}
3) تعریف گزارش
الف) اضافه کردن فایل تشکیل دهنده ساختار و ظاهر گزارش
گزارش‌‌های این کتابخانه مبتنی است بر اشیاء FlowDocument استاندارد WPF . بنابراین از منوی پروژه گزینه‌ی Add new item در قسمت WPF آن یک FlowDocument جدید را به پروژه اضافه کنید ( باید دقت داشت که Build action این فایل باید به Content تنظیم گردد). ساختار ابتدایی این FlowDocument به صورت زیر خواهد بود که به آن FlowDirection و FontFamily مناسب جهت گزارشات فارسی اضافه شده است. همچنین فضای نام مربوط به کتابخانه‌ی گزارشگیری CodeReason.Reports نیز باید اضافه گردد.
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FlowDirection="RightToLeft" FontFamily="Tahoma"
xmlns:xrd="clr-namespace:CodeReason.Reports.Document;assembly=CodeReason.Reports"
PageHeight="29.7cm" PageWidth="21cm" ColumnWidth="21cm">

</FlowDocument>

مواردی که در ادامه ذکر خواهند شد محتوای این گزارش را تشکیل می‌دهند:
ب) مشخص سازی خواص گزارش

<xrd:ReportProperties>
<xrd:ReportProperties.ReportName>SimpleReport</xrd:ReportProperties.ReportName>
<xrd:ReportProperties.ReportTitle>گزارش از محصولات</xrd:ReportProperties.ReportTitle>
</xrd:ReportProperties>
در اینجا ReportName و ReportTitle باید مقدار دهی شوند (دو dependency property که در کتابخانه‌ی CodeReason.Reports تعریف شده‌اند)

ج) مشخص سازی Page Header و Page Footer
اگر می‌خواهید عباراتی در بالا و پایین تمام صفحات گزارش تکرار شوند می‌توان از SectionReportHeader و SectionReportFooter این کتابخانه به صورت زیر استفاده کرد:
    <xrd:SectionReportHeader PageHeaderHeight="2" Padding="10,10,10,0" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportHeader>

<xrd:SectionReportFooter PageFooterHeight="2" Padding="10,0,10,10" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
نام کاربر:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
صفحه
<xrd:InlineContextValue PropertyName="PageNumber" FontWeight="Bold" /> از
<xrd:InlineContextValue PropertyName="PageCount" FontWeight="Bold" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportFooter>

دو نکته در اینجا حائز اهمیت هستند: xrd:InlineDocumentValue و xrd:InlineContextValue
InlineDocumentValue را می‌توان در کد‌های برنامه به صورت سفارشی اضافه کرد. بنابراین هر جایی که نیاز بود مقدار ثابتی از طریق کد نویسی به گزارش تزریق و اضافه شود می‌توان از InlineDocumentValue استفاده کرد. برای مثال در کدهای ViewModel برنامه که در ادامه ذکر خواهد شد دو مقدار PrintDate و RptBy به صورت زیر تعریف و مقدار دهی شده‌اند:
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحید");
برای مشاهده مقادیر مجاز مربوط به InlineContextValue به فایل ReportContextValueType.cs سورس کتابخانه مراجعه کنید که شامل PageNumber, PageCount, ReportName, ReportTitle است و توسط CodeReason.Reports به صورت پویا تنظیم خواهد شد.

د) مشخص سازی ساختار تولیدی گزارش

<Section Padding="80,10,40,10" FontSize="12">
<Paragraph FontSize="24" TextAlignment="Center" FontWeight="Bold">
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
<Paragraph TextAlignment="Center">
گزارش از لیست محصولات در تاریخ:
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
توسط:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
<xrd:SectionDataGroup DataGroupName="ItemList">
<Table CellSpacing="0" BorderBrush="Black" BorderThickness="0.02cm">
<Table.Resources>
<!-- Style for header/footer rows. -->
<Style x:Key="headerFooterRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Background" Value="LightGray"/>
</Style>

<!-- Style for data rows. -->
<Style x:Key="dataRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontSize" Value="12"/>
</Style>

<!-- Style for data cells. -->
<Style TargetType="{x:Type TableCell}">
<Setter Property="Padding" Value="0.1cm"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="0.01cm"/>
</Style>
</Table.Resources>

<Table.Columns>
<TableColumn Width="0.8*" />
<TableColumn Width="0.2*" />
</Table.Columns>
<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>نام محصول</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>قیمت</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>

<TableRowGroup Style="{StaticResource dataRowStyle}">
<xrd:TableRowForDataTable TableName="Product">
<TableCell>
<Paragraph>
<xrd:InlineTableCellValue PropertyName="Name" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<xrd:InlineTableCellValue PropertyName="Price" AggregateGroup="Group1" />
</Paragraph>
</TableCell>
</xrd:TableRowForDataTable>
</TableRowGroup>

<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Right">
<Bold>جمع کل</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" />
</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>

</Table>

<Paragraph TextAlignment="Center" Margin="5">
در این گزارش
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Count"
EmptyValue="هیچ"
FontWeight="Bold" /> محصول با جمع کل قیمت
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" /> وجود دارند.
</Paragraph>
</xrd:SectionDataGroup>
</Section>
برای اینکه بتوان این قسمت‌ها را بهتر توضیح داد، نیاز است تا تصاویر مربوط به خروجی این گزارش نیز ارائه شوند:




در ابتدا توسط دو پاراگراف، عنوان گزارش و یک سطر زیر آن نمایش داده شده‌اند. بدیهی است هر نوع شیء و فرمت مجاز در FlowDocument را می‌توان در این قسمت نیز قرار داد.
سپس یک SectionDataGroup جهت نمایش لیست آیتم‌ها اضافه شده و داخل آن یک جدول که بیانگر ساختار جدول نمایش رکوردهای گزارش می‌باشد، ایجاد گردیده است.
سه TableRowGroup در این جدول تعریف شده‌اند.
TableRowGroup های اولی و آخری دو سطر اول و آخر جدول گزارش را مشخص می‌کنند (سطر عناوین ستون‌ها در ابتدا و سطر جمع کل در پایان گزارش)
از TableRowGroup میانی برای نمایش رکوردهای مرتبط با نام جدول مورد گزارشگیری استفاده شده است. توسط TableRowForDataTable آن نام این جدول باید مشخص شود که در اینجا همان نام کلاس مدل برنامه است. به کمک InlineTableCellValue، خاصیت‌هایی از این کلاس را که نیاز است در گزارش حضور داشته باشند، ذکر خواهیم کرد. نکته‌ی مهم آن AggregateGroup ذکر شده است. توسط آن می‌توان اعمال جمع، محاسبه تعداد، حداقل و حداکثر و امثال آن‌را که در فایل InlineAggregateValue.cs سورس کتابخانه ذکر شده‌اند، به فیلدهای مورد نظر اعمال کرد. برای مثال می‌خواهیم جمع کل قیمت را در پایان گزارش نمایش دهیم به همین جهت نیاز بود تا یک AggregateGroup را برای این منظور تعریف کنیم.
از این AggregateGroup در سومین TableRowGroup تعریف شده به کمک xrd:InlineAggregateValue جهت نمایش جمع نهایی استفاده شده است.
همچنین اگر نیاز بود در پایان گزارش اطلاعات بیشتری نیز نمایش داده شود به سادگی می‌توان با تعریف یک پاراگراف جدید، اطلاعات مورد نظر را نمایش داد.

4) نمایش گزارش تهیه شده
نمایش این گزارش بسیار ساده است. View برنامه به صورت زیر خواهد بود:
<Window x:Class="WpfRptTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CodeReason.Reports.Controls;assembly=CodeReason.Reports"
xmlns:vm="clr-namespace:WpfRptTests.ViewModel"
Title="MainWindow" WindowState="Maximized" Height="350" Width="525">
<Window.Resources>
<vm:ProductViewModel x:Key="vmProductViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmProductViewModel}}">
<c:BusyDecorator IsBusyIndicatorHidden="{Binding RptGuiModel.IsBusyIndicatorHidden}">
<DocumentViewer Document="{Binding RptGuiModel.Document}" />
</c:BusyDecorator>
</Grid>
</Window>

تعریف ابتدایی RptGuiModel به صورت زیر است (جهت مشخص سازی مقادیر IsBusyIndicatorHidden و Document در حین بایندینگ اطلاعات):

using System.ComponentModel;
using System.Windows.Documents;

namespace WpfRptTests.Model
{
public class RptGuiModel
{
public IDocumentPaginatorSource Document { get; set; }
public bool IsBusyIndicatorHidden { get; set; }
}
}
و این View اطلاعات خود را از ViewModel زیر دریافت خواهد نمود:
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using CodeReason.Reports;
using WpfRptTests.Helper;
using WpfRptTests.Model;

namespace WpfRptTests.ViewModel
{
public class ProductViewModel
{
#region Constructors (1)

public ProductViewModel()
{
RptGuiModel = new RptGuiModel();
if (Stat.IsInDesignMode) return;
//انجام عملیات نمایش گزارش در یک ترد دیگر جهت قفل نشدن ترد اصلی برنامه
showReportAsync();
}

#endregion Constructors

#region Properties (1)

public RptGuiModel RptGuiModel { set; get; }

#endregion Properties

#region Methods (3)

// Private Methods (3)

private static List<Product> getProducts()
{
var products = new List<Product>();
for (var i = 0; i < 100; i++)
products.Add(new Product { Name = string.Format("Product{0}", i), Price = i });

return products;
}

private void showReport()
{
try
{
//Show BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = false;

var reportDocument =
new ReportDocument
{
XamlData = File.ReadAllText(@"Report\SimpleReport.xaml"),
XamlImagePath = Path.Combine(Environment.CurrentDirectory, @"Report\")
};

var data = new ReportData();

// تعریف متغیرهای دلخواه و مقدار دهی آن‌ها
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحید");

// استفاده از یک سری اطلاعات آزمایشی به عنوان منبع داده
data.DataTables.Add(getProducts().ToDataTable());

var xps = reportDocument.CreateXpsDocument(data);
//انقیاد آن به صورت غیر همزمان در ترد اصلی برنامه
DispatcherHelper.DispatchAction(
() => RptGuiModel.Document = xps.GetFixedDocumentSequence()
);
}
catch (Exception ex)
{
//وجود این مورد ضروری است زیرا بروز استثناء در یک ترد به معنای خاتمه آنی برنامه است
//todo: log errors
}
finally
{
//Hide BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = true;
}
}

private void showReportAsync()
{
var thread = new Thread(showReport);
thread.SetApartmentState(ApartmentState.STA); //for DocumentViewer
thread.Start();
}

#endregion Methods
}
}

توضیحات:
برای اینکه حین نمایش گزارش، ترد اصلی برنامه قفل نشود، از ترد استفاده شد و استفاده ترد به همراه DocumentViewer کمی نکته دار است:
- ترد تعریف شده باید از نوع STA باشد که در متد showReportAsync مشخص شده است.
- حین بایندیگ Document تولید شده توسط کتابخانه‌ی گزارشگیری به خاصیت Document کنترل، حتما باید کل عملیات در ترد اصلی برنامه صورت گیرد که سورس کلاس DispatcherHelper را در فایل پیوست خواهید یافت.

کل عملیات این ViewModel در متد showReport رخ می‌دهد، ابتدا فایل گزارش بارگذاری می‌شود، سپس متغیرهای سفارشی مورد نظر تعریف و مقدار دهی خواهند شد. در ادامه یک سری داده آزمایشی تولید و به DataTables گزارش ساز اضافه می‌شوند. در پایان XPS Document متناظر آن تولید شده و به کنترل نمایشی برنامه بایند خواهد شد.

دریافت سورس این مثال

نظرات مطالب
شروع به کار با DNTFrameworkCore - قسمت 4 - پیاده‌سازی CRUD API موجودیت‌ها
بررسی شد، مشکلی یافت نشد. برای تست، پروژه DNTFrameworkCore.TestAPI را اجرا کرده و با ابزار Postman درخواست زیر را انجام دهید.

درخواست PUT (حذف Blogs_Edit، ویرایش Blogs_Create و افزودن Blogs_UseCase)
{
    "name": "Administrators",
    "description": "حذف گروه کاربری پیش فرض «مدیران سیستم» باعث ایجاد اختلال در کارکرد صحیح سیستم خواهد شد.",
    "permissions": [
        {
            "name": "Blogs_View1",
            "id": 1,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Edit",
            "id": 2,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Create",
            "id": 3,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_View",
            "id": 4,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Delete",
            "id": 5,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Edit",
            "id": 6,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Create",
            "id": 7,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Delete",
            "id": 8,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_View",
            "id": 9,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_Edit",
            "id": 10,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_Create",
            "id": 11,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_View",
            "id": 12,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_Delete",
            "id": 13,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_Create1",
            "id": 15,
            "trackingState": "Modified",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_UseCase",
            "trackingState": "Added"
        },
        {
            "name": "Blogs_Edit",
            "id": 14,
            "trackingState": "Deleted"
        },
    ],
    "rowVersion": "AAAAAAAAF3M=",
    "id": 1
}

پاسخ سرور:
{
    "name": "Administrators",
    "description": "حذف گروه کاربری پیش فرض «مدیران سیستم» باعث ایجاد اختلال در کارکرد صحیح سیستم خواهد شد.",
    "permissions": [
        {
            "name": "Blogs_View1",
            "id": 1,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Edit",
            "id": 2,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Create",
            "id": 3,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_View",
            "id": 4,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Delete",
            "id": 5,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Edit",
            "id": 6,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Create",
            "id": 7,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Delete",
            "id": 8,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_View",
            "id": 9,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_Edit",
            "id": 10,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_Create",
            "id": 11,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_View",
            "id": 12,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_Delete",
            "id": 13,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_Create1",
            "id": 15,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_UseCase",
            "id": 17,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        }
    ],
    "rowVersion": "AAAAAAAAF3Q=",
    "id": 1
}

دقت کنید که خاصیت راهبری Permissions را در متد BuildFindQuery کلاس سرویس خود، Include کرده باشد.
نکته تکمیلی: در نسخه‌های جدید این زیرساخت، خصوصیت ModifiedProperties به کلاس TrackableEntity به عنوان کلاس پایه موجودیت‎‌هایی که نیاز به TrackingState دارند، اضافه شده است. اگر قصد دارید همه خصوصیات یک رکورد ویرایش شوند، این خصوصیت را با null مقداردهی کنید.

مطالب
Minimal API's در دات نت 6 - قسمت پنجم - پیاده سازی الگوی CQRS
تا قسمت قبل موفق شدیم فایل Program.cs برنامه‌ی Minimal API's را خلوت کنیم و همچنین زیرساختی را برای توسعه‌ی مبتنی بر ویژگی‌ها، ارائه دهیم. اما ... هنوز endpoints ما چنین شکلی را دارند:
        endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) =>
        {
            var authors = await ctx.Authors.ToListAsync();
            return authors;
        });

        endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) =>
        {
            var author = new Author();
            author.FirstName = authorDto.FirstName;
            author.LastName = authorDto.LastName;
            author.Bio = authorDto.Bio;
            author.DateOfBirth = authorDto.DateOfBirth;

            ctx.Authors.Add(author);
            await ctx.SaveChangesAsync();

            return author;
        });
 و یک چنین رویه‌ای جهت کار مستقیم با DbContext در اکشن متدهای MVC هیچگاه توصیه نمی‌شود. برای مثال به طور معمول، عملیاتی که در بدنه‌ی Lambda expressions فوق انجام شده، عموما به Repositories و Services محول شده و در نهایت از سرویس‌ها، در اکشن متدها استفاده می‌شود. در معماری جاری که در پیش گرفته‌ایم، دو لایه‌ی Repositories و Services حذف شده‌اند و دیگر خبری از آن‌ها نیست. در اینجا کار سرویس‌ها و مخازن، به هندلرهای معماری CQRS واگذار خواهند شد. هر هندلر نیز متکی به خود است و مستقل از سایر هندلرها طراحی می‌شود و این‌ها صرفا بر اساس نیازهای ویژگی جاری توسعه خواهند یافت و دقیقا در همان پوشه‌ی ویژگی مورد بررسی نیز قرار می‌گیرند؛ و نه پراکنده در لایه‌ای و یا پروژه‌ای دیگر. به این ترتیب درک یک ویژگی متکی به خود برنامه، ساده‌تر شده و در طول زمان، نگهداری و توسعه‌ی آن نیز ساده‌تر خواهد شد. مشکل داشتن سرویس‌هایی بزرگ که در معماری‌های متداول وجود دارند، استفاده‌ی از متدهای آن‌ها در چندین اکشن متد و چندین کنترلر مختلف است و اگر یکی از متدهای این سرویس بزرگ ما تغییر کند، بر روی چندین کنترلر تاثیر می‌گذارد که ممکن است سبب از کار افتادگی بعضی از آن‌ها شود؛ اما در اینجا هرکاری که انجام می‌شود و هر هندلری که توسعه می‌یابد، فقط مختص به یک کار و یک ویژگی مشخص است.


ایجاد Command و هندلر مخصوص ایجاد یک نویسنده‌ی جدید


در الگوی CQRS، یک دستور، کاری را بر روی بانک اطلاعاتی انجام می‌دهد. برای مثال در اینجا قرار است نویسنده‌ای را ثبت کند. در ادامه می‌خواهیم بدنه‌ی endpoints.MapPost فوق را با الگوی CQRS انطباق دهیم. به همین جهت به یک Command نیاز داریم:
using MediatR;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Api.Features.Authors;

public class CreateAuthorCommand : IRequest<Author>
{
    public AuthorDto AuthorDto { get; set; } = default!;
}
اینترفیس IRequest کتابخانه‌ی MediatR که در انتهای قسمت قبل به پروژه اضافه شد، چنین امضایی را دارد:
public interface IRequest<out TResponse> : IBaseRequest
یعنی <IRequest<Author به این معنا است که قرار است «خروجی» این عملیات، یک Author باشد و CreateAuthorCommand می‌تواند شامل تمام خواصی باشد که در جهت برآورده کردن این دستور مورد نیاز هستند؛ برای مثال کل اطلاعات شیء AuthorDto در اینجا.

سپس نیاز به یک هندلر است تا دستور رسیده را پردازش کند:
namespace MinimalBlog.Api.Features.Authors;

public class CreateAuthorCommandHandler : IRequestHandler<CreateAuthorCommand, Author>
{
    private readonly MinimalBlogDbContext _context;
    private readonly IMapper _mapper;

    public CreateAuthorCommandHandler(MinimalBlogDbContext context, IMapper mapper)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    public async Task<Author> Handle(CreateAuthorCommand request, CancellationToken cancellationToken)
    {
        if (request == null)
        {
            throw new ArgumentNullException(nameof(request));
        }

        var toAdd = _mapper.Map<Author>(request.AuthorDto);
        _context.Authors.Add(toAdd);
        await _context.SaveChangesAsync(cancellationToken);
        return toAdd;
    }
}
اینترفیس IRequestHandler چنین امضایی را دارد:
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
که اولین آرگومان جنریک آن، همان Command ای است که قرار است پردازش کند و خروجی آن، اطلاعاتی است که قرار است بازگشت دهد. یعنی متد Handle فوق، قرار است عملیات endpoints.MapPost را پیاده سازی کند و در اینجا با استفاده از AutoMapper، انتساب‌های آن حذف و ساده شده‌اند و مابقی آن، با بدنه‌ی lambda expression مربوط به endpoints.MapPost، یکی است. این هندلر، معادل یک یا چند متد از متدهای یک سرویس بزرگ است که در اینجا به صورت اختصاصی جهت پردازش فرمانی در کنار هم قرار می‌گیرند و متکی به خود هستند.

پس از این تغییرات، بدنه‌ی lambda expression مربوط به endpoints.MapPost به صورت زیر تغییر کرده و ساده می‌شود:
endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto) =>
{
     var command = new CreateAuthorCommand { AuthorDto = authorDto };
     var author = await mediator.Send(command);
     return author;
});
در اینجا تزریق وابستگی IMediator را مشاهده می‌کنید. با فراخوانی متد Send آن، شیء‌ای به هندلر متناظری ارسال شده، پردازش می‌شود و در نهایت شیءای را بازگشت خواهد داد. برای مثال در اینجا شیء Dto یک نویسنده به هندلر CreateAuthorCommandHandler ارسال و تبدیل به شیءای از نوع Author مربوط به دومین برنامه شده، سپس در بانک اطلاعاتی ذخیره می‌شود و در نهایت این نویسنده که اکنون به همراه یک Id نیز هست، بازگشت داده می‌شود. بنابراین هر هندلر یک object in و یک object out دارد که به عنوان آرگومان‌های جنریک IRequestHandler تعریف می‌شوند.



نکته 1: await داخل بدنه‌ی lambda expression مربوط به endpoints را فراموش نکنید. تمام متدهای IMediator از نوع aysnc هستند؛ هرچند روش نامگذاری SendAsync را رعایت نکرده‌اند و اگر این await فراموش شود، مشاهده خواهید کرد که برنامه در حین فراخوانی endpoints در مرورگر، در حالت هنگ و صبر کردن نامحدود قرار می‌گیرد، بدون اینکه کاری را انجام دهد و یا حتی استثنایی را صادر کند.


نکته 2: در پیاده سازی هندلر، استفاده از cancellationToken را نیز مشاهده می‌کنید. تقریبا تمام متدهای async مربوط به EF-Core به همراه پارامتری جهت دریافت cancellationToken هم هستند. اگر کاربری قصد لغو یک درخواست طولانی را داشته باشد و بر روی دکمه‌ی stop مرورگر کلیک کند و یا حتی صفحه را چندین بار ریفرش کند، این به معنای abort درخواست(های) رسیده‌است. وجود این cancellationTokenها، بار سرور را کاهش داده و عملیات در حال اجرای سمت سرور را در یک چنین حالت‌هایی متوقف می‌کند.
البته هندلری که در اینجا تعریف شده، این cancellationToken را باید از mediator دریافت کند که در کدهای endpoint فوق، چنین نیست. برای رفع این مشکل باید به صورت زیر عمل کرد:
endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) =>
        {
            var request = new GetAllAuthorsQuery();
            var authors = await mediator.Send(request, ct);
            return authors;
        });
این مورد را می‌توان به صورت یک best practice، به تمام endpoints اضافه کرد.


نکته 3: هندلرها عموما چیزی را بازگشت نمی‌دهند؛ صرف نظر از هندلر فوق که نیاز بوده تا Id شیء ذخیره شده را بازگشت دهد، عموما به همراه هیچ خروجی نیستند. به همین جهت در حین تعریف آن‌ها فقط کافی است در آرگومان‌های جنریک آن‌ها، نوع خروجی را ذکر نکنیم:
public class Handler : IRequestHandler<Command>
در یک چنین حالتی، امضای IRequestHandler به صورت خودکار به همراه خروجی از نوع Unit خواهد بود:
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, Unit> where TRequest : IRequest<Unit>
که این Unit معادل Void در کتابخانه‌ی mediator است و به نحو زیر در هندلرها مدیریت می‌شود:
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
   // ...
   return Unit.Value;
}
در یک چنین حالتی، تعریف یک Command نیز بر اساس اینترفیس IRequest انجام می‌شود:
public class Command : IRequest


ایجاد Query و هندلر مخصوص بازگشت لیست نویسنده‌‌ها

در الگوی CQRS، یک کوئری قرار است اطلاعاتی را بازگشت دهد و ... وضعیت بانک اطلاعاتی را تغییر نمی‌دهد. بنابراین در اینجا یک IRequest که قرار است لیستی از نویسندگان را بازگشت دهد، تعریف می‌کنیم. بدنه‌ی آن هم می‌تواند خالی باشد و یا به همراه خواصی مانند اطلاعات صفحه بندی و یا مرتب سازی گزارشگیری رسیده‌ی از درخواست:
using MediatR;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Api.Features.Authors;

public class GetAllAuthorsQuery : IRequest<List<Author>>
{
}
سپس نیاز به یک هندلر است تا درخواست رسیده را پردازش کند. این هندلر، کوئری فوق را دریافت کرده و لیست کاربران را بازگشت می‌دهد:
namespace MinimalBlog.Api.Features.Authors;

public class GetAllAuthorsHandler : IRequestHandler<GetAllAuthorsQuery, List<Author>>
{
    private readonly MinimalBlogDbContext _context;

    public GetAllAuthorsHandler(MinimalBlogDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public Task<List<Author>> Handle(GetAllAuthorsQuery request, CancellationToken cancellationToken)
    {
        return _context.Authors.ToListAsync(cancellationToken);
    }
}
پس از این تغییرات، بدنه‌ی lambda expression مربوط به endpoints.MapGet به صورت زیر تغییر کرده و ساده می‌شود:
endpoints.MapGet("/api/authors", async (IMediator mediator) =>
{
   var request = new GetAllAuthorsQuery();
   var authors = await mediator.Send(request);
   return authors;
});
مزیت استفاده‌ی از الگوی CQRS، تنها به حذف لایه‌ی سرویس و رسیدن به ویژگی‌هایی مستقل و متکی به خود، منحصر نیست. با استفاده از این الگو می‌توان مقیاس پذیری برنامه را نیز افزایش داد. برای مثال یک بانک اطلاعاتی بهینه سازی شده را صرفا برای کوئری‌ها، درنظر گرفت و بانک اطلاعاتی دیگری را تنها برای اعمال Write که Commands بر روی آن اجرا می‌شوند و در اینجا تنها نیاز به همگام سازی اطلاعات بانک اطلاعاتی Write، با بانک اطلاعاتی Read است که در بسیاری از اوقات پرکارتر از بانک‌های اطلاعاتی دیگر است:


و یا حتی معماری CQRS با معماری Event store نیز قابل ترکیب است:


در اینجا بجای استفاده از بانک اطلاعاتی Write، از یک Event store استفاده می‌شود. کار event store، دریافت رویدادهای write است و سپس باز پخش آن‌ها به بانک اطلاعاتی Read؛ تا کار همگام سازی به این نحو صورت گیرد.


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

تا اینجا برای مثال کلاسCreateAuthorCommand  را در یک فایل مجزا و سپس هندلر آن‌را به نام CreateAuthorCommandHandler در یک فایل دیگر تعریف کردیم. می‌توان جهت بالابردن خوانایی برنامه، کاهش رفت و برگشت‌ها برای یافتن کلاس‌های مرتبط و همچنین سهولت یافتن هندلرهای مرتبط با هر متد mediator.Send، از روش زیر نیز استفاده کرد:
public static class CreateAuthor
{
    public class Command : IRequest<AuthorGetDto>
    {
        // ...
    }

    public class Handler : IRequestHandler<Command, AuthorGetDto>
    {
       // ...
    }
}
در اینجا از nested classes استفاده شده‌است. ابتدا نام اصلی Command و یا کوئری ذکر می‌شود؛ که نام کلاس دربرگیرنده‌ی اصلی را تشکیل می‌دهد. سپس دو کلاس بعدی فقط Command و Handler نام می‌گیرند و نه هیچ نام دیگری. به این ترتیب به یکسری نام یک دست در کل پروژه خواهیم رسید. زمانیکه قرار است mediator.Send فراخوانی شود، اینبار چنین شکلی را پیدا می‌کند که مزیت آن، سهولت یافتن هندلر مرتبط، فقط با پیگیری کلاس اصلی CreateAuthor است:
var command = new CreateAuthor.Command { AuthorDto = authorDto };
var author = await mediator.Send(command, ct);

در مورد کوئری‌ها هم می‌توان به قالب مشابهی رسید که در اینجا هم کوئری و هندلر آن، ذیل نام اصلی مدنظر قرار می‌گیرند:
public static class GetAllAuthors
{
    public class Query : IRequest<List<AuthorGetDto>>
    {
       //...
    }

    public class Handler : IRequestHandler<Query, List<AuthorGetDto>>
    {
       //...
    }
}
و اگر کدهای نهایی این سری را که از قسمت اول قابل دریافت است بررسی کنید، از همین ساختار یکدست، برای تعاریف دستورات و کوئری‌ها استفاده شده‌است.
مطالب دوره‌ها
لیست ها و آرایه ها در #F
برای تعریف لیست در #F فقط کافیست از [] و برای جداسازی آیتم‌های موجود در لیست از عملگر :: (بخوانید cons) استفاده کنید. #F از لیست‌های خالی نیز پشتیبانی می‌کند. به مثال هایی از این دست توجه کنید

#1 let emptyList = []
#2 let oneItem = "one " :: []
#3 let twoItem = "one " :: "two " :: []
#1 تعریف یک لیست خالی
#2 تعریف یک لیست به همراه یک آیتم
#3 تعریف یک لیست به همراه دو آیتم
قبول دارم که دستورالعمل بالا برای مقدار دهی اولیه به لیست کمی طولانی و سخت است. برای همین می‌تونید از روش زیر هم استفاده کنید.
let shortHand = ["apples "; "pears"]
*کد بالا یک لیست با دو آیتم که از نوع رشته ای هستند تولید خواهد کرد.
می‌تونید از عملگر @ برای پیوستن دو لیست به هم نیز استفاده کنید.
let twoLists = ["one, "; "two, "] @ ["buckle "; "my "; "shoe "]

نکته : تمام آیتم‌های موجود در لیست باید از یک نوع باشند. بعنی امکان تعریف لیستی که دارای آیتم هایی با datatype‌های متفاوت باشد باعث تولید خطای کامپایلری می‌شود. اما اگر نیاز به لیستی دارید که باید چند datatype رو هم پوشش دهد می‌تونید از object‌ها استفاده کنید.
let objList = [box 1; box 2.0; box "three"]
در بالا یک لیست از object‌ها رو تعریف کرده ایم. فقط دقت کنید برای اینکه آیتم‌های موجود در لیست رو تبدیل به object کنیم از دستور box قبل از هر آیتم استفاده کردیم.

 در هنگام استفاده از عملگر‌ها @ و :: مقدار لیست تغییر نمی‌کند بلکه یک لیست جدید تولید خواهد شد.
#1 let one = ["one "]
#2 let two = "two " :: one
#3 let three = "three " :: two
#4 let rightWayRound = List.rev three

#5 let main() =
printfn "%A" one
printfn "%A" two
printfn "%A" three
printfn "%A" rightWayRound
#1 تعریف لیستی که دارای یک آیتم است.
#2 تعریف لیستی که دارای دو آیتم است(آیتم دوم لیست خود از نوع لیست است)
#3 تعریف لیستی که دارای سه آیتم است(ایتم دوم لیست خود از نوع لیستی است که دارای دو آیتم است)
# از تابع List.rev برای معکوس کردن آیتم‌های لیست three استفاده کردیم و مقادیر در لیستی به نام  rightWayRound قرار گرفت.
#5 تابع main برای چاپ اطلاعات لیست ها
بعد از اجرا خروجی زیر مشاهده می‌شود.
["one "]
["two "; "one "]
["three "; "two "; "one "]
["one "; "two "; "three "]
تفاوت بین لیست‌ها در #F و لیست و آرایه در دات نت(System.Collection.Generic)

 F#List
Net Array
 Net List
 #1 امکان تغییر در عناصر لیست
 NoYes
 Yes
 #2 امکان اضافه کردن عنصر جدید
 NoNo
 Yes
#3 جستجو
On
O1
O1
#1 در #F بعد از ساختن یک لیست امکان تغییر در مقادیر عناصر آن وجود ندارد.
#2 در #F بعد از ساختن یک لیست دیگه نمی‌تونید یک عنصر جدید به لیست اضافه کنید.
#3 جستجوی در لیست‌های #F به نسبت لیست‌ها و آرایه‌های در دات نت کند‌تر عمل می‌کند.

استفاده از عبارات در لیست ها
برای تعریف محدوده در لیست می‌تونیم به راحتی از روش زیر استفاده کنیم
let rangeList = [1..99]
برای ساخت لیست‌ها به صورت داینامیک استفاده از حلقه‌های تکرار در لیست مجاز است.
let dynamicList = [for x in 1..99 -> x*x]
کد بالا معادل کد زیر در #C  است.
for(int x=0;x<99 ; x++)
{
   myList.Add(x*x);
}
لیست‌ها و الگوی Matching
روش عادی برای کار با لیست‌ها در #F استفاده از الگوی Matching و توابع بازگشتی است.
let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]

let rec concatList l =
match l with
| head :: tail -> head @ (concatList tail)
| [] -> []

let primes = concatList listOfList

printfn "%A" primes
در مثال بالا ابتدا یک لیست تعریف کردیم که دارای 3 آیتم است و هر آیتم آن خود یک لیست با سه آیتم است.(تمام آیتم‌ها از نوع داده عددی هستند). یک تابع بازگشتی برای پیمایش تمام آیتم‌های لیست نوشتم که در اون از الگوی Matching استفاده کردیم.
خروجی :
[2; 3; 5; 7; 11; 13; 17; 19; 23; 29]
ماژول لیست
در جدول زیر تعدادی از توابع ماژول لیست رو مشاهده می‌کنید.

 نام تابع
 توضیحات
 List.length  تابعی که طول لیست را برمی گرداند
 List.head  تابعی برای برگشت عنصر اول لیست
 List.tail  تمام عناصر لیست را بر میگرداند به جز عنصر اول
 List.init  یک لیست با توجه به تعداد آیتم ایجاد می‌کند و یم تابع را بر روی تک تک عناصر لیست ایجاد می‌کند.
 List.append  یک لیست را به عنوان ورودی دریافت می‌کند و به لیست مورد نظر اضافه می‌کند و مجموع دو لیست را برگشت می‌دهد
 List.filter  فقط عناصری را برگشت می‌دهد که شرط  مورد نظر بر روی آن‌ها مقدار true را برگشت دهد
 List.map  یک تابع مورد نظر را بر روی تک تک عناصر لیست اجرا می‌کند و لیست جدید را برگشت می‌دهد
 List.iter  یک تابع مورد نظر را بر روی تک تک عناصر لیست اجرا می‌کند  
 List.zip 
 مقادیر دو لیست را با هم تجمیع می‌کند و لیست جدید را برگشت می‌دهد. اگر طول 2 لیست ورودی یکی نباشد خطا رخ خواهدداد 
 List.unzip درست برعکس تابع بالا عمل می‌کند
 List.toArray  لیست را تبدیل به آرایه می‌کند
 List.ofArray آرایه را تبدیل به لیست می‌کند
مثال هایی از توابع بالا
 List.head [5; 4; 3]

List.tail [5; 4; 3]

List.map (fun x -> x*x) [1; 2; 3]

List.filter (fun x -> x % 3 = 0) [2; 3; 5; 7; 9]

Sequence Collection

seq در #F یک توالی از عناصری است که هم نوع باشند. عموما از sequence‌ها زمانی استفاده میکنیم که یک مجموعه از داده‌ها با تعداد زیاد و مرتب شده داشته باشیم ولی نیاز به استفاده از تمام عناصر آن نیست. کارایی sequence  در مجموعه‌های با تعداد زیاد از list‌ها به مراتب بهتر است. sequence‌‌ها را با تابع  seq می‌شناسند که معادل IEnumerable در دات نت است. بنابر این هر مجمو عه ای که IEnumerable رو در دات نت پیاده سازی کرده باشد در #F با seq قابل استفاده است.

مثال هایی از نحوه استفاده seq

#1 seq بامحدوده 1 تا 100 و توالی 10 
seq { 0 .. 10 .. 100 }
#2 استفاده از حلقه‌های تکرار برای تعریف محدوده و توالی در seq
seq { for i in 1 .. 10 do yield i * i }
#3 استفاده از <- به جای yield
seq { for i in 1 .. 10 -> i * i }
#4 استفاده از حلقه for به همراه شرط برای فیلتر کردن
let isprime n =
    let rec check i =
        i > n/2 || (n % i <> 0 && check (i + 1))
    check 2

let aSequence = seq { for n in 1..100 do if isprime n then yield n }
چگونگی استفاده از توابع seq
در این بخش به ارائه مثال هایی کاربردی‌تر از چگونگی استفاده از seq در #F می‌پردازیم. برای شروع نحوه ساخت یک seq خالی یا empty رو خواهم گفت.
let seqEmpty = Seq.empty
روش ساخت یک seq که فقط یک عنصر را برگشت می‌دهد.
let seqOne = Seq.singleton 10
برای ساختن یک seq همانند لیست‌ها می‌تونیم از seq.init استفاده کنیم. عدد 5 که بلافاصله بعد از تابع seq.init آمده است نشان دهنده تعداد آیتم‌ها موجود در seq خواهد بود. seq.iter هم یک تابع مورد نظر رو بر روی تک تک عناصر seq اجرا خواهد کرد.(همانند list.iter)
let seqFirst5MultiplesOf10 = Seq.init 5 (fun n -> n * 10)
Seq.iter (fun elem -> printf "%d " elem) seqFirst5MultiplesOf10
خروجی مثال بالا
0 10 20 30 40
با استفاده از توابع seq.ofArray , seq.ofList می‌تونیم seq مورد نظر خود را از لیست یا آرایه مورد نظر بسازیم.
let seqFromArray2 = [| 1 .. 10 |] |> Seq.ofArray 
البته این نکته رو هم یادآور بشم که به کمک عملیات تبدیل نوع(type casting) هم می‌تونیم آرایه رو به seq تبدیل کنیم. به صورت زیر
let seqFromArray1 = [| 1 .. 10 |] :> seq<int>
برای مشخص کردن اینکه آیا یک آیتم در seq موجود است یا نه می‌تونیم از seq.exists به صورت زیر استفاده کنیم.
let containsNumber number seq1 = Seq.exists (fun elem -> elem = number) seq1
let seq0to3 = seq {0 .. 3}
printfn "For sequence %A, contains zero is %b" seq0to3 (containsNumber 0 seq0to3)
اگر seq پاس داده شده به تابع exists خالی باشد یا یک ArgumentNullException متوقف خواهید شد.

برای جستجو و پیدا کردن یک آیتم در seq می‌تونیم از seq.find استفاده کنیم.
let isDivisibleBy number elem = elem % number = 0
let result = Seq.find (isDivisibleBy 5) [ 1 .. 100 ]
printfn "%d " result
دقت کنید که اگر هیچ آیتمی در sequence با  predicate مورد نظر پیدا نشود یک KeyNotFoundException رخ خواهد داد. در صورتی که مایل نباشید که استثنا رخ دهد می‌توانید از تابع seq.tryFind استفاده کنید. هم چنین خالی بودن sequence ورودی باعث ArgumentNullExceptionخواهد شد.

استفاده از lambda expression در توابع
lamdaExpressoion از توانایی‌ها مورد علاقه برنامه نویسان دات نت است و کمتر کسی است حاضر به استفاده از آن در کوئری‌های linq نباشد. در #F نیز می‌توانید از lambda Expression استفاده کنید. در ادامه به بررسی مثال هایی از این دست خواهیم پرداخت.

تابع skipWhile
همانند skipWhile در linq عمل می‌کند. یعنی یک predicate مورد نظر را بر روی تک تک عناصر یک لیست اجرا می‌کند و آیتم هایی که شرط برای آن‌ها true باشد نادیده گرفته میشوند و مابقی آیتم‌ها برگشت داده می‌شوند.
let mySeq = seq { for i in 1 .. 10 -> i*i }
let printSeq seq1 = Seq.iter (printf "%A ") seq1; printfn "" 
let mySeqSkipWhileLessThan10 = Seq.skipWhile (fun elem -> elem < 10) mySeq
mySeqSkipWhileLessThan10 |> printSeq
می‌بینید که predicate مورد نظر برای تابع skipWhile به صورت lambda expression است که با رنگ متفاوت نمایش داده شده است.(استفاده از کلمه fun).
 خروجی به صورت زیر است:
16 25 36 49 64 81 100
 برای بازگرداندن یک تعداد مشخص از آیتم‌های seq می‌تونید از توابع seq.take یا seq.truncate استفاده کنید. ابتدا باید تعداد مورد نظر و بعد لیست مورد نظر را به عنوان پارامتر مقدار دهی کنید.
مثال:
let mySeq = seq { for i in 1 .. 10 -> i*i }
let truncatedSeq = Seq.truncate 5 mySeq
let takenSeq = Seq.take 5 mySeq

let printSeq seq1 = Seq.iter (printf "%A ") seq1; printfn "" 
#1 truncatedSeq |> printSeq #3 takenSeq |> printSeq
خروجی
1 4 9 16 25 //truncate
1 4 9 16 25 //take

Tuples
tuples در #F به گروهی از مقادیر بی نام ولی مرتب شده که می‌توانند انواع متفاوت هم داشته باشند گفته می‌شود. ساختار کلی آن به صورت ( element , ... , element )  است که هر element خود می‌تواند یک عبارت نیز باشد.(مشابه کلاس Tuple در #C که به صورت generic استفاده می‌کنیم)
// Tuple of two integers.
( 1, 2 ) 

// Triple of strings.
( "one", "two", "three" ) 

// Tuple of unknown types.
( a, b ) 

// Tuple that has mixed types.
( "one", 1, 2.0 ) 

// Tuple of integer expressions.
( a + 1, b + 1)
نکات استفاده از tuple
#1 می‌تونیم از الگوی Matching برای دسترسی به عناصر tuple استفاده کنیم.
let print tuple1 =
   match tuple1 with
    | (a, b) -> printfn "Pair %A %A" a b
#2 میتونیم از let  برای تعربف الگوی tuple استفاده کنیم.
let (a, b) = (1, 2)
#3 توابع fst و snd مقادیر اول و دوم هر tuple رو بازگشت می‌دهند
let c = fst (1, 2) // return 1
let d = snd (1, 2)// return 2
#4 تابعی برای بازگشت عنصر سوم یک tuple وجود ندارد ولی این تابع رو با هم می‌نویسیم:
let third (_, _, c) = c
کاربرد tuple در کجاست
زمانی که یک تابع باید بیش از یک مقدار را بازگشت دهد از tuple‌ها استفاده می‌کنیم. برای مثال
let divRem a b = 
   let x = a / b
   let y = a % b
   (x, y)
خروجی تابع divRem از نوع tuple که دارای 2 مقدار است می‌باشد.
نظرات مطالب
امکان تعریف اعضای static abstract در اینترفیس‌های C# 11
پشتیبانی از ریاضیات جنریک در C# 11

در C# 11، امکان انجام عملیات ریاضی بر روی نوع‌های جنریک میسر شده‌است که قسمتی از آن‌را در مطلب جاری مطالعه کردید. این ویژگی به همراه دو مزیت زیر است:
- اکنون اعضای استاتیک اینترفیس‌ها می‌توانند abstract هم باشند؛ یعنی کلاس‌های پیاده ساز آن‌ها باید این اعضای استاتیک را پیاده سازی کنند.
-  امکان تعریف عملگرهای استاتیک ریاضی در اینترفیس‌ها ممکن شده‌است:
 static T Add<T>(T left, T right) where T : INumber<T> => left + right;
در این مثال، نوع T، تنها می‌تواند از نوع <INumber<T باشد که یکی از اینترفیس‌های جدید NET 7. است. این اینترفیس نیز پیاده سازی کننده‌ی IAdditionOperators است که در آن امکان دسترسی به عملگر + وجود دارد. متد فوق را می‌توان تقریبا توسط تمام نوع‌های ریاضی توکار دات نت مورد استفاده قرار داد؛ از این جهت که تعاریف آن‌ها نیز در دات نت 7 برای پشتیبانی از INumber به‌روز رسانی شده‌اند.
یکی از مهم‌ترین مزیت‌های چنین امکانی، کاهش تعداد overload هایی است که باید توسعه دهندگان کتابخانه‌ها برای پشتیبانی از انواع و اقسام نوع‌های ریاضی ارائه دهند.
 

تغییرات API دات نت 7 در جهت پشتیبانی از ریاضیات جنریک

در مطلب جاری با اینترفیس جدید INumber آشنا شدیم که توسط آن مفاهیمی مانند صفر و یک و همچنین سربارگذاری عملگرهای ریاضی میسر شده‌است. تعداد این اینترفیس‌های توکار در دات نت 7، فراتر از یک مورد فوق است. برای مثال اینترفیس جدید IMinMaxValue امکان دسترسی به T.MinValue و T.MaxValue را میسر می‌کند.
یک مثال: امضای نوع Int32 در دات نت 7 به صورت زیر در آمده‌است:
public readonly struct Int32 : 
  IComparable, 
  IComparable<int>, 
  IConvertible, 
  IEquatable<int>, 
  IFormattable, 
  IParsable<int>, 
  ISpanFormattable, 
  ISpanParsable<int>, 
  IAdditionOperators<int, int, int>, 
  IAdditiveIdentity<int, int>, 
  IBinaryInteger<int>, 
  IBinaryNumber<int>, 
  IBitwiseOperators<int, int, int>, 
  IComparisonOperators<int, int, bool>, 
  IEqualityOperators<int, int, bool>, 
  IDecrementOperators<int>, 
  IDivisionOperators<int, int, int>, 
  IIncrementOperators<int>, 
  IModulusOperators<int, int, int>, 
  IMultiplicativeIdentity<int, int>, 
  IMultiplyOperators<int, int, int>, 
  INumber<int>, 
  INumberBase<int>, 
  ISubtractionOperators<int, int, int>, 
  IUnaryNegationOperators<int, int>, 
  IUnaryPlusOperators<int, int>, 
  IShiftOperators<int, int, int>, 
  IMinMaxValue<int>, 
  ISignedNumber<int>
یعنی تمام نوع‌های ریاضی توکار دات نت 7 از ریاضیات جنریک پشتیبانی می‌کنند. البته نوع‌ها زیر هنوز از یک چنین پشتیبانی برخوردار نیستند:
- System.Half
- System.Numerics.BigInteger
- System.Numerics.Complex
- System.Runtime.InteropServices.NFloat
- System.Int128
- System.UInt128

نوع‌های Int128 و UIn128 جزو تازه‌های دات نت 7 هستند (128-bit signed integer و 128-bit unsigned integer).

البته عموم ما از همان اینترفیس INumber و IBinaryInteger استفاده خواهیم کرد که خود آن نیز به صورت زیر تعریف شده‌است:
public interface INumber<TSelf> : 
IComparable, 
IComparable<TSelf>, 
IEquatable<TSelf>, 
IFormattable, 
IParsable<TSelf>, 
ISpanFormattable, 
ISpanParsable<TSelf>, 
IAdditionOperators<TSelf, TSelf, TSelf>, 
IAdditiveIdentity<TSelf, TSelf>, 
IComparisonOperators<TSelf, TSelf, bool>, 
IEqualityOperators<TSelf, TSelf, bool>, 
IDecrementOperators<TSelf>, 
IDivisionOperators<TSelf, TSelf, TSelf>, 
IIncrementOperators<TSelf>, 
IModulusOperators<TSelf, TSelf, TSelf>, 
IMultiplicativeIdentity<TSelf, TSelf>, 
IMultiplyOperators<TSelf, TSelf, TSelf>, 
INumberBase<TSelf>, 
ISubtractionOperators<TSelf, TSelf, TSelf>, 
IUnaryNegationOperators<TSelf, TSelf>, 
IUnaryPlusOperators<TSelf, TSelf> where TSelf : INumber<TSelf>?
مطالب
استفاده از عبارات Cron در Quartz.NET
Cron چیست؟
قابلیتی است در سیستم عامل‌های مبتنی بر یونیکس که وظیفه اجرای وظایف در زمانبندی‌های خاص را بر عهده دارد، و به کاربران این امکان را می‌دهد که وظایف را زمانبندی کرده و در دوره‌های مشخص اجرا کنند.
اطلاعات بیشتر

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

  1. اجرای وظیفه ایی خاص هر روز ساعت 8 صبح
  2. اجرای وظیفه ایی خاص هر جمعه آخر ماه
  3. و...

این هفت قسمت توسط فاصله از همدیگر جدا می‌شوند :

عبارات cron خیلی قدرتمتد هستند و در عین حال مقداری پیچیده، هدف از ارائه این مطلب آشنایی با این نوع از عبارات و استفاده از آنها در Quartz.NET می‌باشد.

قبل از هر چیز ایتدا باید با فرمت این عبارات آشنا شویم :


پس عبارات cron در ساده‌ترین حالت می‌توانند به صورت :
* * * * ? *
و در حالت پیچیده‌تر می‌توانند به صورت زیر باشند :
0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010
کارکترهای خاص :
  • * به معنی "تمام حالات" می‌باشد به عنوان مثال در فیلد "دقیقه" به معنی هر دقیقه می‌باشد.
  • ؟ به معنی "نگذاشتن مقداری خاص" می‌باشد، به عنوان مثال می‌خواهیم کار ما در یک روز خاص از ماه اتفاق بیفتد اما مهم نیست چه روزی از هفته باشد.
  • - جهت تعیین یک رنج خاص (محدوده ایی خاص) به عنوان مثال "12-10" در فیلد ساعت به معنی "ساعت هایی 10، 11 و 12" می‌باشد.
  • , جهت تعیین مقادیر اضافی برای مثال "MON,WED,FRI" در فیلد  day-of-week به معنای "روزه‌های دوشنبه، چهارشنبه و جمعه" می‌باشد.
  • / جهت اعمالی increment(کاهشی) استفاده می‌شود به طور مثال "0/15" در فیلد seconds به معنای "ثانیه‌های 0 ، 15 ، 30 ، 45" می‌باشد، "5/15" نیز به معنای "ثانیه‌های 5 ، 20 ، 35 ، 50" می‌باشد، به صورت ساده‌تر به طور مثال اگر مقدار "0/15" را در فیلد "minutes" قرار دهیم به معنی "هر 15 دقیقه است و از دقیقه 0 آغاز میشود" با مثلا "3/20"به معنی "هر 20 دقیقه می‌باشد و از دقیقه سوم آغاز میشود".
  • L "آخرین (Last)" همانطور که از شکل بالا مشخص است تنها در فیلدهایی Day of month و Day of week قابل استفاده می‌باشد به طور مثال اگر در فیلد Day of month استفاده شود به معنای "آخرین روز ماه" می‌باشد.
  • W "روز هفته(Weekday)" 
  • # جهت تعیین Xامین روز ماه به طور مثال مقدار "3#6" به "معنای سومین جمعه ماه" می‌باشد، در واقع مقدار 6 روز و مقدار 3# سومین در ماه می‌باشد.

تعدای مثال :

تولید عبارات cron گاهی اوقات به نظر پیچیده می‌آید(به نظر من که اینطور نیست!) اما برای تولید آسان این عبارات می‌توانید از این سرویس آنلاین استفاده نمائید.

حال یک مثال در این رابطه :

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

public class HelloSchedule : ISchedule
    {
        public void Run()
        {

            IJobDetail job = JobBuilder.Create<HelloJob>()
                                       .WithIdentity("job1")
                                       .Build();

            ITrigger trigger = TriggerBuilder.Create()
                                             .ForJob(job)
                                             .WithIdentity("trigger1")
                                             .StartNow()
                                             .WithCronSchedule("0 0 23 ? * MON-FRI *")
                                             .Build();

            ISchedulerFactory sf = new StdSchedulerFactory();
            IScheduler sc = sf.GetScheduler();
            sc.ScheduleJob(job, trigger);

            sc.Start();
        }
کدهای فوق دقیقا همانند مثالهایی هستند که پیشتر در سایت مشاهده کرده اید با این تفاوت به جای استفاده از متد WithSimpleScheduleاز متد WithCronSchedule استفاده می‌کنیم که پارامتر ورودی آن عبارت cron ما می‌باشد.
نظرات اشتراک‌ها
ارتقا به dotnet core 2
با سلام و احترام
قسمت آخر مقاله یعنی قطعه کد
// Only used by EF Tooling
        public static IWebHost BuildWebHost(string[] args)
        {
            return WebHost.CreateDefaultBuilder()
                .ConfigureAppConfiguration((ctx, cfg) =>
                {
                    cfg.SetBasePath(Directory.GetCurrentDirectory())
                        .AddJsonFile("appsettings.json", true) // require the json file!
                        .AddEnvironmentVariables();
                })
                .ConfigureLogging((ctx, logging) => { }) // No logging
                .UseStartup()
                .UseSetting("DesignTime", "true")
                .Build();
        }
راه حل استانداردی نمیباشد و البته که با این حال نیاز به تکمیل شدن دارد به این شکل که باید در فایل startup.cs از تنظیم اضافه شده یعنی
UseSetting("DesignTime", "true")
جهت جلوگیری از Initial شدن دیتابیس در هنگام استفاده از Entity Framework Core Tooling به طور مثال عملیات Migration استفاده کرد
if (_config["DesignTime"] != "true")
{
  using (var scope = app.ApplicationServices.CreateScope())
  {
    var initializer = scope.ServiceProvider.GetRequiredService<YourSampleDataInitializer>();
    initializer.RunAsync().Wait();
  }
}
در ادامه روش توصیه شده توسط مایکروسافت پیاده سازی اینترفیس جدید IDesignTimeDbContextFactory میباشد که به واسطه آن EF موفق به یافتن Context برنامه در هنگام عملیات Migration میشود.قطعه کد زیر نمونه ای از این پیاده سازی است.
public class BloggingContextFactory : IDesignTimeDbContextFactory<BloggingContext>
    {
        public BloggingContext Create(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
            optionsBuilder.UseSqlite("Data Source=blog.db");

            return new BloggingContext(optionsBuilder.Options);
        }
    }
البته همه این موارد زمانی درست کار خواهد کرد که فایل Program.cs برنامه را نیز به شکل جدید آن یعنی
public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
تغییر دهیم.دلیل آن هم ... EF Core 2.0: design-time DbContext discovery
مطالب
محاسبه NOPS برای سرور شیرپوینت
NOPS یا Normalized User Operations Per Second ، عملیات کاربر نرمال در هر ثانیه است که در سرور‌ها برای محاسبه بار وارده بر سرور محاسبه می‌شود . و تعداد آن ، در حقیقت عددی است که تعداد کل عملکرد‌های کاربران معمولی و نرمال را در یک روز معمولی نشان می‌دهد . برای سرور‌های شیرپوینت نیز محاسبه آن پیشنهاد شده است .
توسط این عدد ، شما می‌توانید تعداد کاربرهای قابل پشتیبانی خود را تخمین زده و سخت افزار مورد نیاز را تهیه کنید
 فرمول کلی و ثابت شده زیر ، برای این منظور استفاده می‌شود : 

موارد فوق تقریبا واضح هستند که فقط 2 مورد از آنها را توضیح می‌دهم :
مورد D یعنی peak Factor ، مقداری بین 1 تا 10 است که برای نشان دادن ساعات پیک در طول ساعت کاری استفاده می‌شود . برای مثال اگر سازمانی از از 9 صبح تا 5 بعد از ظهر کار کند ، می‌توان به جرات گفت که اکثر کامندان روز کاری خود را با باز کردن محیط برنامه شیرپوینت آغاز می‌کنند که یک پیک محسوب می‌شود . سپس دقیقا بعد از نهار ، استفاده در حد پیک می‌شود . مقدار Peak Factor برابر 1 به این معنی است که هیچ بارگذاری پیکی وجود ندارد . و peak factor برابر 10 بیانگر این است که تمام آن، پیک محسوب می‌شود . به طور معمول برای یک سازمان این عدد تا سقف 5 بیان می‌شود . اگر می‌خواهید محافظ کارانه برخورد کنید ، تا سقف 7 محاسبه کنید .
 
مورد بعدی C است که تعداد عملکرد به ازای کاربر فعال در روز است . این مقدار نیز که عددی بین 1 تا 10 است ،  با میزان بهره برداری از محیط شیرپوینت در ارتباط است . مقدار 1 به این معنی است که کاربران شما تقریبا هیچ زمانی را به استفاده از شیرپوینت اختصاص نمی‌دهند و مقدار 10 به معنی این است که کاربران تمام روز خود را مشغول کارکردن با شیرپوینت هستند . برای یک سازمان می‌تواند این مقدار عددی نزدیک به 10 باشد . 
برای اطلاعات بیشتر در این زمینه می‌توانید به Appendix A از کتاب Sharepoint Administration و یا مقاله Calculating Bandwidth Requirements to Support Regional Users  مراجعه نمایید
  
مطالب
نحوه انتقال اطلاعات استخراج شده از وب سرویس به SQL Server به کمک SSIS
ممکن است در مواقعی نیاز به اطلاعات استخراج شده از وب سرویسی داشته باشید که در همان مقطع زمانی به آن دسترسی ندارید . مسلما برای این منظور باید آن اطلاعات را ذخیره کرده تا در صورت نیاز بتوان به آنها رجوع کرد . یکی از راه حل‌ها ذخیره آن در پایگاه داده (در اینجا Sql Server) است که در این پست به کمک امکانات BIDS در پکیج‌های SSIS و کوئری‌های SQL این مشکل را برطرف میکنیم. برای مشاهده نحوه استخراج اطلاعات از وب سرویس به اینجا مراجعه کنید .
تنها تفاوتی که در این پست در کار با سرویس با پست اشاره شده در بالا وحود دارد ذخیره اطلاعات استخراج شده است که در آن پست در یک فایل xml ذخیره شدند ولی در اینجا ما نیاز داریم تا اطلاعات را در یک متغیر با حوزه کاری Package ذخیره کنیم (به این معنی که مختص به همان flow نباشد و در تمام پکیج دیده شود) 

به دلیل اینکه هدر xml خروجی از سرویس دارای چندین namespace هست هنگام کار با آنها به مشکل خواهیم خورد. (هم هنگام کار با xml task‌ها و هم هنگام کار با xml در sql) 

به همین دلیل باید این قسمت از محتوا را حذف کرد . برای همین پس از گرفتن اطلاعات از سرویس آن را به کمک یک Script task حذف می‌کنیم 

در این مرحله اطلاعات استخراج شده را باید در SQL درج کنیم . برای همین ساختاری که باید اطلاعات را در SQL نگه دارد را در دیتابیس ایجاد می‌کنیم : 

جدول person برای نگهداری اطلاعات سرویس و XmlContainer برای نگهداری xml‌های سرویس .(برای داشتن History) 

برای درج هم از SP استفاده می‌کنیم :

 

و پیکربندی SQL Task :
 

نکته اول ایجاد کانکشن به پایگاه داده است که در اینجا از نمایش جزییات آن صرف نظر شده است . نکته دیگری پارامتر SP است که چون یک پارامتر دارد یک علامت سوال قرار میگیرد . اگر چند پارامتر بود به صورت علامت‌های سوال با ویرگول از هم جدا می‌شوند . ?,?,?,?
سپس در بخش parameter mapping به ترتیب مقدار دهی می‌شوند : 

و مقدار خروجی SP که شناسه آیتم درج شده است را در یک متغیر نگهداری می‌کنیم :
 

برای قدم بعد می‌خواهیم اطلاعات موجود در XML استخراج شده در پایگاه داده را در جدول مربوطه ذخیره کنیم . برای این کار این SP را می‌نویسیم : 
 

نکات مهم موارد زیر هستند : 
1 - استفاده از OpenXml برای parse کردن xml 
2 - استفاده از sp سیستمی sp_xml_preparedocument و sp_xml_removedocument (بیشتر ) 
3- اطلاعات شناسه و نام و نام خانوادگی inner text‌های نود‌های xml هستند . اگر این موارد به صورت attribute باشند باید قبل از نام آنها علامت @ قرار بگیرند. 
پس از ایجاد این Sp باید آن را در package فراخوانی کنیم : 

و پیکربندی این SQL Task :
 

و پارامتر‌های این SP :

و ذخیره نتیجه تراکنش در متغیری در پکیج :
 

و اکنون پکیج ما ظاهری شبیه به این مورد خواهد داشت :
 

 

در نهایت به عنوان یک facility می‌توانیم وضعیت تراکنش را به کاربر نمایش دهیم ( به کمک Script Task ) : 

و تمام ...

 

موفق باشید 

مطالب
آموزش ساخت و کار با subdomain در حالت لوکال هاست

برای پروژه ای نیاز بود تا از sub-domain استفاده شود. نحوه کار بدین صورت بود که اطلاعات باید بر اساس شهر‌ها بارگذاری می‌شد؛ برای مثال اگر کاربر آدرس tehran.mysite.com را وارد کرد، اطلاعات مربوط به شهر تهران بارگذاری شود و برای سایر شهر‌ها هم به همین ترتیب. در سرور که روش کار و ساخت ساب دومین کاملا مشخص است. اما برای لوکال باید به چه ترتیب عمل شود؟

مسلما برنامه پیش از آپلود حتما باید روی لوکال تست شود. برای ساخت ساب دومین روی لوکال هاست کافی ست به مسیر زیر رفته و فایل hosts را با دسترسی administrator  باز کنید . 

[windows drive]:\Windows\System32\drivers\etc
سپس به شیوه زیر ساب دومین‌های خود را در این فایل وارد کنید .
# localhost name resolution is handled within DNS itself.
127.0.0.1  localhost
127.0.0.1 tehran.localhost
127.0.0.1 mashhad.localhost
127.0.0.1 sabzevar.localhost
خوب، از این به بعد در صورتیکه کاربر هر کدام از ساب دومین‌های وارد شده در این فایل را در مرورگر خود وارد کند، به راحتی می‌توان در رویداد Application_BeginRequest در فایل Gloabal.asax  مسیر url را بدست آورد و کاربر را به مسیر دلخواه هدایت کرد.
نکته مهمی که باید حتما توجه شود استفاده از دستور Request.Url.ToString(); است. هنگامیکه برنامه روی هاست در حال اجراست، قادر به برگرداندن مسیر به همراه با نام ساب دومین می‌باشد، اما این دستور روی لوکال، نام ساب دومین را برنمی‌گرداند! برای این کار روی لوکال استفاده از روش زیر استفاده کنید:
host = Request.Headers["HOST"].ToLower();
اکنون بعد از به دست آوردن مسیر کاربر می‌توانیم او را به مسیر دلخواه بفرستیم. مثلا من ابتدا با به دست آوردن نام شهر از روی url کاربر، کد متناظر با شهر را پیدا کرده و کد را به همراه آدرس به صفحه مورد نظر فرستاده و در آن صفحه با دریافت کد شهر، اطلاعات لازم را از دیتابیس فراخوانی می‌کنم:
void Application_BeginRequest(object sender, EventArgs e)
    {
        string host, url, newUrl;
        host = Request.Headers["HOST"].ToLower();   //for localhost
        //host = Request.Url.ToString();  //  for server
        url = Request.Url.PathAndQuery.ToLower();
        
        if (host.EndsWith(".localhost:8001") && (url == "" || url == "/"))
        {
            int cityId = City.GetCityIdByCityName(host.Replace(".localhost:8001", ""));
            newUrl = string.Format("~/postlist/{0}/", cityId);
            HttpContext.Current.RewritePath(newUrl);
            return;
        }

    }

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

موفق باشید.