مطالب
استفاده از فیلدهای XML در NHibernate

در مورد طراحی یک برنامه "فرم ساز" در مطلب قبلی بحث شد ... حدودا سه سال قبل اینکار را برای شرکتی انجام دادم. یک برنامه درخواست خدمات نوشته شده با ASP.NET که مدیران برنامه می‌توانستند برای آن فرم طراحی کنند؛ فرم درخواست پرینت، درخواست نصب نرم افزار، درخواست وام، درخواست پیک، درخواست آژانس و ... فرم‌هایی که تمامی نداشتند! آن زمان برای حل این مساله از فیلدهای XML استفاده کردم.
فیلدهای XML قابلیت نه چندان جدیدی هستند که از SQL Server 2005 به بعد اضافه شده‌اند. مهم‌ترین مزیت آن‌ها‌ هم امکان ذخیره سازی اطلاعات هر نوع شیء‌ایی به عنوان یک فیلد XML است. یعنی همان زیرساختی که برای ایجاد یک برنامه فرم ساز نیاز است. ذخیره سازی آن هم آداب خاصی را طلب نمی‌کند. به ازای هر فیلد مورد نظر کاربر، یک نود جدید به صورت رشته معمولی باید اضافه شود و نهایتا رشته تولیدی باید ذخیره گردد. از دید ما یک رشته‌ است، از دید SQL Server یک نوع XML واقعی؛ به همراه این مزیت مهم که به سادگی می‌توان با T-SQL/XQuery/XPath از جزئیات اطلاعات این نوع فیلدها کوئری گرفت و سرعت کار هم واقعا بالا است؛ به علاوه بر خلاف مطلب قبلی در مورد dynamic components ، اینبار نیازی نیست تا به ازای هر یک فیلد درخواستی کاربر، واقعا یک فیلد جدید را به جدول خاصی اضافه کرد. داخل این فیلد XML هر نوع ساختار دلخواهی را می‌توان ذخیره کرد. به عبارتی به کمک فیلدهایی از نوع XML می‌توان داخل یک سیستم بانک اطلاعاتی رابطه‌ای، schema-less کار کرد (un-typed XML) و همچنین از این اطلاعات ویژه، کوئری‌های پیچیده هم گرفت.
تا جایی که اطلاع دارم، چند شرکت دیگر هم در ایران دقیقا از همین ایده فیلدهای XML برای ساخت برنامه فرم ساز استفاده کرده‌اند ...؛ البته مطلب جدیدی هم نیست؛ برنامه‌های فرم ساز اوراکل و IBM هم سال‌ها است که از XML برای همین منظور استفاده می‌کنند. مایکروسافت هم به همین دلیل (شاید بتوان گفت مهم‌ترین دلیل وجودی فیلدهای XML در SQL Server)، پشتیبانی توکاری از XML به عمل آورده‌ است.
یا روش دیگری را که برای طراحی سیستم‌های فرم ساز پیشنهاد می‌کنند استفاده از بانک‌های اطلاعاتی مبتنی بر key-value‌ مانند Redis یا RavenDb است؛ یا استفاده از بانک‌های اطلاعاتی schema-less واقعی مانند CouchDb.


خوب ... اکنون سؤال این است که NHibernate برای کار با فیلدهای XML چه تمهیداتی را درنظر گرفته است؟
برای این منظور خاصیتی را که قرار است به یک فیلد از نوع XML نگاشت شود، با نوع XDocument مشخص خواهیم ساخت:
using System.Xml.Linq;

namespace TestModel
{
public class DynamicTable
{
public virtual int Id { get; set; }
public virtual XDocument Document { get; set; }
}
}

سپس باید جهت معرفی این نوع ویژه، به صورت صریح از XDocType استفاده کرد؛ یعنی نکته‌ی اصلی، استفاده از CustomType مرتبط است:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;
using NHibernate.Type;

namespace TestModel
{
public class DynamicTableMapping : IAutoMappingOverride<DynamicTable>
{
public void Override(AutoMapping<DynamicTable> mapping)
{
mapping.Id(x => x.Id);
mapping.Map(x => x.Document).CustomType<XDocType>();
}
}
}

البته لازم به ذکر است که دو نوع NHibernate.Type.XDocType و NHibernate.Type.XmlDocType برای کار با فیلد‌های XML در NHibernate وجود دارند. XDocType برای کار با نوع System.Xml.Linq.XDocument طراحی شده است و XmlDocType مخصوص نگاشت نوع System.Xml.XmlDocument است.

اکنون اگر به کمک کلاس SchemaExport ، اسکریپت تولید جدول متناظر با اطلاعات فوق را ایجاد کنیم به حاصل زیر خواهیم رسید:
   if exists (select * from dbo.sysobjects
where id = object_id(N'[DynamicTable]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [DynamicTable]

create table [DynamicTable] (
Id INT IDENTITY NOT NULL,
Document XML null,
primary key (Id)
)

یک سری اعمال متداول ذخیره سازی اطلاعات و تهیه کوئری نیز در ادامه ذکر شده‌اند:
//insert
object savedId = 0;
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var obj = new DynamicTable
{
Document = System.Xml.Linq.XDocument.Parse(
@"<Doc><Node1>Text1</Node1><Node2>Text2</Node2></Doc>"
)
};
savedId = session.Save(obj);
tx.Commit();
}
}

//simple query
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var entity = session.Get<DynamicTable>(savedId);
if (entity != null)
{
Console.WriteLine(entity.Document.Root.ToString());
}

tx.Commit();
}
}

//advanced query
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var list = session.CreateSQLQuery("select [Document].value('(//Doc/Node1)[1]','nvarchar(255)') from [DynamicTable] where id=:p0")
.SetParameter("p0", savedId)
.List();

if (list != null)
{
Console.WriteLine(list[0]);
}

tx.Commit();
}
}

و در پایان بدیهی است که جهت کار با امکانات پیشرفته‌تر موجود در SQL Server در مورد فیلد‌های XML ( برای نمونه: + و +) باید مثلا رویه ذخیره شده تهیه کرد (یا مستقیما از متد CreateSQLQuery همانند مثال فوق کمک گرفت) و آن‌را در NHibernate مورد استفاده قرار داد. البته به این صورت کار شما محدود به SQL Server خواهد شد و باید در نظر داشت که در کل تعداد کمی بانک اطلاعاتی وجود دارند که نوع‌های XML را به صورت توکار پشتیبانی می‌کنند.

نظرات مطالب
معرفی System.Text.Json در NET Core 3.0.
یک نکته‌ی تکمیلی: نوشتن تبدیلگرهای نوع‌ها برای System.Text.Json

System.Text.Json، در حال حاضر از مفهومی به نام type coercion/inference، پشتیبانی نمی‌کند. type coercion یعنی تبدیل یک مقدار، به مقداری دیگر که به صورت مستقیم قابل انتساب به یکدیگر نیستند. برای مثال اگر رشته‌ی "true" را درنظر بگیریم، قابلیت انتساب به یک خاصیت از نوع bool را ندارد. برای یک چنین مواردی در این API جدید، باید تبدیلگر نوشت.
یک مثال:
using System.Collections.Generic;
using System.Text.Json;

namespace JsonTests
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsInStock { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {            
         var products = JsonSerializer.Deserialize<List<Product>>("[{\"Id\":1026,\"Name\":\"P1\",\"IsInStock\":\"false\"}]");
        }
    }
}
در این مثال، خاصیت IsInStock از نوع bool است، اما مقداری را که باید از طریق متد Deserialize دریافت کنیم، یک رشته‌ی bool ای است که قابل انتساب به bool نیست. در این حالت اگر برنامه را اجرا کنیم به استثنای زیر خواهیم رسید:
An unhandled exception of type 'System.Text.Json.JsonException' occurred in System.Text.Json.dll
Inner exceptions found, see $exception in variables window for more details.
Innermost exception System.InvalidOperationException : Cannot get the value of a token type 'String' as a boolean.
برای رفع این مشکل، می‌توان تبدیلگر زیر را تدارک دید:
    public class BooleanConverter : JsonConverter<bool>
    {
        public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            if (value.Equals("true", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("yes", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("1", StringComparison.Ordinal))
            {
                return true;
            }

            if (value.Equals("false", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("no", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("0", StringComparison.Ordinal))
            {
                return false;
            }

            throw new NotSupportedException($"`{value}` can't be converted to `bool`.");
        }

        public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
        {
            switch (value)
            {
                case true:
                    writer.WriteStringValue("true");
                    break;
                case false:
                    writer.WriteStringValue("false");
                    break;
            }
        }
    }
برای نوشتن یک تبدیلگر bool، کلاس مرتبط، باید <JsonConverter<bool را پیاده سازی کند. کلاس JsonConverter نیز به صورت زیر تعریف شده‌است:
    public abstract class JsonConverter<T> : JsonConverter
    {
        protected internal JsonConverter();

        public override bool CanConvert(Type typeToConvert);
        public abstract T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
        public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
    }
و پیاده سازی دو متد Read و Write آن الزامی است.
در متد Read آن، مقدار رشته‌ای دریافت شده‌ی از منبع داده، در اختیار ما قرار می‌گیرد. سپس باید بر اساس این مقدار، مقدار متناظری را از نوع T که در اینجا bool است، بازگشت دهیم. برای مثال اگر یکی از مقادیر رشته‌ای true ،yes و 1 را دریافت کردیم، بجای آن true را بازگشت می‌دهیم.

اکنون برای استفاده‌ی از آن خواهیم داشت:
var options = new JsonSerializerOptions();
options.Converters.Add(new BooleanConverter());
var products = JsonSerializer.Deserialize<List<Product>>(
   "[{\"Id\":1026,\"Name\":\"P1\",\"IsInStock\":\"false\"}]",
   options);
و یا روش دیگر انجام اینکار، استفاده از ویژگی JsonConverter، برای معرفی تبدیلگر تهیه شده‌است:
[JsonConverter(typeof(BooleanConverter))]
public bool IsInStock { get; set; }

از این تبدیلگر برای حالت Serialize نیز می‌توان استفاده کرد:
var options  = new JsonSerializerOptions() {WriteIndented = true };
options.Converters.Add(new BooleanConverter());
var data = JsonSerializer.Serialize<List<Product>>(productList, options);
مطالب دوره‌ها
پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC
مدتی است در اکثر سایت‌ها و طراحی‌های جدید، به جای استفاده از روش متداول نمایش انتخاب صفحه 1، 2 ... 100، برای صفحه بندی اطلاعات، از روش اسکرول نامحدود یا infinite scroll استفاده می‌کنند. نمونه‌ای از آن‌را هم در سایت جاری با دکمه «بیشتر» در ذیل اکثر صفحات و مطالب سایت مشاهده می‌کنید.


در ادامه قصد داریم نحوه پیاده سازی آن‌را در ASP.NET MVC به کمک امکانات jQuery بررسی کنیم.


مدل برنامه

namespace jQueryMvcSample02.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}
در این برنامه و مثال، قصد داریم لیستی از مطالب را توسط اسکرول نامحدود، نمایش دهیم. هر آیتم نمایش داده شده، ساختاری همانند کلاس BlogPost دارد.


منبع داده فرضی برنامه

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

namespace jQueryMvcSample02.DataSource
{
    public static class BlogPostDataSource
    {
        private static IList<BlogPost> _cachedItems;
        /// <summary>
        /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانی‌ها صورت خواهد گرفت
        /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای
        /// تهیه یک لیست برای تمام کاربران سایت است
        /// </summary>
        static BlogPostDataSource()
        {
            _cachedItems = createBlogPostsInMemoryDataSource();
        }

        /// <summary>
        /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
        /// </summary>        
        private static IList<BlogPost> createBlogPostsInMemoryDataSource()
        {
            var results = new List<BlogPost>();
            for (int i = 1; i < 30; i++)
            {
                results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i });
            }
            return results;
        }

        /// <summary>
        /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند
        /// شماره صفحه از یک شروع می‌شود
        /// </summary>
        public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4)
        {
            var skipRecords = pageNumber * recordsPerPage;
            return _cachedItems
                        .OrderByDescending(x => x.Id)
                        .Skip(skipRecords)
                        .Take(recordsPerPage)
                        .ToList();
        }
    }
}
برای اینکه برنامه نهایی را به سادگی بتوانید اجرا کنید، به عمد از بانک اطلاعاتی خاصی استفاده نشده و صرفا یک منبع داده فرضی تشکیل شده در حافظه، در اینجا مورد استفاده قرار گرفته است. بدیهی است قسمت cachedItems را به سادگی می‌توانید با یک ORM جایگزین کنید.
تنها نکته مهم آن، نحوه تعریف متد GetLatestBlogPosts می‌باشد که برای صفحه بندی اطلاعات بهینه سازی شده است. در اینجا توسط متدهای Skip و Take، تنها بازه‌ای از اطلاعات که قرار است نمایش داده شوند، دریافت می‌گردد. خوشبختانه این متدها معادل‌های مناسبی را در اکثر بانک‌های اطلاعاتی داشته و استفاده از آن‌ها بر روی یک بانک اطلاعاتی واقعی نیز بدون مشکل کار می‌کند و تنها بازه محدودی از اطلاعات را واکشی خواهد کرد که از لحاظ مصرف حافظه و سرعت کار بسیار مقرون به صرفه و سریع است.


کنترلر برنامه

using System.Linq;
using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample02.DataSource;
using jQueryMvcSample02.Security;

namespace jQueryMvcSample02.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            //آغاز کار با صفحه صفر است
            var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0);
            return View(list); //نمایش ابتدایی صفحه
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public virtual ActionResult PagedIndex(int? page)
        {
            var pageNumber = page ?? 0;
            var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber);
            if (list == null || !list.Any())
                return Content("no-more-info"); //این شرط ما است برای نمایش عدم یافتن رکوردها

            return PartialView("_ItemsList", list);
        }

        [HttpGet]
        public ActionResult Post(int? id)
        {
            if (id == null)
                return Redirect("/");

            //todo: show the content here
            return Content("Post " + id.Value);
        }
    }
}
کنترلر برنامه را در اینجا ملاحظه می‌کنید. برای کار با اسکرول نامحدود، به ازای هر صفحه، نیاز به دو متد است:
الف) یک متد که بر اساس HttpGet کار می‌کند. این متد در اولین بار نمایش صفحه فراخوانی می‌گردد و اطلاعات صفحه آغازین را نمایش می‌دهد.
ب) متد دومی که بر اساس HttpPost کار کرده و محدود است به درخواستی‌های AjaxOnly همانند متد PagedIndex.
از این متد دوم برای پردازش کلیک‌های کاربر بر روی دکمه «بیشتر» استفاده می‌گردد. بنابراین تنها کاری که افزونه جی‌کوئری تدارک دیده شده ما باید انجام دهد، ارسال شماره صفحه است. سپس با استفاده از این شماره، بازه مشخصی از اطلاعات دریافت و نهایتا یک PartialView رندر شده برای افزوده شدن به صفحه بازگشت داده می‌شود.


دو View برنامه

همانطور که برای بازگشت اطلاعات نیاز به دو اکشن متد است، برای رندر اطلاعات نیز به دو View نیاز داریم:
الف) یک PartialView که صرفا لیستی از اطلاعات را مطابق سلیقه ما رندر می‌کند. از این PartialView در متد PagedIndex استفاده خواهد شد:
@model IList<jQueryMvcSample02.Models.BlogPost>
<ul>
    @foreach (var item in Model)
    {
        <li>
            <h5>
                @Html.ActionLink(linkText: item.Title,
                                 actionName: "Post",
                                 controllerName: "Home",
                                 routeValues: new  { id = item.Id },
                                 htmlAttributes: null)
            </h5>
            @item.Body
        </li>
    }
</ul>
ب) یک View کامل که در بار اول نمایش صفحه، مورد استفاده قرار می‌گیرد:
@model IList<jQueryMvcSample02.Models.BlogPost>
@{
    ViewBag.Title = "Index";
    var loadInfoUrl = Url.Action(actionName: "PagedIndex", controllerName: "Home");
}
<h2>
    اسکرول نامحدود</h2>
@{ Html.RenderPartial("_ItemsList", Model); }
<div id="MoreInfoDiv">
</div>
<div align="center" style="margin-bottom: 9px;">
    <span id="moreInfoButton" style="width: 90%;" class="btn btn-info">بیشتر</span>
</div>
<div id="ProgressDiv" align="center" style="display: none">
    <br />
    <img src="@Url.Content("~/Content/images/loadingAnimation.gif")" alt="loading..."  />
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#moreInfoButton").InfiniteScroll({
                moreInfoDiv: '#MoreInfoDiv',
                progressDiv: '#ProgressDiv',
                loadInfoUrl: '@loadInfoUrl',
                loginUrl: '/login',
                errorHandler: function () {
                    alert('خطایی رخ داده است');
                },
                completeHandler: function () {
                    // اگر قرار است روی اطلاعات نمایش داده شده پردازش ثانوی صورت گیرد
                },
                noMoreInfoHandler: function () {
                    alert('اطلاعات بیشتری یافت نشد');
                }
            });
        });
    </script>
}
چند نکته در اینجا حائز اهمیت است:
1) مسیر دقیق اکشن متد PagedIndex توسط متد  Url.Action تهیه شده است.
2) در ابتدای نمایش صفحه، متد Html.RenderPartial کار نمایش اولیه اطلاعات را انجام خواهد داد.
3) از div خالی MoreInfoDiv، به عنوان محل افزوده شدن اطلاعات Ajax ایی دریافتی استفاده می‌کنیم.
4) دکمه بیشتر در اینجا تنها یک span ساده است که توسط css به شکل یک دکمه نمایش داده خواهد شد (فایل‌های آن در پروژه پیوست موجود است).
5) ProgressDiv در ابتدای نمایش صفحه مخفی است. زمانیکه کاربر بر روی دکمه بیشتر کلیک می‌کند، توسط افزونه جی‌کوئری ما نمایان شده و در پایان کار مجددا مخفی می‌گردد.
6) section JavaScript کار استفاده از افزونه InfiniteScroll را انجام می‌دهد.


و کدهای افزونه اسکرول نامحدود

// <![CDATA[
(function ($) {
    $.fn.InfiniteScroll = function (options) {
        var defaults = {
            moreInfoDiv: '#MoreInfoDiv',
            progressDiv: '#Progress',
            loadInfoUrl: '/',
            loginUrl: '/login',
            errorHandler: null,
            completeHandler: null,
            noMoreInfoHandler: null
        };
        var options = $.extend(defaults, options);

        var showProgress = function () {
            $(options.progressDiv).css("display", "block");
        }

        var hideProgress = function () {
            $(options.progressDiv).css("display", "none");
        }       

        return this.each(function () {
            var moreInfoButton = $(this);
            var page = 1;
            $(moreInfoButton).click(function (event) {
                showProgress();
                $.ajax({
                    type: "POST",
                    url: options.loadInfoUrl,
                    data: JSON.stringify({ page: page }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = options.loginUrl;
                        }
                        else if (status === 'error' || !data) {
                            if (options.errorHandler)
                                options.errorHandler(this);
                        }
                        else {
                            if (data == "no-more-info") {
                                if (options.noMoreInfoHandler)
                                    options.noMoreInfoHandler(this);
                            }
                            else {
                                var $boxes = $(data);
                                $(options.moreInfoDiv).append($boxes);
                            }
                            page++;
                        }
                        hideProgress();
                        if (options.completeHandler)
                            options.completeHandler(this);
                    }
                });
            });
        });
    };
})(jQuery);
// ]]>
ساختار افزونه اسکرول نامحدود به این شرح است:
هربار که کاربر بر روی دکمه بیشتر کلیک می‌کند، progress div ظاهر می‌گردد. سپس توسط امکانات jQuery Ajax، شماره صفحه (بازه انتخابی) به اکشن متد صفحه بندی اطلاعات ارسال می‌گردد. در نهایت اطلاعات را از کنترلر دریافت و به moreInfoDiv اضافه می‌کند. در آخر هم شماره صفحه را یکی افزایش داده و سپس progress div را مخفی می‌کند.

دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample02.zip
 
نظرات مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
مرورگرهای جدید با استفاده از ویژگی webkitdirectory، امکان انتخاب یک پوشه و تمام زیر پوشه‌های آن‌را دارند:
<form method="post" asp-action="UploadFiles" asp-controller="Home" enctype="multipart/form-data">
    <input type="file" name="files" webkitdirectory />
    <input type="submit" value="Upload" />
</form>
در این حالت کدهای سمت سرور آن به صورت زیر تغییر می‌کند:
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace UploadFolderASPNETCore.Controllers
{
    public class HomeController : Controller
    {
        private readonly IWebHostEnvironment _environment;
        private const int MaxBufferSize = 0x10000;

        public HomeController(IWebHostEnvironment environment)
        {
            _environment = environment;
        }

        public IActionResult Index()
        {
            return View();
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> UploadFiles(IList<IFormFile> files)
        {
            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            CreateDir(uploadsRootFolder);

            foreach (var file in files)
            {
                var dirPath = Path.GetDirectoryName(file.FileName);
                CreateDir(Path.Combine(uploadsRootFolder, dirPath));

                var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                using (var fileStream = new FileStream(filePath, FileMode.Create,
                                                        FileAccess.Write,
                                                        FileShare.None,
                                                        MaxBufferSize,
                                                        useAsync: true
                                                        ))
                {
                    await file.CopyToAsync(fileStream);
                }
            }

            return RedirectToAction("Index");
        }

        private void CreateDir(string path)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
        }
    }
}
مطالب
Func یا Expression Func در EF
با بررسی کدهای مختلف Entity framework گاهی از اوقات در امضای توابع کمکی نوشته شده، <>Func مشاهده می‌شود و در بعضی از موارد <<>Expression<Func و ... به نظر استفاده کنندگان دقیقا نمی‌دانند که تفاوت این دو در چیست و کدامیک را باید/یا بهتر است بکار برد.

ابتدا مثال کامل ذیل را در نظر بگیرید:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
using System.Linq.Expressions;

namespace Sample
{
    public abstract class BaseEntity
    {
        public int Id { set; get; }
    }

    public class Receipt : BaseEntity
    {
        public int TotalPrice { set; get; }
    }

    public class MyContext : DbContext
    {
        public DbSet<Receipt> Receipts { get; set; }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            if (!context.Receipts.Any())
            {
                for (int i = 0; i < 20; i++)
                {
                    context.Receipts.Add(new Receipt { TotalPrice = i });
                }
            }
            base.Seed(context);
        }
    }

    public static class EFUtils
    {
        public static IList<T> LoadEntities<T>(this DbContext ctx, Expression<Func<T, bool>> predicate) where T : class
        {
            return ctx.Set<T>().Where(predicate).ToList();
        }

        public static IList<T> LoadData<T>(this DbContext ctx, Func<T, bool> predicate) where T : class
        {
            return ctx.Set<T>().Where(predicate).ToList();
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            startDB();

            using (var context = new MyContext())
            {
                var list1 = context.LoadEntities<Receipt>(x => x.TotalPrice == 10);
                var list2 = context.LoadData<Receipt>(x => x.TotalPrice == 20);
            }
        }

        private static void startDB()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            // Forces initialization of database on model changes.
            using (var context = new MyContext())
            {
                context.Database.Initialize(force: true);
            }
        }
    }
}
در این مثال ابتدا کلاس Receipt تعریف شده و سپس توسط کلاس MyContext در معرض دید EF قرار گرفته است. در ادامه توسط کلاس Configuration نحوه آغاز بانک اطلاعاتی مشخص گردیده است؛ به همراه ثبت تعدادی رکورد نمونه.
نکته اصلی مورد بحث، کلاس کمکی EFUtils است که در آن دو متد الحاقی LoadEntities و LoadData تعریف شده‌اند. در متد LoadEntities، امضای متد شامل Expression Func است و در متد LoadData فقط Func ذکر شده است.
در ادامه اگر برنامه را توسط فراخوانی متد RunTests اجرا کنیم، به نظر شما خروجی SQL حاصل از list1 و list2 چیست؟
احتمالا شاید عنوان کنید که هر دو یک خروجی SQL دارند (با توجه به اینکه بدنه متدهای LoadEntities و LoadData دقیقا/یا به نظر یکی هستند) اما یکی از پارامتر 10 استفاده می‌کند و دیگری از پارامتر 20. تفاوت دیگری ندارند.
اما ... اینطور نیست!
خروجی SQL متد LoadEntities در متد RunTests به صورت زیر است:
 SELECT
[Extent1].[Id] AS [Id],
[Extent1].[TotalPrice] AS [TotalPrice]
FROM [dbo].[Receipts] AS [Extent1]
WHERE 10 = [Extent1].[TotalPrice]
و ... خروجی متد LoadData به نحو زیر:
 SELECT
[Extent1].[Id] AS [Id],
[Extent1].[TotalPrice] AS [TotalPrice]
FROM [dbo].[Receipts] AS [Extent1]
بله. در لیست دوم هیچ فیلتری انجام نشده (در حالت استفاده از Func خالی) و کل اطلاعات موجود در جدول Receipts، بازگشت داده شده است.
چرا؟
Func اشاره‌گری است به یک متد و Expression Func بیانگر ساختار درختی عبارت lambda نوشته شده است. این ساختار درختی صرفا بیان می‌کند که عبارت lambda منتسب، چه کاری را قرار است یا می‌تواند انجام دهد؛ بجای انجام واقعی آن.
 public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
اگر از Expression Func استفاده شود، از متد Where ایی استفاده خواهد شد که خروجی IQueryable دارد. اگر از Func استفاده شود، از overload دیگری که خروجی و ورودی  IEnumerable دارد به صورت خودکار استفاده می‌گردد.
بنابراین هرچند بدنه دو متد LoadEntities و LoadData به ظاهر یکی هستند، اما بر اساس نوع ورودی Where ایی که دریافت می‌کنند، اگر Expression Func باشد، EF فرصت آنالیز و ترجمه عبارت ورودی را خواهد یافت اما اگر Func باشد، ابتدا باید کل اطلاعات را به صورت یک لیست IEnumerable دریافت و سپس سمت کلاینت، خروجی نهایی را فیلتر کند.
اگر برنامه را اجرا کنید نهایتا هر دو لیست یک و دو، بر اساس شرط عنوان شده عمل خواهند کرد و فیلتر خواهند شد. اما در حالت اول این فیلتر شدن سمت بانک اطلاعاتی است و در حالت دوم کل اطلاعات بارگذاری شده و سپس سمت کاربر فیلتر می‌شود (که کارآیی پایینی دارد).


نتیجه گیری
به امضای متد Where ایی که در حال استفاده است دقت کنید. همینطور در مورد Sum ، Count و یا موارد مشابه دیگری که predicate قبول می‌کنند.
مطالب
تشخیص اصالت ردیف‌های یک بانک اطلاعاتی در EF Core
همیشه فرض بر این است که مدیر سیستم، فردی است امین و درستکار. این شخص/اشخاص کارهای شبکه، پشتیبان‌گیری، نگهداری و امثال آن‌را انجام داده و از سیستم‌ها محافظت می‌کنند. اکنون این سناریوهای واقعی را درنظر بگیرید:
- پس از خداحافظی با شرکتی که در آن کار می‌کردی، شخصی با پوزخند به شما می‌گوید که «می‌دونستی در برنامه‌ی حق و دستمزد شما، بچه‌های ادمین شبکه، دیتابیس برنامه رو مستقیما دستکاری می‌کردند و تعداد ساعات کاری بیشتری رو وارد می‌کردند»؟!
- مسئول فروشی/مسئول پذیرشی که یاد گرفته چطور به صورت مستقیم به بانک اطلاعاتی دسترسی پیدا کند و آمار فروش/پذیرش روز خودش را در بانک اطلاعاتی، با دستکاری مستقیم و خارج از برنامه، کمتر از مقدار واقعی نمایش دهد.
- باز هم مدیر سیستمی/شبکه‌ای که دسترسی مستقیم به بانک اطلاعاتی دارد، در ساعاتی مشخص، کلمه‌ی عبور هش شده‌ی خودش را مستقیما، بجای کلمه‌ی عبور ادمین برنامه در بانک اطلاعاتی وارد کرده و پس از آن ...

این موارد متاسفانه واقعی هستند! اکنون سؤال اینجا است که آیا برنامه‌ی شما قادر است تشخیص دهد رکوردهایی که هم اکنون در بانک اطلاعاتی ثبت شده‌اند، واقعا توسط برنامه و تمام سطوح دسترسی که برای آن طراحی کرده‌اید، به این شکل درآمده‌اند، یا اینکه توسط اشخاصی به صورت مستقیم و با دور زدن کامل برنامه، از طریق management studioهای مختلف، در سیستم وارد و دستکاری شده‌اند؟! در ادامه راه حلی را برای بررسی این مشکل مهم، مرور خواهیم کرد.


چگونه تغییرات رکوردها را در بانک‌های اطلاعاتی ردیابی کنیم؟

روش متداولی که برای بررسی تغییرات رکوردها مورد استفاده قرار می‌گیرد، هش کردن تمام اطلاعات یک ردیف از جدول است و سپس مقایسه‌ی این هش‌ها با هم. علت استفاده‌ی از الگوریتم‌های هش نیز، حداقل به دو علت است:
- با تغییر حتی یک بیت از اطلاعات، مقدار هش تولید شده تغییر می‌کند.
- طول نهایی مقدار هش شده‌ی اطلاعاتی حجیم، بسیار کم است و به راحتی توسط بانک‌های اطلاعاتی، قابل مدیریت و جستجو است.

اگر از SQL Server استفاده می‌کنید، یک چنین قابلیتی را به صورت توکار به همراه دارد:
SELECT
    [Id], 
    (SELECT top 1  * FROM  [AppUsers] FOR XML auto),
    HASHBYTES ('SHA2_256', (SELECT top 1  * FROM  [AppUsers] FOR XML auto)) AS [hash] -- varbinary(n), since 2012
FROM
    [AppUsers]
با این خروجی


کاری که این کوئری انجام می‌دهد شامل دو مرحله است:
الف) کوئری "SELECT top 1 * FROM [AppUsers] FOR XML auto" کاری شبیه به serialization را انجام می‌دهد. همانطور که مشاهده می‌کنید، نام و مقادیر تمام فیلدهای یک ردیف را به صورت یک خروجی XML در می‌آورد. بنابراین دیگر نیازی نیست تا کار تبدیل مقادیر تمام ستون‌های یک ردیف را به عبارتی قابل هش، به صورت دستی انجام دهیم؛ رشته‌ی XML ای آن هم اکنون آماده‌است.
ب) متد HASHBYTES، این خروجی serialized را با الگوریتم SHA2_256، هش می‌کند. الگوریتم‌های SHA2_256 و همچنین SHA2_512، از سال 2012 به بعد به SQL Server اضافه شده‌اند.

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


چگونه تغییرات رکوردها را در بانک‌های اطلاعاتی، توسط EF Core ردیابی کنیم؟

مزیت روش فوق، توکار بودن آن است که کارآیی فوق العاده‌ای را نیز به همراه دارد. اما چون در ادامه قصد داریم از یک ORM استفاده کنیم و ORMها نیز قرار است توانایی کار کردن با انواع و اقسام بانک‌های اطلاعاتی را داشته باشند، دو مرحله‌ی serialization و هش کردن را در کدهای برنامه و با مدیریت EF Core، مستقل از بانک اطلاعاتی خاصی، انجام خواهیم داد.


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

در مثالی که بررسی خواهیم کرد، دو موجودیت Blog و Post تعریف شده‌اند:
using System.Collections.Generic;

namespace EFCoreRowIntegrity
{
    public interface IAuditableEntity
    {
        string Hash { set; get; }
    }

    public static class AuditableShadowProperties
    {
        public static readonly string CreatedDateTime = nameof(CreatedDateTime);
        public static readonly string ModifiedDateTime = nameof(ModifiedDateTime);
    }

    public class Blog : IAuditableEntity
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; set; }

        public string Hash { get; set; }
    }

    public class Post : IAuditableEntity
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }

        public string Hash { get; set; }
    }
}
- در اینجا اینترفیس IAuditableEntity را نیز مشاهده می‌کنید که دارای یک خاصیت Hash است. تمام موجودیت‌هایی که قرار است دارای فیلد هش باشند، نیاز است این اینترفیس را پیاده سازی کنند؛ مانند دو موجودیت Blog و Post. در ادامه مقدار خاصیت هش را به صورت خودکار توسط سیستم Tracking، محاسبه و به روز رسانی می‌کنیم.
- به علاوه جهت تکمیل بحث، دو خاصیت سایه‌ای نیز تعریف شده‌اند تا بررسی کنیم که آیا هش این‌ها نیز درست محاسبه می‌شود یا خیر.
- علت اینکه خاصیت Hash، سایه‌ای تعریف نشد، سهولت دسترسی و بالا بردن کارآیی آن بود.



معرفی ظرفی برای نگهداری نام خواص و مقادیر متناظر با یک موجودیت

در ادامه دو کلاس AuditEntry و AuditProperty را مشاهده می‌کنید:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace EFCoreRowIntegrity
{
    public class AuditEntry
    {
        public EntityEntry EntityEntry { set; get; }
        public IList<AuditProperty> AuditProperties { set; get; } = new List<AuditProperty>();

        public AuditEntry() { }

        public AuditEntry(EntityEntry entry)
        {
            EntityEntry = entry;
        }
    }

    public class AuditProperty
    {
        public string Name { set; get; }
        public object Value { set; get; }

        public bool IsTemporary { set; get; }
        public PropertyEntry PropertyEntry { set; get; }

        public AuditProperty() { }

        public AuditProperty(string name, object value, bool isTemporary, PropertyEntry property)
        {
            Name = name;
            Value = value;
            IsTemporary = isTemporary;
            PropertyEntry = property;
        }
    }
}
زمانیکه توسط سیستم Tracking، موجودیت‌های اضافه شده و یا ویرایش شده را استخراج می‌کنیم، AuditEntry همان موجودیت در حال بررسی است که دارای تعدادی خاصیت یا AuditProperty می‌باشد. این‌ها را توسط دو کلاس فوق برای عملیات بعدی، ذخیره و نگهداری می‌کنیم.


معرفی روشی برای هش کردن مقادیر یک شیء

زمانیکه توسط سیستم Tracking، در حال کاربر بر روی موجودیت‌های اضافه شده و یا ویرایش شده هستیم، می‌خواهیم فیلد هش آن‌ها را نیز به صورت خودکار ویرایش و مقدار دهی کنیم. کلاس زیر، منطق ارائه دهنده‌ی این مقدار هش را بیان می‌کند:
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Newtonsoft.Json;

namespace EFCoreRowIntegrity
{
    public static class HashingExtensions
    {
        public static string GenerateObjectHash(this object @object)
        {
            if (@object == null)
            {
                return string.Empty;
            }

            var jsonData = JsonConvert.SerializeObject(@object, Formatting.Indented);
            using (var hashAlgorithm = new SHA256CryptoServiceProvider())
            {
                var byteValue = Encoding.UTF8.GetBytes(jsonData);
                var byteHash = hashAlgorithm.ComputeHash(byteValue);
                return Convert.ToBase64String(byteHash);
            }
        }

        public static string GenerateEntityEntryHash(this EntityEntry entry, string propertyToIgnore)
        {
            var auditEntry = new Dictionary<string, object>();
            foreach (var property in entry.Properties)
            {
                var propertyName = property.Metadata.Name;
                if (propertyName == propertyToIgnore)
                {
                    continue;
                }
                auditEntry[propertyName] = property.CurrentValue;
            }
            return auditEntry.GenerateObjectHash();
        }

        public static string GenerateEntityHash<TEntity>(this DbContext context, TEntity entity, string propertyToIgnore)
        {
            return context.Entry(entity).GenerateEntityEntryHash(propertyToIgnore);
        }
    }
}
- در اینجا توسط متد JsonConvert.SerializeObject کتابخانه‌ی Newtonsoft.Json، شیء موجودیت را تبدیل به یک رشته‌ی JSON کرده و توسط الگوریتم SHA256 هش می‌کنیم. در آخر هم این مقدار را به صورت Base64 ارائه می‌دهیم.
- نکته‌ی مهم: ما نمی‌خواهیم تمام خواص یک موجودیت را هش کنیم. برای مثال اگر موجودیتی دارای چندین رابطه با جداول دیگری بود، ما مقادیر این‌ها را هش نمی‌کنیم (چون رکوردهای متناظر با آن‌ها در جداول خودشان می‌توانند دارای فیلد هش مخصوصی باشند). بنابراین یک Dictionary را از خواص و مقادیر متناظر با آن‌ها تشکیل داده و این Dictionary را تبدیل به JSON می‌کنیم.
- همچنین در این بین، مقدار خود فیلد Hash یک شیء نیز نباید در هش محاسبه شده، حضور داشته باشد. به همین جهت پارامتر propertyToIgnore را مشاهده می‌کنید.


معرفی Context برنامه که کار هش کردن خودکار موجودیت‌ها را انجام می‌دهد

اکنون نوبت استفاده از تنظیمات انجام شده‌ی تا این مرحله‌است:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;

namespace EFCoreRowIntegrity
{
    public class BloggingContext : DbContext
    {
        public BloggingContext()
        { }

        public BloggingContext(DbContextOptions options)
            : base(options)
        { }

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.EnableSensitiveDataLogging();
                var path = Path.Combine(Directory.GetCurrentDirectory(), "app_data", "EFCore.RowIntegrity.mdf");
                optionsBuilder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=EFCore.RowIntegrity;AttachDbFilename={path};Trusted_Connection=True;");
                optionsBuilder.UseLoggerFactory(new LoggerFactory().AddConsole((message, logLevel) =>
                logLevel == LogLevel.Debug &&
                           message.StartsWith("Microsoft.EntityFrameworkCore.Database.Command")));
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            foreach (var entityType in modelBuilder.Model
                                                   .GetEntityTypes()
                                                   .Where(e => typeof(IAuditableEntity)
                                                   .IsAssignableFrom(e.ClrType)))
            {
                modelBuilder.Entity(entityType.ClrType)
                            .Property<DateTimeOffset?>(AuditableShadowProperties.CreatedDateTime);
                modelBuilder.Entity(entityType.ClrType)
                            .Property<DateTimeOffset?>(AuditableShadowProperties.ModifiedDateTime);
            }
        }

        public override int SaveChanges()
        {
            var auditEntries = OnBeforeSaveChanges();
            var result = base.SaveChanges();
            OnAfterSaveChanges(auditEntries);
            return result;
        }

        private IList<AuditEntry> OnBeforeSaveChanges()
        {
            var auditEntries = new List<AuditEntry>();

            foreach (var entry in ChangeTracker.Entries<IAuditableEntity>())
            {
                if (entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
                {
                    continue;
                }

                var auditEntry = new AuditEntry(entry);
                auditEntries.Add(auditEntry);

                var now = DateTimeOffset.UtcNow;

                foreach (var property in entry.Properties)
                {
                    var propertyName = property.Metadata.Name;
                    if (propertyName == nameof(IAuditableEntity.Hash))
                    {
                        continue;
                    }

                    if (property.IsTemporary)
                    {
                        // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges().
                        auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property));
                        continue;
                    }

                    switch (entry.State)
                    {
                        case EntityState.Added:
                            entry.Property(AuditableShadowProperties.CreatedDateTime).CurrentValue = now;
                            auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property));
                            break;
                        case EntityState.Modified:
                            auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property));
                            entry.Property(AuditableShadowProperties.ModifiedDateTime).CurrentValue = now;
                            break;
                    }
                }
            }

            return auditEntries;
        }

        private void OnAfterSaveChanges(IList<AuditEntry> auditEntries)
        {
            foreach (var auditEntry in auditEntries)
            {
                foreach (var auditProperty in auditEntry.AuditProperties.Where(x => x.IsTemporary))
                {
                    // Now we have the auto-generated value from the DB.
                    auditProperty.Value = auditProperty.PropertyEntry.CurrentValue;
                    auditProperty.IsTemporary = false;
                }
                auditEntry.EntityEntry.Property(nameof(IAuditableEntity.Hash)).CurrentValue =
                    auditEntry.AuditProperties.ToDictionary(x => x.Name, x => x.Value).GenerateObjectHash();
            }
            base.SaveChanges();
        }
    }
}
در اینجا اصل کار، در متد بازنویسی شده‌ی SaveChanges انجام می‌شود:
public override int SaveChanges()
{
    var auditEntries = OnBeforeSaveChanges();
    var result = base.SaveChanges();
    OnAfterSaveChanges(auditEntries);
    return result;
}
در متد OnBeforeSaveChanges، تمام موجودیت‌های تغییر کرده‌ی از نوع IAuditableEntity را که دارای فیلد هش هستند، یافته و نام خاصیت و مقدار متناظر با آن‌ها را در ظرف‌های AuditEntry که پیشتر معرفی شدند، ذخیره می‌کنیم. هنوز در این مرحله کار هش کردن را انجام نخواهیم داد. علت را می‌توانید در بررسی خواص موقتی مشاهده کنید:
if (property.IsTemporary)
{
   // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges().
   auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property));
   continue;
}
خواص موقتی، عموما تولید شده‌ی توسط دیتابیس هستند. برای مثال زمانیکه یک Id عددی خود افزاینده را به عنوان کلید اصلی جدول معرفی می‌کنید، مقدار آن پس از فراخوانی متد base.SaveChanges، از بانک اطلاعاتی دریافت شده و در اختیار برنامه قرار می‌گیرد. به همین جهت است که نیاز داریم لیست این خواص و مقادیر را یکبار پیش از base.SaveChanges ذخیره کنیم و پس از آن، خواص موقتی را که اکنون دارای مقدار هستند، مقدار دهی کرده و سپس هش نهایی شیء را محاسبه کنیم. اگر پیش از base.SaveChanges این هش را محاسبه کنیم، برای مثال حاوی مقدار Id شیء، نخواهد بود.

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


روش بررسی اصالت یک موجودیت

در متد زیر، روش محاسبه‌ی هش واقعی یک موجودیت دریافت شده‌ی از بانک اطلاعاتی را توسط متد الحاقی GenerateEntityHash مشاهده می‌کنید. اگر این هش واقعی (بر اساس مقادیر فعلی این ردیف که حتی ممکن است به صورت دستی و خارج از برنامه تغییر کرده باشد)، با مقدار Hash ثبت شده‌ی پیشین در آن ردیف یکی بود، اصالت این ردیف تائید خواهد شد:
private static void CheckRow1IsAuthentic()
{
    using (var context = new BloggingContext())
    {
        var blog1 = context.Blogs.Single(x => x.BlogId == 1);
        var entityHash = context.GenerateEntityHash(blog1, propertyToIgnore: nameof(IAuditableEntity.Hash));
        var dbRowHash = blog1.Hash;
        Console.WriteLine($"entityHash: {entityHash}\ndbRowHash:  {dbRowHash}");
        if (entityHash == dbRowHash)
        {
            Console.WriteLine("This row is authentic!");
        }
        else
        {
            Console.WriteLine("This row is tampered outside of the application!");
        }
    }
}
یک نمونه خروجی آن به صورت زیر است:
entityHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
This row is authentic!

اکنون بانک اطلاعاتی را خارج از برنامه، مستقیما دستکاری می‌کنیم و برای مثال Url اولین ردیف را تغییر می‌دهیم:


در ادامه یکبار دیگر برنامه را اجرا خواهیم کرد:
entityHash: tdiZhKMJRnROGLLam1WpldA0fy/CbjJaR2Y2jNU9izk=
dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
This row is tampered outside of the application!
همانطور که مشاهده می‌کنید، هش واقعی جدید، با هش ثبت شده‌ی در ردیف، یکی نیست؛ که بیانگر ویرایش مستقیم این ردیف می‌باشد.
به علاوه باید درنظر داشت، محاسبه‌ی این هش بدون خود برنامه، کار ساده‌ای نیست. به همین جهت به روز رسانی دستی آن تقریبا غیرممکن است؛ خصوصا اگر متد GenerateObjectHash، کمی با پیچ و تاب بیشتری نیز تهیه شود.


چگونه وضعیت اصالت تعدادی ردیف را بررسی کنیم؟

مثال قبل، در مورد روش بررسی اصالت یک تک ردیف بود. کوئری زیر روش محاسبه‌ی فیلد جدید IsAuthentic را در بین لیستی از ردیف‌ها نمایش می‌دهد:
var blogs = (from blog in context.Blogs.ToList() // Note: this `ToList()` is necessary here for having Shadow properties values, otherwise they will considered `null`.
             let computedHash = context.GenerateEntityHash(blog, nameof(IAuditableEntity.Hash))
             select new
             {
               blog.BlogId,
               blog.Url,
               RowHash = blog.Hash,
               ComputedHash = computedHash,
               IsAuthentic = blog.Hash == computedHash
             }).ToList();


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: EFCoreRowIntegrity.zip
مطالب
نحوه استخراج آیکون‌های یک قلم در WPF
مطلب «نحوه نمایش تمام آیکون‌های تعریف شده در یک قلم در WPF» را در نظر بگیرید. سؤال: اگر در یک برنامه تنها به تعدادی از این آیکون‌ها یا گلیف‌ها نیاز بود آیا می‌توان این‌ها را به صورت مجزا استخراج و استفاده کرد؟
پاسخ: بلی. همان کلاس  FontFamily موجود در اسمبلی PresentationCore.dll، امکان تبدیل یک گلیف را به معادل هندسی آن نیز دارد. در ادامه کدهای آن‌را مرور خواهیم کرد:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Media;
using CrMap.Models;

namespace CrMap.ViewModels
{
    public class CrMapViewModel
    {
        public IList<Symbol> Symbols { set; get; }
        public int GridRows { set; get; }
        public int GridCols { set; get; }

        public CrMapViewModel()
        {
            fillDataSource();
        }

        private void fillDataSource()
        {
            Symbols = new List<Symbol>();
            GridCols = 15;

            var fontFamily = new FontFamily(new Uri("pack://application:,,,/"), "/Fonts/#whhglyphs");

            GlyphTypeface glyph = null;
            Typeface glyphTypeface = null;
            foreach (var typeface in fontFamily.GetTypefaces())
            {
                if (typeface.TryGetGlyphTypeface(out glyph) && (glyph != null))
                {
                    glyphTypeface = typeface;
                    break;
                }
            }

            if (glyph == null)
                throw new InvalidOperationException("Couldn't find a GlyphTypeface.");

            GridRows = (glyph.CharacterToGlyphMap.Count / GridCols) + 1;

            foreach (var item in glyph.CharacterToGlyphMap)
            {
                var index = item.Key;
                Symbols.Add(new Symbol
                {
                    Character = Convert.ToChar(index),
                    CharacterCode = string.Format("&#x{0:X}", index)
                });

                saveToFile(glyphTypeface, index);
            }
        }

        private static void saveToFile(Typeface glyphTypeface, int index)
        {
            var formattedText = new FormattedText(
                                        textToFormat: Convert.ToChar(index).ToString(),
                                        culture: new CultureInfo("en-us"),
                                        flowDirection: FlowDirection.LeftToRight,
                                        typeface: glyphTypeface,
                                        emSize: 20,
                                        foreground: Brushes.Black);
            var geometry = formattedText.BuildGeometry(new Point(0, 0));
            var path = geometry.GetFlattenedPathGeometry();
            File.WriteAllText(index + ".path", path.ToString());
        }
    }
}
در اینجا تنها متد saveToFile در مقایسه با قسمت قبل افزوده شده است.
شیء FormattedText دارای متدی است به نام BuildGeometry که اطلاعات یک گلیف را تبدیل به معادل هندسی آن می‌کند. سپس توسط GetFlattenedPathGeometry معادل Path آن‌را می‌توان بدست آورد. برای مثال اگر پس از اجرای این مثال، به فایل 48.path تولیدی آن مراجعه کنیم، چنین خروجی را می‌توان مشاهده کرد:
 F1M5,7.47150993347168L5,17.2566661834717 5.732421875,19.0242443084717 7.5,19.7566661834717
12.5,19.7566661834717 13.69140625,19.4441661834717 5,7.47150993347168z
M7.5,4.75666618347168L6.30859375,5.06916618347168 15,17.0418224334717
15,7.25666618347168 14.267578125,5.48908805847168 12.5,4.75666618347168
7.5,4.75666618347168z M7.5,2.25666618347168L12.5,2.25666618347168
14.4189453125,2.62287712097168 16.03515625,3.72150993347168 17.1337890625,5.33772134780884
17.5,7.25666618347168 17.5,17.2566661834717 17.1337890625,19.1756114959717
16.03515625,20.7918224334717 14.4189453125,21.8904552459717 12.5,22.2566661834717
7.5,22.2566661834717 5.5810546875,21.8904552459717 3.96484375,20.7918224334717
2.8662109375,19.1756114959717 2.5,17.2566661834717 2.5,7.25666618347168 2.8662109375,5.33772134780884
3.96484375,3.72150993347168 5.5810546875,2.62287712097168 7.5,2.25666618347168z
که برای استفاده از اطلاعات آن در WPF می‌توان نوشت:
<Path Stroke="DarkRed" Fill="Black" Data="F1M5,7.47150993347168L5,17.2566661834717
              5.732421875,19.0242443084717 7.5,19.7566661834717 12.5,19.7566661834717 13.69140625,19.4441661834717 
              5,7.47150993347168z M7.5,4.75666618347168L6.30859375,5.06916618347168 15,17.0418224334717 
              15,7.25666618347168 14.267578125,5.48908805847168 12.5,4.75666618347168 7.5,4.75666618347168z 
              M7.5,2.25666618347168L12.5,2.25666618347168 14.4189453125,2.62287712097168 
              16.03515625,3.72150993347168 17.1337890625,5.33772134780884 17.5,7.25666618347168 
              17.5,17.2566661834717 17.1337890625,19.1756114959717 16.03515625,20.7918224334717 
              14.4189453125,21.8904552459717 12.5,22.2566661834717 7.5,22.2566661834717 
              5.5810546875,21.8904552459717 3.96484375,20.7918224334717 2.8662109375,19.1756114959717 
              2.5,17.2566661834717 2.5,7.25666618347168 2.8662109375,5.33772134780884 
              3.96484375,3.72150993347168 
              5.5810546875,2.62287712097168 7.5,2.25666618347168z" />
مطالب
نوشتن پرس و جو در Entity Framework‌ با استفاده از LINQ To Entity قسمت اول

موجودیت‌های زیر را در نظر بگیرید: 

public class Customer
{
    public Customer()
    {
        Orders = new ObservableCollection<Order>();
    }
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }

    public string FullName
    {
        get
        {
            return Name + " " + Family;
        }
    }

    public virtual IList<Order> Orders { get; set; }
}
public class Product
{
    public Product()
    {
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
}
public class OrderDetail
{
    public Guid Id { get; set; }
    public Guid ProductId { get; set; }
    public int Count { get; set; }
    public Guid OrderId { get; set; }
    public int Price { get; set; }

    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }

    public string ProductName
    {
        get
        {
            return Product != null ? Product.Name : string.Empty;
        }
    }
}
public class Order
{
    public Order()
    {
        OrderDetail = new ObservableCollection<OrderDetail>();
    }
    public Guid Id { get; set; }
    public DateTime Date { get; set; }

    public Guid CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
    public virtual IList<OrderDetail> OrderDetail { get; set; }

    public string CustomerFullName
    {
        get
        {
            return Customer == null ? string.Empty : Customer.FullName;
        }
    }

    public int TotalPrice
    {
        get
        {
            if (OrderDetail == null)
                return 0;

            return
                OrderDetail.Where(orderdetail => orderdetail.Product != null)
                .Sum(orderdetail => orderdetail.Price*orderdetail.Count);
        }
    }
}

و نگاشت موجودیت ها: 

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration()
    {
        HasKey(c => c.Id);
        Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        HasKey(p => p.Id);
        Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderDetailConfiguration : EntityTypeConfiguration<OrderDetail>
{
    public OrderDetailConfiguration()
    {
        HasKey(od => od.Id);
        Property(od => od.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderConfiguration: EntityTypeConfiguration<Order>
{
    public OrderConfiguration()
    {
        HasKey(o => o.Id);
        Property(o => o.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}

و برای معرفی موجودیت‌ها به Entity Framwork کلاس StoreDbContext را به صورت زیر تعریف می‌کنیم:

public class StoreDbContext : DbContext
{
    public StoreDbContext()
        : base("name=StoreDb")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new CustomerConfiguration());
        modelBuilder.Configurations.Add(new OrderConfiguration());
        modelBuilder.Configurations.Add(new OrderDetailConfiguration());
        modelBuilder.Configurations.Add(new ProductConfiguration());
    }

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderDetail> OrderDetails { get; set; }
}

جهت مقدار دهی اولیه به database تستی یک DataBaseInitializer به صورت زیر تعریف می‌کنیم:

public class MyTestDb : DropCreateDatabaseAlways<StoreDbContext>
{
    protected override void Seed(StoreDbContext context)
    {
        var customer1 = new Customer { Name = "Vahid", Family = "Nasiri" };
        var customer2 = new Customer { Name = "Mohsen", Family = "Jamshidi" };
        var customer3 = new Customer { Name = "Mohsen", Family = "Akbari" };

        var product1 = new Product {Name = "CPU", Price = 350000};
        var product2 = new Product {Name = "Monitor", Price = 500000};
        var product3 = new Product {Name = "Keyboard", Price = 30000};
        var product4 = new Product {Name = "Mouse", Price = 20000};
        var product5 = new Product {Name = "Power", Price = 70000};
        var product6 = new Product {Name = "Hard", Price = 250000};

        var order1 = new Order
        {
            Customer = customer1, Date = new DateTime(2013, 1, 1),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 1, Price = product1.Price},
                new OrderDetail {Product = product2, Count = 1, Price = product2.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
            }
        };

        var order2 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 5),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 2, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 4, Price = product3.Price},
            }
        };

        var order3 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 4, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 5, Price = product3.Price},
                new OrderDetail {Product = product5, Count = 6, Price = product5.Price},
            }
        };

        var order4 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
                new OrderDetail {Product = product6, Count = 1, Price = product6.Price},
            }
        };

        var order5 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 12),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product5, Count = 2, Price = product5.Price},
                new OrderDetail {Product = product6, Count = 5, Price = product6.Price},
            }
        };

        context.Customers.Add(customer3);

        context.Orders.Add(order1);
        context.Orders.Add(order2);
        context.Orders.Add(order3);
        context.Orders.Add(order4);
        context.Orders.Add(order5);

        context.SaveChanges();
    }

و در ابتدای برنامه کد زیر را جهت مقداردهی اولیه به Database مان قرار می‌دهیم:

Database.SetInitializer(new MyTestDb());

در انتها ConnectionString را در App.Config به صورت زیر تعریف می‌کنیم:

<connectionStrings>
    <add name="StoreDb" connectionString="Data Source=.\SQLEXPRESS;
Initial Catalog=StoreDBTest;Integrated Security = true" providerName="System.Data.SqlClient"/>
</connectionStrings>

بسیار خوب، حالا همه چیز محیاست برای اجرای اولین پرس و جو:

using (var context = new StoreDbContext())
{
    var query = context.Customers;

    foreach (var customer in query)
    {
        Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                  customer.Name, customer.Family);
    }
}

پرس و جوی تعریف شده لیست تمام Customer‌ها را باز می‌گرداند. query فقط یک "عبارت" پرس و جو هست و زمانی اجرا می‌شود که از آن درخواست نتیجه شود. در مثال بالا این درخواست در اجرای حلقه foreach اتفاق می‌افتد و درست در این لحظه است که دستور SQL ساخته شده و به Database فرستاده می‌شود. EF در این حالت تمام داده‌ها را در یک لحظه باز نمی‌گرداند بلکه این ارتباط فعال است تا حلقه به پایان برسد و تمام داده‌ها از database واکشی شود. خروجی به صورت زیر خواهد بود:

Customer Name: Vahid, Customer Family: Nasiri
Customer Name: Mohsen, Customer Family: Jamshidi
Customer Name: Mohsen, Customer Family: Akbari

نکته: با هر بار درخواست نتیجه از query ، پرس و جوی مربوطه دوباره به database فرستاده می‌شود که ممکن است مطلوب ما نباشد و باعث افت سرعت شود. برای جلوگیری از تکرار این عمل کافیست با استفاده از متد ToList پرس و جو را در لحظه تعریف به اجرا در آوریم
var customers = context.Customers.ToList();

خط بالا دیگر یک عبارت پرس و جو نخواهد بود بلکه لیست تمام Customer هاست که به یکباره از database بازگشت داده شده است. در ادامه هرجا که از customers استفاده کنیم دیگر پرس و جویی به database فرستاده نخواهد شد.

پرس و جوی زیر مشتریهایی که نام آنها Mohsen هست را باز می‌گرداند:

private static void Query3()
{
    using (var context = new StoreDbContext())
    {
        var methodSyntaxquery = context.Customers
                   .Where(c => c.Name == "Mohsen");
        var sqlSyntaxquery = from c in context.Customers
                             where c.Name == "Mohsen"
                             select c;

        foreach (var customer in methodSyntaxquery)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                      customer.Name, customer.Family);
        }
    }

    // Output:
    // Customer Name: Mohsen, Customer Family: Jamshidi
    // Customer Name: Mohsen, Customer Family: Akbari
}

همانطور که مشاهده می‌کنید پرس و جو به دو روش Method Syntax و Sql Syntax نوشته شده است.

روش Method Syntax روشی است که از متدهای الحاقی (Extention Method)  ‌و عبارت‌های لامبدا (Lambda Expersion) برای نوشتن پرس و جو استفاده می‌شود. اما #C روش Sql Syntax را که همانند دستورات SQL هست، نیز فراهم کرده است تا کسانیکه آشنایی با این روش دارند، از این روش استفاده کنند. در نهایت این روش به Method Syntax تبدیل خواهد شد بنابراین پیشنهاد می‌شود که از همین روش استفاده شود تا با دست و پنجه نرم کردن با این روش، از مزایای آن در بخشهای دیگر کدنویسی استفاده شود.

اگر به نوع Customers که در DbContext تعریف شده است، دقت کرده باشید، خواهید دید که DbSet می‌باشد. DbSet کلاس و اینترفیس‌های متفاوتی را پیاده سازی کرده است که در ادامه با آنها آشنا خواهیم شد:

  • IQueryable<TEntity>, IEnumerable<TEntity>, IQueryable, IEnumerable: که امکان استفاده از متدهای نام آشنای LINQ را برای ما فراهم می‌کند. البته فراموش نشود که EF از Provider ای با نام LINQ To Entity برای تفسیر پرس و جوی ما و ساخت دستور SQL متناظر آن استفاده می‌کند. بنابراین تمامی متدهایی که در LINQ To Object استفاده می‌شوند در اینجا قابل استفاده نیستند. بطور مثال اگر در پرس و جو از LastOrDefault روی Customer استفاده شود در زمان اجرا با خطای زیر مواجه خواهیم شد و در نتیجه در استفاده از این متدها به این مسئله باید دقت شود. 
LINQ to Entities does not recognize the method 'Store.Model.Customer LastOrDefault[Customer](System.Linq.IQueryable`1[Store.Model.Customer], System.Linq.Expressions.Expression`1[System.Func`2[Store.Model.Customer,System.Boolean]])' method, and this method cannot be translated into a store expression.
  • <IDbSet<TEntity: که دارای متدهای Add, Attach, Create, Find, Remove, Local می‌باشد و برای بحث ما Find و Local جهت ساخت پرس و جو استفاده می‌شوند که  در ادامه توضیح داده خواهند شد.
  • <DbQuery<TEntity: که دارای متدهای AsNoTracking و Include می‌باشد و در ادامه توضیح داده خواهند شد.

متد Find: این متد کلید اصلی را به عنوان ورودی گرفته و برای بازگرداندن نتیجه مراحل زیر را طی می‌کند:
  1. داده‌های موجود در حافظه را بررسی می‌کند یعنی آنهایی که Load و یا Attach شده اند.
  2. داده هایی که به DbContext اضافه (Add) ولی هنوز در database درج نشده اند.
  3. داده هایی که در database هستند ولی هنوز Load نشده اند.
Find در صورت پیدا نکردن Exception ای صادر نمی‌کند بلکه مقدار null را بر می‌گرداند.
private static void Query4()
{
    using (var context = new StoreDbContext())
    {
        var customer = context.Customers.Find(new Guid("2ee2fd32-e0e9-4955-bace-1995839d4367"));

        if (customer == null)
            Console.WriteLine("Customer not found");
        else
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
    }
}
با توجه به اینکه Id‌ها توسط Database ساخته می‌شوند. شما باید از Id دیگری که موجود می‌باشد، استفاده کنید تا نتیجه ای برگشت داده شود.
نکته: در صورتیکه کلید اصلی شما از دو یا چند فیلد تشکیل شده بود. می‌بایست این دو یا چند مقدار را به عنوان پارامتر به Find بفرستید.

متد Single: گاهی نیاز هست که داده‌ای پرس و جو شود اما نه با کلید اصلی بلکه با شرط دیگری، در این حالت از Single استفاده می‌شود. این متد یک مقدار را باز می‌گرداند و در صورتی که صفر یا بیش از یک مقدار در شرط صدق کند exception صادر می‌کند. متد SingleOrDefault رفتاری مشابه دارد اما اگر مقداری در شرط صدق نکند مقدار پیش فرض را باز می‌گرداند.
نکته: مقدار پیش فرض بستگی به نوع خروجی دارد که اگر object باشد مقدار null و اگر بطور مثال نوع عددی باشد، صفر می‌باشد.
private static void Query5()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.Single(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        try
        {
            var customer2 = context.Customers.Single(c => c.Name == "Mohsen");  // Exception: Sequence contains more than one element
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer3 = context.Customers.SingleOrDefault(c => c.Name == "Unkown");  // customer3 == null

        var customer4 = context.Customers.Single(c => c.Name == "Vahid");  // customer4 != null
    }
}

متد First:
در صورتیکه به اولین نتیجه پرس و جو نیاز هست می‌توان از First استفاده کرد. اگر پرس و جو نتیجه در بر نداشته باشد یعنی null باشد exception صادر خواهد شد اما اگر FirstOrDefault استفاده شود مقدار پیش فرض برگردانده خواهد شد.
private static void Query6()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.First(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer2 = context.Customers.FirstOrDefault(c => c.Name == "Unknown");  // customer2 == null
        var customer3 = context.Customers.First(c => c.Name == "Mohsen");
    }
}

نظرات مطالب
نوشتن TagHelperهای سفارشی برای ASP.NET Core
یک نکته: برای تبدیل محتوای یک TagBuilder به رشته می‌توان از متد زیر استفاده کرد:
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;

namespace Sample
{
    /// <summary>
    /// Html Helper Extensions
    /// </summary>
    public static class HtmlHelperExtensions
    {
        /// <summary>
        /// Convert IHtmlContent/TagBuilder to string
        /// </summary>
        public static string GetString(this IHtmlContent content)
        {
            using (var writer = new StringWriter())
            {
                content.WriteTo(writer, HtmlEncoder.Default);
                return writer.ToString();
            }
        }
    }
}
مطالب
نحوه نمایش تمام آیکون‌های تعریف شده در یک قلم در WPF
سال نو مبارک! به امید روزهایی شاد، سلامت و پر برکت.

پیرو مطلب قلم‌هایی حاوی آیکون که خصوصا در برنامه‌های مترو بیشتر مرسوم شده‌اند، شاید بد نباشد کار برنامه Character Map ویندوز را با WPF شبیه سازی کنیم.
ابتدا Model و ViewModel  این برنامه را درنظر بگیرید:
namespace CrMap.Models
{
    public class Symbol
    {
        public char Character { set; get; }
        public string CharacterCode { set; get; }
    }
}

using System;
using System.Collections.Generic;
using System.Windows.Media;
using CrMap.Models;

namespace CrMap.ViewModels
{
    public class CrMapViewModel
    {
        public IList<Symbol> Symbols { set; get; }
        public int GridRows { set; get; }
        public int GridCols { set; get; }

        public CrMapViewModel()
        {
            fillDataSource();
        }

        private void fillDataSource()
        {
            Symbols = new List<Symbol>();
            GridCols = 15;

            var fontFamily = new FontFamily(new Uri("pack://application:,,,/"), "/Fonts/#whhglyphs");

            GlyphTypeface glyph = null;
            foreach (var typeface in fontFamily.GetTypefaces())
            {
                if (typeface.TryGetGlyphTypeface(out glyph) && (glyph != null))
                    break;
            }

            if (glyph == null)
                throw new InvalidOperationException("Couldn't find a GlyphTypeface.");

            GridRows = (glyph.CharacterToGlyphMap.Count / GridCols) + 1;

            foreach (var item in glyph.CharacterToGlyphMap)
            {
                var index = item.Key;
                Symbols.Add(new Symbol
                {
                    Character = Convert.ToChar(index),
                    CharacterCode = string.Format("&#x{0:X}", index)
                });
            }
        }
    }
}
توضیحات:

یک سری قابلیت جالب در WPF برای استخراج اطلاعات قلم‌ها وجود دارند که در فضای نام System.Windows.Media اسمبلی PresentationCore.dll قرار گرفته‌اند. برای نمونه پس از وهله سازی FontFamily بر اساس یک قلم مدفون شده در برنامه، امکان استخراج تعداد گلیف‌های موجود در این قلم وجود دارد که نحوه انجام آن‌را در متد fillDataSource ملاحظه می‌کنید.
این اطلاعات استخراج شده و لیست Symbols برنامه را تشکیل می‌دهند. در نهایت برای نمایش این اطلاعات، از ترکیب ItemsControl و UniformGrid استفاده خواهیم کرد:
<Window x:Class="CrMap.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:CrMap.ViewModels"
        Title="MainWindow" WindowStartupLocation="CenterScreen" WindowState="Maximized"
        Height="350" Width="525">
    <Window.Resources>
        <vm:CrMapViewModel x:Key="vmCrMapViewModel" />
    </Window.Resources>
    <ScrollViewer VerticalScrollBarVisibility="Visible">
        <ItemsControl
                      DataContext="{StaticResource vmCrMapViewModel}"  
                      ItemsSource="{Binding Symbols}"
                      Name="MainItemsControl"
                      VerticalAlignment="Top"     
                      HorizontalAlignment="Center"
                      Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="4">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Columns="{Binding GridCols}" 
                        Rows="{Binding GridRows}">
                    </UniformGrid>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <ContentControl>
                        <Border BorderBrush="SlateGray"
                            HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                            BorderThickness="1" CornerRadius="3" Margin="3">
                            <StackPanel Margin="3" Orientation="Vertical">
                                <TextBlock                                         
                                    HorizontalAlignment="Center" 
                                    VerticalAlignment="Center" 
                                    FontFamily="Fonts/#whhglyphs"
                                    Foreground="DarkRed"
                                    FontSize="17"
                                    Text="{Binding Character}" />
                                <TextBlock                                         
                                    HorizontalAlignment="Center" 
                                    VerticalAlignment="Center" 
                                    Text="{Binding CharacterCode}" />
                            </StackPanel>
                        </Border>
                    </ContentControl>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Window>


دریافت مثال این مطلب
CrMap.zip