مطالب
الگوی طراحی Builder همراه با اصول Interface Segregation
الگوی طراحی builder، برای ساختن اشیاء بسیار مفید است؛ اما پروسه ساختن اشیاء آن بسیار پیچده هست و به صورت معمول، این پروسه شامل چندین قسمت می‌شود.
در این مثال ما مشکلات ساختن شیء Person را مورد بررسی قرار می‌دهیم و این شیء از اشیایی کوچکتر مانند Name ، Surname و یا Primary Contact و غیره نیز تشکیل شده است.
class Person : IPerson
{

    private string Name { get; }
    private string Surname { get; }
    private IContact PrimaryContact { get; set; }
    private IList<IContact> AllContacts { get; }

    public Person(string name, string surname, IContact primaryContact)
    {
        if (string.IsNullOrEmpty(name))
            throw new ArgumentException(nameof(name));
        if (string.IsNullOrEmpty(surname))
            throw new ArgumentException(nameof(surname));

        this.Name = name;
        this.Surname = surname;
        this.AllContacts = new List<IContact>();

        this.SetPrimaryContact(primaryContact);
    }

    public void SetPrimaryContact(IContact contact)
    {
        this.AddContact(contact);
        this.PrimaryContact = contact;
    }

    public void AddContact(IContact contact)
    {
        if (contact == null)
            throw new ArgumentNullException(nameof(contact));

        this.AllContacts.Add(contact);
    }
}
همان طور که مشاهده می‌کنید، مقدار دهی شیء IContact  پیچیده‌تر از Name و Surname هست و روش اضافه کردن Contact‌ها نیز بسیار پیچیده است؛ زیرا آنها به دو گروه PrimaryContact و Contacts تقسیم شده‌اند.
 شی Person شامل تعدای Contact مانند تلفن، ایمیل و یا هر چیزی دیگری میتواند باشد. 
در این مثال ما دو نوع Contact داریم که به صورت زیر پیاده سازی شده‌اند: 
interface IContact
{
}

class PhoneNumber : IContact
{

    private string AreaCode { get; }
    private string Number { get; }

    public PhoneNumber(string areaCode, string number)
    {

        if (string.IsNullOrEmpty(areaCode))
            throw new ArgumentException(nameof(areaCode));
        if (string.IsNullOrEmpty(number))
            throw new ArgumentException(nameof(number));

        this.AreaCode = areaCode;
        this.Number = number;
    }
}

class EmailAddress : IContact
{
    private string Address { get; }

    public EmailAddress(string address)
    {
        if (string.IsNullOrEmpty(address))
            throw new ArgumentException(nameof(address));   

        this.Address = address;
    }
}
به صورت کلی سه راه برای ساختن اشیاء وجود دارد:
1) استفاده از سازنده کلاس Person و سپس استفاده از متدهای AddContact و  SetPrimaryContact برای ساختن شیء، به صورت کامل.
2) استفاده از Abstract Factory برای ساختن Person و سپس استفاده از متدهای AddContact و  SetPrimaryContact برای ساختن شیء به صورت کامل.
3) استفاده از Builder برای ساختن شیء به صورت کامل و یکجا همراه با contact‌‌های آن.

 طراحی PersonBuilder : 
interface IPerson
{
    void SetPrimaryContact(IContact primaryContact);
    void AddContact(IContact contact);
}

interface IPersonBuilder
{
    void SetName(string name);
    void SetSurname(string surname);
    void SetPrimaryContact(IContact primaryContact);
    void AddContact(IContact contact);
    IPerson Build();
}
همانطور که مشاهده می‌کنید، یک اینترفیس معمولی از این الگوی طراحی هست که شامل متدهایی است که برای ساختن شیء مورد استفاده قرار میگیرند و در ادامه نحوه پیاده سازی این اینترفیس بیان شده‌است:
class PersonBuilder: IPersonBuilder
{
    private string Name { get; set; }
    private string Surname { get; set; }
    private IContact PrimaryContact { get; set; }
    private IList<IContact> OtherContacts { get; } = new List<IContact>();

    public void SetName(string name)
    {
        if (string.IsNullOrEmpty(name))
            throw new ArgumentException(nameof(name));
        this.Name = name;
    }

    public void SetSurname(string surname)
    {
        if (string.IsNullOrEmpty(surname))
            throw new ArgumentException(nameof(surname));
        this.Surname = surname;
    }

    public void SetPrimaryContact(IContact primaryContact)
    {
        if (primaryContact == null)
            throw new ArgumentNullException(nameof(primaryContact));
        this.PrimaryContact = primaryContact;
    }

    public void AddContact(IContact contact)
    {
        if (contact == null)
            throw new ArgumentNullException(nameof(contact));
        this.OtherContacts.Add(contact);
    }

    public IPerson Build()
    {        
        IPerson person = new Person(this.Name, this.Surname, this.PrimaryContact);

        foreach (IContact contact in this.OtherContacts)
            person.AddContact(contact);

        return person;
    }
}
خوب، اولین مشکلی که در این پیاده سازی مشهود است، مربوط به متد Build هست. اگر مقدار‌های سازنده کلاس Person را به صورت null ارسال کنیم، باعث خطا میشود و این خطا به این خاطر نیست که ما مقدار Null را به کلاس PersonBuilder ارسال کرده‌ایم؛ زیرا ما تمام متد‌های Set را با استفاده NullGurd مورد حفاظت قرار داده‌ایم. مشکل اصلی از وضعیت داخلی شیء PersonBuilder  هست. اگر متد‌های Set را فراخوانی نکنیم، تمام فیلد‌های خصوصی، مقدار null میگیرند و یکی از راه‌های رفع این مشکل این است که پارامتر‌ها را از طریق سازنده PersonBuilder مقدار دهی کنیم. ولی کمی بعدتر متوجه خواهیم شد که این پیاده سازی مانند کلاس person هست و در نتیجه این روش بی استفاده است.

راه حل: استفاده از  Interface Segregation principle در PersonBuilder :
اصل ISP  می‌گوید: "کلاینت‌ها نباید وابسته به متدهایی باشند که آنها را پیاده سازی نمی‌کنند." برای رسیدن به این امر در مثال بالا، باید آن واسط را به واسط‌های کوچکتری تقسیم کرد. این تقسیم بندی باید بر اساس استفاده کنندگان از واسط‌ها صورت گیرد.
برای اینکه شیء Person را بسازیم، متوجه خواهید شد بعضی از داده‌ها الزامی و بعضی دیگر اختیاری هستند؛ مانند PrimaryContact که از داده‌های ضروری شیء Person است. ولی AllContacts می‌تواند به صورت اختیاری تعریف شود و در  پیاده سازی PersonBuilder بالا، کلاینت متوجه نخواهد شد کدام متد اختیاری یا اجباری  هست و در نتیجه ممکن است فراموش کند متد SetPrimaryContact را فراخوانی کند و همین مساله باعث می‌شود تا نرم افزار با خطا مواجه شود.
راه حل: به کد زیر توجه فرمایید: 
class PersonBuilder
{

    private PersonBuilder() { }

    public static IExpectSurnamePersonBuilder WithName(string name)
    {
        ...
    }
}
همانطور که مشاهده می‌فرمایید، سازنده کلاس به صورت خصوصی تعریف شده‌است. درنتیجه بیرون از کلاس نمی‌توان از آن وهله ساخت و ‌‌آن‌را مورد استفاده قرار داد و تنها راه وهله سازی از کلاس PersonBuilder از طریق متد WithName خواهد بود. ثانیا این متد PersonBuilder را برنمی‌گرداند؛ بلکه شیء‌ایی را برمیگ‌رداند که منتظر فراهم کردن مقدار Surname  است و با استفاده از این روش می‌توانیم پروسه فراخوانی متد‌ها را مشخص کنیم.
درنتیجه پروسه ساختن شیء، به چندین قسمت تقسیم شده که به صورت زیر میباشد:
1) فراهم کردن مقدار Surname
2) فراهم کردن مقدار Name
3) فراهم کردن مقدار PrimaryContact
4) فراهم کردن مقدار سایر Contact‌های شخص
5) ساختن شیء Person

پس به ازای هر کدام از عملیات‌ها، یک اینترفیس خواهیم داشت: 
interface IExpectSurnamePersonBuilder
{
    IExpectPrimaryContactPersonBuilder WithSurname(string surname);
}

interface IExpectPrimaryContactPersonBuilder
{
    IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact);
}

interface IExpectOtherContactsPersonBuilder
{
    IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact);
    IPersonBuilder WithNoMoreContacts();
}

interface IPersonBuilder
{
    IPerson Build();
}
حالا نوبت به پیاده سازی PersonBuilder بر اساس اصول ISP است :
class PersonBuilder :
    IExpectSurnamePersonBuilder,
    IExpectPrimaryContactPersonBuilder,
    IExpectOtherContactsPersonBuilder,
    IPersonBuilder
{

    private string Name { get; }
    private string Surname { get; set; }
    private IContact PrimaryContact { get; set; }
    private Person Person { get; set; }

    private PersonBuilder(string name)
    {
        if (string.IsNullOrEmpty(name))
            throw new ArgumentException(nameof(name));
        this.Name = name;
    }

    public static IExpectSurnamePersonBuilder WithName(string name)
    {
        return new PersonBuilder(name);
    }

    public IExpectPrimaryContactPersonBuilder WithSurname(string surname)
    {
        if (string.IsNullOrEmpty(surname))
            throw new ArgumentException(nameof(surname));
        this.Surname = surname;
        return this;
    }

    public IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact)
    {
        if (contact == null)
            throw new ArgumentNullException(nameof(contact));
        this.Person = new Person(this.Name, this.Surname, contact);
        return this;
    }

    public IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact)
    {
        if (contact == null)
            throw new ArgumentNullException(nameof(contact));
        this.Person.AddContact(contact);
        return this;
    }

    public IPersonBuilder WithNoMoreContacts()
    {
        return this;
    }

    public IPerson Build()
    {
        return this.Person;
    }

}
این طراحی به کلاینت کمک خواهد کرد اشیایی را با وضعیت پایدار ایجاد کند و نرم افزاری تولید کند که دارای کمترین خطا باشد.
و اگر کلاینت بخواهد وهله‌ای را از کلاس PersonBuilder بسازد، به صورت زیر خواهد بود:
IPerson person =
    PersonBuilder
    .WithName("Ali")
    .WithSurname("Karimi")
    .WithPrimaryContact(new EmailAddress("admin@gmail.com"))
    .WithOtherContact(new EmailAddress("Test1@work.com"))
    .WithOtherContact(new EmailAddress("Test2@home.com"))
    .WithNoMoreContacts()
    .Build();
اصول طراحی ISP باعث می‌شوند، کد خواناتر شود و همین خوانایی سبب می‌گردد نگهداری و توسعه نرم افزار راحت‌تر شود.

چکیده:
ساختن اشیا در زبان‌های object oriented کار بسیار ساده‌ای است و همین سادگی، خطاهای جبران ناپذیری را به نرم افزار تحمیل میکنند و باعث ایجاد اشیایی ناپایدار در سیستم می‌شود. در اولین گام، الگوی طراحی Builder را به صورت ساده مورد بررسی قرار دادیم و در نهایت این طراحی را تا جای پیش بردیم که بتوانیم اشیایی پایدار را بسازیم. ولی این طراحی هنوز با مشکلاتی رو به رو هست؛ مانند نقض کردن قانون  command query separation  که این مشکل را در مقاله‌ی بعدی برطرف خواهیم کرد.
نظرات مطالب
غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن
یک نکته‌ی تکمیلی: روش ذخیره سازی کلید موقتی تولید شده در بانک اطلاعاتی بجای حافظه‌ی سرور

سیستم data protection به همراه اینترفیسی است به نام IXmlRepository که از آن می‌توان برای مشخص سازی محل ذخیره سازی XML ایی اطلاعات کلید تولید شده استفاده کرد. این امکان هم وجود دارد که این اینترفیس را طوری پیاده سازی کرد تا اطلاعات را درون بانک اطلاعاتی ذخیره کند. به صورت ذیل:
ابتدا کلاس AppDataProtectionKey را به عنوان یک موجودیت جدید به سیستم EF معرفی می‌کنیم:
public class AppDataProtectionKey
{
    public int Id { get; set; }
    public string FriendlyName { get; set; }
    public string XmlData { get; set; }
}
کار این جدول، ذخیره سازی اطلاعات کلید موقتی است تا پس از ری استارت سرور، این اطلاعات از دست نروند و قابلیت بازیابی خودکار را داشته باشند.


سپس آن‌را به Context برنامه به صورت ذیل اضافه می‌کنیم:
 public virtual DbSet<AppDataProtectionKey> AppDataProtectionKeys { get; set; }
با این تنظیمات:
modelBuilder.Entity<AppDataProtectionKey>(builder =>
{
   builder.ToTable("AppDataProtectionKeys");
   builder.HasIndex(e => e.FriendlyName).IsUnique();
});

در ادامه پیاده سازی ویژه‌ی ذیل را از IXmlRepository، که از اطلاعات فوق استفاده می‌کند، تهیه خواهیم کرد:
    public class DataProtectionKeyService : IXmlRepository
    {
        private readonly IServiceProvider _serviceProvider;

        public DataProtectionKeyService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
            _serviceProvider.CheckArgumentIsNull(nameof(_serviceProvider));
        }

        public IReadOnlyCollection<XElement> GetAllElements()
        {
            return _serviceProvider.RunScopedContext<ReadOnlyCollection<XElement>>(context =>
            {
                var dataProtectionKeys = context.Set<AppDataProtectionKey>();
                return new ReadOnlyCollection<XElement>(dataProtectionKeys.Select(k => XElement.Parse(k.XmlData)).ToList());
            });
        }

        public void StoreElement(XElement element, string friendlyName)
        {
            // We need a separate context to call its SaveChanges several times,
            // without using the current request's context and changing its internal state.
            _serviceProvider.RunScopedContext(context =>
            {
                var dataProtectionKeys = context.Set<AppDataProtectionKey>();
                var entity = dataProtectionKeys.SingleOrDefault(k => k.FriendlyName == friendlyName);
                if (null != entity)
                {
                    entity.XmlData = element.ToString();
                    dataProtectionKeys.Update(entity);
                }
                else
                {
                    dataProtectionKeys.Add(new AppDataProtectionKey
                    {
                        FriendlyName = friendlyName,
                        XmlData = element.ToString()
                    });
                }
                context.SaveChanges();
            });
        }
    }
در این اینترفیس نحوه‌ی دسترسی به یک context جدید، اندکی متفاوت است از حالت‌های متداول. در اینجا چون می‌خواهیم این کلاس تاثیری را بر روی واحد کار درخواست جاری نگذارد، یک context جدید را برای آن وهله سازی می‌کنیم و از context موجود در طی طول عمر درخواست جاری استفاده نخواهیم کرد.
اطلاعات متدهای سرویس فوق به صورت خودکار توسط سیستم data-protection تامین می‌شوند. تنها کاری را که در اینجا انجام داده‌ایم، گوش فرادادن به این تغییرات و ذخیره سازی آن‌ها در بانک اطلاعاتی است.

مرحله‌ی آخر کار، معرفی این تغییرات به سیستم است که نحوه‌ی انجام آن‌را در ذیل مشاهده می‌کنید:
        private static void addCustomDataProtection(this IServiceCollection services, SiteSettings siteSettings)
        {
            services.AddScoped<IXmlRepository, DataProtectionKeyService>();
            services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(serviceProvider =>
            {
                return new ConfigureOptions<KeyManagementOptions>(options =>
                {
                    var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
                    using (var scope = scopeFactory.CreateScope())
                    {
                        options.XmlRepository = scope.ServiceProvider.GetService<IXmlRepository>();
                    }
                });
            });
            services
                .AddDataProtection()
                .SetDefaultKeyLifetime(siteSettings.CookieOptions.ExpireTimeSpan)
                .SetApplicationName(siteSettings.CookieOptions.CookieName)
                .UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
                {
                    EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
                    ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
                });
        }
ابتدا محل تامین سرویس IXmlRepository مشخص شده‌است. سپس روش مقدار دهی XmlRepository  را ملاحظه می‌کنید که باید به این صورت باشد. مقدار آن نیز از سرویس DataProtectionKeyService سفارشی ما تامین می‌شود. در انتها طول عمر کلید تولید شده، نام برنامه و الگوریتم‌های مدنظر تنظیم شده‌اند.

همین مقدار تنظیم سبب خواهد شد تا به صورت خودکار اطلاعات موقتی کلیدهای رمزنگاری سیستم data-protection در بانک اطلاعاتی ذخیره شده و یا بازیابی شوند.

این تغییرات به پروژه‌ی DNTIdentity اعمال شده‌اند.
مطالب
امکان مفهوم بخشیدن به رشته‌ها در NET 7.
رشته‌ها، یکی از عمومی‌ترین نوع‌های داده‌ها هستند؛ از آن‌ها در تعریف آدرس‌های اینترنتی، عبارات باقاعده و یا حتی زمان‌ها و تاریخ‌ها استفاده می‌کنیم. در دات نت 7 می‌توان با استفاده از ویژگی جدید StringSyntaxAttribute، به این نوع‌های مختلف اندکی معنا بخشید.


معرفی ویژگی جدید StringSyntax

با استفاده از ویژگی StringSyntax جدید می‌توان مقدار مورد انتظار از رشته‌ی درخواستی را معنادار کرد. برای مثال، Visual Studio سال‌هاست که راهنمایی را در حین تعریف عبارات باقاعده ظاهر می‌کند. اما این راهنما صرفا مختص به ویژوال استودیو است و تا پیش از این راهی وجود نداشت تا عنوان کنیم که برای مثال این رشته قرار است تنها یک عبارت باقاعده باشد. اکنون در دات نت 7 با معرفی ویژگی جدید StringSyntax می‌توان یک چنین intellisense ای را در سایر IDEها نیز شاهد بود.
برای نمونه مثال زیر را درنظر بگیرید:
using System.Diagnostics.CodeAnalysis;

namespace CS11Tests;

public class StringSyntaxAttributeTests
{
    public static void Test()
    {
        RegexTest("");
        DateTest("");
    }

    private static void RegexTest([StringSyntax(StringSyntaxAttribute.Regex)] string regex)
    {
    }

    private static void DateTest([StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string dateTime)
    {
    }
}
در اینجا با استفاده از ویژگی StringSyntax، دقیقا مشخص کرده‌ایم که هدف از تعریف پارامترهای رشته‌ای مدنظر چه چیزی بوده‌است. به این ترتیب، برای مثال در Rider، در حین استفاده از این متدها، به intellisense‌های زیر خواهیم رسید:

راهنمای ظاهر شده جهت تعریف ساده‌تر عبارات باقاعده:


و راهنمای ظاهر شده جهت تعریف ساده‌تر یک DateTime:



امکان استفاده از StringSyntax در دات نت‌های پیش از نگارش 7

هرچند StringSyntax در دات نت 7 تعریف شده‌است؛ اما اگر تعریف کلاس زیر را به همراه فضای نام دقیق آن به پروژه‌های قدیمی‌تر هم اضافه کنیم ... برای دات نت‌های پیش از نگارش 7 هم کار می‌کند:
#if !NET7_0_OR_GREATER

namespace System.Diagnostics.CodeAnalysis
{
  /// <summary>Specifies the syntax used in a string.</summary>
  [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
  public sealed class StringSyntaxAttribute : Attribute
  {
    /// <summary>The syntax identifier for strings containing composite formats for string formatting.</summary>
    public const string CompositeFormat = "CompositeFormat";
    /// <summary>The syntax identifier for strings containing date format specifiers.</summary>
    public const string DateOnlyFormat = "DateOnlyFormat";
    /// <summary>The syntax identifier for strings containing date and time format specifiers.</summary>
    public const string DateTimeFormat = "DateTimeFormat";
    /// <summary>The syntax identifier for strings containing <see cref="T:System.Enum" /> format specifiers.</summary>
    public const string EnumFormat = "EnumFormat";
    /// <summary>The syntax identifier for strings containing <see cref="T:System.Guid" /> format specifiers.</summary>
    public const string GuidFormat = "GuidFormat";
    /// <summary>The syntax identifier for strings containing JavaScript Object Notation (JSON).</summary>
    public const string Json = "Json";
    /// <summary>The syntax identifier for strings containing numeric format specifiers.</summary>
    public const string NumericFormat = "NumericFormat";
    /// <summary>The syntax identifier for strings containing regular expressions.</summary>
    public const string Regex = "Regex";
    /// <summary>The syntax identifier for strings containing time format specifiers.</summary>
    public const string TimeOnlyFormat = "TimeOnlyFormat";
    /// <summary>The syntax identifier for strings containing <see cref="T:System.TimeSpan" /> format specifiers.</summary>
    public const string TimeSpanFormat = "TimeSpanFormat";
    /// <summary>The syntax identifier for strings containing URIs.</summary>
    public const string Uri = "Uri";
    /// <summary>The syntax identifier for strings containing XML.</summary>
    public const string Xml = "Xml";

    /// <summary>Initializes the <see cref="T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute" /> with the identifier of the syntax used.</summary>
    /// <param name="syntax">The syntax identifier.</param>
    public StringSyntaxAttribute(string syntax)
    {
      this.Syntax = syntax;
      this.Arguments = Array.Empty<object>();
    }

    /// <summary>Initializes the <see cref="T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute" /> with the identifier of the syntax used.</summary>
    /// <param name="syntax">The syntax identifier.</param>
    /// <param name="arguments">Optional arguments associated with the specific syntax employed.</param>
    public StringSyntaxAttribute(string syntax, params object?[] arguments)
    {
      this.Syntax = syntax;
      this.Arguments = arguments;
    }

    /// <summary>Gets the identifier of the syntax used.</summary>
    public string Syntax { get; }

    /// <summary>Gets the optional arguments associated with the specific syntax employed.</summary>
    public object?[] Arguments { get; }
  }
}

#endif
مطالب
اتریبیوت اختصاصی برای قفل کردن یک اکشن جهت جلوگیری از تداخلات درخواست‌های همزمان

در کتابخانه‌ی Microsoft AspNetCore Identity میتوان با این کد، فیلد Email را منحصر به‌فرد کرد:

//Program.cs file
builder.Services.AddIdentity<User, Role>(options =>
{
    options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<DatabaseContext>();

برنامه را اجرا و درخواست‌ها را یکی یکی به سمت سرور ارسال میکنیم و اگر ایمیل تکراری باشد به ما خطا میده و میگه: "ایمیل تکراری است".

ولی مشکل اینجاست که کد بالا فیلد Email رو داخل دیتابیس منحصر به‌فرد نمیکنه و فقط از سمت نرم افزار بررسی تکراری بودن ایمیل رو انجام میده. حالا اگه ما با استفاده از نرم افزارهای "تست برنامه‌های وب" مثل Apache JMeter تعداد زیادی درخواست را به سمت برنامه‌مان ارسال کنیم و بعد رکوردهای داخل جدول کاربران را نگاه کنیم، با وجود اینکه داخل نرم افزارمان پراپرتی Email را منحصر به‌فرد کرده‌ایم، ولی چندین رکورد، با یک ایمیل مشابه در داخل جدول User وجود خواهد داشت.

برای تست این سناریو، برنامه Apache JMeter را از این لینک دانلود می‌کنیم (در بخش Binaries فایل zip رو دانلود می کنیم).

نکته: داشتن jdk ورژن 8 به بالا پیش نیاز است. برای اینکه بدونید ورژن جاوای سیستمتون چنده، داخل cmd دستور java -version رو صادر کنید.

اگه تمایل به نصب، یا به روز رسانی jdk را داشتید، میتونید از این لینک استفاده کنید و بسته به سیستم عاملتون، یکی از تب‌های Windows, macOS یا Linux رو انتخاب کنید و فایل مورد نظر رو دانلود کنید (برای Windows فایل x64 Compressed Archive رو دانلود و نصب میکنیم).

حالا فایل دانلود شده JMeter رو استخراج میکنیم، وارد پوشه‌ی bin میشیم و فایل jmeter.bat رو اجرا میکنیم تا برنامه‌ی JMeter اجرا بشه.

قبل از اینکه وارد برنامه JMeter بشیم، کدهای برنامه رو بررسی می‌کنیم.

موجودیت کاربر:

public class User : IdentityUser<int>;

ویوو مدل ساخت کاربر:

public class UserViewModel
{
    public string UserName { get; set; } = null!;

    public string Email { get; set; } = null!;

    public string Password { get; set; } = null!;
}

کنترلر ساخت کاربر:

[ApiController]
[Route("/api/[controller]")]
public class UserController(UserManager<User> userManager) : Controller
{
    [HttpPost]
    public async Task<IActionResult> Add(UserViewModel model)
    {
        var user = new User
        {
            UserName = model.UserName,
            Email = model.Email
        };
        var result = await userManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            return Ok();
        }
        return BadRequest(result.Errors);
    }
}

حالا وارد برنامه JMeter میشیم و اولین کاری که باید انجام بدیم این است که مشخص کنیم چند درخواست را در چند ثانیه قرار است ارسال کنیم. برای اینکار در برنامه JMeter روی TestPlan کلیک راست میکنیم و بعد:

Add -> Threads (Users) -> Thread Group

حالا باید بر روی Thread Group کلیک کنیم و بعد در بخش Number of threads (users) تعداد درخواست‌هایی را که قرار است به سمت سرور ارسال کنیم، مشخص کنیم؛ برای مثال عدد 100.

گزینه Ramp-up period (seconds) برای اینه که مشخص کنیم این 100 درخواست قرار است در چند ثانیه ارسال شوند که آن را روی 0.1 ثانیه قرار می‌دهیم تا درخواست‌ها را با سرعت بسیار زیاد ارسال کند.

الان باید مشخص کنیم چه دیتایی قرار است به سمت سرور ارسال شود:

برای اینکار باید یک Http Request اضافه کنیم. برای این منظور روی Thread Group که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Sampler -> Http Request

حالا روی Http Request کلیک میکنیم و متد ارسال درخواست رو که روی Get هست، به Post تغییر میدیم و بعد Path رو هم به آدرسی که قراره دیتا رو بهش ارسال کنیم، تغییر میدهیم:

https://localhost:7091/api/User

حالا پایینتر Body Data رو انتخاب میکنیم و دیتایی رو که قراره به سمت سرور ارسال کنیم، در قالب Json وارد میکنیم:

{
  "UserName": "payam${__Random(1000, 9999999)}",
  "Email": "payam@gmail.com",
  "Password": "123456aA@"
}

چون بخش UserName در پایگاه داده منحصر به‌فرد است، با این دستور:

${__Random(1000, 9999999)}

یک عدد Random رو به UserName اضافه میکنیم که دچار خطا نشیم.

حالا فقط باید یک Header رو هم به درخواستمون اضافه کنیم، برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Config Element -> Http Header Manager

حالا روی دکمه‌ی Add در پایین صفحه کلیک میکنیم و این Header رو اضافه میکنیم:

Name: Content-Type
Value: application/json

همچنین میتونیم یک View result رو هم اضافه کنیم تا وضعیت تمامی درخواست‌های ارسال شده رو مشاهده کنیم. برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Listener -> View Results Tree

فایل Backup، برای اینکه مراحل بالا رو سریعتر انجام بدید:

File -> Open

حالا بر روی دکمه‌ی سبز رنگ Play در Toolbar بالا کلیک میکنیم تا تمامی درخواست ها را به سمت سرور ارسال کنه و همچنین میتونیم از طریق View result tree ببینیم که چند درخواست موفقیت آمیز و چند درخواست ناموفق انجام شده‌است.

حالا اگر وارد پایگاه داده بشیم، میبینیم که چندین رکورد، با Email یکسان، در جدول User وجود داره:

در حالیکه ایمیل رو در تنظیمات کتابخانه Microsoft AspNetCore Identity به صورت Unique تعریف کرده‌ایم:

//Program.cs file
builder.Services.AddIdentity<User, Role>(options =>
{
    options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<DatabaseContext>();

دلیل این مشکل این است که درخواست‌ها در قالب یک صف، یک به یک اجرا نمیشوند؛ بلکه به صورت همزمان فریم ورک ASP.NET Core برای بالا بردن سرعت اجرای درخواست‌ها از تمامی Thread هایی که در اختیارش هست استفاده می‌کند و در چندین Thread جداگانه، درخواست‌هایی رو به کنترلر User میفرسته و در نتیجه، در یک زمان مشابه، چندین درخواست ارسال میشه که آیا یک ایمیل برای مثال با مقدار payam@yahoo.com وجود داره یا خیر و در تمامی درخواست‌ها چون همزمان انجام شده، جواب خیر است. یعنی ایمیل تکراری با آن مقدار، در پایگاه داده وجود ندارد و تمامی درخواست‌هایی که همزمان به سرور رسیده‌اند، کاربر جدید را با ایمیل مشابهی ایجاد می‌کنند.

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

راه حل

برای حل این مشکل میتوان از Lock statement استفاده کرد که این راه حل نیز یک مشکل دارد که در ادامه به آن اشاره خواهم کرد.

Lock statement به ما این امکان رو میده تا اگر بخشی از کد ما در یک Thread در حال اجرا شدن است، Thread دیگری به آن بخش از کد، دسترسی نداشته باشد و منتظر بماند تا آن Thread کارش با کد ما تموم شود و بعد Thread جدید بتونه کد مارو اجرا کنه.

نحوه استفاده از Lock statement هم بسیار ساده‌است:

public class TestClass
{
    private static readonly object _lock1 = new();

    public void Method1()
    {
        lock (_lock1)
        {
            // Body
        }
    }
}

حالا باید کدهای خودمون رو در بخش Body اضافه کنیم تا دیگر چندین Thread به صورت همزمان، کدهای ما رو اجرا نکنند.

اما یک مشکل وجود داره و آن این است که ما نمیتوانیم در Lock statement، از کلمه کلیدی await استفاده کنیم؛ در حالیکه برای ساخت User جدید باید از await استفاده کنیم:

var result = await userManager.CreateAsync(user, model.Password);

برای حل این مشکل میتوان از کلاس SemaphoreSlim بجای کلمه‌ی کلیدی lock استفاده کرد:

[ApiController]
[Route("/api/[controller]")]
public class UserController(UserManager<User> userManager) : Controller
{
    private static readonly SemaphoreSlim Semaphore = new (initialCount: 1, maxCount: 1);

    [HttpPost]
    public async Task<IActionResult> Add(UserViewModel model)
    {
        var user = new User
        {
            UserName = model.UserName,
            Email = model.Email
        };

        // Acquire the semaphore
        await Semaphore.WaitAsync();
        try
        {
            // Perform user creation
            var result = await userManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                return Ok();
            }
            return BadRequest(result.Errors);
        }
        finally
        {
            // Release the semaphore
            Semaphore.Release();
        }
    }
}

این کلاس نیز مانند lock عمل میکند، ولی توانایی‌های بیشتری را در اختیار ما قرار میدهد؛ برای مثال میتوان تعیین کرد که همزمان چند ترد میتوانند به این کد دسترسی داشته باشند؛ در حالیکه در lock statement فقط یک Thread میتوانست به کد دسترسی داشته باشد. مزیت دیگر کلاس SemaphoreSlim این است که میتوان برای اجرای کدمان Timeout در نظر گرفت تا از بلاک شدن نامحدود Thread جلوگیری کنیم.

با فراخوانی await semaphore.WaitAsync، دسترسی کد ما توسط سایر Thread ها محدود و با فراخوانی Release، کد ما توسط سایر Thread ها قابل دسترسی می‌شود.

مشکل قفل کردن Thread ها

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

پیاده سازی با کمک الگوی AOP

برای اینکه کارمون راحت تر بشه، میتونیم کدهای بالا رو به یک Attribute انتقال بدیم و از اون Attribute در بالای اکشن‌هامون استفاده کنیم تا کل عملیات اکشن‌هامونو رو در یک Thread قفل کنیم:

[AttributeUsage(AttributeTargets.Method)]
public class SemaphoreLockAttribute : Attribute, IAsyncActionFilter
{
    private static readonly SemaphoreSlim Semaphore = new (1, 1);

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // Acquire the semaphore
        await Semaphore.WaitAsync();
        try
        {
            // Proceed with the action
            await next();
        }
        finally
        {
            // Release the semaphore
            Semaphore.Release();
        }
    }
}

حالا میتونیم این Attribute را برای هر اکشنی استفاده کنیم:

[HttpPost]
[SemaphoreLock]
public async Task<IActionResult> Add(UserViewModel model)
{
    var user = new User
    {
        UserName = model.UserName,
        Email = model.Email
    };

    var result = await userManager.CreateAsync(user, model.Password);
    if (result.Succeeded)
    {
        return Ok();
    }
    return BadRequest(result.Errors);
}
مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت دوم
در ادامه قسمت قبلی به برسی ویژگی‌های پیشرفته‌ی AutoMapper می‌پردازیم...


Custom type converters
همانطور که از اسمش مشخصه، زمانی کاربرد داره که نوع عضو یا اعضای یک شی در مبداء، با معادلشون در مقصد یکی نیستند. مثلا فرض کنید نوع Bool در مبداء رو می‌خواهیم به نوع String در مقصد نگاشت کنیم؛ همون Yes و No  معروف بجای True یا False .
کلاس‌های زیر رو در نظر بگیرید:
public class Source
{
    public string Value1 { get; set; }
    public string Value2 { get; set; }
    public string Value3 { get; set; }
}

public class Destination
{
    public int Value1 { get; set; }
    public DateTime Value2 { get; set; }
    public Type Value3 { get; set; }
}
طبق مستندات AutoMapper  اگه بخواهیم این دو رو نگاشت کنیم Exception  میده چون AutoMapper  نمیدونه چطوری باید مثلا Int  رو به String تبدیل کنه؛ برای همین ما باید به AutoMapper  بگیم چطور این تبدیل نوع رو انجام بده.

نکته: در تستی که من انجام دادم، AutoMapper  تبدیل نوع‌های ابتدایی رو خودش انجام میده؛ مثلا همین تبدیل Int به String  رو!

یکی از روش‌های مهیا کردن تبدیل کننده‌ی نوع، پیاده سازی اینترفیس ITypeConverter<TSource, TDestination> هست. تقریبا مثل کاری که در WPF  و SL با پیاده سازی اینترفیس IValueConverter انجام می‌دادیم.   
من برای تست از همون  تبدیل نوع Bool  به String استفاده میکنم و البته بخاطر ساده بودن دیگه  Model ‌ها رو نمی‌نویسم.
ابتدا تعریف کلاس تبدیل کننده‌ی نوع:
public class BooltoStringTypeConvertor : ITypeConverter<bool, string>
    {
        public string Convert(ResolutionContext context)
        {
            return (bool)context.SourceValue ? "Yes" : "No";
        }
    }
و نحوه استفاده:
Mapper.CreateMap<bool,string>().ConvertUsing<BooltoStringTypeConvertor>();
            Mapper.CreateMap<Product, ProductDto>();
            Mapper.AssertConfigurationIsValid();

var product = new Product { Id = 1,Name ="PC" ,InStock = true };
var productDto = Mapper.Map<Product, ProductDto>(product);
خروجی به شکل زیر میشه.

نکته: TypeConvertor‌ها میدان دیدشون سراسریه و نیازی نیست به ازای هر نگاشتی اونو به AutoMapper  معرفی کنیم Global Scope.

Custom value resolvers

کلاس‌های زیر رو در نظر بگیرید

public class Person
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }
}

public class PersonDTO
{
    public int Id { get; set; }

    public string RawData { get; set; }
}
فرض کنید داخل RawData  تمامی اعضای شی مبداء رو به صورت Comma Delimited ذخیره کنیم. برای این کار می‌تونیم از Value Resolver استفاده کنیم.
یک روش برای این کار ارث بری از کلاس Abstract  ی بنام ValueResolver<TSource, TDestination> هست.
public class CommaDelimetedResolver:ValueResolver<Person,string>
    {
        protected override string ResolveCore(Person source)
        {
            return string.Join(",", source.Id, source.FirstName, source.LastName);
        }
    }
و نحوه استفاده
Mapper.CreateMap<Person, PersonDTO>().ForMember(
                des => des.RawData, op => op.ResolveUsing<CommaDelimetedResolver>());


var person = new Person
{
Id = 1,
FirstName = "Mohammad",
LastName = "Saheb",
};

var personDTO = Mapper.Map<Person, PersonDTO>(person);
و خروجی به شکل زیر میشه

نکته: توجه کنید این فقط یک مثال بود و این کار رو با روش‌های دیگه هم میشه انجام داد مثلا MapFrom  و...
نکته: میدان دید Value Resolver‌ها سراسری نیست و باید به ازای هر نگاشتی اونو معرفی کنیم.

Custom Value Formatters
فرض کنید تاریخ رو در بانک، به صورت میلادی ذخیره کرده‌اید و می‌خواهید سمت View به صورت شمسی نمایش بدید. بنابراین در مبدا ویژگی بنام MiladiDate از نوع DateTime دارید و در مقصد ویژگی بنام ShamsiDate از نوع String. هنگام نگاشت، AutoMapper  به صورت پیش فرض ToString رو فراخونی میکنه که بدرد ما نمیخوره و...
برای این کار میشه  از Value Formatter استفاده کرد با پیاده سازی اینترفیس IValueFormatter.
public class ShamsiFormatter:IValueFormatter
    {
        public string FormatValue(ResolutionContext context)
        {
            return ToShamsi(context.SourceValue.ToString());
        }
    }
نحوه استفاده
Mapper.CreateMap<Person, PersonDTO>().ForMember(
            des => des.ShamsiDate, op => op.AddFormatter<ShamsiFormatter>());
مطالب
پیاده سازی عملیات CRUD با استفاده از پروتکل OData
OData  یکی از بهترین روش‌های پیاده سازی RESTful Apis میباشد. Open Data Protocol یا به اصطلاح OData یک data access protocol برای وب میباشد که اجازه‌ی تغییر دادن و نوشتن کوئری درون CRUD مربوطه را میدهد (create - read - update - delete). Asp.Net WebApi از ورژن 3 و 4 این پروتکل بطور کامل پشتیبانی می‌نماید.
در این آموزش ما از WebApi 2.2 , OData V4, Ef 6 استفاده کرده‌ایم.
با استفاده از ویژوال استودیو یک پروژه‌ی Asp.Net را از نوع Empty به نام ProductService میسازیم.

هم چنین در قسمت Add folders and core references تیک گزینه‌ی Web Api را نیز فعال مینماییم.


حال احتیاج به نصب پکیج OData با استفاده از nuget package manager داریم. کافیست دستور زیر را در package manager console وارد نماییم.

Install-Package Microsoft.AspNet.Odata

این دستور آخرین ورژن Odata package را از nuget دانلود مینماید.

بعد از نصب شدن OData نیاز به اضافه کردن یک Model داریم. کلاسی را به نام Product در پوشه‌ی Models میسازیم.

کلاس Product.cs حاوی فیلد‌های زیر است.

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }
    }
}

پراپرتی Id، کلید این entity است و کلاینت میتواند کوئری را بر روی entity، به وسیله‌ی key بزند. برای مثال برای گرفتن Product با Id برابر 2، باید این url را ارسال نمود "(2)Products/"

پرواضح است که Id در Database به عنوان Primary key در نظر گرفته شده است.

حال احتیاج به نصب Entity Framework داریم که با ارسال دستور زیر از طریق nuget نصب خواهد شد

Install-Package EntityFramework

بعد از نصب کردن ef نیاز به اضافه کردن connection string در web config داریم.

<connectionStrings>
    <add name="ProductsContext" connectionString="Data Source=.; 
        Initial Catalog=ProductsContext; Integrated Security=True;MultipleActiveResultSets=True;"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

الان میتوانیم کلاس ProductsContext را درون پوشه‌ی Models ایجاد نماییم. محتویات آن را به صورت زیر وارد مینماییم

using System.Data.Entity;
namespace ProductService.Models
{
    public class ProductsContext : DbContext
    {
        public ProductsContext() 
                : base("name=ProductsContext")
        {
        }
        public DbSet<Product> Products { get; set; }
    }
}

درون Constructor کلاس ProductsContext، داریم name=ProductsContext که باید برابر name درون connection string باشد.

حال نیاز به کانفیگ OData داریم. درون پوشه‌ی App_Start و کلاس WebApiConfig.cs محتویات زیر را جایگزین متد register نمایید:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());
    }
}

این کد دو فرآیند زیر را انجام میدهد

1) ساخت Entity Data Model (EDM)

2) اضافه کردن route

EDM یک مدل انتزاعی از data است. EDM برای تولید سند metadata استفاده میشود. کلاس ODataModelBuilder برای ساخت EDM با استفاده از default naming convention میباشد که باعث کاهش کد‌ها میشود. ضمنا کلاس MapODataServiceRoute برای ساخت OData v4 route میباشد. همانگونه که اطلاع دارید، تعریف route برای مدیریت کردن WebApi و چگونگی مسیریابی درخواست‌های http میباشد.

اگر application شما احتیاج به چند OData endpoint داشته باشد، میتوانید برای هر کدام route‌های جدا و همچنین نام یکتایی را برای routeName و routePrefix آن در نظر بگیرید.


اضافه کردن OData Controller

یک Controller، کلاسی برای مدیریت کردن درخواست‌های http میباشد. شما باید Controllerهای مجزایی را برای هر entity set در OData service خود بسازید. در این مقاله Controller مربوط به موجودیت Product را میسازیم.

در Solution Explorer با کلیک راست بر روی پوشه‌ی Controller، کلاسی به نام ProducsController را میسازیم. دقت کنید نام آن حتما باید به Controller ختم شود.

در OData V3 میتوانیم Controller را با استفاده از Scaffolding بسازیم؛ ولی در V4 این ویژگی وجود ندارد!

محتویات زیر را در این کنترلر اضافه مینماییم:

using ProductService.Models;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.OData;
namespace ProductService.Controllers
{
    public class ProductsController : ODataController
    {
        ProductsContext db = new ProductsContext();
        private bool ProductExists(int key)
        {
            return db.Products.Any(p => p.Id == key);
        } 
        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

این مرحله‌ی ابتدایی از پیاده سازی کنترلر میباشد و در قسمت بعد به پیاده سازی CRUD مربوط به آن میپردازیم.


Querying The Entity Set

این 2 متد را به کنترلر خود اضافه مینماییم

[EnableQuery]
public IQueryable<Product> Get()
{
    return db.Products;
}
[EnableQuery]
public SingleResult<Product> Get([FromODataUri] int key)
{
    IQueryable<Product> result = db.Products.Where(p => p.Id == key);
    return SingleResult.Create(result);
}

ویژگی EnableQuery به معنای امکان Query زدن از سمت کلاینت به آن میباشد. FromODataUri نیز برای امکان پاس دادن پارامتر از طریق Uri است.

متد Get بدون پارامتر، قادر به برگرداندن تمامی Product‌ها میباشد و متد Get با پارامتر، قادر به برگرداندن آن Product خاص با استفاده از unique Id است.

در صورت داشتن EnableQuery با استفاده از Query Option هایی مثل filter$ و sort$ و غیره از سمت کلاینت قادر به تغییر دادن کوئری‌های خود هستیم.


Adding and Entity to Entity Set

برای اجازه دادن به کلاینت، جهت اضافه کردن یک Product به دیتابیس، متد Post زیر را اضافه مینماییم

public async Task<IHttpActionResult> Post(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Created(product);
}


Updation an Entity

OData از دو روش متفاوت برای Update کردن یک موجودیت استفاده مینماید.

1) Patch : امکان partial update برای موجودیت مربوطه را فراهم میسازد.

2) Put : موجودیت جدید را به صورت کامل جایگزین مینماید.

مشکل روش Put این است که کلاینت مجبور به ارسال تمامی فیلد‌های مربوطه میباشد. حتی آن هایی که اساسا تغییری نکرده‌اند. بنابراین روش Patch ترجیح داده میشود.

در هر صورت ما به پیاده سازی هر دو روش می‌پردازیم:

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var entity = await db.Products.FindAsync(key);
    if (entity == null)
    {
        return NotFound();
    }
    product.Patch(entity);
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(entity);
}
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    if (key != update.Id)
    {
        return BadRequest();
    }
    db.Entry(update).State = EntityState.Modified;
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(update);
}

در قسمت Patch کنترلر از <Delta<T استفاده میکند که typeی است برای track کردن تغییرات در مدل مربوطه.


Deleting an Entity

برای حذف هر موجودیت نیز کافیست متد زیر را به کنترلر خود اضافه نمایید:

public async Task<IHttpActionResult> Delete([FromODataUri] int key)
{
    var product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return StatusCode(HttpStatusCode.NoContent);
}

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

حال پروژه‌ی خود را run نموده و آدرس زیر را وارد نمایید:

http://localhost:YourPort/Products

پاسخ، مجموعه‌ای از entity‌های زیر خواهد بود:

{
  "@odata.context":"http://localhost:4516/$metadata#Products","value":[
    {
      "Id":1,"Name":"Ali","Price":2.00,"Category":"aaa"
    },{
      "Id":2,"Name":"Reza","Price":1.00,"Category":"bbb"
    },{
      "Id":3,"Name":"Ahmad","Price":0.00,"Category":"ccc"
    }
  ]
}

شما میتوانید از هر کدام از فیلتر‌های زیر برای کوئری زدن از کلاینت به سمت سرور استفاده نمایید. بطور مثال هر کدام از اینها پاسخ متفاوت و مربوط به خود را برگشت میدهد:

/Products(2)

Productی با آی دی 2 را بر میگرداند.

/Products?$filter=Id gt 1

محصولی را با آی دی بزرگتر از 1، بر میگرداند.

Products?$select=Name

روی محصولات select زده و فقط فیلد Name آن‌ها را بر میگرداند.

Products?$select=Name,Price

آرایه‌ای از objectهایی با پراپرتی Name و Price را بر میگرداند.

/Products?$top=3

فقط 3 رکورد اول را بر میگرداند.


همانطور که ملاحظه میفرمایید، استفاده از OData باعث کمتر شدن کد‌های سمت سرور و همچنین امکان کوئری زدن از سمت کلاینت به سمت سرور را مهیا می‌کند.

بعد از خواندن این مقاله ممکن است به این مساله فکر کنید که این کار باعث کاهش امنیت میشود. باید عرض کنم که امکانات زیادی برای محدود کردن کوئری‌ها، فراهم شده است و هیچ نگرانی از این بابت وجود ندارد. بطور مثال میتوانید تعیین کنید که از entity مربوطه فقط حداکثر 3 پراپرتی قابلیت کوئری زدن را دارند؛ یا اینکه حداکثر در هر کوئری، 10 رکورد قابلیت پاسخ دادن خواهد داشت.

پس بدین صورت میباشد که شما حداکثر امکانات ممکن را به سمت کلاینت میدهید و اختیار بدان واگذار شده که آیا از این امکانات حداکثری، استفاده نماید یا خیر.

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

مطالب
بررسی Bad code smell ها: زنجیره پیام یا Message chain
این کد بد بو در دسته «جلوگیری کنندگان از تغییر» قرار می‌گیرد. معمولا زمانیکه فراخوانی‌هایی مانند تکه کد زیر را در بخشی از کد مشاهده کردید، با چنین کد بد بویی مواجه هستید.  
MethodA().MethodB().MethodC();
فراخوانی هر یک از این متدها در خطی مجزا از کد نیز تشکیل دهنده‌ی این الگوی بد است. استفاده کننده‌ی از این زنجیره پیام، برای استفاده‌ی درست از آن، باید در جریان هریک از حلقه‌های زنجیره و ترتیب فراخوانی آنها باشد. در صورتیکه هر یک از حلقه‌های زنجیره تغییری داشتند، استفاده کننده  نیز باید تغییر کنند. 
به طور مثال:
public class RepresentativeEmployeeQuery 
{ 
    public dynamic GetById(int id) 
    { 
        throw new NotImplementedException(); 
    } 
} 
public class RepresentativeQuery 
{ 
    public dynamic GetById(int id) 
    { 
        throw new NotImplementedException(); 
    } 
} 
public class CustomerQuery 
{ 
    public dynamic GetById(int id) 
    { 
        throw new NotImplementedException(); 
    } 
}
public static class Programm 
{ 
    static void Main(string[] args) 
    { 
        var customer = new CustomerQuery().GetById(1); 
        var representativeId = customer.RepresentativeId; 
        var representative = new RepresentativeQuery().GetById(representativeId); 
        var managerId = representative.ManagerId; 
        var manager  = new RepresentativeEmployeeQuery().GetById(managerId); 
        var managerName = manager.FullName; 
    } 
}
  • کلاس CustomerQuery پرس و جوهای مربوط به مشتری را مدیریت می‌کند.
  • کلاس RepresentativeQuery پرس و جوهای مربوط به نمایندگی را مدیریت می‌کند .
  • کلاس RepresentativeEmployeeQuery پرس و جوهای مربوط به کارمندان نمایندگی را مدیریت می‌کند.
در مثال ذکر شده می‌خواهیم نام مدیریت نمایندگی ای را که یک مشتری از آن خرید کرده است، بدانیم. صرفا جهت نمایش مثال، این کار را در متد main انجام داده‌ایم.  
مشاهده می‌کنید که زنجیره‌ای از پیام‌ها از CustomerQuery تا پایین‌ترین قسمت یعنی RepresentativeEmployeeQuery ارسال شده است. هر یک از مراحل زنجیره بخشی از منطق دریافت نام را مدیریت می‌کنند. مانند دریافت مشتری، دریافت نمایندگی آن و دریافت مدیریت نمایندگی. 

مشکلات این کد بد بو 

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


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

روش‌های اصلاح این کد بد بو حول مخفی کردن زنجیره فراخوانی‌ها هستند. اما در مواردی مانند مثال ذکر شده در این مطلب امکان ادغام یک یا چند متد نیز وجود دارد. به شرطی که این کار ناقض اصل Single responsibility یا دیگر اصول شیء گرایی نباشد. دو نمونه از روش‌های اصلاح این کد بد بو به صورت زیر هستند: 
  • مخفی کردن زنجیره فراخوانی و مستقل سازی استفاده کننده از زنجیره فرخوانی (Hide delegate)  
  • ادغام بخشی از متدها در زنجیره فراخوانی و از بین بردن زنجیره فراخوانی 


چه کدهایی Message Chain نیستند؟ 

معمولا کدهایی که از الگوی domain specific language پیروی می‌کنند، ممکن است شباهت بسیاری به مثال مطرح شده داشته باشند. اما از نظر مفهومی با الگوی مطرح شده متفاوت هستند؛ به این صورت که در کد بد بوی زنجیره پیام‌ها، استفاده کننده از متد نیاز به دانستن تمامی حلقه‌ها و ترتیب آنها را دارد، ولی در domain specific language‌ها معمولا نحوه استفاده از متدها به صورت شبه زبانی گویا هدایت شده و معمولا ترتیب به صورت مخفی مدیریت می‌شود.
مطالب
برنامه نویسی موازی بخش دوم (محافظت از مقادیر مشترک)
 در بخش قبلی، مروری کلی بر مفاهیم اصلی برنامه نویسی موازی، از جمله شرایط و نکات استفاده از آن را بررسی کردیم. در انتهای بخش اول عنوان کردیم که در روند برنامه نویسی موازی، اگر دو یا چند Thread به طور مشترک به داده‌ای دسترسی داشته باشند، امکان بروز Race condition وجود خواهد داشت. پس باید کد خود را Thread Safe کنیم. می‌توان برای کنترل رفتارهای عجیب اشیاء در محیط‌های Multi Thread، عنوان Thread Safety را بکار برد.

به طور کلی ۴ روش در #C برای ایجاد Thread Safety وجود دارند:


1- Lock/Monitor
این دو روش یکسان هستند و مانند هم عمل می‌کنند. در واقع در ابتدا روش Monitor وجود داشته و بعد روش lock برای کوتاهی syntax، به صورت بلاکی به #C افزوده شده‌است. این روش تنهای بر روی Thread‌های داخلی App Domain کنترل دارد (اجازه ورود یک Thread) و نمی‌تواند بر روی Thread‌های خارج از این حوزه در محیط‌های Multi Thread محدودیتی اعمال نماید. منظور از Thread‌های داخلی، Thread هایی هستند که داخل Application ما ایجاد شده‌اند.

به تکه کد زیر توجه کنید:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

 class Program
    {
        static int a = 0;
        static int b = 0;
        static Random random = new Random();
        
        static void Main(string[] args)
        {

            Thread obj = new Thread(Division);
            obj.Start();

            Division();
        }

        static void Division()
        {

            for (int i = 0; i <= 500; i++)
            {

                try
                {
                   
                        //Choosing random numbers between 1 to 5
                        a = random.Next(1, 10);
                        b = random.Next(1, 10);


                        //Dividing
                        double ans = a / b;


                        //Reset Variables
                        a = 0;
                        b = 0;

                        Console.WriteLine("Answer : {0} --> {1}", i, ans);
                    
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            }
        }
    }

همانطور که در کد بالا ملاحظه می‌کنید، متد Division به صورت Thread Safe پیاده سازی نشده‌است! اما مشکل کجاست!؟

با برسی این متد و عملکرد آن متوجه می‌شویم که این متد در یک چرخه‌ی تکرار ۵۰۰ مرتبه‌ای، دو عدد تصادفی را در بازه‌ی ۱ تا ۱۰، انتخاب کرده و آن‌ها را بر هم تقسیم و متغیر‌های تصادفی را با مقدار ۰ پر می‌کند. همین عمل Reset Variable در این متد، باعث بروز خطا در محیط Multi Thread خواهد شد. بدین صورت که اگر این متد مانند مثال بالا توسط دو Thread مجزا فراخوانی شود، یکبار توسط New Thread و بلافاصله در Thread اصلی Application، احتمال این وجود خواهد داشت که در Thread دوم، بعد از انتخاب دو مقدار تصادفی و درست قبل از عملیات تقسیم، به طور همزمان Thread اول عملیات Reset Variable را انجام دهد که باعث بروز خطای تقسیم بر ۰ در Thread دوم می‌شود. این همان مشکلی است که گاها یافتن آن از طریق Debug بسیار دشوار خواهد بود.
اما با تغییر کد به شکل زیر
class Program
    {
        static int a = 0;
        static int b = 0;
        static Random random = new Random();
        static readonly object _object = new object();
        static void Main(string[] args)
        {

            Thread obj = new Thread(Division);
            obj.Start();

            Division();
        }

        static void Division()
        {

            for (int i = 0; i <= 500; i++)
            {

                try
                {
                    Monitor.Enter(_object);
                   
                        //Choosing random numbers between 1 to 5
                        a = random.Next(1, 10);
                        b = random.Next(1, 10);


                        //Dividing
                        double ans = a / b;


                        //Reset Variables
                        a = 0;
                        b = 0;

                        Console.WriteLine("Answer : {0} --> {1}", i, ans);
                    Monitor.Exit(_object);

                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            }
        }
    }

مادامی که یک Thread در حالت انتخاب اعداد تصادفی تا تقسیم و اعلام نتیجه می‌باشد، به Thread‌های داخلی دیگر، اجازه‌ی ورود به این بخش که تحت کنترل Monitor می‌باشد داده نخواهد شد. همانطور که گفته شده، بازه‌ی تحت کنترل مانیتور میتواند با بلاک Lock(object) جایگزین شود. شیء object یک شیء مشترک (static) میان تمام اشیاء است برای کنترل ورود Thread‌ها و قفل گزاری مشترک بین این اشیاء.

2- Mutex:
این نوع قفل گزاری به منظور محافظت منابع مشترک برای جلوگیری از ورود Thread‌های بیرونی استفاده می‌شود. منظور از Thread‌های بیرونی Thread‌های یک کامپیوتر است. همچنین می‌توان از Mutex بجای lock نیز استفاده کرد؛ اما به دلیل هدف کاری Mutex، باید هزینه‌ی بیشتری (تقریبا 50 برابر کندتر از Lock) پرداخت کرد.
 static void Main()
  { 
    using (var mutex = new Mutex (false, "dotnettips.info Demo"))
    {
     
      if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false))
      {
        Console.WriteLine ("Another app instance is running. Bye!");
        return;
      }
      RunProgram();
    }
  }
 
  static void RunProgram()
  {
    Console.WriteLine ("Running. Press Enter to exit");
    Console.ReadLine();
  }
در مثال بالا از یک Mutex نام دار استفاده شده است که به ما این امکان را می‌دهد تا به صورت Computer-Wide روی Thread‌ها ایجاد محدودیت نماییم. اگر متد بالا را در دو ترمینال اجرا کنید، نسخه‌ی دوم اجرا نخواهد شد. البته این نکته را در نظر داشته باشید که این امکان در سیتم عامل‌های مبتنی بر Linux غیرفعال است .
Mutex دارای دو متد مهم است :

۱- WaiteOne : شروع Blocking با این متد خواهد بود و اگر بتواند عملیات blocking را انجام دهد مقدار True را باز می‌گرداند. این متد دارای دو ورودی دیگر نیز هست که در مقالات بعدی به طور مفصل به آن‌ها اشاره خواهد شد. اما بطور خلاصه می‌توان اینگونه عنوان نمود که یک پارامتر زمان وجود دارد که مدت زمان انتظار برای Blocking را مشخص می‌کند و پارامتر Boolean دیگری که در حالت synchronization مورد استفاده قرار می‌گیرد و خروج و یا عدم خروج از دامنه synchronization را مشخص می‌کند.

۲- ReleaseMutex : شروع آزاد سازی انحصار، با این متد انجام می‌شود.

هیچگاه نباید یک Mutex را در کد رها کرد؛ زیرا باعث به‌وجود آمدن خطاهایی در کد خواهد شد. روش‌هایی برای رها سازی وجود دارد مانند Dispose کردن Mutex و یا استفاده از متد ReleaseMutex. قبل از خروج از کد باید دقت داشت در بخش هایی از کد که از این نوع قفل گزاری استفاده شده‌است، حتما باید مکانیسم‌های Exception Handling و یا Disposing را برای مدیریت Mutex ایجاد شده اعمال کرد.

3 -Semaphore 
یک نسخه پیشرفته‌تر از Mutex است که می‌تواند برای Thread‌های داخلی و یا خارجی استفاده شود و روی آنها اعمال محدودیت کند. همچنین می‌تواند اجازه‌ی ورود یک تا چند Thread را به بخشی از کد، برای محافظت از منابع بدهد. Semaphore نیز مانند Mutex دارای متد‌های Wait و Release است. یک Semaphore با ظرفیت ورود یک Thread در لحظه همان Mutex است. همچنین از Semaphore‌‌ها می‌توان در متدهای Async نیز استفاده کرد.

4- SemaphoreSlim
در واقع یک نسخه‌ی پیشرفته از Monitor و یک نسخه‌ی سبک وزن از Semaphore است و به همان شکل به شما اجازه‌ی محدودیت گزاری فقط بر روی Thread‌های داخلی را می‌دهد. اما بجای اجازه‌ی ورود فقط یک Thread، به شما این امکان را می‌دهد که اجازه‌ی ورود همزمان یک یا چند Thread را به انتخاب خود بدهید.

هزینه‌ی اعمال محدودیت (قفل گزاری) روی Thread ها
به طور کل هزینه‌ی قفل گزاری بر روی Thread‌ها بالاست. اما در صورت نیاز باید انتخاب درستی از بین موارد عنوان شده را انتخاب نمود. lock/Monitor و SemaphoreSlim دارای کمترین هزینه و Mutex و Semaphore دارای بیشترین هزینه و سربار هستند. اگر در Application‌های بزرگ از Mutex و Semaphore به درستی استفاده نشود، به جد باعث کندی خواهد شد.

در بخش بعدی مقاله، Double-checked locking را مورد بررسی قرار خواهیم داد.
نظرات مطالب
خلاص شدن از شر deep null check
متاسفانه روش فوق کد نویسی را تا حد زیادی تحت تاثیر قرار می‌دهد، مگر این که روش استفاده از متد الحاقی شما را به خوبی متوجه نشده باشم
به مثال زیر دقت کنید:
public class Customer
    {
        public CustomerInfo Info { get; set; }
        public Int32 GetNameLength()
        {
            return this.IfNotDefault(city => city.Info)
                .IfNotDefault(info => info.CityInfo)
                .IfNotDefault(cityInfo => cityInfo.Name)
                .IfNotDefault(name => name.Length);
        }
    }
    public class CustomerInfo
    {
        public CustomerCityInfo CityInfo { get; set; }
    }
    public class CustomerCityInfo
    {
        public String Name { get; set; }
    }
و برای استفاده داریم:
            Customer customer = new Customer();
            String cityName = customer
                .IfNotDefault(cust => cust.Info)
                .IfNotDefault(info => info.CityInfo)
                .IfNotDefault(city => city.Name);
            Int32 length = customer.GetNameLength();
در حالی که با متد الحاقی زیر داریم
public static TValue GetValue<TObj, TValue>(this TObj obj, Func<TObj, TValue> member, TValue defaultValueOnNull = default(TValue))
        {
            if (member == null)
                throw new ArgumentNullException("member");

            if (obj == null)
                throw new ArgumentNullException("obj");

            try
            {
                return member(obj);
            }
            catch (NullReferenceException)
            {
                return defaultValueOnNull;
            }
        }  
            تعریف ساده‌تر کلاس
   public class Customer
    {
        public CustomerInfo Info { get; set; }

        public Int32 GetNameLength()
        {
            return this.Info.CityInfo.Name.Length;
        }
    }

    public class CustomerInfo
    {
        public CustomerCityInfo CityInfo { get; set; }
    }

    public class CustomerCityInfo
    {
        public String Name { get; set; }
    }  
و سادگی در استفاده
             Customer customer = new Customer();

            String cityName = customer.GetValue(cust => cust.Info.CityInfo.Name, "Not Selected");

            Int32 i = customer.GetValue(cust => cust.GetNameLength());    
شاید بگویید استفاده از Try-Catch سیستم را کند می‌کند، البته نه در آن حدی که فکر می‌کنید، و اگر قسمتی از کد شما به تعداد زیادی در بازه‌ی زمانی کوتاه فراخوانی می‌شود، می‌توانید آنرا به صورت کاملا عادی بنویسید، چون واقعا تعداد این شرایط زیاد نیست و این مورد سناریوی فراگیری نیست، در عوض خوانایی کد بسیار بسیار بالاتر از حالات عادی است.
در ضمن دقت کنید که تا زمانی که خطای NullReference رخ ندهد، سرعت سیستم در حد همان حداقل نیز کاهش نمی‌یابد، بدین جهت که بسیاری از افراد فکر می‌کنند Try-Catch نوشتن به خودی خود برنامه را کند می‌کند، ولی این رخ دادن خطا و جمع آوری StackTrace و ... است که برنامه را کند می‌کند، که شاید در خیلی از موارد اصلا رخ ندهد.
البته کدهای نوشته صرفا نمونه کد است، به هیچ وجه اصول طراحی در آن رعایت نشده است، بلکه سعی کرده ام مثال واضح‌تری بزنم
موفق و پایدار باشید
نظرات مطالب
ایجاد ویژگی‌های اعتبارسنجی سفارشی در ASP.NET Core 3.1 به همراه اعتبارسنجی سمت کلاینت آن‌ها
کتابخانه FluentValidation ،  این قابلیت رو داره در سمت کلاینت اعتبار سنجی انجام بده و شما میتونید از تمامی شروطی که برای مدل خود در نظر گرفتید به همراه بخش کلاینتش رو باهم استفاده کنید. به عنوان مثال میخواهید کد ملی رو اعتبار سنجی کنید؛ مانند خالی نبودن فیلد. ابتدا از کلاس ClientValidationBase ارث بری می‌کنید
public class NationalCodeClientValidator : ClientValidatorBase
    {
        #region Fields

        private NationalCodeValidator NationalCodeValidator => (NationalCodeValidator)Validator;

        #endregion Fields

        #region Methods

        #region Constructors

        public NationalCodeClientValidator(PropertyRule rule, IPropertyValidator validator) : base(rule, validator)
        {
        }

        #endregion Constructors

        #region Override

        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-nationalcode", GetErrorMessage());
        }

        #endregion Override

        #region Utility

        private string GetErrorMessage()
        {
            var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName());
            string messageTemplate;
            try
            {
                messageTemplate = Validator.Options.ErrorMessageSource.GetString(null);
            }
            catch (FluentValidationMessageFormatException)
            {
                messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>();
            }
            var message = formatter.BuildMessage(messageTemplate);
            return message;
        }

        #endregion Utility

        #endregion Methods
    }

سپس کلاسی دیگر تعریف کرده و از PropertyValidator ارث بری می‌کنید:
public class NationalCodeValidator : PropertyValidator
    {
        #region Methods

        #region Constructors

        public NationalCodeValidator() : base(new LanguageStringSource(nameof(NationalCodeValidator)))
        {
        }

        #endregion Constructors

        #region Override

        protected override bool IsValid(PropertyValidatorContext context)
        {
            return true;
        }

        #endregion Override

        #endregion Methods
    }

سپس باید کلاس‌های تعریف شده رو به FluentValidation معرفی کرد:
.AddFluentValidation(option =>
            {
                option.RegisterValidatorsFromAssemblyContaining<CreateBankValidation>();
                option.ConfigureClientsideValidation(AddFluentValidationClientModelValidatorProvider());
            })

 private static Action<FluentValidationClientModelValidatorProvider> AddFluentValidationClientModelValidatorProvider()
        {
            return clientSideValidation =>
            {
                clientSideValidation.Add(typeof(NationalCodeValidator), (context, rule, validator) => new NationalCodeClientValidator(rule, validator));
            };
        }

حال فقط مانده استفاده ساختار Validation در سمت کلاینت:
// بررسی تایید کد ملی

function setCustomValidator() {
    $.validator.unobtrusive.adapters.add('nationalcode', [], function (options) {
        options.rules['nationalcode'] = {};
        options.messages['nationalcode'] = options.message;
    });
    $.validator.addMethod('nationalcode', function (value, element, parameters) {
        if (isValidIranianNationalCode(value)) return true;
        return false;
    });
}