نظرات مطالب
معرفی و استفاده از DDL Triggers در SQL Server
با سلام و احترام؛ همانطور که در متن به عرض رسانده شده:
" از عبارت  ON  برای مشخص کردن محدوده Trigger در سطح SQL Instance (در این صورت ON All SERVER نوشته می‌شود) و یا در سطح Database (در این حالت ON DATABASE نوشته می‌شود)  استفاده می‌شود و از عبارت FOR  برای مشخص کردن رویداد یا گروه رویدادی که سبب فراخوانی  Trigger می‌شود، استفاده  خواهد شد.  "
در خصوص مثالی که اشاره کردید، به نظرم می‌رسد از Trigger برای این منظور استفاده نمی‌شود (در حوزه‌ی بکاپ و ریستور)، شاید اگر قصدتان به منظور ثبت log و ... بایست از Auditing استفاده کنید. به این منظور در Auditing با توجه به جدول زیر می‌توان اقدام به ثبت موارد نمود:
Server-Level Audit Action Groups  
Action group name 
 Event Class   Description 
 BACKUP_RESTORE_GROUP   Audit Backup/Restore  
 یک دستور Backup یا Restore صادر شود  
به طور مختصر Auditing به شرح زیر است:
 بررسی SQL Server Audit
 بازبینی (Auditing) شامل پیگیری و ثبت رویدادهایی است که در سطح SQL Instance و یا Database‌های روی یک سیستم اتفاق می‌افتد. چندین سطح برای Auditing در SQL Server وجود دارد که به صلاحدید و نیازمندی‌های نصب شما وابسته است. شما می‌توانید گروه اقدامات بازبینی سرویس دهنده (server audit action groups) را به ازای هر SQL Instance و گروه اقدامات بازبینی بانک اطلاعاتی (database audit action groups) را  به ازای هر بانک اطلاعاتی ثبت کنید. رویداد Audit هر زمان عملی که مورد رسیدگی قرار گرفته اتفاق افتد، رخ می‌دهد.
تا پیش از SQL SERVER 2008، شما باید از خصیصه‌های متعددی برای انجام یک مجموعه کامل بازبینی (Auditing) برای نمونه DDL Trigger، DML Trigger و SQL Trace، بر روی یک SQL Instance استفاده می‌کردید.
SQL SERVER 2008، همه قابلیت‌های Auditing را روی یک audit specification ترکیب می‌کند. Audit Specification با تعریف یک شی بازبینی (audit object) در سطح سرویس دهنده برای ثبت (logging) یک دنباله بازبینی (audit trial) آغاز می‌شود. توجه شود که بایست یک شیء بازبینی ایجاد کنید پیش از اینکه یک Server Audit Specification و یا Database Audit Specification ایجاد کنید.
Server Audit Specification، گروه اقدامات در سطح سرویس دهنده را جمع آوری می‌کند که با رویدادهای وسیعی فعال می‌شوند، این گروه اقدامات تحت عنوان  Server-Level Audit Action Groups تشریح شده اند. شما می‌توانید یک Server Audit Specification را به ازای هر Audit ایجاد کنید چرا که هر دو در محدوده یک SQL Instance ایجاد می‌شوند.
Database Audit Specification، گروه اقدامات در سطح بانک اطلاعاتی را جمع آوری می‌کند که با رویدادهای وسیعی فعال می‌شود. این گروه اقدامات تحت عنوان‌های Database-Level Audit Action Groups و Database-Level Audit Actions تشریح شده اند.می توانید یک Database Audit Specification را به ازای هر Audit در بانک اطلاعاتی SQL Server ایجاد کنید.
همچنین می‌توانید هر گروه اقدامات بازبینی(audit action groups) یا رویدادهای بازبینی(audit events) را به یک Database Audit Specification اضافه کنید. گروه اقدامات بازبینی، گروه اقدامات از پیش تعریف شده ای هستند و رویدادهای بازبینی اقدامات تجزیه ناپذیری هستند که توسط موتور بانک اطلاعاتی مورد رسیدگی قرار می‌گیرند، هر دو در محدوده بانک اطلاعاتی (Database) هستند. این اقدامات برای Audit فرستاده می‌شوند تا در Target (که می‌تواند یک فایل، Windows Security Log و یا Windows Application Log باشد) ذخیره شوند. برای نوشتن در Windows Security Log لازم است که Service Account سرویس دهنده شما به Policy، Generate security audits اضافه شده باشد، به صورت پیش فرض Local System، Local Service و Network Service بخشی از این Policy می‌باشند. پس از اینکه Audit را ایجاد و فعال کردید، Target ورودی‌ها را دریافت خواهد کرد. 
Server-Level Audit Action Groups (گروه اقدامات بازبینی در سطح سرویس دهنده)
این گروه اقدامات به گروه رویداد Security Audit شبیه هستند. به طور خلاصه این گروه اقدامات، اقداماتی را که در یک SQL Instance شامل می‌شوند، در بر می‌گیرد. برای مثال اگر گروه اقدام مناسب با Server Audit Specification اضافه شده باشد هر شیء در هر Schema که مورد دستیابی قرار می‌گیرد، ثبت می‌شود. اقدامات در سطح سرویس دهنده به شما اجازه نمی‌دهد که جزئیات اقدامات در سطح بانک اطلاعاتی را فیلتر کنید. یک بازبینی در سطح بانک اطلاعاتی برای انجام به جزئیات دقیق فیلتر کردن نیاز دارد، برای مثال اجرای دستور Select روی جدول Customers برای login هایی که در گروه Employee هستند.
Database-Level Audit Action Groups (گروه اقدامات بازبینی در سطح بانک اطلاعاتی)
این گروه اعمال به کلاس‌های رویداد Security Audit  شبیه هستند.
Database-Level Audit Actions
اقدامات در سطح بانک اطلاعاتی، اقدامات بازبینی خاصی را به طور مستقیم روی Database، Schema و اشیاء Schema (از قبیل جداول، View ها، رویه‌های ذخیره شده، توابع و ... ) فراهم می‌کند. این اقدامات برای فیلدها (Columns) صدق نمی‌کنند.
Audit-Level Audit Action Groups
شما می‌توانید اقداماتی را که در فرآیند Auditing هستند، بازبینی کنید که می‌تواند در محدوده سرویس دهنده یا بانک اطلاعاتی باشد. در محدوده بانک اطلاعاتی تنها برای database audit specification رخ می‌دهد.
  جهت بررسی بیشتر به این لینک مراجعه شود.
مطالب
مشکل همزمانی خواندن و به روز رسانی اطلاعات در برنامه‌های وب
فرض کنید در برنامه‌ی خود «کیف پولی» را طراحی کرده‌اید که بر اساس آن، کاربر می‌تواند خرید کند. این کیف پول، از Id کاربر و موجودی فعلی او تشکیل می‌شود:
CREATE TABLE accounts (
user_id INTEGER PRIMARY KEY,
balance INTEGER NOT NULL
);
و برای مثال موجودی فعلی کاربر 1، مقدار 300 است:
INSERT INTO accounts(user_id, balance)
VALUES (1, 300);
اکنون کوئری‌های متداول زیر را که از یک read و سپس update تشکیل شده‌اند، درنظر بگیرید:
DECLARE @amount INT;

SET @amount = (
SELECT balance
FROM accounts
WHERE user_id = 1
);

SELECT @amount as 'balance'

UPDATE accounts
SET balance =  @amount - 100
WHERE user_id = 1;

SELECT balance as 'balance after shopping'
FROM accounts
WHERE user_id = 1
- دو عمل read و سپس update صورت گرفته‌ی فوق، مربوط به یک درخواست خرید است.
- در اینجا مقدار متغیر amount در ابتدای کار، مساوی 300 است که مربوط به همان insert ابتدایی است.
- سپس از این مقدار در کوئری دومی (برای مثال حاصل از خرید شماره یک)، 100 واحد کم می‌شود (برای مثال قیمت کل خرید است).
- در این حالت نتیجه‌ی آن یا همان موجودی جدید کاربر، 200 خواهد بود.

معادل این عملیات در EF-Core چنین دستورات متداولی است:
var account1 =  context.Accounts.First(x => x.UserId == 1);
account1.Balance -= 100;
context.SaveChanges();

سؤال: اگر کوئری‌های فوق را در یک برنامه‌ی ذاتا چند ریسمانی وب، دوبار به صورت همزمان اجرا کنیم، یعنی دو عمل خرید موازی را شبیه سازی کنیم، چه اتفاقی رخ می‌دهد؟ آیا موجودی نهایی اینبار برای مثال 100 می‌شود (با فرض 300 بودن موجودی ابتدایی)؟
پاسخ خیر است! و آن‌را می‌توانید در تصویر زیر مشاهده کنید:



در اینجا برای شبیه سازی اجرای موازی دو کوئری، از دستور WAITFOR TIME استفاده شده‌است که برای برای آزمایش آن می‌توانید مقدار آن‌را به یک دقیقه بعد تنظیم کرده و سپس آن‌را در دو پنجره‌ی SQL server management studio اجرا کنید.
همانطور که مشاهده می‌کنید، با اجرای موازی این دو کوئری، یعنی دوبار خرید کردن همزمان، 100 واحد گم شده‌است ! به این مشکل همزمانی read و سپس update رخ داده، یک «race condition» گفته می‌شود و این روزها که مطالب منتشر شده‌ی از آسیب پذیری‌های برنامه‌های وب ایرانی را بررسی می‌کنم، این مورد در صدر آن‌ها قرار دارد!
علت اینجا است که عموما برنامه نویس‌ها، برنامه‌های وب را در یک تک سشن باز شده‌ی توسط مرورگر خود آزمایش می‌کنند و در این حالت، همه چیز خوب است و اعمال آن به ترتیب پیش می‌روند. اما فراموش می‌کنند که می‌توان قسمت‌های مختلف برنامه‌های وب را به صورت همزمان، موازی و چندباره نیز اجرا کرد؛ حتی اگر آن قسمت متعلق به یک کاربر باشد.


سؤال: آیا استفاده تراکنش‌ها این مشکل را حل نمی‌کنند؟!

عموما برنامه نویس‌ها تصور می‌کنند که می‌توانند تمام اینگونه مشکلات را با تراکنش‌ها حل کنند:



همانطور که مشاهده می‌کنید، اینبار هرچند هر دو عملیات خرید داخل BEGIN TRAN و COMMIT TRAN قرار گرفته‌اند، اما ... مشکل همزمانی هنوز پابرجا است! چون نوع پیش‌فرض تراکنش مورد استفاده، READ COMMITTED isolation level است و عدم دقت به آن ممکن است این تصور را ایجاد کند که با تعریف تراکنش‌ها، تمام مشکلات همزمانی برطرف می‌شوند.


راه‌حل‌های پیشنهادی جهت حل مشکل همزمانی عملیات read/update

برای حل مشکلات مرتبط با race condition و همزمانی درخواست‌های read/update، می‌توان از یکی از روش‌های زیر استفاده کرد:
الف) بجای اینکه یکبار کوئری read و یکبار کوئری update به صورت جداگانه صادر شوند، فقط یکبار کوئری update داشته باشیم.
ب) پیاده سازی Row level locking؛ در صورت پشتیبانی بانک اطلاعاتی مورد استفاده از آن
ج) استفاده از تراکنش‌هایی از نوع SERIALIZABLE
د) پیاده سازی optimistic locking

این موارد را در ادامه با توضیحات بیشتری بررسی می‌کنیم.


الف) پرهیز از خواندن و به روز رسانی جداگانه

بجای اینکه مانند اعمال فوق، یکبار select داشته باشیم و یکبار  update، بهتر است فقط یک دستور update بکارگرفته شود:
UPDATE accounts
SET balance =  balance - 100
WHERE user_id = 1;


اینبار با خلاصه شدن دو دستور select و update به یک دستور update، دیگر پس از دو خرید همزمان، 100 واحد گم شده مشاهده نمی‌شود (!) و موجودی نهایی صحیح است.


ب) پیاده سازی Row level locking

همیشه امکان تغییر عملیات مورد نیاز، به سادگی حالت الف نیست. در یک چنین حالت‌هایی جهت حداقل شدن تغییرات مورد نیاز، می‌توان از row level locking استفاده کرد:
WAITFOR TIME '13:47:00';

SET NOCOUNT, XACT_ABORT ON;

BEGIN TRAN;

DECLARE @amount INT;

SET @amount = (
 SELECT balance
 FROM accounts WITH (UPDLOCK, HOLDLOCK)
 WHERE user_id = 1
 );

SELECT @amount as 'initial user''s balance'

UPDATE accounts
SET balance =  @amount - 100
WHERE user_id = 1;

SELECT balance as 'user''s balance after shopping 1'
FROM accounts
WHERE user_id = 1;

COMMIT TRAN;



در اینجا اضافه شدن WITH (UPDLOCK, HOLDLOCK) را به Select تعریف شده، مشاهده می‌کنید که به آن‌ها locking hints هم گفته می‌شود و داخل BEGIN TRAN و COMMIT TRAN عمل می‌کنند (که نوع پیش‌فرض آن READ COMMITTED isolation level است). کار UPDLOCK، تبدیل shared lock پیش‌فرض، به update lock است و کار HOLDLOCK، نگه داشتن قفل صورت گرفته تا پایان کار تراکنش تعریف شده‌است.
با این تغییرات، هر تراکنش همزمان دیگری، تا زمانیکه قفل صورت گرفته‌ی بر روی ردیف select، رها نشود (یعنی تا زمانیکه تراکنش قفل کننده، به COMMIT TRAN برسد)، نمی‌تواند آن‌را تغییر دهد. به همین جهت است که در تصویر فوق، هرچند هر دو عملیات همزمان اجرا شده‌اند، اما یکی موجودی ابتدایی 300 را می‌بیند و دیگری پس از صبر کردن تا پایان تراکنش و رها شدن قفل، موجودی تغییر یافته‌ی جدیدی را مشاهده کرده و از آن استفاده می‌کند. به این ترتیب دیگر 100 واحدی که در اولین تصویر این مطلب مشاهده کردید، گم نشده‌است.


ج) استفاده از تراکنش‌هایی از نوع SERIALIZABLE

بجای استفاده از روش row level locking یاد شده، روش دیگری را که می‌توان استفاده کرد، تغییر نوع پیش‌فرض تراکنش مورد استفاده‌است. برای مثال اگر از یک SERIALIZABLE transaction استفاده کنیم؛ یعنی SET TRANSACTION ISOLATION LEVEL SERIALIZABLE  را در ابتدای کار ذکر کنیم و برای مثال دو تراکنش همزمان را اجرا کنیم، اگر در تراکنش اول اطلاعاتی خوانده شود، در هیچ تراکنش دیگری نمی‌توان این اطلاعات خوانده شده را تا پایان کار تراکنش اول، تغییر داد:




د) پیاده سازی optimistic locking

پیاده سازی optimistic locking و یا Optimistic concurrency control عموما در سمت برنامه رخ می‌دهد و توسط ORMها زیاد مورد استفاده قرار می‌گیرد؛ مانند اضافه کردن ستون اضافی version و یا timestamp به جداول تعریف شده. در این حالت تمام updateها به همراه یک where اضافی هستند تا بررسی کنند که آیا version دریافتی در حین خواندن ردیف در حال به روز رسانی، تغییر کرده‌است یا خیر؟ اگر تغییر کرده‌است، تراکنش را با خطایی خاتمه خواهند داد. این روش برخلاف حالت‌های ب و ج، حتی خارج از یک تراکنش نیز کار می‌کند و مشکلات قفل کردن طولانی مدت رکوردها توسط آن‌ها را به همراه ندارد.
مطالب
نحوه کار Expression و ایجاد یک DynamicFilter
ساختار Expression‌ها شبیه به ساختار یک درخت است. به عنوان مثال زمانیکه شما یک فیلتر ساده را مانند دستور زیر اجرا میکنید:
Expression<Func<string, bool>> f = s => s.Length < 5;
Expression ایجاد شده از فیلتر شما به صورت زیر میباشد:

منبع : کتاب C# 8 in a Nutshell 

ParameterCollection به پارامترهای استفاده شده در فیلتر اشاره دارد که در فیلتر بالا فقط s استفاده شده‌است و از نوع string است.

BinaryExpression شامل سه قسمت مهم Left , Right و NodeType میباشد. برای فیلتر بالا، مقدار پراپرتی Left برابر s.Length می‌باشد و پراپرتی Right شامل مقدار 5 و مقدار NodeType هم برابر LessThan میباشد. یعنی فیلتر بالا به یک درخت تبدیل شده که نود اصلی آن LessThan است و دو مقدار Left و Right را باهم مقایسه میکند. اما اگر یک شرط دیگر را به فیلتر بالا اعمال کنیم، ساختار Expression کمی تغییر میکند. برای مثال: 

Expression<Func<string, bool>> filter = s => s.Length > 5 && s.Length < 45;

Expression ایجاد شده برای این فیلتر شامل همان ساختار قبلی است؛ اما با این تغییر که هر کدام از پراپرتی‌های Right و Left، خود یک BinaryExpression شده‌اند و مقدار NodeType اصلی از LessThan به AndAlso تغییر پیدا کرده‌است. Expression ایجاد شده از فیلتر بالا ( filter.Body ) به این صورت است که پراپرتی Left آن برابر است با یک BinaryExpression که مقدار NodeType آن برابر است با GreaterThan و پراپرتی Left آن شامل s.Length میباشد و پراپرتی Right آن برابر 5 میباشد. همچنین پراپرتی Right مربوط به filter.Body برابر یک ExpressionBinary است که مقدار NodeType آن برابر است با LessThan و پراپرتی Left آن برابر s.Length است و پراپرتی Right آن برابر 45 میباشد.

filter.Body شبیه به تصویر زیر میباشد :

اگر بخواهیم خودمان یک Expression tree را ایجاد کنیم، باید از پایین‌ترین نود آن شروع کنیم. یعنی ابتدا باید پراپرتی Left و Right را ایجاد کنیم و سپس این دو پراپرتی را با هم مقایسه کنیم (NodeType). در کد زیر Expression مربوط به فیلتر بالا را نوشته‌ایم:

ParameterExpression parameterExpression = Expression.Parameter(typeof(string));
MemberExpression memberExpression = Expression.Property(parameterExpression, "Length");

ConstantExpression greaterThanConstantExpression = Expression.Constant(5);
BinaryExpression greaterThanComparison = Expression.GreaterThan(memberExpression, greaterThanConstantExpression);
var greaterThan = Expression.Lambda<Func<string, bool>>(greaterThanComparison, parameterExpression);

ConstantExpression lessThanConstantExpression = Expression.Constant(45);
BinaryExpression lessThanComparsion = Expression.LessThan(memberExpression, lessThanConstantExpression);
var lessThan = Expression.Lambda<Func<string, bool>>(lessThanComparsion, parameterExpression);

var param = Expression.Parameter(typeof(string), "x");
var body = Expression.AndAlso(
            Expression.Invoke(greaterThan, param),
            Expression.Invoke(lessThan, param)
        );
Expression<Func<string, bool>> filter = Expression.Lambda<Func<string, bool>>(body, param);

ParameterExpression : نوع پارامتری را که میخواهیم روی آن شرط را روی آن اعمال کنیم، مشخص کرده‌ایم.

MemberExpression  : پراپرتی Length را معرفی کرده‌ایم که قرار است شرطی بر روی این پراپرتی اعمال شود.

ConstantExpression   : مقدار ثابتی که پراپرتی MemeberExpression قرار است با آن مقایسه شود.

BinaryExpression   : نود تایپ را مشخص کرده‌ایم که برابر است با GreaterThan.

سپس Expression مربوط به هرکدام را در greaterThan و lessThan ایجاد کرده‌ایم و این دو را باهم And کرده و در متغییر body قرار داده‌ایم و در نهایت filter را با دستور Expression.Lambda ایجاد کرده‌ایم که برابر است با :

Expression<Func<string, bool>> filter = s => s.Length > 5 && s.Length < 45;


ساخت یک داینامیک فیلتر

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

public class DynamicModel
{
    public string Name { get; set; }
    public string Comparison { get; set; }
    public object Data { get; set; }
}

پراپرتی Data مقداری است که باید با آن مقایسه انجام شود.

Comparison نوع عملیات را مشخص میکند مانند : Equal, LessThan, GreaterThan و... .

پراپرتی Name نام پراپرتی است که باید شرط روی آن اعمال شود.

کلاس ثابت ها:

public static class ComparisonConstant
{
    public const string LessThan = "LesThan";
    public const string LessThanEqual = "LesThanEqual";
    public const string GreaterThan = "GreaterThan";
    public const string GreaterThanEqual = "GreaterThanEqual";
    public const string Equal = "Equal";
    public const string NotEqual = "NotEqual";
}

ساخت اکستنشن متد:

public static IQueryable<TModel> DynamicFilter<TModel>(this IQueryable<TModel> iqueryable, IEnumerable<DynamicModel> dynamicModel)
{
    return iqueryable.Where(Filter<TModel>(dynamicModel));
}  
public static Expression<Func<TModel, bool>> Filter<TModel>(IEnumerable<DynamicModel> dynamicModel)
{
    Expression<Func<TModel, bool>> result = a => true;
    foreach (var item in dynamicModel)
    {
        ParameterExpression parameterExpression = Expression.Parameter(typeof(TModel));
        MemberExpression memberExpression = Expression.Property(parameterExpression, item.Name);
        ConstantExpression constantExpression = Expression.Constant(item.Data);
        BinaryExpression comparison = GetBinaryExpression(item.Comparison, memberExpression, constantExpression);
        var expression = Expression.Lambda<Func<TModel, bool>>(comparison, parameterExpression);
        var param = Expression.Parameter(typeof(TModel), "x");
        var body = Expression.AndAlso(
                    Expression.Invoke(result, param),
                    Expression.Invoke(expression, param)
                );
        result = Expression.Lambda<Func<TModel, bool>>(body, param);
    }
    return result;
}

ورودی این مدل، لیستی از DynamicModel میباشد که به ازای هر کدام از آیتم‌ها، یک BinaryExpression ایجاد میکند و آن را با result تعریف شده And میکند. یعنی تمامی آیتم‌های ارسال شده باهم And میشوند.

متد GetBinaryExpression بر اساس مقدار فیلد Comparison که از سمت فرانت ارسال میشود، کار میکند:

private static BinaryExpression GetBinaryExpression(string comparison, MemberExpression memberExpression, ConstantExpression constantExpression)
{
    switch (comparison)
    {
        case ComparisonConstant.Equal:
            return Expression.Equal(memberExpression, constantExpression);
        case ComparisonConstant.LessThan:
            return Expression.LessThan(memberExpression, constantExpression);
        case ComparisonConstant.GreaterThan:
            return Expression.GreaterThan(memberExpression, constantExpression);
        case ComparisonConstant.NotEqual:
            return Expression.NotEqual(memberExpression, constantExpression);
        case ComparisonConstant.GreaterThanEqual:
            return Expression.GreaterThanOrEqual(memberExpression, constantExpression);
        case ComparisonConstant.LessThanEqual:
            return Expression.LessThanOrEqual(memberExpression, constantExpression);
        default:
            return null;
    }
}

 کلاس Category را در نظر بگیرید که شامل دو پراپرتی Title و Id میباشد و میخواهیم از این داینامیک فیلتر، برای فیلتر کردن دیتاها استفاده کنیم از سمت فرانت‌اند. اگر از سمت فرانت‌اند چنین دیتایی ارسال شود:

[
   {
      "Name":"Title",
      "Comparison":"Equal",
      "Data":"Hi"
   },
   {
      "Name":"Id",
      "Comparison":"LesThanEqual",
      "Data": 100
   }
]

تمامی رکوردهایی که مقدار پراپرتی Title آنها برابر Hi باشد و Id آن کوچکتر مساوی 100 باشد، از دیتابیس خوانده میشود.

var categories = _dbContext.Categories
                         .DynamicFilter(filter)//filter => IEnumerable<DynamicModel>
                         .ToList();

گیت هاب داینامیک فیلتر 

مطالب
کوئری نویسی در EF Core - قسمت هفتم - کار با رشته‌‌ها
هدف از این سری مثال‌ها، آشنایی با متدها و توابعی است که در حین کار با خواص رشته‌ای در LINQ to Entities، مجاز به استفاده‌ی از آن‌ها هستیم و همچنین اگر تابعی در EF-Core هنوز تعریف نشده بود، راه حل چیست.


مثال 1: نام تمام کاربران را با قالب 'Surname, Firstname'  نمایش دهید.

var members = context.Members
                                    .Select(member => new { Name = member.Surname + ", " + member.FirstName })
                                    .ToList();
متد Select می‌تواند به همراه اعمال محاسباتی ساده‌ای نیز باشد که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.
با این خروجی:



مثال 2: تمام امکاناتی را که با Tennis شروع می‌شوند، لیست کنید.
این گزارش به همراه تمام ستون‌های جدول است.

var facilities = context.Facilities
                                        .Where(facility => facility.Name.StartsWith("Tennis"))
                                        .ToList();
متدهای استانداردی مانند StartsWith، EndsWith و Contains را می‌توان بر روی خواص رشته‌ای بکار برد.
با این خروجی:



مثال 3: تمام امکاناتی را که با tennis شروع می‌شوند، لیست کنید. این جستجو باید غیرحساس به بزرگی و کوچکی حروف باشد.
این گزارش به همراه تمام ستون‌های جدول است.

نیازی به انجام مجزای این تمرین نیست؛ چون پاسخ آن همان پاسخ مثال 2 است. Collation پیش‌فرض در SQL Server، غیرحساس به بزرگی و کوچکی حروف است. بنابراین چه tennis را جستجو کنیم و یا TeNnis را، تفاوتی نمی‌کند.


مثال 4: شماره تلفن‌های دارای پرانتز را لیست کنید.
این گزارش باید به همراه ستون‌های memid, telephone باشد.

روش اول: در اینجا دوبار از متد Contains استفاده شده‌است:
var members = context.Members
                                    .Select(member => new { member.MemId, member.Telephone })
                                    .Where(member => member.Telephone.Contains("(")
                                                    && member.Telephone.Contains(")"))
                                    .ToList();
با این خروجی:


روش دوم: اگر می‌خواهیم کنترل بیشتری را بر روی خروجی نهایی LIKE تولیدی داشته باشیم، می‌توان از متد سفارشی استاندارد EF.Functions.Like استفاده کرد که از حروف wild cards نیز پشتیبانی می‌کند:
members = context.Members
                                    .Select(member => new { member.MemId, member.Telephone })
                                    .Where(member => EF.Functions.Like(member.Telephone, "%[()]%"))
                                    .ToList();
با این خروجی:



مثال 5: کد پستی‌ها 5 رقمی هستند. گزارشی را تهیه کنید که در آن اگر کدپستی کمتر از 5 رقم بود، ابتدای آن با صفر شروع شود.
هدف اصلی از این مثال، اعمال متد PadLeft(5, '0') به خاصیت member.ZipCode است.

روش اول: EF-Core فعلا قابلیت ترجمه‌ی PadLeft(5, '0') را به معادل SQL آن‌را ندارد. به همین جهت مجبور هستیم ابتدا ZipCode‌ها را به صورت رشته‌ای بازگشت دهیم که در اینجا استفاده‌ی از Convert.ToString مجاز است.
با این خروجی:
SELECT   CONVERT (NVARCHAR (MAX), [m].[ZipCode]) AS [Zip]
FROM     [Members] AS [m]
ORDER BY CONVERT (NVARCHAR (MAX), [m].[ZipCode]);
 سپس می‌توان بر روی لیست آماده‌ی موجود در حافظه، از LINQ to Objects استفاده کرد و در این حالت دسترسی کاملی به تمام امکانات زبان #C وجود دارد:
var members = context.Members
                                    .Select(member => new { ZipCode = Convert.ToString(member.ZipCode) })
                                    .OrderBy(m => m.ZipCode)
                                    .ToList();
// Now using LINQ to Objects
members = members.Select(member => new { ZipCode = member.ZipCode.PadLeft(5, '0') })
                                                    .OrderBy(m => m.ZipCode)
                                                    .ToList();

روش دوم: SQL Server به همراه تابع استانداردی به نام Replicate است که از آن می‌توان برای شبیه سازی PadLeft، بدون متوسل شدن به LINQ to Objects، استفاده کرد. اما چون این تابع هنوز به EF-Core معرفی نشده‌است، نیاز است خودمان اینکار را انجام دهیم. در این روش، از متد SqlDbFunctionsExtensions.SqlReplicate استفاده می‌شود. روش تعریف این نوع متدها را در مطلب «امکان تعریف توابع خاص بانک‌های اطلاعاتی در EF Core» پیشتر بررسی کرده‌ایم که برای مثال در اینجا چنین شکلی را پیدا می‌کند:
namespace EFCorePgExercises.Utils
{
    public static class SqlDbFunctionsExtensions
    {
        public static string SqlReplicate(string expression, int count)
            => throw new InvalidOperationException($"{nameof(SqlReplicate)} method cannot be called from the client side.");

        private static readonly MethodInfo _sqlReplicateMethodInfo = typeof(SqlDbFunctionsExtensions)
            .GetRuntimeMethod(
                nameof(SqlDbFunctionsExtensions.SqlReplicate),
                new[] { typeof(string), typeof(int) }
            );


        public static void AddCustomSqlFunctions(this ModelBuilder modelBuilder)
        {
            modelBuilder.HasDbFunction(_sqlReplicateMethodInfo)
                .HasTranslation(args =>
                {
                    return SqlFunctionExpression.Create("REPLICATE",
                        args,
                        _sqlReplicateMethodInfo.ReturnType,
                        typeMapping: null);
                });
        }
    }
}
پس از آن فقط کافی است متد AddCustomSqlFunctions را به Context برنامه معرفی کنیم:
namespace EFCorePgExercises.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
         // ...

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
         // ...
            modelBuilder.AddCustomSqlFunctions();
         // ...
        }
    }
}
اکنون می‌توان از تابع SqlDbFunctionsExtensions.SqlReplicate جهت شبیه سازی PadLeft به صورت زیر استفاده کرد:
var newMembers = context.Members
                                        .Select(member => new
                                        {
                                            ZipCode =
                                                SqlDbFunctionsExtensions.SqlReplicate(
                                                    "0", 5 - Convert.ToString(member.ZipCode).Length)
                                                + member.ZipCode
                                        })
                        .OrderBy(m => m.ZipCode)
                        .ToList();
با این خروجی:



مثال 6: اولین حرف نام خانوادگی کاربران در کل ردیف‌های جدول چندبار تکرار شده‌است؟
این گزارش باید به همراه ستون‌های letter,  count باشد.

var members = context.Members
                                    .Select(member => new { Letter = member.Surname.Substring(0, 1) })
                                    .GroupBy(m => m.Letter)
                                    .Select(g => new
                                    {
                                        Letter = g.Key,
                                        Count = g.Count()
                                    })
                                    .OrderBy(r => r.Letter)
                                    .ToList();
هدف از این مثال بیان مجاز بودن استفاده‌ی از متد Substring بر روی خواص رشته‌ای است که EF-Core امکان ترجمه‌ی آن‌ها را به کدهای SQL دارد.
با این خروجی:



مثال 7: حروف '-','(',')', ' ' را از شماره تلفن‌ها حذف کنید.
این گزارش باید به همراه ستون‌های memid, telephone باشد.

بانک اطلاعاتی PostgreSQL به همراه تابع استاندارد regexp_replace است و می‌توان از آن برای حل یک چنین مسایلی استفاده کرد:
select memid, regexp_replace(telephone, '[^0-9]', '', 'g') as telephone
from members
order by memid;
اما SQL Server هنوز هم به همراه یک چنین تابعی نیست. بنابراین از روش زیر نیز می‌توان مثال جاری را حل کرد:
var members = context.Members
                                .Select(member => new
                                {
                                    member.MemId,
                                    Telephone = member.Telephone.Replace("-", "")
                                                        .Replace("(", "")
                                                        .Replace(")", "")
                                                        .Replace(" ", "")
                                })
                                .OrderBy(r => r.MemId)
                                .ToList();
با این خروجی:



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
مطالب
کوئری نویسی در EF Core - قسمت هشتم - کوئری‌های بازگشتی
جدول اعضای این مجموعه، خود ارجاع دهنده طراحی شده‌است:
namespace EFCorePgExercises.Entities
{
    public class Member
    {
       // ...

        public virtual ICollection<Member> Children { get; set; }
        public virtual Member Recommender { set; get; }
        public int? RecommendedBy { set; get; }

       // ...
    }
}
در اینجا RecommendedBy، یک کلید خارجی نال پذیر است که به Id همین جدول اشاره می‌کند. دو خاصیت دیگر تعریف شده، مکمل این خاصیت عددی، جهت سهولت کوئری نویسی‌های EF-Core هستند که در قسمت‌های قبل نیز تعدادی کوئری را در این زمینه مشاهده کردید؛ مانند:
- تولید لیست کاربرانی که کاربر دیگری را توصیه کرده‌اند.
- تولید لیست کاربران، به همراه توصیه کننده‌ی آن‌ها.
- تولید لیست کاربران به همراه توصیه کننده‌ی آن‌ها، بدون استفاده از جوین.
- هر کاربر چه تعداد کاربر دیگری را توصیه کرده‌است؟

در این قسمت تعدادی مثال بازگشتی را می‌خواهیم بررسی کنیم.


مثال 1: رنجیره‌ی توصیه کنندگان کاربر با ID مساوی 27 را محاسبه کنید.

می‌خواهیم بدانیم چه کسی کاربر 27 را توصیه کرده و همچنین این کاربر نیز توسط چه شخص دیگری توصیه شده و به همین ترتیب تا بالاترین سطح ممکن.

روش معمول انجام این نوع کوئری‌ها استفاده از «WITH Hierachy» است. اما اگر بخواهیم بدون SQL نویسی مستقیم اینکار را انجام دهیم، می‌توان به صورت زیر عمل کرد:
var id = 27;
var entity27WithAllOfItsParents =
                        context.Members
                            .Where(member => member.MemId == id
                                            || member.Children.Any(m => member.MemId == m.RecommendedBy))
                            .ToList() //It's a MUST - get all children from the database
                            .FirstOrDefault(x => x.MemId == id);// then get the root of the tree


این کوئری ابتدا تمام رکوردهای جدول کاربران را لیست می‌کند. سپس خاصیت Recommender هر کدام را تا n سطح مقدار دهی می‌کند (خود EF-Core اینکار را انجام می‌دهد). تمام این اتفاقات تا قسمت ToList آن رخ می‌دهند. پس از آن یک FirstOrDefault سمت کاربر را هم داریم (LINQ to Objects). هدف از آن، بازگشت تنها ریشه‌ی مرتبط با ID=27 است و تمام Recommenderهای متصل به آن. این موارد را در تصویر ذیل بهتر می‌توانید مشاهده کنید:


لیست تمام کاربران وجود دارند. سپس سیزدهمین مورد آن، همان کاربر 27 است که توسط کاربر 20 توصیه شده. کاربر 20 توسط کاربر 5 توصیه شده و کاربر 5 توسط کاربر 1 و پس از آن خاصیت Recommender نال است که به معنای پایان پیمودن این زنجیره‌است.
بنابراین مرحله‌ی بعدی پس از یافتن ریشه‌ی کاربر 27، پیمودن خاصیت‌های Recommender به صورت بازگشتی است؛ کاری شبیه به متد FindParents زیر:
namespace EFCorePgExercises.Exercises.RecursiveQueries
{
    public static class RecursiveUtils
    {
        public static void FindParents(Member member, List<dynamic> actualResult)
        {
            if (member == null || member.Recommender == null)
            {
                return;
            }

            var item = member.Recommender;
            actualResult.Add(new { Recommender = item.MemId, item.FirstName, item.Surname });

            if (item.Recommender != null)
            {
                FindParents(item, actualResult);
            }
        }
    }
}
که به صورت زیر می‌تواند مورد استفاده قرار گیرد:
var actualResult = new List<dynamic>();
RecursiveUtils.FindParents(entity27WithAllOfItsParents, actualResult);


مثال 2: زنجیره‌ی توصیه شده‌های توسط کاربر با ID مساوی 1 را محاسبه کنید.

می‌خواهیم بدانیم کاربر 1، چه کسی را توصیه کرده و این کاربر نیز چه کاربر دیگری را توصیه کرده و به همین ترتیب تا پایین‌ترین سطح ممکن.
var id = 1;
var entity1WithAllOfItsDescendants =
                        context.Members
                            .Include(member => member.Children)
                            .Where(member => member.MemId == id
                                            || member.Children.Any(m => member.MemId == m.RecommendedBy))
                            .ToList() //It's a MUST - get all children from the database
                            .FirstOrDefault(x => x.MemId == id);// then get the root of the tree
این کوئری نیز شبیه به کوئری مثال قبلی است؛ با یک تفاوت. در اینجا Include(member => member.Children) هم ذکر شده‌است. هدف این است که EF-Core، خاصیت Children را تا n سطح ممکن به صورت خودکار مقدار دهی کند و این مورد دقیقا هدف اصلی مثال جاری است.
وجود Include، سبب تولید یک چنین کوئری می‌شود که در آن جدول کاربران با خودش جوین شده‌است:
SELECT   [m].[MemId],
         [m].[Address],
         [m].[FirstName],
         [m].[JoinDate],
         [m].[RecommendedBy],
         [m].[Surname],
         [m].[Telephone],
         [m].[ZipCode],
         [m0].[MemId],
         [m0].[Address],
         [m0].[FirstName],
         [m0].[JoinDate],
         [m0].[RecommendedBy],
         [m0].[Surname],
         [m0].[Telephone],
         [m0].[ZipCode]
FROM     [Members] AS [m]
         LEFT OUTER JOIN
         [Members] AS [m0]
         ON [m].[MemId] = [m0].[RecommendedBy]
WHERE    ([m].[MemId] = 1)
         OR EXISTS (SELECT 1
                    FROM   [Members] AS [m1]
                    WHERE  ([m].[MemId] = [m1].[RecommendedBy])
                           AND ([m].[MemId] = [m1].[RecommendedBy]))
ORDER BY [m].[MemId], [m0].[MemId];
پس از آن باید خاصیت member.Children را تا هر سطح ممکن به صورت بازگشتی پیمود تا به جواب اصلی این مثال رسید:
namespace EFCorePgExercises.Exercises.RecursiveQueries
{
    public static class RecursiveUtils
    {
        public static void FindChildren(Member member, List<dynamic> actualResult)
        {
            if (member == null)
            {
                return;
            }

            foreach (var item in member.Children)
            {
                actualResult.Add(new { item.MemId, item.FirstName, item.Surname });
                if (item.Children != null)
                {
                    FindChildren(item, actualResult);
                }
            }
        }
    }
}
که به صورت زیر می‌تواند مورد استفاده قرار گیرد:
var actualResult = new List<dynamic>();
RecursiveUtils.FindChildren(entity1WithAllOfItsDescendants, actualResult);


کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت پنجم - تعیین نوع هوک useReducer
هوک استاندارد useReducer، یک نمونه‌ی پیچیده‌تر هوک useState، برای مدیریت حالت است؛ با ساختاری شبیه به Redux، اما تنها مخصوص یک کامپوننت. هوک useReducer شبیه به یک نسخه‌ی کوچک و محلی Redux است. در این قسمت، نحوه‌ی تعیین نوع‌های قسمت‌های مختلف آن‌را با TypeScript بررسی می‌کنیم.


ایجاد کامپوننت جدید ReducerButtons

برای توضیح مفاهیم این قسمت، فایل جدید src\components\ReducerButtons.tsx را به همراه محتوای زیر ایجاد می‌کنیم:
import React, { useReducer } from "react";

const initialState = { rValue: true };
type State = {  rValue: boolean; };
type Action = {   type: string; };

function reducer(state: State, action: Action) {
  switch (action.type) {
    case "one":
      return { rValue: true };
    case "two":
      return { rValue: false };
    default:
      return state;
  }
}

export const ReducerButtons = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {state?.rValue && <h1>Visible</h1>}
      <button onClick={() => dispatch({ type: "one" })}>Action One</button>
      <button onClick={() => dispatch({ type: "two" })}>Action Two</button>
    </div>
  );
};
توضیحات:
- این کامپوننت دو دکمه را رندر می‌کند تا توسط آن‌ها بتوان چندین Action مختلف را جهت مدیریت حالت، سبب شد؛ مانند Action One و Two.
- initialState را در این مثال، یک شیء ساده در نظر گرفته‌ایم که تنها دارای یک مقدار boolean است.
- سپس نیاز به یک تابع ویژه را به نام reducer، داریم که مقدار state جاری و یک action را دریافت می‌کند. این تابع بر اساس نوع Action ای که به آن می‌رسد، state جدیدی را بازگشت می‌دهد و در قسمت رندر آن، با بررسی state?.rValue، سبب نمایش عبارتی خواهیم شد.
- در حین تعریف هوک useReducer، قسمت dispatch آن، تابعی است که یک Action را اجرا می‌کند. فراخوانی آن‌را در رویداد onClick دو دکمه‌ی تعریف شده مشاهده می‌کنید. این تابع یک شیء را دریافت می‌کند که type اکشن ارسالی به تابع reducer را مقدار دهی می‌کند.

توسط useReducer برای ایجاد تغییرات در شیء state کامپوننت جاری، از مفهومی به نام dispatch actions استفاده می‌شود. action در اینجا به معنای رخ‌دادن چیزی است؛ مانند کلیک بر روی یک دکمه و یا دریافت اطلاعاتی از یک API. در این حالت مقایسه‌ای بین وضعیت قبلی state و وضعیت فعلی آن صورت می‌گیرد و تغییرات مورد نیاز جهت اعمال به UI، محاسبه خواهند شد. اصلی‌ترین جزء آن، تابعی است به نام Reducer. این تابع، یک تابع خالص است و دو آرگومان را دریافت می‌کند. تابع Reducer، بر اساس action و یا رخ‌دادی، ابتدا کل state برنامه را دریافت می‌کند و سپس خروجی آن بر اساس منطق این تابع، یک state جدید خواهد بود. اکنون که این state جدید را داریم، برنامه‌ی React ما می‌تواند به تغییرات آن گوش فرا داده و بر اساس آن، UI را به روز رسانی کند.

اولین قسمتی را که در حین کار با useReducer توسط TypeScript به آن برخورد خواهیم کرد، نمایش خطاهایی در مورد نوع‌های پارامترهای state و action تابع reducer است. در حالت متداول جاوا اسکریپتی آن، این پارامترها، بدون نوع ذکر می‌شوند که در اینجا به any تفسیر خواهند شد و یک چنین تعاریفی با توجه به strict بودن تنظیمات tsconfig.json برنامه، مجاز نیست. به همین جهت نیاز به تعریف دو type جدید به نام‌های State و Action در اینجا وجود دارد تا خطاهای بدون نوع بودن پارامترهای تابع reducer برطرف شوند. نوع Action به همراه حداقل یک خاصیت به نام action رشته‌ای است و نوع State بر اساس initialState ای که تعریف کردیم، دارای یک خاصیت متناظر boolean است.

نکته‌ی جالب دومی که در اینجا توسط TypeScript گوشزد می‌شود، الزام به ذکر حالت default مربوط به switch ای است که در تابع reducer تعریف کرده‌ایم. اگر این حالت را حذف کنیم، بلافاصله خطای زیر را در قسمت تعریف useReducer نمایش می‌دهد:


عنوان می‌کند که خروجی تابع reducer، یک state جدید است؛ اما ممکن است از نوع never هم باشد که قابلیت ترجمه‌ی به نوع state را ندارد.


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

تا اینجا مهم‌ترین تغییرات تایپ‌اسکریپتی صورت گرفته، تعریف نوع‌های پارامترهای تابع reducer است. اکنون فرض کنید می‌خواهیم، سومین Action را هم به کامپوننت جاری اضافه کنیم:
<button onClick={() => dispatch({ type: "three" })}>Action Three</button>
با این تغییر، برنامه بدون مشکل کامپایل می‌شود، اما ... در عمل کار خاصی را هم انجام نمی‌دهد؛ چون switch تعریف شده‌ی در تابع reducer، یک case مخصوص به آن‌را تعریف نکرده‌است. یا حتی ممکن است در حین تعریف همان دو تابع dispatch، مقدار type را به اشتباه وارد کنیم و با حالت‌های تعریف شده‌ی در تابع reducer تطابقی نداشته باشد. به همین جهت علاقمندیم تا TypeScript بتواند جلوی یک چنین اشتباهاتی را نیز بگیرد؛ برای این منظور می‌توان از union types استفاده کرد:
type Action = {
   type: "one" | "two" | "three";
};
در اینجا تمام اکشن‌های ممکن و مدنظر را لیست کرده‌ایم که سبب خواهد شد تا اگر مقدار نوع اکشنی به درستی وارد نشود، بلافاصله خطایی را دریافت کنیم:


و یا حتی اگر مقدار action.type ای را در تابع reducer به اشتباه وارد کردیم، باز هم برنامه کامپایل نمی‌شود:



تا اینجا موفق شدیم جلوی ورود actionهای پیش بینی نشده را بگیریم. اما اگر در switch تعریف شده، "case "three را هم قید نکنیم، باز هم برنامه کامپایل می‌شود و خطایی گزارش نخواهد شد. برای این منظور فقط کافی است case default را حذف کنیم. چون action.type دیگر نمی‌تواند مقدار دیگری را بجز موارد ذکر شده‌ی در نوع Action داشته باشد. با حذف case default، اینبار ذکر "case "three یا هر کدام از مقادیر سه‌گانه‌ی مجازی که در اینجا تعریف کردیم، الزامی خواهد بود. در غیراینصورت، همان خطای دریافت خروجی never را از تابع reducer دریافت می‌کنیم که با ذکر هر سه case مجاز، برطرف می‌شود.
با این تغییرات، کدهای نهایی این قسمت به صورت زیر در خواهند آمد:
import React, { useReducer } from "react";

const initialState = { rValue: true };

type State = {
  rValue: boolean;
};

type Action = {
  type: "one" | "two" | "three";
};

function reducer(state: State, action: Action) {
  switch (action.type) {
    case "one":
      return { rValue: true };
    case "two":
      return { rValue: false };
    case "three":
      return { rValue: false };
  }
}

export const ReducerButtons = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {state?.rValue && <h1>Visible</h1>}
      <button onClick={() => dispatch({ type: "one" })}>Action One</button>
      <button onClick={() => dispatch({ type: "two" })}>Action Two</button>
      <button onClick={() => dispatch({ type: "three" })}>Action Three</button>
    </div>
  );
};
مطالب
Garbage Collector در #C - قسمت سوم
در قسمت قبلی درباره تفاوت‌های Stack و Heap، صحبت کرده و به این نتیجه رسیدیم که برای آزادسازی حافظه Heap، در صورتی‌که نخواهیم اینکار را بصورت دستی انجام دهیم، نیاز به Garbage Collector پیدا خواهیم کرد.


تاریخچه‌ای مختصر از GC در NET.

ایده اولیه ایجاد Garbage Collector در NET.، در سال 1990 بود که در آن زمان، مایکروسافت مشغول پیاده سازی خود از JavaScript بنام JScript بود. در ابتدا JScript توسط تیمی چهار نفره توسعه داده میشد و در آن زمان یکی از اعضای این تیم به نام Patrick Dussud که بعنوان پدر Garbage Collector در NET. شناخته میشود، یک Conservative GC را داخل تیم توسعه داد. در آن زمان CLR ای وجود نداشت و Patrick Dussud برروی JVM کار میکرد.

مایکروسافت سعی بر پیاده سازی نسخه‌ای اختصاصی از JVM را برای خود بجای ایجاد چیزی شبیه به NET Runtime. فعلی داشت؛ اما بعد از شکل گیری تیم CLR، به این نتیجه رسیدند که JVM برای آنها محدودیت‌هایی را ایجاد میکند و به همین دلیل شروع به ایجاد Environment خود کردند.

با این تصمیم، Patrick Dussud مجددا یک GC جدید را با ایده "بهترین GC ممکن" با زبان LISP که در آن بیشترین مهارت را داشت، بصورت Prototype نوشت و سپس یک Transpiler از LISP را به ++C نوشت که کدهای آن قابل استفاده در Runtime مایکروسافت باشد.

کدهای فعلی مربوط به Garbage Collector مورد استفاده در NET. در این فایل از ریپازیتوری runtime مایکروسافت قابل دسترسی هستند. در حال حاضر خانم Maoni Stephens مدیر فنی تیم GC مایکروسافت هستند که کنفرانس‌ها و مقالات زیادی نیز درباره نکات مختلف پیاده سازی GC در بلاگ خود نوشته و ارائه کرده‌اند.


در حال حاضر، سه حالت (flavor) از GC در NET. تعبیه شده‌است و هرکدام از این حالات برای انواع مختلفی از برنامه‌ها بهینه شده‌است که در ادامه به بررسی آنها میپردازیم.


Server GC

این نوع GC برای برنامه‌های سمت سرور نظیر ASP.NET Core و WCF بهینه سازی شده‌است که تعداد ریکوئست‌های زیادی به آنها وارد میشود و هر ریکوئست باعث allocate شدن اشیا مختلفی شده و بطور کلی، نرخ allocation و deallocation در آنها بالاست.

Server GC به ازای هر پردازنده، از یک Heap و یک GC Thread مجزا استفاده میکند. این بدین معناست که اگر شما یک پردازنده را با هشت Core داشته باشید، در زمان Garbage Collection، روی هرکدام از Coreها یک Heap و GC Thread مستقل وجود دارد که عمل Garbage Collection را انجام میدهند.

این شکل عملکرد باعث میشود که Collection، در سریعترین زمان ممکن، بدون وقفه اضافه انجام شود و برنامه شما اصطلاحا ((Freeze)) نشود.

Server GC فقط روی پردازنده‌های چند هسته‌ای قابل اجراست و اگر سعی کنید برنامه خود را روی یک سیستم با پردازنده تک هسته‌ای در حالت Server GC اجرا کنید، بصورت خودکار برنامه شما از Non-Concurrent Workstation GC استفاده کرده و اصطلاحا Fallback خواهد شد.

اگر نیاز دارید که در برنامه‌هایی به‌غیر از Server-Side Applicationها، نظیر WPF و Windows Service‌ها و ... از این نوع GC استفاده کنید (به شرط چند هسته بودن پردازنده)، میتوانید این تنظیمات را به فایل app.config یا web.config خود اضافه کنید:
<configuration>
  <runtime>
    <gcServer enabled="true"/>
  </runtime>
</configuration>

همچنین در برنامه‌های NET Core.ای نیز میتوانید این تنظیمات را داخل فایل csproj. برنامه خود اضافه کنید:
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>


Concurrent Workstation GC


این حالت، حالت پیشفرض مورد استفاده در برنامه‌های Windows Forms و Windows Service است. این حالت از GC برای برنامه‌هایی بهینه شده‌است که در آنها، هنگام وقوع Garbage Collection، برنامه توقف و مکث حتی چند لحظه‌ای نداشته و Collection باعث نشود که کاربر نتواند روی یک دکمه کلیک کند و اصطلاحا برنامه ((Unresponsive)) شود.

برای فعالسازی Concurrent Workstation GC این تنظیمات را داخل config برنامه خود باید اعمال کنید:
<configuration>
  <runtime>
    <gcConcurrent enabled="true" />
  </runtime>
</configuration>


Non-Concurrent Workstation GC


این حالت شبیه به حالت Server GC است؛ با این تفاوت که عمل Collection روی Thread ای که درخواست allocate کردن یک object را کرده است، صورت میگیرد.

برای مثال:

  • Thread شماره یک درخواست allocate کردن یک string با طول 10000 کاراکتر را میدهد.
  • حافظه، فضای کافی برای تخصیص این حجم از حافظه را نداشته و سعی میکند با اجرای Garbage Collector، این حجم فضای مورد نیاز از حافظه را خالی کند.
  • CLR تمام Thread‌های برنامه را متوقف میکند و Garbage Collector شروع به کار کرده و اشیا بلااستفاده «روی Thread ای که آن را فراخوانی کرده است» را Collect میکند.
  • بعد از پایان Collection، تمامی Threadهای برنامه که در مرحله قبل متوقف شده بودند، مجددا شروع به کار خواهند کرد.


این حالت از GC برای برنامه‌های Server-Side ای که برروی پردازنده تک هسته‌ای اجرا میشوند، پیشنهاد میشود. برای فعالسازی این حالت، تنظیمات داخل config برنامه به این صورت باید تغییر پیدا کند:
<configuration>
  <runtime>
    <gcConcurrent enabled="false" />
  </runtime>
</configuration>



این جدول، کمک خواهد کرد که بر اساس نوع برنامه خود، تنظیمات درستی را برای GC اعمال نمایید (در اکثر موارد، تنظیمات پیشفرض بهترین انتخاب بوده و نیازی به تغییر روند کار GC نیست):

 Server GC  Non-Concurrent Workstation  Concurrent Workstation  
 Maximize throughput on multi-processor machines for server apps that create multiple threads to handle the same types of requests.  Maximize throughput on single-processor machines.  Balance throughput and responsiveness for client apps with UI. Design Goal 
1 per processor ( hyper thread aware )  1  Number of Heaps
 1 dedicated GC thread per processor The thread which performs the allocation that triggers the GC. The thread which performs the allocation that triggers the GC.  GC Threads
 EE is suspended during a GC. EE is suspended during a GC. EE is suspended much shorter but several times during a GC.  Execution Engine
Suspension
<gcServer enabled="true">
<gcConcurrent enabled="false">
 <gcConcurrent enabled="true"> 
 Config Setting
Non-Concurrent Workstation GC      On a single
processor
(fallback)
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت دوم - جمع آوری اطلاعات آماری کوئری‌ها توسط Extended Events
همانطور که در قسمت قبل نیز بررسی کردیم، Management Studio برای جمع آوری اطلاعات آماری کوئری‌های زنده بسیار مفید است؛ اما تهیه‌ی آن دستی است. باید کوئری را اجرا کرد و سپس مراحلی را طی نمود تا به نتایج آماری حاصل از کوئری‌ها رسید و همچنین دست آخر باید از نتایج آن نیز یک خروجی دستی را تهیه کرد. روش دیگری نیز برای جمع آوری اطلاعات آماری کوئری‌ها در SQL Server توسط Extended Events/Trace وجود دارد که به ازای هر کوئری، قابل استخراج است. علاوه بر آن می‌توان از Dynamic management objects و یا Query store نیز استفاده کرد. این دو برخلاف Extended Events/Trace، اطلاعات تجمعی گروهی از کوئری‌ها را بازگشت می‌دهند. همچنین در اینجا performance monitor نیز می‌تواند مورد استفاده قرار گیرد؛ اما محدوده‌ی دید آن کل بانک اطلاعاتی است.


Extended Events/Trace

Extended Events، زیر ساخت مدیریت رخ‌دادها در SQL Server است. برای مثال در نگارش 2016 آن بیش‌از 300 رخ‌داد در SQL Server تعریف شده‌اند و زمانیکه در مورد اجرای کوئری‌ها بحث می‌کنیم، این رخ‌دادها بیشتر مدنظر ما هستند:
sql_statement_completed
sp_statement_completed
rpc_completed
sql_batch_completed
کار آن‌ها دریافت اطلاعاتی در مورد logical reads، میزان مصرف CPU، مدت زمان اجرای کوئری‌ها و امثال آن‌ها است. در این بین، دو مورد اول بیش از همه مورد استفاده قرار می‌گیرند.
علاوه بر این‌ها، رخ‌دادهای بسط یافته‌ی زیر را نیز می‌توان مورد استفاده قرار داد:
query_post_compilation_showplan
query_post_execution_showplan
query_pre_execution_showplan
اما به علت هزینه‌بر بودن تولید execution plan به ازای هر کوئری، آنچنان مورد استفاده قرار نمی‌گیرند.


استفاده از Extended Events برای جمع آوری اطلاعات آماری کوئری‌ها

برای آزمایش نحوه‌ی کار با Extended Events، ابتدا رویه‌ی ذخیره شده‌ی زیر را ایجاد می‌کنیم:
USE [WideWorldImporters];
GO

DROP PROCEDURE IF EXISTS [Application].[usp_GetCountryInfo];
GO

CREATE PROCEDURE [Application].[usp_GetCountryInfo]
    @Country_Name NVARCHAR(60)
AS

SELECT *
FROM [Application].[Countries] [c]
    JOIN [Application].[StateProvinces] [s]
    ON [s].[CountryID] = [c].[CountryID]
WHERE [c].[CountryName] = @Country_Name;
GO
این کوئری شبیه به کوئری‌است که در قسمت قبل مورد استفاده قرار گرفت؛ با این تفاوت که به همراه یک * SELECT است که استفاده‌ی از آن توصیه نمی‌شود و در اینجا بیشتر جهت بررسی کارآیی این کوئری، تعریف شده‌است.
سپس یک سشن Extended Events سفارشی را به صورت زیر ایجاد می‌کنیم:
/*
Create XE session to capture sql_statement_completed
and sp_statement_completed
*/
IF EXISTS (
SELECT *
FROM sys.server_event_sessions
WHERE [name] = 'QueryPerf')
BEGIN
    DROP EVENT SESSION [QueryPerf] ON SERVER;
END
GO

CREATE EVENT SESSION [QueryPerf] 
ON SERVER 
ADD EVENT sqlserver.sp_statement_completed(WHERE ([duration]>(1000))),
ADD EVENT sqlserver.sql_statement_completed(WHERE ([duration]>(1000)))
ADD TARGET package0.event_file(SET filename=N'C:\Temp\QueryPerf\test.xel',max_file_size=(256))
WITH (
  MAX_MEMORY=16384 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,
  MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,
  MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=OFF,STARTUP_STATE=OFF);
GO
در این سشن، رخ‌دادهای sp_statement_completed و sql_statement_completed مورد استفاده قرار گرفته‌اند. هر کدام نیز بر اساس مدت زمان اجرای کوئری، فیلتر شده‌اند. در اینجا عدد 1000، یعنی یک میلی ثانیه که عدد بسیار کوچکی است؛ اما برای دمو، مفید است. نتیجه‌ی عملیات نیز در مسیر C:\Temp\QueryPerf ذخیره خواهد شد.

سپس نیاز است تا این سشن را که QueryPerf نام دارد، در قسمت management->extended events، اجرا و آغاز کرد:


در ادامه ابتدا بر روی بانک اطلاعاتی WideWorldImporters، کلیک راست کرده و یک پنجره‌ی new query جدید را ایجاد می‌کنیم:
WHILE 1 = 1
BEGIN
   EXECUTE [Application].[usp_GetCountryInfo] N'United States';
END
در این پنجره با یک حلقه‌ی بی‌پایان، رویه‌ی ذخیره شده‌ای را که ایجاد کردیم، بارها و بارها اجرا خواهیم کرد (نکته‌ی «عدم نمایش ردیف‌های بازگشت داده شده‌ی توسط کوئری در حین جمع آوری اطلاعات آماری» قسمت قبل را هم مدنظر داشته باشید).

سپس مجددا یک پنجره‌ی new query دیگر را باز می‌کنیم:
WHILE 1 = 1
BEGIN
    SELECT
        [s].[StateProvinceName],
        [s].[SalesTerritory],
        [s].[LatestRecordedPopulation],
        [s].[StateProvinceCode]
    FROM [Application].[Countries] [c]
        JOIN [Application].[StateProvinces] [s]
        ON [s].[CountryID] = [c].[CountryID]
    WHERE [c].[CountryName] = 'United States';
END
این کوئری شبیه به رویه‌ی ذخیره شده‌ای است که ایجاد کردیم؛ اما یک کوئری Ad Hoc و غیر پارامتری می‌باشد.

کوئری‌های هر دو پنجره را به صورت مجزایی اجرا کنید. سپس در قسمت management->extended events، بر روی سشن QueryPerf کلیک راست کرده و گزینه‌ی View live data را انتخاب کنید:


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

یک نکته: در اینجا در قسمت Details، اگر بر روی هر ردیف کلیک کنید، امکان انتخاب و نمایش آن در لیست بالای صفحه توسط گزینه‌ی Show Column in table وجود دارد.

در آخر در قسمت management->extended events، بر روی سشن QueryPerf کلیک راست کرده و گزینه‌ی Stop Session را انتخاب کنید. اکنون اگر به پوشه‌ی C:\Temp\QueryPerf مراجعه کنید، فایل xel حاوی اطلاعات این گزارش را نیز می‌توانید مشاهده نمائید (به ازای هربار اجرای این سشن، یک فایل جدید را تولید می‌کند).


 این فایل توسط Management Studio قابل گشودن و بررسی است و دقیقا همان نمای گزارش live data را به همراه دارد.
مطالب
آزمایش Web APIs توسط Postman - قسمت اول - معرفی
Postman یک ابزار متکی به خود چند سکویی، رایگان و فوق العاده‌ای است جهت توسعه و آزمایش Web API‌ها (HTTP Restful APIs). برای دریافت آن می‌توانید به این آدرس مراجعه کنید. البته پیشتر افزونه‌ای، مخصوص کروم را نیز ارائه کرده بودند که دیگر پشتیبانی نمی‌شود و اگر بر روی مرورگر شما نصب است، بهتر است آن‌را حذف کنید.


شروع به کار با Postman

پس از نصب و اجرای Postman، در ابتدا درخواست می‌کند که اکانتی را در سایت آن‌ها ایجاد کنید. البته این مورد اختیاری است و امکان ذخیره سازی بهتر کارها را فراهم می‌کند. همچنین در اولین بار اجرای برنامه، یک صفحه‌ی دیالوگ انتخاب گزینه‌های مختلف را نمایش می‌دهد که می‌توانید نمایش آتی آن‌را با برداشتن تیک Show this window on launch، غیرفعال کنید.


رابط کاربری Postman، از چندین قسمت تشکیل می‌شود:
1) Request builder
در قسمت سمت راست و بالای رابط کاربری Postman می‌توان انواع و اقسام درخواست‌ها را جهت ارسال به یک Web API، ساخت و ایجاد کرد. توسط آن می‌توان HTTP method، آدرس، بدنه، هدرها و کوکی‌های یک درخواست را تنظیم کرد. برای مثال در اینجا httpbin.org را وارد کرده و بر روی دکمه‌ی send کلیک کنید:


2) قسمت نمایش Response
پس از ارسال درخواست، بلافاصله، نتیجه‌ی نهایی را در ذیل قسمت ساخت درخواست، می‌توان مشاهده کرد:


در اینجا status code بازگشتی از سرور و همچنین response body را مشاهده می‌کنید. به علاوه نوع خروجی را نیز HTML تشخیص داده‌است و با توجه به اینکه این درخواست، به یک وب سایت معمولی بوده‌است، طبیعی می‌باشد.
همچنین در این خروجی، سه برگه‌ی pretty/raw/preview نیز قابل مشاهده هستند. حالت pretty آن‌را که به همراه syntax highlighting است، مشاهده می‌کنید. اگر حالت نمایش raw را انتخاب کنید، حالت متنی و اصل خروجی بازگشتی از سمت سرور را مشاهده خواهید کرد. برگه‌ی preview آن، این خروجی را شبیه به یک مرورگر نمایش می‌دهد.

3) قسمت History
با ارسال این درخواست، در سمت چپ صفحه، تاریخچه‌ی این عملیات نیز درج می‌شود:


4) رابط کاربری چند برگه‌ای
برای ارسال یک درخواست جدید، یا می‌توان مجددا یکی از گزینه‌های History را انتخاب کرد و آن‌را ارسال نمود و یا می‌توان در همان قسمت سمت راست و بالای رابط کاربری، بر روی دکمه‌ی + کلیک و برگه‌ی جدیدی را جهت ایجاد درخواستی جدید، باز کرد:


در اینجا درخواستی را به endpoint جدید https://httpbin.org/get ارسال کرده‌ایم که در آن نوع پروتکل HTTPS نیز صریحا ذکر شده‌است. اگر به خروجی دریافتی از سرور دقت کنید، اینبار نوع بازگشتی را JSON تشخیص داده‌است که خروجی متداول بسیاری از HTTP Restful APIs است. در این حالت، انتخاب نوع نمایش pretty/raw/preview آنچنان تفاوتی را ایجاد نمی‌کند و همان حالت pretty که syntax highlighting را نیز به همراه دارد، مناسب است.


ارسال کوئری استرینگ‌ها توسط Postman

برای ارسال درخواستی به همراه کوئری استرینگ‌ها مانند https://httpbin.org/get?param1=val1&param2=val2، می‌توان به صورت زیر عمل کرد:


یا می‌توان مستقیما URL فوق را وارد کرد و سپس بر روی دکمه‌ی send کلیک نمود و یا در ذیل این قسمت، در برگه‌ی Params نیز این کوئری استرینگ‌ها به صورت key/valueهایی ظاهر می‌شوند که وارد کردن آن‌ها به این نحو ساده‌تر است؛ خصوصا اگر تعداد این پارامترها زیاد باشد، تغییر پارامترها و آزمایش آن‌ها توسط این رابط کاربری گرید مانند، به سهولت قابل انجام است. همچنین جائیکه علامت check-mark را مشاهده می‌کنید، می‌توان اشاره‌گر ماوس را قرار داد تا آیکن تغییر ترتیب پارامترها نیز ظاهر شود. به این ترتیب توسط drag & drop می‌توان ترتیب این ردیف‌ها را تغییر داد:


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


ذخیره سازی عملیات انجام شده

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


در همینجا بر روی دکمه‌ی Save کنار دکمه‌ی Send کلیک کنید. اگر دقت کنید، دکمه‌ی Save دیالوگ ظاهر شده غیرفعال است:


علت اینجا است که در Postman نمی‌توان یک تک درخواست را به صورت مستقل ذخیره کرد. Postman درخواست‌ها را در مجموعه‌های خاص خودش (collections) مدیریت می‌کند؛ چیزی شبیه به پوشه‌ی bookmarks، در یک مرورگر. بنابراین در همینجا بر روی لینک Create collection کلیک کرده و برای مثال نام گروه دلخواهی را مانند httpbin وارد کنید. سپس بر روی دکمه‌ی check-mark کنار آن کلیک نمائید تا این مجموعه ایجاد شود.


اکنون پس از ایجاد این مجموعه و انتخاب آن، دکمه‌ی Save to httpbin در پایین صفحه ظاهر می‌شود.
به صورت پیش‌فرض، نام فیلد درخواست، در این صفحه‌ی دیالوگ، همان آدرس درخواست است که قابلیت ویرایش را نیز دارد. بنابراین برای مثال فیلد request name را به Get request تغییر داده و سپس بر روی دکمه‌ی Save to httpbin کلیک کنید.


نتیجه‌ی این عملیات را در برگه‌ی Collections سمت چپ صفحه می‌توان مشاهده کرد. در این حالت اگر درخواست مدنظری را انتخاب کنید و سپس جزئیات آن‌را ویرایش کنید، مجددا همان علامت دایره‌ای نارنجی رنگ، بالای برگه‌ی ساخت درخواست ظاهر می‌شود که بیانگر حالت ذخیره نشده‌ی این درخواست است. اکنون اگر بر روی دکمه‌ی Save کنار Send کلیک کنید، در همان آیتم گروه جاری انتخابی، به صورت خودکار ذخیره و بازنویسی خواهد شد.


ارسال درخواست‌هایی از نوع POST

برای آزمایش ارسال یک درخواست از نوع Post، مجددا بر روی دکمه‌ی + کنار آخرین برگه‌ی باز شده کلیک می‌کنیم تا یک برگه‌ی جدید باز شود. سپس در ابتدا، نوع درخواست را از Get پیش‌فرض، به Post تغییر می‌دهیم:


در این حالت آدرس https://httpbin.org/post را وارد کرده و سپس برگه‌ی body را که پس از انتخاب حالت Post فعال شده‌است، انتخاب می‌کنیم:


در اینجا برای مثال گزینه‌ی x-www-form-urlencoded، همان حالتی است که اطلاعات را از طریق یک فرم واقع در صفحات وب به سمت سرور ارسال می‌کنیم. اما اگر برای مثال نیاز باشد تا اطلاعات را با فرمت JSON، به سمت Web API ای ارسال کنیم، نیاز است گزینه‌ی raw را انتخاب کرد و سپس قالب پیش‌فرض آن‌را که text است به JSON تغییر داد:


در اینجا برای مثال یک payload ساده را ایجاد کرده و سپس بر روی دکمه‌ی send کلیک کنید تا به عنوان بدنه‌ی درخواست، به سمت Web API ارسال شود:


که نتیجه‌ی آن چنین خروجی از سمت سرور خواهد بود:


در یک قسمت آن، raw data ما مشخص است و در قسمتی دیگر، اطلاعات با فرمت JSON، به درستی تشخیص داده‌است.
در ادامه بر روی دکمه‌ی Save این برگه کلیک کنید. در صفحه‌ی باز شده، نام پیش‌فرض آن‌را که آدرس درخواست است، به Post request تغییر داده، گروه httpbin را انتخاب و سپس بر روی دکمه‌ی Save to httpbin کلیک کنید:


اکنون مجموعه‌ی httpbin به همراه دو درخواست است:


برای آزمایش آن، تمام برگه‌های باز را با کلیک بر روی دکمه‌ی ضربدر آن‌ها ببندید. در ادامه اگر بر روی هر کدام از آیتم‌های این مجموعه کلیک کنید، جزئیات آن قابل بازیابی خواهد بود.
مطالب
Action و Function در OData
استفاده از OData تنها به عملیات CRUD معطوف نمیشود و در عمل شما این قابلیت را دارید که متد‌های سفارشی و کاملا مجزایی را از همدیگر در سرویس‌های خود داشته باشید.
هرچند در بعضی از سناریو‌ها نیازی به استفاده‌ی بیشتر از CRUD مربوط به آن entity وجود ندارد، اما در اکثر موارد نیاز به رفتاری دارید که به راحتی با استفاده از CRUD معمولی قابلیت پیاده سازی را ندارد. در اینگونه موارد Action‌ها و Function‌ها هستند که به راحتی با استفاده از آنها، قابلیت طراحی ماژول‌های سفارشی فراهم شده است.
Actions و Functions در عمل رفتاری شبیه به هم را دارند؛ اما تفاوت‌های آنها در این است که:
1) Action‌ها برای درخواست‌های از نوع post هستند؛ اما به عکس Function‌ها از نوع get میباشند.
2) Action‌ها اثرات جانبی دارند اما Function‌ها خیر.
3) Function‌ها حتما باید خروجی داشته باشند؛ اما این الزام برای Action‌ها وجود ندارد.
4) هر دو امکان داشتن هر تعداد پارامتری را دارند (یا بدون پارامتر).

اضافه کردن Action
فرض کنید مدل زیر را در اختیار داریم
namespace ProductService.Models
{
    public class ProductRating
    {
        public int ID { get; set; }
        public int Rating { get; set; }
        public int ProductID { get; set; }
        public virtual Product Product { get; set; }  
    }
}
حال نیاز داریم که آن را به EDM اضافه نماییم. در کانفیگ OData، نوع Conventional را استفاده کرده و ابتدا Namespaceی را تعریف کرده و سپس Action خود را تعریف مینماییم؛ به صورت زیر:
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");

builder.Namespace = "ProductService";
builder.EntityType<Product>()
    .Action("Rate")
    .Parameter<int>("Rating");
در اینجا یک متد اکشن را به نام Rate و پارامتری را از نوع integer به نام Rating تعریف مینماییم.

اضافه کردن متد اکشن در کنترلر
[HttpPost]
public async Task<IHttpActionResult> Rate([FromODataUri] int key, ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    int rating = (int)parameters["Rating"];
    db.Ratings.Add(new ProductRating
    {
        ProductID = key,
        Rating = rating
    });

    await db.SaveChangesAsync();
    
    return StatusCode(HttpStatusCode.NoContent);
}
توجه کنید که نام متد نوشته شده، همنام اکشنی است که قبلا تعریف کرده‌ایم. کار ODataActionParameters گرفتن پارامتر‌های ارسالی از سمت کلاینت میباشد. دقت کنید که نام پارامتر را نیز تعیین کرده بودیم.
ذکر [HttpPost] برای تعیین کردن post بودن این متد است. برای فراخوانی این اکشن از سمت کلاینت، درخواستی را از نوع post، بدین شکل ارسال مینماییم:
POST http://localhost/Products(1)/ProductService.Rate HTTP/1.1
Content-Type: application/json
Content-Length: 12

{"Rating":5}
توجه داشته باشید که ProductService همان Nampespaceی است که در کانفیگ تعیین کرده بودیم.
و البته برای فراخوانی این درخواست توسط یک کلاینت C#ی، اینگونه رفتار میکنیم (در مقاله‌های آتی از C# Odata client برای فراخوانی درخواست‌ها به صورت strongly typed استفاده خواهیم نمود)
HttpClient client = new HttpClient();
var response = client.PostAsync(postUrl, new StringContent(JsonConvert.SerializeObject(new { Rating = 5 }), Encoding.UTF8, "application/json")).Result;

اضافه کردن متد Function
برای اضافه کردن متد فانکشن نیز ابتدا باید آن را در کانفیگ OData معرفی نماییم؛ به صورت زیر:
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
builder.EntitySet<Supplier>("Suppliers");

builder.Namespace = "ProductService";
builder.EntityType<Product>().Collection
    .Function("MostExpensive")
    .Returns<double>();
در اینجا یک متد function را به نام MostExpensive، بدون پارامتر و با نوع بازگشتی double، تعریف نموده‌ایم.

نکته:
 برای اینکه نوع بازگشتی از نوع EntitySet باشد:
ReturnsFromEntitySet<Product>("Products")
و یا نوع بازگشتی، لیستی از EntitySet‌ها باشد:
ReturnsCollectionFromEntitySet<Product>("Products");
و یا نوع بازگشتی بطور مثال لیستی از stringها باشد:
ReturnsCollection<string>();

برای فراخوانی این متد میتوان از آدرس زیر استفاده نمود:
GET http://localhost/Products/ProductService.MostExpensive

حال فقط کافیست که متد آن را در کنترلر مربوطه پیاده سازی نماییم:
public class ProductsController : ODataController
{
    [HttpGet]
    public IHttpActionResult MostExpensive()
    {
        var product = db.Products.Max(x => x.Price);
        return Ok(product);
    }

    // Other controller methods not shown.
}
اگر مقاله‌های قبلی را دنبال کرده باشید، این قسمت برای شما آشنا خواهد بود.
نوع response بازگشتی درخواست فوق چیزی شبیه به این خواهد بود:
HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0
Date: Sat, 28 Jun 2016 00:44:07 GMT
Content-Length: 85

{
  "@odata.context":"http://localhost:38479/$metadata#Edm.Decimal","value":50.00
}

اضافه کردن Unbound Function
در مثال قبلی یک function bound نوشتیم که مربوط به یک EntitySet خاص بود. اما اکنون میخواهیم یک متد Unbound تعریف نماییم، به صورت زیر:
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");

builder.Function("GetSalesTaxRate")
    .Returns<double>()
    .Parameter<int>("PostalCode");
توجه کنید اینجا ما متد را به صورت مستقیم از ODataModelBuilder تهیه نمود‌ه‌ایم؛ به جای Entity type یا collection مربوطه. این تنظیم به model builder میگوید که متدی unbound میباشد.
متد آن را نیز اینگونه پیاده سازی مینماییم:
[HttpGet]
[ODataRoute("GetSalesTaxRate(PostalCode={postalCode})")]
public IHttpActionResult GetSalesTaxRate([FromODataUri] int postalCode)
{
    double rate = 5.6;  // Use a fake number for the sample.
    return Ok(rate);
}
اهمیتی ندارد که این متد را در چه Controllerی پیاده سازی نمایید. ذکر [ODataRoute] نیز برای تعریف URl این function میباشد.

برای فراخوانی آن نیز از درخواست زیر استفاده مینماییم:
GET http://localhost/GetSalesTaxRate(PostalCode=10) HTTP/1.1
یک مثال از نوع response درخواست فوق
HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0
Date: Sat, 28 Jun 2016 01:05:32 GMT
Content-Length: 82

{
  "@odata.context":"http://localhost:38479/$metadata#Edm.Double","value":5.6
}

شاید سؤالی برایتان پیش بیاید که آیا برای تعریف هر متد، این همه مراحل کانفیگ لازم است؟! در واقع باید عرض کنم، این نوع استفاده از OData، ابتدایی‌ترین نوع طراحی آن میباشد و قطعا در یک برنامه‌ی واقعی این همه کد نویسی برای نوشتن فقط یک متد، شاید منطقی به نظر نمیرسد. از آنجاییکه این مقاله فقط جنبه‌ی آموزشی خیلی ساده از این پروتکل را دارد، فعلا به همین اندازه بسنده میکنیم. اما در مقاله‌های بعدی راه‌حل‌هایی برای بینهایت ساده کردن کانفیگ OData را شرح خواهیم داد.