مطالب
امکان تعریف ساده‌تر خواص Immutable در C# 9.0 با معرفی ویژگی خواص Init-Only
نگاهی به روند تکاملی نحوه‌ی تعریف خواص از C# 1.0 تا C# 9.0

در C# 1.0 برای تعریف خواص، نیاز به نوشتن مقدار زیادی کد بود:
public class Person 
{ 
    public string _firstName; 
 
    public string FirstName 
    { 
        get 
        { 
            return _firstName; 
        } 
        set 
        { 
            _firstName = value; 
        } 
    }  
}
در اینجا تعریف backing field‌ها (مانند public string _firstName) و استفاده‌ی دستی از آن‌ها الزامی بود.

در C# 2.0 از لحاظ ساده سازی این تعاریف، اتفاق خاصی رخ‌نداد. فقط امکان تعریف سطوح دسترسی مانند private بر روی getter‌ها و setter‌ها میسر شد:
public string _firstName; 
public string FirstName 
{ 
    get 
    { 
        return _firstName; 
    } 
    private set 
    { 
        _firstName = value; 
    } 
}

در C# 3.0 بود که با ارائه‌ی auto-implemented properties، نحوه‌ی تعریف خواص، بسیار ساده شد و دیگر نیازی به تعریف backing field‌ها نبود؛ چون کامپایلر به صورت خودکار آن‌ها را در پشت صحنه ایجاد می‌کرد/می‌کند:
public class Person
{
   public string FirstName { get; set; }
}

در C# 6.0، امکان حذف private setter‌ها از تعریف یک خاصیت میسر شد. یعنی مثال زیر را
public class User
{
   public string Name { get; private set; }
}
به این نحو ساده‌تر و واضح‌تر نیز می‌توان نوشت:
public class User
{
   public string Name { get; }
}
به‌علاوه در همین زمان بود که امکان مقدار دهی اولیه‌ی خواص نیز در همان سطر تعریف آن‌ها ممکن شد:
public class Foo
{
   public string FirstName { get; set; } = "Initial Value";
}
پیش از این برای مقدار دهی اولیه‌ی خواص در همان کلاسی که آن‌ها را تعریف می‌کند، می‌بایستی از طریق مقدار دهی آن‌ها در سازنده‌ی کلاس اقدام می‌شد.

همچنین در C# 6.0 با معرفی expression bodied members که بر روی خواص نیز قابل اعمال است، امکان تعریف خواص readonly محاسبه شده‌ی بر اساس مقدار سایر خواص نیز میسر شد:
public class Foo
{  
   public DateTime DateOfBirth { get; set; }
   public int Age => DateTime.Now.Year - DateOfBirth.Year;  
}

و در C# 9.0، با معرفی واژه‌ی کلیدی init، امکان تعریف ساده‌تر خواص immutable ممکن شد‌ه‌است که در مطلب جاری به آن خواهیم پرداختیم.


روش غیرقابل مقدار دهی کردن خواص، در نگارش‌ها پیش از C# 9.0

در بسیاری از موارد می‌خواهیم که خاصیتی از یک کلاس مدل، در خارج از آن قابل تغییر نباشد (مانند خواص شیء‌ای که به محتوای فایل config ثابت برنامه اشاره می‌کند). راه حل فعلی آن تا پیش از C# 9.0 به صورت زیر است:
public class User
{
   public string Name { get; private set; }
}
که در این حالت دیگر نمی‌توان مقدار خاصیت Name را در خارج از کلاس User مقدار دهی کرد:
var user = new User
{
   Name = "User 1" // Compile Error
};
وبا اینکار خطای کامپایلر زیر را دریافت می‌کنیم:
The property or indexer 'User.Name' cannot be used in this context
because the set accessor is inaccessible [CS9Features]csharp(CS0272)
در این تعریف باتوجه به وجود private set، برای مقداردهی خاصیت Name می‌توان از یکی از دو روش زیر در داخل کلاس User استفاده کرد:
- تنظیم مقدار خاصیت Name در سازنده‌ی کلاس
- و یا تنظیم این مقدار در یک متد ثالث دیگر مانند SetName
public class User
{
  public User(string name)
  {
    this.Name = name;
  }

  public void SetName(string name)
  {
    this.Name = name;
  }

  public string Name { get; private set; }
}
در هر دو حالت، از مقدار دهی مستقیم خاصیت Name توسط Object Initializer (یا همان روش متداول new User { Name = "some name"}) محروم می‌شویم. همچنین در ادامه شاید نیاز باشد که این خاصیت پس از مقدار دهی اولیه، دیگر قابل تغییر نباشد؛ یا به عبارتی immutable شود. در مثال فوق هنوز هم امکان تغییر مقدار خاصیت Name درون کلاس User، با فراخوانی‌های بعدی متد SetName، وجود دارد.


معرفی خواص Init-Only در C# 9.0

برای رفع دو مشکل یاد شده (امکان تنظیم مقدار خاصیت‌ها با همان روش متداول object initializer و همچنین غیرقابل تغییر شدن آن‌ها)، اکنون در C# 9.0 می‌توان بجای private set از واژه‌ی کلیدی init استفاده کرد:
public class User
{
   public string Name { get; init; }
}
در اینجا تنها تغییر صورت گرفته، استفاده از واژه‌ی کلیدی init، در حین تعریف خاصیت Name است. به این ترتیب به دو مزیت زیر دسترسی پیدا می‌کنیم:
الف) امکان مقدار دهی خاصیت Name، در خارج بدنه‌ی کلاس User و توسط روش متداول کار با object initializer‌ها هنوز هم وجود دارد و در این حالت الزامی به تعریف یک سازنده و یا متد خاصی درون کلاس User برای مقدار دهی آن نیست:
var user = new User
{
   Name = "User 1"
};
ب) پس از اولین بار مقدار دهی این خاصیت init-only، دیگر نمی‌توان مقدار آن‌را تغییر داد:
// Compile Time Error
// Init-only property or indexer 'User.Name' can only be assigned in an object initializer,
// or on 'this' or 'base' in an instance constructor or an 'init' accessor. [CS9Features]csharp(CS8852)
user.Name = "Test";
این نکته در مورد متدهای داخل کلاس User هم صدق می‌کند:
public class User
{
   public string Name { get; init; }

   public User(string name)
   {
     this.Name = name; // Works fine
   }

   public void SetName(string name)
   {
     this.Name = name; // Compile Time Error
   }
}
می‌توان یک خاصیت init-only را برای بار اول، در سازنده‌ی همان کلاس نیز مقدار دهی کرد؛ اما مقدار دهی ثانویه‌ی آن در سایر متدهای داخل کلاس User نیز به خطای زمان کامپایل یاد شده، ختم می‌شود و مجاز نیست.


روش تعریف immutable properties در نگارش‌های پیشین #C

با استفاده از واژه‌ی readonly در نگارش‌های قبلی #C نیز می‌توان به صورت زیر، یک خاصیت را به صورت غیرقابل تغییر یا immutable در آورد:
    public class Product
    {
        public Product(string name)
        {
            _name = name;
        }

        private readonly string _name;

        public string Name => _name;
    }
هرچند این روش کار می‌کند اما دیگر همانند init-only properties نمی‌توان از طریق object initializers خاصیت Name را مقدار دهی کرد و این مقدار دهی حتما باید از طریق سازنده‌ی کلاس باشد. همچنین ایجاد یک اصطلاحا backing filed هم برای آن، کدها را طولانی‌تر می‌کند.

یک نکته: امکان استفاده‌ی از فیلدهای readonly با خواص init-only هم وجود دارد؛ از این جهت که این نوع خواص تنها در زمان نمونه سازی اولیه‌ی شیء، اجرا و مقدار دهی می‌شوند، با مفهوم readonly، سازگاری دارند:
    public class Person
    {
        private readonly string _name;

        public string Name
        {
            get => _name;
            init => _name = value;
        }
    }
مطالب
بررسی تغییرات Blazor 8x - قسمت چهاردهم - امکان استفاده از کامپوننت‌های Blazor در برنامه‌های ASP.NET Core 8x
ASP.NET Core 8x به همراه یک IResult جدید به‌نام RazorComponentResult است که توسط آن می‌توان در Endpoint‌های Minimal-API و همچنین اکشن متدهای MVC، از کامپوننت‌های Blazor، خروجی گرفت. این خروجی نه فقط static یا به عبارتی SSR، بلکه حتی می‌تواند تعاملی هم باشد. در این مطلب، جزئیات فعالسازی و استفاده از این IResult جدید را در یک برنامه‌ی Minimal-API بررسی می‌کنیم.


ایجاد یک برنامه‌ی Minimal-API جدید در دات نت 8

پروژه‌ای را که در اینجا پیگیری می‌کنیم، بر اساس قالب استاندارد تولید شده‌ی توسط دستور dotnet new webapi تکمیل می‌شود.


ایجاد یک صفحه‌ی Blazor 8x به همراه مسیریابی و دریافت پارامتر

در ادامه قصد داریم که یک کامپوننت جدید را به نام SsrTest.razor در پوشه‌ی جدید Components\Tests ایجاد کرده و برای آن مسیریابی از نوع page@ هم تعریف کنیم. یعنی نه‌فقط قصد داریم آن‌را توسط RazorComponentResult رندر کنیم، بلکه می‌خواهیم اگر آدرس آن‌را در مرورگر هم وارد کردیم، قابل دسترسی باشد.
به همین جهت یک پوشه‌ی جدید را به نام Components در ریشه‌ی پروژه‌ی Web API جاری ایجاد می‌کنیم، با این محتوا:
برای ایده گرفتن از محتوای مورد نیاز، به «معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8» قسمت دوم این سری مراجعه کرده و برای مثال قالب ساده‌ترین حالت ممکن را توسط دستور زیر تولید می‌کنیم (در یک پروژه‌ی مجزا، خارج از پروژه‌ی جاری):
dotnet new blazor --interactivity None
پس از اینکار، محتویات پوشه‌ی Components آن‌را مستقیما داخل پوشه‌ی پروژه‌ی Minimal-API جاری کپی می‌کنیم. یعنی در نهایت در این پروژه‌ی جدید Web API، به فایل‌های زیر می‌رسیم:
- فایل Imports.razor_ ساده شده برای سهولت کار با فضاهای نام در کامپوننت‌های Blazor (فضاهای نامی را که در آن وجود ندارند و مرتبط با پروژه‌ی دوم هستند، حذف می‌کنیم).
- فایل App.razor، برای تشکیل نقطه‌ی آغازین برنامه‌ی Blazor.
- فایل Routes.razor برای معرفی مسیریابی صفحات Blazor تعریف شده.
- پوشه‌ی Layout برای معرفی فایل MainLayout.razor که در Routes.razor استفاده شده‌است.

و ... یک فایل آزمایشی جدید به نام Components\Tests\SsrTest.razor با محتوای زیر:
@page "/ssr-page/{Data:int}"

<PageTitle>An SSR component</PageTitle>

<h1>An SSR component rendered by a Minimal-API!</h1>

<div>
    Data: @Data
</div>

@code {

    [Parameter]
    public int Data { get; set; }

}
این فایل، می‌تواند پارامتر Data را از طریق فراخوانی مستقیم آدرس فرضی http://localhost:5227/ssr-page/2 دریافت کند و یا ... از طریق خروجی جدید RazorComponentResult که توسط یک Endpoint سفارشی ارائه می‌شود:




تغییرات مورد نیاز در فایل Program.cs برنامه‌ی Web-API برای فعالسازی رندر سمت سرور Blazor

در ادامه کل تغییرات مورد نیاز جهت اجرای این برنامه را مشاهده می‌کنید:
var builder = WebApplication.CreateBuilder(args);

// ...

builder.Services.AddRazorComponents();

// ...

// http://localhost:5227/ssr-component?data=2
// or it can be called directly http://localhost:5227/ssr-page/2
app.MapGet("/ssr-component",
           (int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               return new RazorComponentResult<SsrTest>(parameters);
           });

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>();
app.Run();

// ...
توضیحات:
- همین اندازه تغییر در جهت فعالسازی رندر سمت سرور کامپوننت‌های Blazor در یک برنامه‌ی ASP.NET Core کفایت می‌کند. یعنی اضافه شدن:
AddRazorComponents ،UseAntiforgery و MapRazorComponents
- در اینجا نحوه‌ی ارسال پارامترها را به یک RazorComponentResult نیز مشاهده می‌کنید.
- در حالت فراخوانی از طریق مسیر endpoint (یعنی فراخوانی مسیر http://localhost:5227/ssr-component در مثال فوق)، خود کامپوننت فراخوانی شده، بدون layout تعریف شده‌ی در فایل App.razor، رندر می‌شود. علت اینجا است که layout برنامه به همراه کامپوننت Router و RouteView آن فعال می‌شود که این دو هم مختص به صفحات دارای مسیریابی Blazor هستند و برای رندر کامپوننت‌های خالص آن بکار گرفته نمی‌شوند. خروجی RazorComponentResult تنها یک static SSR خالص است؛ مگر اینکه فایل blazor.web.js را نیز بارگذاری کند.

یک نکته: اگر در حالت رندر توسط RazorComponentResult، علاقمند به استفاده‌ی از layout هستید، می‌توان از کامپوننت LayoutView داخل یک کامپوننت فرضی به صورت زیر استفاده کرد؛ اما این مورد هم شامل اطلاعات فایل App.razor نمی‌شود:
<LayoutView Layout="@typeof(MainLayout)">
    <PageTitle>Home</PageTitle>

    <h2>Welcome to your new app.</h2>
</LayoutView>


سؤال: آیا در این حالت کامپوننت‌های تعاملی هم کار می‌کنند؟

پاسخ: بله. فقط برای ایده گرفتن، یک نمونه پروژه‌ی تعاملی Blazor 8x را در ابتدا ایجاد کنید و قسمت‌های اضافی AddRazorComponents و MapRazorComponents آن‌را در اینجا کپی کنید؛ یعنی برای مثال جهت فعالسازی کامپوننت‌های تعاملی Blazor Server، به این دو تغییر زیر نیاز است:
// ...

builder.Services.AddRazorComponents()
       .AddInteractiveServerComponents();

// ...

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

// ...
همچنین باید دقت داشت که امکانات تعاملی، به دلیل وجود و دسترسی به یک سطر ذیل که در فایل Components\App.razor واقع شده، اجرایی می‌شوند:
<script src="_framework/blazor.web.js"></script>
و همانطور که عنوان شد، اگر از روش new RazorComponentResult استفاده می‌شود، باید این سطر را به صورت دستی اضافه‌کرد؛ چون به همراه رندر layout تعریف شده‌ی در فایل App.razor نیست. برای مثال فرض کنید کامپوننت معروف Counter را به صورت زیر داریم که حالت رندر آن به InteractiveServer تنظیم شده‌است:
@rendermode InteractiveServer

<h1>Counter</h1>

<p role="status">Current count: @_currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int _currentCount;

    private void IncrementCount()
    {
        _currentCount++;
    }

}
در این حالت پس از تعریف endpoint زیر، خروجی آن فقط یک صفحه‌ی استاتیک SSR خواهد بود و دکمه‌ی Click me آن کار نمی‌کند:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<Counter>());
علت اینجا است که اگر به سورس HTML رندر شده مراجعه کنیم، خبری از درج اسکریپت blazor.web.js در انتهای آن نیست. به همین جهت برای مثال فایل جدید CounterInteractive.razor را به صورت زیر اضافه می‌‌کنیم که ساختار آن شبیه به فایل App.razor است:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive server component</title>
    <base href="/"/>
</head>
<body>
   <h1>Interactive server component</h1>

   <Counter/>

  <script src="_framework/blazor.web.js"></script>
</body>
</html>
و هدف اصلی از آن، تشکیل یک قالب و درج اسکریپت blazor.web.js در انتهای آن است.
سپس تعریف endpoint متناظر را به صورت زیر تغییر می‌دهیم:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<CounterInteractive>());
اینبار به علت بارگذاری فایل blazor.web.js، امکانات تعاملی کامپوننت Counter فعال شده و قابل استفاده می‌شوند.


سؤال: آیا می‌توان این خروجی static SSR کامپوننت‌های بلیزر را در سرویس‌های یک برنامه ASP.NET Core هم دریافت کرد؟

منظور این است که آیا می‌توان از یک کامپوننت Blazor، به همراه تمام پیشرفت‌های Razor در آن که در Viewهای MVC قابل دسترسی نیستند، به‌شکل یک رشته‌ی خالص، خروجی گرفت و برای مثال از آن به‌عنوان قالب پویای محتوای ایمیل‌ها استفاده کرد؟
پاسخ: بله! زیر ساخت RazorComponentResult که از سرویس HtmlRenderer استفاده می‌کند، بدون نیاز به برپایی یک endpoint هم قابل دسترسی است:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace WebApi8x.Services;

public class BlazorStaticRendererService
{
    private readonly HtmlRenderer _htmlRenderer;

    public BlazorStaticRendererService(HtmlRenderer htmlRenderer) => _htmlRenderer = htmlRenderer;

    public Task<string> StaticRenderComponentAsync<T>() where T : IComponent
        => RenderComponentAsync<T>(ParameterView.Empty);

    public Task<string> StaticRenderComponentAsync<T>(Dictionary<string, object?> dictionary) where T : IComponent
        => RenderComponentAsync<T>(ParameterView.FromDictionary(dictionary));

    private Task<string> RenderComponentAsync<T>(ParameterView parameters) where T : IComponent =>
        _htmlRenderer.Dispatcher.InvokeAsync(async () =>
                                             {
                                                 var output = await _htmlRenderer.RenderComponentAsync<T>(parameters);
                                                 return output.ToHtmlString();
                                             });
}
برای کار با آن، ابتدا باید سرویس فوق را به صورت زیر ثبت و معرفی کرد:
builder.Services.AddScoped<HtmlRenderer>();
builder.Services.AddScoped<BlazorStaticRendererService>();
و سپس یک نمونه مثال فرضی نحوه‌ی تزریق و فراخوانی سرویس BlazorStaticRendererService به صورت زیر است که در آن روش ارسال پارامترها هم بررسی شده‌است:
app.MapGet("/static-renderer-service-test",
           async (BlazorStaticRendererService renderer, int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               var html = await renderer.StaticRenderComponentAsync<SsrTest>(parameters);
               return Results.Content(html, "text/html");
           });

کدهای کامل این مطلب را می‌توانید از اینجا دریافت کنید: WebApi8x.zip
مطالب
پشتیبانی از Generic Attributes در C# 11
هر کلاسی در #C که از کلاس پایه‌ی System.Attribute مشتق شود، یک Attribute نامیده می‌شود و مهم‌ترین و هدف و کاربرد آن‌ها، مزین کردن و علامتگذاری سایر نوع‌ها و فیلدها هستند تا بر اساس آن‌ها بتوان کارکردهای بیشتری را در اختیار آن نوع‌ها قرار داد. برای مثال، استفاده از  ویژگی‌‌های JsonProperty و یا JsonPropertyName در حین اعمال serializations و یا در کاربردهای اعتبارسنجی مانند ویژگی‌های Required، Range و امثال آن‌ها:
public class Student
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("name")]
    public string Name { get; set; }
}

public class WeatherForecast
{
    [Required]
    public int TemperatureC { get; set; }

    [MinLength(50)]
    public string Summary { get; set; }
}

روش متداول ارسال نوع‌ها به attributes تا پیش از C# 11

تا پیش از C# 11، روش پیاده سازی یک attribute جنریک که بتواند با انواع و اقسام نوع‌ها کار کند، به صورت زیر است:
- ارسال یک پارامتر از نوع System.Type به سازنده‌ی attribute
- تعریف خاصیتی مانند ParamType در صورت نیاز؛ تا مشخص کند که چه نوعی به سازنده‌ی attribute ارسال شده‌است. مانند مثال فرضی زیر:
[AttributeUsage(AttributeTargets.Class)]
public class CustomDoNothingAttribute: Attribute
{
    // Note the type parameter in the constructor
    public CustomDoNothingAttribute(Type t)
    {
        ParamType = t;
    }

    public Type ParamType { get; }
}
و سپس با استفاده از عملگر typeof، نوع مدنظر را به سازنده‌ی ویژگی تعریف شده، ارسال می‌کنیم:
[CustomDoNothing(typeof(string))]
public class Student
{
    public int Id { get; set; }

    public string Name { get; set; }
}
یک نمونه مثال دنیای واقعی آن، [ServiceFilter(typeof(ResponseLoggerFilter))] در ASP.NET Core است که دیگر با وجود جنریک‌ها، آنچنان هماهنگ و یک‌دست با سایر اجزای زبان به نظر نمی‌رسد. نمونه‌ی هماهنگ با پیشرفت‌های زبان، باید چنین شکلی را داشته باشد: [<ServiceFilter<ResponseLoggerFilter]


امکان تعریف ویژگی‌های جنریک در C# 11

‍C# 11 به همراه پیشتیبانی از generic attributes ارائه شده‌است. بنابراین اینبار بجای ارسال پارمتری از نوع Type به سازنده‌ی ویژگی‌، می‌توان کلاس آن attribute را به صورت جنریک تعریف کنیم که می‌تواند یک یا چندین نوع را به عنوان پارامتر بپذیرد. بنابراین مثال قبل در C# 11 به صورت زیر بازنویسی می‌شود:
[AttributeUsage(AttributeTargets.Class)]
public class CustomDoNothingAttribute<T> : Attribute
    where T : class
{
    public T ParamType { get; }
}

[CustomDoNothing<string>]
public class Student
{
    public int Id { get; set; }

    public string Name { get; set; }
}
یک مزیت مهم این روش نسبت به قبل، امکان تعریف قیود و type safety است. برای نمونه در مثال فوق، نوع T به کلاس‌ها محدود شده‌است و نوع‌های دیگر را نمی‌پذیرد. به این ترتیب می‌توان این نوع بررسی‌ها را بجای زمان اجرا و صدور استثناءها، دقیقا در زمان کامپایل انجام داد.
و اگر نیاز به تعیین چند نوع بود، باید خاصیت AllowMultiple نحوه‌ی استفاده از ویژگی را به true تنظیم کرد:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DecorateAttribute<T> : Attribute where T : class
{
    // ....
}
تا بتوان به تعریف زیر رسید:
[Decorate<LoggerDecorator>]
[Decorate<TimerDecorator>]
public class SimpleWorker
{
    // ....
}


محدودیت‌های انتخاب نوع‌ها در ویژگی‌های جنریک C# 11

در ویژگی‌های جنریک نمی‌توان از نوع‌های زیر استفاده کرد (همان محدودیت‌های typeof، در اینجا هم برقرار هستند):
- نوع‌های dynamic
- nullable reference types مانند ?string
- نوع‌های tuple تعریف شده‌ی به کمک C# tuple syntax مانند (int x, int y)

چون این نوع‌ها به همراه یکسری metadata annotations هستند که صرفا بیانگر توضیحی اضافی در مورد نوع بکارگرفته شده هستند و در صورت نیاز، بجای آن‌ها می‌توانید از نوع‌های زیر استفاده کنید:
- از object بجای dynamic
- از string بجای ?string
- از <ValueTuple<int, int بجای (int X, int Y)

همچنین در زمان استفاده‌ی از یک ویژگی جنریک، باید نوع مورد استفاده، کاملا مشخص و در اصطلاح fully constructed باشد:
public class GenericAttribute<T> : Attribute { }

public class GenericType<T>
{
    [GenericAttribute<T>] // Not allowed! generic attributes must be fully constructed types.
    public string Method1() => default;

    [GenericAttribute<string>]
    public string Method2() => default;
}
مطالب
C# 6 - Expression-Bodied Members
در ادامه مطالب منتشر شده در رابطه با قابلیت‌های جدید سی‌شارپ 6، در این مطلب به بررسی یکی دیگر از این قابلیت‌ها، با نام Expression-Bodied Members خواهیم پرداخت. در واقع در سی‌شارپ 6، هدف، ساده‌سازی سینتکس و افزایش بهره‌وری برنامه‌نویس می‌باشد. در نسخه‌های قبلی سی‌شارپ برای یکسری از اعمال روتین می‌بایستی روالی‌هایی را مدام تکرار می‌کردیم؛ به عنوان مثال در تعریف پراپرتی‌های یک کلاس در حالت get-only باید هر بار توسط return مقداری را برگردانیم:
public class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string FullName
   {
       get
       {
                return FirstName + " " + LastName;
       }
   }
}
نوشتن پراپرتی‌هایی همانند FullName منجر به نوشتن خطوط کد اضافه‌تری خواهد شد، هرچند می‌توان این حالت را با برداشتن خطوط اضافی بهبود بخشید:
public string FullName
{
       get { return FirstName + " " + LastName; }
}
اما در سی‌شارپ 6 میتوان آن را توسط expression body به یک خط کاهش داد!

استفاده از expression body برای پراپرتی‌های get-only (فقط خواندنی):

اگر در کلاس‌هایتان پراپرتی‌های get-only دارید، به راحتی می‌توانید بدنه‌ی پراپرتی را با استفاده از expression syntax خلاصه‌نویسی کنید. در واقع شما با استفاده از سینتکس lambda expression اقدام به نوشتن بدنه پراپرتی‌های موردنظرتان می‌کنید. یعنی به جای نوشتن کدی مانند:
{ get { return your expression; } }
به راحتی می‌توانید از سینتکس زیر استفاده نمائید:
=> your expression;
به عنوان مثال، میتوان پراپرتی FullName را در کلاس Person با کمک قابلیت expression body به صورت زیر بازنویسی کنیم:
public class Person
{
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public string FullName => FirstName + " " + LastName;
}
با کد فوق به راحتی توانستیم قسمت‌های اضافه‌ای را حذف کنیم. اکنون ممکن است بپرسید آیا این تغییر در performance برنامه تاثیری دارد؟ خیر؛ زیرا سینتکس فوق دقیقاً همان کد ILی را تولید خواهد کرد که در حالت عادی تولید می‌شود. همچنین delegateی را تولید نخواهد کرد؛ بلکه تنها از سینتکس lambda expression برای خلاصه‌نویسی بدنه پراپرتی استفاده می‌کند. در حال حاضر برای حالت setter سینتکسی ارائه نشده است.

استفاده از expression body برای Indexerها: 

همچنین از این قابلیت برای Indexerها نیز میتوان استفاده کرد، مثلاً به جای نوشتن کد زیر:
public string this[int number]
{
            get
            {
                if (number >= 0 && number < _values.Length)
                {
                    return _values[number];
                }
                return "Error";
            }
}
می‌توانیم کد فوق را به این صورت خلاصه‌نویسی کنیم:
public string this[int number] => (number >= 0 && number < _values.Length) ? _values[number] : "Error";
نکته: توجه داشته باشید که در هر دو حالت فوق تنها می‌توانیم برای get از expression body استفاده کنیم، هنوز سینتکسی برای حالت set ارائه نشده است.

استفاده از expression body برای متدها: 

برای متدها نیز می‌توانیم از قابلیت عنوان شده استفاده نمائیم، به عنوان مثال اگر داخل کلاس Person متد زیر را داشته باشیم:
public override string ToString()
{
      return FirstName;
}
می‌توانیم آن را به صورت زیر بنویسیم:
public override string ToString() => FirstName;
همانطور که مشاهده می‌کنید به جای نوشتن curly braces یا {} از lambda arrow یا <= استفاده کرده‌ایم. در اینجا عبارت سمت راست lambda arrow نمایانگر بدنه‌ی متد است. همچنین برای متدهای دارای پارامتر نیز به این صورت عمل می‌کنیم:
public int DoubleTheValue(int someValue) => someValue * 2;
یک عضو از کلاس که به صورت expression body نوشته شده باشد، expression bodied member نامیده می‌شود. این عضو از کلاس در ظاهر شبیه به عبارات لامبدای ناشناس (anonymous lambda expression) است. اما یک expression bodied member باید دارای نام، مقدار بازگشتی و بدنه متد باشد. 
تقریباً تمامی access modifierها در این حالت قابلیت استفاده را دارند. تنها متدهای abstract نمی‌توانند استفاده شوند.

محدودیت‌های Expression Bodied Members 
  • یکی از محدودیت‌های استفاده از expression body داشتن چندین خط دستور برای بدنه متدهایمان است. در اینحالت باید از روش سابق (statement body) استفاده نمائید. 
  • یکی دیگر از محدودیت‌ها عدم امکان استفاده از if, else, switch است. به عنوان مثال نمی‌توان کد زیر را با داشتن if و else به صورت expression body نوشت:
public override string ToString()
{
       if (MiddleName != null)
       {
                return FirstName + " " + MiddleName + " " + LastName;
       }
       else
       {
                return FirstName + " " + LastName;
       }
}
برای حالت فوق به عنوان یک روش جایگزین می‌توان از conditional operator استفاده کرد:
public override string ToString() =>
                    (MiddleName != null)
                    ? FirstName + " " + MiddleName + " " + LastName
                    : FirstName + " " + LastName;
  • همچنین نمی‌توان از for, foreach, while, do در expression body استفاده کرد، به جای آن می‌توان از عبارت‌های LINQ برای بدنه تابع استفاده کرد. به عنوان مثال متد زیر:
public IEnumerable<int> SmallNumbers()
{
    for (int i = 0; i < 10; i++)
        yield return i;
}
را می‌توان در حالت expression body به این صورت نوشت:
public IEnumerable<int> SmallNumbers() => from n in Enumerable.Range(0, 10)
                                                                         select n;
و یا به این صورت:
public IEnumerable<int> SmallNumbers() => Enumerable.Range(0, 10).Select(n => n);
  • همانطور که عنوان شد، استفاده از expression body در قسمت پراپرتی‌ها تنها محدود به پراپرتی‌های get-only (فقط خواندنی) میباشد.
  • استفاده از این قابلیت برای متدهای سازنده
  • استفاده در رخدادها
  • استفاده در finalizers
نکته: اگر می‌خواهید expression bodied member شما هم initializer داشته باشد و همچنین یک read only auto property باشد، باید مقداری سینتکس آن را تغییر دهید. همانطور که می‌دانید auto propertyها نیازی به backing field ندارند؛ بلکه در زمان کامپایل به صورت خودکار تولید خواهند شد. در نتیجه برای مقداردهی اولیه به backing fieldها می‌توانیم درون سازنده کلاس آنها را initialize کنیم:
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person()
        {
            this.FirstName = "Sirwan";
            this.LastName = "Afifi";
        }
    }
برای نوشتن پراپرتی‌های فوق به صورت expression body می‌توانیم به این صورت عمل کنیم:
public string FirstName { get; set; } = "Sirwan";
public string LastName { get; set; } = "Afifi";
اگر ReSharper را نصب کرده باشید، به شما پیشنهاد می‌دهد که از expression body استفاده نمائید: :
برای حالت فوق:

برای پراپرتی‌ها:



مطالب
FluentValidation #1
FluentValidation یک پروژه سورس باز برای اعتبارسنجی Business Object‌ها با استفاده از Fluent Interface و Lambada Expressions می‌باشد.
جهت نصب این کتابخانه دستور زیر را در Package Manager Console وارد نمایید:
PM> Install-Package FluentValidation

ایجاد یک Validator
برای تعریف مجموعه قوانین اعتبارسنجی برای یک موجودیت ابتدا بایستی یک کلاس ایجاد کرد که از AbstractValidator<T> مشتق می‌شود که T در اینجا برابر موجودیتی است که می‌خواهیم اعتبارسنجی کنیم. به عنوان مثال کلاس مشتری به صورت زیر را در نظر بگیرید:
public class Customer
{
      public int Id { get; set; }
      public string Surname { get; set; }
      public string Forename { get; set; }
      public decimal Discount { get; set; }
      public string Address { get; set; }
}
مجموعه قوانین اعتبارسنجی با استفاده از متد RuleFor و داخل متد سازنده کلاس Validator تعریف می‌شوند. به عنوان مثال برای اطمینان از اینکه مقدار خاصیت Surname برابر Null نباشد باید به صورت زیر عمل کرد:
using FluentValidation;

public class CustomerValidator : AbstractValidator<Customer>
{
      public CustomerValidator
      {
           RuleFor(customer => customer.Surname).NotNull();
      }
}


اعتبارسنجی زنجیره ای برای یک خاصیت

برای اعتبارسنجی یک خاصیت، می‌توان از چندین Validator باهم نیز استفاده کرد:
RuleFor(customer => customer.Surname).NotNull().NotEqual("foo");
در اینجا خاصیت Surname نباید Null باشد و همچنین مقدار آن نباید برابر "Foo" باشد.
برای اجراکردن اعتبارسنجی، ابتدا یک نمونه از کلاس Validator مان را ساخته و شیء ای را که می‌خواهیم اعتبارسنجی کنیم به متد Validate آن میفرستیم:
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
ValidationResult results = validator.Validate(customer);

خروجی متد Validate، یک ValidationResult است که شامل دو خاصیت زیر می‌باشد:

  • IsValid: از نوع bool برای تعیین اینکه اعتبارسنجی موفقیت آمیز بوده یا خیر.
  • Errors: یک مجموعه از ValidationFailure که جزئیات تمام اعتبارسنجی‌های ناموفق را شامل می‌شود.
به عنوان مثال قطعه کد زیر، جزئیات اعتبارسنجی‌های ناموفق را نمایش می‌دهد:
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();

ValidationResult results = validator.Validate(customer);

if(! results.IsValid) 
{
     foreach(var failure in results.Errors)
     {
          Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " +      failure.ErrorMessage);
     }
}


پرتاب استثناها (Throwing Exceptions)

به جای برگرداندن ValidationResult شما میتوانید با کمک متد ValidateAndThrow به FluentValidation بگویید که هنگام اعتبارسنجی ناموفق یک استثنا پرتاب کند:
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
validator.ValidateAndThrow(customer);
در این صورت Validator یک ValidationException را پرتاب خواهد کرد که دربردارنده‌ی پیام‌های خطا در خاصیت Errors خود می‌باشد.

استفاده از Validator‌ها برای Complex Properties

جهت درک این ویژگی تصور کنید که کلاس‌های مشتری و آدرس و همچنین کلاس‌های مربوط به اعتبارسنجی آن‌ها را به صورت زیر داریم:
public class Customer
{
     public string Name { get; set; }
     public Address Address { get; set; }
}

public class Address 
{
     public string Line1 { get; set; }
     public string Line2 { get; set; }
     public string Town { get; set; }
     public string County { get; set; }
     public string Postcode { get; set; }
}

public class AddressValidator : AbstractValidator<Address>
{
     public AddressValidator() 
     {
          RuleFor(address => address.Postcode).NotNull();
          //etc
     }
}

public class CustomerValidator : AbstractValidator<Customer> 
{
     public CustomerValidator()
     {
          RuleFor(customer => customer.Name).NotNull();
          RuleFor(customer => customer.Address).SetValidator(new AddressValidator())
      }
}
در این صورت وقتی متد Validate کلاس CustomerValidator را فراخوانی نمایید AddressValidator نیز فراخوانی خواهد شد و نتیجه این اعتبارسنجی به صورت یکجا در یک ValidationResult برگشت داده خواهد شد.


استفاده از Validator‌ها برای مجموعه‌ها (Collections)

Validator‌ها همچنین می‌توانند بر روی خاصیت هایی که شامل مجموعه ای از یک شیء دیگر هستند نیز استفاده شوند. به عنوان مثال یک مشتری که دارای لیستی از سفارشات است را در نظر بگیرید:
public class Customer
{
     public IList<Order> Orders { get; set; }
}

public class Order 
{
     public string ProductName { get; set; }
     public decimal? Cost { get; set; }
}

var customer = new Customer();
customer.Orders = new List<Order> 
{
     new Order { ProductName = "Foo" },
     new Order { Cost = 5 } 
};
کلاس OrderValidator نیز به صورت زیر خواهد بود:
public class OrderValidator : AbstractValidator<Order>
{
     public OrderValidator() 
     {
          RuleFor(x => x.ProductName).NotNull();
          RuleFor(x => x.Cost).GreaterThan(0);
     }
}

این Validator می‌تواند داخل CustomerValidator مورد استفاده قرار بگیرد (با استفاده از متد SetCollectionValidator):

public class CustomerValidator : AbstractValidator<Customer>
{
     public CustomerValidator()
     {
          RuleFor(x => x.Orders).SetCollectionValidator(new OrderValidator());
     }
}

می توان با استفاده از متد Where یا Unless روی اعتبارسنجی شرط گذاشت:

RuleFor(x => x.Orders).SetCollectionValidator(new OrderValidator()).Where(x => x.Cost != null);


گروه بندی قوانین اعتبارسنجی

RuleSet‌‌ها به شما این امکان را می‌دهند تا بعضی از قوانین اعتبارسنجی را داخل یک گروه قرار دهید تا با یکدیگر اجرا شوند. در حالی که دیگر قوانین نادیده گرفته می‌شوند.
برای مثال تصور کنید شما سه خاصیت در کلاس Person دارید که شامل (Id, Surname, Forename) می‌باشند و همچنین یک قانون برای هرکدام از آن ها. میتوان قوانین مربوط به Surname و Forename را در یک RuleSet مجزا به نام Names قرار داد:
public class PersonValidator : AbstractValidator<Person>
{
     public PersonValidator() 
     {
          RuleSet("Names", () =>
          {
               RuleFor(x => x.Surname).NotNull();
               RuleFor(x => x.Forename).NotNull();
          });
 
          RuleFor(x => x.Id).NotEqual(0);
      }
}
در اینجا دو خاصیت Surname و Forename با یکدیگر داخل یک RuleSet به نام Names گروه شده اند. برای اعتبارسنجی جداگانه این گروه نیز به صورت زیر می‌توان عمل کرد:
var validator = new PersonValidator();
var person = new Person();
var result = validator.Validate(person, ruleSet: "Names");
این ویژگی به شما این امکان را می‌دهد تا یک Validator پیچیده را به چندین قسمت کوچکتر تقسیم کرده و توانایی اعتبارسنجی این قسمت‌ها را به صورت جداگانه داشته باشید.
مطالب
شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکس‌ها
پس از بررسی نحوه‌ی انجام تنظیمات اولیه‌ی کار با EF Core و همچنین آشنایی با مهاجرت‌های آن، مرحله‌ی بعد، مرحله‌ی مدلسازی داده‌ها است و اولین مرحله‌ی آن، نحوه‌ی تعیین کلید اصلی جداول است که در این زمینه، EF Core پیشرفت‌هایی قابل ملاحظه‌ای را نسبت به EF 6.x داشته‌است. در EF 6.x تنها دو حالت کلیدهای اصلی خود افزاینده که توسط بانک اطلاعاتی مدیریت می‌شوند و یا تولید کلید اصلی در سمت کلاینت و توسط برنامه، پشتیبانی می‌شوند. در EF Core، مواردی مانند Sequence و Alternate keys نیز اضافه شده‌اند.


پیش فرض‌های تعیین کلید اصلی در EF Core

به صورت پیش فرض هر خاصیتی که به نام Id و یا type name>Id> باشد، به عنوان primary key تفسیر خواهد شد؛ مانند:
public class Car
{
    public string Id { get; set; }
و یا
public class Car
{
   public string CarId { get; set; }
در مثال اول، نام خاصیت، Id است و در مثال دوم، جمع نام کلاس به همراه Id ذکر شده‌است. یک چنین مواردی، نیازی به تنظیم اضافه‌تری ندارند.


نحوه‌ی تعیین کلید اصلی به صورت صریح

اگر یکی از دو حالت فوق برقرار نباشند، باید کلید اصلی را به نحو صریحی مشخص کرد.
الف) از طریق ویژگی‌ها
public class Car
{
   [Key]
   public string LicensePlate { get; set; }
در اینجا چون LicensePlate نه Id نام دارد و نه جمع نام کلاس به همراه Id است، باید به نحو صریحی توسط ویژگی Key مشخص شود.
ب) با استفاده از روش Fluent API
public class MyContext : DbContext
{
    public DbSet<Car> Cars { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Car>()
                 .HasKey(c => c.LicensePlate);
    }
 }
روش تنظیم کلید اصلی به صورت صریح، از طریق کدنویسی است که به آن Fluent API یا API روان هم گفته می‌شود. برای اینکار باید متد OnModelCreating کلاس Context برنامه را بازنویسی کرد و سپس از طریق متد HasKey، نام خاصیت کلید اصلی را ذکر نمود.


پیشنیاز کار با ویژگی‌ها در EF Core

در اسمبلی که مدل‌های موجودیت‌ها شما قرار دارند، نیاز است وابستگی System.ComponentModel.Annotations به فایل project.json پروژه اضافه شود، تا ویژگی‌هایی مانند Key، شناسایی و قابل استفاده شوند:
{
   "dependencies": {
          "System.ComponentModel.Annotations": "4.1.0"
   }
}


تعیین کلید ترکیبی و یا Composite key

اگر نیاز است چندین خاصیت را به صورت کلید اصلی معرفی کرد که به آن composite key هم می‌گویند، تنها روش ممکن، استفاده از Fluent API و به صورت زیر است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Car>()
                       .HasKey(c => new { c.State, c.LicensePlate });
}
در قسمت HasKey می‌توان چندین خاصیت را نیز جهت تعیین کلید ترکیبی مشخص کرد.


روش‌های مختلف تولید خودکار مقادیر خواص

حالت پیش فرض تولید مقدار فیلدهای Id عددی، همان حالت خود افزاینده‌ای است که توسط بانک اطلاعاتی کنترل می‌شود و یا کلید اصلی که از نوع Guid تعیین شود نیز به صورت خودکار توسط بانک اطلاعاتی در حین عملیات Add، مقدار دهی می‌شود (با استفاده از الگوریتم Guid سری در SQL Server).
 اگر این حالات مطلوب شما نیست، حالت‌های سه گانه‌ی ذیل را می‌توان استفاده کرد:

الف) هیچ داده‌ی خودکاری تولید نشود
برای اینکار می‌توان با استفاده از ویژگی DatabaseGenerated و تنظیم مقدار آن به None، جلوی تولید خودکار کلید اصلی را گرفت. در این حالت باید هم در حین عملیات Add و هم در حین عملیات Update، مقادیر را خودتان مقدار دهی کنید:
public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int BlogId { get; set; }

    public string Url { get; set; }
}
و یا معادل این تنظیم با استفاده از Fluent API به صورت ذیل است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
           .Property(b => b.BlogId)
           .ValueGeneratedNever();
}

ب) تولید داده‌های خودکار فقط در حالت Add
حالت Add به این معنا است که داده‌های خواص مشخصی، برای موجودیت‌های «جدید»، به صورت خودکار تولید خواهند شد. اینکه آیا واقعا این مقادیر به صورت خودکار تولید می‌شوند یا خیر، صرفا وابسته‌است به بانک اطلاعاتی در حال استفاده. برای مثال SQL Server برای نوع‌های Guid، به صورت خودکار با کمک الگوریتم SQL Server sequential GUID، کار مقدار دهی یک چنین فیلدهایی را انجام می‌دهد.
این فیلدها باید توسط ویژگی DatabaseGenerated و با مقدار Identity مشخص شوند. در اینجا Identity به معنای فیلدهایی است که به صورت خودکار توسط بانک اطلاعاتی مقدار دهی می‌شوند و الزاما به کلید اصلی اشاره نمی‌کنند. برای مثال در موجودیت ذیل، خاصیت تاریخ ثبت رکورد، از نوع Identity مشخص شده‌است. به این معنا که در حین ثبت اولیه‌ی رکورد آن، نیازی نیست تا خاصیت Inserted را مقدار دهی کرد. اما اینکه آیا SQL Server یک چنین کاری را به صورت خودکار انجام می‌دهد، پاسخ آن خیر است. SQL server فقط برای فیلدهای عددی و Guid ایی که با DatabaseGeneratedOption.Identity مزین شده باشند، مقادیر متناظری را به صورت خودکار تولید می‌کند. برای حالت DateTime نیاز است، مقدار پیش فرض فیلد را صریحا مشخص کرد که توسط ویژگی‌ها میسر نیست و فقط fluent API از آن پشتیبانی می‌کند.
public class Blog
{
   public int BlogId { get; set; }
   public string Url { get; set; }

   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public DateTime Inserted { get; set; }
}
و یا معادل این تنظیم با استفاده از Fluent API به صورت ذیل است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
           .Property(b => b.Inserted)
           .ValueGeneratedOnAdd();
}
برای تعیین مقدار پیش فرض خاصیت Inserted به نحوی که توسط SQL Server به صورت خودکار مقدار دهی شود، می‌توان از متد HasDefaultValueSql به نحو ذیل استفاده کرد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Inserted)
        .HasDefaultValueSql("getdate()");
}
البته باید درنظر داشت که اگر خاصیت DateTime تعریف شده در اینجا به همین نحو بکاربرده شود، اگر مقداری برای آن در حین تعریف یک وهله جدید از کلاس Blog درکدهای برنامه درنظر گرفته نشود، یک مقدار پیش فرض حداقل به آن انتساب داده خواهد شد (چون value type است). بنابراین نیاز است این خاصیت را از نوع nullable تعریف کرد (public DateTime? Inserted).

یک نکته: در حالت DatabaseGeneratedOption.Identity و یا ValueGeneratedOnAdd فوق، اگر مقداری به این نوع فیلدها انتساب داده شده باشد که با مقدار پیش فرض آن‌ها (property.ClrType.GetDefaultValue) متفاوت باشد، از این مقدار جدید، بجای تولید مقداری خودکار، استفاده خواهد شد. برای مثال مقدار پیش فرض رشته‌ها، نال، مقادیر عددی، صفر و برای Guid مقدار Guid.Empty است. اگر هر مقدار دیگری بجای این‌ها به فیلدهای فوق انتساب داده شوند، از آن‌ها استفاده می‌شود.

ج) تولید داده‌های خودکار در هر دو حالت Add و Update
تولید داده‌ها در حالت‌های Add و Update به این معنا است که یک چنین خواصی، همواره با فراخوانی متد SaveChanges، دارای مقدار خودکار جدیدی خواهند شد و نیازی نیست در کدها مقدار دهی شوند. برای مشخص سازی این نوع خواص، از ویژگی DatabaseGenerated با مقدار Computed و یا متد ValueGeneratedOnAddOrUpdate در حالت Fluent API می‌توان استفاده کرد:
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime LastUpdated { get; set; }
}
و یا معادل این تنظیم با استفاده از Fluent API به صورت ذیل است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
       .Property(b => b.LastUpdated)
       .ValueGeneratedOnAddOrUpdate();
}
همانطور که پیشتر نیز عنوان شد، تولید خودکار مقادیر فیلدها فقط در حالت‌های int و Guid انجام می‌شود (که برای مثال SQL Server از آن‌ها پشتیبانی می‌کند). در مثال فوق، خاصیت LastUpdated از نوع DateTime اینگونه تعریف شده‌است و SQL Server برای یک چنین فیلدهای خاصی، مقدار خودکاری را تولید نکرده و به دنبال مقدار پیش فرض آن می‌گردد. بنابراین در اینجا نیز باید مشخص سازی HasDefaultValueSql("getdate()") را که در قسمت قبل عنوان کردیم، صراحتا در قسمت تنظیمات Fluent API ذکر و تنظیم کرد.

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


خواص محاسباتی (Computed Columns) و تفاوت آن‌ها با DatabaseGeneratedOption.Computed

خواص محاسباتی (Computed Columns)، خواصی هستند که مقادیر آن‌ها در بانک اطلاعاتی محاسبه می‌شوند و کاملا متفاوت هستند با DatabaseGeneratedOption.Computed که مفهوم دیگری دارد. DatabaseGeneratedOption.Computed به این معنا است که این فیلد خاص، با هر بار فراخوانی SaveChanges باید مقدار محاسبه شده‌ی جدیدی را داشته باشد و روش تولید این مقدار خودکار، یا بر اساس Guidهای سری است، یا توسط فیلدهای خود افزاینده‌ی عددی و یا از طریق مقادیر پیش فرضی مانند getdate در حین ثبت یا به روز رسانی، مقدار دهی می‌شوند. اما خواص محاسباتی، یکی از امکانات «گزارشگیری سریع» SQL Server هستند و به نحو ذیل، تنها توسط Fluent API قابل تنظیم می‌باشند:
public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string DisplayName { get; set; }
}

public class MyContext : DbContext
{
    public DbSet<Person> People { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
          modelBuilder.Entity<Person>()
              .Property(p => p.DisplayName)
               .HasComputedColumnSql("[LastName] + ', ' + [FirstName]");
     }
 }
در اینجا فیلد DisplayName یک فیلد محاسباتی بوده و از حاصل جمع دو فیلد دیگر در سمت دیتابیس تشکیل می‌شود. این نگاشت و محاسبه چون در سمت بانک اطلاعاتی انجام می‌شود، بازدهی بیشتری دارد نسبت به حالتی که ابتدا دو فیلد به کلاینت منتقل شده و سپس در این سمت جمع زده شوند.


امکان تعریف Sequence در EF Core 1.0

Sequence قابلیتی است که به SQL Server 2012 اضافه شده‌است و توضیحات بیشتر آن‌را در مطلب «نحوه ایجاد Sequence و استفاده آن در Sql Server 2012» می‌توانید مطالعه کنید.
در EF Core، امکان مدلسازی Sequence نیز پیش بینی شده‌است. آن‌ها به صورت پیش فرض در مدل‌ها ذکر نمی‌شوند و همچنین وابستگی به جدول خاصی ندارند. به همین جهت امکان تعریف آن‌ها صرفا توسط Fluent API وجود دارد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
     modelBuilder.HasSequence<int>("OrderNumbers", schema: "shared") 
           .StartsAt(1000).IncrementsBy(5);

     modelBuilder.Entity<Order>()
         .Property(o => o.OrderNo)
         .HasDefaultValueSql("NEXT VALUE FOR shared.OrderNumbers");
}
پس از اینکه یک Sequence  تعریف شد، می‌توان برای نمونه از آن جهت تولید مقادیر پیش فرض ستون‌ها استفاده کرد.
در مثال فوق، ابتدا یک Sequence نمونه به نام OrderNumbers تعریف شده‌است که از عدد 1000 شروع شده و واحد افزایش آن 5 است. سپس از این نام در قسمت مقدار پیش فرض ستون OrderNo استفاده شده‌است.

و یا از Sequence ‌ها می‌توان برای تعیین مقدار پیش فرض Primary key بجای حالت identity خود افزایش یابنده استفاده کرد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasSequence<int>("PrimaryKeyWithSequenceSequence");
    modelBuilder.Entity<PrimaryKeyWithSequence>(entity =>
     {
       entity.Property(e => e.PrimaryKeyWithSequenceId).HasDefaultValueSql("NEXT VALUE FOR [PrimaryKeyWithSequenceSequence]");
     });
}
در اینجا یک توالی از نوع int تعریف شده و سپس هربار که قرار است رکوردی درج شود، مقدار id آن به صورت خودکار از طریق کوئری Select NEXT VALUE FOR
[PrimaryKeyWithSequenceSequence] دریافت و سپس بجای فیلد id درج می‌شود.

به این روش الگوریتم Hi-Low هم می‌گویند که یکی از مهم‌ترین اهداف آن داشتن یک سری Id منحصربفرد، جهت بالابردن سرعت insertها در یک batch است. در حالت عادی insertها، ابتدا یک insert انجام می‌شود، سپس کوئری گرفته شده و آخرین Id درج شده به کلاینت بازگشت داده می‌شود. این روش، برای انجام تنها یک insert، سریع است. اما برای batch insert، به شدت کارآیی پایینی دارد. به همین جهت دسترسی به بازه‌ای از اعداد منحصربفرد، پیش از شروع به insert تعداد زیادی رکورد، سرعت نهایی کار را بالا می‌برد.


نحوه‌ی تعریف ایندکس‌ها در EF Core 1.0

برای افزودن ایندکس‌ها به EF Core 1.0، تنها روش میسر، استفاده از Fluent API است (و برخلاف EF 6.x از روش data annotations فعلا پشتیبانی نمی‌کند؛ هرچند API جدید آن نسبت به EF 6.x بسیار واضح‌تر است و با ابهامات کمتر).
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
      modelBuilder.Entity<Blog>()
          .HasIndex(b => b.Url)
          .HasName("Index_Url");
اگر قسمت HasName را ذکر نکنید، نام آن <IX_<type name>_<property name درنظر گرفته می‌شود و برای اینکه ایندکس منحصربفردی را تعریف کنید، می‌توان متد IsUnique را به انتهای این زنجیره اضافه کرد:
 modelBuilder.Entity<Blog>().HasIndex(b => b.Url).HasName("Index_Url").IsUnique();
همچنین می‌توان همانند composite keys، در اینجا نیز ترکیبی از خواص را به صورت یک ایندکس معرفی نمود:
modelBuilder.Entity<Person>()
   .HasIndex(idx => new { idx.FirstName, idx.LastName })
   .IsUnique();
در این حالت اگر HasName ذکر نشود، نام آن همانند الگویی است که پیشتر عنوان شد؛ با این تفاوت که قسمت property name آن، جمع نام تمام خواص ذکر شده و جدا شده‌ی با _ خواهد بود.

یک نکته: اگر از پروایدر SQL Server استفاده می‌کنید، می‌توان متد الحاقی ویژه‌ای را به نام ForSqlServerIsClustered نیز برای تعریف clustered indexes، در این زنجیره ذکر کرد.


امکان تعریف Alternate Keys در EF Core 1.0

به Unique Constraints در EF Core، نام Alternate Keys را داده‌اند و این مورد نیز تنها از طریق Fluent API قابل تنظیم است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Car>()
     .HasAlternateKey(c => c.LicensePlate)
     .HasName("AlteranteKey_LicensePlate");
}
برای یک Alternate Key به صورت خودکار هم ایندکس ایجاد می‌شود و هم اینکه این ایندکس منحصربفرد خواهد بود.
اگر متد HasName در اینجا ذکر نشود، نام پیش فرض آن  <type name>_<property name> خواهد بود و اگر همانند composite keys و یا ایندکس‌های ترکیبی، چند خاصیت ذکر شوند، قسمت property name به جمع نام تمام خواص ذکر شده و جدا شده‌ی با _ تنظیم می‌شود.
برای نمونه اگر یک Alternate Key ترکیبی را به صورت ذیل تعریف کنیم:
modelBuilder.Entity<Person>()
     .HasAlternateKey(x => new { x.FirstName, x.LastName });
در قسمت مهاجرت‌هایی که قرار است به بانک اطلاعاتی اعمال شوند، به یک UniqueConstraint ترجمه می‌شود:
 table.UniqueConstraint("AK_Persons_FirstName_LastName", x => new { x.FirstName, x.LastName });


سؤال: یک Unique Constraint با Unique Index چه تفاوتی دارد؟

در پشت صحنه، پیاده سازی یک Unique Constraint با Unique Index تفاوتی ندارند. فقط از دیدگاه روشن‌تر شدن مقصود، استفاده‌ی از Unique Constraint ترجیح داده می‌شود.
البته از دیدگاه بانک اطلاعاتی پیاده سازی کننده نیز برای نمونه SQL Server، این تفاوت‌ها وجود دارند:
الف) یک Unique Constraint را نمی‌توان غیرفعال کرد؛ برخلاف Unique Indexها.
ب) Unique Constraint‌ها موارد اضافه‌تری را مانند FILLFACTOR و IGNORE_DUP_KEY نیز می‌توانند تنظیم کنند.
ج) امکان تعریف فیلترها برای Unique Indexها وجود دارد؛ برخلاف Unique Constraint ها.

که البته از دیدگاه EF، این سه مورد اهمیتی ندارند و بیشتر روشن‌تر شدن مقصود، هدف اصلی آن‌ها است.
نظرات مطالب
C# 8.0 - Nullable Reference Types
یک نکته‌ی تکمیلی: bang operator/null forgiving operator در C# 8.0

زمانیکه یک ! را به عبارتی اضافه می‌کنیم (به آن عملگر bang هم می‌گویند!)، به این معنا است که ممکن است این عبارت در جائی از برنامه حاوی نال باشد، اما مطمئن هستیم که در این نقطه از برنامه، هیچگاه نال نخواهد بود. مثال زیر را درنظر بگیرید:
string? s1 = "Hello";
string s2 = s1!;
در اینجا s1 به صورت نال‌پذیر تعریف شده‌است و سپس به یک رشته‌ی نال نپذیر انتساب داده شده‌است. وجود ! پس از s1 به این معنا است که مطمئن هستیم در این نقطه، s1 نال نیست. این اعلام به کامپایلر سبب خواهد شد تا از صدور خطاها و اخطارهای مرتبط، جلوگیری کند.
مطالب
C# 8.0 - پیشنیاز و روش راه اندازی
پیشنیاز کار با C# 8.0

هرچند بسیاری از قابلیت‌های C# 8.0 در خود کامپایلر #C پیاده سازی شده‌اند، اما برای مثال قابلیتی مانند «پیاده سازی پیش‌فرض اینترفیس‌ها» نیاز به یک runtime جدید دارد که به همراه NET Core 3.0. ارائه می‌شود. بنابراین NET Full 4x. شاهد پیاده سازی C# 8.0 نخواهد بود. همچنین یک سری از قابلیت‌های C# 8.0 وابسته‌ی به NET Standard 2.1. و  netcoreapp3.0  هستند؛ مانند نوع‌های جدید System.IAsyncDisposable و یا System.Range. به همین جهت است که برای کار با C# 8.0، حتما نیاز به نصب NET Core 3.0. نیز می‌باشد و به روز رسانی کامپایلر #C کافی نیست.


چه نگارش‌هایی از Visual Studio از NET Core 3.0. پشتیبانی می‌کنند؟

مطابق مستندات رسمی موجود، یک چنین جدولی در مورد نگارش‌های مختلف NET Core. و نگارش‌های ویژوال استودیوهایی از که از آن‌ها پشتیبانی می‌کنند، وجود دارد:

.NET Core SDK .NET Core Runtime Compatible Visual Studio MSBuild Notes
2.1.5nn 2.1 2017 15 Installed as part of VS 2017 version 15.9
2.1.6nn 2.1 2019 16 Installed as part of VS 2019
2.2.1nn 2.2 2017 15 Installed manually
2.2.2nn 2.2 2019 16 Installed as part of VS 2019
3.0.1nn 3.0 (Preview) 2019 16 Installed manually

بنابراین فقط VS 2019 است که قابلیت پشتیبانی از NET Core 3.0. را دارد. به همین جهت اگر قصد دارید با ویژوال استودیو کار کنید، نصب VS 2019 برای کار با C# 8.0 الزامی است.


فعالسازی C# 8.0 در ویژوال استودیو 2019

در زمان نگارش این مطلب، NET Core 3.0. در حالت پیش‌نمایش، ارائه شده‌است. به همین جهت جزء یکپارچه‌ی VS 2019 محسوب نشده و باید جداگانه نصب شود:


- برای این منظور ابتدا نیاز است آخرین نگارش NET Core 3.0 SDK. را دریافت و نصب کنید.
- سپس از منوی Tools | Options، گزینه‌ی Projects and Solutions را انتخاب و در ادامه گزینه‌ی Use previews of the .NET Core SDK را انتخاب کنید.
- پس از آن، این SDK جدید NET Core. به صورت زیر قابل انتخاب خواهد بود:


البته انتخاب شماره SDK صحیح به تنهایی برای کار با C# 8.0 کافی نیست؛ بلکه باید شماره‌ی زبان مورد استفاده را نیز صریحا انتخاب کرد:


برای اینکار بر روی پروژه کلیک راست کرده و گزینه‌ی Properties آن‌را انتخاب کنید. سپس در اینجا در برگه‌ی Build، بر روی دکمه‌ی Advanced کلیک کنید تا بتوان شماره نگارش زبان را مطابق تصویر فوق انتخاب کرد. در اینجا بجای C# 8.0 (beta)، گزینه‌ی unsupported preview را نیز می‌توانید انتخاب کنید.

یک نکته: خلاصه‌ی تمام این مراحل، منوها و تصاویر، همان تنظیمات فایل csproj است که در ادامه بررسی می‌کنیم.


فعالسازی C# 8.0 در VSCode

مدت‌ها است که برای کار با NET Core. نیازی به استفاده‌ی از نگارش کامل ویژوال استودیو نیست. همینقدر که VSCode را به همراه افزونه‌ی #C آن نصب کرده باشید، می‌توانید برنامه‌های مبتنی بر NET Core. را بر روی سیستم عامل‌های مختلفی که NET Core SDK. بر روی آن‌ها نصب شده‌است، توسعه دهید.
پشتیبانی ابتدایی از C# 8.0، با نگارش v1.18.0 افزونه‌ی #C مخصوص VSCode ارائه شد. بنابراین هم اکنون اگر آخرین نگارش آن‌را نصب کرده باشید، امکان کار با پروژه‌های NET Core 3.0 و C# 8.0 را نیز دارید.
بنابراین در اینجا به صورت خلاصه:
- ابتدا باید NET Core 3.0 SDK. را به صورت جداگانه‌ای دریافت و نصب کنید.
- سپس آخرین نگارش افزونه‌ی #C مخصوص VSCode را نیز نصب کنید.
- در آخر، یک پوشه‌ی جدید را ایجاد کرده و در خط فرمان دستور dotnet new console را صادر کنید. این دستور بر اساس آخرین شماره نگارش SDK نصب شده، یک پروژه‌ی جدید کنسول را ایجاد می‌کند که ساختار فایل csproj آن به صورت زیر است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>
</Project>
همانطور که مشاهده می‌کنید، TargetFramework را به آخرین SDK نصب شده، تنظیم کرده‌است (معادل دومین تصویر این مطلب). مرحله‌ی بعد، تنظیم شماره نگارش زبان آن است. برای این منظور یکی از دو حالت زیر را می‌توان انتخاب کرد:
- یا معادل همان گزینه‌ی unsupported preview در تصویر سوم این مطلب:
<Project Sdk="Microsoft.NET.Sdk"> 
   <PropertyGroup> 
      <OutputType>Exe</OutputType> 
      <TargetFramework>netcoreapp3.0</TargetFramework> 
      <LangVersion>preview</LangVersion> 
   </PropertyGroup>
 </Project>
- و یا تعیین صریح شماره نگارش C# 8.0 (beta) به صورت زیر:
<Project Sdk="Microsoft.NET.Sdk"> 
   <PropertyGroup> 
      <OutputType>Exe</OutputType> 
      <TargetFramework>netcoreapp3.0</TargetFramework> 
      <LangVersion>8.0</LangVersion> 
   </PropertyGroup>
</Project>

یک نکته: در اینجا نمی‌توان LangVersion را به latest تنظیم کرد؛ چون C# 8.0 هنوز در مرحله‌ی بتا است. زمانیکه از مرحله‌ی بتا خارج شد، مقدار پیش‌فرض آن دقیقا latest خواهد بود و ذکر صریح آن غیر ضروری است. انتخاب latest در اینجا به latest minor version یا همان نگارش C# 7.3 فعلی (آخرین نگارش پایدار زبان #C در زمان نگارش این مطلب) اشاره می‌کند.



Rider و پشتیبانی از C# 8.0

Rider 2019.1 نیز به همراه پشتیبانی از C# 8.0 ارائه شده‌است و می‌تواند گزینه‌ی مطلوب دیگری برای توسعه‌ی برنامه‌های مبتنی بر NET Core. باشد.


نصب NET Core 3.0 SDK. و عدم اجرای برنامه‌های پیشین

یکی از مزایای کار با NET Core.، امکان نصب چندین نوع مختلف SDK آن، به موازت هم است؛ بدون اینکه بر روی یکدیگر تاثیری بگذارند. البته این نکته را باید درنظر داشت که برنامه‌های NET Core. بدون وجود فایل مخصوص global.json در پوشه‌ی ریشه‌ی آن‌ها، همواره از آخرین نگارش SDK نصب شده، برای اجرا استفاده خواهند کرد. اگر این مورد بر روی کار شما تاثیرگذار است، می‌توانید شماره SDK مورد استفاده‌ی برنامه‌ی خود را قفل کنید، تا SDKهای جدید نصب شده، به عنوان SDK پیش‌فرض برنامه‌های پیشین، انتخاب نشوند. بنابراین ابتدا لیست SDKهای نصب شده را با دستور زیر پیدا کنید:
> dotnet --list-sdks
سپس برای پروژه‌های قدیمی خود که فعلا قصد به روز رسانی آن‌ها را ندارید، یک فایل global.json را به صورت زیر‌، در ریشه‌ی پروژه تولید کنید:
> dotnet new globaljson --sdk-version 2.2.300
> type global.json
در اینجا 2.2.300 یکی از شماره‌هایی است که توسط دستور dotnet --list-sdks یافته‌اید و پروژه‌ی قبلی شما بر اساس آن کار می‌کند.
مطالب
C# 12.0 - Experimental Attribute
گاهی از اوقات ممکن است یک ویژگی تکمیل نشده، سر از نگارش‌های release درآورد؛ چون نیاز به دریافت بازخوردی در این مورد وجود دارد و یا اینکه قرار است در طی چند مرحله تکمیل شود. برای اینکه یک چنین مساله‌ای خصوصا از طرف نویسندگان کتابخانه‌ها و فریم‌ورک‌ها مشخص شود، ویژگی جدید System.Diagnostics.CodeAnalysis.ExperimentalAttribute به دات‌نت 8 اضافه شده‌است.
در این حالت اگر کدی، شروع به استفاده‌ی از یک چنین عضو‌های آزمایشی کند، یک خطای زمان کامپایل رخ می‌دهد؛ مگر اینکه آن قطعه کد نیز دقیقا با همین ویژگی مزین شود. در اینجا می‌توان نوع‌ها، اسمبلی‌ها و حتی اعضای آن‌ها را آزمایشی تعریف کرد. اگر کل یک نوع را به صورت آزمایشی معرفی کنیم، تمام اعضای آن هم آزمایشی خواهند بود.


بررسی ویژگی Experimental با یک مثال

در ادامه نحوه‌ی اعمال ویژگی Experimental را به همراه یک diagnosticId که به کل یک کلاس اعمال شده‌است، مشاهده می‌کنید. از این diagnosticId در حین تولید متن خطاها و یا برای شناسایی آن‌ها، استفاده می‌شود:
using System.Diagnostics.CodeAnalysis;

namespace CS8Tests;

[Experimental(diagnosticId: "Test001")]
public class ExperimentalAttributeDemo
{
    public void Print()
    {
        Console.WriteLine("Hello Experimental Attribute");
    }
}
پس از این تعریف، اگر در قسمت دیگری از برنامه بخواهیم از این کلاس استفاده کنیم:
var experimentalAttributeDemo = new ExperimentalAttributeDemo();
با خطای زیر مواجه خواهیم شد:
error Test001: 'CS8Tests.ExperimentalAttributeDemo' is for evaluation purposes only
and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
برای مواجه شدن با یک چنین خطایی، می‌توان دو روش زیر را در پیش گرفت:
الف) غیرفعال کردن سراسری گزارش این نوع خطاها در فایل csproj. برنامه:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <NoWarn>Test001</NoWarn>
  </PropertyGroup>
</Project>
در اینجا اضافه شدن NoWarn را بر اساس diagnosticId ویژگی آزمایشی تعریف شده، مشاهده می‌کنید. این تنظیم سراسری است و به تمام قسمت‌های پروژه‌ی جاری اعمال می‌شود. اضافه کردن آن هم فقط یکبار صورت می‌گیرد.

ب) غیرفعال کردن موضعی آن، صرفا در محل استفاده
برای غیرفعال کردن محلی این بررسی، تنها کافی است با استفاده از pragma warning# یکبار آن‌را غیرفعال کرده و پس از پایان کار، مجددا آن‌را فعال کنیم:
#pragma warning disable Test001
var demo = new ExperimentalAttributeDemo();
#pragma warning restore Test001
همانطور که مشاهده می‌کنید، این فعال و غیرفعال کردن هم بر اساس diagnosticId صورت می‌گیرد. بدیهی است این تنظیم سراسری نبوده و درصورت بکارگیری این قطعه کد در قسمت‌های دیگر برنامه، باید مجددا تکرار شود.

و اگر این مثال را کمی پیچیده‌تر کنیم، به حالت زیر می‌رسیم:
using System.Diagnostics.CodeAnalysis;

namespace CS8Tests;

[Experimental(diagnosticId: "Test001")]
public class ExperimentalAttributeDemo
{
    [Experimental(diagnosticId: "Test002")]
    public void Print()
    {
        Console.WriteLine("Hello Experimental Attribute");
    }
}
در اینجا دو ویژگی آزمایشی، با دو diagnosticId متفاوت تعریف شده‌اند. در این حالت اگر سعی کنیم قطعه کد زیر را کامپایل کنیم:
var demo = new ExperimentalAttributeDemo();
demo.Print();
به ازای هر ویژگی آزمایشی تعریف شده، یک خطای کامپایلر جداگانه را دریافت می‌کنیم. به همین جهت برای رفع این خطاها، یا باید از روش غیرفعال سازی سراسری آن‌ها پیش‌رفت:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <NoWarn>Test001,Test002</NoWarn>
  </PropertyGroup>
</Project>
 و یا می‌توان به صورت محلی زیر عمل کرد:
#pragma warning disable Test001,Test002
var demo = new ExperimentalAttributeDemo();
demo.Print();
#pragma warning restore Test001,Test002
در اینجا ذکر هر دو diagnosticId، ضروری است.
مطالب دوره‌ها
محدود کردن بارگذاری اشیاء مرتبط یک ViewModel در حین کار با Entity Framework و AutoMapper
فرض کنید مدل کاربران سایت، دارای دو خاصیت راهبری (navigation properties) آدرس‌های مختلف یک کاربر و ایمیل‌های متفاوت او است:
public class SiteUser
{
    public int Id { get; set; }
    public string Name { get; set; }
 
    public virtual ICollection<Address> Addresses { get; set; }
    public virtual ICollection<Email> Emails { get; set; }
}

public class Email
{
    public int Id { get; set; }
    public string Text { get; set; }
 
    [ForeignKey("SiteUserId")]
    public virtual SiteUser SiteUser { get; set; }
    public int SiteUserId { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string Text { get; set; }
 
    [ForeignKey("SiteUserId")]
    public virtual SiteUser SiteUser { get; set; }
    public int SiteUserId { get; set; }
}
همچنین ViewModel ایی را هم که تعریف کرده‌ایم، شامل همان خواص راهبری مدل می‌شود:
public class UserViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
 
    public ICollection<Address> Addresses { get; set; }
    public ICollection<Email> Emails { get; set; }
}
در این حالت کوئری ذیل:
 var user1 = context.Users.Project().To<UserViewModel>().FirstOrDefault();
سبب خواهد شد تا تمام خواص راهبری ذکر شده‌ی در ViewModel، در طی یک کوئری از بانک اطلاعاتی دریافت شده و مقدار دهی شوند. اما ... شاید در حین استفاده‌ی از آن، صرفا به لیست ایمیل‌های شخص نیاز داشته باشیم و نیازی نباشد تا حتما آدرس‌های او نیز واکشی شوند. برای حل این بارگذاری اضافی، می‌توان از تنظیم ExplicitExpansion استفاده کرد:
public class TestProfile : Profile
{
    protected override void Configure()
    {
        this.CreateMap<SiteUser, UserViewModel>()
                .ForMember(dest => dest.Addresses, opt => opt.ExplicitExpansion())
                .ForMember(dest => dest.Emails, opt => opt.ExplicitExpansion());
    }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
ExplicitExpansion به این معنا است که تا در کوئری مدنظر صریحا قید نشود که قرار است کدام خاصیت راهبری بسط یابد، اطلاعات آن از بانک اطلاعاتی دریافت نخواهد شد.
پس از تنظیم فوق، اگر کوئری ذکر شده را اجرا کنید، مشاهده خواهید کرد که دو خاصیت آدرس‌ها و ایمیل‌های شخص، نال هستند.
برای ذکر صریح خواص راهبری مورد نیاز، اینبار می‌توان از پارامترهای متد Project To مانند مثال ذیل استفاده کرد:
using (var context = new MyContext())
{
    var user1 = context.Users
                       .Project()
                       .To<UserViewModel>(parameters: new { }, membersToExpand: viewModel => viewModel.Emails)
                       .FirstOrDefault(); 
 
    if (user1 != null)
    {
        foreach (var email in user1.Emails)
        {
            Console.WriteLine(email.Text);
        }
    }
}
این کوئری سبب خواهد شد تا صرفا خاصیت Emails از بانک اطلاعاتی واکشی شود و آدرس‌ها خیر. به این ترتیب می‌توان بر روی نحوه‌ی بارگذاری خواص راهبری کنترل کاملی داشت.


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