مطالب
آشنایی با الگوی IOC یا Inversion of Control (واگذاری مسئولیت)

کلاس Kid را با تعریف زیر در نظر بگیرید. هدف از آن نگهداری اطلاعات فرزندان یک شخص خاص می‌باشد:

namespace IOCBeginnerGuide
{
class Kid
{
private int _age;
private string _name;

public Kid(int age, string name)
{
_age = age;
_name = name;
}

public override string ToString()
{
return "KID's Age: " + _age + ", Kid's Name: " + _name;
}
}
}

اکنون کلاس والد را با توجه به اینکه در حین ایجاد این شیء، فرزندان او نیز باید ایجاد شوند؛ در نظر بگیرید:
using System;

namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;
private Kid _obj;

public Parent(int personAge, string personName, int kidsAge, string kidsName)
{
_obj = new Kid(kidsAge, kidsName);
_age = personAge;
_name = personName;
}

public override string ToString()
{
Console.WriteLine(_obj);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}

و نهایتا مثالی از استفاده از آن توسط یک کلاینت:

using System;

namespace IOCBeginnerGuide
{
class Program
{
static void Main(string[] args)
{
Parent p = new Parent(35, "Dev", 6, "Len");
Console.WriteLine(p);

Console.ReadKey();
Console.WriteLine("Press a key...");
}
}
}

که خروجی برنامه در این حالت مساوی سطرهای زیر می‌باشد:

KID's Age: 6, Kid's Name: Len
ParentAge: 35, ParentName: Dev

مثال فوق نمونه‌ای از الگوی طراحی ترکیب یا composition می‌باشد که به آن Object Dependency یا Object Coupling نیز گفته می‌شود. در این حالت ایجاد شیء والد وابسته است به ایجاد شیء فرزند.

مشکلات این روش:
1- با توجه به وابستگی شدید والد به فرزند، اگر نمونه سازی از شیء فرزند در سازنده‌ی کلاس والد با موفقیت روبرو نشود، ایجاد نمونه‌ی والد با شکست مواجه خواهد شد.
2- با از بین رفتن شیء والد، فرزندان او نیز از بین خواهند رفت.
3- هر تغییری در کلاس فرزند، نیاز به تغییر در کلاس والد نیز دارد (اصطلاحا به آن Dangling Reference هم گفته می‌شود. این کلاس آویزان آن کلاس است!).

چگونه این مشکلات را برطرف کنیم؟
بهتر است کار وهله سازی از کلاس Kid به یک شیء، متد یا حتی فریم ورک دیگری واگذار شود. به این واگذاری مسئولیت، delegation و یا inversion of control - IOC نیز گفته می‌شود.

بنابراین IOC می‌گوید که:
1- کلاس اصلی (یا همان Parent) نباید به صورت مستقیم وابسته به کلاس‌های دیگر باشد.
2- رابطه‌ی بین کلاس‌ها باید بر مبنای تعریف کلاس‌های abstract باشد (و یا استفاده از interface ها).

تزریق وابستگی یا Dependency injection
برای پیاده سازی IOC از روش تزریق وابستگی یا dependency injection استفاده می‌شود که می‌تواند بر اساس constructor injection ، setter injection و یا interface-based injection باشد و به صورت خلاصه پیاده سازی یک شیء را از مرحله‌ی ساخت وهله‌ای از آن مجزا و ایزوله می‌سازد.

مزایای تزریق وابستگی‌ها:
1- گره خوردگی اشیاء را حذف می‌کند.
2- اشیاء و برنامه را انعطاف پذیرتر کرده و اعمال تغییرات به آن‌ها ساده‌تر می‌شود.

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

تزریق سازنده یا constructor injection :
در این روش ارجاعی از شیء مورد استفاده، توسط سازنده‌ی کلاس استفاده کننده از آن دریافت می‌شود. برای نمونه در مثال فوق از آنجائیکه کلاس والد به کلاس فرزندان وابسته است، یک ارجاع از شیء Kid به سازنده‌ی کلاس Parent باید ارسال شود.
اکنون بر این اساس تعاریف، کلاس‌های ما به شکل زیر تغییر خواهند کرد:

//IBuisnessLogic.cs
namespace IOCBeginnerGuide
{
public interface IBuisnessLogic
{
}
}

//Kid.cs
namespace IOCBeginnerGuide
{
class Kid : IBuisnessLogic
{
private int _age;
private string _name;

public Kid(int age, string name)
{
_age = age;
_name = name;
}

public override string ToString()
{
return "KID's Age: " + _age + ", Kid's Name: " + _name;
}
}
}

//Parent.cs
using System;

namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;
private IBuisnessLogic _refKids;

public Parent(int personAge, string personName, IBuisnessLogic obj)
{
_age = personAge;
_name = personName;
_refKids = obj;
}

public override string ToString()
{
Console.WriteLine(_refKids);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}

//CIOC.cs
using System;

namespace IOCBeginnerGuide
{
class CIOC
{
Parent _p;

public void FactoryMethod()
{
IBuisnessLogic objKid = new Kid(12, "Ren");
_p = new Parent(42, "David", objKid);
}

public override string ToString()
{
Console.WriteLine(_p);
return "Displaying using Constructor Injection";
}
}
}

//Program.cs
using System;

namespace IOCBeginnerGuide
{
class Program
{
static void Main(string[] args)
{
CIOC obj = new CIOC();
obj.FactoryMethod();
Console.WriteLine(obj);

Console.ReadKey();
Console.WriteLine("Press a key...");
}
}
}

توضیحات:
ابتدا اینترفیس IBuisnessLogic ایجاد خواهد شد. تنها متدهای این اینترفیس در اختیار کلاس Parent قرار خواهند گرفت.
از آنجائیکه کلاس Kid توسط کلاس Parent استفاده خواهد شد، نیاز است تا این کلاس نیز اینترفیس IBuisnessLogic را پیاده سازی کند.
اکنون سازنده‌ی کلاس Parent بجای ارجاع مستقیم به شیء Kid ، از طریق اینترفیس IBuisnessLogic با آن ارتباط برقرار خواهد کرد.
در کلاس CIOC کار پیاده سازی واگذاری مسئولیت وهله سازی از اشیاء مورد نظر صورت گرفته است. این وهله سازی در متدی به نام Factory انجام خواهد شد.
و در نهایت کلاینت ما تنها با کلاس IOC سرکار دارد.

معایب این روش:
- در این حالت کلاس business logic، نمی‌تواند دارای سازنده‌ی پیش فرض باشد.
- هنگامیکه وهله‌ای از کلاس ایجاد شد دیگر نمی‌توان وابستگی‌ها را تغییر داد (چون از سازنده‌ی کلاس جهت ارسال مقادیر مورد نظر استفاده شده است).

تزریق تنظیم کننده یا Setter injection
این روش از خاصیت‌ها جهت تزریق وابستگی‌ها بجای تزریق آن‌ها به سازنده‌ی کلاس استفاده می‌کند. در این حالت کلاس Parent می‌تواند دارای سازنده‌ی پیش فرض نیز باشد.

مزایای این روش:
- از روش تزریق سازنده بسیار انعطاف پذیرتر است.
- در این حالت بدون ایجاد وهله‌ای می‌توان وابستگی اشیاء را تغییر داد (چون سر و کار آن با سازنده‌ی کلاس نیست).
- بدون نیاز به تغییری در سازنده‌ی یک کلاس می‌توان وابستگی اشیاء را تغییر داد.
- تنظیم کننده‌ها دارای نامی با معناتر و با مفهوم‌تر از سازنده‌ی یک کلاس می‌باشند.

نحوه‌ی پیاده سازی آن:
در اینجا مراحل ساخت Interface و همچنین کلاس Kid با روش قبل تفاوتی ندارند. همچنین کلاینت نهایی استفاده کننده از IOC نیز مانند روش قبل است. تنها کلاس‌های IOC و Parent باید اندکی تغییر کنند:

//Parent.cs
using System;

namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;

public Parent(int personAge, string personName)
{
_age = personAge;
_name = personName;
}

public IBuisnessLogic RefKID {set; get;}

public override string ToString()
{
Console.WriteLine(RefKID);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}

//CIOC.cs
using System;

namespace IOCBeginnerGuide
{
class CIOC
{
Parent _p;

public void FactoryMethod()
{
IBuisnessLogic objKid = new Kid(12, "Ren");
_p = new Parent(42, "David");
_p.RefKID = objKid;
}

public override string ToString()
{
Console.WriteLine(_p);
return "Displaying using Setter Injection";
}
}
}

همانطور که ملاحظه می‌کنید در این روش یک خاصیت جدید به نام RefKID به کلاس Parent اضافه شده است که از هر لحاظ نسبت به روش تزریق سازنده با مفهوم‌تر و خود توضیح دهنده‌تر است. سپس کلاس IOC جهت استفاده از این خاصیت اندکی تغییر کرده است.

ماخذ

مطالب
اشیاء تغییر ناپذیر (Immutable Object)
کلمه‌ی mutable به معنای تغییر پذیر و کلمه‌ی immutable به معنای تغیر ناپذیر در زبان انگلیسی تعریف شده‌اند. در دنیای IT این دو واژه نیز همین معنا را دارند. بطور مثال: یک رشته‌ی mutable، یعنی رشته‌ای که بتوان آن را تغییر داد و یک رشته‌ی immutable یعنی رشته‌ای که غیر قابل تغییر است.



در حین مطالعه‌ی منابع مختلف درباره‌ی موضوع این مطلب، جمله‌ای را با این مضمون دیدم: برای ساخت کوزه، گل را تا مرطوب هست باید شکل داد. زمانیکه گل خشک شود، دیگر نمی‌توان کوزه را تغییر شکل داد. اشیاء تغییر ناپذیر هم به همین شکل هستند. بعد از ایجاد این اشیاء، دیگر نمی‌توان به هیچ وجه آنها را تغییر داد.

تعریف اشیاء تغییر ناپذیر (Immutable Objects) :
این اشیاء، اشیائی هستند که بعد از بارگزاری در حافظه، به هیچ وجه نمی‌توانند اصلاح و یا تغییر کنند. نه از طریق خارجی (کاربران External) و نه از طریق داخلی (اعضای کلاس Internal).

چه زمانی از این اشیاء استفاده می‌کنیم؟
اشیاء تغییر ناپذیر برای داده‌های استاتیک استفاده می‌شوند و نمونه‌هایی از آن در بخش زیر لیست شده‌اند:

 • داده‌های اصلی (Master Data): یکی از بیشترین کاربرد‌های اشیاء تغییر ناپذیر، برای بارگذاری داده‌های اصلی است (کشور‌ها، واحد‌های پولی، استان ها) و داده‌هایی که به ندرت تغییر می‌کنند. این داده‌های اصلی بعد از بارگزاری در حافظه، دیگر تغییر نخواهند کرد.

 • اطلاعات پیکره‌بندی (Configuration Data): همه‌ی برنامه‌ها نیاز به اطلاعات پیکره‌بندی دارند. در دنیای برنامه‌های مایکروسافت، عموما این اطلاعات پیکره بندی را در فایل‌های web.config و App.config ذخیره می‌کنیم. این نوع اطلاعات بصورت یک شیء در حافظه بارگذاری می‌شوند و بعدا تغییر نخواهند کرد.

 • اشیاء Singleton: اشیاء singleton اشیائی هستند که تنها یک نمونه از آنها را می‌توان ایجاد کرد. در برنامه‌ها از این اشیاء برای اشتراک گذاشتن اطلاعات استاتیک استفاده می‌کنند. اگر این اطلاعات تغییر نکنند، یکی از گزینه‌ها، استفاده از اشیاء تغییر ناپذیر هستند.

چگونه می‌توان در سی شارپ اشیاء تغییر ناپذیر را ایجاد کنیم؟

اشیاء تغییر ناپذیر (immutable objects) تنها توسط کلاس‌های تغییر ناپذیر (immutable classes) می‌توانند ایجاد شوند.
برای ایجاد کلاسی تغییر ناپذیر، سه مرحله باید طی شود:
 1- حذف بلاک Set: همانطور که گفته شد، بخش Set از property ‌ها باید حذف شود. با حذف این بخش بعد از بارگزاری شیء در حافظه، دیگر نمی‌توان آن را تغییر داد:
public class Currency
{
  private string _currencyName;
  private string _countryName;

  public string CurrencyName
  {
     get { return _currencyName; }
  }

  public string CountryName
  {
     get { return _countryName; }
  }
}

 2- مهیا کردن پارامتر‌ها از طریق سازنده‌ی کلاس: با حذف بلاک set، راهی برای بارگزاری اطلاعات، در کلاس وجود ندارد. از این رو می‌توان از طریق پارامتر‌های سازنده‌ی کلاس، اطلاعات را به شیء ارسال کرد.
  private string _currencyName;
  private string _countryName;
  public Currency(string paramCurrencyName,string paramCountryName)
  {
     _currencyName= paramCurrencyName;
     _countryName = paramCountryName;
  }

 3- تعریف متغیر‌های کلاس به صورت فقط خواندنی READONLY
در تعریف اولیه گفته شد که اشیاء immutable نه از طریق خارجی (کاربر) و «کمی فراتر» نه از طریق داخلی (اعضای کلاس) قابل تغییر نیستند. اما کلاس ایجاد شده را می‌توان بعد از ایجاد نمونه‌ای از آن، مجددا تغییر داد. کافی است یک متد به شکل زیر در آن تعریف کنیم و به‌راحتی وضعیت شیء را از طریق آن تغییر دهیم.
  public void DoSomthing()
  {
     _countryName = "somthing else";
  }

راه حل ارائه شده‌ی برای حل این موضوع، معرفی متغیر‌ها به صورت readonly می‌باشد. متغیرهایی که بصورت فقط خواندنی تعریف می‌شوند، تنها از طریق سازنده‌ی شیء می‌توانند مقداردهی اولیه شوند.
باز طراحی نهایی کلاس Currency  به صورت زیر است:
 public class Currency
{
  private readonly string _currencyName;
  private readonly string _countryName;
  public string CurrencyName
  {
    get { return _currencyName; }
  }

  public string CountryName
  {
    get { return _countryName; }
  }
  public Currency(string paramCurrencyName,string paramCountryName)
  {
     _currencyName= paramCurrencyName;
     _countryName = paramCountryName;
  }
}
نظرات مطالب
ارسال ایمیل در ASP.NET Core
نکته تکمیلی:
در صورت استفاده از mailkit در محیط net core. میتوان کلاس SMTPClient را به شکل زیر مورد استفاده قرار داد.
 public class DiskSmtpClient : SmtpClient
    {
        public DiskSmtpClient(IOptionsSnapshot<MailKitOptions> mailOptionsSnapshot)
        {
            if (mailOptionsSnapshot.Value.SpecifiedPickupDirectory)
            {
                SpecifiedPickupDirectory = true;
                PickupDirectoryLocation = mailOptionsSnapshot.Value.PickupDirectoryLocation;
            }
            
        }
        public bool SpecifiedPickupDirectory { get; set; }
        public string PickupDirectoryLocation { get; set; }

        public override Task SendAsync(MimeMessage message, CancellationToken cancellationToken = new CancellationToken(),
            ITransferProgress progress = null)
        {
            if (!SpecifiedPickupDirectory)
                return base.SendAsync(message, cancellationToken, progress);
            return SaveToPickupDirectory(message, PickupDirectoryLocation);

        }

    



        private async Task SaveToPickupDirectory(MimeMessage message, string pickupDirectory)
        {
            using (var stream = new FileStream($@"{pickupDirectory}\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew))
            {
                await message.WriteToAsync(stream);
            }
        }

       

        public override Task ConnectAsync(string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto,
            CancellationToken cancellationToken = new CancellationToken())
        {
            if (!SpecifiedPickupDirectory)
                return base.ConnectAsync(host, port, options, cancellationToken);
            return Task.CompletedTask;
        }



        public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = new CancellationToken())
        {
            if (!SpecifiedPickupDirectory)
                return base.DisconnectAsync(quit, cancellationToken);

            return Task.CompletedTask;
        }
    }
در کد بالا دو خصوصیت SpecifiedPickupDirectory  و PickupDirectoryLocation به آن اضافه شده اند و با رونویسی از متدهای مورد استفاده به جای ارسال ایمیل در صورت مقداردهی  SpecifiedPickupDirectory  ایمیل در آدرس  PickupDirectoryLocation   ذخیره میگردد. سپس به شکل زیر آن را مورد استفاده قرار میدهیم:
 services.AddTransient<DiskSmtpClient>();
  var email = "mail@dotnettips.info";
            var subject = "subject";
            var message = "message";

            var emailMessage = new MimeMessage();

            emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info"));
            emailMessage.To.Add(new MailboxAddress("", email));
            emailMessage.Subject = subject;
            emailMessage.Body = new TextPart(TextFormat.Html)
            {
                Text = message
            };

            
                _client.SpecifiedPickupDirectory = true;
                _client.PickupDirectoryLocation = "c:\\mail";

                _client.LocalDomain = "dotnettips.info";
                await _client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
                await _client.SendAsync(emailMessage).ConfigureAwait(false);
                await _client.DisconnectAsync(true).ConfigureAwait(false);
در صورتی که قصد ندارید کد اضافه‌تری را نیز اعمال نمایید میتوانید با اضافه کردن تکه کد زیر به فایل startup و محتوای تنظیمات آن به فایل appsettings.json دو خط بالا را حذف نمایید:
services.Configure<MailKitOptions>(options => Configuration.GetSection("MailKitOptions").Bind(options));
  "MailKitOptions": {
    "SpecifiedPickupDirectory": true,
    "PickupDirectoryLocation": "c:\\mail"
  }
در این صورت میتوان تنظیمات جداگانه ای برای حالت انتشار و توسعه نیز در نظر گرفت.
کلاس متناظر MailKitOptions
    public class MailKitOptions
    {
        public  bool SpecifiedPickupDirectory { get; set; }
        public  string PickupDirectoryLocation { get; set; }
    }

مطالب
کوئری های پیشرفته، Error Handling و Data Loader در GraphQL
در قسمت قبل یادگرفتیم که چگونه GraphQL را با ASP.NET Core یکپارچه کنیم و اولین GraphQL query را ایجاد و داده‌ها را از سرور بازیابی کردیم. البته ما به این query ‌های ساده بسنده نخواهیم کرد. در این قسمت می‌خواهیم یاد بگیریم که چگونه query ‌های پیشرفته‌ی GraphQL را بنویسیم و در زمان انجام این کار، نمایش دهیم که چگونه خطا‌ها را  مدیریت کنیم و علاوه بر این با queries, aliases, arguments, fragments نیز کار خواهیم کرد.

Creating Complex Types for GraphQL Queries 
اگر نگاهی به owners و query (در پایان قسمت قبل)  بیندازیم، متوجه خواهیم شد که یک لیست از خصوصیات مدل Owner که در OwnerType معرفی شده‌اند، نسبت به کوئری برگشت داده می‌شود. OwnerType شامل فیلد‌های Id , Name  و Address می‌باشد. یک Owner می‌تواند چندین account مرتبط با خود را داشته باشد. هدف این است که در owners ،query لیست account ‌های مربوط به هر owner را بازگشت دهیم. 
قبل از اضافه کردن فیلد Accounts در کلاس OwnerType نیاز است کلاس AccountType را ایجاد کنیم. در ادامه یک کلاس را به نام AccountType در پوشه GraphQLTypes ایجاد می‌کنیم. 
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the account object.");
        Field(x => x.Description).Description("Description property from the account object.");
        Field(x => x.OwnerId, type: typeof(IdGraphType)).Description("OwnerId property from the account object.");
    }
}

 همانطور که مشخص است، خصوصیت Type را از کلاس Account، معرفی نکرده‌ایم (در ادامه اینکار را انجام خواهیم داد). در ادامه، واسط IAccountRepository و کلاس AccountRepository را باز کرده و آن را مطابق زیر ویرایش می‌کنیم: 
public interface IAccountRepository
{
    IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId);
}

public class AccountRepository : IAccountRepository
{
    private readonly ApplicationContext _context;
 
    public AccountRepository(ApplicationContext context)
    {
       _context = context;
    }
 
    public IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId) => _context.Accounts
        .Where(a => a.OwnerId.Equals(ownerId))
        .ToList();
}

اکنون می‌توان لیست account‌ها را به نتیجه owners ، query اضافه کنیم. پس کلاس OwnerType را باز کرده و آن را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository)
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object.");
        Field(x => x.Name).Description("Name property from the owner object.");
        Field(x => x.Address).Description("Address property from the owner object.");
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context => repository.GetAllAccountsPerOwner(context.Source.Id)
        );
    }
}

چیز خاصی در اینجا وجود ندارد که ما تا کنون ندیده باشیم. به همان روش که یک فیلد را در کلاس AppQuery  ایجاد کردیم، یک فیلد را با نام  accounts در کلاس OwnerType ایجاد می‌کنیم. همچنین متد GetAllAccountsPerOwner نیاز به پارامتر id را دارد و این پارامتر را از طریق context.Source.Id فراهم می‌کنیم. زیرا context شامل خصوصیت Source است که در این حالت مشخص نوع Owner می‌باشد.

اکنون پروژه را اجرا کنید و به آدرس زیر بروید:
https://localhost:5001/ui/playground
سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید که نتیجه آن علاوه بر owner‌ها، لیست account ‌های مربوط به هر owner هم می‌باشد:  
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      ownerId
    }
  }
}

Adding Enumerations in GraphQL Queries 
در کلاس AccountType  فیلد Type را اضافه نکرده‌ایم و این کار را عمدا انجام داده‌ایم. اکنون زمان انجام این کار می‌باشد. برای اضافه کردن گونه شمارشی به کلاس AccountType نیاز است تا در ابتدا یک کلاس تعریف شود که نسبت به type ‌های معمول در GraphQL متفاوت است. یک کلاس را به نام AccountTypeEnumType  در پوشه GraphQLTypes  ایجاد کرده و آن را مطابق زیر ویرایش می‌کنیم:  
public class AccountTypeEnumType : EnumerationGraphType<TypeOfAccount>
{
    public AccountTypeEnumType()
    {
        Name = "Type";
        Description = "Enumeration for the account type object.";
    }
}

کلاس AccountTypeEnumType باید از نوع جنریک کلاس EnumerationGraphType ارث بری کند و پارامتر جنریک آن، یک گونه شمارشی را دریافت می‌کند (که در قسمت قبل آن را ایجاد کردیم؛ TypeOfAccount).  همچنین مقدار خصوصیت Name نیز باید همان نام خصوصیت گونه شمارشی در کلاس Account باشد (نام آن در کلاس Account مساوی Type می‌باشد). سپس گونه شمارشی را در کلاس AccountType به صورت زیر اضافه می‌کنیم:
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        ...
        Field<AccountTypeEnumType>("Type", "Enumeration for the account type object.");
    }
}

اکنون پروژه را اجرا کنید و سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید:
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
}
که نتیجه آن اضافه شدن type  به هر account می‌باشد:


Implementing a Cache in the GraphQL Queries with Data Loader  
دیدم که query، نتیجه دلخواهی را برای ما بازگشت می‌دهد؛ اما این query هنوز به اندازه کافی بهینه نشده‌است. مشکل چیست؟
query  ایجاد شده به حالتی کار می‌کند که در ابتدا همه owner ‌ها را بازیابی می‌کند. سپس به ازای هر owner، یک Sql Query  را به سمت بانک اطلاعاتی ارسال می‌کند تا Account ‌های مربوط به آن Owner را بازگشت دهد که می‌توان log  آن را در Terminal مربوط به VS Code مشاهده کرد.
 


البته زمانیکه چند موجودیت owner را داشته باشیم، این مورد یک مشکل نمی‌باشد؛ ولی وقتی تعداد موجودیت‌ها زیاد باشد چطور؟
 owners ، query را می‌توان با استفاده از DataLoader که توسط GraphQL فراهم شده‌است، بهینه سازی کرد. جهت انجام اینکار در ابتدا واسط IAccountRepository و کلاس AccountRepository را همانند زیر ویرایش می‌کنیم:
public interface IAccountRepository
{
    ...
    Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds);
}
public class AccountRepository : IAccountRepository
{
    ...
    public async Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds)
    {
        var accounts = await _context.Accounts.Where(a => ownerIds.Contains(a.OwnerId)).ToListAsync();
        return accounts.ToLookup(x => x.OwnerId);
    }
}

 نیاز است که یک متد داشته باشیم که <<Task<ILookup<TKey, T  را برگشت می‌دهد؛ زیرا DataLoader نیازمند یک متد با نوع برگشتی که در امضایش عنوان شده است می‌باشد .
در ادامه کلاس OwnerType را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository, IDataLoaderContextAccessor dataLoader)
    {
       ...
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context =>
            {
                var loader = dataLoader.Context.GetOrAddCollectionBatchLoader<Guid, Account>("GetAccountsByOwnerIds", repository.GetAccountsByOwnerIds);
                return loader.LoadAsync(context.Source.Id);
            });
    }
}

در کلاس OwnerType، واسط IDataLoaderContextAccessor را در سازنده کلاس تزریق می‌کنیم و سپس متد Context.GetOrAddCollectionBatchLoader را فراخوانی می‌کنیم که در پارامتر اول آن، یک کلید و در پارامتر دوم آن، متد GetAccountsByOwnerIds را از IAccountRepository معرفی می‌کنیم.
سپس باید DataLoader را در متد ConfigureServices موجود در کلاس Startup ثبت کنیم. در ادامه services.AddGraphQL را مطابق زیر ویرایش می‌کنیم: 
services.AddGraphQL(o => { o.ExposeExceptions = false; })
        .AddGraphTypes(ServiceLifetime.Scoped)
        .AddDataLoader(); 
اکنون پروژه را با دستور زیر اجرا کنید و سپس query قبلی را در UI.Playground اجرا کنید.
 اگر log  موجود در Terminal مربوط به  VS Code را مشاهده کنید، متوجه خواهید شد که در این حالت یک query برای تمام owner ‌ها و یک query برای تمام account ‌ها داریم.


Using Arguments in Queries and Handling Errors 
تا کنون ما  یک query را اجرا می‌کردیم که نتیجه آن بازیابی تمام owner ‌ها به همراه تمام account ‌های مربوط به هر owner بود. اکنون می‌خواهیم  براساس id، یک owner  مشخص را بازیابی کنیم. برای انجام این کار نیاز است که یک آرگومان را در query شامل کنیم.
در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را همانند زیر ویرایش می‌کنیم:
public interface IOwnerRepository
{
    ...
    Owner GetById(Guid id);
}

public class OwnerRepository : IOwnerRepository
{
    ...
    Owner GetById(Guid id) => _context.Owners.SingleOrDefault(o => o.Id.Equals(id));
}
سپس کلاس AppQuery  را مطابق زیر ویرایش می‌کنیم:
public class AppQuery : ObjectGraphType
{
    public AppQuery(IOwnerRepository repository)
    {
        ...

        Field<OwnerType>(
            "owner",
            arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
            resolve: context =>
            {
                var id = context.GetArgument<Guid>("ownerId");
                return repository.GetById(id);
            }
        );
    }
}
در اینجا یک فیلد را ایجاد کرده‌ایم که مقدار برگشتی آن یک OwnerType می‌باشد. نام query را owner تعیین می‌کنیم و از بخش arguments، برای ایجاد کردن آرگومان‌های این query استفاده می‌کنیم. آرگومان این query نمی‌تواند NULL باشد و باید از نوع IdGraphType و با نام ownerId باشد و در نهایت بخش resolve است که کاملا گویا می‌باشد. 
اگر پارامتر id، از نوع Guid نباشد، بهتر است که یک پیام را به سمت کلاینت برگشت دهیم. جهت انجام این کار یک اصلاح کوچک در بخش resolve انجام میدهیم:  
Field<OwnerType>(
    "owner",
    arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
    resolve: context =>
    {
        Guid id;
        if (!Guid.TryParse(context.GetArgument<string>("ownerId"), out id))
        {
            context.Errors.Add(new ExecutionError("Wrong value for guid"));
            return null;
        }
 
         return repository.GetById(id);
     }
);

 اکنون پروژه را اجرا کنید و سپس یک query جدید را در  UI.Playground به صورت زیر ارسال  کنید: 
{
  owner(ownerId:"6f513773-be46-4001-8adc-2e7f17d52d83"){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
که نتیجه آن بازیابی یک owner  با ( Id=6f513773-be46-4001-8adc-2e7f17d52d83 ) می‌باشد.

نکته: 
در صورتیکه قصد داشته باشیم علاوه بر id، یک name را هم ارسال کنیم، در بخش resolve به صورت زیر آن را دریافت می‌کنیم: 
 string name = context.GetArgument<string>("name");
و در زمان ارسال query:
{
  owner(ownerId:"53270061-3ba1-4aa6-b937-1f6bc57d04d2", name:"ANDY") {
   ...
  }
}


Aliases, Fragments, Named Queries, Variables, Directives 
می توانیم برای query ‌های ارسال شده از سمت کلاینت با معرفی aliases، یک سری تغییرات را داشته باشیم. وقتی‌که می‌خواهیم نام نتیجه دریافتی یا هر فیلدی را در نتیجه دریافتی تغییر دهیم، بسیار کاربردی می‌باشند. اگر یک query داشته باشیم که یک آرگومان را دارد و بخواهیم دو تا از این query داشته باشیم، برای ایجاد تفاوت بین query ‌ها می‌توان از aliases  استفاده کرد. 
جهت استفاده باید نام مورد نظر را در ابتدای query یا فیلد قرار دهیم:
{
  first:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  },
  second:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  }
}
اینبار در خروجی بجای ownerId ، id و بجای ownerName ، name و ... را مشاهده خواهید کرد.


همانطور که از مثال بالا مشخص است، دو query با فیلد‌های یکسانی را داریم. اگر بجای 2  query یکسان (مانند مثال بالا) ولی با آرگومان‌های متفاوت، اینبار 10 query یکسان با آرگومان‌های متفاوتی را داشته باشیم، در این حالت خواندن query ‌ها مقداری سخت می‌باشد. در این صورت می‌توان این مشکل را با استفاده از fragment‌ها برطرف کرد. Fragment‌‌ها این اجازه را به ما می‌دهند تا فیلد‌ها را با استفاده از کاما ( ، ) از یکدیگر جدا و تبدیل به یک بخش مجزا کنیم و سپس استفاده مجدد از آن بخش را در تمام query ‌ها داشته باشیم. Syntax آن به حالت زیر می‌باشد: 
fragment SampleName on Type{
  ...
}
تعریف یک fragment به نام ownerFields  و استفاده از آن : 
{
  first:owners{
    ...ownerFields
  },
  second:owners{
    ...ownerFields
  },
  ...
}


fragment ownerFields on OwnerType{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
}

برای ایجاد کردن یک named query، مجبور هستیم از کلمه کلیدی query در آغاز کل query استفاده کنیم؛ به همراه نام query، که بعد از کلمه کلیدی query قرار میگیرد. اگر نیاز داشته باشیم می‌توان آرگومان‌ها را به query ارسال کرد.
نکته مهمی که در رابطه با named query ‌ها وجود دارد این است که اگر یک query آرگومان داشته باشد نیاز است از پنجره QUERY VARIABLES برای تخصیص مقدار به آن آرگومان استفاده کنیم. 
query OwnerQuery($ownerId:ID!)
{
  owner(ownerId:$ownerId){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type
    }
  }
}
و سپس در قسمت QUERY VARIABLES 
{
  "ownerId":"6f513773-be46-4001-8adc-2e7f17d52d83"
}
اکنون اجرا کنید و خروجی را مشاهده کنید .

در نهایت می‌توان بعضی فیلد‌ها را از نتیجه دریافتی با استفاده از directive‌ها در query حذف یا اضافه کرد. دو directive وجود دارد که می‌توان از آن‌ها استفاده کرد (include  و skip). 


در قسمت بعد در رابطه با GraphQL Mutations صحبت خواهیم کرد.  
کد‌های مربوط به این قسمت را از اینجا دریافت کنید .  ASPCoreGraphQL_2.zip  
مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
در ادامه مطالب قبل
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2#

قبل از شروع توضیحات متد‌های کلاس Shape در ادامه پست‌های قبل در ^ و ^ ابتدا به تشریح یک تصویر می‌پردازیم.

نحوه ترسیم شی

خوب همانگونه که در تصویر بالا مشاده می‌نمایید، برای رسم یک شی چهار حالت متفاوت ممکن است پیش بیاید. (دقت کنید که ربع اول محور مختصات روی بوم گرافیکی قرار گرفته است، در واقع گوشه بالا و سمت چپ بوم گرافیکی نقطه (0 و 0) محور مختصات است و عرض بوم گرافیکی محور X‌ها و ارتفاع بوم گرافیکی محور Y‌ها را نشان می‌دهد)
  1. در این حالت StartPoint.X < EndPoint.X و StartPoint.Y < EndPoint.Y خواهد بود. (StartPoint نقطه ای است که ابتدا ماوس شروع به ترسیم می‌کند، و EndPoint زمانی است که ماوس رها شده و پایان ترسیم را مشخص می‌کند.)
  2. در این حالت StartPoint.X > EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
  3. در این حالت StartPoint.X > EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
  4. در این حالت StartPoint.X < EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.

ابتدا یک کلاس کمکی به صورت استاتیک تعریف می‌کنیم که متدی جهت پیش نمایش رسم شی در حالت جابجایی ، رسم، و تغییر اندازه دارد.

using System;
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Helpers
    /// </summary>
    public static class Helpers
    {
        /// <summary>
        /// Draws the preview.
        /// </summary>
        /// <param name="g">The g.</param>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundBrush">The background brush.</param>
        /// <param name="shapeType">Type of the shape.</param>
        public static void DrawPreview(Graphics g, PointF startPoint, PointF endPoint, Color foreColor, byte thickness, bool isFill, Brush backgroundBrush, ShapeType shapeType)
        {
            float x = 0, y = 0;
            float width = Math.Abs(endPoint.X - startPoint.X);
            float height = Math.Abs(endPoint.Y - startPoint.Y);
            if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = startPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = endPoint.X;
                y = endPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = endPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = startPoint.X;
                y = endPoint.Y;
            }

            switch (shapeType)
            {
                case ShapeType.Ellipse:
                    if (isFill)
                        g.FillEllipse(backgroundBrush, x, y, width, height);
                    //else
                    g.DrawEllipse(new Pen(foreColor, thickness), x, y, width, height);
                    break;
                case ShapeType.Rectangle:
                    if (isFill)
                        g.FillRectangle(backgroundBrush, x, y, width, height);
                    //else
                    g.DrawRectangle(new Pen(foreColor, thickness), x, y, width, height);
                    break;
                case ShapeType.Circle:
                    float raduis = Math.Max(width, height);

                    if (isFill)
                        g.FillEllipse(backgroundBrush, x, y, raduis, raduis);
                    //else
                    g.DrawEllipse(new Pen(foreColor, thickness), x, y, raduis, raduis);
                    break;
                case ShapeType.Square:
                    float side = Math.Max(width, height);

                    if (isFill)
                        g.FillRectangle(backgroundBrush, x, y, side, side);
                    //else
                    g.DrawRectangle(new Pen(foreColor, thickness), x, y, side, side);
                    break;
                case ShapeType.Line:
                    g.DrawLine(new Pen(foreColor, thickness), startPoint, endPoint);
                    break;
                case ShapeType.Diamond:
                    var points = new PointF[4];
                    points[0] = new PointF(x + width / 2, y);
                    points[1] = new PointF(x + width, y + height / 2);
                    points[2] = new PointF(x + width / 2, y + height);
                    points[3] = new PointF(x, y + height / 2);
                    if (isFill)
                        g.FillPolygon(backgroundBrush, points);
                    //else
                    g.DrawPolygon(new Pen(foreColor, thickness), points);
                    break;
                case ShapeType.Triangle:
                    var tPoints = new PointF[3];
                    tPoints[0] = new PointF(x + width / 2, y);
                    tPoints[1] = new PointF(x + width, y + height);
                    tPoints[2] = new PointF(x, y + height);
                    if (isFill)
                        g.FillPolygon(backgroundBrush, tPoints);
                    //else
                    g.DrawPolygon(new Pen(foreColor, thickness), tPoints);
                    break;
            }
            if (shapeType != ShapeType.Line)
            {
                g.DrawString(String.Format("({0},{1})", x, y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), x - 20, y - 25);
                g.DrawString(String.Format("({0},{1})", x + width, y + height), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), x + width - 20, y + height + 5);
            }
            else
            {
                g.DrawString(String.Format("({0},{1})", startPoint.X, startPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), startPoint.X - 20, startPoint.Y - 25);
                g.DrawString(String.Format("({0},{1})", endPoint.X, endPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), endPoint.X - 20, endPoint.Y + 5);
            }

        }
    }
}
متد های این کلاس:
  • DrawPreview : این متد پیش نمایشی برای شی در زمان ترسیم، جابجایی و تغییر اندازه آماده می‌کند، پارامترهای آن عبارتند از : بوم گرافیکی، نقطه شروع، نقطه پایان و رنگ قلم ترسیم پیش نمایش شی، ضخامت خط، آیا شی توپر باشد؟، الگوی پر کردن پس زمینه شی ، و نوع شی ترسیمی می‌باشد.
در ادامه پست‌های قبل ادامه کد کلاس Shape را تشریح می‌کنیم.
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Net;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Shape (Base Class)
    /// </summary>
    public abstract partial class Shape
    {
#region Constructors (2) 

        /// <summary>
        /// Initializes a new instance of the <see cref="Shape" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        protected Shape(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
        {
            CalulateLocationAndSize(startPoint, endPoint);
            Zindex = zIndex;
            ForeColor = foreColor;
            Thickness = thickness;
            IsFill = isFill;
            BackgroundColor = backgroundColor;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Shape" /> class.
        /// </summary>
        protected Shape() { }

#endregion Constructors 
        
#region Methods (10) 

// Public Methods (9) 

        /// <summary>
        /// Draws the specified g.
        /// </summary>
        /// <param name="g">The g.</param>
        public virtual void Draw(Graphics g)
        {
            if (!IsSelected) return;
            float diff = Thickness + 4;
            Color myColor = Color.DarkSeaGreen;
            g.DrawString(String.Format("({0},{1})", StartPoint.X, StartPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(myColor), StartPoint.X - 20, StartPoint.Y - 25);
            g.DrawString(String.Format("({0},{1})", EndPoint.X, EndPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(myColor), EndPoint.X - 20, EndPoint.Y + 5);
            if (ShapeType != ShapeType.Line)
            {
                g.DrawRectangle(new Pen(myColor), X, Y, Width, Height);

                //  1 2 3
                //  8   4 
                //  7 6 5   
                var point1 = new PointF(StartPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point2 = new PointF((StartPoint.X - diff / 2 + EndPoint.X) / 2, StartPoint.Y - diff / 2);
                var point3 = new PointF(EndPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point4 = new PointF(EndPoint.X - diff / 2, (EndPoint.Y + StartPoint.Y) / 2 - diff / 2);
                var point5 = new PointF(EndPoint.X - diff / 2, EndPoint.Y - diff / 2);
                var point6 = new PointF((StartPoint.X - diff / 2 + EndPoint.X) / 2, EndPoint.Y - diff / 2);
                var point7 = new PointF(StartPoint.X - diff / 2, EndPoint.Y - diff / 2);
                var point8 = new PointF(StartPoint.X - diff / 2, (EndPoint.Y + StartPoint.Y) / 2 - diff / 2);


                g.FillRectangle(new SolidBrush(myColor), point1.X, point1.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point2.X, point2.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point3.X, point3.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point4.X, point4.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point5.X, point5.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point6.X, point6.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point7.X, point7.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point8.X, point8.Y, diff, diff);
            }
            else
            {
                var point1 = new PointF(StartPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point2 = new PointF(EndPoint.X - diff / 2, EndPoint.Y - diff / 2);
                g.FillRectangle(new SolidBrush(myColor), point1.X, point1.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point2.X, point2.Y, diff, diff);
            }
        }

        /// <summary>
        /// Points the in sahpe.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <param name="tolerance">The tolerance.</param>
        /// <returns>
        ///   <c>true</c> if [has point in sahpe] [the specified point]; otherwise, <c>false</c>.
        /// </returns>
        public virtual bool HasPointInSahpe(PointF point, byte tolerance = 5)
        {
            return point.X > (StartPoint.X - tolerance) && point.X < (EndPoint.X + tolerance) && point.Y > (StartPoint.Y - tolerance) && point.Y < (EndPoint.Y + tolerance);
        }

        /// <summary>
        /// Moves the specified location.
        /// </summary>
        /// <param name="location">The location.</param>
        /// <returns></returns>
        public virtual PointF Move(Point location)
        {
            StartPoint = new PointF(location.X, location.Y);
            EndPoint = new PointF(location.X + Width, location.Y + Height);
            return StartPoint;
        }

        /// <summary>
        /// Moves the specified dx.
        /// </summary>
        /// <param name="dx">The dx.</param>
        /// <param name="dy">The dy.</param>
        /// <returns></returns>
        public virtual PointF Move(int dx, int dy)
        {
            StartPoint = new PointF(StartPoint.X + dx, StartPoint.Y + dy);
            EndPoint = new PointF(EndPoint.X + dx, EndPoint.Y + dy);
            return StartPoint;
        }

        /// <summary>
        /// Resizes the specified dx.
        /// </summary>
        /// <param name="dx">The dx.</param>
        /// <param name="dy">The dy.</param>
        /// <returns></returns>
        public virtual SizeF Resize(int dx, int dy)
        {
            EndPoint = new PointF(EndPoint.X + dx, EndPoint.Y + dy);
            return new SizeF(Width, Height);
        }

        /// <summary>
        /// Resizes the specified start point.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="currentPoint">The current point.</param>
        public virtual void Resize(PointF startPoint, PointF currentPoint)
        {
            var dx = (int)(currentPoint.X - startPoint.X);
            var dy = (int)(currentPoint.Y - startPoint.Y);
            if (startPoint.X >= X - 5 && startPoint.X <= X + 5)
            {
                StartPoint = new PointF(currentPoint.X, StartPoint.Y);
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Height = Width;
                }
            }
            else if (startPoint.X >= EndPoint.X - 5 && startPoint.X <= EndPoint.X + 5)
            {
                Width += dx;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Height = Width;
                }
            }
            else if (startPoint.Y >= Y - 5 && startPoint.Y <= Y + 5)
            {
                Y = currentPoint.Y;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Width = Height;
                }
            }
            else if (startPoint.Y >= EndPoint.Y - 5 && startPoint.Y <= EndPoint.Y + 5)
            {
                Height += dy;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Width = Height;
                }
            }
        }

        /// <summary>
        /// Sets the background brush as hatch.
        /// </summary>
        /// <param name="hatchStyle">The hatch style.</param>
        public virtual void SetBackgroundBrushAsHatch(HatchStyle hatchStyle)
        {
            var brush = new HatchBrush(hatchStyle, BackgroundColor);
            BackgroundBrush = brush;
        }

        /// <summary>
        /// Sets the background brush as linear gradient.
        /// </summary>
        public virtual void SetBackgroundBrushAsLinearGradient()
        {
            var brush = new LinearGradientBrush(StartPoint, EndPoint, ForeColor, BackgroundColor);
            BackgroundBrush = brush;
        }

        /// <summary>
        /// Sets the background brush as solid.
        /// </summary>
        public virtual void SetBackgroundBrushAsSolid()
        {
            var brush = new SolidBrush(BackgroundColor);
            BackgroundBrush = brush;
        }
// Private Methods (1) 

        /// <summary>
        /// Calulates the size of the location and.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        private void CalulateLocationAndSize(PointF startPoint, PointF endPoint)
        {
            float x = 0, y = 0;
            float width = Math.Abs(endPoint.X - startPoint.X);
            float height = Math.Abs(endPoint.Y - startPoint.Y);
            if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = startPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = endPoint.X;
                y = endPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = endPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = startPoint.X;
                y = endPoint.Y;
            }
            StartPoint = new PointF(x, y);
            EndPoint = new PointF(X + width, Y + height);
        }

#endregion Methods 
    }
}

حال به تشریح سازنده کلاس می‌پردازیم:
  • Shape: پارامترهای این سازنده به ترتیب عبارتند از نقطه شروع، نقطه پایان، عمق شی، رنگ قلم، ضخامت خط، آیا شی توپر باشد؟، و رنگ پر کردن شی، در این سازنده ابتدا توسط متدی به نام CalulateLocationAndSize(startPoint, endPoint); b نقاط ابتدا و انتهای شی مورد نظر تنظیم می‌شود، در متد مذکور بررسی می‌شود در صورتی که نقاط شروع و پایان یکی از حالت‌های 1 ، 2، 3، 4 از تصویر ابتدا پست باشد همگی تبدیل به حالت 1 خواهد شد.

سپس به تشریح متدهای کلاس Shape می‌پردازیم:

  • Draw: این متد دارای یک پارامتر ورودی است که بوم گرافیکی مورد نظر می‌باشد، در واقع شی مورد نظر خود را بروی این بوم گرافیکی ترسیم می‌کند. در کلاس پایه کار این متد زیاد پیچیده نیست، در صورتی که شی در حالت انتخاب باشد (IsSelected = true) بروی شی مورد نظر 8 مربع کوچک ترسیم می‌شود و اگر شی مورد نظر خط باشد دو مربع کوچک در طرفین خط رسم می‌شود که نشان دهنده انتخاب شدن شی مورد نظر است. این متد به صورت virtual تعریف شده است یعنی کلاس هایی که از Shape ارث میبرند می‌توانند این متد را برای خود از نو بازنویسی کرده (override کنند) و تغییر رفتار دهند.
  • HasPointInSahpe : این متد نیز به صورت virtual تعریف شده است دارای خروجی بولین می‌باشد. پارامتر‌های این متد عبارتند از یک نقطه و یک عدد که نشان دهنده تلرانش نقطه بر حسب پیکسل می‌باشد. کار این متد این است که یک نقطه را گرفته و بررسی می‌کند که آیا نقطه مورد نظر با تلرانس وارد شده آیا در داخل شی واقع شده است یا خیر (مثلا وجود نقطه در مستطیل یا وجود نقطه در دایره فرمول‌های متفاوتی دارند که در اینجا پیش فرض برای تمامی اشیا حالت مستطیل در نظر گرفته شده که می‌توانید آنها را بازنویسی (override) کنید).
  • Move: این متد به عنوان پارامتر یک نقطه را گرفته و شی مورد نظر را به آن نقطه منتقل می‌کند در واقع نقطه شروع و پایان ترسیم شی را تغییر می‌دهد.
  • Move: این متد نیز برای جابجایی شی به کار می‌رود، این متد دارای پارامترهای جابجابی در راستای محور Xها , جابجایی در راستای محور Yها؛ و شی مورد نظر را به آن نقطه منتقل می‌کند در واقع نقطه شروع و پایان ترسیم شی را با توجه به پارامترهای ورودی تغییر می‌دهد. 
  • Resize: این متد نیز برای تغییر اندازه شی به کار می‌رود، این متد دارای پارامترهای تغییر اندازه در راستای محور Xها , تغییر اندازه در راستای محور Yها می‌باشد و نقطه پایان شی مورد نظر را تغییر می‌دهد اما نقطه شروع تغییری نمی‌کند.
  • Resize: این متد نیز برای تغییر اندازه شی به کار می‌رود، در زمان تغییر اندازه شی با ماوس ابتدا یک نقطه شروع وجود دارد که ماوس در آن نقطه کلیک شده و شروع به درگ کردن شی جهت تغییر اندازه می‌کند (پارامتر اول این متد نقطه شروع درگ کردن جهت تغییر اندازه را مشخص می‌کند startPoint)، سپس در یک نقطه ای درگ کردن تمام می‌شود در این نقطه باید شی تغییر اندازه پیدا کرده و ترسیم شود ( پارامتر دوم این متد نقطه مذکور می‌باشد currentLocation). سپس با توجه با این دو نقطه بررسی می‌شود که تغییر اندازه در کدام جهت صورت گرفته است و اعداد جهت تغییرات نقاط شروع و پایان شی مورد نظر محاسبه می‌شوند. (مثلا تغییر اندازه در مستطیل از ضلع بالا به طرفین، یا از ضلع سمت راست به طرفین و ....). البته برای مربع و دایره باید کاری کنیم که طول و عرض تغییر اندازه یکسان باشد.
  • CalulateLocationAndSize: این متد که در سازنده کلاس استفاده شده در واقع دو نقطه شروع و پایان را گرفته و با توجه به تصویر ابتدای پست حالت‌های 1 و 2 و3  و 4 را به حالت 1 تبدیل کرده و StartPoint و EndPoint را اصلاح می‌کند.
  • SetBackgroundBrushAsHatch: این متد یک الگوی Brush گرفته و با توجه به رنگ پس زمینه شی خصوصیت BackgroundBrush را مقداردهی می‌کند.
  • SetBackgroundBrushAsLinearGradient: این متد با توجه به خصوصیت ForeColor و BackgroundColor یک Gradiant Brush ساخته و آن را به خصوصیت
    BackgroundBrush نسبت می‌کند. 
  • SetBackgroundBrushAsSolid: یک الگوی پر کردن توپر برای شی مورد نظر با توجه به خصوصیت BackgroundColor شی ایجاد کرده و آن را به خصوصیت BackgroundBrush شی نسبت می‌دهد.

تذکر: متد‌های Move، Resize و HasPointInShape به صورت virtual تعریف شده تا کلاس‌های مشتق شده در صورت نیاز خود کد رفتار مورد نظر خود را override کرده یا از همین رفتار استفاده نمایند.

خوشحال می‌شم در صورتی که در Refactoring کد نوشته شده با من همکاری کنید.

در پست‌های آینده به بررسی و پیاده سازی دیگر کلاس‌ها خواهیم پرداخت.

مطالب
ارسال ویدیو بصورت Async توسط Web Api
فریم ورک ASP.NET Web API صرفا برای ساخت سرویس‌های ساده‌ای که می‌شناسیم، نیست و در واقع مدل جدیدی برای برنامه نویسی HTTP است. کارهای بسیار زیادی را می‌توان توسط این فریم ورک انجام داد که در این مقاله به یکی از آنها می‌پردازم. فرض کنید می‌خواهیم یک فایل ویدیو را بصورت Asynchronous به کلاینت ارسال کنیم.

ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب آن را MVC + Web API انتخاب کنید.


ابتدا به فایل WebApiConfig.cs در پوشه App_Start مراجعه کنید و مسیر پیش فرض را حذف کنید. برای مسیریابی سرویس‌ها از قابلیت جدید Attribute Routing استفاده خواهیم کرد. فایل مذکور باید مانند لیست زیر باشد.
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();
    }
}
حال در مسیر ریشه پروژه، پوشه جدیدی با نام Videos ایجاد کنید و یک فایل ویدیو نمونه بنام sample.mp4 در آن کپی کنید. دقت کنید که فرمت فایل ویدیو در مثال جاری mp4 در نظر گرفته شده اما به سادگی می‌توانید آن را تغییر دهید.
سپس در پوشه Models کلاس جدیدی بنام VideoStream ایجاد کنید. این کلاس مسئول نوشتن داده فایل‌های ویدیویی در OutputStream خواهد بود. کد کامل این کلاس را در لیست زیر مشاهده می‌کنید.
public class VideoStream
{
    private readonly string _filename;
    private long _contentLength;

    public long FileLength
    {
        get { return _contentLength; }
    }

    public VideoStream(string videoPath)
    {
        _filename = videoPath;
        using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            _contentLength = video.Length;
        }
    }

    public async void WriteToStream(Stream outputStream,
        HttpContent content, TransportContext context)
    {
        try
        {
            var buffer = new byte[65536];

            using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                var length = (int)video.Length;
                var bytesRead = 1;

                while (length > 0 && bytesRead > 0)
                {
                    bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length));
                    await outputStream.WriteAsync(buffer, 0, bytesRead);
                    length -= bytesRead;
                }
            }
        }
        catch (HttpException)
        {
            return;
        }
        finally
        {
            outputStream.Close();
        }
    }
}

شرح کلاس VideoStream
این کلاس ابتدا دو فیلد خصوصی تعریف می‌کند. یکی filename_ که فقط-خواندنی است و نام فایل ویدیو درخواستی را نگهداری می‌کند. و دیگری contentLength_ که سایز فایل ویدیو درخواستی را نگهداری می‌کند.

یک خاصیت عمومی بنام FileLength نیز تعریف شده که مقدار خاصیت contentLength_ را بر می‌گرداند.

متد سازنده این کلاس پارامتری از نوع رشته بنام videoPath را می‌پذیرد که مسیر کامل فایل ویدیوی مورد نظر است. در این متد، متغیر‌های filename_ و contentLength_ مقدار دهی می‌شوند. نکته‌ی قابل توجه در این متد استفاده از پارامتر FileShare.Read است که باعث می‌شود فایل مورد نظر هنگام باز شدن قفل نشود و برای پروسه‌های دیگر قابل دسترسی باشد.

در آخر متد WriteToStream را داریم که مسئول نوشتن داده فایل‌ها به OutputStream است. اول از همه دقت کنید که این متد از کلمه کلیدی async استفاده می‌کند بنابراین بصورت asynchronous اجرا خواهد شد. در بدنه این متد متغیری بنام buffer داریم که یک آرایه بایت با سایز 64KB را تعریف می‌کند. به بیان دیگر اطلاعات فایل‌ها را در پکیج‌های 64 کیلوبایتی برای کلاینت ارسال خواهیم کرد. در ادامه فایل مورد نظر را باز می‌کنیم (مجددا با استفاده از FileShare.Read) و شروع به خواندن اطلاعات آن می‌کنیم. هر 64 کیلوبایت خوانده شده بصورت async در جریان خروجی نوشته می‌شود و تا هنگامی که به آخر فایل نرسیده ایم این روند ادامه پیدا می‌کند.
while (length > 0 && bytesRead > 0)
{
    bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length));
    await outputStream.WriteAsync(buffer, 0, bytesRead);
    length -= bytesRead;
}
اگر دقت کنید تمام کد بدنه این متد در یک بلاک try/catch قرار گرفته است. در صورتی که با خطایی از نوع HttpException مواجه شویم (مثلا هنگام قطع شدن کاربر) عملیات متوقف می‌شود و در آخر نیز جریان خروجی (outputStream) بسته خواهد شد. نکته دیگری که باید بدان اشاره کرد این است که کاربر حتی پس از قطع شدن از سرور می‌تواند ویدیو را تا جایی که دریافت کرده مشاهده کند. مثلا ممکن است 10 پکیج از اطلاعات را دریافت کرده باشد و هنگام مشاهده پکیج دوم از سرور قطع شود. در این صورت امکان مشاهده ویدیو تا انتهای پکیج دهم وجود خواهد داشت.

حال که کلاس VideoStream را در اختیار داریم می‌توانیم پروژه را تکمیل کنیم. در پوشه کنترلر‌ها کلاسی بنام VideoControllerبسازید. کد کامل این کلاس را در لیست زیر مشاهده می‌کنید.
public class VideoController : ApiController
{
    [Route("api/video/{ext}/{fileName}")]
    public HttpResponseMessage Get(string ext, string fileName)
    {
        string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext));
        if (File.Exists(videoPath))
        {
            FileInfo fi = new FileInfo(videoPath);
            var video = new VideoStream(videoPath);

            var response = Request.CreateResponse();

            response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream,
                new MediaTypeHeaderValue("video/" + ext));

            response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fi.Name.Replace(" ", ""));
            response.Content.Headers.Add("Content-Length", video.FileLength.ToString());

            return response;
        }
        else
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
    }
}

شرح کلاس VideoController
همانطور که می‌بینید مسیر دستیابی به این کنترلر با استفاده از قابلیت Attribute Routing تعریف شده است.

[Route("api/video/{ext}/{fileName}")]
نمونه ای از یک درخواست که به این مسیر نگاشت می‌شود:
api/video/mp4/sample
بنابراین این مسیر فرمت و نام فایل مورد نظر را بدین شکل می‌پذیرد. در نمونه جاری ما فایل sample.mp4 را درخواست کرده ایم.
متد Get این کنترلر دو پارامتر با نام‌های ext و fileName را می‌پذیرد که همان فرمت و نام فایل هستند. سپس با استفاده از کلاس HostingEnvironment سعی می‌کنیم مسیر کامل فایل درخواست شده را بدست آوریم.
string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext));
استفاده از این کلاس با Server.MapPath تفاوتی نمی‌کند. در واقع خود Server.MapPath نهایتا همین کلاس HostingEnvironment را فراخوانی می‌کند. اما در کنترلر‌های Web Api به کلاس Server دسترسی نداریم. همانطور که مشاهده می‌کنید فایل مورد نظر در پوشه Videos جستجو می‌شود، که در ریشه سایت هم قرار دارد. در ادامه اگر فایل درخواست شده وجود داشت وهله جدیدی از کلاس VideoStream می‌سازیم و مسیر کامل فایل را به آن پاس می‌دهیم.
var video = new VideoStream(videoPath);
سپس آبجکت پاسخ را وهله سازی می‌کنیم و با استفاده از کلاس PushStreamContent اطلاعات را به کلاینت می‌فرستیم.
var response = Request.CreateResponse();

response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream, new MediaTypeHeaderValue("video/" + ext));

کلاس PushStreamContent در فضای نام System.Net.Http وجود دارد. همانطور که می‌بینید امضای Action پاس داده شده، با امضای متد WriteToStream در کلاس VideoStream مطابقت دارد.

در آخر دو Header به پاسخ ارسالی اضافه می‌کنیم تا نوع داده ارسالی و سایز آن را مشخص کنیم.
response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fileName);
response.Content.Headers.Add("Content-Length", video.FileLength.ToString());
افزودن این دو مقدار مهم است. در صورتی که این Header‌‌ها را تعریف نکنید سایز فایل دریافتی و مدت زمان آن نامعلوم خواهد بود که تجربه کاربری خوبی بدست نمی‌دهد. نهایتا هم آبجکت پاسخ را به کلاینت ارسال می‌کنیم. در صورتی هم که فایل مورد نظر در پوشه Videos پیدا نشود پاسخ NotFound را بر می‌گردانیم.
if(File.Exists(videoPath))
{
    // removed for bravity
}
else
{
    return Request.CreateResponse(HttpStatusCode.NotFound);
}
خوب، برای تست این مکانیزم نیاز به یک کنترلر MVC و یک View داریم. در پوشه کنترلر‌ها کلاسی بنام HomeController ایجاد کنید که با لیست زیر مطابقت داشته باشد.
public class HomeController : Controller
{
    // GET: Home
    public ActionResult Index()
    {
        return View();
    }
}
نمای این متد را بسازید (با کلیک راست روی متد Index و انتخاب گزینه Add View) و کد آن را مطابق لیست زیر تکمیل کنید.
<div>
    <div>
        <video width="480" height="270" controls="controls" preload="auto">
            <source src="/api/video/mp4/sample" type="video/mp4" />
            Your browser does not support the video tag.
        </video>
    </div>
</div>
همانطور که مشاهده می‌کنید یک المنت ویدیو تعریف کرده ایم که خواص طول، عرض و غیره آن نیز مقدار دهی شده اند. زیر تگ source متنی درج شده که در صورت لزوم به کاربر نشان داده می‌شود. گرچه اکثر مرورگرهای مدرن از المنت ویدیو پشتیبانی می‌کنند. تگ سورس فایلی با مشخصات sample.mp4 را درخواست می‌کند و نوع آن را نیز video/mp4 مشخص کرده ایم.

اگر پروژه را اجرا کنید می‌بینید که ویدیو مورد نظر آماده پخش است. برای اینکه ببینید چطور داده‌های ویدیو در قالب پکیج‌های 64 کیلو بایتی دریافت می‌شوند از ابزار مرورگرتان استفاده کنید. مثلا در گوگل کروم F12 را بزنید و به قسمت Network بروید. صفحه را یکبار مجددا بارگذاری کنید تا ارتباطات شبکه مانیتور شود. اگر به المنت sample دقت کنید می‌بینید که با شروع پخش ویدیو پکیج‌های اطلاعات یکی پس از دیگری دریافت می‌شوند و اطلاعات ریز آن را می‌توانید مشاهده کنید.

پروژه نمونه به این مقاله ضمیمه شده است. قابلیت Package Restore فعال شده و برای صرفه جویی در حجم فایل، تمام پکیج‌ها و محتویات پوشه bin حذف شده اند. برای تست بیشتر می‌توانید فایل sample.mp4 را با فایلی حجیم‌تر جایگزین کنید تا نحوه دریافت اطلاعات را با روشی که در بالا بدان اشاره شد مشاهده کنید.

AsyncVideoStreaming.rar  
مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت سوم
پس از بررسی ساختار یک پروژه‌ی افزونه پذیر و همچنین بهبود توزیع فایل‌های استاتیک آن، اکنون نوبت به کار با داده‌ها است. هدف اصلی آن نیز داشتن مدل‌های اختصاصی و مستقل Entity framework code-first به ازای هر افزونه است و سپس بارگذاری و تشخیص خودکار آن‌ها در Context مرکزی برنامه.

پیشنیازها
- آشنایی با مباحث Migrations در EF Code first
- آشنایی با مباحث الگوی واحد کار
- چگونه مدل‌های EF را به صورت خودکار به Context اضافه کنیم؟
- چگونه تنظیمات مدل‌های EF را به صورت خودکار به Context اضافه کنیم؟


کدهایی را که در این قسمت مشاهده خواهید کرد، در حقیقت همان برنامه‌ی توسعه یافته «آشنایی با مباحث الگوی واحد کار» است و از ذکر قسمت‌های تکراری آن جهت طولانی نشدن مبحث، صرفنظر خواهد شد. برای مثال Context و مدل‌های محصولات و گروه‌های آن‌ها به همراه کلاس‌های لایه سرویس برنامه‌ی اصلی، دقیقا همان کدهای مطلب «آشنایی با مباحث الگوی واحد کار» است.


تعریف domain classes مخصوص افزونه‌ها

در ادامه‌ی پروژه‌ی افزونه پذیر فعلی، پروژه‌ی class library جدیدی را به نام MvcPluginMasterApp.Plugin1.DomainClasses اضافه خواهیم کرد. از آن جهت تعریف کلاس‌های مدل افزونه‌ی یک استفاده می‌کنیم. برای مثال کلاس News را به همراه تنظیمات Fluent آن به این پروژه‌ی جدید اضافه کنید:
using System.Data.Entity.ModelConfiguration;
 
namespace MvcPluginMasterApp.Plugin1.DomainClasses
{
    public class News
    {
        public int Id { set; get; }
 
        public string Title { set; get; }
 
        public string Body { set; get; }
    }
 
    public class NewsConfig : EntityTypeConfiguration<News>
    {
        public NewsConfig()
        {
            this.ToTable("Plugin1_News");
            this.HasKey(news => news.Id);
            this.Property(news => news.Title).IsRequired().HasMaxLength(500);
            this.Property(news => news.Body).IsOptional().IsMaxLength();
        }
    }
}
این پروژه برای کامپایل شدن نیاز به بسته‌ی نیوگت ذیل دارد:
 PM> install-package EntityFramework

مشکل! برنامه‌ی اصلی، همانند مطلب «آشنایی با مباحث الگوی واحد کار» دارای domain classes خاص خودش است به همراه تنظیمات Context ایی که صریحا در آن مدل‌های متناظر با این پروژه در معرض دید EF قرار گرفته‌اند:
public class MvcPluginMasterAppContext : DbContext, IUnitOfWork
{
    public DbSet<Category> Categories { set; get; }
    public DbSet<Product> Products { set; get; }
اکنون برنامه‌ی اصلی چگونه باید مدل‌ها و تنظیمات سایر افزونه‌ها را یافته و به صورت خودکار به این Context اضافه کند؟ با توجه به اینکه این برنامه هیچ ارجاع مستقیمی را به افزونه‌ها ندارد.


تغییرات اینترفیس Unit of work جهت افزونه پذیری

در ادامه، اینترفیس بهبود یافته‌ی IUnitOfWork را جهت پذیرش DbSetهای پویا و همچنین EntityTypeConfigurationهای پویا، ملاحظه می‌کنید:
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IUnitOfWork : IDisposable
    {
        IDbSet<TEntity> Set<TEntity>() where TEntity : class;
        int SaveAllChanges();
        void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class;
        IList<T> GetRows<T>(string sql, params object[] parameters) where T : class;
        IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
        void SetDynamicEntities(Type[] dynamicTypes);
        void ForceDatabaseInitialize();
        void SetConfigurationsAssemblies(Assembly[] assembly);
    }
}
متدهای جدید آن:
SetDynamicEntities : توسط این متد در ابتدای برنامه، نوع‌های مدل‌های جدید افزونه‌ها به صورت خودکار به Context اضافه خواهند شد.
SetConfigurationsAssemblies : کار افزودن اسمبلی‌های حاوی تعاریف EntityTypeConfigurationهای جدید و پویا را به عهده دارد.
ForceDatabaseInitialize: سبب خواهد شد تا مباحث migrations، پیش از شروع به کار برنامه، اعمال شوند.

در کلاس Context ذیل، نحوه‌ی پیاده سازی این متدهای جدید را ملاحظه می‌کنید:
namespace MvcPluginMasterApp.DataLayer.Context
{
    public class MvcPluginMasterAppContext : DbContext, IUnitOfWork
    {
        private readonly IList<Assembly> _configurationsAssemblies = new List<Assembly>();
        private readonly IList<Type[]> _dynamicTypes = new List<Type[]>(); 
 
        public void ForceDatabaseInitialize()
        {
            Database.Initialize(force: true);
        }
 
        public void SetConfigurationsAssemblies(Assembly[] assemblies)
        {
            if (assemblies == null) return;
 
            foreach (var assembly in assemblies)
            {
                _configurationsAssemblies.Add(assembly);
            }
        }
 
        public void SetDynamicEntities(Type[] dynamicTypes)
        {
            if (dynamicTypes == null) return;
            _dynamicTypes.Add(dynamicTypes);
        }
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            addConfigurationsFromAssemblies(modelBuilder);
            addPluginsEntitiesDynamically(modelBuilder);
            base.OnModelCreating(modelBuilder);
        }
 
        private void addConfigurationsFromAssemblies(DbModelBuilder modelBuilder)
        {
            foreach (var assembly in _configurationsAssemblies)
            {
                modelBuilder.Configurations.AddFromAssembly(assembly);
            }
        }
 
        private void addPluginsEntitiesDynamically(DbModelBuilder modelBuilder)
        {
            foreach (var types in _dynamicTypes)
            {
                foreach (var type in types)
                {
                    modelBuilder.RegisterEntityType(type);
                }
            }
        }
    }
}
در متد استاندارد OnModelCreating، فرصت افزودن نوع‌های پویا و همچنین تنظیمات پویای آن‌ها وجود دارد. برای این منظور می‌توان از متدهای modelBuilder.RegisterEntityType و modelBuilder.Configurations.AddFromAssembly کمک گرفت.


بهبود اینترفیس IPlugin جهت پذیرش نوع‌های پویای EF

در قسمت اول، با اینترفیس IPlugin آشنا شدیم. هر افزونه باید دارای کلاسی باشد که این اینترفیس را پیاده سازی می‌کند. از آن جهت دریافت تنظیمات و یا ثبت تنظیمات مسیریابی و امثال آن استفاده می‌شود.
در اینجا متد GetEfBootstrapper آن کار دریافت تنظیمات EF هر افزونه را به عهد دارد.
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IPlugin
    {
        EfBootstrapper GetEfBootstrapper();
        //...به همراه سایر متدهای مورد نیاز
    }
 
    public class EfBootstrapper
    {
        /// <summary>
        /// Assemblies containing EntityTypeConfiguration classes.
        /// </summary>
        public Assembly[] ConfigurationsAssemblies { get; set; }
 
        /// <summary>
        /// Domain classes.
        /// </summary>
        public Type[] DomainEntities { get; set; }
 
        /// <summary>
        /// Custom Seed method.
        /// </summary>
        public Action<IUnitOfWork> DatabaseSeeder { get; set; }
    } 
}
ConfigurationsAssemblies مشخص کننده‌ی اسمبلی‌هایی است که حاوی تعاریف EntityTypeConfigurationهای افزونه‌ی جاری هستند.
DomainEntities بیانگر لیست مدل‌ها و موجودیت‌های هر افزونه است.
DatabaseSeeder کار دریافت منطق متد Seed را بر عهده دارد. برای مثال اگر افزونه‌ای نیاز است در آغاز کار تشکیل جداول آن، دیتای پیش فرض و خاصی را در بانک اطلاعاتی ثبت کند، می‌توان از این متد استفاده کرد. اگر دقت کنید این Action یک وهله از IUnitOfWork را به افزونه ارسال می‌کند. بنابراین در این طراحی جدید، اینترفیس IUnitOfWork به پروژه‌ی MvcPluginMasterApp.PluginsBase منتقل می‌شود. به این ترتیب دیگر نیازی نیست تا تک تک افزونه‌ها ارجاع مستقیمی را به DataLayer پروژه‌ی اصلی پیدا کنند.


تکمیل متد GetEfBootstrapper در افزونه‌ها

اکنون جهت معرفی مدل‌ها و تنظیمات EF آن‌ها، تنها کافی است متد GetEfBootstrapper هر افزونه را تکمیل کنیم:
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return new EfBootstrapper
            {
                DomainEntities = new[] { typeof(News) },
                ConfigurationsAssemblies = new[] { typeof(NewsConfig).Assembly },
                DatabaseSeeder = uow =>
                {
                    var news = uow.Set<News>();
                    if (news.Any())
                    {
                        return;
                    }
 
                    news.Add(new News
                    {
                        Title = "News 1",
                        Body = "news 1 news 1 news 1 ...."
                    });
 
                    news.Add(new News
                    {
                        Title = "News 2",
                        Body = "news 2 news 2 news 2 ...."
                    });
                }
            };
        }
در اینجا نحوه‌ی معرفی مدل‌های جدید را توسط خاصیت DomainEntities و تنظیمات متناظر را به کمک خاصیت ConfigurationsAssemblies مشاهده می‌کنید. باید دقت داشت که هر اسمبلی فقط باید یکبار معرفی شود و مهم نیست که چه تعداد تنظیمی در آن وجود دارند. کار یافتن کلیه‌ی تنظیمات از نوع EntityTypeConfigurationها به صورت خودکار توسط EF صورت می‌گیرد.
همچنین توسط delegate ایی به نام DatabaseSeeder، نحوه‌ی دسترسی به متد Set واحد کار و سپس استفاده‌ی از آن، برای تعریف متد Seed سفارشی نیز تکمیل شده‌است.


تدارک یک راه انداز EF، پیش از شروع به کار برنامه

در پوشه‌ی App_Start پروژه‌ی اصلی یا همان MvcPluginMasterApp، کلاس جدید EFBootstrapperStart را با کدهای ذیل اضافه کنید:
[assembly: PreApplicationStartMethod(typeof(EFBootstrapperStart), "Start")]
namespace MvcPluginMasterApp
{
    public static class EFBootstrapperStart
    {
        public static void Start()
        {
            var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
            using (var uow = SmObjectFactory.Container.GetInstance<IUnitOfWork>())
            {
                initDatabase(uow, plugins);
                runDatabaseSeeders(uow, plugins);
            }
        }
 
        private static void initDatabase(IUnitOfWork uow, IEnumerable<IPlugin> plugins)
        {
            foreach (var plugin in plugins)
            {
                var efBootstrapper = plugin.GetEfBootstrapper();
                if (efBootstrapper == null) continue;
 
                uow.SetDynamicEntities(efBootstrapper.DomainEntities);
                uow.SetConfigurationsAssemblies(efBootstrapper.ConfigurationsAssemblies);
            }
 
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MvcPluginMasterAppContext, Configuration>());
            uow.ForceDatabaseInitialize();
        }
 
        private static void runDatabaseSeeders(IUnitOfWork uow, IEnumerable<IPlugin> plugins)
        {
            foreach (var plugin in plugins)
            {
                var efBootstrapper = plugin.GetEfBootstrapper();
                if (efBootstrapper == null || efBootstrapper.DatabaseSeeder == null) continue;
 
                efBootstrapper.DatabaseSeeder(uow);
                uow.SaveAllChanges();
            }
        }
    }
}
در اینجا یک راه انداز سفارشی از نوع PreApplicationStartMethod تهیه شده‌است. Pre بودن آن به معنای اجرای کدهای متد Start این کلاس، پیش از آغاز به کار برنامه و پیش از فراخوانی متد Application_Start فایل Global.asax.cs است.
همانطور که ملاحظه می‌کنید، ابتدا لیست تمام افزونه‌های موجود، به کمک StructureMap دریافت می‌شوند. سپس می‌توان در متد initDatabase به متد GetEfBootstrapper هر افزونه دسترسی یافت و توسط آن تنظیمات مدل‌ها را یافته و به Context اصلی برنامه اضافه کرد. سپس با فراخوانی ForceDatabaseInitialize تمام این موارد به صورت خودکار به بانک اطلاعاتی اعمال خواهند شد.
کار متد runDatabaseSeeders، یافتن DatabaseSeeder هر افزونه، اجرای آن‌ها و سپس فراخوانی متد SaveAllChanges در آخر کار است.



کدهای کامل این سری را از اینجا می‌توانید دریافت کنید:
MvcPlugin
مطالب
پشتیبانی توکار از ایجاد کلاس‌های Singleton از دات نت 4 به بعد
روش‌های زیادی برای ایجاد یک وهله‌ی Singleton وجود دارند. وهله‌ای که در طول عمر یک برنامه، تنها یکبار ایجاد شده و حفظ می‌شود. برای مثال شاید متداول‌ترین حالت آن که در بسیاری از کدها دیده می‌شود، تعریف یک متغیر استاتیک در کلاس، غیرعمومی تعریف کردن سازنده‌ی کلاس و وهله سازی این فیلد استاتیک در صورت نال بودن آن است:
    public class WrongSingleton
    {
        static WrongSingleton _instance;

        WrongSingleton()
        {
        }

        public static WrongSingleton Instance
        {
            get { return _instance ?? (_instance = new WrongSingleton()); }
        }
    }
هرچند این روش کار می‌کند اما thread-safe نیست. به این معنا که ممکن است دو ترد در آن واحد به بررسی قسمت ?? instance_ بپردازند و چون هنوز نال است، دوبار وهله سازی کلاس، با فراخوانی new WrongSingleton صورت خواهد گرفت و هر ترد در آن لحظه به وهله‌ی متفاوتی دسترسی خواهد داشت.
راه حل‌های زیادی برای رفع این مشکل با اعمال مباحث قفل گذاری تا نکات ریز مربوط به کامپایلر وجود دارند که لیست آن‌ها را در اینجا می‌توانید مطالعه کنید.

از دات نت 4 به بعد با معرفی الگوی Lazy، امکان پیاده سازی lazy thread safe singletons به صورت توکار در دسترس می‌باشد. نمونه‌ای از آن در کدهای IoC Container معروفی به نام StructureMap بکار رفته‌است:
    public class Container
    {
        // ...
    }

    public static class ObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);

        public static Container Container
        {
            get { return _containerBuilder.Value; }
        }

        private static Container defaultContainer()
        {
            return new Container();
        }
    }
در اینجا کلاس ObjectFactory یک وهله از کلاس Container را در اختیار مصرف کننده قرار می‌دهد؛ با این شرایط:
- چون این وهله توسط کلاس Lazy محصور شده‌است، صرفا در اولین بار دسترسی به آن، نمونه سازی خواهد شد. این مورد سبب کاهش مصرف حافظه‌ی برنامه و همچنین بالا رفتن سرعت برپایی اولیه‌ی آن می‌شود. بسیاری از اشیایی که در یک برنامه تعریف می‌شوند، شاید الزاما جهت ارائه راه حلی برای مساله‌ای خاص، مورد استفاده قرار نگیرند. تعریف آن‌ها به صورت Lazy، سربار نمونه سازی الزامی آن‌ها را حذف خواهد کرد و آن‌را به اولین بار استفاده از شیء مورد نظر، به تعویق خواهد انداخت.
- با استفاده از LazyThreadSafetyMode.ExecutionAndPublication، چندین ترد می‌توانند به خاصیت Container دسترسی پیدا کنند، اما تنها یکی از آن‌ها موفق به وهله سازی این کلاس خواهد شد. البته حالت ExecutionAndPublication، حالت پیش فرض است و الزاما نیازی به ذکر آن نیست.

اینبار بازنویسی کلاس ابتدای بحث با توجه به نکات ذکر شده به صورت زیر خواهد بود:
    public sealed class LazySingleton
    {
        private static readonly Lazy<LazySingleton> _instance =
            new Lazy<LazySingleton>(() => new LazySingleton(), LazyThreadSafetyMode.ExecutionAndPublication);

        private LazySingleton()
        {
        }

        public static LazySingleton Instance
        {
            get { return _instance.Value; }
        }
    }
- در آن سازنده‌ی کلاس، خصوصی تعریف شده‌است.
- تنها وهله‌ی در دسترس کلاس، به صورت استاتیک و نمونه سازی کلاس، توسط کلاس Lazy با پارامتر LazyThreadSafetyMode.ExecutionAndPublication انجام می‌شود.
- علت استفاده از lambda در سازنده‌ی کلاس Lazy، امکان دسترسی به اعضای private کلاس، از طریق یک خاصیت static است.
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت سوم - تکمیل مستندات یک API با کامنت‌ها
در قسمت قبل موفق شدیم بر اساس OpenAPI specification endpoint تنظیم شده، رابط کاربری خودکاری را توسط ابزار Swagger-UI تولید کنیم. در ادامه می‌خواهیم این مستندات تولید شده را غنی‌تر کرده و کیفیت آن‌را بهبود دهیم.


استفاده از XML Comments برای بهبود کیفیت مستندات API

نوشتن توضیحات XML ای برای متدها و پارامترها در پروژه‌های دات‌نتی، روشی استاندارد و شناخته شده‌است. برای نمونه در AuthorsController، می‌خواهیم توضیحاتی را به اکشن متد GetAuthor آن اضافه کنیم:
/// <summary>
/// Get an author by his/her id
/// </summary>
/// <param name="authorId">The id of the author you want to get</param>
/// <returns>An ActionResult of type Author</returns>
[HttpGet("{authorId}")]
public async Task<ActionResult<Author>> GetAuthor(Guid authorId)
در این حالت اگر برنامه را اجرا کنیم، این توضیحات XMLای هیچ تاثیری را بر روی OpenAPI specification تولیدی و در نهایت Swagger-UI تولید شده‌ی بر اساس آن، نخواهد داشت. برای رفع این مشکل، باید به فایل OpenAPISwaggerDoc.Web.csproj مراجعه نمود و تولید فایل XML متناظر با این توضیحات را فعال کرد:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
پس از تنظیم خاصیت GenerateDocumentationFile به true، با هر بار Build برنامه، فایل xml ای مطابق نام اسمبلی برنامه، در پوشه‌ی bin آن تشکیل خواهد شد؛ مانند فایل bin\Debug\netcoreapp2.2\OpenAPISwaggerDoc.Web.xml در این مثال.
اکنون نیاز است وجود این فایل را به تنظیمات SwaggerDoc در کلاس Startup برنامه، اعلام کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
                setupAction.SwaggerDoc(
                    // ... 
                   );

                var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlCommentsFullPath = Path.Combine(AppContext.BaseDirectory, xmlCommentsFile);
                setupAction.IncludeXmlComments(xmlCommentsFullPath);
            });
        }
در متد IncludeXmlComments، بجای ذکر صریح نام و مسیر فایل OpenAPISwaggerDoc.Web.xml، بر اساس نام اسمبلی جاری، نام فایل XML مستندات تعیین و مقدار دهی شده‌است.
پس از این تنظیمات اگر برنامه را اجرا کنیم، در Swagger-UI حاصل، این تغییرات قابل مشاهده هستند:




افزودن توضیحات به Response

تا اینجا توضیحات پارامترها و متدها را افزودیم؛ اما response از نوع 200 آن هنوز فاقد توضیحات است:


علت را نیز در تصویر فوق مشاهده می‌کنید. قسمت responses در OpenAPI specification، اطلاعات خودش را از اسکیمای مدل‌های مرتبط دریافت می‌کند. بنابراین نیاز است کلاس DTO متناظر با Author را به نحو ذیل تکمیل کنیم:
using System;

namespace OpenAPISwaggerDoc.Models
{
    /// <summary>
    /// An author with Id, FirstName and LastName fields
    /// </summary>
    public class Author
    {
        /// <summary>
        /// The id of the author
        /// </summary>
        public Guid Id { get; set; }

        /// <summary>
        /// The first name of the author
        /// </summary>
        public string FirstName { get; set; }

        /// <summary>
        /// The last name of the author
        /// </summary>
        public string LastName { get; set; }
    }
}
مشکل! در این حالت اگر برنامه را اجرا کنیم، خروجی این توضیحات را در قسمت schemas مشاهده نخواهیم کرد. علت اینجا است که چون اسمبلی OpenAPISwaggerDoc.Models با اسمبلی OpenAPISwaggerDoc.Web یکی نیست و آن‌را از پروژه‌ی اصلی خارج کرده‌ایم، به همین جهت نیاز است ابتدا به فایل OpenAPISwaggerDoc.Models.csproj مراجعه و GenerateDocumentationFile آن‌را فعال کرد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
</Project>
سپس باید فایل xml مستندات آن‌را به صورت مجزایی به تنظیمات ابتدایی برنامه معرفی نمود:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
                setupAction.SwaggerDoc(
  // ...
                   );
                var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml", SearchOption.TopDirectoryOnly).ToList();
                xmlFiles.ForEach(xmlFile => setupAction.IncludeXmlComments(xmlFile));
            });
        }
با توجه به اینکه تمام فایل‌های xml تولید شده در آخر به پوشه‌ی bin\Debug\netcoreapp2.2 کپی می‌شوند، فقط کافی است حلقه‌ای را تشکیل داده و تمام آن‌ها را یکی یکی توسط متد IncludeXmlComments به تنظیمات AddSwaggerGen اضافه کرد.

در این حالت اگر مجددا برنامه را اجرا کنیم، خروجی ذیل را در قسمت schemas مشاهده خواهیم کرد:



بهبود مستندات به کمک Data Annotations

اگر به اکشن متد UpdateAuthor در کنترلر نویسندگان دقت کنیم، چنین امضایی را دارد:
[HttpPut("{authorId}")]
public async Task<ActionResult<Author>> UpdateAuthor(Guid authorId, AuthorForUpdate authorForUpdate)
جائیکه موجودیت Author را در پروژه‌ی OpenAPISwaggerDoc.Entities تعریف کرده‌ایم، نام و نام خانوادگی اجباری بوده و دارای حداکثر طول 150 حرف، هستند. قصد داریم همین ویژگی‌ها را به DTO دریافتی این متد، یعنی AuthorForUpdate نیز اعمال کنیم:
using System.ComponentModel.DataAnnotations;

namespace OpenAPISwaggerDoc.Models
{
    /// <summary>
    /// An author for update with FirstName and LastName fields
    /// </summary>
    public class AuthorForUpdate
    {
        /// <summary>
        /// The first name of the author
        /// </summary>
        [Required]
        [MaxLength(150)]
        public string FirstName { get; set; }

        /// <summary>
        /// The last name of the author
        /// </summary>
        [Required]
        [MaxLength(150)]
        public string LastName { get; set; }
    }
}
پس از افزودن ویژگی‌های Required و MaxLength به این DTO، خروجی Sawgger-UI به صورت زیر بهبود پیدا می‌کند:



بهبود مستندات متد HttpPatch با ارائه‌ی یک مثال

دو نگارش از اکشن متد UpdateAuthor در این مثال موجود هستند:
یکی HttpPut است
[HttpPut("{authorId}")]
public async Task<ActionResult<Author>> UpdateAuthor(Guid authorId, AuthorForUpdate authorForUpdate)
و دیگری HttpPatch:
[HttpPatch("{authorId}")]
public async Task<ActionResult<Author>> UpdateAuthor(
            Guid authorId,
            JsonPatchDocument<AuthorForUpdate> patchDocument)
این مورد آرام آرام در حال تبدیل شدن به یک استاندارد است؛ چون امکان Partial updates را فراهم می‌کند. به همین جهت نسبت به HttpPut، کارآیی بهتری را ارائه می‌دهد. اما چون پارامتر دریافتی آن از نوع ویژه‌ی JsonPatchDocument است و مثال پیش‌فرض مستندات آن، آنچنان مفهوم نیست:


بهتر است در این حالت مثالی را به استفاده کنندگان از آن ارائه دهیم تا در حین کار با آن، به مشکل برنخورند:
/// <summary>
/// Partially update an author
/// </summary>
/// <param name="authorId">The id of the author you want to get</param>
/// <param name="patchDocument">The set of operations to apply to the author</param>
/// <returns>An ActionResult of type Author</returns>
/// <remarks>
/// Sample request (this request updates the author's first name) \
/// PATCH /authors/id \
/// [ \
///     { \
///       "op": "replace", \
///       "path": "/firstname", \
///       "value": "new first name" \
///       } \
/// ] \
/// </remarks>
[HttpPatch("{authorId}")]
public async Task<ActionResult<Author>> UpdateAuthor(
    Guid authorId,
    JsonPatchDocument<AuthorForUpdate> patchDocument)
در اینجا در حین کامنت نویسی، می‌توان از المان remarks، برای نوشتن توضیحات اضافی مانند ارائه‌ی یک مثال، استفاده کرد که در آن op و path معادل‌های بهتری را نسبت به مستندات پیش‌فرض آن پیدا کرده‌‌اند. در اینجا برای ذکر خطوط جدید باید از \ استفاده کرد؛ وگرنه خروجی نهایی، در یک سطر نمایش داده می‌شود:



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

با فعالسازی GenerateDocumentationFile در فایل csproj برنامه، کامپایلر، بلافاصله برای تمام متدها و خواص عمومی که دارای کامنت نیستند، یک warning را صادر می‌کند. یک روش برطرف کردن این مشکل، افزودن کامنت به تمام قسمت‌های برنامه است. روش دیگر آن، تکمیل خواص کامپایلر، جهت مواجه شدن با عدم وجود کامنت‌ها در فایل csproj برنامه است:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>

    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
    <WarningsAsErrors>NU1605;</WarningsAsErrors>
    <NoWarn>1701;1702;1591</NoWarn>
  </PropertyGroup>
توضیحات:
- اگر می‌خواهید خودتان را مجبور به کامنت نویسی کنید، می‌توانید نبود کامنت‌ها را تبدیل به error کنید. برای این منظور خاصیت TreatWarningsAsErrors را به true تنظیم کنید. در این حالت هر کامنت نوشته نشده، به صورت یک error توسط کامپایلر گوشزد شده و برنامه کامپایل نخواهد شد.
- اگر TreatWarningsAsErrors را خاموش کردید، هنوز هم می‌توانید یکسری از warningهای انتخابی را تبدیل به error کنید. برای مثال NU1605 ذکر شده‌ی در خاصیت WarningsAsErrors، مربوط به package downgrade detection warning است.
- اگر به warning نبود کامنت‌ها دقت کنیم به صورت عبارات warning CS1591: Missing XML comment for publicly visible type or member شروع می‌شود. یعنی  CS1591 مربوط به کامنت‌های نوشته نشده‌است. می‌توان برای صرفنظر کردن از آن، شماره‌ی این خطا را بدون CS، توسط خاصیت NoWarn ذکر کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-03.zip

در قسمت بعد، مشکل خروجی تولید response از نوع 200 را که در قسمت دوم به آن اشاره کردیم، بررسی خواهیم کرد.