مطالب
ساخت قالب‌های نمایشی و ادیتور دکمه سه وضعیتی سازگار با Twitter bootstrap در ASP.NET MVC
گروه بندی دکمه‌ها در Twitter bootstrap

    <div class="btn-group" data-toggle="buttons-radio">
        <button class="btn" type="button">بلی</button>
        <button class="btn" type="button">خیر</button>
    </div>
در این مثال دو دکمه را ملاحظه می‌کنید که در یک div با کلاس btn-group محصور شده‌اند. به این ترتیب این دو دکمه در کنار هم، همانند دکمه‌های یک toolbar قرار خواهند گرفت. همچنین در بوت‌استرپ امکان انتساب ویژگی data-toggle=buttons-radio نیز به این div وجود دارد. در این حالت، این دکمه‌ها همانند دکمه‌های رادیویی رفتار خواهند کرد:

در ادامه قصد داریم یک Editor template و یک Display template مخصوص را جهت تدارک یک چنین دکمه‌هایی، برای مدیریت خواص Boolean ایجاد کنیم. به عبارتی اگر مدل برنامه چنین تعاریفی را داشت:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Mvc4TwitterBootStrapTest.Models
{
    public class User
    {
        public int Id { set; get; }

        [DisplayName("نام")]
        [Required(ErrorMessage="لطفا نام را تکمیل کنید")]
        public string Name { set; get; }

        [DisplayName("نام خانوادگی")]
        [Required(ErrorMessage = "لطفا نام خانوادگی را تکمیل کنید")]
        public string LastName { set; get; }

        [DisplayName("فعال است؟")]
        [UIHint("BootstrapBoolean")]
        public bool? IsActive { set; get; }
    }
}
فیلد nullable bool آن به صورت خودکار به شکل زیر رندر شود:


تهیه قالب ادیتور Views\Shared\EditorTemplates\BootstrapBoolean.cshtml

@model bool?
@{  
    var yesIsSelected = Model.HasValue && Model.Value ? "active" : null;
    var noIsSelected = Model.HasValue && !Model.Value ? "active" : null;
    var isIndeterminate = !Model.HasValue ? "active" : null;
    var htmlField = ViewData.TemplateInfo.HtmlFieldPrefix;   
}
@Html.HiddenFor(model => model)
<div class="btn-group" data-toggle="buttons-radio">
    <button type="button" class="btn btn-info @yesIsSelected bool-@htmlField" onclick="$('#@htmlField').val(true);">
        بلی</button>
    <button type="button" class="btn btn-info @noIsSelected bool-@htmlField" onclick="$('#@htmlField').val(false);">
        خیر</button>
    @if (ViewData.ModelMetadata.IsNullableValueType)
    {
        <button type="button" class="btn btn-info @isIndeterminate bool-@htmlField" onclick="$('#@htmlField').val('');">
            نامشخص</button> 
    }
</div>
سورس کامل فایل BootstrapBoolean.cshtml را که در مسیر Views\Shared\EditorTemplates باید کپی شود، در اینجا ملاحظه می‌کنید.
نوع اطلاعاتی که این قالب ادیتور پردازش خواهد کرد از نوع nullable bool است. البته مشکلی هم با نوع‌های bool معمولی ندارد. در حالت nullable، دکمه سومی را به نام «نامشخص» به مجموعه دکمه‌های «بلی» و «خیر» اضافه می‌کند. گاهی از اوقات در فرم‌های دریافت اطلاعات نیاز است بررسی کنیم آیا واقعا کاربر اطلاعاتی را انتخاب کرده یا اینکه بدون توجه به فیلدها، بر روی دکمه ارسال کلیک کرده است. در یک چنین حالتی تعریف دکمه‌های سه وضعیتی Boolean می‌تواند مفهوم پیدا کند.
در مورد اصول تهیه این قالب در ابتدای مطلب، با کلاس‌های btn-group و ویژگی data-toggle آشنا شدید. دقیقا این سه دکمه نیز در اینجا به همین نحو تعریف شده‌اند.
در ابتدای نمایش یک View، خصوصا در حالت ویرایش اطلاعات، نیاز است اطلاعات موجود، به دکمه‌های تعریف شده اعمال شوند. در اینجا برای انتخاب یک دکمه، باید کلاس active به آن نسبت داده شود، که نحوه تدارک آن‌را در سه متغیر yesIsSelected، noIsSelected و isIndeterminate ابتدای تعاریف قالب مشاهده می‌کنید.
سپس یک فیلد مخفی به صفحه اضافه شده است. از این جهت که به کمک jQuery، در حین کلیک بر روی یکی از دکمه‌ها، مقدار آن‌را به این فیلد که نهایتا به سرور ارسال خواهد شد، اعمال خواهیم کرد.


تهیه قالب نمایشی Views\Shared\DisplayTemplates\BootstrapBoolean.cshtml

@model bool?
@if (Model.HasValue)
{
    if (Model.Value)
    {
    <span class="label label-success">بلی</span> 
    }
    else
    {
    <span class="label label-important">خیر</span> 
    }
}
else
{ 
    <span class="label label-inverse">نامشخص</span> 
}
در حالت صرفا نمایشی، فایل قالب BootstrapBoolean.cshtml قرار گرفته در مسیر Views\Shared\DisplayTemplates، یک چنین تعاریفی را خواهد داشت.

و نهایتا برای استفاده از آن تنها کافی است توسط ویژگی UIHint، نام این قالب، به خاصیت Boolean مدنظر اعمال شود:
[UIHint("BootstrapBoolean")]
public bool? IsActive { set; get; }

نظرات مطالب
چگونگی رسیدگی به Null property در AutoMapper
سلام
من از Automapper واسه مپ کردن مدل و ویو مادل‌ها تو برنامه ام استفاده میکنم
مشکلم اینجاست که در کلاس model یه فیلد از نوع byte[] دارم
 public class News : BaseEntity
    {
public byte[] SmallImage { get; set; } // SmallImage
     }
در viewModel هم دقیقا همین فیلد رو دارم.
منتهی موقع مپ کردن این فیلد null میشه.
news = model.ToEntity(news);
public static News ToEntity(this NewsModel model, News destination)
        {
            Mapper.CreateMap<NewsModel, News>();
            return Mapper.Map(model, destination);
        }
علتش رو نمیدونم، کسی هست بدونه؟
باتشکر
مطالب
آشنایی با مفهوم Indexer در C#.NET
زمانی که صحبت از Indexer می‌شود، بطور ناخوداگاه ذهنمان به سمت آرایه‌ها می‌رود. آرایه‌ها در واقع ساده‌ترین اشیاء ی هستند که مفهوم Index در آنها معنا دار است.
اگر با آرایه‌ها کار کرده باشید با عملگر [] در سی شارپ آشنایی دارید. یک Indexer در واقع نوع خاصی از خاصیت (property) است که در بدنه کلاس تعریف می‌شود و به ما امکان استفاده از عملگر [] را برای نمونه کلاس فراهم می‌کند.
همانطور که به شباهت Indexer و Property اشاره شد نحوه تعریف Indexer بصورت زیر می‌باشد:
public type this [type identifier]
{
     get{ ... }
     set{ ... }
}
در پیاده سازی Indexer به نکات زیر دقت کنید:
  • از کلمه کلیدی this برای نعریف Indexer استفاده می‌شود و یک Indexer نام ندارد.
  • از دستیاب get برای برگرداندن مقدار و از دستیاب set جهت مقدار دهی و انتساب استفاده می‌شود.
  • الزامی به پیاده سازی Indexer با مقادیر صحیح عددی نیست و شما می‌توانید Indexer ی با پارامترهایی از نوع string یا double داشته باشید.
  • Indexer‌ها قابلیت سربارگذاری (overloaded) دارند.
  • Indexer‌ها می‌توانند چندین پارامتری باشند مانند آرایه‌های دو بعدی که دو پارامتری هستند.
به مثالی جهت پیاده سازی کلاس ماتریس توجه کنید:
public class Matrix
{
    // فیلدها
    private int _row, _col;
    private readonly double[,] _values;

    // تعداد ردیف‌های ماتریس
    public int Row
    {
        get { return _row; }
        set
        {
            _row = value > 0 ? value : 3;
        }
    }

    // تعداد ستون‌های ماتریس
    public int Col
    {
        get { return _col; }
        set
        {
            _col = value > 0 ? value : 3;
        }
    }

    // نعریف یک ایندکسر
    public double this[int r, int c]
    {
        get { return Math.Round(_values[r, c], 3); }
        set { _values[r, c] = value; }
    }

    // سازنده 1
    public Matrix()
    {
        _values = new double[_row,_col];
    }

    // سازنده 2
    public Matrix(int row, int col)
    {
        _row = row;
        _col = col;
        _values = new double[_row,_col];
    }

}
نحوه استفاده از کلاس ایجاد شده:
public class UseMatrixIndexer
{
    // ایجاد نمونه از شیء ماتریس
    private readonly Matrix m = new Matrix(5, 5);

    private double item;

    public UseMatrixIndexer()
    {
        // دسترسی به عنصر واقع در ردیف چهار و ستون سه
        item = m[4, 3];
    }
}
مطالب
آشنایی با NHibernate - قسمت چهارم

در این قسمت یک مثال ساده از insert ، load و delete را بر اساس اطلاعات قسمت‌های قبل با هم مرور خواهیم کرد. برای سادگی کار از یک برنامه Console استفاده خواهد شد (هر چند مرسوم شده است که برای نوشتن آزمایشات از آزمون‌های واحد بجای این نوع پروژه‌ها استفاده شود). همچنین فرض هم بر این است که database schema برنامه را مطابق قسمت قبل در اس کیوال سرور ایجاد کرده اید (نکته آخر بحث قسمت سوم).

یک پروژه جدید از نوع کنسول را به solution برنامه (همان NHSample1 که در قسمت‌های قبل ایجاد شد)، اضافه نمائید.
سپس ارجاعاتی را به اسمبلی‌های زیر به آن اضافه کنید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHSample1.dll : در قسمت‌های قبل تعاریف موجودیت‌ها و نگاشت‌ آن‌ها را در این پروژه class library ایجاد کرده بودیم و اکنون قصد استفاده از آن را داریم.

اگر دیتابیس قسمت قبل را هنوز ایجاد نکرده‌اید، کلاس CDb را به برنامه افزوده و سپس متد CreateDb آن‌را به برنامه اضافه نمائید.

using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class CDb
{
public static void CreateDb(IPersistenceConfigurer dbType)
{
var cfg = Fluently.Configure().Database(dbType);

PersistenceModel pm = new PersistenceModel();
pm.AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
var sessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
pm);

var session = sessionSource.CreateSession();
sessionSource.BuildSchema(session, true);
}
}
}
اکنون برای ایجاد دیتابیس اس کیوال سرور بر اساس نگاشت‌های قسمت قبل، تنها کافی است دستور ذیل را صادر کنیم:

CDb.CreateDb(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql());

تمامی جداول و ارتباطات مرتبط در دیتابیسی که در کانکشن استرینگ فوق ذکر شده است، ایجاد خواهد شد.

در ادامه یک کلاس جدید به نام Config را به برنامه کنسول ایجاد شده اضافه کنید:

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class Config
{
public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType
).Mappings(m => m.FluentMappings.AddFromAssembly(typeof(CustomerMapping).Assembly))
.BuildSessionFactory();
}
}
}
اگر بحث را دنبال کرده باشید، این کلاس را پیشتر در کلاس FixtureBase آزمون واحد خود، به نحوی دیگر دیده بودیم. برای کار با NHibernate‌ نیاز به یک سشن مپ شده به موجودیت‌های برنامه می‌باشد که توسط متد CreateSessionFactory کلاس فوق ایجاد خواهد شد. این متد را به این جهت استاتیک تعریف کرده‌ایم که هیچ نوع وابستگی به کلاس جاری خود ندارد. در آن نوع دیتابیس مورد استفاده ( برای مثال اس کیوال سرور 2008 یا هر مورد دیگری که مایل بودید)، به همراه اسمبلی حاوی اطلاعات نگاشت‌های برنامه معرفی شده‌اند.

اکنون سورس کامل مثال برنامه را در نظر بگیرید:

کلاس CDbOperations جهت اعمال ثبت و حذف اطلاعات:

using System;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CDbOperations
{
ISessionFactory _factory;

public CDbOperations(ISessionFactory factory)
{
_factory = factory;
}

public int AddNewCustomer()
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer vahid = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

Console.WriteLine("Saving a customer...");

session.Save(vahid);
session.Flush();//چندین عملیات با هم و بعد

transaction.Commit();

return vahid.Id;
}
}
}

public void DeleteCustomer(int id)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer customer = session.Load<Customer>(id);
Console.WriteLine("Id:{0}, Name: {1}", customer.Id, customer.FirstName);

Console.WriteLine("Deleting a customer...");
session.Delete(customer);

session.Flush();//چندین عملیات با هم و بعد

transaction.Commit();
}
}
}
}
}
و سپس استفاده از آن در برنامه

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class Program
{
static void Main(string[] args)
{
//CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
//return;

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
CDbOperations db = new CDbOperations(session);
int id = db.AddNewCustomer();
Console.WriteLine("Loading a customer and delete it...");
db.DeleteCustomer(id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
توضیحات:
نیاز است تا ISessionFactory را برای ساخت سشن‌های دسترسی به دیتابیس ذکر شده در تنظمیات آن جهت استفاده در تمام تردهای برنامه، ایجاد نمائیم. لازم به ذکر است که تا قبل از فراخوانی BuildSessionFactory این تنظیمات باید معرفی شده باشند و پس از آن دیگر اثری نخواهند داشت.
ایجاد شیء ISessionFactory هزینه بر است و گاهی بر اساس تعداد کلاس‌هایی که باید مپ شوند، ممکن است تا چند ثانیه به طول انجامد. به همین جهت نیاز است تا یکبار ایجاد شده و بارها مورد استفاده قرار گیرد. در برنامه به کرات از using استفاده شده تا اشیاء IDisposable را به صورت خودکار و حتمی، معدوم نماید.

بررسی متد AddNewCustomer :
در ابتدا یک سشن را از ISessionFactory موجود درخواست می‌کنیم. سپس یکی از بهترین تمرین‌های کاری جهت کار با دیتابیس‌ها ایجاد یک تراکنش جدید است تا اگر در حین اجرای کوئری‌ها مشکلی در سیستم، سخت افزار و غیره پدید آمد، دیتابیسی ناهماهنگ حاصل نشود. زمانیکه از تراکنش استفاده شود، تا هنگامیکه دستور transaction.Commit آن با موفقیت به پایان نرسیده باشد، اطلاعاتی در دیتابیس تغییر نخواهد کرد و از این لحاظ استفاده از تراکنش‌ها جزو الزامات یک برنامه اصولی است.
در ادامه یک وهله از شیء Customer را ایجاد کرده و آن‌را مقدار دهی می‌کنیم (این شیء در قسمت‌های قبل ایجاد گردید). سپس با استفاده از session.Save دستور ثبت را صادر کرده، اما تا زمانیکه transaction.Commit فراخوانی و به پایان نرسیده باشد، اطلاعاتی در دیتابیس ثبت نخواهد شد.
نیازی به ذکر سطر فلاش در این مثال نبود و NHibernate اینکار را به صورت خودکار انجام می‌دهد و فقط از این جهت عنوان گردید که اگر چندین عملیات را با هم معرفی کردید، استفاده از session.Flush سبب خواهد شد که رفت و برگشت‌ها به دیتابیس حداقل شود و فقط یکبار صورت گیرد.
در پایان این متد، Id ثبت شده در دیتابیس بازگشت داده می‌شود.

چون در متد CreateSessionFactory ، متد ShowSql را نیز ذکر کرده بودیم، هنگام اجرای برنامه، عبارات SQL ایی که در پشت صحنه توسط NHibernate تولید می‌شوند را نیز می‌توان مشاهده نمود:



بررسی متد DeleteCustomer :
ایجاد سشن و آغاز تراکنش آن همانند متد AddNewCustomer است. سپس در این سشن، یک شیء از نوع Customer با Id ایی مشخص load‌ خواهد گردید. برای نمونه، نام این مشتری نیز در کنسول نمایش داده می‌شود. سپس این شیء مشخص و بارگذاری شده را به متد session.Delete ارسال کرده و پس از فراخوانی transaction.Commit ، این مشتری از دیتابیس حذف می‌شود.

برای نمونه خروجی SQL پشت صحنه این عملیات که توسط NHibernate مدیریت می‌شود، به صورت زیر است:

Saving a customer...
NHibernate: select next_hi from hibernate_unique_key with (updlock, rowlock)
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 17, @p1 = 16
NHibernate: INSERT INTO [Customer] (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 16016
Loading a customer and delete it...
NHibernate: SELECT customer0_.Id as Id2_0_, customer0_.FirstName as FirstName2_0_, customer0_.LastName as LastName2_0_, customer0_.AddressLine1 as AddressL4_2_0_, customer0_.AddressLine2 as AddressL5_2_0_, customer0_.PostalCode as PostalCode2_0_, customer0_.City as City2_0_, customer0_.CountryCode as CountryC8_2_0_ FROM [Customer] customer0_ WHERE customer0_.Id=@p0;@p0 = 16016
Id:16016, Name: Vahid
Deleting a customer...
NHibernate: DELETE FROM [Customer] WHERE Id = @p0;@p0 = 16016
Press a key...
استفاده از دیتابیس SQLite بجای SQL Server در مثال فوق:

فرض کنید از هفته آینده قرار شده است که نسخه سبک و تک کاربره‌ای از برنامه ما تهیه شود. بدیهی است SQL server برای این منظور انتخاب مناسبی نیست (هزینه بالا برای یک مشتری، مشکلات نصب، مشکلات نگهداری و امثال آن برای یک کاربر نهایی و نه یک سازمان بزرگ که حتما ادمینی برای این مسایل در نظر گرفته می‌شود).
اکنون چه باید کرد؟ باید برنامه را از صفر بازنویسی کرد یا قسمت دسترسی به داده‌های آن‌را کلا مورد باز بینی قرار داد؟ اگر برنامه اسپاگتی ما اصلا لایه دسترسی به داده‌ها را نداشت چه؟! همه جای برنامه پر است از SqlCommand و Open و Close ! و عملا استفاده از یک دیتابیس دیگر یعنی باز نویسی کل برنامه.
همانطور که ملاحظه می‌کنید، زمانیکه با NHibernate کار شود، مدیریت لایه دسترسی به داده‌ها به این فریم ورک محول می‌شود و اکنون برای استفاده از دیتابیس SQLite تنها باید تغییرات زیر صورت گیرد:
ابتدا ارجاعی را به اسمبلی System.Data.SQLite.dll اضافه نمائید (تمام این اسمبلی‌های ذکر شده به همراه مجموعه FluentNHibernate ارائه می‌شوند). سپس:
الف) ایجاد یک دیتابیس خام بر اساس کلاس‌های domain و mapping تعریف شده در قسمت‌های قبل به صورت خودکار

CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
ب) تغییر آرگومان متد CreateSessionFactory

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql()
))
{
...

نمایی از دیتابیس SQLite تشکیل شده پس از اجرای متد قسمت الف ، در برنامه Lita :




دریافت سورس برنامه تا این قسمت

نکته:
در سه قسمت قبل، تمام خواص پابلیک کلاس‌های پوشه domain را به صورت معمولی و متداول معرفی کردیم. اگر نیاز به lazy loading در برنامه وجود داشت، باید تمامی کلاس‌ها را ویرایش کرده و واژه کلیدی virtual را به کلیه خواص پابلیک آن‌ها اضافه کرد. علت هم این است که برای عملیات lazy loading ، فریم ورک NHibernate باید یک سری پروکسی را به صورت خودکار جهت کلاس‌های برنامه ایجاد نماید و برای این امر نیاز است تا بتواند این خواص را تحریف (override) کند. به همین جهت باید آن‌ها را به صورت virtual تعریف کرد. همچنین تمام سطرهای Not.LazyLoad نیز باید حذف شوند.

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


نظرات مطالب
مقابله با XSS ؛ یکبار برای همیشه!
سلام. مدل برنامه من به شرح زیر است:
 public class Post
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int PostId { get; set; }
        [Required(ErrorMessage = "فیلد عنوان الزامی است")]
        [Display(Name = "عنوان پست")]
        public string PostTitle { get; set; }
        [Display(Name = "محتوا")]
        [AllowHtml]
        public string Content { get; set; }
        [DataType(DataType.Date)]
        [Display(Name = "تاریخ نشر")]
        public DateTime PublishDate { get; set; }
    }
و برای فیلد Content یک TextArea قرار دادم که به Froala متصل شده و برای ذخیره اکشن زیر رو نوشتم
[HttpPost]

        public ActionResult CreateNewPost(Post post)
        {
            if (ModelState.IsValid)
            {
                post.Content = post.Content.ToSafeHtml();
                PostData.AddNewPost(post);
                return RedirectToAction("Index", "Home", new { area = "" });
            }
            return View(post);
        }
مشکل اینجاست که وقتی از Froala استفاده میکنم تابع ToSafeHtml از کتابخانه AntiXss مایکروسافت و ToSafeHtml مطرح شده در این بحث هیچ تاثیری روی فیلد Content نداره اما وقتی Froala را با TextArea جایگزین میکنم همه چی درست میشه. میخواستم ببینم چطور میشه این مشکل رو برطرف کرد؟
مطالب
ذخیره‌ی سوابق کامل تغییرات یک رکورد در یک فیلد توسط Entity framework Core
در این مقاله، نوشته‌ی ایمان محمدی، ذخیره‌ی اطلاعات نظارتی هر Entity توسط دو فیلد CreatedSources و ModifiedSources به صورت JSON انجام می‌شود که در هر کدام از این فیلدها، اطلاعات مختلفی مانند ip کاربر، شناسه دستگاه، HostName، ClientName و یک سری اطلاعات دیگر ذخیره می‌شوند. بیایید به این اطلاعات متادیتا بگوییم. در این حالت اگر رکورد، چندین بار تغییر کند، متادیتای آخرین تغییرات در فیلد ModifiedSources ذخیره می‌شود. حالا اگر ما بخواهیم اطلاعات متادیتای همه‌ی تغییرات را داشته باشیم چه؟ اگر بخواهیم علاوه بر اطلاعات بالا، اینکه چه کسی و در چه زمانی این تغییرات را انجام داده است، نیز داشته باشیم چطور؟ اگر بخواهیم حتی اطلاعات متادیتای حذف یک رکورد را داشته باشیم چطور (در حالت soft-delete که رکورد واقعا پاک نمی‌شود)؟ سوال جالبتر اینکه اگر بخواهیم تمام تاریخچه‌ی مقادیر مختلف یک رکورد را از ابتدای ایجاد شدن داشته باشیم چطور؟ در این مقاله قصد داریم به همه‌ی این موارد اضافی برسیم؛ آن هم فقط با یک ستون در Entityهایمان، به اسم Audit!

ابتدا کلاس پایه موجودیت‌هایمان را تعریف می‌کنیم؛ تا بر روی Entityهایمان بتوانیم فیلد نظارتی Audit را اعمال کنیم:
public class BaseEntity : IBaseEntity
{
   [JsonIgnore]
   int Id { get; set; } 

   [JsonIgnore] 
    string? Audit { get; set; }
}
ویژگی [JsonIgnore]  به این منظور استفاده شده است تا از serialize کردن این فیلدها هنگام ایجاد Audit، جلوگیری شود؛ تا در نهایت حجم جیسن Audit کاهش یابد. با مطالعه‌ی ادامه‌ی مقاله، متوجه این قضیه خواهید شد.

دقیقا مانند مقاله‌ی اشاره شده (که خواندن آن توصیه می‌شود)، کلاس AuditSourceValues را ایجاد می‌کنیم:
public class AuditSourceValues
{
    [JsonProperty("hn")]
    public string? HostName { get; set; }

    [JsonProperty("mn")]
    public string? MachineName { get; set; }

    [JsonProperty("rip")]
    public string? RemoteIpAddress { get; set; }

    [JsonProperty("lip")]
    public string? LocalIpAddress { get; set; }

    [JsonProperty("ua")]
    public string? UserAgent { get; set; }

    [JsonProperty("an")]
    public string? ApplicationName { get; set; }

    [JsonProperty("av")]
    public string? ApplicationVersion { get; set; }

    [JsonProperty("cn")]
    public string? ClientName { get; set; }

    [JsonProperty("cv")]
    public string? ClientVersion { get; set; }

    [JsonProperty("o")]
    public string? Other { get; set; }
}
با تعریف کردن نام برای فیلد‌های JSON و نادیده گرفتن مقادیر نال، سعی کردیم حجم خروجی JSON پایین باشد.

اکنون کلاس EntityAudit را ایجاد می‌کنیم که شامل تمامی اطلاعات مورد نیاز ما برای ثبت تاریخچه‌ی کامل هر موجودیت است:
public class EntityAudit<TEntity>
{
    [JsonProperty("type")]
    [JsonConverter(typeof(StringEnumConverter))]
    public EntityEventType EventType { get; set; }

    [JsonProperty("user", NullValueHandling = NullValueHandling.Include)]
    public int? ActorUserId { get; set; }

    [JsonProperty("at")]
    public DateTime ActDateTime { get; set; }

    [JsonProperty("sources")]
    public AuditSourceValues? AuditSourceValues { get; set; }

    [JsonProperty("newValues", NullValueHandling = NullValueHandling.Include)]
    public TEntity NewEntity { get; set; } = default!;

    public string? SerializeJson()
    {
        return JsonSerializer.Serialize(this, 
            options: new JsonSerializerOptions { WriteIndented = false, IgnoreNullValues = true }); 
    }
}

دقت کنید که این کلاس به صورت جنریک تعریف شده است تا اگر بعدا بخواهیم آن را Deserialize کنیم و مثلا از آن API بسازیم، یا استفاده‌ی خاصی را از آن داشته باشیم، به‌راحتی به Entity مد نظر تبدیل شود. در این مقاله فقط به ذخیره‌ی آن پرداخته می‌شود و استفاده از این فیلد که به راحتی و با کمک DbFunctionها در Entity Framework قابل انجام است به خواننده واگذار می‌شود. 

همچنین اینام EntityEventType که تعریف آن در زیر می‌آید دارای ویژگی [JsonConverter(typeof(StringEnumConverter))]  می‌باشد تا مقدار رشته‌ای آن را بجای مقدار عددی، در خروجی جیسن داشته باشیم. این اینام، شامل  تمامی عملیاتی است که بر روی یک رکورد قابل انجام است و به این صورت تعریف می‌شود:
public enum EntityEventType
{
    Create = 0,
    Update = 1,
    Delete = 2
}

تامین اطلاعات کلاس AuditSourceValues به همان صورت است که در مقاله‌ی اشاره شده آمده‌است؛ ابتدا تعریف اینترفیس IAuditSourcesProvider و سپس ایجاد کلاس AuditSourcesProvider:
public interface IAuditSourcesProvider
{
    AuditSourceValues GetAuditSourceValues();
}
public class AuditSourcesProvider : IAuditSourcesProvider
{
    protected readonly IHttpContextAccessor HttpContextAccessor;

    public AuditSourcesProvider(IHttpContextAccessor httpContextAccessor)
    {
        HttpContextAccessor = httpContextAccessor;
    }

    public virtual AuditSourceValues GetAuditSourceValues()
    {
        var httpContext = HttpContextAccessor.HttpContext;

        return new AuditSourceValues
        {
            HostName = GetHostName(httpContext),
            MachineName = GetComputerName(httpContext),
            LocalIpAddress = GetLocalIpAddress(httpContext),
            RemoteIpAddress = GetRemoteIpAddress(httpContext),
            UserAgent = GetUserAgent(httpContext),
            ApplicationName = GetApplicationName(httpContext),
            ClientName = GetClientName(httpContext),
            ClientVersion = GetClientVersion(httpContext),
            ApplicationVersion = GetApplicationVersion(httpContext),
            Other = GetOther(httpContext)
        };
    }

    protected virtual string? GetUserAgent(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["User-Agent"].ToString();
    }

    protected virtual string? GetRemoteIpAddress(HttpContext httpContext)
    {
        return httpContext.Connection?.RemoteIpAddress?.ToString();
    }

    protected virtual string? GetLocalIpAddress(HttpContext httpContext)
    {
        return httpContext.Connection?.LocalIpAddress?.ToString();
    }

    protected virtual string GetHostName(HttpContext httpContext)
    {
        return httpContext.Request.Host.ToString();
    }

    protected virtual string GetComputerName(HttpContext httpContext)
    {
        return Environment.MachineName;
    }
    protected virtual string? GetApplicationName(HttpContext httpContext)
    {
        return Assembly.GetEntryAssembly()?.GetName().Name;
    }

    protected virtual string? GetApplicationVersion(HttpContext httpContext)
    {
        return Assembly.GetEntryAssembly()?.GetName().Version.ToString();
    }

    protected virtual string? GetClientVersion(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["client-version"];
    }
    protected virtual string? GetClientName(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["client-name"];
    }

    protected virtual string? GetOther(HttpContext httpContext)
    {
        return null;
    }
}

حالا برای تامین اطلاعات کلاس EntityAudit کار مشابهی می‌کنیم. ابتدا اینترفیس IEntityAuditProvider را به صورت زیر تعریف می‌کنیم: 
public interface IEntityAuditProvider
{
    string? GetAuditValues(EntityEventType eventType, object? entity, string? previousJsonAudit = null);
}

  و سپس کلاس EntityAuditProvider را ایجاد می‌کنیم:
public class EntityAuditProvider : IEntityAuditProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IAuditSourcesProvider _auditSourcesProvider;

    #region Constructor Injections

    public EntityAuditProvider(IHttpContextAccessor httpContextAccessor, IAuditSourcesProvider auditSourcesProvider)
    {
        _httpContextAccessor = httpContextAccessor;
        _auditSourcesProvider = auditSourcesProvider;
    }

    #endregion

    public virtual string? GetAuditValues(EntityEventType eventType, object? newEntity, string? previousJsonAudit = null)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        int? userId;

        var user = httpContext.User;

        if (!user.Identity.IsAuthenticated)
            userId = null;
        else
            userId = user.Claims.Where(x => x.Type == "UserID").Select(x => x.Value).First().ToInt();

        var auditSourceValues = _auditSourcesProvider.GetAuditSourceValues();

        var auditJArray = new JArray();

        // Update & Delete
        if (eventType == EntityEventType.Update || eventType == EntityEventType.Delete)
        {
            auditJArray = JArray.Parse(previousJsonAudit!);
        }

        // Delete => No NewValues
        if (eventType == EntityEventType.Delete)
        {
            newEntity = null;
        }

        JObject newAuditJObject = JObject.FromObject(new EntityAudit<object?>
        {
            EventType = eventType,
            ActorUserId = userId,
            ActDateTime = DateTime.Now,
            AuditSourceValues = auditSourceValues,
            NewEntity = newEntity
        }, new JsonSerializer
        {
            NullValueHandling = NullValueHandling.Ignore,
            Formatting = Formatting.None
        });

        auditJArray.Add(newAuditJObject);

        return auditJArray.SerializeToJson(true);
    }
}
در این کلاس برای اینکه به جیسن قبلی Audit که تاریخچه‌ی قبلی رکورد می‌باشد یک آیتم را اضافه کنیم، از JArray و JObject پکیج Newtonsoft استفاده کرد‌ه‌ایم.

حالا همه چیز آماده است. مانند مقاله‌ی اشاره شده، از مفهوم Interceptor استفاده می‌کنیم. کلاس AuditSaveChangesInterceptor را که از کلاس SaveChangesInterceptor مشتق می‌شود، به صورت زیر ایجاد می‌کنیم: 
public class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly IEntityAuditProvider _entityAuditProvider;

    #region Constructor Injections

    public AuditSaveChangesInterceptor(IEntityAuditProvider entityAuditProvider)
    {
        _entityAuditProvider = entityAuditProvider;
    }

    #endregion

    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
    {
        ApplyAudits(eventData.Context.ChangeTracker);
        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
        CancellationToken cancellationToken = new CancellationToken())
    {
        ApplyAudits(eventData.Context.ChangeTracker);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private void ApplyAudits(ChangeTracker changeTracker)
    {
        ApplyCreateAudits(changeTracker);
        ApplyUpdateAudits(changeTracker);
        ApplyDeleteAudits(changeTracker);
    }

    private void ApplyCreateAudits(ChangeTracker changeTracker)
    {
        var addedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Added);

        foreach (var addedEntry in addedEntries)
        {
            if (addedEntry.Entity is IBaseEntity entity)
            {              
                entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Create, entity);
            }
        }
    }

    private void ApplyUpdateAudits(ChangeTracker changeTracker)
    {
        var modifiedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Modified);

        foreach (var modifiedEntry in modifiedEntries)
        {
            if (modifiedEntry.Entity is IBaseEntity entity)
            {
                var eventType = entity.IsArchived ? EntityEventType.Delete : EntityEventType.Update; // Maybe Soft Delete
                entity.Audit = _entityAuditProvider.GetAuditValues(eventType, entity, entity.Audit);
            }
        }
    }

    private void ApplyDeleteAudits(ChangeTracker changeTracker)
    {
        var deletedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Deleted);

        foreach (var modifiedEntry in deletedEntries)
        {
            if (modifiedEntry.Entity is IBaseEntity entity)
            {
                entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Delete, entity, entity.Audit);
            }
        }
    }

}


و سپس آن را به سیستم معرفی می‌کنیم:

services.AddDbContext<ATADbContext>((serviceProvider, options) =>
{
    options
        .UseSqlServer(...)

    // Interceptors
    var entityAuditProvider = serviceProvider.GetRequiredService<IEntityAuditProvider>();
    options.AddInterceptors(new AuditSaveChangesInterceptor(entityAuditProvider));

});

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

نمونه‌ی نتیجه‌ای را که از این روش بدست می‌آید، در این تصویر می‌بینید. اگر بخواهید به صورت نرم‌افزاری یا کدی از این دیتا استفاده کنید، باید آن را Deserialize کنید که همانطور که گفته شد با امکاناتی که SQL Server برای خواندن فیلدهای JSON دارد و معرفی آن به EF، قابل انجام است. در غیر اینصورت استفاده از این دیتا به صورت چشمی یا استفاده از Json Formatterها به‌راحتی امکان پذیر است. 

 
نمونه‌ی کامل فیلد Audit که در JsonFormatter قرار داده شده است، بعد از ایجاد شدن و یکبار آپدیت و سپس حذف نرم رکورد:
[
   {
      "type":"Create",
      "user":1,
      "at":"2020-11-24T23:05:54.2692711+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":{               
         "Name":"Farshad"
      }
   },
   {
      "type":"Update",
      "user":1,
      "at":"2020-11-24T23:06:20.0838188+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":{                 
         "Name":"Edited Farshad"
      }
   },
   {
      "type":"Delete",
      "user":null,
      "at":"2020-11-24T23:06:28.601837+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":null
   }
]

یک روش مرسوم داشتن تاریخچه‌ی تغییرات رکورد که با جستجو در اینترنت نیز می‌توان به آن رسید، داشتن یک جدول جداگانه به اسم Audit است که با هر بار تغییر هر Entity، یک رکورد در آن ایجاد می‌شود. ساختار آن مانند تصاویر زیر است:


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

مطالب
چگونگی گزارشگیری از Business Objects مانند List توسط StimulSoft

می‌خواهیم از یک لیست در گزارش خود استفاده کنیم؛ بطور مثال وقتی در LINQ  از دستور ToList استفاده می‌کنیم و می‌خواهیم آنرا بصورت مستقیم به Stimul بفرستیم. فرض بر این است که شما DLLهای Stimul را به پروژه اضافه کرده اید و آماده گزارشگیری هستید.

مثلا مدلی در Entity FrameWork با نام base_CenterType 

public class base_CenterType
    {
        public int ID { get; set; }
        public string Title { get; set; }
        public string Dsc { get; set; }
     }

و متدی بصورت ذیل:

public IList<base_CenterType> GetAll()
        {
            return _base_CenterType.ToList();
        }

طراحی گزارش برای این لیست به این صورت است:

1- اضافه کردن StiWebReport به فرم به نام StiWebReport1

2- با کلیک بر روی فلش سمت راست و بالای StiWebReport1 و انتخاب Design Report، وارد قسمت طراحی می‌شویم:

3- با راست کلیک بر روی Business Object و انتخاب New Business Object  پنجره مربوطه باز میشود:

4- بعد از زدن OK پنجره زیر باز خواهد شد که باید در کادر Name نام Business Object را انتخاب کنیم که برای خوانایی بهتر است همان نام کلاس را برای آن انتخاب کنیم. چون Category  نداریم پس باید کادر آن خالی بماند. 

در قسمت Columns باید ستون‌های هم نام و هم نوع با خواص کلاس base_CenterType  را ایجاد کنیم. 

و نهایتا Business Objectی به نام base_CenterType با سه ستون ایجاد خواهد شد. 

  حال می‌توانید ستون‌های مورد نظر را در گزارش بکار ببرید.

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

StiReport mainreport = new StiReport();            
mainreport.RegBusinessObject("base_CenterType", base_CenterTypeService.GetAll());
mainreport.Load("C:\\StiWebReport2.mrt");
mainreport.Show();
نظرات مطالب
نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular
شبیه به پیاده سازی EmailValidatorDirective مطلب جاری، از Regex ذیل استفاده کنید:
[RegularExpression(@"^[\u0600-\u06FF,\u0590-\u05FF,0-9\s]*$",
                          ErrorMessage = "لطفا تنها از اعداد و حروف فارسی استفاده نمائید")]
public string FriendlyName { get; set; }
مطالب
روش‌هایی برای بهبود سرعت برنامه‌های مبتنی بر Entity framework
در این مطلب تعدادی از شایع‌ترین مشکلات حین کار با Entity framework که نهایتا به تولید برنامه‌هایی کند منجر می‌شوند، بررسی خواهند شد.

مدل مورد بررسی

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; }
    }

    public class BlogPost
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int UserId { get; set; }
    }
کوئری‌هایی که در ادامه بررسی خواهند شد، بر روی رابطه‌ی one-to-many فوق تعریف شده‌اند؛ یک کاربر به همراه تعدادی مطلب منتشر شده.


مشکل 1: بارگذاری تعداد زیادی ردیف
 var data = context.BlogPosts.ToList();
در بسیاری از اوقات، در برنامه‌های خود تنها نیاز به مشاهده‌ی قسمت خاصی از یک سری از اطلاعات، وجود دارند. به همین جهت بکارگیری متد ToList بدون محدود سازی تعداد ردیف‌های بازگشت داده شده، سبب بالا رفتن مصرف حافظه‌ی سرور و همچنین بالا رفتن میزان داده‌ای که هر بار باید بین سرور و کلاینت منتقل شوند، خواهد شد. یک چنین برنامه‌هایی بسیار مستعد به استثناهایی از نوع out of memory هستند.
راه حل:  با استفاده از Skip و Take، مباحث صفحه‌ی بندی را اعمال کنید.


مشکل 2: بازگرداندن تعداد زیادی ستون
 var data = context.BlogPosts.ToList();
فرض کنید View برنامه، در حال نمایش عناوین مطالب ارسالی است. کوئری فوق، علاوه بر عناوین، شامل تمام خواص تعریف شده‌ی دیگر نیز هست. یک چنین کوئری‌هایی نیز هربار سبب هدر رفتن منابع سرور می‌شوند.
راه حل: اگر تنها نیاز به خاصیت Content است، از Select و سپس ToList استفاده کنید؛ البته به همراه نکته 1.
 var list = context.BlogPosts.Select(x => x.Content).Skip(15).Take(15).ToList();


مشکل 3: گزارشگیری‌هایی که بی‌شباهت به حمله‌ی به دیتابیس نیستند
 foreach (var post in context.BlogPosts)
{
     Console.WriteLine(post.User.Name);
}
فرض کنید قرار است رکوردهای مطالب را نمایش دهید. در حین نمایش این مطالب، در قسمتی از آن باید نام نویسنده نیز درج شود. با توجه به رابطه‌ی تعریف شده، نوشتن post.User.Name به ازای هر مطلب، بسیار ساده به نظر می‌رسد و بدون مشکل هم کار می‌کند. اما ... اگر خروجی SQL این گزارش را مشاهده کنیم، به ازای هر ردیف نمایش داده شده، یکبار رفت و برگشت به بانک اطلاعاتی، جهت دریافت نام نویسنده یک مطلب وجود دارد.
این مورد به lazy loading مشهور است و در مواردی که قرار است با یک مطلب و یک نویسنده کار شود، شاید اهمیتی نداشته باشد. اما در حین نمایش لیستی از اطلاعات، بی‌شباهت به یک حمله‌ی شدید به بانک اطلاعاتی نیست.
راه حل: در گزارشگیری‌ها اگر نیاز به نمایش اطلاعات روابط یک موجودیت وجود دارد، از متد Include استفاده کنید تا Lazy loading لغو شود.
 foreach (var post in context.BlogPosts.Include(x=>x.User))


مشکل 4:  فعال بودن بی‌جهت مباحث ردیابی اطلاعات
 var data = context.BlogPosts.ToList();
در اینجا ما فقط قصد داریم که لیستی از اطلاعات را دریافت و سپس نمایش دهیم. در این بین، هدف، ویرایش یا حذف اطلاعات این لیست نیست. یک چنین کوئری‌هایی مساوی هستند با تشکیل dynamic proxies مخصوص EF جهت ردیابی تغییرات اطلاعات (مباحث AOP توکار). EF توسط این dynamic proxies، محصور کننده‌هایی را برای تک تک آیتم‌های بازگشت داده شده از لیست تهیه می‌کند. در این حالت اگر خاصیتی را تغییر دهید، ابتدا وارد این محصور کننده (غشاء نامرئی) می‌شود، در سیستم ردیابی EF ذخیره شده و سپس به شیء اصلی اعمال می‌گردد. به عبارتی شیء در حال استفاده، هر چند به ظاهر post.User است اما در واقعیت یک User دارای روکشی نامرئی از جنس dynamic proxy‌های EF است. تهیه این روکش‌ها، هزینه‌بر هستند؛ چه از لحاظ میزان مصرف حافظه و چه از نظر سرعت کار.
راه حل: در گزاشگیری‌ها، dynamic proxies را توسط متد AsNoTracking غیرفعال کنید:
 var data = context.BlogPosts.AsNoTracking().Skip(15).Take(15).ToList();


مشکل 5: باز کردن  تعداد اتصالات زیاد به بانک اطلاعاتی در طول یک درخواست

هر Context دارای اتصال منحصربفرد خود به بانک اطلاعاتی است. اگر در طول یک درخواست، بیش از یک Context مورد استفاده قرار گیرد، بدیهی است به همین تعداد اتصال باز شده به بانک اطلاعاتی، خواهیم داشت. نتیجه‌ی آن فشار بیشتر بر بانک اطلاعاتی و همچنین کاهش سرعت برنامه است؛ از این لحاظ که اتصالات TCP برقرار شده، هزینه‌ی بالایی را به همراه دارند.
روش تشخیص:
        private void problem5MoreThan1ConnectionPerRequest() 
        {
            using (var context = new MyContext())
            {
                var count = context.BlogPosts.ToList();
            }
        }
داشتن متدهایی که در آن‌ها کار وهله سازی و dispose زمینه‌ی EF انجام می‌شود (متدهایی که در آن‌ها new Context وجود دارد).
راه حل: برای حل این مساله باید از روش‌های تزریق وابستگی‌ها استفاده کرد. یک Context وهله سازی شده‌ی در طول عمر یک درخواست، باید بین وهله‌های مختلف اشیایی که نیاز به Context دارند، زنده نگه داشته شده و به اشتراک گذاشته شود.


مشکل 6: فرق است بین IList و IEnumerable
DataContext = from user in context.Users
                      where user.Id>10
                      select user;
خروجی کوئری LINQ نوشته شده از نوع IEnumerable است. در EF، هربار مراجعه‌ی مجدد به یک کوئری که خروجی IEnumerable دارد، مساوی است با ارزیابی مجدد آن کوئری. به عبارتی، یکبار دیگر این کوئری بر روی بانک اطلاعاتی اجرا خواهد شد و رفت و برگشت مجددی صورت می‌گیرد.
زمانیکه در حال تهیه‌ی گزارشی هستید، ابزارهای گزارشگیر ممکن است چندین بار از نتیجه‌ی کوئری شما در حین تهیه‌ی گزارش استفاده کنند. بنابراین برخلاف تصور، data binding انجام شده، تنها یکبار سبب اجرای این کوئری نمی‌شود؛ بسته به ساز و کار درونی گزارشگیر، چندین بار ممکن است این کوئری فراخوانی شود.
راه حل: یک ToList را به انتهای این کوئری اضافه کنید. به این ترتیب از نتیجه‌ی کوئری، بجای اصل کوئری استفاده خواهد شد و در این حالت تنها یکبار رفت و برگشت به بانک اطلاعاتی را شاهد خواهید بود.


مشکل 7: فرق است بین IQueryable و IEnumerable

خروجی IEnumerable، یعنی این عبارت را محاسبه کن. خروجی IQueryable یعنی این عبارت را درنظر داشته باش. اگر نیاز است نتایج کوئری‌ها با هم ترکیب شوند، مثلا بر اساس رابط کاربری برنامه، کاربر بتواند شرط‌های مختلف را با هم ترکیب کند، باید از ترکیب IQueryableها استفاده کرد تا سبب رفت و برگشت اضافی به بانک اطلاعاتی نشویم.


مشکل 8: استفاده از کوئری‌های Like دار
 var list = context.BlogPosts.Where(x => x.Content.Contains("test"))
این نوع کوئری‌ها که در نهایت به Like در SQL ترجمه می‌شوند، سبب full table scan خواهند شد که کارآیی بسیار پایینی دارند. در این نوع موارد توصیه شده‌است که از روش‌های full text search استفاده کنید.


مشکل 9: استفاده از Count بجای Any

اگر نیاز است بررسی کنید مجموعه‌ای دارای مقداری است یا خیر، از Count>0 استفاده نکنید. کارآیی Any و کوئری SQL ایی که تولید می‌کند، به مراتب بیشتر و بهینه‌تر است از Count>0.


مشکل 10: سرعت insert پایین است

ردیابی تغییرات را خاموش کرده و از متد جدید AddRange استفاده کنید. همچنین افزونه‌هایی برای Bulk insert نیز موجود هستند.


مشکل 11: شروع برنامه کند است

می‌توان تمام مباحث نگاشت‌های پویای کلاس‌های برنامه به جداول و روابط بانک اطلاعاتی را به صورت کامپایل شده در برنامه ذخیره کرد. این مورد سبب بالا رفتن سرعت شروع برنامه خصوصا در حالتیکه تعداد جداول بالا است می‌شود.
مطالب
پلاگین DataTables کتابخانه jQuery - قسمت چهارم
همان طور که قبلا اشاره کردیم، این پلاگین می‌تواند از یک زبان برنامه نویسی سمت سرور داده‌های مورد نیاز خودش را دریافت کند. می‌توانید داده‌ها را با استفاده از AJAX و به صورت JSON از سرور دریافت کرده و با استفاده از DataTables آنها را در جدول تزریق کنید. در این قسمت سعی خواهیم کرد تا با استفاده از jQuery DataTables یک گرید را در MVC ایجاد کنیم.  البته برای حذف جزئیات داده‌ها به جای این که از یک بانک اطلاعاتی دریافت شوند، در حافظه ساخته می‌شوند. در هر صورت اساس کار یکی است.

قصد داریم تا مانند مثال قسمت قبل، مجموعه ای از اطلاعات مربوط به مرورگرهای مختلف را در یک جدول نشان دهیم، اما این بار منبع داده ما فرق می‌کند. منبع داده از طرف سرور فراهم می‌شود. هر مرورگر - همان طور که در قسمت قبل مشاهده نمودید - شامل اطلاعات زیر خواهد بود:
  1. موتور رندرگیری (Engine)
  2. نام مرورگر (Name)
  3. پلتفرم (Platform)
  4. نسخه موتور (Version)
  5. نمره سی اس اس (Grade)

به همین دلیل در سمت سرور، کلاسی خواهیم ساخت که نمایانگر یک مرورگر باشد. بدین صورت:

public class Browser
{
    public int Id { get; set; }
    public string Engine { get; set; }
    public string Name { get; set; }
    public string Platform { get; set; }
    public float Version { get; set; }
    public string Grade { get; set; }
}

استفاده از روش server side processing برای دریافت داده‌ها از سرور

این روش، یکی از امکانات jQuery DataTables است که با استفاده از آن، کلاینت تنها یک مصرف کننده صرف خواهد بود و وظیفه پردازش اطلاعات - یعنی تعداد رکوردهایی که برگشت داده می‌شود، صفحه بندی، مرتب سازی، جستجو، و غیره - به عهده سرور خواهد بود.

برای به کار گیری این روش، اولین کار این است که ویژگی bServerSide را true کنیم، مثلا بدین صورت:
var $table = $('#browsers-grid');
$table.dataTable({
      "bServerSide": true,
      "sAjaxSource": "/Home/GetBrowsers"
 });

همچنین ویژگی sAjaxSource را به Url ی که باید داده‌ها از آن دریافت شوند مقداردهی می‌کنیم.

به صورت پیش فرض مقدار ویژگی bServerSide مقدار false است؛ که یعنی منبع داده این پلاگین از سمت سرور خوانده نشود. اگر true باشد منبع داده و خیلی اطلاعات دیگر مربوط به داده‌های درون جدول باید از سرور به مرورگر کاربر پس فرستاده شوند. با true کردن مقدار bServerSide، آنگاه DataTables اطلاعاتی را راجع به شماره صفحه جاری، اندازه هر صفحه، شروط فیلتر کردن داده ها، مرتب سازی ستون ها، و غیره را به سرور می‌فرستد. همجنین انتظار می‌رود تا سرور در پاسخ به این درخواست، داده‌های مناسبی را به فرمت JSON به مرورگر پس بفرستد. در حالتی که bServerSide مقدار true به خود بگیرد، پلاگین فقط رابطه متقابل بین کاربر و سرور را مدیریت می‌کند و هیچ پردازشی را انجام نمی‌دهد.


در این درخواست XHR یا Ajax ی پارامترهایی که به سرور ارسال می‌شوند این‌ها هستند:

iDisplayStart عدد صحیح
نقظه شروع مجموعه داده جاری

iDisplayLength عدد صحیح
تعداد رکوردهایی که جدول می‌تواند نمایش دهد. تعداد رکوردهایی که از طرف سرور برگشت داده می‌شود باید با این عدد یکسان باشند.

iColumns عدد صحیح
تعداد ستونهایی که باید نمایش داده شوند.

sSearch رشته
فیلد جستجوی عمومی

bRegex بولین
اگر true باشد معنی آن این است که می‌توان از عبارات باقاعده برای جستجوی عبارتی خاص در کل ستون‌های جدول استفاده کرد. مثلا در کادر جستجو نوشت :

^[1-5]$
که یعنی 1 و 5 همه عددهای بین 1و 5.

bSearchable_(int)    بولین
نمایش می‌دهد که یک ستون در طرف کاربر قابلیت searchable آن true هست یا نه.

sSearch_(int)   رشته
فیلتر مخصوص هر ستون. اگر از ویژگی multi column filtering پلاگین استفاده شود به صورت sSearch0 ، sSearch1 ، sSeach2 و ... به طرف سرور ارسال می‌شوند. شماره انتهای هر کدام از پارامترها بیانگر شماره ستون جدول است.

bRegex_(int)  بولین
اگر true باشد، بیان می‌کند که می‌توان از عبارت با قاعده در ستون شماره int جهت جستجو استفاده کرد.

bSortable_(int) بولین
مشخص می‌کند که آیا یک ستون در سمت کلاینت، قابلیت مرتب شدن بر اساس آن وجود دارد یا نه. (در اینجا int اندیس ستون را مشخص می‌کند)

iSortingCols   عدد صحیح
تعداد ستون هایی که باید مرتب سازی بر اساس آنها صورت پذیرد. در صورتی که از امکان multi column sorting استفاده کنید این مقدار می‌تواند بیش از یکی باشد.

iSortCol_(int)   عدد صحیح
شماره ستونی که باید بر اساس آن عملیات مرتب سازی صورت پذیرد.

sSortDir_(int)    رشته
نحوه مرتب سازی ؛ شامل صعودی (asc) یا نزولی (desc)

mDataProp_(int)    رشته
اسم ستون‌های درون جدول را مشخص می‌کند.

sEcho     رشته
اطلاعاتی که datatables از آن برای رندر کردن جدول استفاده می‌کند.

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



شکل ب ) پارامترهای ارسالی به سرور به صورت json

بعضی از این پارامترها بسته به تعداد ستون‌ها قابل تغییر هستند. (آن پارامترهایی که آخرشان یک عدد هست که نشان دهنده شماره ستون مورد نظر می‌باشد)

در پاسخ به هر درخواست XHR که datatables به سرور می‌فرستد، انتظار دارد تا سرور نیز یک شیء json را با فرمت مخصوص که شامل پارامترهای زیر می‌شود به او پس بفرستد:

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


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

sEcho    عدد صحیح 
یک عدد صحیح است که در قالب رشته در تعامل بین سرور و کلاینت جا به جا می‌شود. این مقدار به ازاء هر درخواست تغییر می‌کند. همان مقداری که مرورگر به سرور می‌دهد را سرور هم باید به مرورگر تحویل بدهد. برای جلوگیری از حملات XSS باید آن را تبدیل به عدد صحیح کرد. پلاگین DataTables مقدار این پارامتر را برای هماهنگ کردن و منطبق کردن درخواست ارسال شده و جواب این درخواست استفاده می‌کند. همان مقداری که مروگر به سرور می‌دهد را باید سرور تحویل به مرورگر بدهد. 

sColumns    رشته
اسم ستون‌ها که با استفاده از کاما از هم جدا شده اند. استفاده از آن اختیاری است و البته منسوخ هم شده است و در نسخه‌های جدید jQuery DataTables از آن پشتیبانی نمی‌شود.

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

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


شکل ب ) پارامترهای دریافتی از سرور به صورت json

استفاده از روش server side processing در mvc
همان طور که گفتیم، کلاینت به سرور یک سری پارامترها را ارسال می‌کند و آن پارامترها را هم شرح دادیم. برای دریافت این پارامتر‌ها طرف سرور، احتیاج به یک مدل هست. این مدل به صورت زیر پیاده سازی خواهد شد:
/// <summary>
/// Class that encapsulates most common parameters sent by DataTables plugin
/// </summary>
public class jQueryDataTableParamModel
{
    /// <summary>
    /// Request sequence number sent by DataTable,
    /// same value must be returned in response
    /// </summary>
    public string sEcho { get; set; }
    /// <summary>
    /// Text used for filtering
    /// </summary>
    public string sSearch { get; set; }
    /// <summary>
    /// Number of records that should be shown in table
    /// </summary>
    public int iDisplayLength { get; set; }
    /// <summary>
    /// First record that should be shown(used for paging)
    /// </summary>
    public int iDisplayStart { get; set; }
    /// <summary>
    /// Number of columns in table
    /// </summary>
    public int iColumns { get; set; }
    /// <summary>
    /// Number of columns that are used in sorting
    /// /// </summary>
    public int iSortingCols { get; set; }
    /// <summary>
    /// Comma separated list of column names
    /// </summary>
    public string sColumns { get; set; }
}

مدل بایندر mvc وظیفه مقداردهی به خصوصیات درون این کلاس را بر عهده دارد، بقیه پارامترهایی که به سرور ارسال می‌شوند و در این کلاس نیامده اند، از طریق شیء Request در دسترس خواهند بود.


اکشن متدی که مدل بالا را دریافت می‌کند، می‌تواند به صورت زیر پیاده سازی شود. این اکشن متد وظیفه پاسخ دادن به درخواست DataTables بر اساس پارامترهای ارسال شده در مدل DataTablesParam را دارد. خروجی این اکشن متد شامل پارارمترهای مورد نیاز پلاگین DataTables برای تشکیل جدول است که آنها را هم شرح دادیم.

public JsonResult GetBrowsers(jQueryDataTableParamModel param)
{
        IQueryable<Browser> allBrowsers = new Browsers().CreateInMemoryDataSource().AsQueryable();

        IEnumerable<Browser> filteredBrowsers;

        // Apply Filtering
        if (!string.IsNullOrEmpty(param.sSearch))
        {
                filteredBrowsers = new Browsers().CreateInMemoryDataSource()
                    .Where(x => x.Engine.Contains(param.sSearch)
                                       || x.Grade.Contains(param.sSearch)
                                       || x.Name.Contains(param.sSearch)
                                       || x.Platform.Contains(param.sSearch)
                    ).ToList();
                float f;
                if (float.TryParse(param.sSearch, out f))
                {
                    filteredBrowsers = filteredBrowsers.Where(x => x.Version.Equals(f));
                }
        }
        else
        {
                filteredBrowsers = allBrowsers;
        }

        // Apply Sorting
        var sortColumnIndex = Convert.ToInt32(Request["iSortCol_0"]);
        Func<Browser, string> orderingFunction = (x => sortColumnIndex == 0 ? x.Engine :
                                                            sortColumnIndex == 1 ? x.Name :
                                                            sortColumnIndex == 2 ? x.Platform :
                                                            sortColumnIndex == 3 ? x.Version.ToString() :
                                                            sortColumnIndex == 4 ? x.Grade :
                                                                x.Name);

        var sortDirection = Request["sSortDir_0"]; // asc or desc
        filteredBrowsers = sortDirection == "asc" ? filteredBrowsers.OrderBy(orderingFunction) : filteredBrowsers.OrderByDescending(orderingFunction);

        // Apply Paging
        var enumerable = filteredBrowsers.ToArray();
        IEnumerable<Browser> displayedBrowsers = enumerable.Skip(param.iDisplayStart).
                Take(param.iDisplayLength).ToList();

        return Json(new
        {
                sEcho = param.sEcho,
                iTotalRecords = allBrowsers.Count(),
                iTotalDisplayRecords = enumerable.Count(),
                aaData = displayedBrowsers
        }, JsonRequestBehavior.AllowGet);
}

تشریح اکشن متد GetBrowsers :

این اکشن متد از مدل jQueryDataTableParamModel به عنوان پارامتر ورودی خود استفاده می‌کند. این مدل همان طور هم که گفتیم، شامل یک سری خصوصیت است که توسط پلاگین jQuery DataTables مقداردهی می‌شوند و همچنین مدل بایندر mvc وظیفه بایند کردن این مقادیر به خصوصیات درون این کلاس را بر عهده خواهد داشت. درون بدنه اکشن متد GetBrowsers داده‌ها بعد از اعمال عملیات فیلترینگ، مرتب سازی، و صفحه بندی به فرمت مناسبی درآمده و به طرف مرورگر فرستاده خواهند شد.

برای پیاده سازی کدهای طرف کلاینت نیز، درون یک View کدهای زیر قرار خواهند گرفت:
$(function () {
        var $table = $('#browsers-grid');
        $table.dataTable({
                "bProcessing": true,
                "bStateSave": true,
                "bServerSide": true,
                "bFilter": true,
                "sDom": 'T<"clear">lftipr',
                "aLengthMenu": [[5, 10, 25, 50, -1], [5, 10, 25, 50, "All"]],
                "bAutoWidth": false,
                "sAjaxSource": "/Home/GetBrowsers",
                "fnServerData": function (sSource, aoData, fnCallback) {
                    $.ajax({
                        "dataType": 'json',
                        "type": "POST",
                        "url": sSource,
                        "data": aoData,
                        "success": fnCallback
                    });
                },
                "aoColumns": [
                    { "mDataProp": "Engine" },
                    { "mDataProp": "Name" },
                    { "mDataProp": "Platform" },
                    { "mDataProp": "Version" },
                    { "mDataProp": "Grade" }
                ],
                "oLanguage": {
                        "sUrl": "/Content/dataTables.persian.txt"
                }
        });
});

تشریح کدها:

fnServerData :

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

oLanguage :

برای فعال سازی زبان فارسی، فیلدهای مورد نیاز ترجمه شده و در یک فایل متنی قرار داده شده اند. کافی است آدرس این فایل متنی به ویژگی oLanguage اختصاص داده شوند.

مثال این قسمت را از لینک زیر دریافت کنید:
DataTablesTutorial04.zip

لازم به ذکر است پوشه bin، obj، و packages جهت کاهش حجم این مثال از solution حذف شده اند. برای اجرای این مثال از اینجا کمک بگیرید.


مطالعه بیشتر

برای مطالعه بیشتر در مورد این پلاگین و نیز پیاده سازی آن در MVC می‌توانید به لینک زیر نیز مراجعه بفرمائید که بعضی از قسمتهای این مطلب هم از مقاله زیر استفاده کرده است:
jQuery DataTables and ASP.NET MVC Integration - Part I