نظرات مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت دوم - پیاده سازی سمت کلاینت
پیاده سازی جستجوی بر روی این گرید، شامل موارد زیر است:
اضافه کردن دو خاصیت جدید به کلاس PagedQueryModel سمت کلاینت جهت مشخص سازی ستونی که قرار است بر روی آن جستجو انجام شود و همچنین مقدار آن:
export class PagedQueryModel {
  constructor(
    // ...
    public filterByColumn: string,
    public filterByValue: string,
  ) { }
}
سپس به ProductsListComponent دو متد زیر را اضافه می‌کنیم:
  doFilter() {
    this.queryModel.page = 1;
    this.getPagedProductsList();
  }

  resetFilter() {
    this.queryModel.page = 1;
    this.queryModel.filterByColumn = "";
    this.queryModel.filterByValue = "";
    this.getPagedProductsList();
  }
اولی کار جستجو را انجام می‌دهد و دومی بازگشت حالت گرید به وضعیت اول آن است. متد getPagedProductsList قابلیت واکشی خودکار اطلاعات دو خاصیت جدیدی را که اضافه کردیم دارد و نیازی به تنظیمات اضافه‌تری ندارد. یعنی filterByColumn و filterByValue را به صورت خودکار به سمت سرور ارسال می‌کند.

پس از آن، قالب این گرید (products-list.component.html) جهت افزودن جستجو، به صورت زیر تغییر می‌کند:
<div class="panel panel-default">
  <div class="panel-body">
    <div class="form-group">
      <input type="text" [(ngModel)]="queryModel.filterByValue" placeholder="Search For ..."
        class="form-control" />
    </div>
    <div class="form-group">
      <select class="form-control" name="filterColumn" [(ngModel)]="queryModel.filterByColumn">
        <option value="">Filter by ...</option>
        <option *ngFor="let column of columns" [value]="column.propertyName">
          {{ column.title }}
        </option>
      </select>
    </div>
    <button class="btn btn-primary" type="button" (click)="doFilter()">Search</button>
    <button class="btn btn-default" type="button" (click)="resetFilter()">Reset</button>
  </div>
</div>
که در آن queryModel.filterByColumn و queryModel.filterByValue از کاربر دریافت می‌شوند. همچنین دو متد doFilter و resetFilter را نیز فراخوانی می‌کند.
با این شکل:


تغییرات سمت سرور آن نیز به صورت ذیل است:
ابتدا IPagedQueryModel را با همان دو خاصیت جدید ستون فیلتر شونده و مقدار آن، تکمیل می‌کنیم:
    public interface IPagedQueryModel
    {
    // ....
        string FilterByColumn { get; set; }
        string FilterByValue { get; set; }
    }

    public class ProductQueryViewModel : IPagedQueryModel
    {
        // ... other properties ...

// ...
        public string FilterByColumn { get; set; }
        public string FilterByValue { get; set; }
    }
از این دو خاصیت جدید، جهت افزودن متد اعمال جستجو، همانند متد ApplyOrdering که پیشتر تعریف شد، استفاده می‌کنیم:
    public static class IQueryableExtensions
    {
        public static IQueryable<T> ApplyFiltering<T>(
          this IQueryable<T> query,
          IPagedQueryModel model,
          IDictionary<string, Expression<Func<T, object>>> columnsMap)
        {
            if (string.IsNullOrWhiteSpace(model.FilterByValue) || !columnsMap.ContainsKey(model.FilterByColumn))
            {
                return query;
            }

            var func = columnsMap[model.FilterByColumn].Compile();
            return query.Where(x => func(x).ToString() == model.FilterByValue);
        }
در اینجا همان columnsMap مورد استفاده در متد ApplyOrdering جهت نگاشت نام‌های رشته‌ای ستون‌ها به معادل Expression آن‌ها استفاده شده‌است.

در آخر، به کنترلر ProductController و اکشن متد GetPagedProducts آن مراجعه کرده و پیش از ApplyOrdering، متد جدید ApplyFiltering فوق را اضافه می‌کنیم:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
query = query.ApplyFiltering(queryModel, columnsMap);
query = query.ApplyOrdering(queryModel, columnsMap);

کدهای کامل این تغییرات را از اینجا می‌توانید دریافت کنید.
مطالب
SASS #2
در قسمت قبل، روش‌های مختلف کامپایل فایل‌های SASS را بررسی کردیم. در ادامه می‌خواهیم با syntax آن بیشتر آشنا شویم.

متغیرها (Variables)

متغیرها در SASS با استفاده از $ در ابتدای نام آن، به عنوان یک مقدار مورد استفاده‌ی در CSS تعریف می‌شوند. شما در SASS می‌توانید متغیرهایی را برای margin ،font-size و یا padding و غیره، تعریف کنید. استفاده از متغیرها این امکان را به شما می‌دهد که خیلی راحت‌تر از style‌های تعریف شده، مجدد استفاده کنید.

شما 6 نوع مختلف متغیر را می‌توانید با استفاده از SASS بکار ببرید.

  • Strings (مثال: ;"myString: "your text here$ )
  • Numbers (مثال: ;myNum: 16px$)
  • Colors (مثال: ;myColor: aqua$)
  • Booleans (مثال: ;myBool: true$)
  • Lists (مثال: ;myItemList: 1px solid red$)
  • Nulls (مثال: ;myVar: null$)

برای مثالی از استفاده‌ی از این متغیرها، یک فایل را با نام styles.scss ایجاد کرده و کدهای زیر را در آن وارد کنید:

$myColor: #FFF726;
$myBackColor: #2B14FF;
$myString: "I Love ";
$myFontSize: 13px;
$myMargin: 0px auto;
$myWidth: 300px;
 
h1 {
   color: $myColor;
   margin: 0;
   padding: 0;
}

h1:before{
  content: $myString;
}

#container {
   width: $myWidth;
   margin: $myMargin;
   background-color:$myBackColor;
   text-align:center;
}
پس از کامپایل فایل SASS و اجرای کد بالا، مرورگر شما باید خروجی را به این صورت داشته باشد:


ریاضی (Math)

برخلاف SASS ،CSS به ما امکان استفاده از عبارات ریاضی را می‌دهد. عملگرهای جمع + ، تفریق - ، تقسیم / ، ضرب * ، باقیمانده % ، مساوی == ، نامساوی =! را  SASS پشتیبانی می‌کند. در هنگام استفاده از عبارات ریاضی چند نکته وجود دارد که باید رعایت کنید:

نکته1: چون علامت / در CSS به عنوان یک کوتاه کننده استفاده می‌شود مانند font: 14px/16px، در صورتیکه بخواهید عمل تقسیم را بر روی مقدار ثابتی انجام دهید باید آنها را درون پرانتر قرار دهید.

$fontDiff: (14px/16px);
نکته2: شما نمی توانید واحدهای مختلف را با هم استفاده کنید.
$container-width: 100% - 20px;
مثال بالا کار نمی‌کند، در صورتیکه نیاز به چنین محاسبه‌ای داشتید می‌توانید از تابع calc در CSS استفاده کنید؛ چرا که نیاز به محاسبه‌ی در زمان اجرا را دارید.

حال می‌خواهیم براساس عرض container، ستون‌های پویایی را ایجاد کنیم:
$container-width: 100%;

.container {
  width: $container-width;
}

.col-4 {
  width: $container-width / 4;
}
که پس از کامپایل به صورت زیر تبدیل می‌شود:
.container {
   width: 100%;
}

.col-4 {
    width: 25%;
}

مشاهده‌ی پیاده سازی مثال بالا اینجا.

توابع (Functions)

یکی از بهترین قسمت‌های SASS، توابع پیاده سازی شده‌ی آن است. شما می‌توانید لیست بزرگی از توابع SASS را در اینجا مشاهده کنید.

برای نمونه برخی از توابع مربوط به کار با رنگ‌ها را توضیح می‌دهیم:

  • (darken ($color, $amount: این تابع برای تیره‌تر کردن یک کد رنگ می‌باشد. شما برای استفاده‌ی از این تابع باید دو مقدار را به آرگومان‌های ورودی این تابع که به ترتیب کد رنگ و میزان تیره‌تر شدن آن به صورت درصد از %0 تا %100 می‌باشند، ارسال کنید و خروجی آن کد رنگ تولید شده است. توجه داشته باشید نوع سیستم رنگی ارسال شده به عنوان پارامتر color$، خروجی این تابع نیز همان نوع می‌باشد.
darken(hsl(25, 100%, 80%), 30%) => hsl(25, 100%, 50%)
darken(#800, 20%) => #200
  • (lighten ($color, $amount: این تابع برای روشن‌تر کردن یک رنگ می‌باشد و دقیقا برعکس تابع darken عمل می‌کند.
lighten(hsl(0, 0%, 0%), 30%) => hsl(0, 0, 30)
lighten(#800, 20%) => #e00
  • (alpha ($color) / opacity($color: با استفاده از این دو تابع می‌توانید میزان شفافیت/کدری را مشخص کنید.
  • (mix ($color1, $color2, $weight:50% : با استفاده از این تابع می‌توانید دو رنگ را با هم ترکیب کنید. مقدار پیش فرض آرگومان weight$ برابر %50 می‌باشد و تعیین آن اختیاری است. محدوده‌ی پذیرش مقدار weight$ هرچه به %0 نزدیک‌تر باشد، باعث نزدیک‌تر بودن رنگ خروجی به رنگ دوم و هرچه به %100 نزدیکتر باشد رنگ خروجی به رنگ اول نزدیک‌تر می‌شود. توجه: میزان شفافیت/کدری را نیز می‌تواند تشخیص دهد.
mix(#f00, #00f) => #7f007f
mix(#f00, #00f, 25%) => #3f00bf
mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75)

تو در تو (Nesting)

SASS امکان تعریف استایل‌های تو در تو را به شما می‌دهد که باعث خواناتر شدن استایل‌های نوشته شده می‌شود. به عنوان مثال به کد CSS زیر توجه کنید:

#container {
    width: 500px;
    margin: 0 auto;
}

#container p {
   font-family: Arial;
   font-size: 13px;
}

#container h1 {
   font-family: Tahoma;
   font-size: 15px;
}

#container h2 {
   font-family: Helvetica;
   font-size: 14px;
}
حال نسخه‌ی SASS مثال بالا می‌شود:
$myFontsize1: 13px;
$myFontsize2: 18px;
$myFontsize3: 25px;
$myWidth: 500px;
$myMargin: 0px auto;

#container {
    width: $myWidth;
    margin: $myMargin;

    p {
        font-family: Arial;
        font-size: $myFontsize1;
    }

    h1 {
        font-family: Tahoma;
        font-size: $myFontsize3;
    }

    h2 {
        font-family: Helvetica;
        font-size: $myFontsize2;
    }
}
توجه کنید تمام element هایی که درون container# قرار دارند، بدون ذکر نام container# نوشته شده‌اند و این کار سبب کمتر شدن کدهای نوشته شده و خواناتر شدن آن می‌شود.
در صورتیکه نیاز به دسترسی به والد داشته باشید کافیست از علامت & استفاده کنید.
a.myAnchor {
    color: blue;

    &:hover {
        text-decoration: underline;
    }

    &:visited {
        color: purple;
    }
}
خوب تا اینجا شما با روش نوشتن استایل‌های به صورت تو در تو آشنا شدید. حال اگر بخواهید کد‌های نوشته شده‌ی تو در تو را به صورت غیر تو در تو کامپایل کنید، باید از کلمه‌ی کلیدی at-root@ قبل استایلی که می‌خواهید به صورت تو در تو تعریف نشود، استفاده کنید.
.first-component {
    .text { font-size: 1.4em; }
    .button { font-size: 1.7em; }

    .second-component {
        .text { font-size: 1.2em; }
        .button { font-size: 1.4em; }
    }
}
مثال بالا در حالت تو در تو، پس از کامپایل به صورت زیر تبدیل می‌شود:
.first-component .text {
  font-size: 1.4em;
}
.first-component .button {
  font-size: 1.7em;
}
.first-component .second-component .text {
  font-size: 1.2em;
}
.first-component .second-component .button {
  font-size: 1.4em;
}
حال با استفاده از at-root@ به صورت زیر می‌شود:
.first-component .text {
  font-size: 1.4em;
}
.first-component .button {
  font-size: 1.7em;
}
.second-component .text {
  font-size: 1.2em;
}
.second-component .button {
  font-size: 1.4em;
}
اشتراک‌ها
Entity Framework Core 6.0 Preview 2 منتشر شد

Today, the Entity Framework Core team announces the second preview release of EF Core 6.0. This release includes changes to handling the synchronization context when SaveChangesAsync is called, smoother integration with System.Linq.Async, updates to string concatenation and improvements to free text search. 

Entity Framework Core 6.0 Preview 2 منتشر شد
مطالب
مروری بر کتابخانه ReactJS - قسمت هشتم - آخرین قسمت - چرخه حیات کامپوننت‌ها

هر کامپوننتی در React یک چرخه زندگی دارد. زمانیکه یک کامپوننت را به روش React.createClass یا React.Component تعریف میکنیم و در ReactDOM.render نمونه‌ای از کامپوننت را برای نمایش در مرورگر می‌سازیم، چرخه حیات آن شروع میشود. 


ReactDOMServer

کتابخانه ReactDOMServer جهت ساخت یا render کردن کامپوننت‌ها در سمت سرور استفاده میشود. توسط این کتابخانه میتوانیم کامپوننت‌ها را در سمت سرور ایجاد کنیم و نتیجه آن را که تگ‌های HTML هستند به مرورگر ارسال کنیم. این روش جهت داشتن صفحه‌های وب سریع‌تر و اهداف SEO مفید است. جهت اطلاعات بیشتر و روش‌های استفاده به مستندات آن رجوع کنید. در مثال زیر روش استفاده از این کتابخانه به اختصار آمده.

var persons = [
    { id: 1, personName: "Parham", personContact: "parhamda@gmail.com" },
    { id: 2, personName: "Roham", personContact: "roham@yahoo.com" },
    { id: 3, personName: "Raha", personContact: "raha@live.com" }
];

class Person extends React.Component{
    render(){
        return (
            <div>
                <p>{this.props.personName}</p>
                <p>{this.props.personContact}</p>
            </div>
        )
    }
}

let person1 = persons[0];
let personElement = <Person personName={person1.personName} personContact={person1.personContact}/>
console.log(ReactDOMServer.renderToStaticMarkup(personElement));

در کد بالا مواردی که جدید هستند، یکی ساخت یک نمونه از کامپوننت Person است و دیگری ساخت آن در سمت سرور، بدون آن که فعلا نمایشی در مرورگر داشته باشیم. در کنسول میتوانیم خروجی کتابخانه را که تگ‌های HTML هستند ببینیم. ReactDOMServer دو متد را فراهم کرده که کارکردی مشابه دارند؛ اما در جزئیات متفاوت هستند. 

  • renderStaticMarkup یک خروجی استاتیک و بدون attributeهای اضافه را تولید میکند که بیشتر برای بررسی یا استفاده در صفحه‌های وب ایستا مفید هستند.
  • renderToString یک خروجی به صورت HTML String ایجاد میکند که برای HTML DOM در سمت کاربر سازگار‌تر است و مناسب برای صفحات پویا. 

در نهایت خروجی از هر نوع که بود، برای اینکه در سمت کاربر قابل مشاهده باشد باید از همان متد ReactDOM.render استفاده کنیم. از آنجایی که این مجموعه جهت معرفی و بررسی ابزارهای اصلی React به صورت مختصر است، از آوردن مثال‌های زیاد و پیچیده پرهیز میکنم. در اینجا میتوانید یک نمونه ساده برای استفاده از ReactDOMServer به صورت استاندارد و با جزئیات را بررسی کنید.


متدهای چرخه حیات در React

React چند متد را برای زمان‌های قبل و بعد از ساخت شدن یک کامپوننت در DOM دارد که میشود رفتارهایی را برای کامپوننت، در این متدها در نظر گرفت تا در زمان مناسب اجرا شوند. در ادامه این متد‌ها معرفی و کاربرد هر یک بیان میشود. 


 componentWillMount: این متد قبل از اینکه کامپوننت، تگ‌های متد render را بسازد اجرا میشود. این متد هم در سمت کلاینت کاربرد دارد و هم در سمت سرور. به همین جهت برای گرفتن log از داده‌های کامپوننت و کار با پایگاه داده مکان مناسبی است. به عنوان مثال در قطعه کد زیر داده‌های کامپوننت، توسط Ajax ارسال شده‌اند. 

componentWillMount() {
   Ajax.post("/componentLog", {
     name: this.constructor.name,
     props: this.props
   });
}

componentDidMount: این متد بعد از اینکه بخش render اجرا شد فراخوانی میشود. همچنین فقط در سمت کلاینت و زمانیکه از ReactDOM.render استفاده میکنیم کاربرد دارد. این متد مناسب برای تعامل کامپوننت با افزونه‌ها و API‌ها است؛ مانند دریافت اطلاعات مورد نیاز کامپوننت از سایتی دیگر توسط یک API. از  این متد در قسمت چهارم مثالی آورده شده. 


(componentWillReciveProps(nextProps: این متد زمانی اجرا میشود که داده‌های ورودی کامپوننت با مقادیری جدید تغییر کنند.

componentWillReceiveProps(nextProps) {
    // Do something with new received data and change the state. 
}

ReactDOM.render(
    <TestComponent someData={newDataEveryFiveSecond()}/>,
    document.getElementById("divTest")
);

در مثال بالا یک کامپوننت داریم که داده‌های ورودی خود را از یک تابع میگیرد. این تابع هر پنج ثانیه یک بار یک داده تازه ایجاد میکند و به کامپوننت ارسال میکند. میتوانیم داخل کامپوننت، از متد componentWillReceiveProps جهت دستکاری داده‌های رسیده و تغییر وضعیت کامپوننت توسط setState استفاده کنیم. 


(shouldComponentUpdate(nextProps, nextState: این متد شبیه به متد componentWillReceiveProps است، البته با تفاوت‌هایی. این متد هم مقدار ورودی جدید برای پارامتر‌های کامپوننت میگیرد و هم مقداری برای وضعیتی که کامپوننت دارد. این متد باید یک مقدار بازگشتی false یا  true داشته باشد. با این مقدار بازگشتی میتوان کنترل کرد که آیا کامپوننت بر اساس داده‌های جدید بروز بشود یا نه. 

class ComponentExample extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        return notEqual(this.props, nextProps) ||
            notEqual(this.state, nextState);
    }
}

در مثال بالا پارامترها و وضعیت جاری کامپوننت، با مقدارهای تازه تغییر یافته و وضعیت جدید مقایسه میشوند. اگر مقادیر مقایسه شده برابر نباشند (یعنی داده تکراری وارد نشده) مقدار بازگشتی true خواهد بود و React کامپوننت را بر اساس وضعیت جدید و داده‌های تازه دوباره میسازد.


(componentWillUpdate(nextProps, nextState: این متد زمانیکه کامپوننت ساخته شده، داده‌های جدیدی را دریافت کند و یا وضعیت آن تغییر کند و دقیقا قبل از اجرای render فراخوانی میشود. اگر از متد shouldComponentUpdate مقدار false بازگشت داده شود، این متد دیگر اجرا نخواهد شد. باید توجه داشته باشیم که setState را نمیشود در این متد پیاده‌سازی کرده. به این علت که، زمانیکه وضعیت کامپوننت تغییر میکند، React متد componentWillUpdate و بلافاصله بعد از آن render را اجرا میکند و برای تغییر وضعیت دیگر دیر شده! تفاوت componentWillUpdate با componentWillMount  این است که Will Mount در اولین وهله سازی از کامپوننت اجرا میشود، ولی Will Update بعد از هر دوباره سازی (rerender). 


(componentDidUpdate(prevProps, prevStat: احتمالا میشود به راحتی حدس زد که این متد دقیقا بعد از دوباره سازی کامپوننتی که ساخته شده فراخوانی میشود.


componentWillUnmount: این متد زمانی اجرا میشود که یک کامپوننت از DOM پاک شود. برای پاک کردن نمونه‌ای از یک کامپوننت که در DOM در حال نمایش است میتوانیم از دستور زیر استفاده کنیم. 

ReactDOM.unmountComponentAtNode(document.getElementById("react"));
بازخوردهای پروژه‌ها
مشکل عمل نکردن فونت فارسی
سلام
باتشکر از شما
آقای نصیری بنده برای راحتی استفاده در برنامه یک کلاس استاتیک بصورت زیر تعریف کرده ام :
  public static class ReportMethod
    {
        static FontSelector fontSelector = new FontSelector();
        const char RightToLeftEmbedding = (char)PersianDate.RightToLeftEmbedding;
        const char PopDirectionalFormatting = (char)PersianDate.PopDirectionalFormatting;
        public static Dictionary<string, string> fontDicBody = new Dictionary<string, string> { { "BMitra", "B Mitra" }, { "tahoma", "tahoma" } };
        public static Dictionary<string, string> fontDicHeader1 = new Dictionary<string, string> { { "BMitraBd", "B Mitra Bold" }, { "tahoma", "tahoma" } };
        public static Dictionary<string, string> fontDicHeader2 = new Dictionary<string, string> { { "BTitrBd", "B Titr Bold" }, { "tahoma", "tahoma" } };
        public static Dictionary<string, string> fontDicFooter = new Dictionary<string, string> { { "BMitra", "B Mitra" }, { "tahoma", "tahoma" } };

        public static string FixWeakCharacters(string data)
        {
            if (string.IsNullOrWhiteSpace(data))
                return string.Empty;
            var weakCharacters = new[] { @"\", "/", "+", "-", "=", ";", "$" };
            foreach (var weakCharacter in weakCharacters)
            {
                data = data.Replace(weakCharacter, RightToLeftEmbedding + weakCharacter + PopDirectionalFormatting);
            }
            return data;
        }

        public static Phrase SetFont(string data, int fontType)
        {
            Dictionary<string, string> fontDic;
            float fontSize = 11;
            switch (fontType)
            {
                case 0:
                    fontDic = fontDicBody;
                    fontSize = 11;
                    break;
                case 1:
                    fontDic = fontDicHeader1;
                    fontSize = 14;
                    break;
                case 2:
                    fontDic = fontDicFooter;
                    fontSize = 12;
                    break;
                case 11:
                    fontDic = fontDicHeader2;
                    fontSize = 18;
                    break;
                default:
                    fontDic = fontDicBody;
                    fontSize = 11;
                    break;
            }
            foreach (var item in fontDic)
            {
                FontFactory.Register("c:\\windows\\fonts\\" + item.Key + ".ttf");
                Font newfont = FontFactory.GetFont(item.Value, BaseFont.IDENTITY_H, fontSize);
                if (newfont.Familyname != "unknown")
                    fontSelector.AddFont(newfont);
            }
            return fontSelector.Process(FixWeakCharacters(data));
        }

        public static PdfPCell SetCell(string text, int border, int colspan, int Horizontal, int Vertical, bool DirectionRTL, int fontType = 0)
        {
            if (DirectionRTL)
            {
                var cell = new PdfPCell { RunDirection = PdfWriter.RUN_DIRECTION_RTL };
                cell.Border = border;
                cell.Colspan = colspan;
                cell.HorizontalAlignment = Horizontal;
                cell.VerticalAlignment = Vertical;
                cell.Phrase = new Phrase(ReportMethod.SetFont(text, fontType));
                return cell;
            }
            else
            {
                var cell = new PdfPCell();
                cell.Border = border;
                cell.Colspan = colspan;
                cell.HorizontalAlignment = Horizontal;
                cell.VerticalAlignment = Vertical;
                cell.Phrase = new Phrase(ReportMethod.SetFont(text, fontType));
                return cell;
            }
        }
    }
که این کلاس برای ایجاد سلول با فونت مورد نظر من معرفی شده است
و کد گزارش من به صورت زیر تعریف شده است :
    public IPdfReportData CreatePdfReport_SRptTeach(int MemberID, List<sp_Teach_Communicate_Select_ReportTeachResult> Teach_Result, string st, List<sp_Institute_Center_Info_Select_Name_MasterResult> Info)
        {
            string fileName = string.Format("SRptTeach-{0}.pdf", Guid.NewGuid().ToString("N"));            
            return new PdfReport()
                                  .DocumentPreferences(doc =>
                                  {
                                      doc.RunDirection(PdfRunDirection.RightToLeft);
                                      doc.Orientation(PageOrientation.Landscape);
                                      doc.PageSize(PdfPageSize.A4);
                                      doc.DocumentMetadata(new DocumentMetadata { Author = Info[0].InstName, Application = "PdfRpt", Keywords = "گزارش", Subject = "گزارش ویژه", Title = "گزارش کارکرد مدرسید" });
                                      doc.Compression(new CompressionSettings
                                      {
                                          EnableCompression = true,
                                          EnableFullCompression = true
                                      });
                                      doc.PrintingPreferences(new PrintingPreferences
                                      {
                                          ShowPrintDialogAutomatically = false
                                      });
                                  })
                                  .DefaultFonts(fonts =>
                                  {
                                      fonts.Path(System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), "fonts\\" + ReportMethod.fontDicBody.ElementAt(0).Key + ".ttf"),
                                          System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), "fonts\\" + ReportMethod.fontDicBody.ElementAt(1).Key + ".ttf"));
                                      fonts.Size(11);
                                      fonts.Color(System.Drawing.Color.Black);
                                  })
                                  .PagesFooter(footer =>
                                  {
                                      footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.RightToLeft));
                                  })
                                  .PagesHeader(header =>
                                  {
                                      header.CustomHeader(new CustomHeader_SRptTeach(MemberID, st, Info));
                                  })
                                  .MainTableTemplate(template =>
                                  {
                                      //template.BasicTemplate(BasicTemplate.SimpleTemplate);
                                      template.CustomTemplate(new TransparentTemplate());
                                  })
                                  .MainTablePreferences(table =>
                                  {
                                      table.ColumnsWidthsType(TableColumnWidthType.Relative);
                                      table.GroupsPreferences(new GroupsPreferences
                                      {
                                          GroupType = GroupType.IncludeGroupingColumns,
                                          RepeatHeaderRowPerGroup = true,
                                          ShowOneGroupPerPage = false,
                                          SpacingBeforeAllGroupsSummary = 5f,
                                          ShowGroupingPropertiesInAllRows = true
                                      });
                                  })
                                  .MainTableDataSource(dataSource =>
                                  {
                                      dataSource.StronglyTypedList<sp_Teach_Communicate_Select_ReportTeachResult>(Teach_Result);
                                  })
                                  .MainTableSummarySettings(summarySettings =>
                                  {
                                      summarySettings.OverallSummarySettings("جمع مبالغ");
                                      summarySettings.AllGroupsSummarySettings("جمع کل مبالغ");
                                  })
                                  .MainTableColumns(columns =>
                                  {
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.RowNo);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(0);
                                          column.Width(4);
                                          column.HeaderCell("ردیف");
                                      });

                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.ParentName);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(1);
                                          column.Width(5);
                                          column.HeaderCell("مرکز");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.FullName);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(2);
                                          column.Width(12);
                                          column.HeaderCell("نام و نام خانوادگی");
                                          column.Group(true,
                                              (val1, val2) =>
                                              {
                                                  return val1.ToString() == val2.ToString();
                                              });
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TermInfoName);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(3);
                                          column.Width(8);
                                          column.HeaderCell("ترم");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Contract_NO);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(4);
                                          column.Width(7);
                                          column.HeaderCell("شماره قرارداد");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.LessonFullCode);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(5);
                                          column.Width(4);
                                          column.HeaderCell("کد درس");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.LessonName);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(6);
                                          column.Width(10);
                                          column.HeaderCell("نام درس");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Start_Date_Lesson);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(7);
                                          column.Width(6);
                                          column.HeaderCell("تاریخ شروع");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.End_Date_Lesson);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(8);
                                          column.Width(6);
                                          column.HeaderCell("تاریخ پایان");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TeachAmount);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(9);
                                          column.Width(6);
                                          column.HeaderCell("مبلغ حق التدریس(ریال)");
                                          column.ColumnItemsTemplate(template =>
                                          {
                                              template.TextBlock();
                                              template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                                          });
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.DoTeacherTime);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(10);
                                          column.Width(3);
                                          column.HeaderCell("ساعت کارکرد");
                                          column.ColumnItemsTemplate(template =>
                                          {
                                              template.TextBlock();
                                              template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                                          });
                                          column.AggregateFunction(aggregateFunction =>
                                          {
                                              aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                                              aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                                          });
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName("CanPay");
                                          column.CalculatedField(true,
                                              list =>
                                              {
                                                  if (list == null)
                                                      return string.Empty;
                                                  var amount = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TeachAmount);
                                                  var doTime = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.DoTeacherTime);
                                                  var result = float.Parse(amount) * float.Parse(doTime);
                                                  return Convert.ToDecimal(result);
                                              });
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(11);
                                          column.Width(7);
                                          column.HeaderCell("قابل پرداخت(ریال)");
                                          column.ColumnItemsTemplate(template =>
                                          {
                                              template.TextBlock();
                                              template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                                          });
                                          column.AggregateFunction(aggregateFunction =>
                                          {
                                              aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                                              aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                                          });
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Personal_Education);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(12);
                                          column.Width(6);
                                          column.HeaderCell("مدرک تحصیلی");
                                      });
                                      //columns.AddColumn(column =>
                                      //{
                                      //    column.PropertyName("Descriptions");
                                      //    column.CalculatedField(true,
                                      //        list =>
                                      //        {
                                      //            if (list == null)
                                      //                return string.Empty;
                                      //            var Row = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.RowNoPerson);
                                      //            return "";
                                      //        });
                                      //    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                      //    column.IsVisible(true);
                                      //    column.Order(13);
                                      //    column.Width(3);
                                      //    column.HeaderCell("توضیحات");
                                      //});
                                  })
                                  .MainTableEvents(events =>
                                  {
                                      events.DataSourceIsEmpty(message: "اطلاعاتی برای نمایش وجود ندارد.");
                                      events.DocumentClosing(docClose =>
                                      {
                                          string[] msgField = { "مدیر گروه", Info.Where(sp => sp.ID == MemberID).FirstOrDefault().InstKindName, Info.Where(sp => sp.ID == 0).FirstOrDefault().InstKindName, "امور مالی", "معاون پشتیبانی" };
                                          string[] dataField = { "", Info.Where(sp => sp.ID == MemberID).FirstOrDefault().MasterName, Info.Where(sp => sp.ID == 0).FirstOrDefault().MasterName, "", Info.Where(sp => sp.ID == 1).FirstOrDefault().MasterName };
                                          var infoTable = new PdfGrid(msgField.Length) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, WidthPercentage = 100 };
                                          foreach (var item in msgField)
                                          {
                                              infoTable.AddCell(ReportMethod.SetCell(item, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true));
                                          }
                                          foreach (var item in dataField)
                                          {
                                              infoTable.AddCell(ReportMethod.SetCell(item, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true));
                                          }
                                          docClose.PdfDoc.Add(infoTable);
                                      });
                                  })
                                  .Export(export =>
                                  {
                                      export.ToExcel();
                                      export.ToCsv();
                                      export.ToXml();
                                      export.ToString();
                                  })
                                  .Generate(data =>
                                  {
                                      fileName = HttpUtility.UrlEncode(fileName, Encoding.UTF8);
                                      data.FlushInBrowser(fileName, FlushType.Inline);
                                  });
            //.Generate(data => data.AsPdfFile(string.Format("{0}\\PlansPage\\RptIListSample-{1}.pdf", AppPath.ApplicationPath, Guid.NewGuid().ToString("N"))));
        }
و قسمت هدر گزارش به صورت سفارشی به صورت زیر معرفی شده است :
namespace Academy.Control.Reports
{
    public class CustomHeader_SRptTeach : IPageHeader
    {
        public IPdfFont PdfRptFont { set; get; }
        string st;
        List<sp_Institute_Center_Info_Select_Name_MasterResult> Info;
        int MemberID;

        public CustomHeader_SRptTeach(int MemberID, string st, List<sp_Institute_Center_Info_Select_Name_MasterResult> Info)
        {
            this.st = st;
            this.Info = Info;
            this.MemberID = MemberID;
        }

        public PdfGrid RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> rowdata, IList<SummaryCellData> summaryData)
        {
            // return null;
            var groupFullName = rowdata.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.FullName);
            var groupPersonalEducation = rowdata.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Personal_Education);

            var table = new PdfGrid(2) { WidthPercentage = 100 };
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "نام و نام خانوادگی:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = groupFullName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "مدرک تحصیلی :";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = groupPersonalEducation;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 10f);

        }    

        public PdfGrid RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            var tableMain = new PdfGrid(1) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, WidthPercentage = 100 };
            tableMain.DefaultCell.Border = PdfPCell.NO_BORDER;
            PdfGrid table = new PdfGrid(3);
            table.DefaultCell.Border = PdfPCell.NO_BORDER;

            table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false));
            table.AddCell(ReportMethod.SetCell("گزارش کارکرد مدرسین ", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true,11));
            PdfPTable tbRight = new PdfPTable(1) { RunDirection = PdfWriter.RUN_DIRECTION_RTL };
            tbRight.DefaultCell.Border = PdfPCell.NO_BORDER;

            Image _image = Image.GetInstance(System.IO.Path.Combine(AppPath.ApplicationPath, "Content\\Images\\p_jahad2.jpg"));
            var cellImg = new PdfPCell(_image, false) { Border = PdfPCell.NO_BORDER };
            cellImg.HorizontalAlignment = PdfPCell.ALIGN_CENTER;
            tbRight.AddCell(cellImg);
            tbRight.AddCell(ReportMethod.SetCell(Info[0].InstName, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false,1));
            tbRight.AddCell(ReportMethod.SetCell(Info.Where(sp => sp.ID == MemberID).FirstOrDefault().SecondName, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false,1));
            table.AddCell(tbRight);

            PdfGrid tbLeft = new PdfGrid(2) { RunDirection = PdfWriter.RUN_DIRECTION_RTL };
            tbLeft.DefaultCell.Border = PdfPCell.NO_BORDER;
            tbLeft.AddCell(ReportMethod.SetCell("تاریخ گزارش : " + System.DateTime.Now.ToPersianDateTime("/", false), PdfPCell.NO_BORDER, 2, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false));
            tbLeft.AddCell(ReportMethod.SetCell("از تاریخ " + st.Split(';')[0], PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false));
            tbLeft.AddCell(ReportMethod.SetCell("تا تاریخ " + st.Split(';')[1], PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false));

            table.AddCell(tbLeft);
            table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false));
            table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false));
            tableMain.AddCell(table);
            return tableMain;
        }
    }
}
حالا من دو مشکل دارم که هرچی سعی کردم نتونستم این موارد را رفع کنم و درخواست راهنمایی دارم از شما :
1. فونت هایی که من معرفی کردم برای هدر اصلا اعمال نمی‌شود و همینطور فونت در متن اصلی هم که تغییر میدم باز تغییر ایجاد نمیشه و به نظر میاد اصلا اعمال نمیشه به کل و هدر و متن با یک فونت نمایش داده میشه در صورتی که من فونت‌ها و سایز‌های متفاوتی برای متن‌ها انتخاب میکنم و اعمال میکنم
2. قسمت گروه هدری که من معرفی کردم اصلا کار نمیکنه و نمایش داده نمیشه
ممنون میشم شما من رو راهنمایی کنید 
مثال‌های قبلی رو هم دیدم در مورد فونت و گروه هدر و سعی کردم مثل همون موارد اعمال کنم اما باز اعمال نشد
متشکرم از وقتی که می‌گذارید
مطالب
ایجاد یک فیلتر سفارشی جهت تعیین Layout برای کنترلر و یا اکشن متد
همانطور که می‌دانید در صورت عدم تعریف صریح layout در یک View، این تعریف از فایل Views\_ViewStart.cshtml دریافت می‌گردد:
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}

برای معرفی صریح فایل layout، تنها کافی است مسیر کامل فایل layout را در یک View مشخص کنیم: 
@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<h2>Index</h2>
حال ما می‌خواهیم یک فیلتر سفارشی را تعریف کنیم تا به براحتی امکان تعریف Layout در سطح کنترلر و هم در سطح اکشن متد به صورت Attribute را داشته باشد :
[SetLayoutAttribute("_MyLayout")]
public ActionResult Index()
{
      return View();
}

 همچنین می‌توانیم این فیلتر سفارشی را در سطح کنترلر تعریف کنیم تا تمام اکشن متدهای داخل کنترلر از Layout مربوطه استفاده کنند:
[SetLayoutAttribute("_MyLayout")]
    public class HomeController : Controller
    {
        
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }
    }

 برای تعریف چنین اکشن فیلتری کد زیر را می‌نویسیم :
public class SetLayoutAttribute : ActionFilterAttribute
    {
        private readonly string _masterName;
        public SetLayoutAttribute(string masterName)
        {
            _masterName = masterName;
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);
            var result = filterContext.Result as ViewResult;
            if (result != null)
            {
                result.MasterName = _masterName;
            }
        }
    }

همانطور که می‌دانید برای تعریف یک اکشن فیلتر سفارشی می‌بایست از کلاس ActionFilterAttribute ارث بری کنیم، حالا برای کلاسی که تعریف کرده ایم یک خصوصیت readonly را تعریف و سپس برابر با یک پارامتر با نام masterName که از طریق سازنده کلاس دریافت می‌شود قرار داده ایم. در نهایت متد OnActionExecuted را بازنویسی کرده ایم به این صورت که مقدار دریافتی توسط سازنده کلاس را برابر با نام Layout موردنظرمان است را به خاصیت MasterName  اختصاص میدهیم.
مطالب
آشنایی با مفاهیم شیء گرایی در جاوا اسکریپت #2
از آنجا که برای کار با جاوا اسکریپت نیاز به درک کاملی درباره‌ی مفهوم حوزه کارکرد متغیرها (Scope) می‌باشد و نحوه فراخوانی توابع نیز نقش اساسی در این مورد بازی می‌کند، در این قسمت با این موارد آشنا خواهیم شد:
جاوا اسکریپت از مفهومی به نام functional scope برای تعیین حوزه متغیرها استفاده می‌کند و به این معنی است که با تعریف توابع، حوزه عملکرد متغیر مشخص می‌شود. در واقع هر متغیری که در یک تابع تعریف می‌شود در کلیه قسمتهای آن تابع، از قبیل If statement – for loops و حتی nested function نیز در دسترس میباشد.
اجازه دهید با مثالی این موضوع را بررسی نماییم.
function testScope() {
var myTest = true;
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
testScope(); // will alert "I am changed!"
همانگونه که میبینیم با اینکه در داخل بلاک if یک متغیر جدید تعریف شده، ولی در خارج از این بلاک نیز این متغیر قابل دسترسی میباشد. البته در مثال بالا اگر بخواهیم به متغیر myTest در خارج از function دسترسی داشته باشیم، با خطای undefined مواجه خواهیم شد. یعنی برای مثال در کد زیر:
function testScope() {
var myTest = true;
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
testScope(); // will alert "I am changed!"
alert(myTest); // will throw a reference error, because it doesn't exist outside of the function
 برای حل این مشکل دو راه وجود دارد: 
1 – متغیر myTest را در بیرون بلاک testScope() تعریف کنیم
2 – هنگام تعریف متغیر myTest، کلمه کلیدی var را حذف کنیم که این موضوع باعث میشود این متغیر در کل window قابل دسترس باشد و یا به عبارتی متغیر global میشود.
قبل از پرداختن به ادامه بحث خواندن مقاله مربوط به Closure در جاوااسکریپت توصیه میگردد .
در پایان بحث Scope‌ها با یک مثال نسبتا جامع اکثر این حالات به همراه خروجی را نشان میدهیم :
<script type="text/javascript">
          // a globally-scoped variable
        var a = 1;
        // global scope
        function one()
        {
            alert(a);
        }
        // local scope
        function two(a)
        {
            alert(a);
        }
        // local scope again
        function three()
        {
            var a = 3;
            alert(a);
        }
        // Intermediate: no such thing as block scope in javascript
        function four()
        {
            if (true)
            {
                var a = 4;
            }
            alert(a); // alerts '4', not the global value of '1'
        }
        // Intermediate: object properties
        function Five()
        {
            this.a = 5;
        }
        // Advanced: closure
        var six = function ()
        {
            var foo = 6;
            return function ()
            {
                // javascript "closure" means I have access to foo in here, 
                // because it is defined in the function in which I was defined.
                alert(foo);
            }
        }()
        // Advanced: prototype-based scope resolution
        function Seven()
        {
            this.a = 7;
        }
        // [object].prototype.property loses to [object].property in the lookup chain
        Seven.prototype.a = -1; // won't get reached, because 'a' is set in the constructor above.
        Seven.prototype.b = 8; // Will get reached, even though 'b' is NOT set in the constructor.
        // These will print 1-8
        one();
        two(2);
        three();
        four();
        alert(new Five().a);
        six();
        alert(new Seven().a);
        alert(new Seven().b);
</Script>
برای مطالعه بیشتر به اینجا  مراجعه نمایید.

Function Invocation Patterns In JavaScript :
از آنجا که توابع در جاوااسکریپت به منظور 1 – ساخت اشیاء  و 2 – حوزه دسترسی متغیرها(Scope)  نقش اساسی ایفا می‌کنند بهتر است کمی درباره استفاده و نحوه فراخوانی آنها  (Function Invocation Patterns) در جاوااسکریپت بحث نماییم.
در جاوااسکریپت 4 مدل فراخوانی تابع داریم که به نامهای زیر مطرح هستند:
1. Method Invocation
2. Function Invocation
3. Constructor Invocation
4. Apply And Call Invocation
 در فراخوانی توابع به هر یک از روشهای بالا باید به این نکته توجه داشت که حوزه دسترسی متغیرها در جاوااسکریپت ابتدا و انتهای توابع هستند و اگر به عنوان مثال از توابع تو در تو استفاده کردیم ،حوزه شی this برای توابع داخلی تغییر خواهد کرد .این موضوع را در طی مثالهایی نشان خواهیم داد.
Method Invocation :
وقتی یک تابع قسمتی از یک شی باشد به آن متد میگوییم به عنوان مثال :
var obj = {
    value: 0,
    increment: function() {
        this.value+=1;
    }
};
obj.increment(); //Method invocation
در اینحالت this به شی (Object) اشاره میکند که متد در آن فراخوانی شده است و در زمان اجرا نیز به عناصر شی Bind میشود ،در مثال بالا حوزه  this شی obj خواهد شد و به همین منظور به متغیر value دسترسی داریم.
Function Invocation:
در اینحالت که از () برای فراخوانی تابع استفاده میگردد ،This به شی سراسری (global object ) اشاره می‌کند؛ منظور اینکه this به اجزای تابعی که فراخوانی آن انجام شده اشاره نمی‌کند. اجازه دهید با مثالی این موضوع را روشن کنیم
<script type="text/javascript">
var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        this.value++;
        var innerFunction = function() {
            alert(this.value);
        }
        innerFunction(); //Function invocation pattern
    }
}
obj.increment(); //Method invocation pattern
<script type="text/javascript">
Result : 500
از آنجا که  () innerFunction به شکل  Function invocation pattern فراخوانی شده است به متغیر value در داخل تابع increment دسترسی نداریم و حوزه دسترسی global میشود و اگر در حوزه global نیز این متغیر تعریف نشده بود به خطای undefined میرسیدیم .
برای حل این گونه مشکلات ساختار کد نویسی ما بایستی به شکل زیر باشد :
<script type="text/javascript">
var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        var that = this;
        that.value++;
        var innerFunction = function() {
            alert(that.value);
        }
        innerFunction(); //Function invocation pattern
    }
}
obj.increment();
<script type="text/javascript">
Result : 1
در واقع با تعریف یک متغیر با نام مثلا that و انتساب شی  this به آن میتوان در توابع بعدی که به شکل   Function invocation pattern فراخوانی میگردند به این متغیر دسترسی داشت .
Constructor Invocation :
در این روش برای فراخوانی تابع از کلمه new استفاده میکنیم. در این حالت یک شیء مجزا ایجاد شده و به متغیر دلخواه ما اختصاص پیدا می‌کند. به عنوان مثال داریم :
 var Dog = function(name) {   
  //this == brand new object ({});    
    this.name = name;    
    this.age = (Math.random() * 5) + 1;
};
var myDog = new Dog('Spike');
//myDog.name == 'Spike'
//myDog.age == 2
var yourDog = new Dog('Spot');
//yourDog.name == 'Spot'
//yourDog.age == 4
در این مورد با استفاده از New باعث میشویم همه خواص و متدهای تابع function برای هر نمونه از آن که ساخته میشود ( از طریق مفهوم Prototype که قبلا درباره آن بحث شد) بطور مجزا اختصاص یابد. در مثال بالا شی mydog چون حاوی یک نمونه از تابع dog بصورت  Constructor Invocation میباشد، در نتیجه به خواص تابع dog از قبیل name  و age دسترسی داریم. در اینجا اگر کلمه new استفاده نشود به این خواص دسترسی نداریم؛ در واقع با اینکار، this به mydog اختصاص پیدا میکند.
اگر از new استفاده نشود متغیر myDog ،undefined میشود.
یک مثال دیگر :
var createCallBack = function(init) { //First function
    return new function() { //Second function by Constructor Invocation
        var that = this;
        this.message = init;
        return function() { //Third function
            alert(that.message);
        }
    }
}
window.addEventListener('load', createCallBack("First Message"));
window.addEventListener('load', createCallBack("Second Message"));
در مثال بالا از مفهوم closure  نیز در مثالمان استفاده کرده ایم .
Apply And Call Invocation:
تمامی توابع جاوااسکریپت دارای دو متد توکار apply() و call() هستند که توسط این متدها میتوان این توابع را با context دلخواه فراخوانی کرد.
نحوه فراخوانی به شکل مقابل است :
myFunction.apply(thisContext, arrArgs);
myFunction.call(thisContext, arg1, arg2, arg3, ..., argN);
که thisContext به حوزه اجرایی (execution context) تابع اشاره میکند. تفاوت دو متد apply() و call() در نحوه فرستادن آرگومانها به تابع میباشد که در اولی توسط آرایه اینکار انجام میشود و در دومی همه آرگومانها را بطور صریح نوشته و با کاما از هم جدا میکنیم .
مثال :
var contextObject = {
testContext: 10
}
var otherContextObject = {
testContext: "Hello World!"
}
var testContext = 15; // Global variable
function testFunction() {
alert(this.testContext);
}
testFunction(); // This will alert 15
testFunction.call(contextObject); // Will alert 10
testFunction.apply(otherContextObject); // Will alert "Hello World”
در این مثال دو شی متفاوت با خواص همنام تعریف کرده و یک متغیر global نیز تعریف میکنیم. در انتها یک تابع تعریف میکنیم که مقدار this.testContext را نمایش میدهد. در ابتدا حوزه اجرایی تابع (this) کل window جاری میباشد و وقتی testFunction() اجرا شود مقدار متغیر global نمایش داده میشود. در اجرای دوم this به contextObject اشاره کرده و حوزه اجرایی عوض میشود و در نتیجه مقدار testContext مربوطه که در این حالت 10 میباشد نمایش داده میشود و برای فراخوانی سوم نیز به همین شکل .
یک مثال کاملتر :
var o = {
  i : 0,
  F : function() {
    var a = function() { this.i = 42; };
    a();
    document.write(this.i);
  }
};
o.F();
Result :0
خط o.f() تابع f را به شکل Method invocation اجرا میکند. در داخل تابع f یک تابع دیگر به شکل function invocation اجرا میشود که در اینحال this به global object اشاره میکند و باعث میشود مقدار i در خروجی 0 چاپ شود .
برای حل این مشکل 2 راه وجود دارد  
راه اول :
var p = {
  i : 0,
  F : function() {
    var a = function() { this.i = 42; };
    a.apply(this);
    document.write(this.i);
  }
};
 p.F();
Result :42
با اینکار this را موقع اجرای تابع درونی برایش فرستاده تا حوزه اجرای تابع عوض شود و به i دسترسی پیدا کنیم .
یا اینکه همانند مثالهای قبلی :
var q = {
  i: 0,
  F: function F() {
    var that = this;
    var a = function () {
      that.i = 42;
    }
    a();
    document.write(this.i);
  }
}
 q.F();

منابع :
Javascript programmer,s refrence
 
مطالب
پیاده سازی SoftDelete در EF Core
در مورد حذف منطقی در EF 6x، پیشتر مطالبی را در این سایت مطالعه کرده‌اید:
- «پیاده سازی حذف منطقی در Entity framework» حذف منطقی، یکی از الگوهای بسیار پرکاربرد در برنامه‌های تجاری است. توسط آن بجای حذف فیزیکی اطلاعات، آن‌ها را تنها به عنوان رکوردی حذف شده، «علامتگذاری» می‌کنیم. مزایای آن نیز به شرح زیر هستند:
- داشتن سابقه‌ی حذف اطلاعات
- جلوگیری از cascade delete
- امکان بازیابی رکوردها و امکان ایجاد قسمتی به نام recycle bin در برنامه (شبیه به recycle bin در ویندوز که امکان بازیابی موارد حذف شده را می‌دهد)
- امکان داشتن رکوردهایی که در یک برنامه (به ظاهر) حذف شده‌اند، اما هنوز در برنامه‌ی دیگری در حال استفاده هستند.
- بالابردن میزان امنیت برنامه. فرض کنید سایت شما هک شده و شخصی، دسترسی به پنل مدیریتی و سطوح دسترسی مدیریتی برنامه را پیدا کرده‌است. در این حالت حذف تمام رکوردهای سایت توسط او، تنها به معنای تغییر یک بیت، از یک به صفر است و بازگرداندن این درجه از خسارت، تنها با روشن کردن این بیت، برطرف می‌شود.

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


نیاز به تعریف دو خاصیت جدید در هر جدول

هر جدولی که قرار است soft delete به آن اعمال شود، باید دارای دو فیلد جدید bool IsDeleted و DateTime? DeletedAt باشد. می‌توان این خواص را به هر موجودیتی به صورت دستی اضافه کرد و یا می‌توان ابتدا یک کلاس پایه‌ی abstract را برای آن ایجاد کرد:
using System;

namespace EFCoreSoftDelete.Entities
{
    public abstract class BaseEntity
    {
        public int Id { get; set; }


        public bool IsDeleted { set; get; }
        public DateTime? DeletedAt { set; get; }
    }
}
و سپس موجودیت‌هایی را که قرار است از soft delete پشتیبانی کنند، توسط آن علامتگذاری کرد؛ مانند موجودیت Blog:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Blog : BaseEntity
    {
        public string Name { set; get; }

        public virtual ICollection<Post> Posts { set; get; }
    }

    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder.Property(blog => blog.Name).HasMaxLength(450).IsRequired();
            builder.HasIndex(blog => blog.Name).IsUnique();

            builder.HasData(new Blog { Id = 1, Name = "Blog 1" });
            builder.HasData(new Blog { Id = 2, Name = "Blog 2" });
            builder.HasData(new Blog { Id = 3, Name = "Blog 3" });
        }
    }
}
که هر بلاگ از تعدادی مطلب تشکیل شده‌است:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Post : BaseEntity
    {
        public string Title { set; get; }

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

    public class PostConfiguration : IEntityTypeConfiguration<Post>
    {
        public void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.Property(post => post.Title).HasMaxLength(450);
            builder.HasOne(post => post.Blog).WithMany(blog => blog.Posts).HasForeignKey(post => post.BlogId);

            builder.HasData(new Post { Id = 1, BlogId = 1, Title = "Post 1" });
            builder.HasData(new Post { Id = 2, BlogId = 1, Title = "Post 2" });
            builder.HasData(new Post { Id = 3, BlogId = 1, Title = "Post 3" });
            builder.HasData(new Post { Id = 4, BlogId = 1, Title = "Post 4" });
            builder.HasData(new Post { Id = 5, BlogId = 2, Title = "Post 5" });
        }
    }
}
مزیت علامتگذاری این کلاس‌ها، امکان کوئری گرفتن از آن‌ها نیز می‌باشد که در ادامه از آن استفاده خواهیم کرد.


حذف خودکار رکوردهایی که Soft Delete شده‌اند، از نتیجه‌ی کوئری‌ها و گزارشات

تا اینجا فقط دو خاصیت ساده را به کلاس‌های مدنظر خود اضافه کرده‌ایم. پس از آن یا می‌توان در هر جائی برای مثال شرط context.Blogs.Where(blog => !blog.IsDeleted) را به صورت دستی اعمال کرد و در گزارشات، رکوردهای حذف منطقی شده را نمایش نداد و یا از زمان ارائه‌ی EF Core 2x می‌توان برای آن‌ها Query Filter تعریف کرد. برای مثال می‌توان به تنظیمات موجودیت Blog و یا Post مراجعه نمود و با استفاده از متد HasQueryFilter، همان شرط blog => !blog.IsDeleted را به صورت سراسری به تمام کوئری‌های مرتبط با این موجودیت‌ها اعمال کرد:
    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            // ...
            builder.HasQueryFilter(blog => !blog.IsDeleted);
        }
    }
از این پس ذکر context.Blogs دقیقا معنای context.Blogs.Where(blog => !blog.IsDeleted) را می‌دهد و دیگر نیازی به ذکر صریح شرط متناظر با soft delete نیست.
در این حالت کوئری‌های نهایی به صورت خودکار دارای شرط زیر خواهند شد:
SELECT [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[IsDeleted] <> CAST(1 AS bit)


اعمال خودکار QueryFilter مخصوص Soft Delete به تمام موجودیت‌ها

همانطور که عنوان شد، مزیت علامتگذاری موجودیت‌ها با کلاس پایه‌ی BaseEntity، امکان کوئری گرفتن از آن‌ها است:
namespace EFCoreSoftDelete.DataLayer
{
    public static class GlobalFiltersManager
    {
        public static void ApplySoftDeleteQueryFilters(this ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model
                                                    .GetEntityTypes()
                                                    .Where(eType => typeof(BaseEntity).IsAssignableFrom(eType.ClrType)))
            {
                entityType.addSoftDeleteQueryFilter();
            }
        }

        private static void addSoftDeleteQueryFilter(this IMutableEntityType entityData)
        {
            var methodToCall = typeof(GlobalFiltersManager)
                                .GetMethod(nameof(getSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)
                                .MakeGenericMethod(entityData.ClrType);
            var filter = methodToCall.Invoke(null, new object[] { });
            entityData.SetQueryFilter((LambdaExpression)filter);
        }

        private static LambdaExpression getSoftDeleteFilter<TEntity>() where TEntity : BaseEntity
        {
            return (Expression<Func<TEntity, bool>>)(entity => !entity.IsDeleted);
        }
    }
}
در اینجا در ابتدا تمام موجودیت‌هایی که از BaseEntity ارث بری کرده‌اند، یافت می‌شوند. سپس بر روی آن‌ها قرار است متد SetQueryFilter فراخوانی شود. این متد بر اساس تعاریف EF Core، یک LambdaExpression کلی را قبول می‌کند که نمونه‌ی آن در متد getSoftDeleteFilter تعریف شده و سپس توسط متد addSoftDeleteQueryFilter به صورت پویا به modelBuilder اعمال می‌شود.

محل اعمال آن نیز در انتهای متد OnModelCreating است تا به صورت خودکار به تمام موجودیت‌های موجود اعمال شود:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {

        //...


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

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseEntity).Assembly);
            modelBuilder.ApplySoftDeleteQueryFilters();
        }


مشکل! هنوز هم حذف فیزیکی رخ می‌دهد!

تنظیمات فوق، تنها بر روی کوئری‌های نوشته شده تاثیر دارند؛ اما هیچگونه تاثیری را بر روی متد Remove و سپس SaveChanges نداشته و در این حالت، هنوز هم حذف واقعی و فیزیکی رخ می‌دهد.
 برای رفع این مشکل باید به EF Core گفت، هر چند دستور حذف صادر شده، اما آن‌را تبدیل به دستور Update کن؛ یعنی فیلد IsDelete را به 1 و فیلد DeletedAt را با زمان جاری مقدار دهی کن:
namespace EFCoreSoftDelete.DataLayer
{
    public static class AuditableEntitiesManager
    {
        public static void SetAuditableEntityOnBeforeSaveChanges(this ApplicationDbContext context)
        {
            var now = DateTime.UtcNow;

            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        //TODO: ...
                        break;
                    case EntityState.Modified:
                        //TODO: ...
                        break;
                    case EntityState.Deleted:
                        entry.State = EntityState.Unchanged; //NOTE: For soft-deletes to work with the original `Remove` method.

                        entry.Entity.IsDeleted = true;
                        entry.Entity.DeletedAt = now;
                        break;
                }
            }
        }
    }
}
در اینجا با استفاده از سیستم tracking، رکوردهای حذف شده‌ی با وضعیت EntityState.Deleted، به وضعیت EntityState.Unchanged تغییر پیدا می‌کنند، تا دیگر حذف نشوند. اما در ادامه چون دو خاصیت IsDeleted و DeletedAt این موجودیت، ویرایش می‌شوند، وضعیت جدید Modified خواهد بود که به کوئری‌های Update تفسیر می‌شوند. به این ترتیب می‌توان همانند قبل یک رکورد را حذف کرد:
var post1 = context.Posts.Find(1);
if (post1 != null)
{
   context.Remove(post1);

   context.SaveChanges();
}
اما دستوری که توسط EF Core صادر می‌شود، یک Update است:
Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:11:32' (Nullable = true), @p1='True'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

محل اعمال متد SetAuditableEntityOnBeforeSaveChanges فوق، پیش از فراخوانی SaveChanges و به صورت زیر است:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            ChangeTracker.DetectChanges();

            beforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges(acceptAllChangesOnSuccess);

            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

        // ...

        private void beforeSaveTriggers()
        {
            setAuditProperties();
        }

        private void setAuditProperties()
        {
            this.SetAuditableEntityOnBeforeSaveChanges();
        }
    }
}


مشکل! رکوردهای وابسته حذف نمی‌شوند!

حالت پیش‌فرض حذف رکوردها در EFCore به cascade delete تنظیم شده‌است. یعنی اگر blog با id=1 حذف شود، نه فقط این blog، بلکه تمام مطالب وابسته‌ی به آن نیز حذف خواهند شد. اما در اینجا اگر این بلاگ را حذف کنیم:
 ar blog1 = context.Blogs.FirstOrDefault(blog => blog.Id == 1);
if (blog1 != null)
{
   context.Remove(blog1);

   context.SaveChanges();
}
تنها تک رکورد متناظر با آن حذف منطقی شده و مطالب متناظر با آن خیر. برای رفع این مشکل باید به صورت زیر عمل کرد:
var blog1AndItsRelatedPosts = context.Blogs
    .Include(blog => blog.Posts)
    .FirstOrDefault(blog => blog.Id == 1);
if (blog1AndItsRelatedPosts != null)
{
    context.Remove(blog1AndItsRelatedPosts);

    context.SaveChanges();
}
ابتدا باید رکوردهای وابسته را توسط یک Include به حافظه وارد کرد و سپس دستور Delete را بر روی کل آن صادر نمود که یک چنین خروجی را تولید می‌کند:
SELECT [t].[Id], [t].[DeletedAt], [t].[IsDeleted], [t].[Name], [t0].[Id], [t0].[BlogId], [t0].[DeletedAt], [t0].[IsDeleted], [t0].[Title]
FROM (
SELECT TOP(1) [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE ([b].[IsDeleted] <> CAST(1 AS bit)) AND ([b].[Id] = 1)
) AS [t]
LEFT JOIN (
SELECT [p].[Id], [p].[BlogId], [p].[DeletedAt], [p].[IsDeleted], [p].[Title]
FROM [Posts] AS [p]
WHERE [p].[IsDeleted] <> CAST(1 AS bit)
) AS [t0] ON [t].[Id] = [t0].[BlogId]
ORDER BY [t].[Id], [t0].[Id]

Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:25:00' (Nullable = true), @p1='True',
 @p5='2', @p3='2020-09-17T05:25:00' (Nullable = true), @p4='True', @p8='3',
@p6='2020-09-17T05:25:00' (Nullable = true), @p7='True',
 @p11='4', @p9='2020-09-17T05:25:00' (Nullable = true), @p10='True'], CommandType='Text', CommandTimeout='30']

SET NOCOUNT ON;
UPDATE [Blogs] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p3, [IsDeleted] = @p4
WHERE [Id] = @p5;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p6, [IsDeleted] = @p7
WHERE [Id] = @p8;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p9, [IsDeleted] = @p10
WHERE [Id] = @p11;
SELECT @@ROWCOUNT;
ابتدا اولین بلاگ را حذف منطقی کرده؛ سپس تمام مطالب متناظر با آن‌را که پیشتر حذف منطقی نشده‌اند، یکی یکی به صورت حذف شده، علامتگذاری می‌کند. به این ترتیب cascade delete منطقی نیز در اینجا میسر می‌شود.


یک نکته: مشکل حذف منطقی و رکوردهای منحصربفرد

فرض کنید در جدولی، فیلد نام کاربری را به عنوان یک فیلد منحصربفرد تعریف کرده‌اید و اکنون رکوردی در این بین، حذف منطقی شده‌است. مشکلی که در آینده بروز خواهد کرد، عدم امکان ثبت رکورد جدیدی با همان نام کاربری است که حذف منطقی شده‌است؛ چون یک unique index بر روی آن وجود دارد. در این حالت اگر از SQL Server استفاده می‌کنید، از قابلیتی به نام filtered indexes پشتیبانی می‌کند که در آن امکان تعریف یک شرط و predicate، در حین تعریف ایندکس‌ها وجود دارد. در این حالت می‌توان رکوردهای حذف منطقی شده را به ایندکس وارد نکرد.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreSoftDelete.zip
مطالب
طراحی و پیاده سازی مکانیزم مدیریت Transactionها در ServiceLayer

هدف ارائه راه حلی برای مدیریت Transactionها به عنوان یک Cross Cutting Concern، توسط ApplicationServiceها می‌باشد. 

پیش نیازها:

پیش فرض ما این است که شما از EF به عنوان OR-Mapper استفاده می‌کنید و الگوی Context Per Request را پیاده سازی کرده اید یا از طریق پیاده سازی الگوی Container Per Request به داشتن Context یکتا برای هر درخواست رسیده اید.
کتابخانه StructureMap.Mvc5 پیاده سازی از الگوی Container Per Request را با استفاده از امکانات Nested Container مربوط به StructureMap ارائه می‌دهد. اشیاء موجود در Nested Container طول عمر Singleton دارند.

ایده کار به این صورت هست که توسط یک Interceptor، به هنگام ورود به متد موجود در یک ApplicationService که با TransactionalAttribute تزئین شده است، یک تراکنش ایجاد شده و بعد از اتمام کار متد مورد نظر، در صورت عدم بروز استثناء، Commit و در غیر این صورت Rollback شود. حال اگر یک متد تراکنشی، داخل خود از متد تراکنشی دیگری استفاده کند، صرفا متد بیرونی تراکنش را ایجاد می‌کند و متد داخلی از همان تراکنش استفاده خواهد کرد و اصطلاحا  این تراکنش به صورت Ambient Transaction می باشد. 


واسط ITransaction

    public interface ITransaction : IDisposable
    {
        void Commit();
        void Rollback();
    }

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


واسط IUnitOfWork

    public interface IUnitOfWork : IDisposable
    {
        ...

        ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot);
        ITransaction Transaction { get; }
        IDbConnection Connection { get; }
        bool HasTransaction { get; }
    }

اعضای جدید واسط IUnitOfWork کاملا مشخص هستند.

پیاده سازی واسط ITransaction، توسط یک Nested Type در دل کلاس DbContextBase انجام می‌گیرد.

 public abstract class DbContextBase : DbContext
    {
        ...

        #region Fields
        
        private ITransaction _currenTransaction;

        #endregion

        #region NestedTypes

        private class DbContextTransactionAdapter : ITransaction
        {
            private DbContextTransaction _transaction;

            public DbContextTransactionAdapter(DbContextTransaction transaction)
            {
                Guard.NotNull(transaction, nameof(transaction));

                _transaction = transaction;
            }

            public void Commit()
            {
                _transaction?.Commit();
            }

            public void Rollback()
            {
                if (_transaction?.UnderlyingTransaction.Connection != null)
                    _transaction.Rollback();
            }

            public void Dispose()
            {
                _transaction?.Dispose();
                _transaction = null;
            }
        }

        #endregion

        #region Public Methods
        ...

        public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
        {
            if (_currenTransaction != null) return _currenTransaction;

            return _currenTransaction = new DbContextTransactionAdapter(Database.BeginTransaction(isolationLevel));
        }
        #endregion

        #region Properties
        ...

        public ITransaction Transaction => _currenTransaction;
        public IDbConnection Connection => Database.Connection;
        public bool HasTransaction => _currenTransaction != null;

        #endregion
}

public class ApplicationDbContext : DbContextBase, IUnitOfWork, ITransientDependency
{
     
}

کلاس DbContextTransactionAdapter همانطور که از نام آن مشخص می‌باشد، پیاده سازی از الگوی Adapter برای وفق دادن DbContextTransaction با واسط ITransaction، می‌باشد. متد BeginTransaction در صورتی که تراکنشی برای وهله جاری DbContext ایجاد نشده باشد، تراکنشی را ایجاد کرده و فیلد currentTransaction_ را نیز مقدار دهی می‌کند. 


TransactionalAttribute

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
    public sealed class TransactionalAttribute : Attribute
    {
        public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.ReadCommitted;
        public TimeSpan? Timeout { get; set; }
    }

TransactionInterceptor

    public class TransactionInterceptor : ISyncInterceptionBehavior
    {
        private readonly IUnitOfWork _unitOfWork;

        public TransactionInterceptor(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }
        public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation)
        {
            var transactionAttribute = GetTransactionaAttributeOrNull(methodInvocation.InstanceMethodInfo);

            if (transactionAttribute == null || _unitOfWork.HasTransaction) return methodInvocation.InvokeNext();

            using (var transaction = _unitOfWork.BeginTransaction(transactionAttribute.IsolationLevel))
            {
                var result = methodInvocation.InvokeNext();

                if (result.Successful)
                    transaction.Commit();
                else
                {
                    transaction.Rollback();
                }

                return result;
            }
        }

        private static TransactionalAttribute GetTransactionaAttributeOrNull(MemberInfo methodInfo)
        {
            var transactionalAttribute =
                ReflectionHelper.GetAttributesOfMemberAndDeclaringType<TransactionalAttribute>(
                    methodInfo
                ).FirstOrDefault();

            return transactionalAttribute;
        }
    }

واسط ISyncInterceptionBehavior، مربوط می‌شود به کتابخانه جانبی دیگری که برای AOP توسط تیم StructureMap به نام StructureMap.DynamicInterception ارائه شده‌است. در متد Intercept، ابتدا چک می‌شود که که آیا این متد با TransactionAttribute تزئین شده و طی درخواست جاری برای Context جاری تراکنشی ایجاد نشده باشد؛ سپس تراکنش جدیدی ایجاد شده و بدنه اصلی متد اجرا می‌شود و نهایتا در صورت موفقیت آمیز بودن عملیات، تراکنش مورد نظر Commit می‌شود.

در آخر لازم است این Interceptor در تنظیمات اولیه StructureMap به شکل زیر معرفی شود:

Policies.Interceptors(new DynamicProxyInterceptorPolicy(
                type => typeof(IApplicationService).IsAssignableFrom(type), typeof(AuthorizationInterceptor), typeof(TransactionInterceptor), typeof(ValidationInterceptor)));



نکته: فرض کنید در بدنه اکشن متد یک کنترلر ASP.NET MVC یا ASP.NET Core، دو متد تراکنشی فراخوانی شود؛ در این صورت شاید لازم باشد که این دو متد طی یک تراکنش واحد به جای تراکنش‌های مجزا، اجرا شوند؛ بنابراین نیاز است از الگوی Transaction Per Request استفاده شود. برای این کار می‌توان یک ActionFilterAttribute سفارشی ایجاد کرد که ایجاد کننده تراکنش باشد و متدهای داخلی که هر کدام جدا تراکنشی بودند، نیز از تراکنش ایجاد شده استفاده کنند.

مطالب
ایندکس منحصر به فرد با استفاده از Data Annotation در EF Code First
در حال حاضر امکان خاصی برای ایجاد ایندکس منحصر به فرد در EF First Code وجود ندارد, برای این کار راه‌های زیادی وجود دارد مانند پست قبلی آقای نصیری, در این آموزش از Data Annotation و یا همان Attribute  هایی که بالای Property‌های مدل‌ها قرار می‌دهیم, مانند کد زیر : 
public class User
    {
        public int Id { get; set; }

        [Unique]
        public string Email { get; set; }

        [Unique("MyUniqueIndex",UniqueIndexOrder.ASC)]
        public string Username { get; set; }

        [Unique(UniqueIndexOrder.DESC)]
        public string PersonalCode{ get; set; }

        public string Password { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

همانطور که در کد بالا می‌بینید با استفاده از Attribute Unique ایندکس منحصر به فرد آن در دیتابیس ساخته خواهد شد.
ابتدا یک کلاس برای Attribute Unique به صورت زیر ایحاد کنید : 
using System;

namespace SampleUniqueIndex
{
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public class UniqueAttribute : Attribute
    {
        public UniqueAttribute(UniqueIndexOrder order = UniqueIndexOrder.ASC) {
            Order = order;
        }
        public UniqueAttribute(string indexName,UniqueIndexOrder order = UniqueIndexOrder.ASC)
        {
            IndexName = indexName;
            Order = order;
        }
        public string IndexName { get; private set; }
        public UniqueIndexOrder Order { get; set; }
    }

    public enum UniqueIndexOrder
    {
        ASC,
        DESC
    }
}
در کد بالا یک Enum برای مرتب سازی ایندکس به دو صورت صعودی و نزولی قرار دارد, همانند کد ابتدای آموزش که مشاهده می‌کنید امکان تعریف این Attribute به سه صورت امکان دارد که به صورت زیر می‌باشد:
1. ایجاد Attribute بدون هیچ پارامتری که در این صورت نام ایندکس با استفاده از نام جدول و آن فیلد ساخته خواهد شد :  IX_Unique_TableName_FieldName و مرتب ساری آن به صورت صعودی می‌باشد.
2.نامی برای ایندکس انتخاب کنید تا با آن نام در دیتابیس ذخبره شود, در این حالت مرتب سازی آن هم به صورت صعودی می‌باشد.
3. در حالت سوم شما ضمن وارد کردن نام ایندکس مرتب سازی آن را نیز وارد می‌کنید.
بعد از کلاس Attribute حالا نوبت به کلاس اصلی میرسد که به صورت زیر می‌باشد:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
using System.Linq;
using System.Reflection;

namespace SampleUniqueIndex
{
    public static class DbContextExtention
    {
        private static BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;

        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
                foreach (var uniqueProp in GetUniqueProperties(context, entityType, table))
                {
                    var indexName = string.IsNullOrWhiteSpace(uniqueProp.IndexName) ?
                        "IX_Unique_" + uniqueProp.TableName + "_" + uniqueProp.FieldName :
                        uniqueProp.IndexName;

                    if (!currentIndexes.Contains(indexName))
                    {
                        query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] ADD CONSTRAINT [" + indexName + "] UNIQUE ([" + uniqueProp.FieldName + "] " + uniqueProp.Order + "); ";
                    }
                    else
                    {
                        currentIndexes.Remove(indexName);
                    }
                }
                foreach (var index in currentIndexes)
                {
                    query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] DROP CONSTRAINT " + index + "; ";
                }
            }

            if (query.Length > 0)
                context.Database.ExecuteSqlCommand(query);
        }

        private static List<string> GetCurrentUniqueIndexes(DbContext context, string tableName)
        {
            var sql = "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS where table_name = '"
                      + tableName + "' and CONSTRAINT_TYPE = 'UNIQUE'";
            var result = context.Database.SqlQuery<string>(sql).ToList();
            return result;
        }
        private static IEnumerable<PropertyDescriptor> GetDbSets(DbContext context)
        {
            foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(context))
            {
                var notMapped = prop.GetType().GetCustomAttributes(typeof(NotMappedAttribute),true);
                if (prop.PropertyType.Name == typeof(DbSet<>).Name && notMapped.Length == 0)
                    yield return prop;
            }
        }
        private static List<UniqueProperty> GetUniqueProperties(DbContext context, Type entity, TableInfo tableInfo)
        {
            var indexedProperties = new List<UniqueProperty>();
            var properties = entity.GetProperties(PublicInstance);
            var tableName = tableInfo.TableName;
            foreach (var prop in properties)
            {
                if (!prop.PropertyType.IsValueType && prop.PropertyType != typeof(string)) continue;

                UniqueAttribute[] uniqueAttributes = (UniqueAttribute[])prop.GetCustomAttributes(typeof(UniqueAttribute), true);
                NotMappedAttribute[] notMappedAttributes = (NotMappedAttribute[])prop.GetCustomAttributes(typeof(NotMappedAttribute), true);
                if (uniqueAttributes.Length > 0 && notMappedAttributes.Length == 0)
                {
                    var fieldName = GetFieldName(context, entity, prop.Name);
                    if (fieldName != null)
                    {
                        indexedProperties.Add(new UniqueProperty
                        {
                            TableName = tableName,
                            IndexName = uniqueAttributes[0].IndexName,
                            FieldName = fieldName,
                            Order = uniqueAttributes[0].Order.ToString()
                        });
                    }
                }
            }
            return indexedProperties;
        }
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }

        internal class UniqueProperty
        {
            public string TableName { get; set; }
            public string FieldName { get; set; }
            public string IndexName { get; set; }
            public string Order { get; set; }
        }
        internal class TableInfo
        {
            public string EntityName { get; set; }
            public string TableName { get; set; }
            public string TableSchema { get; set; }
        }
    }
}
در کد بالا با استفاده از Extension Method برای کلاس DbContext یک متد با نام ExecuteUniqueIndexes  ایجاد می‌کنیم تا برای ایجاد ایندکس‌ها در دیتابیس از آن استفاده کنیم.
روند اجرای کلاس بالا به صورت زیر می‌باشد:
در ابتدای متد ()ExecuteUniqueIndexes  :
 public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            ...
        }
با استفاده از متد ()GetTables ما تمام جداول ساخته توسط دیتایس توسط DbContext را گرفنه:
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
با استفاده از این طریق چنانچه کاربر نام دیگری برای هر جدول در نظر بگیرد مشکلی ایجاد نمی‌شود و همینطور Schema جدول نیز گرفته می‌شود, سه مشخصه نام مدل و نام جدول و Schema جدول در کلاس TableInfo قرار داده می‌شود و در انتها تمام جداول در یک Collection قرار داده میشوند و به عنوان خروجی متد استفاده می‌شوند.
بعد از آنکه نام جداول متناظر با نام مدل آنها را در اختیار داریم نوبت به گرفتن تمام DbSet‌ها در DbContext می‌باشد که با استفاده از متد ()GetDbSets :
public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
            ....
        }
در این متد چنانچه Property دارای Attribute NotMapped باشد در لیست خروجی متد قرار داده نمی‌شود. 
سپس داخل چرخه DbSet‌ها نوبت به گرفتن ایندکس‌های موجود با استفاده از متد ()GetCurrentUniqueIndexes برای این مدل می‌باشد تا از ایجاد دوباره آن جلوگیری شود و البته اگر ایندکس هایی را در مدل تعربف نکرده باشید از دیتابیس حذف شوند.
        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            ...
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
            }
        }
بعد از آن نوبت به گرفتن Property‌های دارای Attribute Unique می‌باشد که این کار نیز با استفاده از متد ()GetUniqueProperties انجام خواهد شد.
در متد ()GetUniqueProperties چند شرط بررسی خواهد شد از جمله اینکه Property از نوع Value Type باشد و نه یک کلاس سپس Attribute NotMapped را نداشته باشد و بعد از آن می‌بایست نام متناظر با آن Property را در دیتابیس به دست بیاریم برای این کار از متد ()GetFieldName استفاده می‌کنیم:
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }
برای این کار با استفاده از MetadataWorkspace در DbContext دو لیست SSpace و OSpace استفاده می‌کنیم که در ادامه در مورد این گونه لیست ها بیشتر توضیح می‌دهیم , سپس با استفاده از Member‌های این دو لیست و ایندکس‌های متناظر در این دو لیست نام متناظر با Property را در دیتابیس پیدا خواهیم کرد, البته یک نکته مهم هست چنانچه برای فیلد‌های دیتابیس OrderColumn قرار داده باشید دو لیست Member‌ها از نظر ایندکس متناظر متفاوت خواهند شد پس در نتیجه ایندکس به اشتباه برروی یک فیلد دیگر اعمال خواهد شد.
لیست‌ها در MetadataWorkspace:
1. CSpace : این لیست شامل آبجکت‌های Conceptual از مدل‌های شما می‌باشد تا برای Mapping دیتابیس با مدل‌های شما مانند مبدلی این بین عمل کند.
2. OSpace : این لیست شامل آبجکت‌های مدل‌های شما می‌باشد.
3. SSpace : این لیست نیز شامل آبجکت‌های مربوط به دیتابیس از مدل‌های شما می‌باشد
4. CSSpace : این لیست شامل تنظیمات Mapping بین دو لیست SSpace و CSpace می‌باشد.
5. OCSpace : این لیست شامل تنظیمات Mapping بین دو لیست OSpace و CSpace می‌باشد.
روند Mapping مدل‌های شما از OSpace شروع شده و به SSpace ختم میشود که سه لیست دیگز شامل تنظیماتی برای این کار می‌باشند.
و حالا در متد اصلی ()ExecuteUniqueIndexes ما کوئری مورد نیاز برای ساخت ایندکس‌ها را ساخته ایم.

حال برای استفاده از متد()ExecuteUniqueIndexes می‌بایست در متد Seed آن را صدا بزنیم تا کار ساخت ایندکس‌ها شروع شود، مانند کد زیر:
protected override void Seed(myDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
            context.ExecuteUniqueIndexes();
        }
چند نکته برای ایجاد ایندکس منحصر به فرد وجود دارد که در زیر به آنها اشاره می‌کنیم:
1. فیلد‌های متنی باید حداکثر تا 350 کاراکتر باشند تا ایندکس اعمال شود.
2. همانطور که بالاتر اشاره شد برای فیلد‌های دیتابیس OrderColumn اعمال نکنید که علت آن در بالا توضیح داده شد

دانلود فایل پروژه:
Sample_UniqueIndex.zip