اشتراک‌ها
dnSpy : ابزاری برای Debug و تغییر در DLL های دات نت

dnSpy is a debugger and .NET assembly editor. You can use it to edit and debug assemblies even if you don't have any source code available.

Some Features 

  • Debug .NET Framework, .NET Core and Unity game assemblies, no source code required
  • Set breakpoints and step into any assembly
  • Locals, watch, autos windows 
  • All metadata can be edited
  • Edit methods and classes in C# or Visual Basic with IntelliSense, no source code required
  • Add new methods, classes or members in C# or Visual Basic
  • IL editor for low level IL method body editing 

dnSpy : ابزاری برای Debug و تغییر در DLL های دات نت
اشتراک‌ها
Rider 2019.3 منتشر شد
  • Outstanding support for T4 templates is here with capabilities such as template execution, debugging, code highlighting, navigation, refactorings, code completion, and much more. 
Rider 2019.3 منتشر شد
اشتراک‌ها
ویدیوهای NET Conf 2022.
NET Conf is our annual virtual developer event co-organized by the .NET community and Microsoft and sponsored by the .NET Foundation and ecosystem partners. It is a chance to learn and get inspired for your next software project. You will learn to build for web, mobile, cloud, desktop, games, services, libraries and more for a variety of platforms and devices all with .NET. We have sessions for everyone, no matter if you are just beginning or are a seasoned engineer. We’ll have presentations on .NET 7, C# 11, Azure, Visual Studio, and much more. 
ویدیوهای NET Conf 2022.
اشتراک‌ها
عرضه‌ی اولین نسخه RC برای SQL Server 2017
  • Linux support for tier-1, mission-critical workloads  SQL Server 2017 support for Linux includes the same high availability solutions on Linux as Windows Server, including Always On availability groups integrated with Linux native clustering solutions like Pacemaker.
  • Graph data processing in SQL Server  With the graph data features available in SQL Server 2017and Azure SQL Database, customers can create nodes and edges, and discover complex and many-to-many relationships.
  • Adaptive query processing  Adaptive query processing is a family of features in SQL Server 2017 that automatically keeps database queries running as efficiently as possible without requiring additional tuning from database administrators. In addition to the capability to adjust batch mode memory grants, the feature set includes batch mode adaptive joins and interleaved execution capabilities.
  • Python integration for advanced analytics  Microsoft Machine Learning Services now brings you the ability to run in-database analytics using Python or R in a parallelized and scalable way. The ability to run advanced analytics in your operational store without ETL means faster time to insights for customers while easy deployment and rich extensibility make it fast to get up and running on the right model. 
عرضه‌ی اولین نسخه RC برای SQL Server 2017
اشتراک‌ها
NET 5.0 Preview 5. منتشر شد

we’re about half-way through the release now. Most of the features are now included, but there are still many changes that we expect in the next few previews to complete experiences and round off rough edges that still exist. Take care. 

NET 5.0 Preview 5. منتشر شد
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 5 - آشنایی با کلاس ServiceDescriptor
در بخش پنجم از سری نوشتار «تزریق وابستگی‌ها در ASP.NET Core»، می‌خواهیم به شرح کلاس ServiceDescriptor بپردازیم. اگر تعریف اینترفیس IServiceCollection را مشاهده کنیم، می‌بینیم که IServicecollection در واقع لیستی از اشیائی از نوع ServiceDescriptor را نگهداری می‌کند:
namespace Microsoft.Extensions.DependencyInjection
{
    public interface IServiceCollection : 
       ICollection<ServiceDescriptor>, IEnumerable<ServiceDescriptor>,
       IEnumerable, IList<ServiceDescriptor>
    {
    }
}

ServiceProvider و مؤلفه‌های درونی آن، از یک مجموعه از ServiceDescriptor‌ها برای برنامه‌ی شما بر اساس سرویس‌های ثبت شده‌ی توسط IServiceCollection استفاده می‌کنند. ServiceDescriptor حاوی اطلاعاتی در مورد سرویس‌های ثبت شده‌است. اگر به کد منبع این کلاس برویم، می‌بینیم پنج Property اصلی دارد که با استفاده از آن‌ها اطلاعات یک سرویس ثبت و نگهداری می‌شوند. با استفاده از این  اطلاعات در هنگام اجرا ، DI Container به واکشی و ساخت نمونه‌هایی از سرویس درخواستی اقدام می‌کند:

public Type ImplementationType { get; }
public object ImplementationInstance { get; }
public Func<IServiceProvider, object> ImplementationFactory { get; }
public ServiceLifetime Lifetime { get; }
public Type ServiceType { get; }

هر کدام از این Property ‌ها کاربرد خاص خود را دارند:

  • · ServiceType : نوع سرویسی را که می‌خواهیم ثبت شود، مشخص می‌کنیم ( مثلا اینترفیس IMessageService ) .  
  • · ImplementionType : نوع پیاده سازی سرویس مورد نظرمان را مشخص می‌کند ( مثلا کلاس MessageService ).  
  • · LifeTime : طول حیات سرویس را مشخص می‌کند. DI Container بر اساس این ویژگی، اقدام به ساخت و از بین بردن نمونه‌هایی از سرویس می‌کند.  
  • · ImplementionInstance : نمونه‌ی ساخته شده‌ی از سرویس است.  
  • · ImplementionFactory : یک Delegate است که چگونگی ساخته شدن یک نمونه از پیاده سازی سرویس را در خود نگه می‌دارد. این Delegate یک IServiceProvider را به عنوان ورودی دریافت می‌کند و یک object را بازگشت می‌دهد.

به صورت عادی، در سناریوهای معمول ثبت سرویس‌ها درون IServiceCollection، نیازی به استفاده از ServiceDescriptor نیست؛ ولی اگر بخواهیم سرویس‌ها را به روش‌های پیشرفته‌تری ثبت کنیم، مجبوریم که به صورت مستقیم با این کلاس کار کنیم.

 

می توانیم یک ServiceDesciriptor را به روش‌های زیر تعریف کنیم:

var serviceDescriptor1 = new ServiceDescriptor(
   typeof(IMessageServiceB),
   typeof(MessageServiceBB),
   ServiceLifetime.Scoped);

var serviceDescriptor2 = ServiceDescriptor.Describe(
   typeof(IMessageServiceB),
   typeof(MessageServiceBB),
   ServiceLifetime.Scoped);

var serviceDescriptor3 = ServiceDescriptor.Singleton(typeof(IMessageServiceB), typeof(MessageServiceBB));

var serviceDescriptor4 = ServiceDescriptor.Singleton<IMessageServiceB, MessageServiceBB>();
در بالا روش‌های تعریف یک ServiceDescriptor را می‌بینید. اولین متد و تعریف پارامترها در سازنده‌ها، روش پایه است؛ ولی برای راحتی کار، توسعه دهندگان تعدادی متد static نیز تعریف کرده‌اند که خروجی آنها یک نمونه از ServiceDescriptor است.

همانطور که دیدیم، IServiceCollection در واقع لیست و مجموعه‌ای از اشیاء است که از نمونه‌های جنریک IServiceCollection ، IList ، IEnumerable و Ienumberabl ارث بری می‌کند؛ بنابراین می‌توان از متدهای تعریف شده‌ی در این اینترفیس‌ها برای IServiceCollection نیز استفاده کرد. حالا ما برای اضافه کردن این سرویس‌های جدید، بدین طریق عمل می‌کنیم:

Services.Add(serviceDescriptor1);

استفاده از متدهای TryAdd() 

به کد زیر نگاه کنید :

services.AddScoped<IMessageServiceB, MessageServiceBA>();
services.AddScoped<IMessageServiceB, MessageServiceBB>();
همانطور که می‌بینید، در اینجا یک اینترفیس را دوبار ثبت کردیم. در این حالت موقع واکشی سرویس، DI Container آخرین نمونه‌ی ثبت شده‌ی برای اینترفیس را واکشی کرده و نمونه سازی می‌کند و به کلاس‌ها تزریق می‌کند. این یکی از مواردی است که ترتیب ثبت کردن سرویس‌های مهم است.

برای جلوگیری از این خطا می‌توانیم از متدهای TryAddSingleton() ، TryAddScoped() و TryAddTransient() استفاده کنیم. این متدها درون فضای نام Microsoft.Extionsion.DependencyInjection.Extension قرار دارند.

عملکرد کلی این متدها درست مثل متد‌های Add() است؛ با این تفاوت که این متد ابتدا IServiceCollection را جستجو می‌کند و اگر برای type مورد نظر سرویسی ثبت نشده بود، آن را ثبت می‌کند:

services.TryAddScoped<IMessageServiceB, MessageServiceBA>();
services.TryAddScoped<IMessageServiceB, MessageServiceBB>();

جایگذاری یک سرویس با نمونه‌ای دیگر

گاهی اوقات می‌خواهیم یک پیاده سازی دیگر را بجای پیاده سازی فعلی، در DI Container ثبت کنیم. در این حالت از متد Replace() بر روی IServiceCollection برای این کار استفاده می‌کنیم. این متد فقط یک ServiceDescriptor را به عنوان پارامتر ورودی می‌گیرد:

services.Replace(serviceDescriptor3);
ناگفته نماند که متد Replace() فقط اولین سرویس را با نمونه‌ی مورد نظر ما جایگذاری می‌کند. اگر می‌خواهید تمام نمونه سرویس‌های ثبت شده را برای یک نوع حذف کنید، می‌توانید از متد RemoveAll() استفاده کنید:
services.RemoveAll<IMessageServiceB>();

معمولا در پروژه‌های معمول خودمان نیازی به استفاده از Replace() و RemoveAll() نداریم؛ مگر اینکه بخواهیم پیاده سازی اختصاصی خودمان را برای سرویس‌های درونی فریم ورک یا کتابخانه‌های شخص ثالث، بجای پیاده سازی پیش فرض، ثبت و استفاده کنیم.  

 

AddEnumerable()

فرض کنید دارید برنامه‌ی نوبت دهی یک کلینیک را می‌نویسید و به صورت پیش فرض از شما خواسته‌اند که هنگام صدور نوبت، این قوانین را بررسی کنید:

  •   هر شخص در هفته نتواند بیش از 2 نوبت برای یک تخصص بگیرد.
  •   اگر شخص در ماه بیش از 3 نوبت رزرو شده داشته باشد ولی مراجعه نکرده باشد، تا پایان ماه، امکان رزرو نوبت را نداشته باشد .
  •   تعداد نوبت‌های ثبت شده‌ی برای پزشک در آن روز نباید بیش از تعدادی باشد که پزشک پذیرش می‌کند.
  •   و ...

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

  •   نوبت‌های ثبت شده برای یک شخص نباید دارای تداخل باشند.

در این حالت ما باید دوباره سرویس Register را باز کنیم و به متد بررسی کردن قوانین برویم و دوباره کدهایی را برای بررسی کردن قانون جدید بنویسیم و احتمالا کد ما به این صورت خواهد شد:

public class RegisterAppointmentService : RegisterAppointmentService
{
  public Task<Result> RegisterAsync(
    PatientInfoDTO patientIfno , DateTimeOffset requestedDateTime ,
    PhysicianId phusicianId )
  {
      CheckRegisterantionRule(patientInfo);
      // code here
  }

  private Task CheckRegisterationRule(PatientInfoDTO patientInfo)
  {
       CheckRule1(patientInfo);
       CheckRule2(patientInfo);
       CheckRule3(patientInfo);
  }
}  

در این حالت باید به ازای هر قانون جدید، به متد CheckRegisterationRule برویم و به ازای هر قانون، یک متد private جدید را بسازیم. مشکل این روش این است که در این حالت ما مجبوریم با هر کم و زیاد شدن قانون، این کلاس را باز کنیم و آن را تغییر دهیم و با هر تغییر دوباره، تست‌های واحد آن را دوباره نویسی کنیم. در یک کلام در کد بالا اصول Separation of Concern و  Open/Closed Principle را رعایت نمی‌شود.

یک راهکار این است که یک سرویس جداگانه را برای بررسی کردن قوانین بنویسیم و آن را به سرویس ثبت نوبت تزریق کنیم:

public class ICheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService
{
     public Task CheckRegisterantionRule(PatientInfoDTO patientInfo)
     {
                CheckRule1(patientInfo);
                CheckRule2(patientInfo);
                CheckRule3(patientInfo);
      }
}

public class RegisterAppointmentService : IRegisterAppointmentService
{
  private ICheckRegisterationRuleForAppointmentService  _ruleChecker;
 
  public RegisterAppointmentService (RegisterAppointmentService  ruleChecker)
  {
          _ruleChecker = ruleChecker;  
  }

  public Task<Result> RegisterAsync(
     PatientInfoDTO patientIfno , 
     DateTimeOffset requestedDateTime , 
     PhysicianId phusicianId )
  {
             _ruleChecker.CheckRegisterantionRule(patientInfo);
                // code here
  }
}

با این کار وظیفه‌ی چک کردن قوانین و وظیفه‌ی ثبت و ذخیره سازی قوانین را از یکدیگر جدا کردیم؛ ولی همچنان در سرویس بررسی کردن قوانین، اصل Open/Closed رعایت نشده‌است. خب راه حل چیست !؟

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


interface IAppointmentRegisterationRule
{
  Task CheckRule(PatientInfo patientIfno);
}

public class AppointmentRegisterationRule1 : IAppointmentRegisterationRule
{
      public Task CheckRule(PatientInfo patientIfno)
      {
          Console.WriteLine("Rule 1 is checked");
          return Task.CompletedTask;
      }
}

public class AppointmentRegisterationRule2 : IAppointmentRegisterationRule
{
     public Task CheckRule(PatientInfo patientIfno)
     {
Console.WriteLine("Rule 2 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule3 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 3 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule4 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 4 is checked"); return Task.CompletedTask; } }
حالا که ما قوانین خودمان را تعریف کردیم، به روش زیر می‌توانیم آن‌ها را درون سازنده ثبت کنیم:
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule1>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule2>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule3>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule4>();
حالا می‌توانیم درون سازنده‌ی سرویس مورد نظرمان، لیستی از سرویس‌های ثبت شده‌ی برای یک نوع خاص را به با استفاده از اینترفیس جنریک IEnumerable<T> دریافت کنیم که در اینجا T، برابر نوع سرویس مورد نظرمان است:
public class CheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService
{
       private IEnumerable<IAppointmentRegisterationRule> _rules ;

       public CheckRegisterationRuleForAppointmentService(IEnumerable<IAppointmentRegisterationRule> rules)
       {
           _rules = rules;
       }

      public Task CheckRegisterantionRule(PatientInfoDTO patientInfo)
      {
          foreach(var rule in rules)
          {
                rule.CheckRule(patientInfo);
          }
      }
}
با این تغییرات، هر زمانیکه خواستیم می‌توانیم با استفاده از DI Container، قوانین جدیدی را اضافه یا کم کنیم و با این کار، اصل Open/Closed را نیز رعایت کرده‌ایم.

 کد بالا به نظر کامل می‌آید ولی مشکلی دارد! اگر در DI Container برای IAppointmentRegisterationRule یک قانون را دو یا چند بار ثبت کنیم، در هر بار بررسی کردن قوانین، آن را به همان تعداد بررسی می‌کند و اگر این فرآیند منابع زیادی را به کار می‌گیرد، می‌تواند عملکرد برنامه‌ی ما را به هم بریزد.  برای جلوگیری از این مشکل، از متد TryAddEnumerabl() استفاده می‌کنیم که لیستی از ServiceDescriptor ‌ها را می‌گیرد و هر serviceDescriptor را فقط یکبار ثبت می‌کند:

services.TryAddEnumerable(new[] {
  ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule1)),
  ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule2)),
  ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule3)),
  ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule4)),
});

مطالب
الگوی طراحی 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  که این مشکل را در مقاله‌ی بعدی برطرف خواهیم کرد.
مطالب
طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها

در این مطلب قصد داریم علاوه بر طراحی زیرساختی برای راه اندازی هرچه سریعتر ServiceLayer، طراحی ای برای مکانیزم Validation به عنوان یک Cross Cutting Concern، نیز ارائه داده و آن را پیاده سازی کنیم.

پیش نیازها:

 ServiceLayer در معماری لایه‌ای، در برگیرنده ApplicationService هایی می‌باشد که به عنوان مدخل ورودی (Entry Point) برنامه، در معرض دید لایه Presentation قرار گرفته و داده را به فرمت مورد نیاز Presentation در اختیارش قرار خواهند داد.
 این سرویس‌ها DTO‌ها را به عنوان پارامتر دریافت کرده و DTO هایی را به عنوان خروجی برگشت خواهند داد. مباحثی مانند Logging، Caching، Business Validation Authorization و مدیریت تراکنش‌ها را می‌توان در این لایه در نظر گرفت.

در ادامه اگر واژه «سرویس» به کار گرفته می‌شود منظور ما ApplicationServiceها می‌باشند.

کار را با ارائه یکسری واسط و کلاس پایه برای عملیات CRUD در سرویس‌ها به صورت زیر پیش می‌بریم.

قرار است به صورت قراردادی، تمام سرویس‌های ما واسط زیر را پیاده سازی کرده باشند. این مورد در مباحث تعریف Policy‌های مربوط به StructureMap مفید خواهد بود.

namespace MvcFramework.Framework.Application.Services
{
    public interface IApplicationService : ITransientDependency
    {
    }
}

دو واسط دیگر برای اعمال طول عمر اشیاء به صورت قراردادی در StructureMap به شکل زیر در نظر گرفته شده‌اند.

namespace MvcFramework.Framework.Dependency
{
    public interface ISingletonDependency
    {
    }
}
namespace MvcFramework.Framework.Dependency
{
    public interface ITransientDependency
    {
    }
}

و با پیاده سازی یک LifeCyclePolicy از دو واسط بالا به شکل زیر استفاده خواهیم کرد.

namespace MvcFramework.Framework.Dependency
{
    public class LifeCyclePolicy : IInstancePolicy
    {
        public void Apply(Type pluginType, Instance instance)
        {
            if (typeof(ISingletonDependency).IsAssignableFrom(instance.ReturnedType))
                instance.SetLifecycleTo<SingletonLifecycle>();
            else if (typeof(ITransientDependency).IsAssignableFrom(instance.ReturnedType))
                instance.SetLifecycleTo<TransientLifecycle>();
        }
    }
}

به این صورت تنظیم طول عمر اشیاء ساخته شده توسط StructureMap این بار به صورت قرادادی بوده و لازم به ذکر تک تک این موارد در تنظیمات اولیه مربوط به Container آن نیست.

کلاس پایه‌ای را که پیاده ساز واسط IApplicationService می‌باشد، برای مقابله با عدم نگارش پذیری واسط‌ها، به شکل زیر در نظر میگیریم. 

namespace MvcFramework.Framework.Application.Services
{
    public abstract class ApplicationService : IApplicationService
    {
    }
}

بسته به نیاز پروژه خودتان می‌توانید اعضای مشترک بین سرویس‌ها را در دل این کلاس قرار دهید.

در ادامه واسط ICrudApplicationSevie را به شکل زیر طراحی خواهیم کرد.

namespace MvcFramework.Framework.Application.Services
{
    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, DynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TDynamicListRequest> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TDynamicListRequest : DynamicListRequest
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest,
        TPagedListResponse> :
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse,
            DynamicListRequest>
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>
    {
    }

    public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest,
        TPagedListResponse,
        in TDynamicListRequest> : IApplicationService
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>
        where TDynamicListRequest : DynamicListRequest
    {
        void Create(TCreateModel model);
        void Create(IList<TCreateModel> models);
        Task CreateAsync(TCreateModel model);
        Task CreateAsync(IList<TCreateModel> models);

        IList<TModel> GetList();
        DynamicListResponse GetDynamicList(TDynamicListRequest request);
        TPagedListResponse GetPagedList(TPagedListRequest request);
        IList<LookupItem> GetLookup();
        TModel GetById(long id);
        TEditModel GetForEdit(long id);
        bool Exists(long id);
        Task<IList<TModel>> GetListAsync();
        Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request);
        Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request);
        Task<IList<LookupItem>> GetLookupAsync();
        Task<TModel> GetByIdAsync(long id);
        Task<TEditModel> GetForEditAsync(long id);
        Task<bool> ExistsAsync(long id);

        void Edit(TEditModel model);
        void Edit(IList<TEditModel> models);
        Task EditAsync(TEditModel model);
        Task EditAsync(IList<TEditModel> models);
        
        void Delete(TDeleteModel model);
        void Delete(IList<TDeleteModel> models);
        Task DeleteAsync(TDeleteModel model);
        Task DeleteAsync(IList<TDeleteModel> models);
    }
}

سرویسی که نیاز دارد از عملیات CRUD نیز پشتیبانی داشته باشد، بهتر است واسط آن از یک چنین واسطی که در بالا معرفی شد، ارث بری کند. 

مدل‌ها و واسط‌های پیش فرضی را که در واسط بالا از آنها استفاده شده است، در زیر مشاهده می‌کنید:

 واسط IModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IModel
    {
        long Id { get; set; }
    }
}

واسط IEditModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IEditModel : IModel
    {
        byte[] RowVersion { get; set; }
    }
}

واسط IDeleteModel

namespace MvcFramework.Framework.Application.Models
{
    public interface IDeleteModel : IModel
    {
        byte[] RowVersion { get; set; }
    }
}

کلاس LookupItem

namespace MvcFramework.Framework.Application.Models
{
    public class LookupItem
    {
        public string Value { get; set; }
        public string Text { get; set; }
        public bool Selected { get; set; }
    }
}

کلاس PagedListRequest

namespace MvcFramework.Framework.Application.Models
{
    public class PagedListRequest : IShouldNormalize
    {
        public long TotalCount { get; set; }
        public int PageNumber { get; set; }
        public int PageSize { get; set; }

        /// <summary>
        ///     Sorting information.
        ///     Should include sorting field and optionally a direction (ASC or DESC)
        ///     Can contain more than one field separated by comma (,).
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name"
        ///     "Name DESC"
        ///     "Name ASC, Age DESC"
        /// </example>
        public string SortBy { get; set; }

        public void Normalize()
        {
            if (PageNumber < 1)
                PageNumber = 1;

            if (PageSize < 0)
                PageSize = 10;

            if (SortBy.IsEmpty())
                SortBy = "Id DESC";
        }
    }
}

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


کلاس PagedListResponse

namespace MvcFramework.Framework.Application.Models
{
    public class PagedListResponse<TModel, TPagedListRequest>
        where TPagedListRequest : PagedListRequest, new()
        where TModel : IModel
    {
        public PagedListResponse()
        {
            Result = new List<TModel>();
            Request = new TPagedListRequest();
        }
        public IList<TModel> Result { get; set; }
        public TPagedListRequest Request { get; set; }
    }
}

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


کلاس‌های DynamicListRequest و DynamicListResponse برگرفته از کتابخانه Kendo.DynamicLinq می باشند.


کلاس Entity

namespace MvcFramework.Framework.Domain.Entities
{
    public abstract class Entity
    {
        #region Properties

        public long Id { get; set; }
        public byte[] RowVersion { get; set; }
        public EntityChangeState State { get; set; }

        #endregion

        #region Public Methods

        [SuppressMessage("ReSharper", "BaseObjectGetHashCodeCallInGetHashCode")]
        [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
        public override int GetHashCode()
        {
            if (IsTransient())
                return base.GetHashCode();

            unchecked
            {
                var hash = this.GetRealType().GetHashCode();
                return (hash * 31) ^ Id.GetHashCode();
            }
        }

        public virtual bool IsTransient()
        {
            return Id == 0;
        }

        public override bool Equals(object obj)
        {
            var other = obj as Entity;
            if (ReferenceEquals(other, null)) return false;

            if (ReferenceEquals(this, other)) return true;

            var typeOfThis = this.GetRealType();
            var typeOfOther = other.GetRealType();

            if (typeOfThis != typeOfOther) return false;

            if (IsTransient() || other.IsTransient()) return false;

            return Id.Equals(other.Id);
        }

        public override string ToString()
        {
            return $"[{this.GetRealType().Name} : {Id}]";
        }

        #endregion

        #region Operators

        public static bool operator ==(Entity left, Entity right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(Entity left, Entity right)
        {
            return !(left == right);
        }

        #endregion
    }
}

در این کلاس یکسری خصوصیات پایه ای مانند Id و متدهای مشترک بین Entityها قرار گرفته شده است. این کلاس پایه تمام Entity‌های سیستم می‌باشد.

پیاده سازی پیش فرض از واسط ICrudApplicationService به شکل زیر می‌باشد.

namespace MvcFramework.Framework.Application.Services
{
    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, DynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TDynamicListRequest> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest,
            PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TDynamicListRequest : DynamicListRequest
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedListRequest,
        TPagedListResponse> :
        CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest,
            TPagedListResponse,
            DynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new()
    {
        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }
    }

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedListRequest,
        TPagedListResponse, TDynamicListRequest> : ApplicationService,
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse,
            TDynamicListRequest>
        where TEntity : Entity
        where TCreateModel : class
        where TEditModel : class, IEditModel
        where TModel : class, IModel
        where TDeleteModel : class, IDeleteModel
        where TPagedListRequest : PagedListRequest, new()
        where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new()
        where TDynamicListRequest : DynamicListRequest

    {
        #region Constructor

        protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper)
        {
            Guard.ArgumentNotNull(unitOfWork, nameof(unitOfWork));
            Guard.ArgumentNotNull(mapper, nameof(mapper));

            UnitOfWork = unitOfWork;
            Mapper = mapper;
            EntitySet = UnitOfWork.Set<TEntity>();
        }

        #endregion

        #region Properties

        protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking();
        protected IUnitOfWork UnitOfWork { get; }
        protected IMapper Mapper { get; }
        protected IDbSet<TEntity> EntitySet { get; }

        #endregion

        #region ICrudApplicationService Members

        #region Methods

        [Transactional]
        public virtual void Create(TCreateModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            EntitySet.Add(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Create(IList<TCreateModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.AddRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task CreateAsync(TCreateModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            EntitySet.Add(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task CreateAsync(IList<TCreateModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.AddRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }


        [Transactional]
        public virtual void Edit(TEditModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsChanged(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Edit(IList<TEditModel> models)
        {
            Guard.ArgumentNotNull(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.UpdateRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task EditAsync(TEditModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsChanged(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task EditAsync(IList<TEditModel> models)
        {
            Guard.ArgumentNotNull(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.UpdateRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }


        public virtual IList<TModel> GetList()
        {
            return EntitySet.ProjectToList<TModel>(Mapper.ConfigurationProvider);
        }

        public virtual DynamicListResponse GetDynamicList(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            return query.ProjectTo<TModel>().ToListResponse(request);
        }

        public virtual TPagedListResponse GetPagedList(TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            request.TotalCount = query.LongCount();

            query = ApplySorting(query, request);
            query = ApplyPaging(query, request);

            var result = query.ProjectToList<TModel>(Mapper.ConfigurationProvider);

            return new TPagedListResponse
            {
                Result = result,
                Request = request
            };
        }

        public virtual IList<LookupItem> GetLookup()
        {
            return EntitySet.ProjectToList<LookupItem>(Mapper.ConfigurationProvider);
        }

        public virtual TModel GetById(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity =
                EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetById");

            return entity;
        }

        public virtual TEditModel GetForEdit(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity =
                EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TEditModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEdit");

            return entity;
        }

        public virtual bool Exists(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            return EntitySet.Any(a => a.Id == id);
        }

        public virtual async Task<IList<TModel>> GetListAsync()
        {
            return await EntitySet.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider);
        }

        public virtual Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            return query.ProjectTo<TModel>().ToListResponseAsync(request);
        }

        public virtual async Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            var query = ApplyFiltering(request);

            request.TotalCount = await query.LongCountAsync().ConfigureAwait(false);

            query = ApplySorting(query, request);
            query = ApplyPaging(query, request);

            var result = await query.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider).ConfigureAwait(false);

            return new TPagedListResponse
            {
                Result = result,
                Request = request
            };
        }

        public virtual async Task<IList<LookupItem>> GetLookupAsync()
        {
            return await EntitySet.ProjectToListAsync<LookupItem>(Mapper.ConfigurationProvider);
        }

        public virtual async Task<TModel> GetByIdAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity = await UnTrackedEntitySet.Where(a => a.Id == id)
                .ProjectToFirstOrDefaultAsync<TModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetByIdAsync");

            return entity;
        }

        public virtual async Task<TEditModel> GetForEditAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            var entity = await UnTrackedEntitySet.Where(a => a.Id == id)
                .ProjectToFirstOrDefaultAsync<TEditModel>(Mapper.ConfigurationProvider);

            if (entity == null)
                throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEditAsync");

            return entity;
        }

        public virtual Task<bool> ExistsAsync(long id)
        {
            Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id));

            return EntitySet.AnyAsync(a => a.Id == id);
        }


        [Transactional]
        public virtual void Delete(TDeleteModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsDeleted(entity);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual void Delete(IList<TDeleteModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.RemoveRange(entities);
            UnitOfWork.SaveChanges();
        }

        [Transactional]
        public virtual Task DeleteAsync(TDeleteModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var entity = Mapper.Map<TEntity>(model);

            UnitOfWork.MarkAsDeleted(entity);
            return UnitOfWork.SaveChangesAsync();
        }

        [Transactional]
        public virtual Task DeleteAsync(IList<TDeleteModel> models)
        {
            Guard.ArgumentNotEmpty(models, nameof(models));
            Guard.ArgumentNotEmpty(models, nameof(models));

            var entities = Mapper.Map<IList<TEntity>>(models);

            UnitOfWork.RemoveRange(entities);
            return UnitOfWork.SaveChangesAsync();
        }

        #endregion

        #endregion

        #region Protected Methods

        /// <summary>
        ///     Apply Filtering To GetDynamicList
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TDynamicListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            return UnTrackedEntitySet;
        }

        /// <summary>
        ///     Apply Filtering To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));

            return UnTrackedEntitySet;
        }

        /// <summary>
        ///     Apply Sorting To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="query">query</param>
        /// <param name="request">PagedListRequest</param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return !request.SortBy.IsEmpty() ? query.OrderBy(request.SortBy) : query.OrderByDescending(e => e.Id);
        }

        /// <summary>
        ///     Apply Paging To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="request">PagedListRequest</param>
        /// <param name="query">query</param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TPagedListRequest request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return request != null
                ? query.Page((request.PageNumber - 1) * request.PageSize, request.PageSize)
                : query;
        }

        #endregion
    }
}

همه متد‌های این کلاس پایه، قابلیت override شدن را دارند. به عنوان مثال یکسری متد با دسترسی protected مثلا ApplyFiltering هم برای بازنویسی نحوه فیلتر کردن خروجی GetPagedList می‌توانند در SubClassها مورد استفاده قرار گیرند. برای مباحث مرتب سازی هم از کتابخانه System.Linq.Dynamic استفاده شده است. 

برای مکانیزم Validation خودکار هم از کتابخانه FluentValidatoin کمک گرفته شده است و با استفاده از Interceptor زیر در صورت یافتن Validator مربوط به Model ورودی، عملیات اعتبارسنجی انجام میگرد و در صورت معتبر نبودن، استثنایی صادر خواهد شد که حاوی اطلاعات مربوط به جزئیات خطاها نیز می‌باشد.

ValidatorInterceptor

namespace MvcFramework.Framework.Aspects.Validation
{
    public class ValidatorInterceptor : ISyncInterceptionBehavior
    {
        private readonly IValidatorFactory _validatorFactory;

        public ValidatorInterceptor(IValidatorFactory validatorFactory)
        {
            _validatorFactory = validatorFactory;
        }

        public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation)
        {
            var argumentValues = methodInvocation.Arguments.Select(a => a.Value).ToArray();

            var validator = new MethodInvocationValidator(_validatorFactory, methodInvocation.MethodInfo,
                argumentValues);

            validator.Validate();

            return methodInvocation.InvokeNext();
        }
    }
}

کتابخانه جانبی دیگری برای AOP توسط تیم StructureMap به نام StructureMap.DynamicInterception ارائه شده است. نمونه‌ی استفاده از آن، در بالا مشخص می‌باشد. در اینجا انتقال مسئولیت اعتبارسنجی پارامترهای متدی که قرار است Intercept شود، به کلاسی به نام MethodInvocationValidator سپرده شده‌است.

کلاس MethodInvocationValidator

namespace MvcFramework.Framework.Aspects.Validation
{
    internal class MethodInvocationValidator
    {
        #region Constructor

        public MethodInvocationValidator(IValidatorFactory validatorFactory, MethodInfo method,
            object[] parameterValues)
        {
            Guard.ArgumentNotNull(method, nameof(method));
            Guard.ArgumentNotNull(parameterValues, nameof(parameterValues));
            Guard.ArgumentNotNull(validatorFactory, nameof(validatorFactory));

            _method = method;
            _parameterValues = parameterValues;
            _validatorFactory = validatorFactory;
            _parameters = method.GetParameters();

            _parametersToBeNormalized = new List<IShouldNormalize>();
        }

        #endregion

        #region Public Methods

        public void Validate()
        {
            if (!CheckShouldBeValidate()) return;

            foreach (var parameterValue in _parameterValues)
                ValidateMethodParameter(parameterValue);

            foreach (var parameterToBeNormalized in _parametersToBeNormalized)
                parameterToBeNormalized.Normalize();
        }

        #endregion

        #region Fields

        private readonly MethodInfo _method;
        private readonly object[] _parameterValues;
        private readonly ParameterInfo[] _parameters;
        private readonly IValidatorFactory _validatorFactory;
        private readonly List<IShouldNormalize> _parametersToBeNormalized;

        #endregion

        #region Private Methods

        private bool CheckShouldBeValidate()
        {
            if (!_method.IsPublic)
                return false;

            if (IsValidationDisabled())
                return false;

            if (_parameters.IsNullOrEmpty())
                return false;

            if (_parameters.Length != _parameterValues.Length)
                throw new Exception("Method parameter count does not match with argument count!");

            return true;
        }

        private bool IsValidationDisabled()
        {
            if (_method.IsDefined(typeof(EnableValidationAttribute), true))
                return false;

            return ReflectionHelper
                       .GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<DisableValidationAttribute>(_method) != null;
        }

        private void ValidateMethodParameter(object parameterValue)
        {
            if (parameterValue == null) return;

            var parameterValueList = parameterValue as IEnumerable<object>;
            if (parameterValueList != null)
            {
                var valueList = parameterValueList.ToList();

                ValidateMethodParameterValues(valueList);
            }
            else
            {
                ValidateMethodParameterValues(new List<object> { parameterValue });
            }

            if (parameterValue is IShouldNormalize)
                _parametersToBeNormalized.Add(parameterValue as IShouldNormalize);
        }

        private void ValidateMethodParameterValues(List<object> valueList)
        {
            var ruleSet = GetRuleSet(_method);

            var validator = _validatorFactory.GetValidator(valueList.First().GetType());
            if (validator == null) return;

            foreach (var item in valueList)
                ValidateWithReflection(validator, item, ruleSet);
        }

        private static string GetRuleSet(MemberInfo method)
        {
            const string @default = "default";

            var attribute = method.GetCustomAttribute<ValidateWithRuleAttribute>();

            if (attribute == null)
                return @default;

            var rules = new List<string> { @default };

            rules.AddRange(attribute.RuleSetNames);

            return string.Join(",", rules).TrimEnd(',');
        }

        private static void ValidateAndThrow<T>(IValidator<T> validator, T argument, string ruleSet)
        {
            validator.ValidateAndThrow(argument, ruleSet);
        }

        private void ValidateWithReflection(IValidator validator, object argument, string ruleSet)
        {
            GetType().GetMethod(nameof(ValidateAndThrow), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(argument.GetType())
                .Invoke(null, new[] { validator, argument, ruleSet });
        }

        #endregion
    }
}

در متد Validate آن ابتدا چک می‌شود که آیا اعتبارسنجی می‌بایستی انجام شود یا خیر. سپس تک تک آرگومان‌های ارسالی را با استفاده از متد ValidateMethodParameter وارد مکانیزم اعتبارسنجی می‌کند. در داخل این متد ابتدا نوع آرگومان تشخیص داده شده و این مقادیر به متد ValidateMethodParameterValues ارسال شده و داخل آن ابتدا Validator مرتبط را یافته و آن را به متد ValidateWithReflection ارسال می‌کند. در این بین متد GetRuleSets وظیفه واکشی اسامی RuleSet هایی که بر روی متد مورد نظر تنظیم شده اند را دارد؛ برای مواقعی که از یک ویومدل برای ویرایش، درج و حذف استفاده کنید، در این صورت با توجه به اینکه برای یک ویومدل یک Validator خواهید داشت، امکانات RuleSet مربوط به FluentValidation کارساز خواهند بود. به این صورت که برای هر کدام از عملیات حذف، ویرایش و درج، RuleSet مناسب را تعریف کرده و با استفاده از ValidateWithRuleAttribute برروی متدهای مورد نظر، این ruleها در سیستم اعتبارسنجی ارائه شده اعمال خواهند شد.

با توجه به اینکه متد ValidateAndThrow در واسط IValidator‎<T>‎ تعریف شده‌است و از آنجاییکه ما نوع داده مدل مورد نظر را هم نداریم لازم است با استفاده از MakeGenericMethod به صورت داینامیک نوع داده T را مشخص کنیم و فراخوانی متد استاتیک ValidatorWithThrow‎<T>‎ را با Reflection انجام دهیم.

در ادامه لازم است ValidatorInterceptor معرفی شده را به StructureMap نیز معرفی کنیم. برای این منظور به شکل زیر عمل خواهیم کرد.

namespace MvcFramework.Framework
{
    public class FrameworkRegistry : Registry
    {
        public FrameworkRegistry()
        {
            For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>();

            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.LookForRegistries();
            });

            Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor),typeof(TransactionInterceptor)));
        }
    }
}

در کد بالا با استفاده از DynamicProxyInterceptorPolicy، یک Policy را برای Intercept کردن متدهای مربوط به کلاس هایی که پیاده ساز IApplicationService می‌باشند، معرفی کرده‌ایم.

کار اعتبارسنجی هم به پایان رسید؛ در زیر استفاده از سرویس پایه معرفی شده را می‌توانید مشاهده کنید.

namespace MyApp.ServiceLayer.Roles
{
    public interface IRoleApplicationService :
        ICrudApplicationService<RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel>
    {
    }
}

namespace MyApp.ServiceLayer.Roles
{
    public class RoleApplicationService :
        CrudApplicationService<Role, RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel>,
        IRoleApplicationService
    {
        #region Constructor

        public RoleApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
        {
        }

        #endregion
    }
}


نکته: در این لایه بندی نکات مربوط به مطلب «پیاده سازی ماژولار Autofac» نیز با استفاده از StructureMap اعمال شده است. بدین ترتیب در هر لایه یک Registry مربوط به StructureMap ایجاد شده است. به شکل زیر:

FrameworkRegistry

namespace MyApp.Framework
{
    public class FrameworkRegistry : Registry
    {
        public FrameworkRegistry()
        {
            For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>();

            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.LookForRegistries();
            });

            Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor)/*, typeof(TransactionInterceptor)*/));
        }
    }
}


DataLayerRegistry

namespace MyApp.DataLayer
{
    public class DataLayerRegistry : Registry
    {
        public DataLayerRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnStartTask>();
            });

            //todo:use container per request (Nested Containers) instead of HttpContextLifeCycle
            For<IUnitOfWork>().Use<ApplicationDbContext>();
        }
    }
}


ServiceLayerRegistry

namespace MyApp.ServiceLayer
{
    public class ServiceLayerRegistry : Registry
    {
        #region Constructor

        public ServiceLayerRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.Assembly(typeof(DataLayerRegistry).Assembly);
                scan.LookForRegistries();

                scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName);
                scan.AddAllTypesOf<IHaveCustomMappings>().NameBy(item => item.FullName);
            });

            FluentValidationConfig();
            AutoMapperConfig();
        }

        #endregion

        #region Private Methods

        private void AutoMapperConfig()
        {
            For<MapperConfiguration>().Singleton().Use("MapperConfig", ctx =>
            {
                var config = new MapperConfiguration(cfg =>
                {
                    cfg.CreateMissingTypeMaps = true;
                    AddProfiles(ctx, cfg);
                    AddIHaveCustomMappings(ctx, cfg);
                    AddMapFrom(cfg);
                });

                config.AssertConfigurationIsValid();

                return config;
            });

            For<IMapper>().Singleton().Use(ctx => ctx.GetInstance<MapperConfiguration>().CreateMapper(ctx.GetInstance));
        }

        private void FluentValidationConfig()
        {
            AssemblyScanner.FindValidatorsInAssembly(Assembly.GetExecutingAssembly())
                .ForEach(result =>
                {
                    For(result.InterfaceType)
                        .Singleton()
                        .Use(result.ValidatorType);
                });
        }

        private static void AddMapFrom(IProfileExpression cfg)
        {
            var types = typeof(RoleViewModel).Assembly.GetExportedTypes();
            var maps = (from t in types
                        from i in t.GetInterfaces()
                        where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>) && !t.IsAbstract &&
                              !t.IsInterface
                        select new
                        {
                            Source = i.GetGenericArguments()[0],
                            Destination = t
                        }).ToArray();

            foreach (var map in maps)
                cfg.CreateMap(map.Source, map.Destination);
        }

        private static void AddProfiles(IContext ctx, IMapperConfigurationExpression cfg)
        {
            var profiles = ctx.GetAllInstances<Profile>().ToList();
            foreach (var profile in profiles)
                cfg.AddProfile(profile);
        }

        private static void AddIHaveCustomMappings(IContext ctx, IMapperConfigurationExpression cfg)
        {
            var mappings = ctx.GetAllInstances<IHaveCustomMappings>().ToList();
            foreach (var mapping in mappings)
                mapping.CreateMappings(cfg);
        }

        #endregion
    }
}


WebRegistry

namespace MyApp.Web
{
    public class WebRegistry : Registry
    {
        public WebRegistry()
        {
            Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AssembliesFromApplicationBaseDirectory();
                
                scan.AddAllTypesOf<IRunOnEndTask>();
                scan.AddAllTypesOf<IRunOnOwinStartupTask>();
                scan.AddAllTypesOf<IRunOnStartTask>();
                scan.AddAllTypesOf<IRunOnBeginRequestTask>();
                scan.AddAllTypesOf<IRunOnErrorTask>();
                scan.AddAllTypesOf<IRunOnEndRequestTask>();

                scan.Assembly(typeof(ServiceLayerRegistry).Assembly);
                scan.LookForRegistries();
            });
        }
    }
}

در این طراحی، لایه Web یا همان Presentation به DataLayer و DomainClasses هیچ ارجاعی ندارد.


در قسمت بعد استفاده از این سرویس را در یک برنامه ASP.NET MVC با هم بررسی خواهیم کرد. 

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید.