اشتراک‌ها
Decrypting/Encrypting تنظیمات برنامه در ASP.NET CORE
مطالب
Blazor 5x - قسمت سوم - مبانی Razor
پیش از شروع به کار توسعه‌ی برنامه‌های مبتنی بر Blazor، باید با مبانی Razor آشنایی داشت. Razor امکان ترکیب کدهای #C و HTML را در یک فایل میسر می‌کند. دستور زبان آن از @ برای سوئیچ بین کدهای #C و HTML استفاده می‌کند. کدهای Razor را می‌توان در فایل‌های cshtml. نوشت که عموما مخصوص صفحات و Viewها هستند و یا در فایل‌های razor. که برای توسعه‌ی کامپوننت‌های Balzor بکار گرفته می‌شوند. در اینجا مهم نیست که پسوند فایل مورد استفاده چیست؛ چون اصول razor بکار گرفته شده در آن‌ها یکی است. البته در اینجا تاکید ما بیشتر بر روی فایل‌های razor. است که در برنامه‌های مبتنی بر Blazor بکار گرفته می‌شوند.


ایجاد یک پروژه‌ی جدید Blazor WASM

برای پیاده سازی و اجرای مثال‌های این قسمت، نیاز به یک پروژه‌ی جدید Blazor WASM را داریم که می‌توان آن‌را با اجرای دستور dotnet new blazorwasm --hosted در یک پوشه‌ی خالی، ایجاد کرد.

یک نکته: دستور فوق به همراه یک سری پارامتر اختیاری مانند hosted-- نیز هست. برای مشاهده‌ی لیست آن‌ها دستور dotnet new blazorwasm --help را صادر کنید. برای مثال ذکر پارامتر hosted-- سبب می‌شود تا یک ASP.NET Core host نیز برای Blazor WebAssembly app ایجاد شده تولید شود.

حالت hosted-- آن یک چنین ساختاری را دارد که از سه پروژه و پوشه‌ی Client ،Server و Shared تشکیل می‌شود:


در اینجا یک پروژه‌ی خالی WASM ایجاد شده که برخلاف حالت معمولی dotnet new blazorwasm که در قسمت قبل آن‌را بررسی کردیم، دیگر از فایل استاتیک wwwroot\sample-data\weather.json در آن خبری نیست. بجای آن، یک پروژه‌ی استاندارد ASP.NET Core Web API را در پوشه‌ی جدید Server ایجاد کرده که کار ارائه‌ی اطلاعات این سرویس آب و هوا را انجام می‌دهد و برنامه‌ی WASM ایجاد شده، این اطلاعات را توسط HTTP Client خود، از سرور Web API دریافت می‌کند.

بنابراین اگر مدل برنامه‌ای که قصد دارید تهیه کنید، ترکیبی از یک Web API و WASM است، روش hosted--، آغاز آن‌را بسیار ساده می‌کند.

نکته: روش اجرای این نوع برنامه‌ها با اجرای دستور dotnet run در داخل پوشه‌ی Server پروژه، انجام می‌شود. با اینکار هم سرور ASP.NET Core آغاز می‌شود و هم برنامه‌ی WASM توسط آن ارائه می‌گردد. در این حالت اگر آدرس https://localhost:5001 را در مرورگر باز کنیم، هم قسمت‌های بدون نیاز به سرور پروژه‌ی WASM قابل دسترسی است (مانند کار با شمارشگر آن) و هم قسمت دریافت اطلاعات از سرور آن، در منوی Fetch Data.


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

پس از ایجاد یک پروژه‌ی جدید WASM، به فایل Client\Pages\Index.razor آن مراجعه کرده و محتوای پیش‌فرض آن‌را بجز سطر اول زیر، حذف می‌کنیم:
@page "/"
این سطر، بیانگر مسیریابی منتهی به کامپوننت جاری است. یعنی با گشودن برنامه‌ی WASM در مرورگر و مراجعه به ریشه‌ی سایت، محتوای این کامپوننت را مشاهده خواهیم کرد.
در فایل‌های razor. می‌توان ترکیبی از کدهای #C و HTML را نوشت. برای مثال:
@page "/"

<p>Hello, @name</p>

@code
{
    string name = "Vahid N.";
}
در اینجا قصد داریم مقدار یک متغیر را در یک پاراگراف درج کنیم. به همین جهت برای تعریف آن و شروع به کدنویسی می‌توان با تعریف یک قطعه کد که در فایل‌های razor با code@ شروع می‌شود، اینکار را انجام داد. در این قطعه کد، نوشتن هر نوع کد #C ای مجاز است که نمونه‌ای از آن‌را در اینجا با تعریف یک متغیر مشاهده می‌کنید. اکنون برای درج مقدار این متغیر در بین کدهای HTML از حرف @ استفاده می‌کنیم؛ مانند name@ در اینجا. نمونه‌ای از خروجی تغییرات فوق را در تصویر زیر مشاهده می‌کنید:


یک نکته: با توجه به اینکه تغییرات زیادی را در فایل جاری اعمال خواهیم کرد، بهتر است برنامه را با دستور dotnet watch run اجرا کرد، تا این تغییرات را تحت نظر قرار داده و آن‌ها را به صورت خودکار کامپایل کند. به این صورت دیگر نیازی نخواهد بود به ازای هر تغییر، یکبار دستور dotnet run اجرا شود.

در زمان درج متغیرهای #C در بین کدهای HTML توسط razor، استفاده از تمام متدهای الحاقی زبان #C نیز مجاز هستند؛ مانند:
 <p>Hello, @name.ToUpper()</p>
بنابراین درج حرف @ در بین کدهای HTML به این معنا است که به کامپایلر razor اعلام می‌کنیم، پس از این حرف، هر عبارتی که قرار می‌گیرد، یک عبارت معتبر #C است.

یا حتی می‌توان یک متد جدید را مانند CustomToUpper در قطعه کد razor، تعریف کرد و از آن به صورت زیر استفاده نمود:
@page "/"

<p>Hello, @name.ToUpper()</p>
<p>Hello, @CustomToUpper(name)</p>

@code
{
    string name = "Vahid N.";

    string CustomToUpper(string value) => value.ToUpper();
}
در این مثال‌ها، ابتدای عبارت #C تعریف شده با حرف @ شروع می‌شود و انتهای آن‌را خود کامپایلر razor بر اساس بسته شدن تگ p تعریف شده، تشخیص می‌دهد. اما اگر قصد داشته باشیم برای مثال جمع دو عدد را در اینجا محاسبه کنیم چطور؟
<p>Let's add 2 + 2 : @2 + 2 </p>
در این حالت امکان تشخیص ابتدا و انتهای عبارت #C توسط کامپایلر میسر نیست. برای رفع این مشکل می‌توان از پرانتزها استفاده کرد:
<p>Let's add 2 + 2 : @(2 + 2) </p>
نمونه‌ی دیگر نیاز به تعریف ابتدا و انتهای یک قطعه کد، در حین تعریف مدیریت کنندگان رویدادها است:
<button @onclick="@(()=>Console.WriteLine("Test"))">Click me</button>
در اینجا onclick@ مشخص می‌کند که با کلیک بر روی این دکمه قرار است قطعه کد #C ای اجرا شود. سپس با استفاده از ()@ محدوده‌ی این قطعه کد، مشخص می‌شود و اکنون در داخل آن می‌توان یک anonymous function را تعریف کرد که خروجی آن را در قسمت console ابزارهای توسعه دهندگان مرورگر می‌توان مشاهده کرد:


در اینجا اگر از Console.WriteLine("Test")@ استفاده می‌شد، به معنای انتساب یک رشته‌ی محاسبه شده به رویداد onclick بود که مجاز نیست.
روش دیگر انجام اینکار به صورت زیر است:
@page "/"

<button @onclick="@WriteLog">Click me 2</button>

@code
{
    void WriteLog()
    {
        Console.WriteLine("Test");
    }
}
می‌توان یک متد void را تعریف کرد و سپس فقط نام آن‌را توسط @ به onlick انتساب داد. ذکر این نام، اشاره‌گری خواهد بود به متد اجرا نشده‌ی WriteLog. در این حالت اگر نیاز به ارسال پارامتری به متد WriteLog بود، چطور؟
@page "/"

<button @onclick="@(()=>WriteLogWithParam("Test 3"))">Click me 3</button>

@code
{
    void WriteLogWithParam(string value)
    {
        Console.WriteLine(value);
    }
}
در این حالت نیز می‌توان از روش بکارگیری anonymous function‌ها برای تعریف پارامتر استفاده کرد.

یک نکته: اگر به اشتباه بجای WriteLogWithParam، همان WriteLog قبلی را بنویسیم، کامپایلر (در حال اجرای توسط دستور dotnet watch run) خطای زیر را نمایش می‌دهد؛ پیش از اینکه برنامه در مرورگر اجرا شود:
BlazorRazorSample\Client\Pages\Index.razor(12,25): error CS1501: No overload for method 'WriteLog' takes 1 arguments


امکان تعریف کلاس‌ها در فایل‌های razor.

در فایل‌های razor.، محدود به تعریف یک سری متدها و متغیرهای ساده نیستیم. در اینجا امکان تعریف کلاس‌ها نیز وجود دارد و همچنین می‌توان از کلاس‌های خارجی (کلاس‌هایی که خارج از فایل razor جاری تعریف شده‌اند) نیز استفاده کرد.
@page "/"

<p>Hello, @StringUtils.MyCustomToUpper(name)</p>

@code
{
    public class StringUtils
    {
        public static string MyCustomToUpper(string value) => value.ToUpper();
    }
}
برای نمونه در اینجا یک کلاس کمکی را جهت تعریف متد MyCustomToUpper، اضافه کرده‌ایم. در ادامه نحوه‌ی استفاده از این متد را در پاراگراف تعریف شده، مشاهده می‌کنید که همانند کار با کلاس و متدهای متداول #C است.
البته این کلاس را تنها می‌توان داخل همین کامپوننت استفاده کرد. برای اینکه بتوان از امکانات این کلاس، در سایر کامپوننت‌ها نیز استفاده کرد، می‌توان آن‌را در پروژه‌ی Shared قرار داد. اگر به تصویر ابتدای مطلب جاری دقت کنید، سه پروژه ایجاد شده‌است:
الف) پروژه‌ی کلاینت: که همان WASM است.
ب) پروژه‌ی سرور: که یک پروژه‌ی ASP.NET Core Web API ارائه کننده‌ی سرویس و API آب و هوا است و همچنین هاست کننده‌ی WASM ما.
ج) پروژه‌ی Shared: کدهای این پروژه، بین هر دو پروژه به اشتراک گذاشته می‌شوند و برای مثال محل مناسبی است برای تعریف DTO ها. برای نمونه WeatherForecast.cs قرار گرفته‌ی در آن، DTO یا data transfer object سرویس API برنامه است که قرار است به کلاینت بازگشت داده شود. به این ترتیب دیگر نیازی نخواهد بود تا این تعاریف را در پروژه‌های سرور و کلاینت تکرار کنیم و می‌توان کدهای اینگونه را به اشتراک گذاشت.
کاربرد دیگر آن تعریف کلاس‌های کمکی است؛ مانند StringUtils فوق. به همین به پروژه‌ی Shared مراجعه کرده و کلاس StringUtils را به صورت زیر در آن تعریف می‌کنیم (و یا حتی می‌توان این قطعه کد را داخل یک پوشه‌ی جدید، در همان پروژه‌ی WASM نیز قرار داد):
namespace BlazorRazorSample.Shared
{
    public class StringUtils
    {
        public static string MyNewCustomToUpper(string value) => value.ToUpper();
    }
}
اگر به فایل‌های csproj دو پروژه‌ی سرور و کلاینت جاری مراجعه کنیم، از پیش، مدخلی را به فایل Shared\BlazorRazorSample.Shared.csproj دارند. بنابراین جهت معرفی این اسمبلی به آن‌ها، نیاز به کار خاصی نیست و از پیش، ارجاعی به آن تعریف شده‌است.

پس از آن روش استفاده‌ی از این کلاس کمکی خارجی اشتراکی به صورت زیر است:
@page "/"

@using BlazorRazorSample.Shared

<p>Hello, @StringUtils.MyNewCustomToUpper(name)</p>
ابتدا فضای نام این کلاس را با استفاده از using@ مشخص می‌کنیم و سپس امکان دسترسی به امکانات آن میسر می‌شود.

یک نکته: می‌توان به فایل Client\_Imports.razor مراجعه و مدخل زیر را به انتهای آن اضافه کرد:
@using BlazorRazorSample.Shared
به این ترتیب دیگر نیازی به ذکر این using@ تکراری، در هیچکدام از فایل‌های razor. پروژه‌ی کلاینت نخواهد بود؛ چون تعاریف درج شده‌ی در فایل Client\_Imports.razor سراسری هستند.


کار با حلقه‌ها در فایل‌های razor.

همانطور که عنوان شد، یکی از کاربردهای پروژه‌ی Shared، امکان به اشتراک گذاشتن مدل‌ها، در برنامه‌های کلاینت و سرور است. برای مثال یک پوشه‌ی جدید Models را در این پروژه ایجاد کرده و کلاس MovieDto را به صورت زیر در آن تعریف می‌کنیم:
using System;

namespace BlazorRazorSample.Shared.Models
{
    public class MovieDto
    {
        public string Title { set; get; }

        public DateTime ReleaseDate { set; get; }
    }
}
سپس به فایل Client\_Imports.razor مراجعه کرده و فضای نام این پوشه را اضافه می‌کنیم؛ تا دیگر نیازی به تکرار آن در تمام فایل‌های razor. برنامه‌ی کلاینت نباشد:
@using BlazorRazorSample.Shared.Models
اکنون می‌خواهیم لیستی از فیلم‌ها را در فایل Client\Pages\Index.razor نمایش دهیم:
@page "/"

<div>
    <h3>Movies</h3>
    @foreach(var movie in movies)
    {
        <p>Title: <b>@movie.Title</b></p>
        <p>ReleaseDate: @movie.ReleaseDate.ToString("dd MMM yyyy")</p>
    }
</div>

@code
{
    List<MovieDto> movies = new List<MovieDto>
    {
        new MovieDto
        {
            Title = "Movie 1",
            ReleaseDate = DateTime.Now.AddYears(-1)
        },
        new MovieDto
        {
            Title = "Movie 2",
            ReleaseDate = DateTime.Now.AddYears(-2)
        },
        new MovieDto
        {
            Title = "Movie 3",
            ReleaseDate = DateTime.Now.AddYears(-3)
        }
    };
}
در اینجا در ابتدا لیستی از MovieDto‌ها در قسمت code@ تعریف شده و سپس روش استفاده‌ی از یک حلقه‌ی foreach سی‌شارپ را در کدهای razor نوشته شده، مشاهده می‌کنید که این خروجی را ایجاد می‌کند:


یک نکته: در حین تعریف فیلدهای code@، امکان استفاده‌ی از var وجود ندارد؛ مگر اینکه از آن بخواهیم در داخل بدنه‌ی یک متد استفاده کنیم.

و یا نمونه‌ی دیگری از حلقه‌های #‍C مانند for را می‌توان به صورت زیر تعریف کرد:
    @for(var i = 0; i < movies.Count; i++)
    {
        <div style="background-color: @(i % 2 == 0 ? "blue" : "red")">
            <p>Title: <b>@movies[i].Title</b></p>
            <p>ReleaseDate: @movies[i].ReleaseDate.ToString("dd MMM yyyy")</p>
        </div>
    }
در اینجا روش تغییر پویای background-color هر ردیف را نیز به کمک کدهای razor، مشاهده می‌کنید. اگر شماره‌ی ردیفی زوج بود، با آبی نمایش داده می‌شود؛ در غیراینصورت با قرمز. در اینجا نیز از ()@ برای تعیین محدوده‌ی کدهای #C نوشته شده، کمک گرفته‌ایم.


نمایش شرطی عبارات در فایل‌های razor.

اگر به مثال توکار Client\Pages\FetchData.razor مراجعه کنیم (مربوط به حالت host-- که در ابتدای مطلب عنوان شد)، کدهای زیر قابل مشاهده هستند:
@page "/fetchdata"
@using BlazorRazorSample.Shared
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }

}
در این مثال، روش کار با یک سرویس تزریق شده‌ی async که قرار است از Web API اطلاعاتی را دریافت کند، مشاهده می‌کنید. در اینجا برخلاف مثال قبلی ما، از روال رویدادگردان OnInitializedAsync برای مقدار دهی لیست یا آرایه‌ای از اطلاعات وضعیت هوا استفاده شده‌است (و نه به صورت مستقیم در یک فیلد قسمت code@). این مورد جزو life-cycle‌های کامپوننت‌های razor است که در قسمت‌های بعد بیشتر بررسی خواهد شد. متد OnInitializedAsync برای بارگذاری اطلاعات یک سرویس از راه دور استفاده می‌شود و در اولین بار اجرای کامپوننت فراخوانی خواهد شد. نکته‌ی مهمی که در اینجا وجود دارد، نال بودن فیلد forecasts در زمان رندر اولیه‌ی کامپوننت جاری است؛ از این جهت که کار دریافت اطلاعات از سرور زمان‌بر است ولی رندر کامپوننت، به صورت آنی صورت می‌گیرد. در این حالت زمانیکه نوبت به اجرای foreach (var forecast in forecasts)@ می‌رسد، برنامه با یک استثنای نال بودن forecasts، متوقف خواهد شد؛ چون هنوز کار OnInitializedAsync به پایان نرسیده‌است:


 برای رفع این مشکل، ابتدا یک if@ مشاهده می‌شود، تا نال بودن forecasts را بررسی کند:
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
و همچنین عبارت در حال بارگذاری را نمایش می‌دهد. سپس در قسمت else آن، نمایش اطلاعات دریافت شده را توسط یک حلقه‌ی foreach مشاهده می‌کنید. با مقدار دهی forecasts در متد OnInitializedAsync، مجددا کار رندر جدول انجام خواهد شد.


روش نمایش عبارات HTML در فایل‌های razor.

فرض کنید عنوان اول فیلم مثال جاری، به همراه یک تگ HTML هم هست:
new MovieDto
{
   Title = "<i>Movie 1</i>",
   ReleaseDate = DateTime.Now.AddYears(-1)
},
در این حالت اگر برنامه را اجرا کنیم، خروجی آن دقیقا به صورت <Title: <i>Movie 1</i خواهد بود. این مورد به دلایل امنیتی انجام شده‌است. اگر پیشتر تگ‌های HTML را تمیز کرده‌اید و مطمئن هستید که خطری را ایجاد نمی‌کنند، می‌توانید با استفاده از روش زیر، آن‌ها را رندر کرد:
<p>Title: <b>@((MarkupString)movie.Title)</b></p>


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-03.zip
برای اجرای آن وارد پوشه‌ی Server شده و دستور dotnet run را اجرا کنید.
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 6 - Implementation Factory

در بعضی از شرایط پیش رفته، ممکن است نمونه سازی از یک Implementation Type، نیاز به دخالت مستقیم ما را داشته باشد. Implementation Factory کنترل بیشتری بر چگونگی استفاده‌ی از Implementation Type‌ها را  به ما ارائه می‌دهد. در هنگام ثبت سرویسی که Implementation Factory را در اختیار ما قرار می‌دهد، ما یک Delegate را برای فراخوانی سرویس استفاده می‌کنیم. این Delegate مسئول ساخت یک نمونه از Service Type است. برای مثال وقتی از الگوهای builder یا factory برای ساخت یک شیء استفاده می‌کنید، شاید نیاز باشد که Implementation Factory را به صورت دستی پیاده سازی کنید. اولین قدم این است که کدتان را در صورت امکان چنان refactor کنید تا DI Container بتواند آن را به صورت خودکار بسازد؛ ولی اینکار همیشه ممکن نیست. برای مثال بعضی از برنامه نویسان ترجیح می‌دهند یک Config را مستقیما از IOptionMonitor   بگیرند و بعد در هر جائیکه خواستند، بجای تزریق IOptionMonitor به سرویس، مستقیما از همان سرویس ثبت شده استفاده کنند:

services.AddSingleton<ILiteDbConfig>(sp =>sp.GetRequiredService<IOptionsMonitor<LiteDbConfig>>().CurrentValue);

پیاده سازیComposite Pattern 

یک کاربرد بالقوه‌ی دیگر برای Implementation Factory ، استفاده از Composite Pattern است. هر چند Microsoft DI Container به صورت پیش فرض از Composite Pattern پشتیبانی نمی‌کند، ولی ما می‌توانیم آن‌را پیاده سازی کنیم. فرض کنید که قبلا به ازای انجام کاری، به کاربر یک ایمیل را می‌فرستادیم؛ ولی حالا مالک محصول می‌آید و می‌گوید که علاوه بر ایمیل، باید پیامک هم بفرستید و ما یا این سرویس پیامک را از قبل داریم و یا باید آن را بسازیم که فرض می‌کنیم از قبل آن را داریم. برای این کار ما یک اینترفیس کلی‌تر به نام INotificationService می‌سازیم که دو سرویس IEmailNotificationService و ISmsNotificaitonService از آن ارث بری می‌کنند:

public interface INotificationService
{
      void SendMessage(string msg, int userId);
}
حالا CompositeNotificationService را به این صورت تعریف می‌کنیم:
    public class CompositeNotificationService : INotificationService
    {
        private readonly IEnumerable<INotificationService> _notificationServices;
        public CompositeNotificationService(IEnumerable<INotificationService> notificationServices)
        {
            _notificationServices = notificationServices;
        }

        public void SendMessage(string msg, int userId)
        {
            foreach (var notificationServicei in _notificationServices) 
            {
                notificationServicei.SendMessage(msg, userId);
            }
        }
    }
و این سرویس‌ها را به این صورت در DI Container ثبت می‌کنیم: 
services.AddScoped<IEmailNotificationService, EmailNotificationService>();
services.AddScoped<ISMSNotificationService, SMSNotificationService>();

services.AddSingleton<INotificationService>(sp => new CompositeNotificationService(
      new INotificationService[] { 
          sp.GetRequiredService<IEmailNotificationService>() , 
          sp.GetRequiredService<ISMSNotificationService>()
      }
));
حالا هر زمانیکه بخواهیم همزمان، هم ایمیل و هم پیامک بفرستیم، کافی است که سرویس INotificationService را در سازنده‌ی کلاس مورد نظر تزریق کرده و از آن در مکان‌ها و شرایط مختلفی استفاده کنیم. اگر هر کدام از سرویس‌های ارسال ایمیل و سرویس‌های پیامک را به صورت جداگانه بخواهیم، می‌توانیم آنها را به صورت تکی ثبت و استفاده کنیم.  


وهله سازی سفارشی

در مثال بعدی نشان می‌دهیم که چطور می‌توانیم از Implementation Factory برای برگرداندن پیاده‌سازی سرویس‌هایی که Service Provider امکان ساخت خودکار آنها را ندارد، استفاده کنیم. فرض کنید یک کلاس Account داریم که از IAccount ارث بری می‌کند و برای ساخت آن باید از متدهای IAccountBuilder که فرآیند ساخت را انجام می‌دهند، استفاده کنیم. بنابراین امکان ساخت مستقیم یک شیء از IAccount وجود ندارد. در این حالت بدین صورت عمل می‌کنیم: 

services.AddTransient<IAccountBuilder, AccountBuilder>();

services.AddScoped<IAccount>(sp => {
    var builder = sp.GetService<IAccountBuilder>();
    builder.WithAccountType(AccountType.Guest);
    return builder.Build();
});


ثبت یک نمونه برای چندین اینترفیس

ممکن است بنا به دلایلی مجبور باشیم یک implementation Type را برای چند سرویس (اینترفیس) به ثبت برسانیم. در این حالت نمونه‌ی شیء ساخته شده‌، توسط هر کدام از اینترفیس‌ها قابل استفاده است. فرض کنید یک سرویس Greeting داریم که پیش از این فقط اینترفیس IHomeGreetingService را پیاده سازی می‌کرد؛ ولی بنا به دلایلی تصمیم گرفتیم که سرویسی جامع‌تر را با نیازمندی‌های دیگری نیز تعریف کنیم و GreetingService آن را پیاده سازی کند: 

public class GreetingService : IHomeGreetingService , IGreetingService
{ // code here }

احتمالا اولین چیزی که به ذهنمان می‌رسد این است: 

services.TryAddSingleton<IHomeGreetingService, GreetingService>();
services.TryAddSingleton<IGreetingService, GreetingService>();

مشکل روش بالا این است که دو نمونه از GreetingService ساخته می‌شود و درون حافظه باقی می‌ماند و در حقیقت برای هر اینترفیس، یک نوع جداگانه از GreetingService ثبت می‌شود؛ در حالیکه ما می‌خواهیم هر دو اینترفیس فقط از یک نمونه از شیء GreetingService استفاده کنند.  دو راه حل برای این مشکل وجود دارد:

var greetingService = new GreetingService(Environment);
services.TryAddSingleton<IHomeGreetingService>(greetingService);
services.TryAddSingleton<IGreetingService>(greetingService);

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

روش بعدی همان روش استفاده از Implementation Factory است که در ادامه آن را می‌بینید: 

services.TryAddSingleton<GreetingService>();
services.TryAddSingleton<IHomeGreetingService>(sp => sp.GetRequiredService<GreetingService>());
services.TryAddSingleton<IGreetingService>(sp => sp.GetRequiredService<GreetingService>());

در این روش خود DI Container مسئول نمونه سازی از GreetingService است. علاوه بر این می‌توان با استفاده از روش فوق از طول حیات‌های Scoped و Transient هم استفاده کرد؛ در حالیکه در روش قبلی این کار امکان پذیر نبود.

 

Open Generics Service

گاهی از اوقات می‌خواهید سرویس‌هایی را ثبت کنید که از اینترفیسی جنریک ارث بری می‌کنند. هر نوع جنریک در زمان اجرا، نوع مخصوص به خود را واکشی می‌کند. ثبت کردن دستی این سرویس‌ها می‌تواند خسته کننده باشد. برای همین مایکروسافت در DI Container خود قابلیت ثبت و واکشی سرویس‌های جنریک را نیز در اختیار ما گذاشته‌است. بیایید نگاهی به سرویس ILogger<T>  بیندازیم. این یک سرویس درونی فریمورک است و می‌تواند به ازای هر نوع، کارهای مربوط به ثبت log را انجام بدهد و در پروژه‌ها معمولا از این اینترفیس برای ثبت لاگ‌ها در سطح کنترلر و سرویس‌ها استفاده می‌شود: 

public interface ILogger<out TCategoryName> : ILogger
{
}

در حالت عادی اگر سرویسی مشابه سرویس فوق را داشته باشیم، برای ثبت کردن هر سرویس با نوع جنریک اختصاصی آن، مجبوریم به صورت دستی آن را درون DI Container ثبت کنیم؛ مثلا باید به این صورت عمل کنیم: 

services.TryAddScoped<ILogger<HomeController>,Logger<HomeController>>();
این کاری طاقت فرساست. به همین جهت مایکروسافت قابلیت Open Generics Service را در اختیار ما گذاشته تا بتوانیم اینگونه سرویس‌ها را فقط و فقط یکبار ثبت کنیم: 
services.TryAddScoped(typeof(ILogger<>) , typeof(Logger<>));
و اینگونه می‌توانیم نمونه‌های مختلف از ILogger<T> را به هر جایی که خواستیم، تزریق کنیم.

 

دسته بندی سرویس‌ها درون متدهای مختلف و پاکسازی  متد ConfigurationService

 تا اینجای کار ما سرویس‌های مختلفی را به روش‌های مختلفی ثبت کرده‌ایم. حتی در همین آموزش ساده، تعداد زیاد سرویس‌های ثبت شده، باعث شلوغی و در هم ریختگی کدهای ما می‌شوند که خوانایی و در ادامه اشکال زدایی و توسعه‌ی کدها را برای ما سخت‌تر می‌کنند.  ساده‌ترین کار برای دسته بندی کدها، استفاده از متدهای private محلی یا استفاده از متدهای توسعه‌ای(الحاقی) است که در اینجا مثالی از استفاده از متدهای توسعه‌ای را آورده‌ام:
namespace AspNetCoreDependencyInjection.Extensions
{
    public static class DICRegisterationExetnsion
    {
        /// <summary>
        /// مثال ثبت برای اپن جنریت
        /// </summary>
        /// <param name="services"></param>
        public static void OpenGenericRegisterationExample(this IServiceCollection services)
        {
            services.TryAddScoped<ILogger<HomeController>, Logger<HomeController>>();
            services.TryAddScoped(typeof(ILogger<>), typeof(Logger<>));
        }


        /// <summary>
        /// ثبت تنظیمات به روش‌های مختلف 
        /// </summary>
        public static void RegisterConfiguration(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddSingleton(services => new AppConfig
            {
                ApplicationName = configuration["ApplicationName"],
                GreetingMessage = configuration["GreetingMessage"],
                AllowedHosts = configuration["AllowedHosts"]
            });

            services.AddSingleton(services => new AccountTypeBalanceConfig(
                    new List<(AccountType, long)> {
                        (AccountType.Guest , Convert.ToInt64 (configuration["AccountInitialBalance.Guest"]) ) ,
                        (AccountType.Silver , Convert.ToInt64 (configuration["AccountInitialBalance.Silver"]) ) ,
                        (AccountType.Gold , Convert.ToInt64 (configuration["AccountInitialBalance.Gold"]) ) ,
                        (AccountType.Platinum , Convert.ToInt64 (configuration["AccountInitialBalance.Platinum"]) ) ,
                        (AccountType.Titanium , Convert.ToInt64 (configuration["AccountInitialBalance.Titanium"]) ) ,
                    })
            );

            services.AddSingleton(services => new LiteDbConfig
            {
                ConnectionString = configuration["LiteDbConfig:ConnectionString"],
            });

            services.Configure<UserOptionConfig>(configuration.GetSection("UserOptionConfig"));
        }
    }
}

حالا در کلاس ConfigureServices ، درون متدStartup ، به این صورت از این متدهای توسعه‌ای استفاده می‌کنیم:
services.RegisterConfiguration(this.Configuration);
services.OpenGenericRegisterationExample();

می‌توانید کد منبع این آموزش را در اینجا  ببینید.
اشتراک‌ها
بررسی معماری Vertical Slice در ASP.NET Core

In this article, we are going to learn about Vertical Slice Architecture in ASP.NET Core and the benefits it provides to make our code more maintainable. We’re also going to learn how it differs from traditional architecture and its advantages over the traditional approach. 

بررسی معماری Vertical Slice در ASP.NET Core