نظرات مطالب
اشیاء تغییر ناپذیر (Immutable Object)
یک نکته‌ی تکمیلی
در C# 6 می‌توان کد نهایی مطرح شده را به صورت زیر خلاصه کرد:
public class Currency
{
  public string CurrencyName { get; } 
  public string CountryName { get; }

  public Currency(string paramCurrencyName,string paramCountryName)
  {
     CurrencyName= paramCurrencyName;
     CountryName = paramCountryName;
  }
}
مطالب
توابع تعمیم یافته در #C
از توابع تعمیم یافته می‌توان برای توسعه توابع هر کلاس یا اینترفیسی استفاده کرد. یعنی می‌‎توان یک تابع را به هر کلاسی اضافه کرد.

قبل از C# 3.0  فقط می‌شد یک کلاس را از طریق ارث‌بری از آن توسعه داد و به کلاس مهروموم شده یاSealed  نیز نمی‌شد تابعی افزود که البته ممکن است بگویید که این کار قوانین شیءگرایی را نقض می‌کند اما در پاسخ باید گفت که توابع تعمیم یافته به اعضاء خصوصی کلاسی که تعمیم می‌یابد، دسترسی ندارند.

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

 public static class  ExtendingClassName
 {
      public static ReturnType MethodName(this   ExtendedMethod  arg)
      {
           //دستورات درون متد
          Return ReturnType;
      } 
 } 
توجه کنید که:
  1. کلاس توسعه‌دهنده و تابع توسه‌دهنده باید استاتیک باشند.
  2. در داخل آرگومان تابع، کلمه کلیدی this  استفاده می‌‎‎شود.
  3. بعد از this عنوان کلاسی که قصد توسعه آن را داریم، ذکر می‌کنیم.
  4. در هرجا که خواستیم از قابلیت تعمیم داده شده استفاده کنیم باید فضای نام مربوط به آن را ذکر کنیم.
  5. با کلمه کلیدی static  نمی‎توان کلاسی با متدهایvirtual  ، abstract  و override را توسعه داد. 

مثلا اگر قصد داریم به کلاس String  تابع AddPrefix  را اضافه کنیم به این شکل عمل می‌‎کنیم :

public static class  ExtendingString
{
  public static  string  AddPrefix(this   string  arg)
  {
     return String.Format("prefix{0}",arg);
  } 
} 

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

string  s = "Student";
Console.WriteLine(s.AddPrefix());

و خروجی آن نمایش prefixStudent است. 

اگر بخواهیم عبارت پیشوند را از طریق آرگومان ارسال کنیم به این شکل عمل می‎کنیم: 

public static class ExtendingString   
{
    public static string AddPrefix(this   string arg, string prefix)
    {
         return String.Format("{0}{1}", prefix, arg);
     }
} 

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

var  s = "Student";
Console.WriteLine(s.AddPrefix("tbl"));

و خروجی آن نمایش tblStudent است.

به عنوان مثال دوم کلاس زیر را در نظر بگیرید: 

public class Test 
{
     public int AddOne(int val)
     {
         return val + 1;
     }
  }

اگر بخواهیم کلاس فوق را توسعه داده و متد دیگری مثلا با عنوان AddTwo اضافه کنیم، کلاس توسعه دهنده را به این شکل می‌نویسیم:

public static class ExtendingTest   
{
     public static int AddTwo(this   Test  arg, int val)
     {
         return val + 2;
     }
}

و روش استفاده از آن بصورت زیر است: 

static void Main(string[] args)
{
       var x = new Test();
       Console.WriteLine(x.AddOne(10));
       Console.WriteLine(x.AddTwo(10));
       Console.Read();
}
نظرات مطالب
معرفی Reactive extensions
زمانیکه از ری‌شارپر استفاده می‌کنید، این تعیین نوع صریح را به صورت کم رنگ (به معنای کد مرده یا زاید) معرفی می‌کند:


علت اینجا است که نوع آرگومان جنریک به صورت خودکار توسط نوع پارامتر ارسالی به متد قابل تشخیص است (در اینجا چون ToObservable یک متد الحاقی است، اولین پارامتر آن، عناصر توالی query هستند که از نوع IEnumerable of int تعریف شدند).
برای مطالعه بیشتر مراجعه کنید به C# specs (ECMA-334) part 25.6.4 Inference of type arguments 
مطالب دوره‌ها
نکاتی درباره برنامه نویسی دستوری(امری)
در این فصل نکاتی را درباره برنامه نویسی دستوری در #F فرا خواهیم گرفت. برای شروع از mutale خواهیم گفت.

mutable
Keyword
در فصل دوم(شناسه ها) گفته شد که برای یک شناسه امکان تغییر مقدار وجود ندارد. اما در #F راهی وجود دارد که در صورت نیاز بتوانیم مقدار یک شناسه را تغییر دهیم.در #F هرگاه بخواهیم شناسه ای تعریف کنیم که بتوان در هر زمان مقدار شناسه رو به دلخواه تغییر داد از کلمه کلیدی mutable کمک می‌گیریم و برای تغییر مقادیر شناسه‌ها کافیست از علامت (->) استفاده کنیم. به یک مثال در این زمینه دقت کنید:
#1 let mutable phrase = "Can it change? "

#2 printfn "%s" phrase

#3 phrase <- "yes, it can."

#4 printfn "%s" phrase


در خط اول یک شناسه را به صورت mutable(تغییر پذیر) تعریف کردیم و در خط سوم با استفاده از (->) مقدار شناسه رو update کردیم. خروجی مثال بالا به صورت زیر است:
Can it change?  
yes, it can.

نکته اول: در این روش هنگام update کردن مقدار شناسه حتما باید مقدار جدید از نوع مقدار قبلی باشد در غیر این صورت با خطای کامپایلری متوقف خواهید شد.
#1 let mutable phrase = "Can it change?  "

#3 phrase <- 1

اجرای کد بالا خطای زیر را به همراه خواهد داشت.(خطا کاملا واضح است و نیاز به توضیح دیده نمی‌شود)
Prog.fs(9,10): error: FS0001: This expression has type
int
but is here used with type
string
نکته دوم :ابتدا به مثال زیر توجه کنید.
let redefineX() =
let x = "One"
printfn "Redefining:\r\nx = %s" x
if true then
   let x = "Two"
printfn "x = %s" x
printfn "x = %s" x

در مثال بالا در تابع redefineX یک شناسه به نام x تعریف کردم با مقدار "One". یک بار مقدار شناسه x رو چاپ می‌کنیم و بعد دوباره بعد از شرط true یک شناسه دیگر با همون نام یعنی x تعریف شده است و در انتها هم دو دستور چاپ. ابتدا خروجی مثال بالا رو با هم مشاهده می‌کنیم.
Redefining:
x = One
x = Two
x = One
همان طور که میبینید شناسه دوم x بعد از تعریف دارای مقدار جدید Two بود و بعد از اتمام محدوده(scope) مقدار x دوباره به One تغییر کرد.(بهتر است بگوییم منظور از دستور print x سوم اشاره به شناسه x اول برنامه است). این رفتار مورد انتظار ما در هنگام استفاده از روش تعریف مجدد شناسه هاست. حال به بررسی رفتار muatable در این حالت می‌پردازیم.
let mutableX() =
let mutable x = "One"
printfn "Mutating:\r\nx = %s" x
if true then
   x <- "Two"
printfn "x = %s" x
printfn "x = %s" x
تنها تفاوت در استفاده از mutable keyword و (->) است. خروجی مثال بالا نیز به صورت زیر خواهد بود. کاملا واضح است که مقدار شناسه x بعد از تغییر و اتمام محدوده(scope) هم چنان Two خواهد بود.
Mutating:
x = One
x = Two
x = Two

Reference Cells

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

 Member Or Field
Description
 Definition
(derefence operator)!
 مقدار مشخص شده را برگشت می‌دهد
 

let (!) r = r.contents 

 (Assignment operator)=: مقدار مشخص شده را تغییر می‌دهد
 

let (:=) r x = r.contents <- x 

ref operator
 یک مقدار را در یک reference cell  جدید کپسوله می‌کند
 

let ref x = { contents = x } 

Value Property
 برای عملیات get  یا set  مقدار مشخص شده
 

member x.Value = x.contents 

 contents record field
 برای عملیات get  یا set  مقدار مشخص شده

let ref x = { contents = x } 

  یک مثال:
let refVar = ref 6

refVar := 50
printfn "%d" !refVar
خروجی مثال بالا 50 خواهد بود.
let xRef : int ref = ref 10

printfn "%d" (xRef.Value)
printfn "%d" (xRef.contents)

xRef.Value <- 11
printfn "%d" (xRef.Value)
xRef.contents <- 12
printfn "%d" (xRef.contents)
خروجی مثال بالا:
10
10
11
12 

خصیصه اختیاری در #F
در #F زمانی از خصیصه اختیاری استفاده می‌کنیم که برای یک متغیر مقدار وجود نداشته باشد. option  در #F نوعی است که می‌تواند هم مقدار داشته باشد و هم نداشته باشد.
let keepIfPositive (a : int) = if a > 0 then Some(a) else None
از None زمانی استفاده می‌کنیم که option مقدار نداشته باشد و از Some  زمانی استفاده می‌کنیم که option مقدار داشته باشد.
let exists (x : int option) = 
    match x with
    | Some(x) -> true
    | None -> false
در مثال بالا ورودی تابع exists از نوع int و به صورت اختیاری تعریف شده است.(معادل با ?int یا<Nullable<int در #C) در صورتی که x مقدار داشته باشد مقدار true در غیر این صورت مقدار false را برگشت می‌دهد.

چگونگی استفاده  از option
مثال
let rec tryFindMatch pred list =
    match list with
    | head :: tail -> if pred(head)
                        then Some(head)
                        else tryFindMatch pred tail
    | [] -> None

let result1 = tryFindMatch (fun elem -> elem = 100) [ 200; 100; 50; 25 ] //برابر با 100 است

let result2 = tryFindMatch (fun elem -> elem = 26) [ 200; 100; 50; 25 ]// برابر با None است
یک تابع به نام tryFindMatch داریم با دو پارامتر ورودی. با استفاده از الگوی Matching از عنصر ابتدا تا انتها را در لیست (پارامتر list) با مقدار پارامتر pred مقایسه می‌کنیم. اگر مقادیر برابر بودند مقدار head در غیر این صورت None(یعنی option مقدار ندارد) برگشت داده می‌شود.
یک مثال کاربردی تر
open System.IO
let openFile filename =
    try 
        let file = File.Open (filename, FileMode.Create)
        Some(file)
    with
        | ex -> eprintf "An exception occurred with message %s" ex.Message
                None
در مثال بالا از option‌ها برای بررسی وجود یا عدم وجود فایل‌های فیزیکی استفاده کردم.

Enumeration
تقریبا همه با نوع داده شمارشی یا enums آشنایی دارند. در اینجا فقط به نحوه پیاده سازی آن در #F می‌پردازیم. ساختار کلی تعریف آن به صورت زیر است:
type enum-name =
   | value1 = integer-literal1
   | value2 = integer-literal2
   ...
یک مثال از تعریف:
type Color =
   | Red = 0
   | Green = 1
   | Blue = 2
نحوه استفاده
let col1 : Color = Color.Red
enums فقط از انواع داده ای sbyte, byte, int16, uint16, int32, uint32, int64, uint16, uint64, char پشتیبانی می‌کند که البته مقدار پیش فرض آن Int32 است. در صورتی که بخواهیم صریحا نوع داده ای را ذکر کنیم به صورت زیر عمل می‌شود.
type uColor =
   | Red = 0u
   | Green = 1u
   | Blue = 2u
let col3 = Microsoft.FSharp.Core.LanguagePrimitives.EnumOfValue<uint32, uColor>(2u)

توضیح درباره use
در دات نت خیلی از اشیا هستند که اینترفیس IDisposable رو پیاده سازی کرده اند. این بدین معنی است که حتما یک متد به نام dispose برای این اشیا وجود دارد که فراخوانی آن به طور قطع باعث بازگرداندن حافظه ای که در اختیار این کلاس‌ها بود می‌شود. برای راحتی کار در #C یک عبارت به نام using وجود دارد که در انتها بلاک متد dispose شی مربوطه را فراخوانی می‌کند.
using(var writer = new StreamWriter(filePath))
{
   
}
در #F نیز امکان استفاده از این عبارت با اندکی تفاوت وجود دارد.مثال:
let writeToFile fileName =
    use sw = new System.IO.StreamWriter(fileName : string)
    sw.Write("Hello ")
Units Of Measure
در #F اعداد دارای علامت و اعداد شناور دارای وابستگی با واحد‌های اندازه گیری هستند که به نوعی معرف اندازه و حجم و مقدار و ... هستند. در #F شما مجاز به تعریف واحد‌های اندازه گیری خاص خود هستید و در این تعاریف نوع عملیات اندازه گیری را مشخص می‌کنید. مزیت اصلی استفاده از این روش جلوگیری از رخ دادن خطاهای کامپایلر در پروژه است. ساختار کلی تعریف:
[<Measure>] type unit-name [ = measure ]
یک مثال از تعریف واحد cm:
[<Measure>] type cm
مثالی از تعریف میلی لیتر:
[<Measure>] type ml = cm^3
برای استفاده از این واحد‌ها می‌تونید به روش زیر عمل کنید.
let value = 1.0<cm>
توابع تبدیل واحد ها
قدرت اصلی واحد‌های اندازه گیری  #F در توابع تبدیل است. تعریف توابع تبدیل به صورت زیر می‌باشد:
[<Measure>] type g                 تعریف واحد گرم
[<Measure>] type kg               تعریف واحد کیلوگرم
let gramsPerKilogram : float<g kg^-1> = 1000.0<g/kg>    تعریف تابع تبدیل
یک مثال دیگر :
[<Measure>] type degC // دما بر حسب سلسیوس
[<Measure>] type degF // دما بر حسب فارنهایت

let convertCtoF ( temp : float<degC> ) = 9.0<degF> / 5.0<degC> * temp + 32.0<degF> // تابع تبدیل سلسیوس به فارنهایت
let convertFtoC ( temp: float<degF> ) = 5.0<degC> / 9.0<degF> * ( temp - 32.0<degF>) // تابع تبدیل فارنهایت به سلسیوس

let degreesFahrenheit temp = temp * 1.0<degF> // درجه به فارنهایت
let degreesCelsius temp = temp * 1.0<degC> // درجه به سلسیوس

printfn "Enter a temperature in degrees Fahrenheit." 
let input = System.Console.ReadLine()
let mutable floatValue = 0.
if System.Double.TryParse(input, &floatValue)// اگر ورودی عدد بود
   then 
      printfn "That temperature in Celsius is %8.2f degrees C." ((convertFtoC (degreesFahrenheit floatValue))/(1.0<degC>))
   else
      printfn "Error parsing input."

خروجی مثال بالا :

Enter a temperature in degrees Fahrenheit.
90
That temperature in degrees Celsius is    32.22.
مطالب
طراحی و پیاده سازی DomainEvents

شرایطی را در نظر بگیرید که نیاز است از تغییرات یک Entity در سیستم آگاه شویم. برای مثلا در زمان ثبت سفارش جدید در فروشگاه، ایمیلی به مدیر فروشگاه ارسال شود، یک Business Rule نیز چک شود و همچنین بنابر نیاز مشتری، تعداد آنها روز به روز ممکن است افزایش یابد و چه بسا در اعمال این Ruleها، موجودیت‌های مختلفی درگیر باشند. در این صورت است که خواسته یا ناخواسته اتصال بین کلاس‌ها خیلی افزایش خواهد یافت. یکی از راه حل‌های رهایی از این پیچیدگی و اتصال بالا، استفاده از Event می‌باشد.

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


Domain Event چیست؟

چیزی که در یک Domain خاصی رخ داده است و هدف از آن آگاه کردن سایر بخش‌های آن Domain می‌باشد تا بتوانند واکنش مناسبی را نشان دهند. با بهره گیری از این نوع رویدادها، می‌توان Separation Of Concerns خوبی را بین کلاس‌های موجود در آن Domain اعمال کرد و به طراحی ای با Coupling پایین رسید. این رویداد‌ها عموما داخل پروسه Raise می‌شوند.

برای اطلاعات بیشتر در این زمینه پیشنهاد میکنم این مطلب را مطالعه کنید.
کار را با معرفی واسط IDomainEvent آغاز می‌کنیم.
namespace DomainEventsSample.Framework.Eventing.DomainEvents
{
    public interface IDomainEvent : ITransientDependency
    {
    }
}
در کد بالا، واسط ITransientDepedency برای اعمال طول عمر وهله‌های ساخته شده توسط StructureMap در نظر گرفته شده است. برای علامت گذاری DomainEventها، از واسط بالا استفاده خواهیم کرد. 

واسط IDomainEventHandler
namespace DomainEventsSample.Framework.Eventing.DomainEvents
{
    public interface IDomainEventHandler<in T> : ITransientDependency
        where T : IDomainEvent
    {
        bool IsAdvisable { get; }
        void Handle(T domainEvent);
    }
}
از کلاس‌های پیاده ساز واسط بالا، می‌توان برای مدیریت رویداد خاصی استفاده کرد. برای علامت گذاری DomainEventHandlerها نیز از این واسط استفاده میشود. 
خصوصیت IsAdvisable: اگر مقدار آن true باشد، در این صورت در زمان صدور استثنایی در روند اجرای متد Handle آن از این استثناء چشم پوشی شده و مابقی هندلرها فراخوانی خواهند شد.

پیاده سازی Engine مربوط به Raise کردن رویدادها
روند کار به این شکل است:
  1. متد Raise مربوط به Engine برای رویداد خاصی فراخوانی می‌شود.
  2. با استفاده از یک IOC Container، تمام هندلرهای مربوط به رویداد جمع آوری می‌شود.
  3. متد Handle مربوط به تک تک هندلرها، فراخوانی خواهد شد.
namespace DomainEventsSample.Framework.Eventing.DomainEvents
{
    public interface IDomainEventEngine : ISingletonDependency
    {
        void Raise<T>(T domainEvent) where T : IDomainEvent;
    }
}

namespace DomainEventsSample.Framework.Eventing.DomainEvents
{
    public class DomainEventEngine : IDomainEventEngine
    {
        private readonly IContainer _container;

        public DomainEventEngine(IContainer container)
        {
            _container = container;
        }

        public void Raise<T>(T domainEvent) where T : IDomainEvent
        {
            foreach (var handler in _container.GetAllInstances<IDomainEventHandler<T>>())
                try
                {
                    handler.Handle(domainEvent);
                }
                catch (Exception)
                {
                    if (domainEvent.IsAdvisable && handler.IsAdvisable)
                        throw;
                }
        }
    }
}
طول عمر این Engine به صورت Singleton در نظر گرفته شده است. همانطور که گفته شد، در صورت صدور استثناء، در صورت IsAdvisable بودن خود رویداد، خصوصیت IsAdvisable هندلر آن بررسی خواهد شد.
شاید بهتر باشد یکسری رویداد پیش فرض هم در زیرساخت پروژه خود داشته باشیم. برای مثال رویدادهای مربوط به Entityها که در زیر آنها را مشاهده می‌کنید:
namespace DomainEventsSample.Framework.Domain.Events
{
    public abstract class EntityDomainEvent<TEntity> : IDomainEvent
        where TEntity : Entity
    {
        protected EntityDomainEvent(TEntity entity)
        {
            Entity = entity;
        }

        public TEntity Entity { get; }
    }
}

کلاس بالا به عنوان کلاس پایه یکسری رویداد مشترک مابین Entity‌های سیستم در نظر گرفته شده است.

namespace DomainEventsSample.Framework.Domain.Events
{
    public class EntityCreatingEvent<TEntity> : EntityDomainEvent<TEntity>
        where TEntity : Entity
    {
        public EntityCreatingEvent(TEntity entity) : base(entity)
        {
        }
    }
}
namespace DomainEventsSample.Framework.Domain.Events
{
    public class EntityCreatedEvent<TEntity> : EntityDomainEvent<TEntity>
        where TEntity : Entity
    {
        public EntityCreatedEvent(TEntity entity) : base(entity)
        {
        }
    }
}

این رویدادها مربوط به زمان قبل و بعد از ایجاد یک Entity می‌باشند.

namespace DomainEventsSample.Framework.Domain.Events
{
    public class EntityEditingEvent<TEntity> : EntityDomainEvent<TEntity>
        where TEntity : Entity
    {
        public EntityEditingEvent(TEntity entity) : base(entity)
        {
        }
    }
}
namespace DomainEventsSample.Framework.Domain.Events
{
    public class EntityEditedEvent<TEntity> : EntityDomainEvent<TEntity>
        where TEntity : Entity
    {
        public EntityEditedEvent(TEntity entity) : base(entity)
        {
        }
    }
}

این رویدادها مربوط به زمان قبل و بعد از ویرایش یک Entity می‌باشند. 

namespace DomainEventsSample.Framework.Domain.Events
{
    public class EntityDeletingEvent<TEntity> : EntityDomainEvent<TEntity>
        where TEntity : Entity
    {
        public EntityDeletingEvent(TEntity entity) : base(entity)
        {
        }
    }
}
namespace DomainEventsSample.Framework.Domain.Events
{
    public class EntityDeletedEvent<TEntity> : EntityDomainEvent<TEntity>
        where TEntity : Entity
    {
        public EntityDeletedEvent(TEntity entity) : base(entity)
        {
        }
    }
}

این رویدادها مربوط به زمان قبل و بعد از حذف یک Entity می‌باشند.

namespace DomainEventsSample.Framework.Domain.Events
{
    public class EntitySavingEvent<TEntity> : EntityDomainEvent<TEntity>
        where TEntity : Entity
    {
        public EntitySavingEvent(TEntity entity) : base(entity)
        {
        }
    }
}

namespace DomainEventsSample.Framework.Domain.Events
{
    public class EntitySavedEvent<TEntity> : EntityDomainEvent<TEntity>
        where TEntity : Entity
    {
        public EntitySavedEvent(TEntity entity) : base(entity)
        {
        }
    }
}

این رویدادها مربوط به زمان قبل و بعد از ذخیره (ایجاد و ویرایش) یک Entity می‌باشند.

نکته: برای اسکن کردن تمام هندلرها لازم است کد زیر را به تنظیمات StructureMap اضافه کنید:

Scan(scan =>
{
    scan.ConnectImplementationsToTypesClosing(typeof(IDomainEventHandler<>));
});

مثال: برای مثال این بار برای آگاه سازی کاربران به صورت بلادرنگ از اضافه شدن یک کالا، می‌توان با هندل کردن رویداد مربوط به ایجاد کالا، به شکل زیر عمل کرد:
public class ProductCreatedEventHandler : IDomainEventHandler<EntityCreatedEvent<Product>>
{
    public bool IsAdvisable => false;

    public void Handle(EntityCreatedEvent<Product> domainEvent)
    {
        //todo: notify users
    }
}

در متد Create مربوط به ProductApplicationService و بعد از عملیات ذخیره سازی به شکل زیر می‌بایست عمل کرد:

public class ProductApplicationService : IProductApplicationService
{
    private readonly IDomainEventEngine _eventEngine;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;

    public ProductApplicationService(IDomainEventEngine eventEngine,IMapper mapper,IUnitOfWork unitOfWork)
    {
        _eventEngine = eventEngine;
        _mapper=mapper;
        _unitOfWork=unitOfWork;
    }

    [Transactional]
    public void Create(ProductCreateViewModel model)
    {
         var entity=_mapper.Map<Product>(model);
        _unitOfWork.Set<Product>().Add(entity);
        _unitOfWork.SaveChanges();
        _eventEngine.Raise(new EntityCreatedEvent<Product>(entity));
    }
}

البته بهتر است برای Raise کردن این نوع رویدادها از مکانیزم Hook استفاده کرد و در زمان ذخیره سازی و فراخوانی متد SaveChange، این عملیات به صورت خودکار صورت گیرند.

در مقاله بعدی با استفاده از Hookها این عملیات را انجام خواهیم داد. 


کدهای این قسمت را می‌توانید از اینجا دریافت کنید.
مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
پیشنیاز بحث
- «فرمت کردن اطلاعات نمایش داده شده به کمک Kendo UI Grid»

Kendo UI Grid دارای امکانات ثبت، ویرایش و حذف توکاری است که در ادامه نحوه‌ی فعال سازی آن‌‌ها را بررسی خواهیم کرد. مثالی که در ادامه بررسی خواهد شد، در تکمیل مطلب «فرمت کردن اطلاعات نمایش داده شده به کمک Kendo UI Grid» است.



تنظیمات Data Source سمت کاربر

برای فعال سازی صفحه بندی سمت سرور، با قسمت read منبع داده Kendo UI پیشتر آشنا شده بودیم. جهت فعال سازی قسمت‌های ثبت اطلاعات جدید (create)، به روز رسانی رکوردهای موجود (update) و حذف ردیفی مشخص (destroy) نیاز است تعاریف قسمت‌های متناظر را که هر کدام به آدرس مشخصی در سمت سرور اشاره می‌کنند، اضافه کنیم:
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    create: {
                        url: "api/products",
                        contentType: 'application/json; charset=utf-8',
                        type: "POST"
                    },
                    update: {
                        url: function (product) {
                            return "api/products/" + product.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (product) {
                            return "api/products/" + product.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    //...
                },
                schema: {
                    //...
                    model: {
                        id: "Id", // define the model of the data source. Required for validation and property types.
                        fields: {
                            "Id": { type: "number", editable: false }, //تعیین نوع فیلد برای جستجوی پویا مهم است
                            "Name": { type: "string", validation: { required: true } },
                            "IsAvailable": { type: "boolean" },
                            "Price": { type: "number", validation: { required: true, min: 1 } },
                            "AddDate": { type: "date", validation: { required: true } }
                        }
                    }
                },
                batch: false, // enable batch editing - changes will be saved when the user clicks the "Save changes" button
                //...
            });
- همانطور که ملاحظه می‌کنید، حالت‌های update و destroy بر اساس Id ردیف انتخابی کار می‌کنند. این Id را باید در قسمت model مربوط به اسکیمای تعریف شده، دقیقا مشخص کرد. عدم تعریف فیلد id، سبب خواهد شد تا عملیات update نیز در حالت create تفسیر شود.
- به علاوه در اینجا به ازای هر فیلد، مباحث اعتبارسنجی نیز اضافه شده‌اند؛ برای مثال فیلدهای اجباری با required: true مشخص گردیده‌اند.
- اگر فیلدی نباید ویرایش شود (مانند فیلد Id)، خاصیت editable آن‌را false کنید.
- در data source امکان تعریف خاصیتی به نام batch نیز وجود دارد. حالت پیش فرض آن false است. به این معنا که در حالت ویرایش، تغییرات هر ردیفی، یک درخواست مجزا را به سمت سرور سبب خواهد شد. اگر آن‌را true کنید، تغییرات تمام ردیف‌ها در طی یک درخواست به سمت سرور ارسال می‌شوند. در این حالت باید به خاطر داشت که پارامترهای سمت سرور، از حالت یک شیء مشخص باید به لیستی از آن‌ها تغییر یابند.


مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات

در حالت ثبت، متد Post، توسط آدرس مشخص شده در قسمت create منبع داده گرید، فراخوانی می‌گردد:
namespace KendoUI06.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Post(Product product)
        {
            if (!ModelState.IsValid)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            var id = 1;
            var lastItem = ProductDataSource.LatestProducts.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            product.Id = id;
            ProductDataSource.LatestProducts.Add(product);

            var response = Request.CreateResponse(HttpStatusCode.Created, product);
            response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = product.Id }));
            // گرید آی دی جدید را به این صورت دریافت می‌کند
            response.Content = new ObjectContent<DataSourceResult>(
                new DataSourceResult { Data = new[] { product } }, new JsonMediaTypeFormatter());
            return response;
        }
    }
}
نکته‌ی مهمی که در اینجا باید به آن دقت داشت، نحوه‌ی بازگشت Id رکورد جدید ثبت شده‌است. در این مثال، قسمت schema منبع داده سمت کاربر به نحو ذیل تعریف شده‌است:
            var productsDataSource = new kendo.data.DataSource({
                //...
                schema: {
                    data: "Data",
                    total: "Total",
                }
                //...
            });
از این جهت که خروجی متد Get بازگرداننده‌ی اطلاعات صفحه بندی شده، از نوع DataSourceResult است و این نوع، دارای خواصی مانند Data، Total و Aggergate است:
namespace KendoUI06.Controllers
{
    public class ProductsController : ApiController
    {
        public DataSourceResult Get(HttpRequestMessage requestMessage)
        {
            var request = JsonConvert.DeserializeObject<DataSourceRequest>(
                requestMessage.RequestUri.ParseQueryString().GetKey(0)
            );

            var list = ProductDataSource.LatestProducts;
            return list.AsQueryable()
                       .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter);
        }
    }
}
بنابراین در متد Post نیز باید بر این اساس، response.Content را از نوع لیستی از DataSourceResult تعریف کرد تا Kendo UI Grid بداند که Id رکورد جدید را باید از فیلد Data، همانند تنظیمات schema منبع داده خود، دریافت کند.
response.Content = new ObjectContent<DataSourceResult>(
                              new DataSourceResult { Data = new[] { product } }, new JsonMediaTypeFormatter());
اگر این تنظیم صورت نگیرد، Id رکورد جدید را در گرید، مساوی صفر مشاهده خواهید کرد و عملا بدون استفاده خواهد شد؛ زیرا قابلیت ویرایش و حذف خود را از دست می‌دهد.

متدهای حذف و به روز رسانی سمت سرور نیز چنین امضایی را خواهند داشت:
namespace KendoUI06.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = ProductDataSource.LatestProducts.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            ProductDataSource.LatestProducts.Remove(item);

            return Request.CreateResponse(HttpStatusCode.OK, item);
        }

        [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT'
        public HttpResponseMessage Update(int id, Product product)
        {
            var item = ProductDataSource.LatestProducts
                                        .Select(
                                            (prod, index) =>
                                                new
                                                {
                                                    Item = prod,
                                                    Index = index
                                                })
                                        .FirstOrDefault(x => x.Item.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);


            if (!ModelState.IsValid || id != product.Id)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            ProductDataSource.LatestProducts[item.Index] = product;
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
حالت Update از HTTP Verb خاصی به نام Put استفاده می‌کند و ممکن است در این بین خطای The requested resource does not support http method 'PUT' را دریافت کنید. برای رفع آن ابتدا بررسی کنید که آیا Web.config برنامه دارای تعاریف ExtensionlessUrlHandler هست یا خیر. همچنین مزین کردن این متد با ویژگی HttpPut، مشکل را برطرف می‌کند.


تنظیمات Kendo UI Grid جهت فعال سازی CRUD

در ادامه کلیه تغییرات مورد نیاز جهت فعال سازی CRUD را در Kendo UI، به همراه مباحث بومی سازی عبارات متناظر با دکمه‌ها و صفحات خودکار مرتبط، مشاهده می‌کنید:
            $("#report-grid").kendoGrid({
                //....
                editable: {
                    confirmation: "آیا مایل به حذف ردیف انتخابی هستید؟",
                    destroy: true, // whether or not to delete item when button is clicked
                    mode: "popup", // options are "incell", "inline", and "popup"
                    //template: kendo.template($("#popupEditorTemplate").html()), // template to use for pop-up editing
                    update: true, // switch item to edit mode when clicked?
                    window: {
                        title: "مشخصات محصول"   // Localization for Edit in the popup window
                    }
                },
                columns: [
                //....
                    {
                        command: [
                            { name: "edit", text: "ویرایش" },
                            { name: "destroy", text: "حذف" }
                        ],
                        title: "&nbsp;", width: "160px"
                    }
                ],
                toolbar: [
                    { name: "create", text: "افزودن ردیف جدید" },
                    { name: "save", text: "ذخیره‌ی تمامی تغییرات" },
                    { name: "cancel", text: "لغو کلیه‌ی تغییرات" },
                    { template: kendo.template($("#toolbarTemplate").html()) }
                ],
                messages: {
                    editable: {
                        cancelDelete: "لغو",
                        confirmation: "آیا مایل به حذف این رکورد هستید؟",
                        confirmDelete: "حذف"
                    },
                    commands: {
                        create: "افزودن ردیف جدید",
                        cancel: "لغو کلیه‌ی تغییرات",
                        save: "ذخیره‌ی تمامی تغییرات",
                        destroy: "حذف",
                        edit: "ویرایش",
                        update: "ثبت",
                        canceledit: "لغو"
                    }
                }
            });
- ساده‌ترین حالت CRUD در Kendo UI با مقدار دهی خاصیت editable آن به true آغاز می‌شود. در این حالت، ویرایش درون سلولی یا incell فعال خواهد شد که مباحث batching ابتدای بحث، فقط در این حالت کار می‌کند. زمانیکه incell editing فعال است، کاربر می‌تواند تمام ردیف‌ها را ویرایش کرده و در آخر کار بر روی دکمه‌ی «ذخیره‌ی تمامی تغییرات» موجود در نوار ابزار، کلیک کند. در سایر حالات، هر بار تنها یک ردیف را می‌توان ویرایش کرد.
- برای فعال سازی تولید صفحات خودکار ویرایش و افزودن ردیف‌ها، نیاز است خاصیت editable را به نحوی که ملاحظه می‌کنید، مقدار دهی کرد. خاصیت mode آن سه حالت incell (پیش فرض)، inline و popup را پشتیبانی می‌کند.
- اگر حالت‌های inline و یا popup را فعال کردید، در انتهای ستون‌های تعریف شده، نیاز است ستون ویژه‌ای به نام command را مطابق تعاریف فوق، تعریف کنید. در این حالت دو دکمه‌ی ویرایش و ثبت، فعال می‌شوند و اطلاعات خود را از تنظیمات data source گرید دریافت می‌کنند. دکمه‌ی ویرایش در حالت incell کاربردی ندارد (چون در این حالت کاربر با کلیک درون یک سلول می‌تواند آن‌را مانند برنامه‌ی اکسل ویرایش کند). اما دکمه‌ی حذف در هر سه حالت قابل استفاده است.
- به نوار ابزار گرید، سه دکمه‌ی افزودن ردیف‌های جدید، ذخیره‌ی تمامی تغییرات و لغو تغییرات صورت گرفته، اضافه شده‌اند. این دکمه‌ها استاندارد بوده و در اینجا نحوه‌ی بومی سازی پیام‌های مرتبط را نیز مشاهده می‌کنید. همانطور که عنوان شد، دکمه‌های «تمامی تغییرات» در حالت فعال سازی batching در منبع داده و استفاده از incell editing معنا پیدا می‌کند. در سایر حالات این دو دکمه کاربردی ندارند. اما دکمه‌ی افزودن ردیف‌های جدید در هر سه حالت کاربرد دارد و یکسان است.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
KendoUI06.zip
مطالب
مدیریت حالت در برنامه‌های Blazor توسط الگوی Observer - قسمت دوم
در قسمت قبل، روشی را بر اساس الگوی Observer، برای به اشتراک گذاری حالت و مدیریت سراسری آن، بررسی کردیم. در این روش می‌توان چندین مخزن حالت را نیز داشت؛ اما هر کدام مستقل از هم عمل می‌کنند. برای تکمیل آن فرض کنید قرار است عمل افزودن مقدار یک شمارشگر، در دو مخزن حالت متفاوت و مجزای از هم، در هر کدام سبب بروز تغییر حالتی خاص شود که در این مطلب روش مدیریت آن‌را بررسی خواهیم کرد.


نیاز به یک Dispatcher برای تعامل با بیش از یک مخزن حالت


در اینجا برای نمونه دو مخزن حالت تعریف شده‌اند؛ اما روش تعامل با این مخازن حالت، دیگر مانند قبل نیست. برای نمونه در اثر تعامل یک کاربر با View ای خاص، رخدادی صادر شده و اینبار مدیریت این رخداد توسط یک Action (که عموما یک پیام رشته‌ای است)، به Dispatcher مرکزی ارسال می‌شود (و نه مستقیما به مخزن حالت خاصی). اکنون این Dispatcher، اکشن رسیده را به مخازن کد مشترک به آن ارسال می‌کند تا عمل متناسب با آن اکشن درخواستی را انجام دهند. مابقی آن همانند قبل است که پس از تغییر حالت در هر کدام از مخازن حالت، کار به روز رسانی UI، در کامپوننت‌های مشترک صورت خواهد گرفت. بدیهی است در اینجا مخازن حالت، مجاز به صرفنظر کردن از یک اکشن خاص هستند و الزامی به پیاده سازی آن ندارند. هدف اصلی این است که اگر اکشنی قرار بود در تمام مخازن حالت پیاده سازی شود و حالت‌های آن‌ها را تغییر دهد، روشی را برای مدیریت آن داشته باشیم.
بنابراین اگر به این الگوی جدید دقت کنید، چیزی نیست بجز یک الگوی Observer دو سطحی:
الف) Dispatcher ای (Subject) که مشترک‌هایی را مانند مخازن حالت دارد (Observers).
ب) مخازن حالتی (Subjects) که مشترک‌هایی را مانند کامپوننت‌ها دارند (Observers).

اگر پیشتر با React کار کرده باشید، این الگو را تحت عناوینی مانند Flux و یا Redux می‌شناسید و در اینجا می‌خواهیم پیاده سازی #C آن‌را بررسی کنیم:


در الگوی Flux، در اثر تعامل یک کاربر با کامپوننتی، اکشنی به سمت یک Dispatcher ارسال می‌شود. سپس Dispatcher این اکشن را به مخزن حالتی جهت مدیریت آن ارسال می‌کند که در نهایت سبب تغییر حالت آن شده و به روز رسانی UI را در پی خواهد داشت.


پیاده سازی یک Dispatcher برای تعامل با بیش از یک مخزن حالت

پیش از هر کاری نیاز است قالب اکشن‌های ارسالی را که قرار است توسط مخازن حالت مورد پردازش قرار گیرند، مشخص کنیم:
namespace BlazorStateManagement.Stores
{
    public interface IAction
    {
        public string Name { get; }
    }
}
عموما هر اکشنی با نام و یا پیامی مشخص می‌شود. بر این اساس می‌توان اکشن افزودن و یا کاهش مقادیر شمارشگر را به صورت زیر تعریف کرد:
namespace BlazorStateManagement.Stores.CounterStore
{
    public class IncrementAction : IAction
    {
        public const string Increment = nameof(Increment);

        public string Name { get; } = Increment;
    }

    public class DecrementAction : IAction
    {
        public const string Decrement = nameof(Decrement);

        public string Name { get; } = Decrement;
    }
}
مزیت تعریف و استفاده از یک کلاس در اینجا این است که اگر نیاز بود به همراه اکشنی، اطلاعات اضافه‌تری نیز به سمت مخازن کد ارسال شوند، می‌توان آن‌ها را داخل هر کدام از کلاس‌ها، بسته به نیاز برنامه تعریف کرد و صرفا محدود به Name و یا یک مقدار رشته‌ای معرف آن، نخواهند بود.

پس از تعریف ساختار یک اکشن، اکنون نوبت به پیاده سازی راه حلی برای ارسال آن به تمام مخازن حالت برنامه است:
using System;

namespace BlazorStateManagement.Stores
{
    public interface IActionDispatcher
    {
        void Dispatch(IAction action);
        void Subscribe(Action<IAction> actionHandler);
        void Unsubscribe(Action<IAction> actionHandler);
    }

    public class ActionDispatcher : IActionDispatcher
    {
        private Action<IAction> _actionHandlers;

        public void Subscribe(Action<IAction> actionHandler) => _actionHandlers += actionHandler;

        public void Unsubscribe(Action<IAction> actionHandler) => _actionHandlers -= actionHandler;

        public void Dispatch(IAction action) => _actionHandlers?.Invoke(action);
    }
}
پیاده سازی ActionDispatcher ای را که ملاحظه می‌کنید، دقیقا مشابه CounterStore قسمت قبل است و در اینجا توسط متد Subscribe، مخازن حالت برنامه مشترک آن شده و یا توسط متد Unsubscribe، قطع اشتراک می‌کنند. همچنین متد Dispatch نیز شبیه به متد BroadcastStateChange قسمت قبل عمل می‌کند و سبب می‌شود تا اکشن ارسالی به آن، به تمام مشترکین این سرویس، ارسال شود.
این سرویس را نیز با طول عمر Scoped به سیستم تزریق وابستگی‌های برنامه معرفی می‌کنیم که سبب می‌شود تا پایان عمر برنامه (بسته شدن مرورگر یا ریفرش کامل صفحه‌ی جاری)، در حافظه باقی مانده و وهله سازی مجدد نشود. به همین جهت تزریق آن در مخازن حالت مختلف برنامه، دقیقا حالت یک Dispatcher اشتراکی را پیدا خواهد کرد.
namespace BlazorStateManagement.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IActionDispatcher, ActionDispatcher>();
            // ...
        }
    }
}


استفاده از IActionDispatcher در مخازن حالت برنامه

در ادامه می‌خواهیم مخازن حالت برنامه را تحت کنترل سرویس IActionDispatcher قرار دهیم تا کاربر بتواند اکشنی را به Dispatcher ارسال کند و سپس Dispatcher این درخواست را به تمام مخازن حالت موجود، جهت بروز واکنشی (در صورت نیاز)، اطلاعات رسانی نماید.
برای این منظور سرویس ICounterStore قسمت قبل ، به صورت زیر تغییر می‌کند که اینترفیس IDisposable را پیاده سازی کرده و همچنین دیگر به همراه متدهای عمومی افزایش و یا کاهش مقدار نیست:
using System;

namespace BlazorStateManagement.Stores.CounterStore
{
    public interface ICounterStore : IDisposable
    {
        CounterState State { get; }

        void AddStateChangeListener(Action listener);
        void BroadcastStateChange();
        void RemoveStateChangeListener(Action listener);
    }
}
بر این اساس، پیاده سازی CounterStore به صورت زیر تغییر خواهد کرد:
using System;

namespace BlazorStateManagement.Stores.CounterStore
{
    public class CounterStore : ICounterStore
    {
        private readonly CounterState _state = new();
        private bool _isDisposed;
        private Action _listeners;
        private readonly IActionDispatcher _actionDispatcher;

        public CounterStore(IActionDispatcher actionDispatcher)
        {
            _actionDispatcher = actionDispatcher ?? throw new ArgumentNullException(nameof(actionDispatcher));
            _actionDispatcher.Subscribe(HandleActions);
        }

        private void HandleActions(IAction action)
        {
            switch (action)
            {
                case IncrementAction:
                    IncrementCount();
                    break;
                case DecrementAction:
                    DecrementCount();
                    break;
            }
        }

        public CounterState State => _state;

        private void IncrementCount()
        {
            _state.Count++;
            BroadcastStateChange();
        }

        private void DecrementCount()
        {
            _state.Count--;
            BroadcastStateChange();
        }

        public void AddStateChangeListener(Action listener) => _listeners += listener;

        public void RemoveStateChangeListener(Action listener) => _listeners -= listener;

        public void BroadcastStateChange() => _listeners.Invoke();

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                try
                {
                    if (disposing)
                    {
                        _actionDispatcher.Unsubscribe(HandleActions);
                    }
                }
                finally
                {
                    _isDisposed = true;
                }
            }
        }
    }
}
توضیحات:
- با توجه به اینکه CounterStore یک سرویس ثبت شده‌ی در سیستم است، می‌تواند از مزیت تزریق سایر سرویس‌ها در سازنده‌ی خودش بهره‌مند شود؛ مانند تزریق سرویس جدید IActionDispatcher.
- پس از تزریق سرویس جدید IActionDispatcher، متدهای Subscribe آن‌را در سازنده‌ی کلاس و Unsubscribe آن‌را در حین Dispose سرویس، فراخوانی می‌کنیم. البته فراخوانی و یا پیاده سازی Unsubscribe و Dispose در اینجا غیرضروری است؛ چون طول عمر این کلاس با طول عمر برنامه یکی است.
- بر اساس این الگوی جدید، هر اکشنی که به سمت Dispatcher مرکزی ارسال می‌شود، در نهایت به متد HandleActions یکی از مخازن حالت تعریف شده، خواهد رسید:
        private void HandleActions(IAction action)
        {
            switch (action)
            {
                case IncrementAction:
                    IncrementCount();
                    break;
                case DecrementAction:
                    DecrementCount();
                    break;
            }
        }
در اینجا می‌توان با استفاده از patterns matching، بر اساس نوع اکشن مدنظر، عملیات خاصی را انجام داد. فقط در اینجا دیگر متدهای IncrementCount و DecrementCount، عمومی نیستند. به همین جهت باید به کامپوننت شمارشگر مراجعه کرد و تعریف قبلی:
@inject ICounterStore CounterStore

@code {

    private void IncrementCount()
    {
        CounterStore.IncrementCount();
    }
را به صورت زیر تغییر داد:
- ابتدا در انتهای فایل Client\_Imports.razor، فضای نام سرویس جدید IActionDispatcher را اضافه می‌کنیم:
@using BlazorStateManagement.Stores
- سپس از آن جهت ارسال IncrementAction به مخازن حالت برنامه استفاده خواهیم کرد:
// ...
@inject IActionDispatcher ActionDispatcher


@code {

    private void IncrementCount()
    {
        ActionDispatcher.Dispatch(new IncrementAction());
    }
با این تغییر جدید، هربار که بر روی دکمه‌ی افزایش مقدار شمارشگر، کلیک می‌شود، در آخر یک IncrementAction به تمام مخازن حالت موجود در برنامه ارسال خواهد شد و آن‌ها بر اساس نیازشان تصمیم خواهند گرفت که آیا به آن واکنش نشان دهند یا خیر.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorStateManagement-Part-2.zip
نظرات مطالب
Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها
یک راه حل جایگزین برای ارسال پیغام‌های خطاهای حاصل از اجرای قواعد تجاری خاص در لایه‌های داخلی

در مطلب طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها راه حلی برای خودکار سازی فرآیند اعتبارسنجی‌های خصوصیات مرتبط با DTO ها، ارائه شد. در برخی از سناریوها در میان منطق تجاری نیاز است یکسری قواعد هم بررسی شوند و داده مورد نیاز این بررسی در دل پیاده سازی متد مورد نظر وجود دارد و نیازی نیست تا برای بررسی این نوع قواعد، این داده را در Validator مرتبط با DTO نیز واکشی کنیم. در این مواقع یک راه حل همان صدور استثناء می‌باشد البته با یک trade off مناسب؛ راه حل بعدی آن همانطور که در مطلب جاری هم به آن اشاره شد، بحث بازگشت OperationResult می‌باشد. یک راه حل دیگر آن نیز به شکل زیر می‌باشد:
واسط IValidationDictionary برای کپسوله کردن امکانات ModelState
public interface IValidationDictionary
{
    void AddError(string key, string message);
    bool IsValid { get; }
}
کلاس ModelStateWrapper 
public class ModelStateWrapper : IValidationDictionary
{
    private ModelStateDictionary _modelState;

    public ModelStateWrapper(ModelStateDictionary modelState)
    {
        _modelState = modelState;
    }

    public void AddError(string key, string errorMessage)
    {
        _modelState.AddModelError(key, errorMessage);
    }

    public bool IsValid
    {
        get { return _modelState.IsValid; }
    }
}
واسط IApplicationService
public interface IApplicationService
{
    void Initialize(IValidationDictionary validationDictionary);
}
واسط مرتبط با سرویس موجودیت User
public interface IUserService : IApplicationService
{
    Task CreateAsync(UserCreateModel model);
}

پیاده ساز واسط IUserService
public class UserService : IUserService
{
    private readonly IUnitOfWork _uow;
    private IValidationDictionary _validationDictionary;

    public UserService(IUnitOfWork uow)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));
    }

    public void Initialize(IValidationDictionary validationDictionary)
    {
        _validationDictionary = validationDictionary ?? throw new ArgumentNullException(nameof(validationDictionary));
    }

    public Task CreateAsync(UserCreateModel model)
    {
        //todo: logic for create new user

        if (condition)
            _validationDictionary.AddError(string.Empty, "پیغامی که قرار است برای کاربرنهایی قابل مشاهده باشد");

        if (other condition)
            _validationDictionary.AddError(string.Empty, "پیغامی که قرار است برای کاربرنهایی قابل مشاهده باشد");
    }
}

تزریق واسط سرویس کاربران و استفاده از آن
public class UsersController : Controller
{
    private readonly IUserService _service;

    public UsersController(IUserService service)
    {
        _service = service ?? throw new ArgumentNullException(nameof(service));
        _service.Initialize(new ModelStateWrapper(ModelState));
    }

    [HttpPost]
    public async Task<IActionResult> Create([FromForm]UserCreateModel model)
    {
        if (!ModelState.IsValid) return View(model);

        await _service.CreateAsync(model);

        //todo: Display ModelState's Errors
    }
}

 نکته اضافی تکه کد بالا، فراخوانی متد Initialize مرتبط با ApplicationService مورد نظر می‌باشد برای ارسال پیاده سازی از IValidationDictionary به آن.
این بار بعد از اجرای متد CreateAsync اگر خطایی اعتبارسنجی حاصل از اجرای قواعد تجاری موجود باشد، می‌توان از طریق ModelState به آن رسید.
نظرات مطالب
طراحی گزارش در Stimulsoft Reports.Net – بخش 2

سلام

گزارش مستر دیتیل را در Ef برای پوکوهای virstul collection<detail> details چه طوری پیاده سازی باید کرد؟

برای مثال :

public class Master (){
   public int Id {get; set;}
   public string Name {get; set;}
   public Icollection<Detail>Details {get; st;}
}

public Class detail(){
  public int code {get; set;}
  public string datadetail {get; set;}
}

اگر یک IEnumerable<Master> d داشته باشیم. مستر دیتایل و اتصال اونها به stimulsoft چطوری انجام میشه؟

نظرات مطالب
EF Code First #7
به ازای هر FK یک Collection مجزا باید داشته باشید و همچنین از Fluent API هم باید استفاده کنید:
public class User
{
    public int UserId { get; set;} 
    public string Name { get; set; }

    public virtual ICollection<Comment> HomeCommentes { get; set; }
    public virtual ICollection<Comment> AwayCommentes { get; set; }
}

public class Comment
{
    public int CommentId { get; set; }

    public int HomeUserId { get; set; }
    public int GuestUserId { get; set; }

    public virtual User HomeUser { get; set; }
    public virtual User GuestUser { get; set; }
}


public class Context : DbContext
{
    ...

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Comment>()
                    .HasRequired(m => m.HomeUser)
                    .WithMany(t => t.HomeCommentes)
                    .HasForeignKey(m => m.HomeUserId)
                    .WillCascadeOnDelete(false);

        modelBuilder.Entity<Comment>()
                    .HasRequired(m => m.GuestUser)
                    .WithMany(t => t.AwayCommentes)
                    .HasForeignKey(m => m.GuestUserId)
                    .WillCascadeOnDelete(false);
    }
}