نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت سوم - نرمال سازها و اعتبارسنج‌ها
تغییرات مطابق موارد زیر داده شد ولی باز هم همان ارور 
public class CustomNormalizer : ILookupNormalizer
    {
        public string NormalizeName(string key)
        {
            key = Normalize(key);
            key = key.ApplyCorrectYeKe()
                     .RemoveDiacritics()
                     .CleanUnderLines()
                     .RemovePunctuation();
            key = key.Trim().Replace(" ", "");
            return key;
        }
        public string NormalizeEmail(string key)
        {
            key = Normalize(key);
            key = fixGmailDots(key);
            return key;
        }
        public string Normalize(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
            {
                return null;
            }

            key = key.Trim();
            key = key.ToUpperInvariant();
            return key;
        }

        private static string fixGmailDots(string email)
        {
            email = email.ToLowerInvariant().Trim();
            var emailParts = email.Split('@');
            var name = emailParts[0].Replace(".", string.Empty);

            var plusIndex = name.IndexOf("+", StringComparison.OrdinalIgnoreCase);
            if (plusIndex != -1)
            {
                name = name.Substring(0, plusIndex);
            }

            var emailDomain = emailParts[1];
            emailDomain = emailDomain.Replace("googlemail.com", "gmail.com");

            string[] domainsAllowedDots =
            {
                "gmail.com",
                "facebook.com"
            };

            var isFromDomainsAllowedDots = domainsAllowedDots.Any(domain => emailDomain.Equals(domain));
            return !isFromDomainsAllowedDots ? email : string.Format("{0}@{1}", name, emailDomain);
        }
    }

اینترفیس ILookupNormalizer   به شکل زیر است

#region Assembly Microsoft.Extensions.Identity.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
// C:\Users\kazemi\.nuget\packages\microsoft.extensions.identity.core\3.0.0-preview-18579-0056\lib\netstandard2.0\Microsoft.Extensions.Identity.Core.dll
#endregion

namespace Microsoft.AspNetCore.Identity
{
    //
    // Summary:
    //     Provides an abstraction for normalizing keys for lookup purposes.
    public interface ILookupNormalizer
    {
        //
        // Summary:
        //     Returns a normalized representation of the specified key.
        //
        // Parameters:
        //   key:
        //     The key to normalize.
        //
        // Returns:
        //     A normalized representation of the specified key.
        string Normalize(string key);
    }
}

مطالب
امکان داشتن خروجی‌های Covariant در C# 9.0
در زبان #C، زمانیکه از کلاسی ارث‌بری می‌شود، امکان بازنویسی متدهای کلاس پایه، در صورت معرفی آن‌ها به صورت virtual و یا abstract، وجود دارد؛ اما در این بازنویسی‌ها، تغییر نوع خروجی این متدها مجاز نیست. این محدودیت در C# 9.0 با معرفی Covariant returns برداشته شده‌است؛ با یک شرط: نوع جدید این خروجی، باید covariant نوع اصلی متدی باشد که از کلاس پایه‌ی آن ارث‌بری شده‌است.


وضعیت خروجی متدهای بازنویسی شده تا پیش از C# 9.0

برای توضیح بهتر Covariant returns، نیاز است مثال زیر را بررسی کنیم:
public abstract class Product
{
  public string Name { get; set; }
  public abstract ProductOrder Order(int quantity);
}

public class Book : Product
{
  public string ISBN { get; set; }
  public override ProductOrder Order(int quantity) =>
new BookOrder { Quantity = quantity, Product = this };
}

public class ProductOrder
{
  public int Quantity { get; set; }
}

public class BookOrder : ProductOrder
{
  public Book Product { get; set; }
}
در اینجا یک کلاس abstract و پایه‌ی Product وجود دارد که می‌توان متد abstract سفارش دهی آن‌را در کلاس‌های مشتق شده‌ی از آن، مانند Book، بازنویسی کرد.
همانطور که مشاهده می‌کنید، در کلاس Book، تنها خروجی که برای متد Order بازنویسی شده می‌توان درنظر گرفت، همانی است که در کلاس پایه‌ی Product تعریف شده‌است و قابل تغییر نیست؛ یعنی همان ProductOrder.
همچنین در حین استفاده‌ی از این کلاس‌ها، تبدیل خروجی متد Order، به BookOrder ضروری است:
var book = new Book
{
  Name = "My book",
  ISBN = "11-1-12-22-0"
};
BookOrder orderBook = (BookOrder)book.Order(1);


امکان تغییر خروجی متدهای بازنویسی شده در C# 9.0

در C# 9.0 با مجاز اعلام شدن خروجی‌های covariant، می‌توان تغییرات زیر را به کدهای فوق اعمال کرد:
public class Book : Product
{
  public string ISBN { get; set; }
  public override BookOrder Order(int quantity) =>
      new BookOrder { Quantity = quantity, Product = this };
}
چون کلاس BookOrder از ProductOrder تعریف شده‌ی در کلاس پایه مشتق شده‌است (مفهوم covariant بودن خروجی متد)، می‌توان در C# 9.0 آن‌را به عنوان خروجی متد Order بازنویسی شده‌ی در کلاس Book، تنظیم کرد.
مزایای این ویژگی:
- داشتن یک خروجی مختص و متناسب با کلاس کتاب، مانند BookOrder؛ بجای ارائه‌ی یک خروجی بسیار عمومی ProductOrder.
- نیاز به کار با Generics را برای اینگونه اختصاصی سازی‌ها منتفی می‌کند.
- با این تغییر، دیگر نیازی به تبدیل نوع خروجی متد Order یک کتاب نیست و سطر سفارش دهی را می‌توان به صورت زیر خلاصه کرد:
BookOrder orderBook = book.Order(1);
مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت اول - معرفی، نصب و تعریف قواعد اعتبارسنجی
روش مرسوم اعتبارسنجی اطلاعات مدل‌های ASP.NET Core، با استفاده از data annotations توکار آن است که در بسیاری از موارد هم به خوبی کار می‌کند. اما اگر به دنبال ویژگی‌های دیگری مانند نوشتن آزمون‌های واحد برای اعتبارسنجی اطلاعات، جداسازی شرط‌های اعتبارسنجی از تعاریف مدل‌ها، نوشتن اعتبارسنجی‌های پیچیده به همراه تزریق وابستگی‌ها هستید، کتابخانه‌ی FluentValidation می‌تواند جایگزین بهتر و بسیار کاملتری باشد.


نصب کتابخانه‌ی FluentValidation در پروژه

فرض کنید پروژه‌ی ما از سه پوشه‌ی FluentValidationSample.Web، FluentValidationSample.Models و FluentValidationSample.Services تشکیل شده‌است که اولی یک پروژه‌ی MVC است و دو مورد دیگر classlib هستند.
در پروژه‌ی FluentValidationSample.Models، بسته‌ی نیوگت کتابخانه‌ی FluentValidation را به صورت زیر نصب می‌کنیم:
dotnet add package FluentValidation.AspNetCore


جایگزین کردن سیستم اعتبارسنجی مبتنی بر DataAnnotations با FluentValidation

اکنون فرض کنید در پروژه‌ی Models، مدل ثبت‌نام زیر را اضافه کرده‌ایم که از همان data annotations توکار و استاندارد ASP.NET Core برای اعتبارسنجی اطلاعات استفاده می‌کند:
using System.ComponentModel.DataAnnotations;

namespace FluentValidationSample.Models
{
    public class RegisterModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }

        [DataType(DataType.EmailAddress)]
        [Display(Name = "Email")]
        [EmailAddress]
        public string Email { get; set; }

        [Range(18, 60)]
        [Display(Name = "Age")]
        public int Age { get; set; }
    }
}
برای جایگزین کردن data annotations اعتبارسنجی اطلاعات با روش FluentValidation، می‌توان به صورت زیر عمل کرد:
using FluentValidation;

namespace FluentValidationSample.Models
{
    public class RegisterModelValidator : AbstractValidator<RegisterModel>
    {
        public RegisterModelValidator()
        {
            RuleFor(x => x.UserName).NotNull();
            RuleFor(x => x.Password).NotNull().Length(6, 100);
            RuleFor(x => x.ConfirmPassword).Equal(x => x.Password);
            RuleFor(x => x.Email).EmailAddress();
            RuleFor(x => x.Age).InclusiveBetween(18, 60);
        }
    }
}
برای این منظور ابتدا یک کلاس Validator را با ارث بری از AbstractValidator از نوع مدلی که می‌خواهیم قواعد اعتبارسنجی آن‌را مشخص کنیم، ایجاد می‌کنیم. سپس در سازنده‌ی آن، می‌توان به متدهای تعریف شده‌ی در این کلاس پایه دسترسی یافت.
در اینجا در ابتدا به ازای هر خاصیت کلاس مدل مدنظر، یک RuleFor تعریف می‌شود که با استفاده از static reflection، امکان تعریف strongly typed آن‌ها وجود دارد. سپس ویژگی Required به متد NotNull تبدیل می‌شود و ویژگی StringLength توسط متد Length قابل تعریف خواهد بود و یا ویژگی Compare توسط متد Equal به صورت strongly typed به خاصیت دیگری متصل می‌شود.

پس از این تعاریف، می‌توان ویژگی‌های اعتبارسنجی اطلاعات را از مدل ثبت نام حذف کرد و تنها ویژگی‌های خاص Viewهای MVC را در صورت نیاز باقی گذاشت:
using System.ComponentModel.DataAnnotations;

namespace FluentValidationSample.Models
{
    public class RegisterModel
    {
        [Display(Name = "User name")]
        public string UserName { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        public string ConfirmPassword { get; set; }

        [DataType(DataType.EmailAddress)]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Display(Name = "Age")]
        public int Age { get; set; }
    }
}


تعریف پیام‌های سفارشی اعتبارسنجی

روش تعریف پیام‌های سفارشی شکست اعتبارسنجی اطلاعات را توسط متد WithMessage در ادامه مشاهده می‌کنید:
using FluentValidation;

namespace FluentValidationSample.Models
{
    public class RegisterModelValidator : AbstractValidator<RegisterModel>
    {
        public RegisterModelValidator()
        {
            RuleFor(x => x.UserName)
                .NotNull()
                    .WithMessage("Your first name is required.")
                .MaximumLength(20)
                    .WithMessage("Your first name is too long!")
                .MinimumLength(3)
                    .WithMessage(registerModel => $"Your first name `{registerModel.UserName}` is too short!");

            RuleFor(x => x.Password)
                .NotNull()
                    .WithMessage("Your password is required.")
                .Length(6, 100);

            RuleFor(x => x.ConfirmPassword)
                .NotNull()
                    .WithMessage("Your confirmation password is required.")
                .Equal(x => x.Password)
                    .WithMessage("The password and confirmation password do not match.");

            RuleFor(x => x.Email).EmailAddress();
            RuleFor(x => x.Age).InclusiveBetween(18, 60);
        }
    }
}
به ازای هر متد تعریف یک قاعده‌ی اعتبارسنجی جدید، بلافاصله می‌توان از متد WithMessage نیز استفاده کرد. همچنین این متد می‌تواند به اطلاعات اصل model دریافتی نیز همانند پیام سفارشی مرتبط با MinimumLength نام کاربری، دسترسی پیدا کند.


روش تعریف اعتبارسنجی‌های سفارشی خواص مدل

فرض کنید می‌خواهیم یک کلمه‌ی عبور وارد شده‌ی معتبر، حتما از جمع حروف کوچک، بزرگ، اعداد و symbols تشکیل شده باشد. برای این منظور می‌توان از متد Must استفاده کرد:
using System.Text.RegularExpressions;
using FluentValidation;

namespace FluentValidationSample.Models
{
    public class RegisterModelValidator : AbstractValidator<RegisterModel>
    {
        public RegisterModelValidator()
        {
            RuleFor(x => x.Password)
                .NotNull()
                    .WithMessage("Your password is required.")
                .Length(6, 100)
                .Must(password => hasValidPassword(password));
            //...

        }

        private static bool hasValidPassword(string password)
        {
            var lowercase = new Regex("[a-z]+");
            var uppercase = new Regex("[A-Z]+");
            var digit = new Regex("(\\d)+");
            var symbol = new Regex("(\\W)+");
            return lowercase.IsMatch(password) &&
                    uppercase.IsMatch(password) &&
                    digit.IsMatch(password) &&
                    symbol.IsMatch(password);
        }
    }
}
متد Must، می‌تواند مقدار خاصیت متناظر را نیز در اختیار ما قرار دهد و بر اساس آن مقدار می‌توان خروجی true/false ای را بازگشت داد تا نشان شکست و یا موفقیت آمیز بودن اعتبارسنجی اطلاعات باشد.

البته lambda expression نوشته شده را می‌توان توسط method groups، به صورت زیر نیز خلاصه نوشت:
RuleFor(x => x.Password)
    .NotNull()
        .WithMessage("Your password is required.")
    .Length(6, 100)
    .Must(hasValidPassword);


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

اگر نیاز به استفاده‌ی از متد hasValidPassword در کلاس‌های دیگری نیز وجود دارد، می‌توان اینگونه اعتبارسنجی‌های سفارشی را به کلاس‌های مجزایی نیز تبدیل کرد. برای مثال فرض کنید که می‌خواهیم ایمیل دریافت شده، فقط از یک دومین خاص قابل قبول باشد.
using System;
using FluentValidation;
using FluentValidation.Validators;

namespace FluentValidationSample.Models
{
    public class EmailFromDomainValidator : PropertyValidator
    {
        private readonly string _domain;

        public EmailFromDomainValidator(string domain)
            : base("Email address {PropertyValue} is not from domain {domain}")
        {
            _domain = domain;
        }

        protected override bool IsValid(PropertyValidatorContext context)
        {
            if (context.PropertyValue == null) return false;
            var split = context.PropertyValue.ToString().Split('@');
            return split.Length == 2 && split[1].Equals(_domain, StringComparison.OrdinalIgnoreCase);
        }
    }
}
برای این منظور یک کلاس جدید را با ارث‌بری از PropertyValidator تعریف شده‌ی در فضای نام FluentValidation.Validators، ایجاد می‌کنیم. سپس متد IsValid آن‌را بازنویسی می‌کنیم تا برای مثال ایمیل‌ها را صرفا از دومین خاصی بپذیرد.
PropertyValidatorContext امکان دسترسی به نام و مقدار خاصیت در حال اعتبارسنجی را میسر می‌کند. همچنین مقدار کل model جاری را نیز به صورت یک object در اختیار ما قرار می‌دهد.

اکنون برای استفاده‌ی از آن می‌توان از متد SetValidator استفاده کرد:
RuleFor(x => x.Email)
    .SetValidator(new EmailFromDomainValidator("gmail.com"));
و یا حتی می‌توان یک متد الحاقی fluent را نیز برای آن طراحی کرد تا SetValidator را به صورت خودکار فراخوانی کند:
    public static class CustomValidatorExtensions
    {
        public static IRuleBuilderOptions<T, string> EmailAddressFromDomain<T>(
            this IRuleBuilder<T, string> ruleBuilder, string domain)
        {
            return ruleBuilder.SetValidator(new EmailFromDomainValidator(domain));
        }
    }
سپس تعریف قاعده‌ی اعتبارسنجی ایمیل‌ها به صورت زیر تغییر می‌کند:
RuleFor(x => x.Email).EmailAddressFromDomain("gmail.com");


تعریف قواعد اعتبارسنجی خواص تو در تو و لیستی

فرض کنید به RegisterModel این قسمت، دو خاصیت آدرس و شماره تلفن‌ها نیز اضافه شده‌است که یکی به شیء آدرس و دیگری به مجموعه‌ای از آدرس‌ها اشاره می‌کند:
    public class RegisterModel
    {
        // ...

        public Address Address { get; set; }

        public ICollection<Phone> Phones { get; set; }
    }

    public class Phone
    {
        public string Number { get; set; }
        public string Description { get; set; }
    }

    public class Address
    {
        public string Location { get; set; }
        public string PostalCode { get; set; }
    }
در یک چنین حالتی، ابتدا به صورت متداول، قواعد اعتبارسنجی Phone و Address را جداگانه تعریف می‌کنیم:
    public class PhoneValidator : AbstractValidator<Phone>
    {
        public PhoneValidator()
        {
            RuleFor(x => x.Number).NotNull();
        }
    }

    public class AddressValidator : AbstractValidator<Address>
    {
        public AddressValidator()
        {
            RuleFor(x => x.PostalCode).NotNull();
            RuleFor(x => x.Location).NotNull();
        }
    }
سپس برای تعریف اعتبارسنجی دو خاصیت پیچیده‌ی اضافه شده، می‌توان از همان متد SetValidator استفاده کرد که اینبار پارامتر ورودی آن، نمونه‌ای از AbstractValidator‌های هرکدام است. البته برای خاصیت مجموعه‌ای اینبار باید با متد RuleForEach شروع کرد:
    public class RegisterModelValidator : AbstractValidator<RegisterModel>
    {
        public RegisterModelValidator()
        {
            // ...

            RuleFor(x => x.Address).SetValidator(new AddressValidator());

            RuleForEach(x => x.Phones).SetValidator(new PhoneValidator());
        }


در قسمت بعد، روش‌های مختلف استفاده‌ی از قواعد اعتبارسنجی تعریف شده را در یک برنامه‌ی ASP.NET Core بررسی می‌کنیم.



برای مطالعه‌ی بیشتر
- «FluentValidation #1»
مطالب
آشنایی با JSON؛ ساده - خوانا - کم حجم

(JSON (JavaScript Object Notation یک راه مناسب برای نگهداری اطلاعات است و از لحاظ ساختاری شباهت زیادی به XML، رقیب قدیمی خود دارد.

وب سرویس و آجاکس برای انتقال اطلاعات از این روش استفاده می‌کنند و بعضی از پایگاه‌های داده مانند RavenDB بر مبنای این تکنولوژی پایه گذاری شده اند.

هیچ چیزی نمی‌تواند مثل یک مثال؛ خوانایی ، سادگی و کم حجم بودن این روش را نشان دهد :

اگر یک شئ با ساختار زیر در سی شارپ داشته باشید :

class Customer
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

ساختار JSON متناظر با آن ( در صورت این که مقدار دهی شده باشد ) به صورت زیر است: 

{
   "Id":1,
   "FirstName":"John",
   "LastName":"Doe"
}

و در یک مثال پیچیده‌تر :

class Customer
{
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public Car Car { get; set; }
        public IEnumerable<Location> Locations { get; set; }
}

class Location
{
        public int Id { get; set; }
        public string Address { get; set; }
        public int Zip { get; set; }
}

class Car
{
        public int Id { get; set; }
        public string Model { get; set; }
}
{
      "Id":1,
      "FirstName":"John",
      "LastName":"Doe",
      "Car": {
                     "Id":1,
                     "Model":"Nissan GT-R"
               },
      "Locations":[
                            {
                                  "Id":1,
                                  "Address":"30 Mortensen Avenue, Salinas",
                                  "Zip":93905
                            },
                            {
                                  "Id":2,
                                  "Address":"65 West Alisal Street, #210, Salinas",
                                  "Zip":95812
                            }
                      ]
}

ساختار JSON را مجموعه ای از ( نام - مقدار ) تشکیل می‌دهد. ساختار مشابه آن در زبان سی شارپ KeyValuePair است.

مشاهده این تصاویر، بهترین درک را از ساختار JSON به شما می‌دهد.

Json.net یکی از بهترین کتابخانه هایی است که برای کار با این تکنولوژی در net. ارائه شده است. بهترین روش اضافه نمودن آن به پروژه NuGet است.برای این کار دستور زیر را در Package Manager Console وارد کنید.

PM> Install-Package Newtonsoft.Json

با استفاده از کد زیر می‌توانید یک Object را به فرمت JSON تبدیل کنید.

 var customer = new Customer
                               {
                                   Id = 1,
                                   FirstName = "John",
                                   LastName = "Doe",
                                   Car = new Car
                                             {
                                                 Id = 1,
                                                 Model = "Nissan GT-R"
                                             },
                                   Locations = new[]
                                                   {
                                                       new Location
                                                           {
                                                               Id = 1,
                                                               Address = "30 Mortensen Avenue, Salinas",
                                                               Zip = 93905
                                                           },
                                                       new Location
                                                           {
                                                               Id = 2,
                                                               Address = "65 West Alisal Street, #210, Salinas",
                                                               Zip = 95812
                                                           },
                                                   }
                               };
 var data = Newtonsoft.Json.JsonConvert.SerializeObject(customer);

خروجی تابع SerializeObject رشته ای است که محتوی آن را در چهارمین بلاک کد که در بالا‌تر آمده است، می‌توانید مشاهده کنید.

برای Deserialize کردن (Cast اطلاعات با فرمت JSON به کلاس موردنظر) از روش زیر بهره می‌گیریم :

var customer = Newtonsoft.Json.JsonConvert.DeserializeObject<Customer>(data);

آشنایی با این تکنولوژی، پیش درآمدی برای چشیدن طعم NoSQL و معرفی کارآمد‌ترین روش‌های آن است که در آینده خواهیم آموخت...
خوشحال می‌شوم اگر نظرات شما را در باره این موضوع بدانم.
مطالب
آشنایی با Refactoring - قسمت 11

قسمت یازدهم آشنایی با Refactoring به توصیه‌هایی جهت بالا بردن خوانایی تعاریف مرتبط با اعمال شرطی می‌پردازد.

الف) شرط‌های ترکیبی را کپسوله کنید

عموما حین تعریف شرط‌های ترکیبی، هدف اصلی از تعریف آن‌ها پشت انبوهی از && و || گم می‌شود و برای بیان مقصود، نیاز به نوشتن کامنت خواهند داشت. مانند:

using System;

namespace Refactoring.Day11.EncapsulateConditional.Before
{
public class Element
{
private string[] Data { get; set; }
private string Name { get; set; }
private int CreatedYear { get; set; }

public string FindElement()
{
if (Data.Length > 1 && Name == "E1" && CreatedYear > DateTime.Now.Year - 1)
return "Element1";

if (Data.Length > 2 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2)
return "Element2";

return string.Empty;
}
}
}

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

using System;

namespace Refactoring.Day11.EncapsulateConditional.After
{
public class Element
{
private string[] Data { get; set; }
private string Name { get; set; }
private int CreatedYear { get; set; }

public string FindElement()
{
if (hasOneYearOldElement)
return "Element1";

if (hasTwoYearsOldElement)
return "Element2";

return string.Empty;
}

private bool hasTwoYearsOldElement
{
get { return Data.Length > 2 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2; }
}

private bool hasOneYearOldElement
{
get { return Data.Length > 1 && Name == "E1" && CreatedYear > DateTime.Now.Year - 1; }
}
}
}


همانطور که ملاحظه می‌کنید پس از این جایگزینی، خوانایی متد FindElement بهبود یافته است و برنامه نویس اگر 6 ماه بعد به این کدها مراجعه کند نخواهد گفت: «من این کدها رو نوشتم؟!»؛ چه برسد به سایرینی که احتمالا قرار است با این کدها کار کرده و یا آن‌ها را نگهداری کنند.


ب) از تعریف خواص Boolean با نام‌های منفی پرهیز کنید

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

namespace Refactoring.Day11.RemoveDoubleNegative.Before
{
public class Customer
{
public decimal Balance { get; set; }

public bool IsNotFlagged
{
get { return Balance > 30m; }
}
}
}

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

namespace Refactoring.Day11.RemoveDoubleNegative.After
{
public class Customer
{
public decimal Balance { get; set; }

public bool IsFlagged
{
get { return Balance <= 30m; }
}
}
}

مطالب
Query Options در پروتکل OData
در قسمت قبل  با OData به صورت مختصر آشنا شدیم. در این قسمت به امکانات توکار OData و جزئیات query options پرداخته و همچنین قابلیت‌های امنیتی این پروتکل را بررسی مینماییم.
در قسمت قبلی، config مربوط به OData و همچنین Controller و Crud مربوط به آن entity پیاده سازی شد. در این قسمت ابتدا سه موجودیت را به نام‌های Product ، Category و همچنین Supplier، به صورت زیر تعریف مینماییم:

به این صورت مدل‌های خود را تعریف کرده و طبق مقاله‌ی قبلی، Controller‌های هر یک را پیاده سازی نمایید:

public class Supplier
{
    [Key]
    public string Key {get; set; }
    public string Name { get; set; }
}
public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخه‌ی قبلی آن است. اولین نکته‌ی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Option‌های مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:

public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder();

            var product = odataModelBuilder.EntitySet<Product>("Products");
            var category = odataModelBuilder.EntitySet<Category>("Categories");
            var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers");

            var edmModel = odataModelBuilder.GetEdmModel();

            supplier.EntityType.Ignore(c => c.Name);

            config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.MaxTop(25);
            config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);

            //config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی
           config.MapODataServiceRoute("ODataRoute", "odata",
                builder =>
                {
                    builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
                    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
                });
}
باید همه‌ی Query option‌هایی را که به آنها نیاز داریم، معرفی نماییم و فرض کنید که ProductsController و متد Get آن بدین صورت پیاده سازی شده باشد:
[EnableQuery]
        public IQueryable<Product> Get()
        {
            return new List<Product>
            {
                new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } },
                new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } },
                new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } },
                new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } },
            }.AsQueryable(); ;
        }
در این پروتکل به صورت توکار، optionهای زیر قابل استفاده است:

 Description Option 
 بسط دادن موجودیت مرتبط  $expand
 فیلتر کردن نتیجه، بر اساس شرط‌های Boolean ی  $filter
 فرمان به سرور که تعداد رکورد‌های بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging )  $count
 مرتب کردن نتیجه‌ی بازگشتی  $orderby
 select زدن روی پراپرتی‌های درخواستی  $select
 پرش کردن از اولین رکورد به اندازه‌ی n عدد  $skip
 فقط بازگرداندن n رکورد اول  $top
کوئری‌های زیر را در نظر بگیرید:

 در کوئری اول، فقط فیلد‌های Id,Name از Products برگشت داده خواهند شد و در کوئری دوم، از 2 رکورد اول، صرفنظر می‌شود و از بقیه‌ی آنها، فقط 3 رکورد بازگشت داده میشود:
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
در پاسخ کوئری فوق، تعداد رکورد‌های بازگشتی نیز نمایش داده میشوند:
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…}
@odata.context:"http://localhost:4516/odata/$metadata#Products"
@odata.count:4
value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…]
0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0}
1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0}
2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0}
3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
در response، این مقادیر به همراه تعداد رکورد بازگشتی، نمایش داده میشوند که برای پیاده سازی paging مناسب است.
/odata/Products?$filter=Id eq 1
در کوئری فوق eq مخفف equal و به معنای برابر است و بجای آن میتوان از gt به معنای بزرگتر و lt به معنای کوچکتر نیز استفاده کرد:
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
در کوئری فوق نیز به صورت واضح، بر روی فیلد Id، مرتب سازی به صورت نزولی خواهد بود و در صورت وجود نداشتن کلمه کلیدی desc، به صورت صعودی خواهد بود.
بسط دادن موجودیت‌های دیگر نیز بدین شکل زیر میباشد:
/odata/Products?$expand=Category
برای اینکه چندین موجودیت دیگر نیز بسط داده شوند، اینگونه رفتار مینماییم:
/odata/Products?$expand=Category,Supplier
برای اینکه به صورت عمیق به موجودیت‌های دیگر بسط داده شود، بصورت زیر:
/odata/Categories(1)?$expand=Products/Supplier
و برای اینکه حداکثر تعداد رکورد بازگشتی را مشخص نماییم:
[EnableQuery(PageSize = 10)]


محدود کردن Query Options
به صورت زیر میتوانیم فقط option‌های دلخواه را فراخوانی نماییم. مثلا در اینجا فقط اجازه‌ی Skip و Top داده شده است و بطور مثال Select قابل فراخوانی نیست:
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
برای اینکه فقط اجازه‌ی logical function زیر را بدهیم (فقط eq):
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
برای اینکه Property خاصی در edm قابلیت نمایش نباشد، در config بطور مثال Price را ignore مینماییم:
var product = odataModelBuilder.EntitySet<Product>("Products");
product.EntityType.Ignore(e => e.Price);
برای اینکه فقط بر روی فیلد‌های خاصی بتوان از orderby استفاده نمود:
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
یک option به نام value$ برای بازگرداندن تنها آن رکورد مورد نظر، به صورت مجزا میباشد. برای اینکار بطور مثال متد زیر را به کنترلر خود اضافه کنید:
public System.Web.Http.IHttpActionResult GetName(int key)
        {
            Product product = Get().Single(c => c.Id == key);            
            return Ok(product.Name);
        }
/odata/Products(1)/Name/$value
و حاصل کوئری فوق، مقداری بطور مثال برابر زیر خواهد بود و نه به صورت convention پاسخ‌های OData، فرمت بازگشتی "text/plain" خواهد بود و نه json:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3

Ali

Attribute Convention
هایی هم برای اعتبارسنجی پراپرتی‌ها موجود است که نام آن‌ها واضح تعریف شده‌اند:
 Description  Attribute
 اجازه‌ی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد  NotFilterable
 اجازه‌ی مرتب کردن بر روی آن پراپرتی داده نخواهد شد  NotSortable
 اجازه‌ی select زدن بر روی آن پراپرتی داده نمیشود  NotNavigable
 اجازه‌ی شمارش دهی بر روی آن Collection داده نمیشود  NotCountable
 اجازه‌ی بسط دادن آن Collection داده نمیشود  NotExpandable

و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.

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

public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }

        [NotFilterable, NotSortable]
        public decimal Price { get; set; }

        [ForeignKey(nameof(SupplierId))]
        [NotNavigable]
        public virtual Supplier Supplier { get; set; }
        public int SupplierId { get; set; }


        [ForeignKey(nameof(CategoryId))]
        public virtual Category Category { get; set; }
        public int CategoryId { get; set; }

        [NotExpandable]
        public virtual ICollection<TestEntity> TestEnities { get; set; }
    }

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

public DateTimeOffset CreatedOn { get; set; }

حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:

/odata/Products?$filter=year(CreatedOn) eq 2016

در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شده‌اند:

/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)

کوئری فوق تاریخ مورد نظر را Cast کرده و همه‌ی Product هایی را که قبل از این تاریخ ثبت شده‌اند، باز می‌گرداند.

Nested Filter In Expand

/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)

همه‌ی Category‌ها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد

و یا حتی بر روی موجودیت بسط داده شده، select زده شود:

/odata/Categories?$expand=Products($select=Id,Name)


Custom Attribute

ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:

public class MyEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
       // Don't apply Skip and Top.
       var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top;
       return queryOptions.ApplyTo(queryable, ignoreQueryOptions);
    }
}

روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.

Dependency Injection در آخرین نسخه‌ی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخه‌های قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخه‌ی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخش‌های OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامه‌ی خود اضافه کنید؛ برای مثال:

 public class CaseInsensitiveResolver : ODataUriResolver
    {
        private bool _enableCaseInsensitive;

        public override bool EnableCaseInsensitive
        {
            get { return true; }
            set { _enableCaseInsensitive = value; }
        }
    }

اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.

حال به صورت زیر آن را می‌توان به وابستگی‌های config برنامه، اضافه نمود:

config.MapODataServiceRoute("ODataRoute", "odata",
                builder =>
                {
                    builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
                    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
                    builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive
                });

در قسمت بعدی به Action‌ها و Function‌ها در OData میپردازیم.

نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
سلام ، با دیباگ کردن فهمیدم بعد از لاگین در قسمت زیر Claim را null برمیگردونه
        public static string GetClaimValue(this IPrincipal currentPrincipal, string key)
        {
            var identity = currentPrincipal.Identity as ClaimsIdentity;
            if (identity == null)
                return null;

            var claim = identity.Claims.FirstOrDefault(c => c.Type == key);
            return claim?.Value;
        }
که با برداشتن breakpoint قسمت CreateUserIdentityAsync در ApplicationSignInManager
        public override Task<ClaimsIdentity> CreateUserIdentityAsync(User user)
        {
            return _userManager.GenerateUserIdentityAsync(user);
        }
و استفاده در AccountController در اکشن login قسمت post بصورت زیر مشکل حل شد
                case SignInStatus.Success:
                    var user = await _userManager.FindByNameAsync(model.UserName);
                    await _signInManager.CreateUserIdentityAsync(user);
                    return RedirectToLocal(returnUrl);

نظرات مطالب
C# 7 - Tuple return types and deconstruction
مزیت خروجی‌های از نوع Tuple برای طراحان کتابخانه‌ها 
تبدیل خروجی‌های dynamic قبلی
// query dapper via "dynamic"
int id = ...
var row = conn.QuerySingle("select Name, LastUpdated from Records where Id=@id", new {id});
string name = row.Name; // "dynamic"
به tuple جدید
// query dapper via C# 7 tuples
int id = ...
var row = conn.QuerySingle<(string name, DateTime lastUpdated)>(
     "select Name, LastUpdated from Records where Id=@id", new {id});
// use row.name and row.lastUpdated
با مزیت نداشتن هیچ نوع سربار اضافه‌ای (کد dynamic ابتدایی جهت پردازش، DLR را وارد صحنه می‌کند؛ اما کد فوق خیر. به علاوه چون value type است، حتی سربار heap allocation را هم ندارد).
مطالب
معرفی List Patterns Matching در C# 11
در C# 11، افزونه‌ای به switch expressionها اضافه شده‌است که امکان بررسی توالی مقادیر آرایه‌ها و مجموعه‌ها را نیز می‌دهد که به آن list expressions هم می‌گویند. List Patterns امکان بررسی شکل یک لیست و یا آرایه را ممکن می‌کنند. برای مثال اگر نیاز است بررسی کنیم که آیا مجموعه‌ای با یک مقدار خاص، شروع می‌شود، پایان می‌یابد و یا حاوی آن است، List Patterns مفید واقع خواهند شد. در اینجا List Patterns، با [] مشخص می‌شوند و در بین []ها، توالی مقادیری را که قرار است با اعضای مجموعه‌ی مشخص شده، انطباق داده شوند، مشخص می‌کنیم. این افزونه به همراه ویژگی slice pattern نیز هست که امکان انطباق با صفر و یا چند المان یک مجموعه را میسر می‌کند. در این حالت از دو نقطه برای نمایش آن در بین []ها استفاده می‌شود. برای مثال الگوی زیر:
[1, 2, .., 10]
با تمام آرایه‌های زیر انطباق دارد:
int[] arr1 = { 1, 2, 10 };
int[] arr2 = { 1, 2, 5, 10 };
int[] arr3 = { 1, 2, 5, 6, 7, 8, 9, 10 };

بررسی چند مثال جهت آشنایی با مفهوم List Patterns

ابتدا مجموعه‌ی زیر را در نظر بگیرید:
int[] collection = { 1, 2, 3, 4 };

الف) روش انطباق با یک توالی مشخص
Console.WriteLine(collection is [1, 2, 3, 4]); // True
Console.WriteLine(collection is [1, 2, 4]); // False
توالی مشخص شده‌ی در الگوی اول، دقیقا با توالی عناصر آرایه انطباق دارد. اما در حالت دوم، چون توالی اعداد الگوی مشخص شده، با توالی اعداد آرایه یکی نیست، انطباقی رخ نداده‌است.

ب) امکان استفاده از discard و همچنین لیستی از عناصر
Console.WriteLine(collection is [_, 2, _, 4]); // True
Console.WriteLine(collection is [.., 3, _]); // True
- اگر نیاز به صرفنظر کردن از عناصر خاصی در یک توالی بود، می‌توان از discard و یا همان _ استفاده کرد؛ مانند الگوی اول. الگوی اول به معنای نیاز به انطباق با چهار عدد است که حتما باید دومین و چهارمین آن‌ها اعداد 2 و 4 باشند؛ اما مقدار اولین و سومین آن‌ها، مهم نیست.
- الگوی دوم به معنای تعریف یک توالی نامشخص، اما خاتمه یافته‌ای با عنصر 3 است و سپس صرفنظر کردن از آخرین عنصر آرایه.

در مثال زیر، الگوی انطباق با مجموعه‌ای که حداقل دو عضو دلخواهی را دارد، مشاهده می‌کنید:
if (new[] { 6, 7, 8 } is [_, _, ..])
{
   Console.WriteLine($"collection with at least two items");
}
و الگوی انطباق با مجموعه‌ای که اولین و آخرین عضو آن صفر هستند:
if (new[] { 0, 42, 42, 0 } is [0, .., 0])
{
   Console.WriteLine($"collection with first and last element equal to 0");
}


ج) امکان تعریف اعمال منطقی
Console.WriteLine(collection is [_, >= 2, _, _]); // True
بر اساس این الگو، هر مجموعه‌ی چهارتایی که عنصر دوم آن، بزرگتر و یا مساوی 2 باشد، معتبر شناخته می‌شود؛ صرفنظر از مقدار سایر عناصر آن.

در مثال زیر، الگوی انطباق با مجموعه‌ای را که اولین عضو آن یک عدد مثبت است، مشاهده می‌کنید:
if (new[] { 9, -1, -2 } is [> 0, ..])
{
   Console.WriteLine($"collection with positive first element");
}
و یا الگوی انطباق با مجموعه‌ای که دومین عضو آن، یکی از دو عدد 42 و منهای 42 می‌تواند باشد:
if (new[] { 1, 42, 0 } is [_, 42 or -42, ..])
{
   Console.WriteLine($"collection with second element equal to 42 or -42");
}


یک مثال دیگر: بررسی نحوه‌ی عملکرد List Patterns

namespace CS11Tests;

public static class ListPatternsMatching
{
    public static void Test()
    {
        Console.WriteLine(CheckSwitch(new[] { 1, 2, 10 }));          // prints 1
        Console.WriteLine(CheckSwitch(new[] { 1, 2, 7, 3, 3, 10 })); // prints 1
        Console.WriteLine(CheckSwitch(new[] { 1, 2 }));              // prints 2
        Console.WriteLine(CheckSwitch(new[] { 1, 3 }));              // prints 3
        Console.WriteLine(CheckSwitch(new[] { 1, 3, 5 }));           // prints 4
        Console.WriteLine(CheckSwitch(new[] { 2, 5, 6, 7 }));        // prints 50
    }

    public static int CheckSwitch(int[] values)
        => values switch
        {
            [1, 2, .., 10] => 1,
            [1, 2] => 2,
            [1, _] => 3,
            [1, ..] => 4,
            [..] => 50
        };
}
توضیحات:

- اولین الگوی تعریف شده‌ی در متد CheckSwitch، به معنای انطباق با هر توالی است که با 1 و 2 شروع می‌شود و سپس می‌تواند شامل هر نوع توالی دلخواهی باشد (صرفنظر از مقدار و یا ترتیب این مقادیر) و در نهایت با عدد 10 خاتمه پیدا می‌کند.
- دومین الگوی تعریف شده، تنها یک آرایه‌ی دو عضوی با مقادیر مشخص 1 و 2 را می‌پذیرد.
- توالی قابل انطباق با سومین الگوی تعریف شده، از دو عضو تشکیل می‌شود. اولین عضو آن حتما باید 1 باشد و مقدار دومین عضو آن مهم نیست.
- توالی قابل انطباق با چهارمین الگوی تعریف شده، از یک یا چند عضو دلخواه تشکیل می‌شود که اولین عضو آن حتما باید عدد 1 باشد.
- هر توالی تعریف شده‌ای با پنجمین الگوی تعریف شده، انطباق پیدا می‌کند.


امکان ترکیب list pattern matching و object pattern matching

در مثال‌های زیر، نمونه‌ای از ترکیب list pattern matching و object pattern matching را جهت ساخت شرط‌های پیچیده‌ای، مشاهده می‌کنید:
if (new[] { 1, 2, 3 } is [var first, _, _])
{
   Console.WriteLine($"three item collection with first item {first}");
}

if (new[] { 4, 5, 6 } is [_, var second, _])
{
   Console.WriteLine($"three item collection with second item {second}");
}
این الگو که var pattern هم نامیده می‌شود، به همراه ذکر var و نام یک متغیر است. در این حالت کار الگو، دریافت مقدار واقع شده‌ی در آن موقعیت خاص است.
نمونه مثالی از این قابلیت جهت جدا سازی اجزای یک URL:
var uri = new Uri("http://www.mysite.com/categories/category-a/sub-categories/sub-category-a.html");
var result = uri.Segments switch
{
    ["/"] => "Root",
    [_, var single] => single,
    [_, .. string[] entries, _] => string.Join(" > ", entries)
};


سایر نوع‌هایی که توسط List patterns قابل بررسی هستند

List patterns تنها با آرایه‌ها و لیست‌ها کار نمی‌کنند. بلکه می‌توان از آن‌ها با هر نوعی که به همراه تعریف indexer‌ها و یا خواص Length و Count است نیز استفاده کرد. اگر نیاز به استفاده از Slice patterns بود، این الگو با نوع‌هایی کار می‌کند که دارای indexer هایی با آرگومان‌هایی از نوع Range است و یا به همراه متد Slice دارای دو آرگومان Int است. برای مثال رشته‌ها نیز در اینجا قابل بررسی هستند.