مطالب
PowerShell 7.x - قسمت دوازدهم - آشنایی با GitHub Actions و بررسی یک مثال
GitHub Actions، یک راه‌حل Continuous Integration است که توسط آن می‌توان یکسری trigger workflowهایی را حین push کردن، ارسال PR و … اجرا کرد. برای کارهایی از قبیل اجرای تست‌های خودکار، اجرای یکسری تست و همچنین deploy کردن از آن استفاده میشود. GitHub Actions در واقع یک managed serviceیی است که توسط GitHub ارائه میشود. به این معنا که نیازی نیست خودمان درگیر مدیریت منابع باشیم. همچنین تعداد زیادی اکشن توسط community برای استفاده توسعه داده شده‌اند. در ادامه ابتدا مرور سریعی بر GitHub Actions خواهیم داشت، سپس یک مثال از آن را به همراه PowerShell بررسی خواهیم کرد.

ساختار یک اکشن
  • Workflow: یکی از مفاهیمی که باید با آن آشنا باشیم workflowها هستند. یک workflow مجموعه‌ایی از jobهایی هستند که در رخدادهای خاصی اجرا میشوند. در واقع یک workflow یک CI pipeline است که با کمک YAML آنها را تعریف میکنیم.
  • Runner: اینها به اصطلاح compute machineهایی هستند که workflowها را اجرا میکنند. این runnerها هم میتوانند به صورت سفارشی باشند و هم سرویس‌های ارائه شده توسط GitHub باشند.
  • Job: مجموعه‌ایی از مراحلی که درون یک runner workspace اجرا میشوند.
  • Step: در نهایت stepها هستند که کوچکترین بخش GitHub Actions هستند. stepها میتوانند فایل اسکریپت، Dockerfile یا یک community action باشند.

نمونه‌ی یک Workflow
در ادامه یک workflow را مشاهده میکنید. در اینجا نام آن را به Build Application Code تنظیم کرده‌ایم. سپس با کمک on، تریگر اجرای این workflow را تعیین کرده‌ایم. به این معنا که با push کردن بر روی ریپوزیتوری، workflow اجرا خواهد شد. سپس توسط job، لیست jobهایی را که میخواهیم این workflow اجرا کند، مشخص کرده‌ایم. اولین jobی که اجرا خواهد شد، build است. این job قرار است بر روی یک ماشین با آخرین نگارش ابونتو اجرا شود. مراحل یا stepهای این job نیز به ترتیب، clone کردن سورس‌کد و سپس نصب وابستگی‌های پروژه است. در نهایت job بعدی، test خواهد بود که با کمک needs تعیین کرده‌ایم که ابتدا مرحله‌ی قبل یعنی build اجرا شود و سپس وارد این مرحله شود. 
name: Build Application Code

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v2
      - name: Install Libraries
        uses: pip install -r requirements.txt -t .
    
    test:
      runs-on: ubuntu-latest
      needs: build
      steps:
      ...

مثال PowerShell
هدف، پویا کردن قسمت README یک پروفایل GitHub است. برای این مثال من از پروفایل خودم استفاده خواهم کرد. درون فایل README میخواهم لیست آخرین بلاگ‌پست‌هایی را که منتشر کرده‌ام، به همراه یک کامپوننت، تعداد قدم‌هایی را که در طول روز پیاده‌روی میکنم، نمایش دهم. برای نمایش آخرین دیتای درون پروفایلم، نیاز به دو Action Workflow داشتیم که هر یک در تایم خاصی اجرا شده و اسکریپت‌هایی را که در ادامه توضیح خواهم داد، اجرا کنند. برای اینکار درون دایرکتوری مخصوص github.، ساختار زیر را ایجاد کرده‌ام: 
├── .github
│   ├── scripts
│   └── workflows
├── README.md
├── assets
└── deps
ابتدا workflow اول یعنی نمایش بلاگ‌پست‌های اخیر را بررسی خواهیم کرد: 
name: Update Recent Blog Posts

on:
  schedule:
    - cron: "0 0 * * 0" # Run once a week at 00:00 (midnight) on Sunday
  workflow_dispatch:

jobs:
  update_posts:
    runs-on: ubuntu-latest

    steps:
    - name: Check out repository code
      uses: actions/checkout@v3

    - name: Run the script for fetching latest blog posts
      shell: pwsh
      run: |
        . ./.github/scripts/Get-Posts.ps1
        
    - name: Commit and Push the changes
      uses: mikeal/publish-to-github-action@master
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
workflow فوق یکبار در هفته فایل PowerShell موردنظر را اجرا خواهد کرد. در ادامه محتویات این فایل را مشاهده می‌کنید: 
Function Get-Posts {
    Param (
        [Parameter(Mandatory = $false)]
        [string]$rssUrl
    )
    $posts = @()
    $feed = [xml](Invoke-WebRequest -Uri $rssUrl).Content
    $feed.rss.channel.item | Select-Object -First 3 | ForEach-Object {
        $post = [PSCustomObject]@{
            Title       = $_.title."#cdata-section" ?? $_.title
            Link        = $_.link
            Description = $_.description."#cdata-section" ?? $_.description
            PubDate     = $_.pubDate
        }
        $posts += $post
    }
    $posts
}

Function Get-DntipsPosts {
    $assemblyPath = "$(Get-Location)/deps/CodeHollow.FeedReader.dll"
    [Reflection.Assembly]::LoadFile($assemblyPath)
    $feed = [CodeHollow.FeedReader.FeedReader]::ReadAsync("https://www.dntips.ir/feed/author/%d8%b3%db%8c%d8%b1%d9%88%d8%a7%d9%86%20%d8%b9%d9%81%db%8c%d9%81%db%8c").Result
    $posts = @()
    $feed.Items | Select-Object -First 3 | ForEach-Object {
        $post = [PSCustomObject]@{
            Title       = $_.Title
            Link        = $_.Link
            Description = $_.Description
            PubDate     = $_.PublishingDate
        }
        $posts += $post
    }
    $posts
}

Function Set-Posts {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSCustomObject[]]$posts,
        [Parameter(Mandatory = $false)]
        [string]$marker = "## Recent Blog Posts - English"
    )
    Begin {
        $readMePath = "./README.md"
        $readmeContents = Get-Content -Path $readMePath -Raw
        $markdownTable = "| Link | Published At |`n"
        $markdownTable += "| --- | --- |`n"
    }
    Process {
        if ($null -eq $_.Title) {
            return
        }
        $date = Get-Date -Date $_.PubDate
        $link = "[$($_.Title)]($($_.Link))"
        
        $markdownTable += "| $($link) | $($date.ToString("dd/MM/yy")) |`n"
    }
    End {
        $updatedContent = $readmeContents -replace "$marker\n([\s\S]*?)(?=#| $)", "$marker`n$($markdownTable)`n"
        $updatedContent | Set-Content -Path $readMePath
    }
}

Function Set-Blogs {
    $recentBlogPostsStr = "## Recent blog posts -"
    Get-Posts("https://dev.to/feed/sirwanafifi") | Set-Posts -marker "$recentBlogPostsStr dev.to"
    Get-Posts("https://sirwan.infohttps://www.dntips.ir/rss.xml") | Set-Posts -marker "$recentBlogPostsStr sirwan.info"
    Get-DntipsPosts | Set-Posts -marker "$recentBlogPostsStr dntips.ir"
}

Set-Blogs

در اینجا تابع Set-Blogs فراخوانی خواهد شد. کاری که این تابع انجام میدهد، دریافت آخرین بلاگ‌پست‌هایی که در جاهای مختلف منتشر کرده‌ام و سپس آپدیت کردن فایل README با دیتای جدید است. همانطور که مشاهده میکنید برای خواندن فید سایت جاری، از پکیج FeedReader استفاده کرده‌ام. در PowerShell توسط Invoke-WebRequest میتوانیم یک فید را پارز کنیم؛ اما برای سایت جاری با خطا روبرو شدم و در نهایت تصمیم گرفتم از یک پکیج دات‌نتی استفاده کنم. وابستگی موردنظر، درون دایرکتوری dep به صورت DLL قرار دارد. سپس از طریق PowerShell اسمبلی مربوطه بارگذاری شده و از کتابخانه موردنظر استفاده شده‌است. در نهایت برای آپدیت کردن فایل README.md یکسری marker تعیین کرده‌ام که با یک جایگزینی محتویات موردنظر، آنجا قرار خواهند گرفت.

workflow بعدی نیز به صورت زیر میباشد که در پایان هر روز در یک ساعت مشخص اجرا خواهد شد: 
name: Update Step Component

on:
  schedule:
    - cron: "0 18 * * *"
  workflow_dispatch:

jobs:
  update_steps:
    runs-on: ubuntu-latest

    steps:
    - name: Check out repository code
      uses: actions/checkout@v3

    - name: Run the script for fetching my latest steps
      shell: pwsh
      env:
          STEPS_URI: ${{ secrets.STEPS_URI }}
      run: |
        . ./.github/scripts/Get-Steps.ps1
    
    - name: Commit and Push the changes
      uses: mikeal/publish-to-github-action@master
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
workflow فوق نیز همانند روال قبل فایل اسکریپت موردنظر را توسط dot sourcing اجرا میکند. این روال هر روز، ساعت ۱۸ انجام خواهد شد. اسکریپت مربوطه نیز به صورت زیر پیاده‌سازی شده است: 
Function Set-Steps {
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSObject]$json
    )
    Write-Host ($json | ConvertTo-Json)
    $SvgPath = "$(Get-Location)/assets/step.svg"
    $SvgContent = Get-Content -Path $SvgPath -Raw
    $TextTags = @"
    <tspan id="step-count" font-weight="bold">$([System.String]::Format("{0:n0}", [int]$json.steps))</tspan>
"@
    $DatetimeTags = "<text id=""datetime"" x=""800"" y=""72"" font-size=""39"" fill="#99989E"">$($json.date)</text>"
    $SvgContent = $SvgContent -Replace '<tspan id="step-count" font-weight="bold">.*?</tspan>', $TextTags
    $SvgContent = $SvgContent -Replace '<text id="datetime" x="800" y="72" font-size="39" fill="#99989E">.*?</text>', $DatetimeTags
    $SvgContent | Set-Content -Path $SvgPath
}

Function Get-LatestSteps {
    Try {
        $Uri = $env:STEPS_URI
        Write-Host "Uri: $Uri"
        $JsonResult = (Invoke-WebRequest -Uri $Uri).Content | ConvertFrom-Json
        Write-Host "Steps: $($JsonResult.steps)"
        Return $JsonResult
    }
    Catch {
        Return @{
            steps = 0
            date  = Get-Date -Format "yyyy-MM-dd"
        }
    }
}

Write-Host "Getting latest steps..."
Get-LatestSteps | Set-Steps
Write-Host "Done!"
اسکریپت فوق نیز همانند منطق اسکریپت قبلی یعنی جایگذاری رشته‌ی موردنظر با کمک عبارات باقاعده انجام شده‌است. در اینجا دیتای مربوط به قدم‌های من از APIایی که از طریق Environment Variable تعیین شده‌است، دریافت میشود و سپس خروجی آن که یک JSON است به تابع Set-Steps برای بروزرسانی فایل README.md ارسال میشود. در دو workflow نشان داده شده بعد از ایجاد تغییرات بر روی فایل‌های README.md و همچنین فایل SVG نیاز است که تغییرات را مجدداً به ریپوزیتوری پوش کنیم. برای اینکار از یک community action با نام  publish-to-github-action استفاده شده‌است. این اکشن نیاز به دسترسی پوش به ریپوزیتوری‌مان دارد که در اینجا ما از یک secret key مخصوص، با نام GITHUB_TOKEN استفاده کرده‌ایم. این توکن به صورت خودکار جنریت میشود و نیازی نیست خودمان آن را تنظیم کنیم.
خروجی در نهایت، اینچنین خواهد بود:

مطالب دوره‌ها
استفاده از StructureMap به عنوان یک IoC Container
StructureMap یکی از IoC containerهای بسیار غنی سورس باز نوشته شده برای دات نت فریم ورک است. امکان تنظیمات آن توسط کدنویسی و یا همان Fluent interfaces، به کمک فایل‌های کانفیگ XML و همچنین استفاده از ویژگی‌ها یا Attributes نیز میسر است. امکانات جانبی دیگری را نیز مانند یکی شدن با فریم ورک‌های Dynamic Proxy برای ساده سازی فرآیندهای برنامه نویسی جنبه‌گرا یا AOP، دارا است. در ادامه قصد داریم با نحوه استفاده از این فریم ورک IoC بیشتر آشنا شویم.


دریافت StructureMap

برای دریافت آن نیاز است دستور پاورشل ذیل را در کنسول نیوگت ویژوال استودیو فراخوانی کنید:
 PM> Install-Package structuremap
البته باید دقت داشت که برای استفاده از StructureMap نیاز است به خواص پروژه مراجعه و سپس حالت Client profile را به Full profile تغییر داد تا برنامه قابل کامپایل باشد (در برنامه‌های دسکتاپ البته)؛ از این جهت که StructureMap ارجاعی را به اسمبلی استاندارد System.Web دارد.


آشنایی با ساختار برنامه

ابتدا یک برنامه کنسول را آغاز کرده و سپس یک Class library جدید را به نام Services نیز به آن اضافه کنید. در ادامه کلاس‌ها و اینترفیس‌های زیر را به Class library ایجاد شده، اضافه کنید. سپس از طریق نیوگت به روشی که گفته شد، StructureMap را به پروژه اصلی (ونه پروژه Class library) اضافه نمائید و Target framework آن‌را نیز در حالت Full قرار دهید بجای حالت Client profile.
namespace DI03.Services
{
    public interface IUsersService
    {
        string GetUserEmail(int userId);
    }
}


namespace DI03.Services
{
    public interface IEmailsService
    {
        void SendEmailToUser(int userId, string subject, string body);
    }
}

using System;

namespace DI03.Services
{
    public class UsersService : IUsersService
    {
        public UsersService()
        {
            //هدف صرفا نمایش وهله سازی خودکار این وابستگی است
            Console.WriteLine("UsersService ctor.");
        }

        public string GetUserEmail(int userId)
        {
            //برای مثال دریافت از بانک اطلاعاتی و بازگشت یک نمونه جهت آزمایش برنامه
            return "name@site.com";
        }
    }
}

using System;

namespace DI03.Services
{
    public class EmailsService: IEmailsService
    {
        private readonly IUsersService _usersService;
        public EmailsService(IUsersService usersService)
        {
            Console.WriteLine("EmailsService ctor.");
            _usersService = usersService;
        }

        public void SendEmailToUser(int userId, string subject, string body)
        {
            var email = _usersService.GetUserEmail(userId);
            Console.WriteLine("SendEmailTo({0})", email);
        }
    }
}
در لایه سرویس برنامه، یک سرویس کاربران و یک سرویس ارسال ایمیل تدارک دیده شده‌اند.
سرویس کاربران بر اساس آی دی یک کاربر، برای مثال از بانک اطلاعاتی ایمیل او را بازگشت می‌دهد. سرویس ارسال ایمیل، نیاز به ایمیل کاربری برای ارسال ایمیلی به او دارد. بنابراین وابستگی مورد نیاز خود را از طریق تزریق وابستگی‌ها در سازنده کلاس و وهله سازی شده در خارج از آن (معکوس سازی کنترل)، دریافت می‌کند.
در سازنده‌های هر دو کلاس سرویس نیز از Console.WriteLine استفاده شده‌است تا زمان وهله سازی خودکار آن‌ها را بتوان بهتر مشاهده کرد.
نکته مهمی که در اینجا وجود دارد، بی‌خبری لایه سرویس از وجود IoC Container مورد استفاده است.


استفاده از لایه سرویس و تزریق وابستگی‌ها به کمک  StructureMap

using DI03.Services;
using StructureMap;

namespace DI03
{
    class Program
    {
        static void Main(string[] args)
        {
            // تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود
            ObjectFactory.Initialize(x =>
            {
                x.For<IEmailsService>().Use<EmailsService>();
                x.For<IUsersService>().Use<UsersService>();
            });

            //نمونه‌ای از نحوه استفاده از تزریق وابستگی‌های خودکار
            var emailsService = ObjectFactory.GetInstance<IEmailsService>();
            emailsService.SendEmailToUser(userId: 1, subject: "Test", body: "Hello!");
        }
    }
}
کدهای برنامه را به نحو فوق تغییر دهید. در ابتدا نحوه سیم کشی‌های آغازین برنامه را مشاهده می‌کنید. برای مثال کدهای ObjectFactory.Initialize باید در متدهای آغازین یک پروژه قرار گیرند و تنها یکبار هم نیاز است فراخوانی شوند.
به این ترتیب IoC Container ما زمانیکه قرار است object graph مربوط به IEmailsService درخواستی را تشکیل دهد، خواهد دانست ابتدا به سازنده‌ی کلاس EmailsService می‌رسد. در اینجا برای وهله سازی این کلاس به صورت خودکار، باید وابستگی‌های آن‌را نیز وهله سازی کند. بنابراین بر اساس تنظیمات آغازین برنامه می‌داند که باید از کلاس UsersService برای تزریق خودکار وابستگی‌ها در سازنده کلاس ارسال ایمیل استفاده نماید.
در این حالت اگر برنامه را اجرا کنیم، به خروجی زیر خواهیم رسید:
UsersService ctor.
EmailsService ctor.
SendEmailTo(name@site.com)
بنابراین در اینجا با مفهوم Object graph نیز آشنا شدیم. فقط کافی است وابستگی‌ها را در سازنده‌های کلاس‌ها تعریف کرده و سیم کشی‌های آغازین صحیحی را نیز در ابتدای برنامه معرفی نمائیم. کار وهله سازی چندین سطح با تمام وابستگی‌های متناظر با آن‌ها در اینجا به صورت خودکار انجام خواهد شد و نهایتا یک شیء قابل استفاده بازگشت داده می‌شود.
ابتدایی‌ترین مزیت استفاده از تزریق وابستگی‌ها امکان تعویض آن‌ها است؛ خصوصا در حین Unit testing. اگر کلاسی برای مثال قرار است با شبکه کار کند، می‌توان پیاده سازی آن‌را با یک نمونه اصطلاحا Fake جایگزین کرد و در این نمونه تنها نتیجه‌ی کار را بازگشت داد. کلاس‌های لایه سرویس ما تنها با اینترفیس‌ها کار می‌کنند. این تنظیمات قابل تغییر اولیه IoC container مورد استفاده هستند که مشخص می‌کنند چه کلاس‌هایی باید در سازنده‌های کلاس‌ها تزریق شوند.


تعیین طول عمر اشیاء در StructureMap

برای اینکه بتوان طول عمر اشیاء را بهتر توضیح داد، کلاس سرویس کاربران را به نحو زیر تغییر دهید:
using System;

namespace DI03.Services
{
    public class UsersService : IUsersService
    {
        private int _i;
        public UsersService()
        {
            //هدف صرفا نمایش وهله سازی خودکار این وابستگی است
            Console.WriteLine("UsersService ctor.");
        }

        public string GetUserEmail(int userId)
        {
            _i++;
            Console.WriteLine("i:{0}", _i);
            //برای مثال دریافت از بانک اطلاعاتی و بازگشت یک نمونه جهت آزمایش برنامه
            return "name@site.com";
        }
    }
}
به عبارتی می‌خواهیم بدانیم این کلاس چه زمانی وهله سازی مجدد می‌شود. آیا در حالت فراخوانی ذیل،
 //نمونه‌ای از نحوه استفاده از تزریق وابستگی‌های خودکار
var emailsService1 = ObjectFactory.GetInstance<IEmailsService>();
emailsService1.SendEmailToUser(userId: 1, subject: "Test1", body: "Hello!");

var emailsService2 = ObjectFactory.GetInstance<IEmailsService>();
emailsService2.SendEmailToUser(userId: 1, subject: "Test2", body: "Hello!");
ما شاهد چاپ عدد 2 خواهیم بود یا عدد یک:
 UsersService ctor.
EmailsService ctor.
i:1
SendEmailTo(name@site.com)
UsersService ctor.
EmailsService ctor.
i:1
SendEmailTo(name@site.com)
همانطور که ملاحظه می‌کنید، به ازای هربار فراخوانی ObjectFactory.GetInstance، یک وهله جدید ایجاد شده است. بنابراین مقدار i در هر دو بار مساوی عدد یک است.
اگر به هر دلیلی نیاز بود تا این رویه تغییر کند، می‌توان بر روی طول عمر اشیاء تشکیل شده نیز تاثیر گذار بود. برای مثال تنظیمات آغازین برنامه را به نحو ذیل تغییر دهید:
// تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود
ObjectFactory.Initialize(x =>
{
   x.For<IEmailsService>().Use<EmailsService>();
   x.For<IUsersService>().Singleton().Use<UsersService>();
});
اینبار اگر برنامه را اجرا کنیم، به خروجی ذیل خواهیم رسید:
 UsersService ctor.
EmailsService ctor.
i:1
SendEmailTo(name@site.com)
EmailsService ctor.
i:2
SendEmailTo(name@site.com)
بله. با Singleton معرفی کردن تنظیمات UsersService، تنها یک وهله از این کلاس ایجاد خواهد شد و نهایتا در فراخوانی دوم ObjectFactory.GetInstance، شاهد عدد i مساوی 2 خواهیم بود (چون از یک وهله استفاده شده است).

حالت‌های دیگر تعیین طول عمر مطابق متدهای زیر هستند:
 Singleton()
HttpContextScoped()
HybridHttpOrThreadLocalScoped()
با انتخاب حالت HttpContext، به ازای هر HttpContext ایجاد شده، کلاس معرفی شده یکبار وهله سازی می‌گردد.
در حالت ThreadLocal، به ازای هر Thread، وهله‌ای متفاوت در اختیار مصرف کننده قرار می‌گیرد.
حالت Hybrid ترکیبی است از حالت‌های HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد کرد در غیراینصورت به ThreadLocal سوئیچ می‌کند.

شاید بپرسید که کاربرد مثلا HttpContextScoped در کجا است؟
در یک برنامه وب نیاز است تا یک وهله از DbContext (مثلا Entity framework) را در اختیار کلاس‌های مختلف لایه سرویس قرار داد. به این ترتیب چون هربار new Context صورت نمی‌گیرد، هربار هم اتصال جداگانه‌ای به بانک اطلاعاتی باز نخواهد شد. نتیجه آن رسیدن به یک برنامه سریع، با سربار کم و همچنین کار کردن در یک تراکنش واحد است. چون هربار فراخوانی new Context به معنای ایجاد یک تراکنش جدید است.
همچنین در این برنامه وب قصد نداریم از حالت طول عمر Singleton استفاده کنیم، چون در این حالت یک وهله از Context در اختیار تمام کاربران سایت قرار خواهد گرفت (و DbContext به صورت Thread safe طراحی نشده است). نیاز است به ازای هر کاربر و به ازای طول عمر هر درخواست، تنها یکبار این وهله سازی صورت گیرد. بنابراین در این حالت استفاده از HttpContextScoped توصیه می‌شود. به این ترتیب در طول عمر کوتاه Object graph‌های تشکیل شده، فقط یک وهله از DbContext ایجاد و استفاده خواهد شد که بسیار مقرون به صرفه است.
مزیت دیگر مشخص سازی طول عمر به نحو HttpContextScoped، امکان Dispose خودکار آن به صورت زیر است:
protected void Application_EndRequest(object sender, EventArgs e)  
{  
  ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();  
}

تنظیمات خودکار اولیه در StructureMap

اگر نام اینترفیس‌های شما فقط یک I در ابتدا بیشتر از نام کلاس‌های متناظر با آن‌ها دارد، مثلا مانند ITest و کلاس Test هستند؛ فقط کافی است از قراردادهای پیش فرض StructureMap برای اسکن یک یا چند اسمبلی استفاده کنیم:
 // تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود
ObjectFactory.Initialize(x =>
{
   //x.For<IEmailsService>().Use<EmailsService>();
   //x.For<IUsersService>().Singleton().Use<UsersService>();  
   x.Scan(scan =>
   {
       scan.AssemblyContainingType<IEmailsService>();
       scan.WithDefaultConventions();
   });  
});
در این حالت دیگر نیازی نیست به ازای اینترفیس‌های مختلف و کلاس‌های مرتبط با آن‌ها، تنظیمات اضافه‌تری را تدارک دید. کار یافتن و برقراری اتصالات لازم در اینجا خودکار خواهد بود.


دریافت مثال قسمت جاری
DI03.zip

به روز شده‌ی این مثال‌ها را بر اساس آخرین تغییرات وابستگی‌های آن‌ها از مخزن کد ذیل می‌توانید دریافت کنید:
Dependency-Injection-Samples
 
مطالب
آشنایی با Refactoring - قسمت 14

در بسیاری از زبان‌های برنامه نویسی امکان null بودن Reference types وجود دارد. به همین جهت مرسوم است پیش از استفاده از آن‌ها، بررسی شود آیا شیء مورد استفاده نال است یا خیر و سپس برای مثال متد یا خاصیت مرتبط با آن فراخوانی گردد؛ در غیر اینصورت برنامه با یک استثناء خاتمه خواهد یافت.
مشکلی هم که با این نوع بررسی‌ها وجود دارد این است که پس از مدتی کد موجود را تبدیل به مخزنی از انبوهی از if و else ها خواهند کرد که هم درجه‌ی پیچیدگی متدها را افزایش می‌دهند و هم نگهداری ‌آن‌ها را در طول زمان مشکل می‌سازند. برای حل این مساله، الگوی استانداردی وجود دارد به نام null object pattern؛ به این معنا که بجای بازگشت دادن null و یا سبب بروز یک exception شدن، بهتر است واقعا مطابق شرایط آن متد یا خاصیت، «هیچ‌کاری» را انجام نداد. در ادامه، توضیحاتی در مورد نحوه‌ی پیاده سازی این «هیچ‌کاری» را انجام ندادن، ارائه خواهد شد.


الف) حین معرفی خاصیت‌ها از محافظ استفاده کنید.

برای مثال اگر قرار است خاصیتی به نام Name را تعریف کنید که از نوع رشته‌ است، حالت امن آن رشته بجای null بودن، «خالی» بودن است. به این ترتیب مصرف کننده مدام نگران این نخواهد بود که آیا الان این Name نال است یا خیر. مدام نیاز نخواهد داشت تا if و else بنویسد تا این مساله را چک کند. نحوه پیاده سازی آن هم ساده است و در ادامه بیان شده است:

private string name = string.Empty;
public string Name
{
    get { return this.name; }
    set
    {
        if (value == null)
        {
            this.name = "";
            return;
        }
        this.name = value;
    }
}

دقیقا در زمان انتساب مقداری به این خاصیت، بررسی می‌شود که آیا مثلا null است یا خیر. اگر بود، همینجا و نه در کل برنامه، مقدار آن «خالی» قرار داده می‌شود.

ب) سعی کنید در متدها تا حد امکان null بازگشت ندهید.

برای نمونه اگر متدی قرار است لیستی را بازگشت دهد:

public IList<string> GetCultures()
{
//...
}

و حین تهیه این لیست، عضوی مطابق منطق پیاده سازی آن یافت نشد، null را بازگشت ندهید؛ یک new List خالی را بازگشت دهید. به این ترتیب مصرف کننده دیگری نیازی به بررسی نال بودن خروجی این متد نخواهد داشت.


ج) از متدهای الحاقی بجای if و else استفاده کنید.

پیاده سازی حالت الف زمانی میسر خواهد بود که طراح اصلی ما باشیم و کدهای برنامه کاملا در اختیار ما باشند. اما در شرایطی که امکان دستکاری کدهای یک کتابخانه پایه را نداریم چه باید کرد؟ مثلا دسترسی به تعاریف کلاس XElement دات نت فریم ورک را نداریم (یا حتی اگر هم داریم، تغییر آن تا زمانیکه در کدهای پایه اعمال نشده باشد، منطقی نیست). در این حالت می‌شود یک یا چند extension method را طراحی کرد:

public static class LanguageExtender
{
public static string GetSafeStringValue(this XElement input)
{
return (input == null) ? string.Empty : input.Value;
}

public static DateTime GetSafeDateValue(this XElement input)
{
return (input == null) ? DateTime.MinValue : DateTime.Parse(input.Value);
}
}

به این ترتیب می‌توان امکانات کلاس پایه‌‌ای را بدون نیاز به دسترسی به کدهای اصلی آن مطابق نیاز‌های خود تغییر و توسعه داد.


مطالب
آشنایی با WPF قسمت پنجم : DataContext بخش دوم
در ادامه قسمت قبلی قصد داریم دو کنترل دیگر را نیز بایند کنیم؛ ولی از آنجا که مقادیر آن‌ها رشته‌ای یا عددی نیست و مقداری متفاوت هست، از مبحثی به نام ValueConverter استفاده خواهیم کرد.
Value Converter چیست؟


موقعی که شما قصد بایند کردن دو نوع داده متفاوت را به هم دارید، نیاز به یک کد واسط پیدا می‌کنید تا این کد واسط مقادیر شما را از مبدا دریافت کرده و تبدیل به نوعی کند که مقصد بتواند از آن استفاده کند یا بلعکس. ValueConverter نام کلاسی است که از یک اینترفیس به نام IValueConverter ارث بری کرده است و شامل دو متد تبدیل نوع از مبدا به مقصد Convert و دیگری از مقصد به مبدا  ConvertBack می‌شود که خیلی کمتر پیاده سازی می‌شود.
پیاده سازی یک کلاس مبدل سه مرحله دارد:
  • مرحله اول :ساخت کلاس ValueConverter
  • مرحله دوم : تعریف آن به عنوان یک منبع یا ریسورس
  • مرحله سوم : استفاده از آن در عملیات بایند کردن Binding

در مرحله اول، نحوه پیاده سازی کلاس ValueConverter به شکل زیر است:
public class BoolToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        // Do the conversion from bool to visibility
    }
 
    public object ConvertBack(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        // Do the conversion from visibility to bool
    }
}

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

در مرحله‌ی دوم نحوه تعریف مبدل ما در پنجره XAML به صورت زیر می‌باشد:
<Window x:Class="test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:نامی دلخواه="clr-namespace:فضای نامی که کلاس مبدل در آن قرار دارد"
        >

    <Window.Resources>
        <نام دلخواهی که در بالا تعریف کرده اید: ClassNameنام کلاس  x:Key="کلید این آیتم در ریسورس"/>
    </Window.Resources>

کلمه‌های کلیدی xmlns که مخفف XML NameSpace هستند جهت تعریف دسترسی به فضاهای نام طراحی شده‌اند و طبق تعریف مایکروسافت همان مفهوم تگ‌های تعریف فضای نام در XML را دارند که توسط مایکروسافت توسعه یافته‌اند. این تعریف‌ها در تگ ریشه (در اینجا window) تعریف می‌شوند. دو فضای نام اولی که به طور پیش فرض در همه جای پروژه قرار دارند، اشاره به فریم ورک WPF دارند. کلمه‌ی کلیدی x در خط شماره سه، نام دلخواهی است که دسترسی ما را به خصوصیات یا تعاریف XAML موجود در sdk باز می‌کند؛ مثلا استفاده از خصوصیاتی چون x:key یا x:class را به همراه دارد.
پس الان باید خط چهارم برای ما روشن باشد؛ فضای نام جدیدی را در برنامه خودمان ایجاد کرده‌ایم که این تگ به آن اشاره می‌کند و نام دلخواهی هم برای اشاره به این فضای نام برایش در نظر گرفته‌ایم. هر موقع در برنامه این نام دلخواه تعیین شده قرار گیرد، یعنی اشاره به این فضای نام که در قسمت Window.resource خط هشتم تعریف شده است.
در خط هشتم، یک ریسورس (منبع) را به برنامه معرفی کرده‌ایم:
ریسورس‌ها برای ذخیره سازی داده‌ها در سطح یک کنترل، سطح محلی در یک پنجره، یا سطح عمومی در کل پروژه به کار می‌روند. محدودیتی در ذخیره داده‌ها وجود ندارد و هر چقدر که دوست دارید می‌توانید داده به آن پاس کنید. این داده‌ها می‌توانند یک سری اطلاعات ذخیره شده در یک ساختار ساده تا یک ساختار سلسه مراتبی از کنترل‌ها باشند. ریسورس‌ها به شما این اجازه را می‌دهند تا داده‌ها را در یک مکان ذخیره کرده و آن‌ها را در یک یا چندجا مورد استفاده قرار دهید.

از آن جا که مباحث ریسورس‌ها را در یک مقاله‌ی جداگانه بررسی می‌کنیم، فقط به ذکر نکات بالا جهت کد فعلی بسنده خواهیم کرد و ادامه‌ی آن را در یک مقاله دیگر مورد بررسی قرار می‌دهیم.
هر ریسورس دارای یک نام یا یک کلید است که با خصوصیت x:key تعریف می‌شود.
ریسورس بالا یک کلاس را که در فضای نام دلخواهی قرار دارد، تعریف می‌کند و یک کلید هم به آن انتساب می‌دهد.
مرحله‌ی سوم معرفی ریسورس به عملیات Binding است:
{Binding نام پراپرتی کلاس, Converter={StaticResource کلید آیتم مربوطه در ریسورس}, ConverterParameter=پارامتری که به کلاس مبدل پاس می‌شود}

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

حال که با اصول نوشتار آشنا شدیم کار را آغاز می‌کنیم.
قصد داریم یک مبدل برای فیلد جنیست درست کنیم. از آنجا که این فیلد Boolean است و خصوصیت IsChecked یک RadioButton هم Boolean است، می‌توان یک ارتباط مستقیم را ایجاد کرد. ولی مشکل در اینجا هست که True برای مذکر است و false برای مؤنث. در نتیجه تنها Radiobutton مربوطه به جنس مذکر به این حالت پاسخ می‌دهد و از آنجا که  برای جنس مونث  false در نظر گرفته شده است، انتخاب آن هم false خواهد بود. پس باید در مبدل، مقداری که کنترل می‌خواهد را شناسایی کرده و اگر مقدار با آن برابر بود، True را بازگردانیم. مقداری که هر کنترل درخواست می‌کند را از طریق پارامتر به تابع مبدل ارسال می‌کنیم. radiobutton مذکر، مقدار True را به عنوان پارامتر ارسال می‌کند و radiobutton مونث هم مقدار false را به عنوان پارامتر ارسال می‌کند. اگر تابع مبدل را ببینید، این مقدار‌ها با پارامترها همخوانی دارند، True در غیر این صورت false بر میگرداند.

مرحله اول تعریف کلاس ValueConveter:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class GenderConverter: IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

            bool bparam;

            bool test = bool.TryParse(parameter.ToString(), out bparam);
            if (test)
            {
                return ((bool)value).Equals(bparam);
            }
            return DependencyProperty.UnsetValue;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
در کد بالا پارامتر ارسالی را دریافت می‌کنیم و اگر برابر با Null باشد، مقداری برگشتی را عدم ذکر شدن خصوصیت وابسته اعلام می‌کنیم. در نتیجه انگار این خصوصیت مقداردهی نشده است. اگر مخالف Null باشد، کار ادامه پیدا می‌کند. در نهایت مقایسه‌ای بین پارامتر و مقدار پراپرتی (value) صورت گرفته و نتیجه‌ی مقایسه را برگشت می‌دهیم.

برای تعریف این مبدل در محیط XAML به صورت زیر اقدام می‌کنیم:
<Window x:Class="test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:valueConverters="clr-namespace:test.ValueConverters"
        Title="MainWindow" Height="511.851" Width="525">

    <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
    </Window.Resources>
</window>
 کلمه valueConveter به فضای نام test.valueConverters اشاره می‌کند که در قسمت ریسورس، از کلاس GenderConverter آن استفاده می‌کنیم و کلیدی که برای این ریسورس در نظر گرفتیم را GenderConverter تعریف کردیم (هم نامی کلید ریسورس و نام کلاس مبدل اتفاقی است و ارتباطی با یکدیگر ندارند).

نکته: در صورتی که بعد از تعریف ریسورس با خطای زیر روبرو شدید و محیط طراحی Design را از دست دادید یکبار پروژه را بیلد کنید تا مشکل حل شود.
The name "GenderConverter" does not exist in the namespace "clr-namespace:test.ValueConverters".


اکنون در عملیات بایندینگ دو کنترل اینگونه می‌نویسیم:
     <RadioButton GroupName="Gender" IsChecked="{Binding Gender, Converter={StaticResource GenderConverter}, ConverterParameter=True}" Name="RdoMale"  >Male</RadioButton>
                <RadioButton GroupName="Gender" IsChecked="{Binding Gender, Converter={StaticResource GenderConverter}, ConverterParameter=False}" Name="RdoFemale" Margin="0 5 0 0"  >Female</RadioButton>
حال برنامه را اجرا کرده و نتیجه را ببینید. برای تست بهتر می‌توانید جنسیت فرد را در منبع داده تغییر دهید. همچنین از آنجا که این مقدار برای جنس مذکر نیازی به بررسی و تبدیل ندارد می‌توانید عملیات بایند را عادی بنویسید:
<RadioButton GroupName="Gender" IsChecked="{Binding Gender}" Name="RdoMale"  >Male</RadioButton>
این کد می‌تواند برای تمامی وضعیت‌های دو مقداری چون جنسیت و وضعیت تاهل و ... هم به کار برود. ولی حالا سناریویی را تصور کنید که که مقادیر از دو تا بیشتر شود؛ مثل وضعیت تحصیلی ، دسترسی‌ها و غیره که این‌ها نیاز به داده‌های شمارشی چون Enum‌ها دارند. روند کار دقیقا مانند بالاست:
هر کنترل مقداری از یک enum را میپذیرد که میتواند آن مقدار را با استفاده از پارامتر، به تابع مبدل ارسال کند و سپس تابع چک می‌کند که آیا چنین مقداری در enum مدنظر یافت می‌شود یا خیر؛ این کد الان کار می‌کندو واقعا هم کد درستی است و هیچ مشکلی هم ندارد. ولی برای یک لحظه تصور کنید پنجره شما شامل چهار خصوصیت enum دار است. یعنی الان باید 4 تابع مبدل بنویسید. پس باید کد را بازنویسی کنیم تا به هر enum که مدنظر است پاسخ دهد؛ به این ترتیب تنها یک تابع مبدل می‌نویسیم.

جهت یادآوری نگاهی به کلاس برنامه می‌اندازیم:
public enum FieldOfWork
    {
        Actor=0,
        Director=1,
        Producer=2
    }
    public class Person : INotifyPropertyChanged
    { 
        public bool Gender { get; set; }

        public string ImageName { get; set; }

        public string Country { get; set; }

        public DateTime Date { get; set; }

        public FieldOfWork FieldOfWork { get; set; }

        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        }
فعلا IList را از پراپرتی FieldOfWork بر میداریم تا این سناریو باشد که تنها یک Enum قابل انتخاب است:
public FieldOfWork FieldOfWork { get; set; }
بعدا حالت قبلی را بررسی میکنیم.

کد کلاس مبدل را به صورت زیر می‌نویسیم:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class EnumConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

            if (Enum.IsDefined(value.GetType(), value) == false)
                return DependencyProperty.UnsetValue;

            object paramvalue = Enum.Parse(value.GetType(), ParameterString);
            return paramvalue.Equals(value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
در مرحله اول مثل کد قبلی بررسی می‌شود که آیا پارامتری ارسال شده است یا خیر. در مرحله دوم بررسی می‌شود که نوع داده مقدار پراپرتی چیست یعنی چه Enum ایی مورد استفاده قرار گرفته است. اگر در Enum مقداری که در پارامتر به آن ذکر شده است وجود نداشته باشد، بهتر هست که کار در همین جا به پایان برسد؛ زیرا که یک پارامتر اشتباهی ارسال شده است و چنین مقداری در Enum وجود ندارد. در غیر اینصورت کار ادامه می‌یابد و پارامتر را به enum تبدیل کرده و با مقدار مقایسه می‌کنیم. اگر برابر باشند نتیجه true را باز میگردانیم.

کد قسمت ریسورس را با کلاس جدید به روز می‌کنیم:
    <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
        <valueConverters:EnumConverter x:Key="EnumConverter"></valueConverters:EnumConverter>
    </Window.Resources>
کد چک باکس‌ها هم به شکل زیر تغییر می‌یابد:
    <CheckBox Name="ChkActor" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Actor}" >Actor/Actress</CheckBox>
                <CheckBox Name="ChkDirector" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Director}" >Director</CheckBox>
                <CheckBox Name="ChkProducer" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumConverter}, ConverterParameter=Producer}" >Producer</CheckBox>
کلاس GetPerson که منبع داده ما را فراهم می‌کند هم به شکل زیر است:
  public static Person GetPerson()
        {
            return new Person()
            {
                Name = "Leo",
                Gender =true,
                ImageName ="man.jpg",
                Country = "Italy",
                FieldOfWork = test.FieldOfWork.Actor,
                Date = DateTime.Now.AddDays(-3)
            };
        }

برنامه را اجرا کنید تا نتیجه کار را ببینید. باید چک باکس Actor تیک خورده باشد. میتوانید منبع داده را تغییر داده تا نتیجه کار را ببینید.

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

کلاس را به همراه کد GetPerson به شکل زیر تغییر می‌دهیم:
    public enum FieldOfWork
    {
        Actor=0,
        Director=1,
        Producer=2
    }
    public class Person : INotifyPropertyChanged
    { 
        public bool Gender { get; set; }

        public string ImageName { get; set; }

        public string Country { get; set; }

        public DateTime Date { get; set; }

        public IList<FieldOfWork> FieldOfWork { get; set; }

        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        }

        public static Person GetPerson()
        {
            return new Person
            {
                Name = "Leo",
                Gender = true,
                ImageName = "man.jpg",
                Country = "Italy",
                FieldOfWork = new FieldOfWork[] { test.FieldOfWork.Actor, test.FieldOfWork.Producer },
                Date = DateTime.Now.AddDays(-3)
            };
        }
}

دو Enum بازیگر و تهیه کننده را انتخاب کرده‌ایم، پس در زمان اجرا باید این دو گزینه انتخاب شوند.
کد مبدل را به صورت زیر می‌نویسیم:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace test.ValueConverters
{
    public class EnumList : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ParameterString = parameter as string;
            if (ParameterString == null)
                return DependencyProperty.UnsetValue;

         var enumlist= (IList) value;         

            if(enumlist==null && enumlist.Count<1)
                return DependencyProperty.UnsetValue;

            if (Enum.IsDefined(enumlist[0].GetType(), ParameterString) == false)
                return DependencyProperty.UnsetValue;


            /*
              foreach (var item  in enumlist)
            {
                object paramvalue = Enum.Parse(item.GetType(), ParameterString);
                bool result = item.Equals(paramvalue);
                if (result)
                    return true;
            }
            return false;
             */
            return (from object item in enumlist let paramvalue = Enum.Parse(item.GetType(), ParameterString) select item.Equals(paramvalue)).Any(result => result);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
مقدار پراپرتی را به نوع IList تبدیل می‌کنید و اگر لیست معتبر بود، برنامه را ادامه می‌دهیم؛ در غیر اینصورت تابع خاتمه می‌یابد. بعد از اینکه صحت وجود لیست اعلام شد، بررسی میکنیم آیا Enum چنین مقداری که پارامتر ذکر کرده است را دارد یا یک پارامتر اشتباهی است.
در حلقه‌ای که به شکل توضیح درآمده، همه آیتم‌های مربوطه در لیست را بررسی کرده و اگر آیتمی برابر پارامتر باشد، True بر میگرداند و در صورتی که حلقه به اتمام برسد و آیتم پیدا نشود، مقدار False را برمی‌گرداند. این حلقه از آن جهت به شکل توضیح درآمده است که کد Linq آن در زیر نوشته شده است.

تعریف کلاس بالا در ریسورس:
   <Window.Resources>
        <valueConverters:GenderConverter x:Key="GenderConverter"/>
        <valueConverters:EnumConverter x:Key="EnumConverter"></valueConverters:EnumConverter>
        <valueConverters:EnumList x:Key="EnumList"></valueConverters:EnumList>
    </Window.Resources>
و تغییر کلید ریسورس در خطوط چک باکس ها، باقی موارد همانند قبل ثابت هستند:
     <CheckBox Name="ChkActor" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Actor}" >Actor/Actress</CheckBox>
                <CheckBox Name="ChkDirector" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Director}" >Director</CheckBox>
                <CheckBox Name="ChkProducer" IsChecked="{Binding FieldOfWork, Converter={StaticResource EnumList}, ConverterParameter=Producer}" >Producer</CheckBox>

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

  بدیهی است خروجی‌های بالا برای کنترل هایی است که مقدار Boolean را می‌پذیرند و برای سایر کنترل‌ها باید با کمی تغییر در کد و نوع برگشتی که تحویل خروجی متد مبدل می‌شود، دهید.

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

متدی تحت عنوان ValidateEmail را تصور کنید. این متد از حیث بازگشت نتیجه به عنوان خروجی می‌تواند به اشکال مختلفی پیاده سازی شود که در ادامه مشاهده می‌کنیم:


متد ValidateEmail با خروجی Boolean

        public bool ValidateEmail(string email)
        {
            var valid = true;
            if (string.IsNullOrWhiteSpace(email))
            {
                valid = false;
            }

            var isValidFormat = true;//todo: using RegularExpression
            if (!isValidFormat)
            {
                valid = false;
            }

            var isRealDoamin = true;//todo: Code here that confirms whether domain exists.
            if (!isRealDoamin)
            {
                valid = false;
            }

            return valid;
        }

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

var email = "email@example.com";
var isValid = ValidateEmail(email);
if(isValid)
{
    //do something
}


متد ValidateEmail با صدور استثناء

        public void ValidateEmail(string email)
        {
            if (string.IsNullOrWhiteSpace(email)) throw new ArgumentNullException(nameof(email));

            var isValidFormat = true;//todo: using RegularExpression
            if (!isValidFormat) throw new ArgumentException("email is not in a correct format");

            var isRealDoamin = true;//todo: Code here that confirms whether domain exists.
            if (!isRealDoamin) throw new ArgumentException("email does not include a valid domain.")
        }

روش بالا هم جواب می‌دهد ولی بهتر است کلاس Exception سفارشی به عنوان مثال ValidationException برای این قضیه در نظر گرفته شود تا بتوان وهله‌های صادر شده از این نوع را در لایه‌های بالاتر مدیریت کرد.


متد ValidateEmail با چندین خروجی


برای این منظور چندین راه حل پیش رو داریم.


با استفاده از پارامتر out:

        public bool ValidateEmail(string email, out string message)
        {
            var valid = true;
            message = string.Empty;

            if (string.IsNullOrWhiteSpace(email))
            {
                valid = false;
                message = "email is null.";
            }

            if (valid)
            {
                var isValidFormat = true;//todo: using RegularExpression
                if (!isValidFormat)
                {
                    valid = false;
                    message = "email is not in a correct format";
                }
            }

            if (valid)
            {
                var isRealDoamin = true;//todo: Code here that confirms whether domain exists.
                if (!isRealDoamin)
                {
                    valid = false;
                    message = "email does not include a valid domain.";
                }
            }

            return valid;
        }
و نحوه استفاده از آن:
var email = "email@example.com";
var isValid = ValidateEmail(email, out string message);
if (isValid)
{
    //do something
}
خب کمی بهتر شد؛ ولی امکان دریافت لیست خطاهای اعتبارسنجی را به صورت یکجا نداریم و یک تک پیغام را در اختیار ما قرار می‌دهد. برای بهبود آن می‌توان از یک Tuple به شکل زیر برای تولید خروجی متد بالا نیز استفاده کرد.
Tuple<bool, List<string>> result = Tuple.Create<bool, List<string>>(true, new List<string>());
یا بهتر است یک کلاس مشخصی برای این منظور در نظر گرفت؛ به عنوان مثال:
        public class OperationResult
        {
            public bool Success { get; set; }
            public IList<string> Messages { get; } = new List<string>();

            public void AddMessage(string message)
            {
                Messages.Add(message);
            }
        }
در این صورت بدنه متد ValidateEmail به شکل زیر تغییر خواهد کرد:
        public OperationResult ValidateEmail(string email)
        {
            var result = new OperationResult();

            if (string.IsNullOrWhiteSpace(email))
            {
                result.Success = false;
                result.AddMessage("email is null.");
            }

            if (result.Success)
            {
                var isValidFormat = true;//todo: using RegularExpression
                if (!isValidFormat)
                {
                    result.Success = false;
                    result.AddMessage("email is not in a correct format");
                }
            }

            if (result.Success)
            {
                var isRealDoamin = true;//todo: Code here that confirms whether domain exists.
                if (!isRealDoamin)
                {
                    result.Success = false;
                    result.AddMessage("email does not include a valid domain.");
                }
            }

            return result;
        }

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


استفاده از Exception برای نمایش پیغام برای کاربر نهایی

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

به عنوان مثال داشتن یک کلاس Exception سفارشی تحت عنوان UserFriendlyException در این راستا یک الزام می‌باشد.

   [Serializable]
   public class UserFriendlyException : Exception
   {
       public string Details { get; private set; }
       public int Code { get; set; }

       public UserFriendlyException()
       {
       }

       public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context)
           : base(serializationInfo, context)
       {

       }

       public UserFriendlyException(string message)
           : base(message)
       {
       }

       public UserFriendlyException(int code, string message)
           : this(message)
       {
           Code = code;
       }

       public UserFriendlyException(string message, string details)
           : this(message)
       {
           Details = details;
       }

       public UserFriendlyException(int code, string message, string details)
           : this(message, details)
       {
           Code = code;
       }

       public UserFriendlyException(string message, Exception innerException)
           : base(message, innerException)
       {
       }

       public UserFriendlyException(string message, string details, Exception innerException)
           : this(message, innerException)
       {
           Details = details;
       }
   }

و همچنین لازم است در بالاترین لایه سیستم خود به عنوان مثال برای یک پروژه ASP.NET MVC یا ASP.NET Core MVC می‌توان یک ExceptionFilter سفارشی نیز تهیه کرد که هم به صورت سراسری استثنا‌ءهای سفارشی شما را مدیریت کند و همچنین خروجی مناسب Json برای استفاده در سمت کلاینت را نیز مهیا کند. به عنوان مثال برای درخواست‌های Ajax ای لازم است در سمت کلاینت نیز پاسخ‌های رسیده از سمت سرور به صورت سراسری مدیریت شوند و برای سایر درخواست‌ها همان نمایش صفحات خطای پیغام مرتبط با استثناء رخ داده شده کفایت می‌کند.


یک مدل پیشنهادی برای تهیه خروجی مناسب برای ارسال جزئیات استثنا رخ داده در درخواست‌های Ajax ای

    [Serializable]
    public class MvcAjaxResponse : MvcAjaxResponse<object>
    {
        public MvcAjaxResponse()
        {
        }

        public MvcAjaxResponse(bool success)
            : base(success)
        {
        }

        public MvcAjaxResponse(object result)
            : base(result)
        {
        }

        public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false)
            : base(error, unAuthorizedRequest)
        {
        }
    }
   

    [Serializable]
    public class MvcAjaxResponse<TResult> : MvcAjaxResponseBase
    {
        public MvcAjaxResponse(TResult result)
        {
            Result = result;
            Success = true;
        }

        public MvcAjaxResponse()
        {
            Success = true;
        }

        public MvcAjaxResponse(bool success)
        {
            Success = success;
        }

        public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false)
        {
            Error = error;
            UnAuthorizedRequest = unAuthorizedRequest;
            Success = false;
        }

        /// <summary>
        ///     The actual result object of AJAX request.
        ///     It is set if <see cref="MvcAjaxResponseBase.Success" /> is true.
        /// </summary>
        public TResult Result { get; set; }
    }

    public class MvcAjaxResponseBase
    {
        public string TargetUrl { get; set; }

        public bool Success { get; set; }

        public ErrorInfo Error { get; set; }

        public bool UnAuthorizedRequest { get; set; }

        public bool __mvc { get; } = true;
    }

و کلاس  ErrorInfo:
    [Serializable]
    public class ErrorInfo
    {
        public int Code { get; set; }
        public string Message { get; set; }
        public string Detail { get; set; }
        public Dictionary<string, string> ValidationErrors { get; set; }

        public ErrorInfo()
        {
        }
        public ErrorInfo(string message)
        {
            Message = message;
        }
        public ErrorInfo(int code)
        {
            Code = code;
        }

        public ErrorInfo(int code, string message)
            : this(message)
        {
            Code = code;
        }

        public ErrorInfo(string message, string details)
            : this(message)
        {
            Detail = details;
        }

        public ErrorInfo(int code, string message, string details)
            : this(message, details)
        {
            Code = code;
        }
    }

یک مثال واقعی
        public async Task CheckIsDeactiveAsync(long id)
        {
            if (await _organizationalUnits.AnyAsync(a => a.Id == id && !a.IsActive).ConfigureAwait(false))
                throw new UserFriendlyException("واحد سازمانی جاری غیرفعال می‌باشد.");
        }

روش نام گذاری متدهایی که امکان بازگشت خروجی Null را دارند

متد زیر را در نظر بگیرید:
public User GetById(long id);
وظیفه این متد یافت و بازگشت یک وهله از کلاس User می‌باشد و نباید خروجی Null تولید کند. در صورتیکه در پیاده سازی آن امکان یافت چنین کاربری نبود، بهتر است یک استثنای سفارشی دیگر شبیه به EntityNotFoundException زیر را صادر کنید:
    [Serializable]
    public class EntityNotFoundException : Exception
    {
        public Type EntityType { get; set; }
        public object Id { get; set; }
        public EntityNotFoundException()
        {
        }

        public EntityNotFoundException(string message)
            : base(message)
        {

        }

        public EntityNotFoundException(string message, Exception innerException)
            : base(message, innerException)
        {
        }

        public EntityNotFoundException(SerializationInfo serializationInfo, StreamingContext context)
            : base(serializationInfo, context)
        {

        }
        public EntityNotFoundException(Type entityType, object id)
            : this(entityType, id, null)
        {

        }
        public EntityNotFoundException(Type entityType, object id, Exception innerException)
            : base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException)
        {
            EntityType = entityType;
            Id = id;
        }

    }
یا اگر امکان بازگشت مقدار Null را داشته باشد، بهتر است نام آن به GetByIdOrNull تغییر یابد. در این صورت تکلیف استفاده کننده از این متد مشخص می‌باشد.

یک مثال واقعی 

        public async Task<UserOrganizationalUnitInfo> GetCurrentOrganizationalUnitInfoOrNullAsync(long userId)
        {
            return (await _setting.GetSettingValueForUserAsync(
                    UserSettingNames.CurrentOrganizationalUnitInfo, userId).ConfigureAwait(false))
                .FromJsonString<UserOrganizationalUnitInfo>();
        }

مطالب
کدام سلسله متدها، متد جاری را فراخوانی کرده‌اند؟
یکی از نیازهای نوشتن یک برنامه‌ی پروفایلر، نمایش اطلاعات متدهایی است که سبب لاگ شدن اطلاعاتی شده‌اند. برای مثال در طراحی interceptorهای EF 6 به یک چنین متدهایی می‌رسیم:
        public void ScalarExecuted(DbCommand command,
                                   DbCommandInterceptionContext<object> interceptionContext)
        {
        }

سؤال: در زمان اجرای ScalarExecuted دقیقا در کجا قرار داریم؟ چه متدی در برنامه، در کدام کلاس، سبب رسیدن به این نقطه شده‌است؟
تمام این اطلاعات را در زمان اجرا توسط کلاس StackTrace می‌توان بدست آورد:
        public static string GetCallingMethodInfo()
        {
            var stackTrace = new StackTrace(true);
            var frameCount = stackTrace.FrameCount;

            var info = new StringBuilder();
            var prefix = "-- ";
            for (var i = frameCount - 1; i >= 0; i--)
            {
                var frame = stackTrace.GetFrame(i);
                var methodInfo = getStackFrameInfo(frame);
                if (string.IsNullOrWhiteSpace(methodInfo))
                    continue;

                info.AppendLine(prefix + methodInfo);
                prefix = "-" + prefix;
            }

            return info.ToString();
        }
ایجاد یک نمونه جدید از کلاس StackTrace با پارامتر true به این معنا است که می‌خواهیم اطلاعات فایل‌های متناظر را نیز در صورت وجود دریافت کنیم.
خاصیت stackTrace.FrameCount مشخص می‌کند که در زمان فراخوانی متد GetCallingMethodInfo که اکنون برای مثال درون متد ScalarExecuted قرار گرفته‌است، از چند سطح بالاتر این فراخوانی صورت گرفته‌است. سپس با استفاده از متد stackTrace.GetFrame می‌توان به اطلاعات هر سطح دسترسی یافت.
در هر StackFrame دریافتی، با فراخوانی stackFrame.GetMethod می‌توان نام متد فراخوان را بدست آورد. متد stackFrame.GetFileLineNumber دقیقا شماره سطری را که فراخوانی از آن صورت گرفته، بازگشت می‌دهد و stackFrame.GetFileName نیز نام فایل مرتبط را مشخص می‌کند.

یک نکته:
شرط عمل کردن متدهای stackFrame.GetFileName و stackFrame.GetFileLineNumber در زمان اجرا، وجود فایل PDB اسمبلی در حال بررسی است. بدون آن اطلاعات محل قرارگیری فایل سورس مرتبط و شماره سطر فراخوان، قابل دریافت نخواهند بود.


اکنون بر اساس این اطلاعات، متد getStackFrameInfo چنین پیاده سازی را خواهد داشت:
        private static string getStackFrameInfo(StackFrame stackFrame)
        {
            if (stackFrame == null)
                return string.Empty;

            var method = stackFrame.GetMethod();
            if (method == null)
                return string.Empty;

            if (isFromCurrentAsm(method) || isMicrosoftType(method))
            {
                return string.Empty;
            }

            var methodSignature = method.ToString();
            var lineNumber = stackFrame.GetFileLineNumber();
            var filePath = stackFrame.GetFileName();

            var fileLine = string.Empty;
            if (!string.IsNullOrEmpty(filePath))
            {
                var fileName = Path.GetFileName(filePath);
                fileLine = string.Format("[File={0}, Line={1}]", fileName, lineNumber);
            }

            var methodSignatureFull = string.Format("{0} {1}", methodSignature, fileLine);
            return methodSignatureFull;
        }
و خروجی آن برای مثال چنین شکلی را خواهد داشت:
 Void Main(System.String[]) [File=Program.cs, Line=28]
که وجود file و line آن تنها به دلیل وجود فایل PDB اسمبلی مورد بررسی است.

در اینجا خروجی نهایی متد GetCallingMethodInfo به شکل زیر است که در آن چند سطح فراخوانی را می‌توان مشاهده کرد:
 -- Void Main(System.String[]) [File=Program.cs, Line=28]
--- Void disposedContext() [File=Program.cs, Line=76]
---- Void Opened(System.Data.Common.DbConnection, System.Data.Entity.Infrastructure.Interception.DbConnectionInterceptionContext) [File=DatabaseInterceptor.cs,Line=157]

جهت تعدیل خروجی متد GetCallingMethodInfo، عموما نیاز است مثلا از کلاس یا اسمبلی جاری صرفنظر کرد یا اسمبلی‌های مایکروسافت نیز در این بین شاید اهمیتی نداشته باشند و بیشتر هدف بررسی سورس‌های موجود است تا فراخوانی‌های داخلی یک اسمبلی ثالث:
        private static bool isFromCurrentAsm(MethodBase method)
        {
            return method.ReflectedType == typeof(CallingMethod);
        }

        private static bool isMicrosoftType(MethodBase method)
        {
            if (method.ReflectedType == null)
                return false;

            return method.ReflectedType.FullName.StartsWith("System.") ||
                   method.ReflectedType.FullName.StartsWith("Microsoft.");
        }


کد کامل CallingMethod.cs را از اینجا می‌توانید دریافت کنید:
CallingMethod.cs
مطالب
Image Annotations
می‌خواهیم با تغییر jQuery Image Annotation  این پلاگین و برای asp.net استفاده کنیم

ایجاد دیتابیس
ابتدا یک دیتابیس به نام Coordinates  ایجاد کنید و سپس جدول زیر رو ایجاد کنید
USE [Coordinates]
GO
CREATE TABLE [dbo].[Coords2](
[top] [int] NULL,
[left] [int] NULL,
[width] [int] NULL,
[height] [int] NULL,
[text] [nvarchar](50) NULL,
[id] [uniqueidentifier] NULL,
[editable] [bit] NULL
) ON [PRIMARY]
GO

ایجاد کلاس Coords برای خواندن و ذخیره اطلاعات
public class Coords
{
    public string top;
    public string left;
    public string width;
    public string height;
    public string text;
    public string id;
    public string editable;

    public Coords(string top, string left, string width, string height, string text, string id, string editable)
    {
        this.top = top;
        this.left = left;
        this.width = width;
        this.height = height;
        this.text = text;
        this.id = id;
        this.editable = editable;


    }
}
فرم اصلی برنامه شامل 3 وب سرویس به شرح زیر می‌باشد

1-GetDynamicContext 
این متد در زمان لود اطلاعات از دیتابیس استفاده می‌شود (وقتی که postback صورت می‌گیرد)
[WebMethod]
    public static List<Coords> GetDynamicContext(string entryId, string entryName)
    {
        List<Coords> CoordsList = new List<Coords>();

        string connect = "Connection String";
        using (SqlConnection conn = new SqlConnection(connect))
        {
            string query = "SELECT [top], [left], width, height, text, id, editable FROM  Coords2";
            using (SqlCommand cmd = new SqlCommand(query, conn))
            {
                conn.Open();
                using (SqlDataReader reader=cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        CoordsList.Add(new Coords(reader["top"].ToString(), reader["left"].ToString(),
                                                  reader["width"].ToString(), reader["height"].ToString(),
                                                  reader["text"].ToString(), reader["id"].ToString(),
                                                  reader["editable"].ToString()));
                    }
                }
               
               conn.Close();
            }
        }

         return CoordsList;


    }

2,3 -SaveCoordsو DeleteCoords 
این دو متد هم واسه ذخیره و حذف می‌باشند که نکته خاصی ندارند و خودتون بهینه اش کنید(در فایل ضمیمه موجودند)

تغییر فایل jquery.annotate.js  جهت فراخوانی وب سرویس ها
فقط لازمه که سه قسمت زیر رو در فایل اصلی تغییر بدید
$.fn.annotateImage.ajaxLoad = function (image) {
        ///<summary>
        ///Loads the annotations from the "getUrl" property passed in on the
        ///     options object.
        ///</summary>
      
        $.ajax({
            type: "POST",
            contentType: "application/json; charset=utf-8",
            url: "Default.aspx/GetDynamicContext",
            data: "{'entryId': '" + 1 + "','entryName': '" + 2 + "'}",
            dataType: "json",
            success: function (msg) {
                image.notes = msg.d;
                $.fn.annotateImage.load(image);
            }
        });
};

 $.fn.SaveCoords = function (note) {
        $.ajax({
            type: "POST",
            contentType: "application/json; charset=utf-8",
            url: "Default.aspx/SaveCoords",
            data: "{'top': '" + note.top + "','left': '" + note.left + "','width': '" + note.width + "','height': '" + note.height + "','text': '" + note.text + "','id': '" + note.id + "','editable': '" + note.editable + "'}",
            dataType: "json",
            success: function (msg) {
                note.id = msg.d;
             }
        });
 };
$.fn.annotateView.prototype.edit = function () {
///<summary>
///Edits the annotation.
///</summary>
if (this.image.mode == 'view') {
this.image.mode = 'edit';
var annotation = this;
// Create/prepare the editable note elements
var editable = new $.fn.annotateEdit(this.image, this.note);
$.fn.annotateImage.createSaveButton(editable, this.image, annotation);
// Add the delete button
var del = $('<a>حذف</a>');
del.click(function () {
var form = $('#image-annotate-edit-form form');
$.fn.annotateImage.appendPosition(form, editable)
if (annotation.image.useAjax) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "Default.aspx/DeleteCoords",
// url: annotation.image.deleteUrl,
// data: form.serialize(),
data: "{'id': '" + editable.note.id + "'}",
dataType: "json",
success: function (msg) {
// image.notes = msg.d;
// $.fn.annotateImage.load(image);
},
error: function (e) { alert("An error occured deleting that note.") }
});
}
annotation.image.mode = 'view';
editable.destroy();
annotation.destroy();
});
editable.form.append(del);
$.fn.annotateImage.createCancelButton(editable, this.image);
}
};
این پروژه شامل یه سری فایل css هم هست که می‌تونید کل پروژه رو از  اینجا دانلود کنید
مطالب
آشنایی با کلاس JavaScriptSerializer

برای استفاده از jQuery Ajax یکی از روش‌های ارسال دیتا به برنامه، تبدیل داده‌ها به فرمت JSON می‌باشد. برای داده‌های ساده، تشخیص این فرمت ساده است. مثلا اگر امضای تابع وب سرویس اجکس ما به صورت زیر باشد:
public static bool IsUserAvailable(string username)
اطلاعات جی‌سونی را که قرار است ارسال کنیم، فرمت زیر را باید داشته باشد:
{'username':'value'}
حال اگر آرگومان‌های ما پیچیده‌تر بودند چطور؟ مثلا بجای یک رشته ساده، یک لیست جنریک داشتیم، فرمت ورودی را چگونه باید تشخیص داد؟
برای این منظور در دات نت 3 و نیم، کلاسی جهت انجام اینگونه تبدیلات پیش بینی شده است که شرح مختصر آن به صورت زیر است:
ابتدا باید ارجاعی را به اسمبلی system.web.extensions به برنامه افزود و سپس جهت سهولت کار می‌توان یک extension method از کلاس JavaScriptSerializer مهیا در فضای نام System.Web.Script.Serialization ایجاد کرد:

public static string ToJson(this object data)
{
return new JavaScriptSerializer().Serialize(data);
}
اکنون چند مثال زیر را در نظر بگیرید:
        public static string GetJsonTest0()
{
var data = "a1";
return data.ToJson();
}

public static string GetJsonTest1()
{
var data = new List<string> { "a1", "a2", "a3" };
return data.ToJson();
}

public static string GetJsonTest2()
{

var lst =
new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("Name", "علی"),
new KeyValuePair<string, object>("Number", 10),
new KeyValuePair<string, object>("Desc", "منابع مورد نیاز")
};

return lst.ToJson();

}
خروجی‌های آن‌ها به ترتیب به صورت زیر خواهند بود:

"a1"
["a1","a2","a3"]
[{"Key":"Name","Value":"علی"},{"Key":"Number","Value":10},{"Key":"Desc","Value":"منابع مورد نیاز"}]

این کلاس همچنین قابلیت Deserialize و تبدیل داده‌هایی به فرمت JSON به اشیاء مورد نظر ما را نیز دارا است.

مطالب
ویژگی های کمتر استفاده شده در NET. - بخش اول

ObsoleteAttribute

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

با استفاده از پروپرتی Message آن پیامی را به کاربر استفاده کننده نشان خواهد داد و توصیه می‌شود در این پیام یک راه حل نیز ارائه شود.

پروپرتی IsError در صورتی که مقدار آن به true تعیین شده باشد و کامپایلر در صورتی که عنصری که این خصوصیت بر روی آن تعریف شده است، استفاده شده باشد، در پنجره Error List، پیام مربوط به Obsolete را نشان می‌دهد. برای مثال پس از استفاده از کلاس زیر، OrderDetailTotal به صورت warning و CalculateOrderDetailTotal به صورت Error در پنجره Error List نشان داده می‌شود.

public static class ObsoleteExample
{
    // Mark OrderDetailTotal As Obsolete.
    [ObsoleteAttribute("This property (OrderDetailTotal) is obsolete. Use InvoiceTotal instead.", false)]
    public static decimal OrderDetailTotal
    {
        get  {  return 12m; }
    }

    public static decimal InvoiceTotal
    {
        get  {  return 25m;  }
    }

    // Mark CalculateOrderDetailTotal As Obsolete.
    [ObsoleteAttribute("This method is obsolete. Call CalculateInvoiceTotal instead.", true)]
    public static decimal CalculateOrderDetailTotal()
    {
        return 0m;
    }

    public static decimal CalculateInvoiceTotal()
    {
        return 1m;
    }
}

DefaultValueAttribute

DefaultValueAttribute جهت تعیین مقدار پیش فرض یک پروپرتی استفاده می‌شود. شما می‌توانید یک DefaultValueAttribute را با هر مقداری ایجاد کنید. ایجاد مقدار پیش فرض برای یک پروپرتی باعث نمی‌شود که مقداردهی اولیه‌ای به آن انجام گیرد؛ برای این کار نیاز به کدنویسی می‌باشد.
مثال زیر نحوه استفاده و مقداردهی اولیه پروپرتی‌ها را نشان می‌دهد.
public class DefaultValueAttributeTest
{
    public DefaultValueAttributeTest()
    {
        // Use the DefaultValue propety of each property to actually set it, via reflection.
        foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(this))
        {
            var attr = prop.Attributes[typeof(DefaultValueAttribute)] as DefaultValueAttribute;
            if (attr != null)
                prop.SetValue(this, attr.Value);
        }
    }

    [DefaultValue(28)]
    public int Age { get; set; }

    [DefaultValue("Vahid")]
    public string FirstName { get; set; }

    [DefaultValue("Mohammad Taheri")]
    public string LastName { get; set; }

    public override string ToString()
    {
        return $"{this.FirstName} {this.LastName} is {this.Age}.";
    }
}

DebuggerBrowsableAttribute 

در صورت استفاده از DebuggerBrowsableAttribute ، شما می‌توانید نحوه نمایش یک عضو را در پنجره متغیرها، در زمان دیباگ، تعیین کنید.
public class DebuggerBrowsableTest
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)] // عدم نمایش در زمان دیباگ در پنجره متغیرها
    public string FirstName { get; set; }

    [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] // مقدار پیش فرض
    public string LastName { get; set; }

    [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] // عدم نمایش در زمان دیباگ در پنجره متغیرها
    public string FullName => FirstName + " " + LastName;

    [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] // تنها در زمانی که یک آرایه یا لیست باشد نمایش داده می‌شود
    public string[] FullNameArray => new string[] { FirstName + " " + LastName };
}

 اگر از کد مثال بالا استفاده کنید و با استفاده از کلید F11 به صورت خط به خط دستورات را اجرا کنید، مشاهده خواهید کرد متغیر FirstName و FullName در پنجره Autos نشان داده نخواهد شد.

 

Operator ??

عملگر ??  در صورتی که عملوند سمت چپ آن تهی (null) نباشد، مقدار آن را باز می‌گرداند و در غیر اینصورت مقدار عملوند سمت راست خود را باز می‌گرداند. نوع‌های تهی پذیر (nullable) می‌توانند دارای مقدار و یا به صورت تعریف نشده باشند. عملگر ?? وقتی که یک نوع تهی پذیر به یک نوع غیرتهی پذیر انتساب داده می‌شود، مقدار پیش فرض آن را باز می‌گرداند.

int? x = null;
int y = x ?? -1;
Console.WriteLine("y now equals -1 because x was null => {0}", y);
int i = DefaultValueOperatorTest.GetNullableInt() ?? default(int);
Console.WriteLine("i equals now 0 because GetNullableInt() returned null => {0}", i);
string s = DefaultValueOperatorTest.GetStringValue();
Console.WriteLine("Returns 'Unspecified' because s is null => {0}", s ?? "Unspecified");
مطالب
روش تولید خودکار URLها در برنامه‌های ASP.NET Core
پیشتر مطلب «نحوه صحیح تولید Url در ASP.NET MVC» را در این سایت مطالعه کرده‌اید و خلاصه‌ی آن به این صورت است که اگر در جائی از برنامه‌ی خود، مسیر Home/Details/1 را به صورت دستی وارد کرده‌اید، با تغییر الگوی مسیریابی برنامه برای مثال به صورت "uni/{controller=Home}/{action=Index}/{id?}" در آینده، مسیر یاد شده دیگر معتبر و قابل دسترسی نبوده و نیاز خواهید داشت تمام مسیرهای دستی وارد شده‌ی اینگونه را در سراسر برنامه اصلاح کنید. به همین جهت در این مطلب روش جدید تولید خودکار URL‌ها را در برنامه‌های ASP.NET Core بررسی می‌کنیم.


تولید URL‌های منتهی به اکشن متدها در کنترلرها

هنوز هم در اکشن متدهای ASP.NET Core می‌توان از متد Url.Action برای تولید لینکی به سایر اکشن متدهای کنترلرهای دیگر استفاده کرد. البته اینبار مقادیر مسیریابی آن به عنوان پارامتر سوم باید وارد شوند و همچنین می‌توان بر اساس Scheme جاری، به صورت خودکار http و یا https را در ابتدای URL درج کرد (البته ذکر Scheme سبب تولید URLهای مطلق می‌شود؛ اگر نیاز به مسیرهای نسبی است، آن‌را ذکر نکنید):
var url = this.Url.Action("About", "Home", new { id = 1 }, this.Request.Scheme);

اما اگر در اکشن متدهای کنترلرها قرار نداشتیم چطور؟
در هر قسمتی از برنامه که دسترسی به httpContext وجود دارد، می‌توان به سرویس IUrlHelper آن نیز دسترسی یافت (this.Url در یک اکشن متد، وهله‌ای از IUrlHelper است):
var urlHelper = httpContext.RequestServices.GetRequiredService<IUrlHelper>()
و پس از آن برای نمونه متد urlHelper.Action یاد شده (معادل this.Url.Action) در دسترس می‌باشد.


تولید URL‌های منتهی به اکشن متدها در خارج از کنترلرها

فرض کنید می‌خواهید در متد Configure فایل آغازین برنامه، آدرس منتهی به یک اکشن متد خاصی را تولید کنید و یا در یک میان‌افزار که عملکرد آن باتوجه به محل قرارگیری آن، پیش از رخ‌دادن و اجرای میان‌افزار MVC است. در این حالت دیگر روش IUrlHelper یاد شده کار نمی‌کند؛ چون در این مکان‌ها دسترسی به Action Context میان‌افزار MVC وجود ندارد و هنوز این میان‌افزار اجرا نشده‌است.
برای رفع این مشکل، از زمان ASP.NET Core 2.2 به بعد، سرویس توکار جدیدی به نام LinkGenerator اضافه شده‌است که الزاما برای کار کردن، نیازی به Http Context و همچنین Action Context را ندارد. برای مثال اگر در متد void Configure(IApplicationBuilder app, IWebHostEnvironment env)، دسترسی به app وجود دارد، توسط آن می‌توان سرویس LinkGenerator را دریافت کرد و سپس با کمک متد GetPathByAction آن، مسیر منتهی به یک اکشن متد خاص را به صورت خودکار تولید کرد:
var generator = app.ApplicationServices.GetRequiredService<LinkGenerator>();
var controllerName = nameof(HomeController).Replace("Controller", "");
var url = generator.GetPathByAction(nameof(HomeController.Index), controllerName)
بدیهی است اگر در قسمتی از برنامه امکان تزریق وابستگی‌ها در سازنده‌ی کلاس مدنظر وجود داشته باشد، می‌توان LinkGenerator را به آن تزریق کرد (بدون نیاز به تنظیم خاصی) و از امکانات آن استفاده کرد. طول عمر LinkGenerator به صورت پیش‌فرض به Singleton تنظیم شده‌است. بنابراین می‌توان آن‌را به سازنده‌ی یک میان‌افزار نیز تزریق کرد (چون طول عمر میان‌افزاها نیز Singleton است):
public class MyMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public MyMiddleware(RequestDelegate next, LinkGenerator linkGenerator) 
   {
       _linkGenerator = linkGenerator;
   }   

    public async Task Invoke(HttpContext httpContext)
    {
        var url = _linkGenerator.GenerateLink(new { controller = "Store", action = "ListProducts" });
    
        httpContext.Response.ContentType = "text/plain";
        return httpContext.Response.WriteAsync($"Go to {url} to see the list of products.");
    }
}
و یا حتی روش ()<var linkGenerator = httpContext.RequestServices.GetService<LinkGenerator نیز در اینجا برای دسترسی به سرویس LinkGenerator کار می‌کند. به علاوه امکان تزریق مستقیم آن به Viewها و صفحات Razor نیز وجود دارد

یک نکته: متد GetUriByAction امکان دریافت HttpContext را نیز دارد:
public static string GetPathByAction(this LinkGenerator generator,
 HttpContext httpContext, string action = null, string controller = null,
 object values = null, PathString? pathBase = null,
 FragmentString fragment = default, LinkOptions options = null);
مانند:
var url = _linkGenerator.GetUriByAction(_accessor.HttpContext,
action: "GetContentByFileId",
values: new { FileId = 1 }
);
در این مثال accessor همان IHttpContextAccessor تزریق شده‌ی به یک سرویس خاص است. مزیت این روش، عدم نیاز به تکمیل سایر پارامترهای متد GetUriByAction است. اگر متد GetUriByAction را بدون HttpContext استفاده کنید، نیاز خواهید داشت تعداد پارامترهای بیشتری از آن‌را برای راهنمایی به آن تکمیل کنید:
public static string GetPathByAction(this LinkGenerator generator, string action, string controller,
 object values = null, PathString pathBase = default,
FragmentString fragment = default, LinkOptions options = null);