مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
Ajax.BeginForm در ASP.NET MVC از jQuery Ajax برای ارسال مقادیر فرم، به سرور استفاده می‌کند. در این بین اگر یکی از عناصر فرم، المان ارسال فایل به سرور باشد، مقدار دریافتی در سمت سرور نال خواهد بود. مشکل اینجا است که نمی‌توان به کمک Ajax معمولی (یا به عبارتی XMLHttpRequest) فایلی را به سرور ارسال کرد. یا باید از سیلورلایت یا فلش استفاده نمود و یا از مرورگرهایی که XMLHttpRequest Level 2 را پشتیبانی می‌کنند (از IE 10 به بعد مثلا) که امکان Ajax upload توکار به همراه گزارش درصد آپلود را بدون نیاز به فلش یا سیلورلایت، دارند.
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل می‌شود، مقادیر معمولی فرم (تمام المان‌ها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback می‌شود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر می‌رسد. برای انجام اینکار تنها کافی است از افزونه‌ی AjaxFileUpload استفاده کنیم که در ادامه نحوه‌ی استفاده از آن‌را بررسی خواهیم کرد.


پیشنیازها

در ادامه فرض بر این است که افزونه‌ی AjaxFileUpload را دریافت کرده و به فایل Layout برنامه افزوده‌اید:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.11.1.min.js"></script>
    <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
    <script src="~/Scripts/ajaxfileupload.js"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>


مدل، کنترلر و View برنامه

مدل برنامه مشخصات یک محصول است:
namespace MVCAjaxFormUpload.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

کنترلر آن از سه متد تشکیل شده‌است:
using System.Threading;
using System.Web;
using System.Web.Mvc;
using MVCAjaxFormUpload.Models;

namespace MVCAjaxFormUpload.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Product product)
        {
            var isAjax = this.Request.IsAjaxRequest();

            return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public ActionResult UploadFiles(HttpPostedFileBase image1, int id)
        {
            var isAjax = this.Request.IsAjaxRequest();
            Thread.Sleep(3000); //شبیه سازی عملیات طولانی
            return Json(new { FileName = "/Uploads/filename.ext" }, "text/html", JsonRequestBehavior.AllowGet);
        }
    }
}
Index اول، کار نمایش صفحه‌ی ارسال اطلاعات را انجام خواهد داد.
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام می‌دهد. HttpPost آن معمولی است.

و کدهای View این مثال نیز به شرح زیر است:
@model MVCAjaxFormUpload.Models.Product
@{
    ViewBag.Title = "Index";
}

<h2>Ajax Form Upload</h2>

@using (Ajax.BeginForm(actionName: "Index",
                       controllerName: "Home",
                       ajaxOptions: new AjaxOptions { HttpMethod = "POST" },
                       routeValues: null,
                       htmlAttributes: new { id = "uploadForm" }))
{
    <label>Name:</label>
    @Html.TextBoxFor(model => model.Name)
    <br />
    <label>Image:</label>
    <br />
    <input type="file" name="Image1" id="Image1" />
    <br />
    <input type="submit" value="Submit" />
    <img id="loading" src="~/Content/Images/loading.gif" style="display:none;">
}

@section Scripts
{
    <script type="text/javascript">
        $(function () {
            $('#uploadForm').submit(function () {
                $("#loading").show();
                $.ajaxFileUpload({
                    url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود
                    secureuri: false,
                    fileElementId: 'Image1', // آی دی المان ورودی فایل
                    dataType: 'json',
                    data: { id: 1, data: 'test' }, // اطلاعات اضافی در صورت نیاز
                    success: function (data, status) {
                        $("#loading").hide();
                        if (typeof (data.FileName) != 'undefined') {
                            alert(data.FileName);
                        }
                    },
                    error: function (data, status, e) {
                        $("#loading").hide();
                        alert(e);
                    }
                });
            });
        });
    </script>
}
فرمی که توسط Ajax.BeginForm تشکیل شده‌است، یک فرم معمولی Ajax ایی است و نکته‌ی جدیدی ندارد. تنها در آن یک المان ارسال فایل قرار گرفته‌است و همچنین Id آن‌را نیز جهت استفاده توسط jQuery مشخص کرده‌ایم.
در ادامه نحوه‌ی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده می‌کنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره می‌کند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحه‌است. از قسمت data جهت ارسال اطلاعات اضافه‌تری به اکشن متد UploadFiles استفاده می‌شود. سایر قسمت‌های آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا می‌شوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمه‌ی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمه‌ی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام می‌شوند.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
MVCAjaxFormUpload.zip
مطالب
آشنایی با Saltarelle کامپایلر قدرتمند #C به جاوااسکریپت

شاید ساده‌ترین تعریف برای  Saltarelle  این باشد که «کامپایلریست که کد‌های C# را به جاوا اسکریپت تبدیل می‌کند». محاسن زیادی را می‌توان برای اینگونه کامپایلر‌ها نام برد؛ مخصوصا در پروژه‌های سازمانی که نگهداری از کد‌های جاوا اسکریپت بسیار سخت و گاهی خارج از توان است و این شاید مهمترین عامل ظهور ابزارهای جدید از قبیل Typescript باشد.

در هر صورت اگر حوصله و وقت کافی برای تجهیز تیم نرم افزاری، به دانش یک زبان جدید مانند Typescript نباشد، استفاده از توان و دانش تیم تولید، از زبان C# ساده‌ترین راه حل است و اگر ابزاری مطمئن برای استفاده از حداکثر قدرت JavaScript همراه با امکانات نگهداری و توسعه کد‌ها وجود داشته باشد، بی شک Saltarelle یکی از بهترین‌های آنهاست.

قبلا کامپایلر هایی از این دست مانند  Script# وجود داشتند، اما فاقد همه امکانات C# بوده وعملا قدرت کامل C# در کد نویسی وجود نداشت. اما با توجه به ادعای توسعه دهندگان این کامپایلر سورس باز در استفاده‌ی حداکثری از کلیه ویژگی‌های C# 5 و با وجود Library ‌های متعدد می‌توان Saltarelle  را عملا یک کامپایلر موفق در این زمینه دانست.

برای استفاده از Saltarelle در یک برنامه وب ساده باید یک پروژه Console Application به Solution اضافه کرد و پکیج Saltarelle.Compiler را از nuget نصب نمایید. بعد از نصب این پکیج، کلیه Reference ‌ها از پروژه حدف می‌شوند و هر بار Build توسط کامپایلر Saltarelle  انجام می‌شود. البته با اولین Build، مقداری Error را خواهید دید که برای از بین بردنشان نیاز است پکیج Saltarelle.Runtime را نیز در این پروژه نصب نمایید:

PM> Install-Package Saltarelle.Compiler
PM> Install-Package Saltarelle.Runtime

در صورتیکه کماکان Build  نهایی با Error همرا بود، یکبار این پروژه را Unload  و سپس مجددا Load نمایید



UI یک پروژه وب MVC است و Client یک Console Application که پکیج‌های مورد نیاز Saltarelle  روی آن نصب شده است.

در صورتیکه پروژه را Build نماییم و نگاهی به پوشه‌ی Debug بیاندازیم، یک فایل JavaScript همنام پروژه وجود دارد:


برای اینکه بعد از هر بار Build ، فایل اسکریپت به پوشه‌ی مربوطه در پروژه UI منتقل شود کافیست کد زیر را در Post Build  پروژه Client بنویسیم: 

copy "$(TargetDir)$(TargetName).js" "$(SolutionDir)SalratelleSample.UI\Scripts"

اکنون پس از هر بار Build ، فایل اسکریپت مورد نظر در پوشه‌ی Scripts پروژه UI  آپدیت می‌شود:


در ادامه کافیست فایل اسکریپت را به layout اضافه کنیم. 

<script src="~/Scripts/SaltarelleSample.Client.js"></script>

در پوشه‌ی Saltarelle.Runtime در پکیج‌های نصب شده، یک فایل اسکریپت به نام mscorlib.min.js نیز وجود دارد که حاوی اسکریپت‌های مورد نیاز Saltarelle در هنگام اجراست. آن را به پوشه اسکریپت‌های پروژه UI کپی نمایید و سپس به Layout  اضافه کنید. 

<script src="~/Scripts/mscorlib.min.js"></script>
<script src="~/Scripts/SaltarelleSample.Client.js"></script>

حال نوبت به اضافه نمودن library‌های مورد نیازمان است. برای دسترسی به آبجکت هایی از قبیل document, window, element و غیره در جاوااسکریپت می‌توان پکیج Saltarelle.Web را در پروژه‌ی Client نصب نمود و برای دسترسی به اشیاء و فرمانهای jQuery، پکیج Salratelle.jQuery را نصب نمایید. 

> Install-Package Saltarelle.Web
> Install-Package Saltarelle.jQuery

به این library‌ها imported library می‌گویند. در واقع، در زمان کامپایل، برای این library‌ها فایل اسکریپتی تولید نمی‌شود و فقط آبجکت‌های #C هستند که که هنگام کامپایل تبدیل به کدهای ساده اسکریپت می‌شوند که اگر اسکریپت مربوط به آنها به صفحه اضافه نشده باشد، اجرای اسکریپت با خطا مواجه می‌شود.

به طور ساده‌تر وقتی از jQuery library استفاده می‌کنید هیچ فایل اسکریپت اضافه‌ای تولید نمی‌شود، اما باید اسکریپت jQuery به صفحه شما اضافه شده باشد.

<script src="~/Scripts/jquery-1.10.2.min.js"></script>

مثال ما یک اپلیکیشن ساده برای خواندن فید‌های همین سایت است. ابتدا کد‌های سمت سرور را در پروژه UI  می نویسیم.

کلاس‌های مورد نیاز ما برای این فید ریدر: 

public class Feed
    {
        public string FeedId { get; set; }
        public string Title { get; set; }
        public string Address { get; set; }

    }
    public class Item
    {
        public string Title { get; set; }
        public string Link { get; set; }
        public string Description { get; set; }
    }

و یک کلاس برای مدیریت منطق برنامه 

 public class SiteManager
    {
        private static List<Feed> _feeds;
        public static List<Feed> Feeds
        {
            get
            {
                if (_feeds == null)
                    _feeds = CreateSites();
                return _feeds;
            }
        }
        private static List<Feed> CreateSites()
        {
            return new List<Feed>() { 
                new Feed(){
                    FeedId = "1",
                    Title = "آخرین تغییرات سایت",
                    Address = "https://www.dntips.ir/rss.xml"
                },
                 new Feed(){
                    FeedId = "2",
                    Title = "مطالب سایت",
                    Address = "https://www.dntips.ir/feeds/posts"
                },
                 new Feed(){
                    FeedId = "3",
                    Title = "نظرات سایت",
                    Address = "https://www.dntips.ir/feeds/comments"
                },
                 new Feed(){
                    FeedId = "4",
                    Title = "خلاصه اشتراک ها",
                    Address = "https://www.dntips.ir/feed/news"
                },
            };
        }

        public static IEnumerable<Item> GetNews(string id)
        {
            XDocument feedXML = XDocument.Load(Feeds.Find(s=> s.FeedId == id).Address);
            var feeds = from feed in feedXML.Descendants("item")
                        select new Item
                        {
                            Title = feed.Element("title").Value,
                            Link = feed.Element("link").Value,
                            Description = feed.Element("description").Value
                        };
            return feeds;
        }

    }

کلاس SiteManager فقط یک لیست از فید‌ها دارد و متدی که با گرفتن شناسه‌ی فید ، یک لیست از آیتم‌های موجود در آن فید ایجاد می‌کند.

حال دو ApiController برای دریافت داده‌ها ایجاد می‌کنیم

public class FeedController : ApiController  
{
        // GET api/<controller>
        public IEnumerable<Feed> Get()
        {
            return SiteManager.Feeds;
        }
    }

public class ItemsController : ApiController
    {
        // GET api/<controller>/5
        public IEnumerable<Item> Get(string id)
        {
            return SiteManager.GetNews(id);
        }
    }

در View پیش‌فرض که Index از کنترلر Home  است،  یک Html ساده برای فرم  صفحه اضافه می‌کنیم 

<div>
    <div>
        <h2>Feeds</h2>
        <ul id="Feeds">
           
        </ul>
    </div>
    <div>
        <h2>Items</h2>
        <p id="FeedItems">
        </p>
    </div>
   
</div>

در المنت Feeds لیست فید‌ها را قرار می‌دهیم و در FeedItems آیتم‌های مربوط به هر فید. حال به سراغ کد‌های سمت کلاینت می‌رویم و به جای جاوا اسکریپت از Saltarelle استفاده می‌کنیم.

کلاس Program را از پروژه Client باز می‌کنیم و متد Main را به شکل زیر تغییر می‌دهیم:

static void Main()
        {
            jQuery.OnDocumentReady(() => {
                FillFeeds();
            });
        }

بعد از کامپایل شدن، کد #C شارپ بالا به صورت زیر در می‌آید: 

$SaltarelleSample_Client_$Program.$main = function() {
$(function() {
$SaltarelleSample_Client_$Program.$fillFeeds();
});
};
$SaltarelleSample_Client_$Program.$main();

و این همان متد معروف jQuery است که Saltarelle.jQuery برایمان ایجاد کرده است.

متد FillFeeds را به شکل زیر پیاده سازی می‌کنیم

private static void FillFeeds()
        {
            jQuery.Ajax(new jQueryAjaxOptions()
            {
                Url = "/api/feed",
                Type = "GET",
                Success = (d,t,r) => {

                    // Fill 
                    var ul = jQuery.Select("#Feeds");
                    jQuery.Each((List<Feed>)d, (idx,i) => {
                        var li = jQuery.Select("<li>").Text(i.Title).CSS("cursor", "pointer");
                        li.Click(eve => {
                            FillData(i.FeedId);
                        });
                        ul.Append(li);
                    });
                }
            });
        }

آبجکت jQuery، متدی به نام Ajax دارد که یک شی از کلاس jQueryAjaxOptions را به عنوان پارامتر می‌پذیرد. این کلاس کلیه خصوصیات متد Ajax در jQuery را پیاده سازی می‌کند. نکته شیرین آن توانایی نوشتن lambda برای Delegate هاست.

خاصیت Success یک Delegate است که 3 پارامتر ورودی را می‌پذیرد.

public delegate void AjaxRequestCallback(object data, string textStatus, jQueryXmlHttpRequest request);

data همان مقداریست که api باز می‌گرداند که یک لیست از Feed هاست. برای زیبایی کار، من یک کلاس Feed در پروژه Client اضافه می‌کنم که خصوصیاتی مشترک با کلاس اصلی سمت سرور دارد و مقدار برگشی Ajax را به آن تبدیل می‌کنم.

کلاس Feed و Item

 [PreserveMemberCase()]
    public class Feed
    {
        //[ScriptName("FeedId")]
        public string FeedId;

        //[ScriptName("Title")]
        public string Title;

        //[ScriptName("Address")]
        public string Address;

    }

    [PreserveMemberCase()]
    public class Item
    {
        // [ScriptName("Title")]
        public string Title;

        // [ScriptName("Link")]
        public string Link;

        // [ScriptName("Description")]
        public string Description;
    }
Attrubute‌های زیادی در Saltarelle وجود دارند و از آنجایی که کامپایلر اسم فیلد‌ها را camelCase کامپایل می‌کند من برای جلوگیری از آن از PreserveMemberCase  بر روی هر کلاس استفاده کردم. می‌توانید اسم هر فیلد را سفارشی کامپایل نمایید. 
jQuery.Each((List<Feed>)d, (idx,i) => {
                        var li = jQuery.Select("<li>").Text(i.Title).CSS("cursor", "pointer");
                        li.Click(eve => {
                            FillData(i.FeedId);
                        });
                        ul.Append(li);
                    });

به ازای هر آیتمی که در شیء بازگشتی وجود دارد، با استفاد از متد each در jQuery یک li ایجاد می‌کنیم. همان طور که می‌بینید کلیه خواص، به شکل Fluent قابل اضافه شدن می‌باشد. سپس برای li یک رویداد کلیک که در صورت وقوع، متد FillData را با شناسه فید کلیک شده فراخوانی می‌کند و در آخر li را به المنت ul اضافه می‌کنیم.

برای هر کلیک هم مانند مثال بالا api را با شناسه‌ی فید مربوطه فراخوانی کرده و به ازای هر آیتم، یک سطر ایجاد می‌کنیم.

private static void FillData(string p)
        {
            jQuery.Ajax(new jQueryAjaxOptions()
            {
                Url = "/api/items/" + p,
                Type = "GET",
                Success = (d, t, r) => {
                    var content = jQuery.Select("#FeedItems");
                    content.Html("");
                    foreach (var item in (List<Item>)d)
                    {
                        var row = jQuery.Select("<div>").AddClass("row").CSS("direction", "rtl");
                        var link = jQuery.Select("<a>").Attribute("href", item.Link).Text(item.Title);
                        row.Append(link);
                        content.Append(row);
                    }
                }
            });
        }
خروجی برنامه به شکل زیر است: 

در این مثال ما از Saltarelle.jQuery برای استفاده از jQuery.js استفاده نمودیم. library‌های متعددی برای Saltarelle  از قبیل  linq,angular,knockout,jQueryUI,nodeJs ایجاد شده و همچنین قابلیت‌های زیادی برای نوشتن imported library‌های سفارشی نیز وجود دارد. 

مطمئنا استفاده از چنین کامپایلرهایی راه حلی سریع برای رهایی از مشکلات متعدد کد نویسی با جاوا اسکریپت در نرم افزارهای بزرگ مقیاس است. اما مقایسه آنها با ابزارهایی از قبیل typescript احتیاج به زمان و تجربه کافی در این زمینه دارد.

مطالب
ایجاد HTTP API توسط Feather HTTP
Feather HTTP یک فریم‌ورک HTTP سبک، برای ایجاد APIهای NET Core. است، در واقع یک wrapper بر روی APIهای موجود ASP.NET Core می‌باشد که به ما امکان ایجاد HTTP API را در کمترین زمان میدهد. در این مطلب نحوه ایجاد یک API را توسط این فریم‌ورک بررسی خواهیم کرد.

معرفی قالب FeatherHttp.Templates به سیستم dotnet
برای شروع می‌توانیم قالب پروژه Feather HTTP را به لیست قالب‌های از پیش نصب شده‌ی dotnet اضافه کنیم. برای اینکار کافی است در خط فرمان دستور زیر را وارد کنیم:
dotnet new -i FeatherHttp.Templates::0.1.67-alpha.g69b43bed72 --nuget-source https://f.feedz.io/featherhttp/framework/nuget/index.json
پس از نصب قالب می‌توانید Feather HTTP را در لیست قالب‌ها توسط دستور dotnet new --list مشاهده کنید:
Templates                                         Short Name               Language          Tags
----------------------------------------------------------------------------------------------------------------------------------
FeatherHttp                                       feather                  [C#]              Web/ASP.NET/FeatherHttp

نحوه‌ی ایجاد یک پروژه‌ی جدید بر اساس قالب جدید
برای ایجاد یک پروژه‌ی جدید کافی است از دستور dotnet new feather استفاده کنید، در ادامه یک پروژه جدید تحت عنوان todoAPI ایجاد خواهیم کرد:
dotnet new feather --name todoAPI
خروجی دستور فوق یک پروژه با ساختار ذیل است:

همانطور که مشاهده می‌کنید پروژه‌ی فوق تنها شامل دو فایل .csproj و Program.cs است. درون Program.cs و متد Main کار initialize کردن سرور HTTP صورت گرفته است. WebApplication.Create دقیقا همانند Host.CreateDefaultBuilder پروژه‌های ASP.NET Core عمل می‌کند؛ یعنی پیکربندی pipeline از قبیل اضافه کردن متغیرهای محیطی، خواندن از فایل JSON و ... را انجام میدهد اما با کد boilerplate کمتر. بنابراین خروجی WebApplication.Create یک ASP.NET Core Pipeline با قابلیت اضافه کردن تنظیمات دلخواه است. در ادامه جهت بررسی بیشتر Feather HTTP، یک مدل را به همراه یک سری دیتای In-memory به پروژه اضافه خواهیم کرد:

using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Linq;

namespace todoAPI.Models
{
    public class Todo
    {
        [JsonPropertyName("id")]
        public int Id { get; set; }
        [JsonPropertyName("title")]
        public string Title { get; set; }
        [JsonPropertyName("completed")]
        public bool Completed { get; set; }
    }

    public class TodoData
    {
        private readonly IList<Todo> _db = new List<Todo>
        {
            new Todo { Id = 1, Title = "Read book" },
            new Todo { Id = 2, Title = "Watch an episode of Dark" },
            new Todo { Id = 3, Title = "Publish a post on dotnettips" },
            new Todo { Id = 4, Title = "Skype with my friend" },
        };
        public IList<Todo> GetAllToDoItmes()
        {
            return _db;
        }
        public void AddTodo(Todo item)
        {
            _db.Add(item);
        }
        public void ToggleTodo(int id)
        {
            var todo = _db.FirstOrDefault(x => x.Id == id);
            todo.Completed = !todo.Completed;
        }

        public void DeleteTodo(int id)
        {
            var todo = _db.FirstOrDefault(x => x.Id == id);
            _db.Remove(todo);
        }
    }
}

در مثال فوق برای نگاشت نام خواص، از System.Text.Json توکار NET Core 3.0. استفاده شده‌است. در ادامه نیز از یک کلاس برای شبیه‌سازی CRUD یک Todo استفاده شده‌است. سپس برای داشتن اندپوینت‌های موردنظر به ازای هر کدام از متدهای فوق درون متد Main، از app.Map... استفاده کرده‌ایم:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using todoAPI.Models;

namespace todoAPI
{
    class Program
    {
        private static readonly TodoData db = new TodoData();
        static async Task Main(string[] args)
        {
            var app = WebApplication.Create(args);

            app.MapGet("/", GetTodos);
            app.MapPost("/api/todos", CreateTodo);
            app.MapPost("/api/todos/{id}", ToggleTodo);
            app.MapDelete("/api/todos/{id}", DeleteTodo);

            await app.RunAsync();
        }

        static async Task GetTodos(HttpContext http)
        {
            var todos = db.GetAllToDoItmes();
            await http.Response.WriteJsonAsync(todos);
        }

        static async Task CreateTodo(HttpContext http)
        {
            var todo = await http.Request.ReadJsonAsync<Todo>();
            db.AddTodo(todo);
            http.Response.StatusCode = 204;
        }

        static async Task ToggleTodo(HttpContext http)
        {
            if (!http.Request.RouteValues.TryGet("id", out int id))
            {
                http.Response.StatusCode = 400;
                return;
            }
            db.ToggleTodo(id);
            http.Response.StatusCode = 204;
        }

        static async Task DeleteTodo(HttpContext http)
        {
            if (!http.Request.RouteValues.TryGet("id", out int id))
            {
                http.Response.StatusCode = 400;
                return;
            }
            db.DeleteTodo(id);
            http.Response.StatusCode = 204;
        }
    }
}


هر کدام از اندپوینت‌های فوق، یک ورودی HttpContext دریافت خواهند کرد. توسط این شیء می‌توانیم به درخواست جاری و همچنین به پاسخ درخواست، دسترسی داشته باشیم. 


استفاده از سیستم DI توکار NET Core.

همانطور که در ابتدای مطلب نیز عنوان شد، Feather HTTP یک wrapper بر روی APIهای موجود ASP.NET Core است، بنابراین می‌توانیم از همان سرویس DI که درون پروژه‌های ASP.NET Core در اختیار داریم در اینجا نیز استفاده کنیم. در ادامه یک پوشه‌ی جدید را به مثال قبل، با نام Controllers اضافه خواهیم کرد و درون آن یک فایل TodoController را با محتویات زیر ایجاد خواهیم کرد:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using todoAPI.Models;
using todoAPI.Services;

namespace todoAPI.Controllers
{
    public class TodoController
    {
        private readonly ITodoService _todoService;

        public TodoController(ITodoService todoService)
        {
            _todoService = todoService;
        }

        public async Task GetTodos(HttpContext http)
        {
            var todos = _todoService.GetAllToDoItmes();
            await http.Response.WriteJsonAsync(todos);
        }

        public async Task CreateTodo(HttpContext http)
        {
            var todo = await http.Request.ReadJsonAsync<Todo>();
            _todoService.AddTodo(todo);
            http.Response.StatusCode = 204;
        }

        public async Task ToggleTodo(HttpContext http)
        {
            if (!http.Request.RouteValues.TryGet("id", out int id))
            {
                http.Response.StatusCode = 400;
                return;
            }
            _todoService.ToggleTodo(id);
            http.Response.StatusCode = 204;
        }

        public async Task DeleteTodo(HttpContext http)
        {
            if (!http.Request.RouteValues.TryGet("id", out int id))
            {
                http.Response.StatusCode = 400;
                return;
            }
            _todoService.DeleteTodo(id);
            http.Response.StatusCode = 204;
        }
    }
}


کاری که انجام شده است، انتقال تمامی متدهای static به کلاس فوق و سپس جایگزین کردن کلمه‌ی کلیدی static با public است. همچنین یه ارجاع به اینترفیس جدید با عنوان ITodoService اضافه شده است؛ درون پیاده‌سازی این اینترفیس همان متدهای کلاس TodoData را اضافه کرده‌ایم:

using System.Collections.Generic;
using todoAPI.Models;
using System.Linq;

namespace todoAPI.Services
{
    public interface ITodoService
    {
        void AddTodo(Todo item);
        void DeleteTodo(int id);
        IList<Todo> GetAllToDoItmes();
        void ToggleTodo(int id);
    }

    public class TodoService : ITodoService
    {
        private readonly IList<Todo> _db = new List<Todo>
        {
            new Todo { Id = 1, Title = "Read book" },
            new Todo { Id = 2, Title = "Watch an episode of Dark" },
            new Todo { Id = 3, Title = "Publish a post on dotnettips" },
            new Todo { Id = 4, Title = "Skype with my friend" },
        };
        public IList<Todo> GetAllToDoItmes()
        {
            return _db;
        }
        public void AddTodo(Todo item)
        {
            _db.Add(item);
        }
        public void ToggleTodo(int id)
        {
            var todo = _db.FirstOrDefault(x => x.Id == id);
            todo.Completed = !todo.Completed;
        }

        public void DeleteTodo(int id)
        {
            var todo = _db.FirstOrDefault(x => x.Id == id);
            _db.Remove(todo);
        }
    }
}


نکته: برای ایجاد اینترفیس از روی یک کلاس درون VS Code می‌توانیم اینگونه عمل کنیم:



تغییرات فایل Program.cs

ابتدا باید using مربوط به DI را در ابتدای فایل اضافه کنیم:

using Microsoft.Extensions.DependencyInjection;


سپس توسط ServiceProvider یک وهله از کلاس موردنظر را ایجاد کرده‌ایم و همچنین سرویس‌های موردنظر را درون DI Container اضافه کرده‌ایم:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using todoAPI.Controllers;
using todoAPI.Services;

namespace todoAPI
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddTransient<TodoController>();
            builder.Services.AddTransient<ITodoService, TodoService>();

            var serviceProvider = builder.Services.BuildServiceProvider();
            var todoController = serviceProvider.GetService<TodoController>();

            var app = WebApplication.Create(args);

            app.MapGet("/", todoController.GetTodos);
            app.MapPost("/api/todos", todoController.CreateTodo);
            app.MapPost("/api/todos/{id}", todoController.ToggleTodo);
            app.MapDelete("/api/todos/{id}", todoController.DeleteTodo);

            await app.RunAsync();
        }
    }
}



Convention Over Configuration

در کد قبلی به صورت دستی TodoController را توسط Service Location از DI درخواست کرده‌ایم. اینکار را در ادامه می‌توانیم به Feather HTTP سپرده تا کار وهله‌سازی را براساس قواعد توکار برایمان انجام دهد:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using todoAPI.Services;

namespace todoAPI
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            builder.Services.AddControllers();

            builder.Services.AddSingleton<ITodoService, TodoService>();

            var serviceProvider = builder.Services.BuildServiceProvider();

            var app = builder.Build();

            app.MapControllers();

            await app.RunAsync();
        }
    }
}


سپس در ادامه برای دسترسی به HTTP Context درون TodoController از IHttpContextAccessor استفاده کرده‌ایم:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using todoAPI.Models;
using todoAPI.Services;

namespace todoAPI.Controllers
{
    public class TodoController
    {
        private readonly ITodoService _todoService;
        private readonly IHttpContextAccessor _accessor;
        public TodoController(ITodoService todoService, IHttpContextAccessor accessor)
        {
            _todoService = todoService;
            _accessor = accessor;
        }

        [HttpGet("/todos")]
        public async Task GetTodos()
        {
            var todos = _todoService.GetAllToDoItmes();
            await _accessor.HttpContext.Response.WriteJsonAsync(todos);
        }

        [HttpPost("/todos")]
        public async Task CreateTodo()
        {
            var todo = await _accessor.HttpContext.Request.ReadJsonAsync<Todo>();
            _todoService.AddTodo(todo);
            _accessor.HttpContext.Response.StatusCode = 204;
        }

        [HttpPost("/todos/{id}")]
        public async Task ToggleTodo(int id)
        {
            _todoService.ToggleTodo(id);
            _accessor.HttpContext.Response.StatusCode = 204;
        }

        [HttpDelete("/todos/{id}")]
        public async Task DeleteTodo(int id)
        {
            _todoService.DeleteTodo(id);
            _accessor.HttpContext.Response.StatusCode = 204;
        }
    }
}


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

مطالب
Message Header سفارشی در WCF
فرض کنید در حال توسعه یک سیستم مبتنی بر WCF هستید. بنابر نیاز باید  یک سری اطلاعات مشخص در اکثر درخواست‌های بین سرور و کلاینت ارسال شوند یا ممکن است بعد از انجام بیش از 50 درصد پروژه این نیاز به وجود آید که  یک یا بیش از یک  پارامتر (که البته از سمت کلاینت تامین خواهند شد) در اکثر کوئری‌های گرفته شده سمت سرور شرکت داده شوند. خوب! در این وضعیت علاوه بر حس همدردی با اعضای تیم توسعه دهنده این پروژه چه می‌توان کرد؟
»اولین راه حلی که به ذهن می‌رسد این است که پارامتر‌های مشخص شده را در متد‌های سرویس‌های مورد نظر قرار داد و به نوعی تمام سرویس‌ها را به روز رسانی کرد. این روش به طور قطع در خیلی از قسمت‌های پروژه به صورت مستقیم اثرگذار خواهد بود و در صورت نبود ابزار‌های تست ممکن است با مشکلات جدی روبرو شوید.
»راه حل دوم این است که یک Message Header سفارشی بسازیم و در هر درخواست اطلاعات مورد نظر را در هدر قرار داده و سمت سرور این اطلاعات را به دست آوریم. این روش کمترین تغییر مورد نظر را برای پروژه دربر خواهد داشت و از طرفی نیاز متد‌های سرویس به پارامتر را از بین می‌برد و دیگر نیازی نیست تا تمام متد‌های سرویس‌ها دارای پارامتر‌های یکسان باشند.
پیاده سازی
برای شروع کلاس مورد نظر برای ارسال اطلاعات را به صورت زیر خواهیم ساخت:
 [DataContract]
    public class ApplicationContext
    {
        [DataMember( IsRequired = true )]
        public string UserId
        {
            get {  return _userId; }
            set
            {
                _userId = value;
            }
        }
        private string _userId;      

        [DataMember( IsRequired = true )]
        public static ApplicationContext Current
        {
            get
            {
                return _current;
            }
            private set { _current = value; }
        }
        private static ApplicationContext _current;  
 public static void Register( ApplicationContext appContext ) { Current = appContext; IsRegistered = true; } }
در این کلاس به عنوان نمونه مقدار Id کاربر جاری باید در هر درخواست به سمت سرور ارسال شود. حال نیاز به یک MessageInspector داریم ، کافیست که اینترفیس IClientMessageInspector را توسط یک کلاس به صورت زیر پیاده سازی نماییم:
public class ClientMessageHeaderInspector<T> : IClientMessageInspector
    {
        private readonly T _vaccine;

        public ClientMessageHeaderInspector( T vaccine )
        {
            this._vaccine = vaccine;
        }

        public void AfterReceiveReply( ref Message reply, object correlationState )
        {
        }

        public object BeforeSendRequest( ref Message request, IClientChannel channel )
        {
            MessageHeader messageHeader = MessageHeader.CreateHeader( typeof( T ).Name, typeof( T ).Namespace, this._vaccine );
            request.Headers.Add( messageHeader );
            return null;
        }
    }
نوع T مورد استفاده برای تعیین نوع داده ارسالی سمت سرور است که در این مثال کلاس ApplicationContext خواهد بود. در متد BeforeSendRequest باید Header سفارشی را ساخته و آن را به هدر درخواست اضافه نماییم. حال باید MessageInspector ساخته شده بالا را با استفاده از IEndPointBehavior به MessageInspcetor‌های نمونه ساخته شده از ClientRuntime اضافه نماییم. برای این کار به صورت زیر عمل می‌نماییم:
public class ApplicationContextMessageBehavior : IEndpointBehavior
    {
        ClientMessageHeaderInspector<ApplicationContext> inspector = null;

        public ApplicationContextMessageBehavior()
        {
            inspector = new ClientMessageHeaderInspector<ApplicationContext>( ApplicationContext.Current );
        }

        public void AddBindingParameters( ServiceEndpoint endpoint, BindingParameterCollection bindingParameters )
        {
        }

        public void ApplyClientBehavior( ServiceEndpoint endpoint, ClientRuntime clientRuntime )
        {
            clientRuntime.MessageInspectors.Add( inspector );
        }

        public void ApplyDispatchBehavior( ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher )
        {
        }

        public void Validate( ServiceEndpoint endpoint )
        {          
        }
    }
همان طور که می‌بینید در کلاس بالا یک نمونه از کلاس ClientMessageInspector را بر اساس ApplicationContext می‌سازیم و در متد ApplyClientBehavior به نمونه clientRuntime اضافه می‌نماییم. اگر دقت کرده باشید می‌توان هر تعداد MessageInspector را به clientRunTime اضافه کرد.
در مرحله آخر باید تنظیمات مربوط به ChannelFactory را انجام دهیم.
public class ServiceMapper<TChannel>
    {      
        internal static EndpointAddress EPAddress
        {
            get
            {
                return _epAddress;
            }
        }
        private static EndpointAddress _epAddress;

        public static TChannel CreateChannel( Binding binding, string uriBase, string serviceName, bool setCredential )
        {
            _epAddress = new EndpointAddress( String.Format( "{0}{1}", uriBase, serviceName ) );

            var factory = new ChannelFactory<TChannel>( binding, _epAddress );        
         
           ApplicationContext.Register( new ApplicationContext
            {
                UserId = Guid.NewGuid()
            } );  
factory.Endpoint.Behaviors.Add( new ApplicationContextMessageBehavior() ); TChannel proxy = factory.CreateChannel(); if ( factory.Endpoint.Behaviors.OfType<ApplicationContextMessageBehavior>().Any() ) { using ( var scope = new OperationContextScope( ( IClientChannel )proxy ) ) { OperationContext.Current.OutgoingMessageHeaders.Add( MessageHeader.CreateHeader( typeof( ApplicationContext ).Name, typeof( ApplicationContext ).Namespace, ApplicationContext.Current ) ); } } return proxy; }
چند نکته:
»در متد CreateChannel، ابتدا تنظیمات مربوط به EndPointAddress و ChannelFactory انجام می‌شود. سپس یک نمونه از کلاس ApplicationContext را  توسط متد Register به کلاس مورد نظر رجیستر می‌کنیم. به این ترتیب مقدار خاصیت Current در کلاس ApplicationContext برابر با نمونه ساخته شده می‌شود. سپس کلاس ApplicationContextMessageBehavior به خاصیت Behavior در ChannelFactory  اضافه می‌شود. در انتها نیز هدر سفارشی ساخته شده به MessageHeader‌های نمونه جاری OperationContext اضافه می‌شود. این عمل توسط کد زیر انجام می‌گیرد:
OperationContext.Current.OutgoingMessageHeaders.Add( MessageHeader.CreateHeader( typeof( ApplicationContext ).Name, typeof( ApplicationContext ).Namespace, AppConfiguration.Application ) );
از این پس هر درخواستی که از سمت کلاینت به سمت سرور ارسال شود به همراه خود یک نمونه از کلاس ApplicationContext را خواهد داشت. فقط دقت داشته باشید که برای ساخت ChanelFactory باید همیشه از متد CreateChannel استفاده نمایید.
استفاده از هدر سفارشی سمت سرور

حال قصد داریم که اطلاعات مورد نظر را از هدر درخواست در سمت سرور به دست آورده و از آن در کوئری‌های خود استفاده نماییم. کد زیر این کار را برای ما انجام می‌دهد:
 if ( OperationContext.Current != null && OperationContext.Current.IncomingMessageHeaders.FindHeader( typeof( ApplicationContext ).Name , typeof( ApplicationContext ).Namespace ) > 0 )
            {
                _application = OperationContext.Current.IncomingMessageHeaders.GetHeader<ApplicationContext>( typeof( ApplicationContext ).Name , typeof( ApplicationContext ).Namespace );
            }
متد FindHeader در خاصیت IncomingMessageHeader با استفاده از نام و فضای نام به دنبال هدر سفارشی می‌گردد. اگر خروجی متد از 0 بیشتر بود بعنی هدر مورد نظر موجود است. در پایان نیز با استفاده از متد GetHeader، نمونه ساخته شده کلاس ApplicationContext را به دست می‌آوریم.

مطالب
اصل Command Query separation

در ادامه مطلب قبلی، یکی از مشکلاتی که طراحی Builder از آن رنج می‌برد، نقض کردن قانون command query separation است که در ادامه درباره‌ی این اصل بیشتر بحث خواهیم کرد.

اصل Command query separation یا به اختصار CQS، در کتاب Object-Oriented Software Construction توسط Bertrand Meyer معرفی شد‌ه‌است. بر اساس آن، عملیات‌های سیستم باید یا Command باشند و یا Query و نه هر دوی آن‌ها. وقتی یک کلاینت به امضای یک متد توجه می‌کند، اینکه این متد چه کاری را انجام میدهد Commands نام داشته و به شیء فرمان می‌دهد تا کاری را انجام بدهد. این عملیات وضعیت خود شیء و یا اشیاء دیگر را تغییر می‌دهد. در اینجا Queries به شیء فرمان می‌دهند تا نتیجه‌ی سؤال ( ویا درخواست) را برگرداند.

در آن سوی دیگر، متدهایی را که وضعیت شیء را تغییر می‌‌دهند، به عنوان Command در نظر میگیریم (بدون آنکه مقداری را برگردانند). اگر این نوع متدها، مقداری را برگردانند، باعث سردرگمی کلاینت می‌شوند؛ زیرا کلاینت نمی‌داند این متد باعث تغییر شیء شده‌است و یا Query؟

 همانطور که میدانیم، متد‌ها می‌توانند هر دو کار را با هم انجام دهند؛ یعنی مقداری را برگردانند و همچنین وضعیت شیء را تغییر دهند و همین مورد باعث سردرگمی و نقض می‌شود. وقتی متد‌های Command را از Query جدا میکنیم، ما را به سمت یک طراحی قابل فهم هدایت می‌کند. متدهایی که مقدار  void برمی گردانند، Command و سایر آنهایی که نوعی (type ) را برمی‌گردانند، Query هستند.
به کد زیر توجه فرمایید:
public class FileStore
    {
        public string WorkingDirectory { get; set; }

        public string Save(int id, string message)
        {
            var path = Path.Combine(this.WorkingDirectory + id + ".txt");
            File.WriteAllText(path, message);
            return path;
        }

        public event EventHandler<MessageEventArgs> MessageRead;

        public void Read(int id)
        {
            var path = Path.Combine(this.WorkingDirectory + id + ".txt");
            var msg = File.ReadAllText(path);
            this.MessageRead(this, new MessageEventArgs { Message = msg });    
        }
    }
اولین مشکلی که در طراحی این کلاس وجود مربوط به متد Read است؛ زیرا این متد void برمی‌گرداند. پس درنتیجه از نوع Command است. ولی اگر بیشتر به این متد توجه فرمایید احساس خواهید کرد که متد Read باید به صورت Query باشد. زیرا این متد قرار بوده مقداری را برگرداند؛ ولی اینجا به صورت void پیاده سازی شده‌است. در عوض  متد Save به صورت Query پیاده سازی شده است.
برای حل این مشکل کافی است تا امضای متد Read را به این صورت تغییر دهیم:
 public string Read(int id)
 {
     var path = Path.Combine(this.WorkingDirectory + id + ".txt");
     var msg = File.ReadAllText(path);
     this.MessageRead(this, new MessageEventArgs { Message = msg });
     return msg;
  }
خوب؛ اولین سوالی که پیش می‌آید این است که آیا این Query چیزی را تغییر می‌دهد؟ (تغییر شیء یا اشیایی دیگر) 
در ادامه متوجه خواهید شد این کد باعث فراخواندن یک event می‌شود. حالا آیا این event از نوع Command است یا Query؟ از نوع Command است؛ چون EventHandler  مانند متد‌هایی هستند که مقدار void را بر می‌گردانند و همانطور که میدانید، متدهایی که مقدار void را بر می‌گردانند، از نوع Command میباشند که وضعیت شیء را تغییر می‌دهند و برای اینکه از اصل CQS پیروی کنیم، باید این event را حذف کنیم تا متد Read از نوع Query باشد.
اگر به امضای متد Save  دقت کنید، به صورت یک Query است. ولی اگر به پیاده سازی آن دقت کنید، بیشتر شبیه به یک Command است تا یک Query و مهمترین ویژگی یک Command این است که مقدار void را بر می‌گرداند و برای حل این مشکل، متد Save را به صورت زیر تغییر می‌دهیم:
public void Save(int id, string message)
{
    var path = Path.Combine(this.WorkingDirectory + id + ".txt");
    File.WriteAllText(path, message);
}
همانطور که متوجه شدید، با این تغییر دیگر ما دسترسی به  مقدار path نخواهیم داشت و شاید مقدار path برای کلاینت مهم باشد. برای حل این مشکل متد جدیدی را به نام GetFileName به کلاس اضافه می‌کنیم؛ تا کلاینت به مقدار Path دسترسی داشته باشد. توجه داشته باشید که امضای متد GetFileName به صورت query پیاده سازی شده‌است.
public class FileStore
    {
        public string WorkingDirectory { get; set; }

        public void Save(int id, string message)
        {
            var path = GetFileName(id);  //ok to query from Command
            File.WriteAllText(path, message);            
        }

        public string Read(int id)
        {
            var path = GetFileName(id);
            var msg = File.ReadAllText(path);
            return msg;
        }
     
        public string GetFileName(int id)
        {
            return Path.Combine(this.WorkingDirectory , id + ".txt");     
        }
    }
تنها نکته‌ای که در اینجا بد نیست به آن اشاره کنیم این است که متدهایی که از نوع command هستند، می‌توانند بدون هیچگونه مشکلی متد‌های query را فراخوانی کنند. زیرا مهمترین ویژگی query‌ها این هستند که وضعیت شیء را تغییر نمی‌دهند و در نتیجه در هر بار فراخوانی، همان نتیجه را بازگشت می‌دهند.

چکیده:

هدف اصلی از طراحی نرم افزار، غالب شدن بر پیچیدگی‌ها می‌باشد. اصل CQS متد‌ها را به دو دسته‌ی Command و Query تقسیم می‌کند که Query ، اطلاعاتی را از وضعیت سیستم بر می‌گرداند، ولی command  وضعیت سیستم را تغییر می‌دهد و مهمترین دستاورد CQS ما را به سمت کدی تمیز‌تر و با قابلیت درک بهتر می‌رساند.

مطالب
حذف فضاهای خالی در خروجی صفحات ASP.NET MVC
صفحات خروجی وب سایت زمانی که رندر شده و در مرورگر نشان داده می‌شود شامل فواصل اضافی است که تاثیری در نمایش سایت نداشته و صرفا این کاراکترها فضای اضافی اشغال می‌کنند. با حذف این کاراکترهای اضافی می‌توان تا حد زیادی صفحه را کم حجم کرد. برای این کار در ASP.NET Webform کارهایی (^ ) انجام شده است.
روال کار به این صورت بوده که قبل از رندر شدن صفحه در سمت سرور خروجی نهایی بررسی شده و با استفاده از عبارات با قاعده الگوهای مورد نظر لیست شده و سپس حذف می‌شوند و در نهایت خروجی مورد نظر حاصل خواهد شد. برای راحتی کار و عدم نوشتن این روال در تمامی صفحات می‌تواند در مستر پیج این عمل را انجام داد. مثلا:
private static readonly Regex RegexBetweenTags = new Regex(@">\s+<", RegexOptions.Compiled);
        private static readonly Regex RegexLineBreaks = new Regex(@"\r\s+", RegexOptions.Compiled);

        protected override void Render(HtmlTextWriter writer)
        {
            using (var htmlwriter = new HtmlTextWriter(new System.IO.StringWriter()))
            {
                base.Render(htmlwriter);
                var html = htmlwriter.InnerWriter.ToString();

                html = RegexBetweenTags.Replace(html, "> <");
                html = RegexLineBreaks.Replace(html, string.Empty);
                html = html.Replace("//<![CDATA[", "").Replace("//]]>", "");
                html = html.Replace("// <![CDATA[", "").Replace("// ]]>", "");

                writer.Write(html.Trim());
            }
        }
در هر صفحه رویدادی به نام Render وجود دارد که خروجی نهایی را می‌توان در آن تغییر داد. همانگونه که مشاهده می‌شود عملیات یافتن و حذف فضاهای خالی در این متد انجام می‌شود.
این عمل در ASP.NET Webform به آسانی انجام شده و باعث حذف فضاهای خالی در خروجی صفحه می‌شود.
برای انجام این عمل در ASP.NET MVC روال کار به این صورت نیست و نمی‌توان مانند ASP.NET Webform عمل کرد.
چون در MVC از ViewPage استفاده می‌شود و ما مستقیما به خروجی آن دسترسی نداریم یک روش این است که می‌توانیم یک کلاس برای ViewPage تعریف کرده و رویداد Write آن را تحریف کرده و مانند مثال بالا فضای خالی را در خروجی حذف کرد. البته برای استفاده باید کلاس ایجاد شده را به عنوان فایل پایه جهت ایجاد صفحات در MVC فایل web.config معرفی کنیم. این روش در اینجا به وضوح شرح داده شده است.
اما هدف ما پیاده سازی با استفاده از اکشن فیلتر هاست. برای پیاده سازی ایتدا یک اکشن فیلتر به نام CompressAttribute تعریف می‌کنیم مانند زیر:
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;

namespace PWS.Common.ActionFilters
{
    public class CompressAttribute : ActionFilterAttribute
    {
         #region Methods (2) 

        // Public Methods (1) 

        /// <summary>
        /// Called by the ASP.NET MVC framework before the action method executes.
        /// </summary>
        /// <param name="filterContext">The filter context.</param>
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var response = filterContext.HttpContext.Response;
            if (IsGZipSupported(filterContext.HttpContext.Request))
            {
                String acceptEncoding = filterContext.HttpContext.Request.Headers["Accept-Encoding"];
                if (acceptEncoding.Contains("gzip"))
                {
                    response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
                    response.AppendHeader("Content-Encoding", "gzip");
                }
                else
                {
                    response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
                    response.AppendHeader("Content-Encoding", "deflate");
                }
            }
            // Allow proxy servers to cache encoded and unencoded versions separately
            response.AppendHeader("Vary", "Content-Encoding");
           //حذف فضاهای خالی
response.Filter = new WhitespaceFilter(response.Filter); } // Private Methods (1)  /// <summary> /// Determines whether [is G zip supported] [the specified request]. /// </summary> /// <param name="request">The request.</param> /// <returns></returns> private Boolean IsGZipSupported(HttpRequestBase request) { String acceptEncoding = request.Headers["Accept-Encoding"]; if (acceptEncoding == null) return false; return !String.IsNullOrEmpty(acceptEncoding) && acceptEncoding.Contains("gzip") || acceptEncoding.Contains("deflate"); } #endregion Methods  } /// <summary> /// Whitespace Filter /// </summary> public class WhitespaceFilter : Stream { #region Fields (3)  private readonly Stream _filter; /// <summary> /// /// </summary> private static readonly Regex RegexAll = new Regex(@"\s+|\t\s+|\n\s+|\r\s+", RegexOptions.Compiled); /// <summary> /// /// </summary> private static readonly Regex RegexTags = new Regex(@">\s+<", RegexOptions.Compiled); #endregion Fields  #region Constructors (1)  /// <summary> /// Initializes a new instance of the <see cref="WhitespaceFilter" /> class. /// </summary> /// <param name="filter">The filter.</param> public WhitespaceFilter(Stream filter) { _filter = filter; } #endregion Constructors  #region Properties (5)  //methods that need to be overridden from stream /// <summary> /// When overridden in a derived class, gets a value indicating whether the current stream supports reading. /// </summary> /// <returns>true if the stream supports reading; otherwise, false.</returns> public override bool CanRead { get { return true; } } /// <summary> /// When overridden in a derived class, gets a value indicating whether the current stream supports seeking. /// </summary> /// <returns>true if the stream supports seeking; otherwise, false.</returns> public override bool CanSeek { get { return true; } } /// <summary> /// When overridden in a derived class, gets a value indicating whether the current stream supports writing. /// </summary> /// <returns>true if the stream supports writing; otherwise, false.</returns> public override bool CanWrite { get { return true; } } /// <summary> /// When overridden in a derived class, gets the length in bytes of the stream. /// </summary> /// <returns>A long value representing the length of the stream in bytes.</returns> public override long Length { get { return 0; } } /// <summary> /// When overridden in a derived class, gets or sets the position within the current stream. /// </summary> /// <returns>The current position within the stream.</returns> public override long Position { get; set; } #endregion Properties  #region Methods (6)  // Public Methods (6)  /// <summary> /// Closes the current stream and releases any resources (such as sockets and file handles) associated with the current stream. Instead of calling this method, ensure that the stream is properly disposed. /// </summary> public override void Close() { _filter.Close(); } /// <summary> /// When overridden in a derived class, clears all buffers for this stream and causes any buffered data to be written to the underlying device. /// </summary> public override void Flush() { _filter.Flush(); } /// <summary> /// When overridden in a derived class, reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. /// </summary> /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset" /> and (<paramref name="offset" /> + <paramref name="count" /> - 1) replaced by the bytes read from the current source.</param> /// <param name="offset">The zero-based byte offset in <paramref name="buffer" /> at which to begin storing the data read from the current stream.</param> /// <param name="count">The maximum number of bytes to be read from the current stream.</param> /// <returns> /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. /// </returns> public override int Read(byte[] buffer, int offset, int count) { return _filter.Read(buffer, offset, count); } /// <summary> /// When overridden in a derived class, sets the position within the current stream. /// </summary> /// <param name="offset">A byte offset relative to the <paramref name="origin" /> parameter.</param> /// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin" /> indicating the reference point used to obtain the new position.</param> /// <returns> /// The new position within the current stream. /// </returns> public override long Seek(long offset, SeekOrigin origin) { return _filter.Seek(offset, origin); } /// <summary> /// When overridden in a derived class, sets the length of the current stream. /// </summary> /// <param name="value">The desired length of the current stream in bytes.</param> public override void SetLength(long value) { _filter.SetLength(value); } /// <summary> /// When overridden in a derived class, writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. /// </summary> /// <param name="buffer">An array of bytes. This method copies <paramref name="count" /> bytes from <paramref name="buffer" /> to the current stream.</param> /// <param name="offset">The zero-based byte offset in <paramref name="buffer" /> at which to begin copying bytes to the current stream.</param> /// <param name="count">The number of bytes to be written to the current stream.</param> public override void Write(byte[] buffer, int offset, int count) { string html = Encoding.Default.GetString(buffer); //remove whitespace html = RegexTags.Replace(html, "> <"); html = RegexAll.Replace(html, " "); byte[] outdata = Encoding.Default.GetBytes(html); //write bytes to stream _filter.Write(outdata, 0, outdata.GetLength(0)); } #endregion Methods  } }
در این کلاس فشرده سازی (gzip و deflate نیز اعمال شده است) در متد OnActionExecuting ابتدا در خط 24 بررسی می‌شود که آیا درخواست رسیده gzip را پشتیبانی می‌کند یا خیر. در صورت پشتیبانی خروجی صفحه را با استفاده از gzip یا deflate فشرده سازی می‌کند. تا اینجای کار ممکن است مورد نیاز ما نباشد. اصل کار ما (حذف کردن فضاهای خالی) در خط 42 اعمال شده است. در واقع برای حذف فضاهای خالی باید یک کلاس که از Stream ارث بری دارد تعریف شده و خروجی کلاس مورد نظر به فیلتر درخواست ما اعمال شود.
در کلاس WhitespaceFilter با تحریف متد Write الگوهای فضای خالی موجود در درخواست یافت شده و آنها را حذف می‌کنیم. در نهایت خروجی این کلاس که از نوع استریم است به ویژگی فیلتر صفحه اعمال می‌شود.

برای معرفی فیلتر تعریف شده می‌توان در فایل Global.asax در رویداد Application_Start به صورت زیر فیلتر مورد نظر را به فیلترهای MVC اعمال کرد.
GlobalFilters.Filters.Add(new CompressAttribute());
برای آشنایی بیشتر فیلترها در ASP.NET MVC را مطالعه نمایید.
پ.ن: جهت سهولت، در این کلاس ها، صفحات فشرده سازی و همزمان فضاهای خالی آنها حذف شده است.
اشتراک‌ها
TypeScript 3.8 RC منتشر شد
class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
TypeScript 3.8 RC منتشر شد
نظرات مطالب
پشتیبانی توکار از ایجاد کلاس‌های Singleton از دات نت 4 به بعد
آیا برداشت من بشکل زیر صحیح است؟
در صورتی که بخواهم یک پراپرتی سراسری به این کلاس (LazySingleton)  اضافه کنیم مانند زیر عمل می‌کنم:
.....
private int _myVar;
public int MyProperty
{
     get { return _myVar; }
     set { _myVar = value; }
}
.....

 و نحوه مصرف آن به شکل زیر است؟
LazySingleton.Instance.MyProperty = 1000;
OR
var foo = LazySingleton.Instance.MyProperty;


مطالب
بهبود کارآیی Reflection در دات نت 7
استفاده‌ی از Reflection در زیر ساخت‌های دات نت و ASP.NET Core، بسیار گسترده‌است؛ به همین جهت هرگونه بهبود کارآیی در این زمینه، نه فقط بر روی خود فریم‌ورک، بلکه تمام برنامه‌هایی که از آن استفاده می‌کنند هم تاثیر گذار است. از این لحاظ دات نت 7 شاهد تغییرات گسترده‌ای است تا حدی که کارآیی برنامه‌های مبتنی بر دات نت 7 ای که از Reflection استفاده می‌کنند، نسبت به نگارش‌های قبلی دات نت، حداقل 2 برابر شده‌است و این برنامه‌ها تنها کاری را که باید انجام دهند، صرفا تغییر target framework مورد استفاده‌ی در آن‌ها به نگارش جدید است. در این مطلب نحوه‌ی رسیدن به این کارآیی بالاتر را بررسی خواهیم کرد.


تدارک یک آزمایش برای بررسی میزان افزایش کارآیی Reflection در دات نت 7

یک برنامه‌ی کنسول جدید را ایجاد کرده و سپس کلاس Person را به صورت زیر به آن اضافه می‌کنیم:
namespace NET7Reflection;

public class Person
{
    private int _age;

    internal Person(int age) => _age = age;

    private int GetAge() => _age;

    private void SetAge(int age) => _age = age;
}
همانطور که مشاهده می‌کنید، سازنده‌ی این کلاس، internal است و همچنین دو متد private هم دارد که اگر بخواهیم از آن در جای  دیگری استفاده کنیم، یکی از روش‌های متداول جهت دسترسی به این امکانات خصوصی، استفاده از Reflection است.
به همین جهت ابتدا کتابخانه‌ی BenchmarkDotNet را با TargetFramework دات نت 7 به صورت زیر به پروژه اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
  </ItemGroup>

</Project>
در ادامه، یک کلاس آزمایش کارآیی برنامه را که با استفاده از Reflection، به امکانات خصوصی کلاس Person دسترسی پیدا می‌کند، مشاهده می‌کنید:
using System.Reflection;
using BenchmarkDotNet.Attributes;

namespace NET7Reflection;

[MemoryDiagnoser(false)]
public class Benchmarks
{
    private readonly object?[] _ageParams = { 30 };

    private readonly ConstructorInfo _ctor =
        typeof(Person).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(int) })!;

    private readonly MethodInfo _getAgeMethod =
        typeof(Person).GetMethod("GetAge", BindingFlags.NonPublic | BindingFlags.Instance)!;

    private readonly Person _person = new(10);

    private readonly MethodInfo _setAgeMethod =
        typeof(Person).GetMethod("SetAge", BindingFlags.NonPublic | BindingFlags.Instance)!;

    [Benchmark]
    public int GetAge() =>
        (int)typeof(Person).GetMethod("GetAge", BindingFlags.NonPublic | BindingFlags.Instance)!
                           .Invoke(_person, Array.Empty<object?>())!;

    [Benchmark]
    public int GetAgeCachedMethod() => (int)_getAgeMethod.Invoke(_person, Array.Empty<object?>())!;

    [Benchmark]
    public void SetAge() =>
        typeof(Person).GetMethod("SetAge", BindingFlags.NonPublic | BindingFlags.Instance)!
                      .Invoke(_person, new object?[] { 30 });

    [Benchmark]
    public void SetAgeCachedMethod() => _setAgeMethod.Invoke(_person, new object?[] { 30 });

    [Benchmark]
    public void SetAgeCachedMethodCachedParams() => _setAgeMethod.Invoke(_person, _ageParams);

    [Benchmark]
    public Person Ctor() =>
        (Person)typeof(Person).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(int) })!
                              .Invoke(_person, new object?[] { 30 })!;

    [Benchmark]
    public Person CtorCachedCtorInfo() => (Person)_ctor.Invoke(_person, new object?[] { 30 })!;

    [Benchmark]
    public Person CtorCachedCtorInfoCachedParams() => (Person)_ctor.Invoke(_person, _ageParams)!;
}
توضیحات:
- در اینجا نحوه‌ی کار با متدهای خصوصی کلاس Person را توسط Reflection مشاهده می‌کنید. برای مثال در متد GetAge، به نحو متداولی این کار صورت گرفته‌است. در متد GetAgeCachedMethod، قسمت دریافت اطلاعات متد، کش شده‌است و برای نمونه در متد SetAgeCachedMethodCachedParams، هم کش شدن قسمت دریافت اطلاعات متد را مشاهده می‌کنید و هم کش شدن پارامتر ارسالی به آن‌را.
- این آزمایش را با فراخوانی زیر و تنظیم target framework به دات نت 6 و سپس دات نت 7، به صورت جداگانه‌ای اجرا می‌کنیم:
using BenchmarkDotNet.Running;
using NET7Reflection;

BenchmarkRunner.Run<Benchmarks>();
حاصل اجرای آن با target framework دات نت 6 به صورت زیر است:



و با target framework دات نت 7 به صورت زیر:


همانطور که مشاهده می‌کنید، در اکثر موارد، کارآیی Reflection در دات نت 7، حداقل 2 برابر نمونه‌ی مشابه دات نت 6 است.


چه تغییری در دات نت 7 سبب بهبود قابل ملاحظه‌ی کارآیی Reflection شده‌است؟

جزئیات تغییرات صورت گرفته‌ی در Reflection دات نت 7 را می‌توانید در این pull request مشاهده کنید که در حقیقت از امکانات سطح پایین IL Emit استفاده کرده‌اند. در این مورد پیشتر تعدادی مطلب ذیل عنوان «آشنایی با Reflection.Emit» در این سایت منتشر شده‌اند که می‌توانید آن‌ها را بررسی کنید.
در کل هرچند تغییرات جدید دات نت مانند ارائه‌ی انواع و اقسام source generators، در تعدادی از موارد نیاز به Reflection را کمتر کرده‌اند و کارآیی بیشتری را ارائه داده‌اند، اما Reflection هیچگاه منسوخ نخواهد شد و هرگونه بهبود کارآیی در این زمینه، بر روی کل فریم‌ورک و برنامه‌های مشتق شده‌ی از آن، تاثیر قابل توجهی را خواهد گذاشت.
نظرات مطالب
بازسازی کد: ارتباط یک طرفه و دو طرفه بین کلاس ها
بنده به هیچ کدام از این روش‌ها انتقادی نداشتم. تصمیم طراحی باید بر اساس شرایط پروژه اتخاد بشه. 
در مورد استفاده از navigation property 
در entity framework استفاده از Navigation property برعکس (در مثال شما کالکشن Orders در کلاس Customer) برای ایجاد یک رابطه یک به چند الزامی نیست. (برای ایجاد relation نیازی به حضوری این collection نیست و حضور خصوصیت Customer در کلاس Order کافی است) و در صورتی که شما از نظر کسب و کاری نیازی به چنین خصوصیتی نداشته باشید می‌توانید از ایجاد آن صرف نظر کنید. 
public class Order
{
    public int Id { get; set; }
    public Customer Customer { get; set; }
}
public class Customer
{
    public int Id { get; set; }
}
......

modelBuilder.Entity<Order>().HasRequired(ff => ff.Customer);


حضور خصوصیت Orders در کلاس Customer در مثال شما از نظر کسب و کاری به این معنی است که شما برای رسیدن به لیست سفارشات نیاز دارید که به شی مشتری دسترسی پیدا کنید. 
نکته اصلی این بازسازی کد اجتناب از استفاده بی مورد ارتباط دوطرفه است.