نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 11 - بررسی بهبودهای Razor
یک نکته‌ی تکمیلی: دریافت خطای «Error RZ3007: Targeted tag name cannot be null or whitespace» پس از نصب SDK جدید

اگر SDK جدیدی را نصب کنید و بلافاصله سعی در اجرای برنامه‌ای که با SDK قبلی کامپایل شده‌است داشته باشید، ممکن است خطای فوق را دریافت کنید. برای رفع آن، ابتدای پوشه‌های bin و obj را حذف کنید. سپس دستور dotnet restore را صادر کنید. اکنون برنامه بدون مشکل اجرا می‌شود.
نظرات اشتراک‌ها
دوراهی انتخاب NHibernate و Entityframework
استفاده از NH در مقابل EF Code first سورس باز اشتباه است؛ به دلایل زیر:
- توهم پشتیبانی از بانک‌های اطلاعاتی مختلف توسط NH . به شخصه با حداقل یک مورد نیم بند آن سروکار داشتم: SQL-CE. تقریبا بیشتر از نیمی از توانایی‌های این بانک اطلاعاتی در NH لحاظ نشده و حتی یک کوئری ساده از تاریخ تا تاریخ را توسط آن نمی‌توانید تهیه کنید. این مورد برعکس EF Code first است. کامل و بی‌نقص کار می‌کند. کلا تمام محصولات مایکروسافت به همین نحو هستند. اگر عنوان می‌کنند این محصول دو قابلیت دارد، واقعا این دو قابلیت، درست کار می‌کنند. نه اینکه عنوان کنند 100 قابلیت را ارائه می‌دهیم و فقط 10 تای آن کامل پیاده سازی شده باشند.
- تیم مدیریتی به شدت مغرور و ناراحت NH. باز هم برای این تیم ناراحت، جهت تکمیل نقایص کار با SQL-CE بیشتر از یکسال قبل وصله‌ای رو ارسال کردم. تا به امروز حتی یک پاسخ که آیا خوبه، بده ... ارسال نشده. با اکثر همکاری‌ها هم به همین نحو رفتار می‌کنند.
خلاصه حال و هوای یک پروژه سالم سورس باز را ندارد.
- پس از ارائه EF Code first این پروژه تقریبا غیرفعال شده: (^)
- نیم بند بودن پشتیبانی از LINQ. باز هم اگر تصور می‌کنید که راحت می‌تونید مثل EF از کوئری‌های LINQ در اینجا استفاده کنید، سخت در اشتباه هستید.
- دیر به روز شدن کتابخانه‌های جانبی آن. این مساله هم به مدیریت بد پروژه NH بر می‌گردد. شاید بیشتر از 10 مورد افزونه برای NH هست، مانند کش و اعتبار سنجی و غیره. اما ... صاحبان آن مشخص نیستند! امروز NH3 ارائه می‌شود، سه ماه بعد کتابخانه اعتبارسنجی متناظر با آن. تیم NH هم حاضر نشده تمام این‌ها رو کنار هم قرار بده و یک کار یکپارچه رو ارائه کنه. NH اعتبار خودش رو از این افزونه‌های موجود در NH Contrib کسب می‌کنه، اما حاضر نیستند مدیریت و نگهداری یکپارچه آن‌ها را قبول کنند.
- ناسازگاری با اکوسیستم دات نت. اگر از NH استفاده کنید مدام در حال جنگ با دات نت هستید. مثلا سیستم اعتبار سنجی EF با سیستم اعتبار سنجی سمت کلاینت و سرور MVC یکپارچه است. با NH اینطور نیست و از این نوع مثال‌ها زیاد است. همین مساله حجم کاری شما را چندبرابر می‌کند.
- طراحی زمخت NH در مقابل طراحی روان EF. برای مثال در EF Code first شما به ندرت نیاز خواهید داشت که نگاشت‌ها را تعریف یا حتی سفارشی سازی کنید. یک سری پیش فرض بسیار عالی در آن به صورت توکار (و نه به شکل افزونه) وجود دارد که حجم کاری شما را به شدت کاهش می‌دهند. در NH کتابخانه fluent nh سعی کرده که اینکار را انجام دهد اما جالب اینجا است که این کتابخانه از طرف تیم اصلی NH اصلا تحویل گرفته نشده و ... دست آخر هم یک روش دیگر را برای نوشتن نگاشت‌ها با کد تهیه کردند.
- مستندات NH کامل نیست. برای مثال شاید یک سری بلاگ‌های متفرقه را پیدا کنید که در مورد روش تهیه نگاشت‌ها با کد مطلب نوشته باشند ... نه توسط کسانی که این کتابخانه را تهیه کرده‌اند! این روند رو مقایسه کنید با وبلاگ EF مثلا : (^) این بلاگ تا این حد کامل است که مرجع بسیاری از مطالب آموزشی و کتاب‌های مرتبط با EF می‌باشد.
- سیستم migration موجود در EF Code first نسبت به نمونه NH بسیار کاملتر است؛ با قابلیت سفارشی سازی، مقایسه هش مدل‌ها با جداول جهت جلوگیری از تداخلات و اشتباهات، تولید اسکریپت آپدیت بانک اطلاعاتی و غیره.

یک زمانی بود دات نت ORM قابل ملاحظه‌ای نداشت. زمان دات نت2. در آن موقع NH حرف برای گفتن داشت اما ... نه الان.
مطالب
معماری لایه بندی نرم افزار #4

UI

در نهایت نوبت به طراحی و کدنویسی UI می‌رسد تا بتوانیم محصولات را به کاربر نمایش دهیم. اما قبل از شروع باید موضوعی را یادآوری کنم. اگر به یاد داشته باشید، در کلاس ProductService موجود در لایه‌ی Domain، از طریق یکی از روشهای الگوی Dependency Injection به نام Constructor Injection، فیلدی از نوع IProductRepository را مقداردهی نمودیم. حال زمانی که بخواهیم نمونه ای را از ProductService ایجاد نماییم، باید به عنوان پارامتر ورودی سازنده، شی ایجاد شده از جنس کلاس ProductRepository موجود در لایه Repository را به آن ارسال نماییم. اما از آنجایی که می‌خواهیم تفکیک پذیری لایه‌ها از بین نرود و UI بسته به نیاز خود، نمونه مورد نیاز را ایجاد نموده و به این کلاس ارسال کند، از ابزارهایی برای این منظور استفاده می‌کنیم. یکی از این ابزارها StructureMap می‌باشد که یک Inversion of Control Container یا به اختصار IoC Container نامیده می‌شود. با Inversion of Control در مباحث بعدی بیشتر آشنا خواهید شد. StructureMap ابزاری است که در زمان اجرا، پارامترهای ورودی سازنده‌ی کلاسهایی را که از الگوی Dependency Injection استفاده نموده اند و قطعا پارامتر ورودی آنها از جنس یک Interface می‌باشد را، با ایجاد شی مناسب مقداردهی می‌نماید.

به منظور استفاده از StructureMap در Visual Studio 2012 باید بر روی پروژه UI خود کلیک راست نموده و گزینه‌ی Manage NuGet Packages را انتخاب نمایید. در پنجره ظاهر شده و از سمت چپ گزینه‌ی Online و سپس در کادر جستجوی سمت راست و بالای پنجره واژه‌ی structuremap را جستجو کنید. توجه داشته باشید که باید به اینترنت متصل باشید تا بتوانید Package مورد نظر را نصب نمایید. پس از پایان عمل جستجو، در کادر میانی structuremap ظاهر می‌شود که می‌توانید با انتخاب آن و فشردن کلید Install آن را بر روی پروژه نصب نمایید.

جهت آشنایی بیشتر با NuGet و نصب آن در سایر نسخه‌های Visual Studio می‌توانید به لینک‌های زیر رجوع کنید:

1. آشنایی با  NuGetقسمت اول

2. آشنایی با  NuGetقسمت دوم

3. Installing NuGet

کلاسی با نام BootStrapper را با کد زیر به پروژه UI خود اضافه کنید:

using StructureMap;
using StructureMap.Configuration.DSL;
using SoCPatterns.Layered.Repository;
using SoCPatterns.Layered.Model;

namespace SoCPatterns.Layered.WebUI
{
    public class BootStrapper
    {
        public static void ConfigureStructureMap()
        {
            ObjectFactory.Initialize(x => x.AddRegistry<ProductRegistry>());
        }
    }
    public class ProductRegistry : Registry
    {
        public ProductRegistry()
        {
            For<IProductRepository>().Use<ProductRepository>();
        }
    }
}

ممکن است یک WinUI ایجاد کرده باشید که در این صورت به جای فضای نام SoCPatterns.Layered.WebUI از SoCPatterns.Layered.WinUI استفاده نمایید.

هدف کلاس BootStrapper این است که تمامی وابستگی‌ها را توسط StructureMap در سیستم Register نماید. زمانی که کدهای کلاینت می‌خواهند به یک کلاس از طریق StructureMap دسترسی داشته باشند، Structuremap وابستگی‌های آن کلاس را تشخیص داده و بصورت خودکار پیاده سازی واقعی (Concrete Implementation) آن کلاس را، براساس همان چیزی که ما برایش تعیین کردیم، به کلاس تزریق می‌نماید. متد ConfigureStructureMap باید در همان لحظه ای که Application آغاز به کار می‌کند فراخوانی و اجرا شود. با توجه به نوع UI خود یکی از روالهای زیر را انجام دهید:

در WebUI:

فایل Global.asax را به پروژه اضافه کنید و کد آن را بصورت زیر تغییر دهید:

namespace SoCPatterns.Layered.WebUI
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            BootStrapper.ConfigureStructureMap();
        }
    }
}

در WinUI:

در فایل Program.cs کد زیر را اضافه کنید:

namespace SoCPatterns.Layered.WinUI
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            BootStrapper.ConfigureStructureMap();
            Application.Run(new Form1());
        }
    }
}

سپس برای طراحی رابط کاربری، با توجه به نوع UI خود یکی از روالهای زیر را انجام دهید:

در WebUI:

صفحه Default.aspx را باز نموده و کد زیر را به آن اضافه کنید:

<asp:DropDownList AutoPostBack="true" ID="ddlCustomerType" runat="server">
    <asp:ListItem Value="0">Standard</asp:ListItem>
    <asp:ListItem Value="1">Trade</asp:ListItem>
</asp:DropDownList>
<asp:Label ID="lblErrorMessage" runat="server" ></asp:Label>
<asp:Repeater ID="rptProducts" runat="server" >
    <HeaderTemplate>
        <table>
            <tr>
                <td>Name</td>
                <td>RRP</td>
                <td>Selling Price</td>
                <td>Discount</td>
                <td>Savings</td>
            </tr>
            <tr>
                <td colspan="5"><hr /></td>
            </tr>
    </HeaderTemplate>
    <ItemTemplate>
            <tr>
                <td><%# Eval("Name") %></td>
                <td><%# Eval("RRP")%></td>
                <td><%# Eval("SellingPrice") %></td>
                <td><%# Eval("Discount") %></td>
                <td><%# Eval("Savings") %></td>
            </tr>
    </ItemTemplate>
    <FooterTemplate>
        </table>
    </FooterTemplate>
</asp:Repeater>

در WinUI:

فایل Form1.Designer.cs را باز نموده و کد آن را بصورت زیر تغییر دهید:

#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
    this.cmbCustomerType = new System.Windows.Forms.ComboBox();
    this.dgvProducts = new System.Windows.Forms.DataGridView();
    this.colName = new System.Windows.Forms.DataGridViewTextBoxColumn();
    this.colRrp = new System.Windows.Forms.DataGridViewTextBoxColumn();
    this.colSellingPrice = new System.Windows.Forms.DataGridViewTextBoxColumn();
    this.colDiscount = new System.Windows.Forms.DataGridViewTextBoxColumn();
    this.colSavings = new System.Windows.Forms.DataGridViewTextBoxColumn();
    ((System.ComponentModel.ISupportInitialize)(this.dgvProducts)).BeginInit();
    this.SuspendLayout();
    //
    // cmbCustomerType
    //
    this.cmbCustomerType.DropDownStyle =                                                                                                                                                                             System.Windows.Forms.ComboBoxStyle.DropDownList;
    this.cmbCustomerType.FormattingEnabled = true;
    this.cmbCustomerType.Items.AddRange(new object[] {
        "Standard",
        "Trade"});
    this.cmbCustomerType.Location = new System.Drawing.Point(12, 90);
    this.cmbCustomerType.Name = "cmbCustomerType";
    this.cmbCustomerType.Size = new System.Drawing.Size(121, 21);
    this.cmbCustomerType.TabIndex = 3;
    //
    // dgvProducts
    //
    this.dgvProducts.ColumnHeadersHeightSizeMode =     System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
    this.dgvProducts.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
    this.colName,
    this.colRrp,
    this.colSellingPrice,
    this.colDiscount,
    this.colSavings});
    this.dgvProducts.Location = new System.Drawing.Point(12, 117);
    this.dgvProducts.Name = "dgvProducts";
    this.dgvProducts.Size = new System.Drawing.Size(561, 206);
    this.dgvProducts.TabIndex = 2;
    //
    // colName
    //
    this.colName.DataPropertyName = "Name";
    this.colName.HeaderText = "Product Name";
    this.colName.Name = "colName";
    this.colName.ReadOnly = true;
    //
    // colRrp
    //
    this.colRrp.DataPropertyName = "Rrp";
    this.colRrp.HeaderText = "RRP";
    this.colRrp.Name = "colRrp";
    this.colRrp.ReadOnly = true;
    //
    // colSellingPrice
    //
    this.colSellingPrice.DataPropertyName = "SellingPrice";
    this.colSellingPrice.HeaderText = "Selling Price";
    this.colSellingPrice.Name = "colSellingPrice";
    this.colSellingPrice.ReadOnly = true;
    //
    // colDiscount
    //
    this.colDiscount.DataPropertyName = "Discount";
    this.colDiscount.HeaderText = "Discount";
    this.colDiscount.Name = "colDiscount";
    //
    // colSavings
    //
    this.colSavings.DataPropertyName = "Savings";
    this.colSavings.HeaderText = "Savings";
    this.colSavings.Name = "colSavings";
    this.colSavings.ReadOnly = true;
    //
    // Form1
    //
    this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    this.ClientSize = new System.Drawing.Size(589, 338);
    this.Controls.Add(this.cmbCustomerType);
    this.Controls.Add(this.dgvProducts);
    this.Name = "Form1";
    this.Text = "Form1";
    ((System.ComponentModel.ISupportInitialize)(this.dgvProducts)).EndInit();
    this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.ComboBox cmbCustomerType;
private System.Windows.Forms.DataGridView dgvProducts;
private System.Windows.Forms.DataGridViewTextBoxColumn colName;
private System.Windows.Forms.DataGridViewTextBoxColumn colRrp;
private System.Windows.Forms.DataGridViewTextBoxColumn colSellingPrice;
private System.Windows.Forms.DataGridViewTextBoxColumn colDiscount;
private System.Windows.Forms.DataGridViewTextBoxColumn colSavings;

سپس در Code Behind، با توجه به نوع UI خود یکی از روالهای زیر را انجام دهید:

در WebUI:

وارد کد نویسی صفحه Default.aspx شده و کد آن را بصورت زیر تغییر دهید:

using System;
using System.Collections.Generic;
using SoCPatterns.Layered.Model;
using SoCPatterns.Layered.Presentation;
using SoCPatterns.Layered.Service;
using StructureMap;

namespace SoCPatterns.Layered.WebUI
{
    public partial class Default : System.Web.UI.Page, IProductListView
    {
        private ProductListPresenter _productListPresenter;
        protected void Page_Init(object sender, EventArgs e)
        {
            _productListPresenter = new ProductListPresenter(this,ObjectFactory.GetInstance<Service.ProductService>());
            this.ddlCustomerType.SelectedIndexChanged +=
                delegate { _productListPresenter.Display(); };
        }
        protected void Page_Load(object sender, EventArgs e)
        {
            if(!Page.IsPostBack)
                _productListPresenter.Display();
        }
        public void Display(IList<ProductViewModel> products)
        {
            rptProducts.DataSource = products;
            rptProducts.DataBind();
        }
        public CustomerType CustomerType
        {
            get { return (CustomerType) int.Parse(ddlCustomerType.SelectedValue); }
        }
        public string ErrorMessage
        {
            set
            {
                lblErrorMessage.Text =
                    String.Format("<p><strong>Error:</strong><br/>{0}</p>", value);
            }
        }
    }
}

در WinUI:

وارد کدنویسی Form1 شوید و کد آن را بصورت زیر تغییر دهید:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using SoCPatterns.Layered.Model;
using SoCPatterns.Layered.Presentation;
using SoCPatterns.Layered.Service;
using StructureMap;

namespace SoCPatterns.Layered.WinUI
{
    public partial class Form1 : Form, IProductListView
    {
        private ProductListPresenter _productListPresenter;
        public Form1()
        {
            InitializeComponent();
            _productListPresenter =
                new ProductListPresenter(this, ObjectFactory.GetInstance<Service.ProductService>());
            this.cmbCustomerType.SelectedIndexChanged +=
                delegate { _productListPresenter.Display(); };
            dgvProducts.AutoGenerateColumns = false;
            cmbCustomerType.SelectedIndex = 0;
        }
        public void Display(IList<ProductViewModel> products)
        {
            dgvProducts.DataSource = products;
        }
        public CustomerType CustomerType
        {
            get { return (CustomerType)cmbCustomerType.SelectedIndex; }
        }
        public string ErrorMessage
        {
            set
            {
                MessageBox.Show(
                    String.Format("Error:{0}{1}", Environment.NewLine, value));
            }
        }
    }
}

با توجه به کد فوق، نمونه ای را از کلاس ProductListPresenter، در لحظه‌ی نمونه سازی اولیه‌ی کلاس UI، ایجاد نمودیم. با استفاده از متد ObjectFactory.GetInstance مربوط به StructureMap، نمونه ای از کلاس ProductService ایجاد شده است و به سازنده‌ی کلاس ProductListPresenter ارسال گردیده است. در مورد Structuremap در مباحث بعدی با جزئیات بیشتری صحبت خواهم کرد. پیاده سازی معماری لایه بندی در اینجا به پایان رسید.

اما اصلا نگران نباشید، شما فقط پرواز کوتاه و مختصری را بر فراز کدهای معماری لایه بندی داشته اید که این فقط یک دید کلی را به شما در مورد این معماری داده است. این معماری هنوز جای زیادی برای کار دارد، اما در حال حاضر شما یک Applicaion با پیوند ضعیف (Loosely Coupled) بین لایه‌ها دارید که دارای قابلیت تست پذیری قوی، نگهداری و پشتیبانی آسان و تفکیک پذیری قدرتمند بین اجزای آن می‌باشد. شکل زیر تعامل بین لایه‌ها و وظایف هر یک از آنها را نمایش می‌دهد.

اشتراک‌ها
bfalt، کتابخانه‌ای برای C# با خاصیت‌های Go

این کتابخانه ترکیبی از Roslyn و NativeAOT است. همچنین  در متن توضیحات این مخزن اینطور آمده است: «همان c# که میشناسید اما با ابزارهایی الهام گرفته شده از Go»

 هر چند استفاده از این کتابخانه ممکن است در برنامه کاری ما نباشد، اما ایده و سورس کد این مخزن، بسیار جالب است.

bfalt، کتابخانه‌ای برای C# با خاصیت‌های Go
نظرات مطالب
VS Code برای توسعه دهندگان ASP.NET Core - قسمت دوم - ایجاد و اجرای اولین برنامه
دستور dotnet run مطابق مستندات آن وابسته‌است به دستور dotnet build. بنابراین با فراخوانی آن نیازی به اجرای صریح دستور dotnet build نیست.
 The command depends on the dotnet build command to build the code.
البته اگر وابستگی‌های پروژه دریافت نشده‌اند (عدم انتخاب گزینه‌ی restore در تصاویر فوق)، با دستور dotnet restore هم می‌توان همان کار بازیابی را انجام داد.
مطالب
فشرده سازی با فرمت 7z

جی‌میل هر ایمیلی را که به همراه آن یک فایل اجرایی پیوست شده باشد برگشت می‌زند. Zip‌ کردن آن هم فایده ندارد چون محتویات فایل‌های zip را هم بررسی می‌کند! فقط به نظر فرمت rar و همچنین 7z را بررسی نمی‌کند (احتمالا با مجوز آن مشکل دارد).
قوی‌ترین برنامه سورس بازی که این فرمت را پشتیبانی می‌کند، برنامه 7zip است و خوشبختانه محصور کننده‌هایی نیز جهت کار با کتابخانه‌های این برنامه برای دات نت فریم ورک موجود است. برای مثال:


مزیت استفاده از این کتابخانه این است که اغلب فرمت‌های پر کاربرد را نیز پشتیبانی می‌کند (شامل zip ، gz ، rar و ...).
برای استفاده از آن به فایل‌های 7z.dll و SevenZipSharp.dll نیاز خواهید داشت. 7z.dll از برنامه 7zip گرفته شده و SevenZipSharp.dll هم محصور کننده دات نتی آن است.

مثالی در مورد فشرده سازی با فرمت 7z با کمک کتابخانه‌های نامبرده شده:

using SevenZip;
using System.Windows.Forms;
using System;

class C7Z
{
public static void Compress7Z(string filePath, string outPath)
{
SevenZipCompressor.SetLibraryPath(String.Format(@"{0}\7z.dll", Application.StartupPath));
SevenZipCompressor cmp = new SevenZipCompressor
{
ArchiveFormat = OutArchiveFormat.SevenZip,
CompressionMethod = CompressionMethod.Lzma,
CompressionMode = CompressionMode.Create,
CompressionLevel = CompressionLevel.High,
VolumeSize = 0
};
cmp.CompressFiles(outPath, filePath);
}

}

C7Z.Compress7Z(@"C:\test\test.txt", @"C:\test\test.7z");
مثال‌های بیشتری را با دریافت سورس SevenZipSharp می‌توانید مشاهده کنید.

مطالب
آزمون‌های یکپارچگی در برنامه‌های ASP.NET Core
تا اینجا دو روش را برای آزمایش کلی یک سیستم Web API به همراه تمام زیر ساخت‌های آن، بررسی کردیم:
- استفاده از Postman برای آزمایش یک برنامه‌ی Web API
- استفاده از strest برای آزمایش یک برنامه‌ی Web API

روش سومی هم برای انجام اینکار وجود دارد که به صورت توکار از زمان ارائه‌ی ASP.NET Core 2.1 به همراه TestServer آزمایشی آن میسر شد. این روش در نگارش 3.1، با تغییر روش تعریف فایل program.cs، جهت سازگاری آن با آزمون‌های یکپارچگی/آزمایش کل سیستم، بهبود یافته‌است که خلاصه‌ای از آن را در این مطلب بررسی می‌کنیم.


آزمون‌های یکپارچگی در ASP.NET Core

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

برای ایجاد آزمون‌های یکپارچگی در برنامه‌های ASP.NET Core، حداقل سه مرحله باید طی شوند:
الف) ایجاد یک class library که ارجاعی را به پروژه‌ی اصلی دارد. این پروژه حاوی آزمایش‌های ما خواهد بود.
ب) راه اندازی یک هاست وب آزمایشی برای ارسال درخواست‌ها به آن و دریافت پاسخ‌های نهایی.
ج) استفاده از یک test runner (انواع و اقسام فریم ورک‌های unit testing) برای اجرای آزمایش‌ها


ایجاد یک پروژه‌ی کتابخانه برای هاست و اجرای آزمایش‌های یکپارچگی

فرض کنید می‌خواهیم برای همان پروژه‌ی ایجاد JWTها، آزمایش یکپارچگی بنویسیم. پس از ایجاد یک پروژه‌ی کتابخانه‌ی جدید که قرار است هاست آزمایش‌های ما شود، نیاز است محتوای فایل csproj آن‌را به صورت زیر تغییر داد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <NoWarn>RCS1090</NoWarn>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\ASPNETCore2JwtAuthentication.WebApp\ASPNETCore2JwtAuthentication.WebApp.csproj" />
  </ItemGroup>
  <ItemGroup>
    <None Include="..\ASPNETCore2JwtAuthentication.WebApp\appsettings.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>
  <ItemGroup>
    <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c329}" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="fluentassertions" Version="5.10.3" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
    <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
    <PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
  </ItemGroup>
</Project>
در اینجا، این نکات قابل مشاهده هستند:
1) TargetFramework آن باید به netcoreapp تنظیم شود.
2) باید ارجاع مستقیمی به کل پروژه‌ی نهایی WebApp در آن وجود داشته باشد. چون در ادامه می‌خواهیم فایل Program.cs آن‌را برای راه اندازی یک هاست وب آزمایشی، فراخوانی کنیم.
3) بسته‌ی نیوگتی که کار راه اندازی هاست وب آزمایشی را انجام می‌دهد، Microsoft.AspNetCore.Mvc.Testing نام دارد. این بسته، کار کپی فایل‌های پروژه‌ی اصلی و همچنین تنظیم مسیر پروژه را به این مسیر جدید نیز انجام می‌دهد.
4) روش افزودن بسته‌های MSTest را مشاهده می‌کنید.
5) همچنین جهت ساده‌تر شدن بررسی نتایج آزمون‌های انجام شده می‌توان از fluentassertions نیز استفاده کرد.


راه اندازی هاست وب آزمایشی جهت انجام آزمون‌های واحد

پس از انجام تنظیمات ابتدایی پروژه‌ی آزمون یکپارچگی، نیاز است یک WebApplicationFactory سفارشی را ایجاد کرد:
using ASPNETCore2JwtAuthentication.WebApp;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    public class CustomWebApplicationFactory : WebApplicationFactory<Program>
    {
        protected override IWebHostBuilder CreateWebHostBuilder()
        {
            var builder = base.CreateWebHostBuilder();
            builder.ConfigureLogging(logging =>
            {
                //TODO: ...
            });
            return builder;
        }

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureTestServices(services =>
            {
                // Don't run `IHostedService`s when running as a test
                services.RemoveAll(typeof(IHostedService));
            });
        }
    }
}
در این تعریف، Program در <WebApplicationFactory<Program، دقیقا به همان کلاس Program برنامه‌ی اصلی وب اشاره می‌کند. به همین جهت امضای این کلاس در نگارش 3.1 تغییر کرده‌است تا با WebApplicationFactory بسته‌ی Microsoft.AspNetCore.Mvc.Testing هماهنگ شود.
در ادامه روش سفارشی سازی WebApplicationFactory  را مشاهده می‌کنید. برای مثال اگر خواستید سرویس‌ها و تنظیمات پیش‌فرض برنامه‌ی اصلی را تغییر دهید می‌توانید متد CreateWebHostBuilder را بازنویسی کنید و یا اگر خواستید سرویس جدیدی را اضافه و یا حذف کنید، می‌توان متد ConfigureWebHost را بازنویسی کرد.


استفاده از WebApplicationFactory سفارشی، جهت ایجاد یک HttpClient

هدف اصلی از ایجاد CustomWebApplicationFactory نه فقط راه اندازی یک هاست وب سفارشی است، بلکه توسط متد CreateClient آن می‌توان به یک HttpClient دسترسی یافت که قابلیت ارسال اطلاعات را به برنامه‌ی وبی که در پشت صحنه راه اندازی می‌شود، دارا است. کار CustomWebApplicationFactory شبیه به راه اندازی dotnet run در پشت صحنه‌است. در اینجا دیگر نیازی نیست تا اینکار را به صورت دستی انجام داد. به همین جهت چون برنامه‌ی وب اصلی به نحو متداولی در پشت صحنه اجرا می‌شود، عموما راه اندازی آن که شامل تنظیمات اولیه و یا حتی ایجاد بانک اطلاعاتی است، کمی کند است و اگر قرار باشد هربار اینکار صورت گیرد، به آزمون‌های بسیار کندی خواهیم رسید. به همین جهت می‌توان یک کلاس singleton را برای مدیریت تک وهله‌ی نهایی HttpClient آن به صورت زیر ایجاد کرد:
using System;
using System.Threading;
using System.Net.Http;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    public static class TestsHttpClient
    {
        private static readonly Lazy<HttpClient> _serviceProviderBuilder =
                new Lazy<HttpClient>(getHttpClient, LazyThreadSafetyMode.ExecutionAndPublication);

        /// <summary>
        /// A lazy loaded thread-safe singleton
        /// </summary>
        public static HttpClient Instance { get; } = _serviceProviderBuilder.Value;

        private static HttpClient getHttpClient()
        {
            var services = new CustomWebApplicationFactory();
            return services.CreateClient(); //NOTE: This action is very time consuming, so it should be defined as a singleton.
        }
    }
}
مزیت کار با این کلاس، عدم راه اندازی مجدد برنامه به ازای هر آزمون انجام شده‌است. چون به ازای هر آزمونی که خواهیم نوشت، نیاز است به HttpClient دسترسی داشته باشیم.


نوشتن اولین آزمون یکپارچگی

پس از تنظیم هاست وب آزمایشی و ایجاد یک HttpClient از پیش تنظیم شده که به آن اشاره می‌کند، اکنون می‌توان اولین آزمون یکپارچگی را به صورت زیر نوشت:
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    [TestClass]
    public class JwtTests
    {
        [TestMethod]
        public async Task TestLoginWorks()
        {
            // Arrange
            var client = TestsHttpClient.Instance;

            // Act
            var token = await doLoginAsync(client);

            // Assert
            token.Should().NotBeNull();
            token.AccessToken.Should().NotBeNullOrEmpty();
            token.RefreshToken.Should().NotBeNullOrEmpty();
        }

        [TestMethod]
        public async Task TestCallProtectedApiWorks()
        {
            // Arrange
            var client = TestsHttpClient.Instance;

            // Act
            var token = await doLoginAsync(client);

            // Assert
            token.Should().NotBeNull();
            token.AccessToken.Should().NotBeNullOrEmpty();
            token.RefreshToken.Should().NotBeNullOrEmpty();

            // Act
            const string protectedApiUrl = "/api/MyProtectedApi";
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
            var response = await client.GetAsync(protectedApiUrl);
            response.EnsureSuccessStatusCode();

            // Assert
            var responseString = await response.Content.ReadAsStringAsync();
            responseString.Should().NotBeNullOrEmpty();
            var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
            var apiResponse = JsonSerializer.Deserialize<MyProtectedApiResponse>(responseString, options);
            apiResponse.Title.Should().NotBeNullOrEmpty();
            apiResponse.Title.Should().Be("Hello from My Protected Controller! [Authorize]");
        }

        private static async Task<Token> doLoginAsync(HttpClient client)
        {
            const string loginUrl = "/api/account/login";
            var user = new { Username = "Vahid", Password = "1234" };
            var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, loginUrl)
            {
                Content = new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json")
            });
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            responseString.Should().NotBeNullOrEmpty();
            return JsonSerializer.Deserialize<Token>(responseString);
        }
    }
}
توضیحات:
- در هر آزمونی نیاز است در ابتدا به TestsHttpClient.Instance، که همان HttpClient ساخته شده‌ی توسط CustomWebApplicationFactory است، دسترسی یافت و همانطور که عنوان شد، دسترسی به وهله‌ای از HttpClient که به هاست وب آزمایشی برنامه‌ی اصلی اشاره می‌کند، عموما بسیار زمانبراست و برای مثال در دو آزمایش نوشته شده‌ی در اینجا اگر قرا باشد هربار اینکار از صفر انجام شود، زمان به اتمام رسیدن این آزمایش‌ها بسیار طولانی خواهد شد. به همین جهت طول عمر TestsHttpClient را singleton تعریف کردیم تا فقط یکبار کار برپایی وب سرور آزمایشی در پشت صحنه انجام شود.
- سپس مابقی کار، همان روش استاندارد کار با HttpClient است. در ابتدا درخواستی را به سمت سرور آزمایشی که در پشت صحنه در حال اجرا است، ارسال می‌کنیم. چون HttpClient دریافتی توسط  CustomWebApplicationFactory تنظیم شده‌است، دیگر نیازی به ذکر آدرس پایه‌ی وب سایت مانند https://localhost:5001 نیست و آدرس‌های ذکر شده‌ی در اینجا، نسبی هستند. سپس محتوای Response دریافتی از سرور را جهت تکمیل آزمایشات، بررسی خواهیم کرد.


یک نکته: اگر OpenAPI را در برنامه‌های Web API فعال کنید، می‌توان با استفاده از ابزارهای تولید کد، کدهای مرتبط با HttpClient را نیز به صورت خودکار تولید و سپس از آن‌ها در اینجا استفاده کرد.


اجرای آزمون‌های یکپارچگی نوشته شده

چون ظاهر این آزمون‌ها با آزمون‌های واحد MSTest یا هر فریم ورک مشابه دیگری یکسان است، می‌توان از امکانات IDEها برای اجرای آن‌ها استفاده کرد و یا حتی می‌توان دستور dotnet test را نیز در ریشه‌ی این پروژه‌ی جدید برای اجرای تمام آزمون‌های نوشته شده، اجرا کرد:



کدهای کامل این مطلب را در اینجا می‌توانید مشاهده کنید.
مطالب
چرا باید از Git Hooks استفاده کنیم؟ معرفی Husky.Net
قبل از شروع این مقاله بهتر است ابتدا یک خاطره‌ی کوچک را تعریف کنم. مدتی پیش بود که برای سایت داکیومنتیشن یکی از پروژه‌های Open-Source سعی داشتم از vuepress که یک static site generator هست استفاده کنم. متاسفانه نسخه‌ی بتایی که استفاده میکردم یک فیچر مورد نیازم را نداشت و مجبور شدم خودم به‌آن این فیچر را اضافه کنم. سوروس را گرفتم، فیچر اضافه شد و ماجرا از اینجا شروع می‌شود ...

  • commit lint : اول اجازه نداد که کامیت را انجام دهم! با این توضیح که متن کامیت شما باید فرمت کامیت‌های این پروژه را رعایت کند. اگر به تصویر زیر دقت کنید، کامیت‌ها در این پروژه بسیار منظم هستند و هر کسی با هر متنی که دلش خواست کامیتی انجام نمی‌دهد. با یک بررسی کوچک متوجه شدم چطور باید فرمت این پروژه را رعایت کنم و مشکل رفع شد.



  • eslint : مرحله بعدی فایل‌هایی که تغییر داده بودم و یا ایجاد کرده بودم، با ابزار eslint به صورت خودکار بررسی شد. (eslint مشابه analyzer‌های دات نت است که برای بررسی مشکلات موجود در سورس کد استفاده می‌شود). در تصویر زیر یک مثال از خطای ایجاد شده‌ی توسط eslint را مشاهده می‌کنید. در این مرحله من با خطایی مواجه نشدم؛ ولی متوجه شدم کدی که نوشتم، مشکل خاصی ندارد. 



  • prettier  : مرحله سوم کد نوشته شده‌ی من توسط دستورات فرمتی که این پروژه رعایت می‌کند مانند style کد نویسی، استفاده از space یا tab، فاصله فرو رفتگی (indent) خطوط و غیره ... به صورت خودکار فرمت شد و کامیت من انجام شد. 

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

تا اینجا با یکسری مزایای پایه‌ی داشتن چنین سیستمی آشنا شدیم ولی چطور این اتفاق رخ می‌دهد!؟ پاسخ: با استفاده از Git Hooks . البته ما صرفا محدود به انجام همین کارها نیستیم. هر کاری که در ترمینال سیستم قابل انجام باشد، از طریق Git hooks هم قابل انجام است. مثلا یکی دیگر از استفاده‌های رایج از git hooks میتواند اجرای Unit-Test‌ها قبل از push باشد.
در پروژه‌ی vuepress، تعداد زیادی ابزار به صورت هماهنگ نصب شده و کار میکنند که در نهایت با استفاده از Hook‌های Git، اجرا میشوند (مثلا از هوک pre-commit برای اجرای یکسری فرمان قبل از هر کامیت). با سه مورد از این ابزارها، در ابتدای این مقاله آشنا شدیم. ولی کلید اصلی تمام این‌ها، دو ابزار  husky  و  lint-staged هستند. husky ساخت و استفاده از git hook‌ها را آسان میکند و lint-staged وظیفه دارد تا فایل‌های stage شده‌ی در گیت را لیست کند و به سایر ابزارها برای اجرا ارسال نماید. 

شاید بپرسید که تمام اینها از Nodejs و npm استفاده میکنند، چطور میتوانیم از این امکانات در دات نت استفاده کنیم!   

معرفی Husky.Net 
این کتابخانه ترکیبی از ابزارهای husky و lint-staged و چند ابزار دیگر برای برنامه نویسان دات نت است. با استفاده از این ابزار به راحتی میتوانید از هوک‌های Git برای اجرای دستورات خود یا سایر ابزارها استفاده کنید. husky.net یک task-runner داخلی دارد که تعریف و ایجاد تسک‌های قابل اجرا توسط هوک‌ها را آسان میکند.

نصب husky.net
# local installation (recommended)
cd <Your project root directory>
dotnet new tool-manifest
dotnet tool install Husky

# global installation
dotnet tool install --global Husky

نکته نهایی
من در این مقاله قصد داشتم فقط دلیل نیاز به چنین ابزاری را شرح دهم؛ توضیح اینکه husky.net چه امکاناتی دارد و چطور میتوان از آن استفاده کرد، خارج از حوصله‌ی این مقاله است. اگر شد در آینده مقاله‌ای برای آموزش این ابزار تهیه میکنم؛ فعلا برای کسب اطلاعات بیشتر میتوانید به صفحه‌ی گیت هاب این ابزار مراجعه کنید و دلیل دیگر اینکه فکر میکنم الان زمان مناسبی برای آموزش نیست؛ چون این پروژه بسیار جوان است و به تازگی نوشته شده و احتمال اینکه فیچر‌های آن به زودی تغییر کند هم وجود دارد. همینطور از علاقمندان به مشارکت در پروژه‌های open-source دعوت می‌کنم که به بهبود این ابزار و یا تهیه‌ی ابزارهایی که در حال حاضر مشابهی در دات نت ندارند، کمک کنند. به طور مثال برای فرمت کد و آنالایزر، dotnet-format  مایکروسافت وجود دارد که بسیار کاربردیست؛ ولی ما ابزاری مثل commit lint هنوز در دات نت نداریم. فعلا در صورت نیاز باید برای استفاده از آن همچنان node و npm را نصب کنیم.
پاسخ به بازخورد‌های پروژه‌ها
استفاده از Fluent Query در دیتا سورس PDFReporter
متشکر از راهنمایی؛ در حال حاضر یک مشکل با دسترسی به پوشه pdf  بر روی سرور است در زمانی که پروژه را روی local  اجرا میکنم مشکل ندارم اما روی سرور خطا 
Access to the path 'C:\Inetpub\vhosts\dehbakri.ir\httpdocs\Pdf\RptIListSample.pdf' is denied.
را دریافت میکنم. پوشه را روی هاست دستی ایجاد کردم نشد، پاک  کردم و از طریق پروژه پابلش کردم و روی هاست کپی کردم (برای ورود به پوشه پابلیش یه فایل معمولی داخلش قرار دادم) باز هم همین خطا را میدهد