مطالب
بهبود کارآیی استفاده از JSON در دات نت 6 با معرفی Source generators آن
دات نت 6 به همراه source generator‌های توکاری است که می‌توانند کار serialization و deserialization نوع JSON را با کارآیی بسیار بیشتری انجام دهند؛ با آزمایش‌هایی که این بهبود را در حد 40 درصد سریعتر نسبت به حالت متداول آن نمایش می‌دهند و ... این مساله بسیار مهم است. از این جهت که این روزها، JSON را در همه‌جا مشاهده می‌کنیم؛ در Web APIها، در تنظیمات برنامه‌ها، در ارسال پیام‌ها بین برنامه‌ها و غیره. بنابراین هرگونه بهبودی در زمینه‌ی کارآیی serialization و deserialization آن، تاثیر بسیار قابل ملاحظه‌ای را بر روی کارآیی کلی یک برنامه بجا خواهد گذاشت.


System.Text.Json source generator چیست؟

پا‌یه‌ی تمام اعمال serialization و deserialization در دات نت، استفاده از Reflection است که در زمینه‌ی ارائه‌ی برنامه‌هایی با کارآیی بالا و با مصرف حافظه‌ی پایین، بهینه عمل نمی‌کند. راه‌حل جایگزین استفاده از Reflection که در زمان اجرای برنامه رخ می‌دهد، به همراه دات نت 5 ارائه شد و source generators نام دارد. Source generators امکان تولید فایل‌های #C را در زمان کامپایل برنامه میسر می‌کنند که نسبت به راه‌حل Reflection که در زمان اجرای برنامه فعال می‌شود، کارآیی بسیار بیشتری را ارائه می‌کنند. برای مثال به همراه دات نت 6، علاوه بر روش پیش‌فرض مبتنی بر Reflection ارائه شده‌ی توسط System.Text.Json، راه حل جدید امکان استفاده‌ی از source generators توکار آن نیز پیش بینی شده‌است. کار اصلی آن، انجام تمام مراحلی است که پیشتر توسط Reflection در زمان اجرای برنامه صورت می‌گرفت، اینبار در زمان کامپایل برنامه و ارائه‌ی آن به صورت از پیش آماده شده و مهیا.
مزایای این روش شامل موارد زیر است:
- بالا رفتن سرعت برنامه
- کاهش زمان آغاز اولیه‌ی برنامه
- کاهش میزان حافظه‌ی مورد نیاز برنامه
- عدم نیاز به استفاده‌ی از System.Reflection و System.Reflection.Emit
- ارائه‌ی Trim-compatible serialization که سبب کاهش اندازه‌ی نهایی برنامه می‌شود. برای مثال در برنامه‌های Blazor می‌توان با فعالسازی Trimming، کدهای استفاده نشده را از فایل‌های بایناری نهایی حذف کرد. استفاده از source generators، با این روش سازگاری کاملی دارد.



مثالی از نحوه‌ی کار با JSON در دات نت 6، توسط source generators آن

فرض کنید قصد داریم اعمال serialization و deserialization از نوع JSON را بر روی نمونه‌های کلاس زیر انجام دهیم:
namespace Test
{
    internal class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
اولین کاری که در این زمینه باید انجام شود، ایجاد یک کلاس خالی، با نامی دلخواه، اما مشتق شده‌ی از JsonSerializerContext است. در این حالت اخطارهایی را در IDE خود مبتنی بر نیاز به پیاده سازی تعدادی از متدهای این کلاس پایه دریافت می‌کنیم. اما ... ما قصد نداریم این متدها را پیاده سازی کنیم؛ Source generator قرار است اینکار را انجام دهد. به همین جهت این کلاس را partial تعریف کرده (تا source generator بتواند آن‌را در فایلی دیگر تکمیل کند) و همچنین آن‌را مزین به ویژگی JsonSerializable از نوع کلاسی که می‌خواهیم آن‌را serialize کنیم، خواهیم کرد تا سبب فعال شدن source generator بر روی این کلاس شویم:
using System.Text.Json.Serialization;

namespace Test
{
    [JsonSerializable(typeof(Person))]
    internal partial class MyJsonContext : JsonSerializerContext
    {
    }
}
و ... همین! کدهای این کلاس partial توسط source generator در زمان کامپایل برنامه به صورت خودکار تولید و تکمیل می‌شوند.
پس از آن فقط کافی است MyJsonContext را به عنوان پارامتر متدهای جدید Serialize و یا Deserialize، به صورت زیر ارسال کنیم تا از آن استفاده شود:
Person person = new() { FirstName = "Jane", LastName = "Doe" };
byte[] utf8Json = JsonSerializer.SerializeToUtf8Bytes(person, MyJsonContext.Default.Person);
person = JsonSerializer.Deserialize(utf8Json, MyJsonContext.Default.Person);

متدهای جدید این API مبتنی بر source generators را در ادامه ملاحظه می‌کنید:
namespace System.Text.Json
{
    public static class JsonSerializer
    {
        public static object? Deserialize(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerContext context) => ...;
        public static object? Deserialize(ReadOnlySpan<char> json, Type returnType, JsonSerializerContext context) => ...;
        public static object? Deserialize(string json, Type returnType, JsonSerializerContext context) => ...;
        public static object? Deserialize(ref Utf8JsonReader reader, Type returnType, JsonSerializerContext context) => ...;
        public static ValueTask<object?> DeserializeAsync(Stream utf8Json, Type returnType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static TValue? Deserialize<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static TValue? Deserialize<TValue>(string json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static TValue? Deserialize<TValue>(ReadOnlySpan<char> json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static TValue? Deserialize<TValue>(ref Utf8JsonReader reader, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static string Serialize(object? value, Type inputType, JsonSerializerContext context) => ...;
        public static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerContext context) { }
        public static Task SerializeAsync(Stream utf8Json, object? value, Type inputType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task SerializeAsync<TValue>(Stream utf8Json, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static byte[] SerializeToUtf8Bytes(object? value, Type inputType, JsonSerializerContext context) => ...;
        public static byte[] SerializeToUtf8Bytes<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static void Serialize<TValue>(Utf8JsonWriter writer, TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { }
        public static string Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
    }
}


روش معرفی تنظیمات Serializer به Source generator

برای معرفی تنظیمات serialization و deserialization، برای مثال تهیه‌ی خروجی‌های CamelCase، می‌توان از ویژگی JsonSourceGenerationOptions به صورت زیر استفاده کرد:
using System.Text.Json.Serialization;

namespace Test
{
    [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
    [JsonSerializable(typeof(Person))]
    internal partial class MyJsonContext : JsonSerializerContext
    {
    }
}
در این حالت مابقی کدها مانند قبل باقی خواهند ماند:
string json = JsonSerializer.Serialize(person, MyJsonContext.Default.Person);
Person person = JsonSerializer.Deserialize(json, MyJsonContext.Default.Person);


روش استفاده از JSON Source generators در برنامه‌های ASP.NET Core

در این نوع برنامه‌ها، JsonSerializerContext‌ها را می‌توان توسط متد AddContext به صورت زیر به تنظیمات JSON برنامه معرفی کرد:
services.AddControllers().AddJsonOptions(options => options.AddContext<MyJsonContext>());


روش استفاده از JSON Source generators در برنامه‌های Blazor

البته در اینجا بیشتر منظور امکان استفاده‌ی از آن‌ها توسط HttpClient است که به صورت زیر توسط متد GetFromJsonAsync واقع در فضای نام System.Net.Http.Json، میسر شده‌است:
[JsonSerializable(typeof(WeatherForecast[]))]
internal partial class MyJsonContext : JsonSerializerContext { }

@code {
    private WeatherForecast[] forecasts;

    private static JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
    private static MyJsonContext Context = new MyJsonContext(Options);

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync("sample-data/weather.json", Context.WeatherForecastArray);
    }
}
لیست کامل‌تر این API جدید به صورت زیر است:
namespace System.Net.Http.Json
{
    public static partial class HttpClientJsonExtensions
    {
        public static Task<object?> GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<object?> GetFromJsonAsync(this HttpClient client, System.Uri? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
    }
    public static partial class HttpContentJsonExtensions
    {
        public static Task<object?> ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<T?> ReadFromJsonAsync<T>(this HttpContent content, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
    }
}
مطالب
بررسی روش مشاهده خروجی SQL حاصل از کوئری‌های Entity framework Core
هنوز تا Entity framework Core 1.1، مفهوم interceptors موجود در EF 6.x پیاده سازی نشده‌است. اما شبیه به مفاهیم «ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging»، در EF Core نیز زیرساختی جهت مشاهده‌ی SQL نهایی تولیدی وجود دارد.


ایجاد یک ثبت کننده‌ی وقایع EF Core

مرحله‌ی اول مشاهده‌ی خروجی‌های نهایی EF Core، پیاده سازی اینترفیس ILoggerProvider است که در آن قرار است وهله‌ی از نوع ILogger بازگشت داده شود. به همین جهت یک کلاس تو در توی خصوصی را در اینجا مشاهده می‌کنید که اینترفیس ILogger را نیز پیاده سازی کرده‌است:
using System;
using Microsoft.Extensions.Logging;

namespace Tests
{
    public class MyLoggerProvider : ILoggerProvider
    {
        public ILogger CreateLogger(string categoryName)
        {
            return new MyLogger();
        }

        public void Dispose()
        { }

        private class MyLogger : ILogger
        {
            public bool IsEnabled(LogLevel logLevel)
            {
                return true;
            }

            public void Log<TState>(
                        LogLevel logLevel, 
                        EventId eventId, 
                        TState state, 
                        Exception exception, 
                        Func<TState, Exception, string> formatter)
            {
                //File.AppendAllText(@"C:\temp\log.txt", formatter(state, exception));
                Console.WriteLine("");
                Console.WriteLine(formatter(state, exception));
            }

            public IDisposable BeginScope<TState>(TState state)
            {
                return null;
            }
        }
    }
}
در اینجا خروجی‌هایی نهایی توسط Console.WriteLine نمایش داده شده‌اند و مناسب برنامه‌های کنسول هستند و یا می‌توان برای مثال توسط File.AppendAllText، اطلاعات رسیده را در یک فایل نیز ذخیره کرد.
در متد Log:
- پارامتر logLevel، سطح اهمیت اطلاعات رسیده را به همراه دارد. برای مثال اطلاعات است یا خطا؟
برای مثال شاید نیاز به ذخیره سازی اطلاعاتی با سطح‌های بحرانی، خطا و یا اخطار در یک بانک اطلاعاتی وجود داشته باشد:
   if (logLevel == LogLevel.Critical || logLevel == LogLevel.Error || logLevel == LogLevel.Warning)
- eventId: نوع رخداد رسیده را مشخص می‌کند.
- state: می‌تواند هر نوع شیءایی، حاوی اطلاعات وضعیت رخداد رسیده باشد.
- exception: بیانگر استثنای احتمالی رخ داده است.
- formatter: کار آن تولید یک رشته‌ی قابل خواندن، توسط اطلاعات حالت و استثناء است.


معرفی Logger تهیه شده به برنامه

پس از تهیه‌ی Logger فوق، جهت معرفی آن به یک برنامه‌ی کنسول، می‌توان به صورت ذیل عمل کرد:
using (var db = new MyContext())
{
   var loggerFactory = (ILoggerFactory)db.GetInfrastructure().GetService(typeof(ILoggerFactory));
   loggerFactory.AddProvider(new MyLoggerProvider());
 }
این ثبت تنها باید یکبار در آغاز برنامه انجام شود و پس از آن تمام وهله‌ی دیگر Context از آن استفاده خواهند کرد.

در برنامه‌های ASP.NET Core، کار معرفی MyLoggerProvider در متد Configure کلاس آغازین برنامه انجام می‌شود:
public void Configure(ILoggerFactory loggerFactory)
{
   loggerFactory.AddProvider(new MyLoggerProvider());


اختصاصی سازی ثبت وقایع رسیده

کلاس MyLoggerProvider، هر نوع اطلاعات داخلی EF Core را نیز لاگ می‌کند. اگر هدف صرفا بررسی خروجی SQL نهایی تولیدی است، می‌توان در متد ذیل:
public ILogger CreateLogger(string categoryName)
بر اساس categoryName رسیده، یا new MyLogger را بازگشت داده و یا یک NullLogger.Instance را که کاری را انجام نمی‌دهد. به این ترتیب می‌توان کار فیلتر کردن اطلاعات رسیده را انجام داد.
برای این منظور، ابتدای Logger تهیه شده چنین شکلی را پیدا می‌کند:
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging;
using System.Linq;
using System;
 
namespace Tests
{
    public class MyLoggerProvider : ILoggerProvider
    {
        private static readonly string[] _categories =
            {
                typeof(RelationalCommandBuilderFactory).FullName,
                typeof(SqlServerConnection).FullName
            };
        public ILogger CreateLogger(string categoryName)
        {
            if (_categories.Contains(categoryName))
            {
                return new MyLogger();
            }
 
            return NullLogger.Instance;
        }
نظرات مطالب
EF Code First #3
فرض کنید به یکی از خاصیت‌های کلاس، متادیتای   DatabaseGeneratedOption.Computed داده ایم. اما سوال اینجاست که چگونه می‌توانیم فرمول t-Sql مربوط به اون فیلد یا مقدار default value مربوط به اون فیلد را در  EF به دیتابیس معرفی کنیم؟
مطالب
MVVM و امکان استفاده از یک وهله از ViewModel جهت چند View مرتبط

عموما هنگام طراحی یک View، خیلی زود به حجم انبوهی از کدهای XAML خواهیم رسید. در ادامه بررسی خواهیم کرد که چطور می‌توان یک View را به چندین View خرد کرد، بدون اینکه نیازی باشد تا از چندین ViewModel (یا همان code behind عاری از ارجاعات بصری سابق قرار گرفته در یک پروژه جدای دیگر) استفاده شود و تمام این View های خرد شده هم تنها از یک وهله از ViewModel ایی خاص استفاده کنند و با اطلاعاتی یکپارچه سروکار داشته باشند؛ یا در عمل یکپارچه کار کنند.
این مشکل از جایی شروع می‌شود که مثلا خرد کردن یک user control به چند یوزر کنترل، یعنی کار کردن با چند وهله از اشیایی متفاوت. هر چند نهایتا تمام این‌ها قرار است در یک صفحه در کنار هم قرار گیرند اما در عمل از هم کاملا مجزا هستند و اگر به ازای هر کدام یکبار ViewModel را وهله سازی کنیم، به مشکل برخواهیم خورد؛ چون هر وهله نسبت به وهله‌ای دیگر ایزوله است. اگر در یکی Name مثلا Test بود در دیگری ممکن است مقدار پیش فرض نال را داشته باشد؛ چون با چند وهله از یک کلاس، در یک فرم نهایی سروکار خواهیم داشت.

ابتدا Model و ViewModel ساده زیر را در نظر بگیرید:
using System.ComponentModel;

namespace SplittingViewsInMvvm.Models
{
public class GuiModel : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
raisePropertyChanged("Name");
}
}

string _lastName;
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
raisePropertyChanged("LastName");
}
}

#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

using SplittingViewsInMvvm.Models;

namespace SplittingViewsInMvvm.ViewModels
{
public class MainViewModel
{
public GuiModel GuiModelData { set; get; }

public MainViewModel()
{
GuiModelData = new GuiModel();
GuiModelData.Name = "Name";
GuiModelData.LastName = "LastName";
}
}
}

سپس View زیر هم از این اطلاعات استفاده خواهد کرد:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<GroupBox Margin="2" Header="Group 1">
<TextBlock Text="{Binding GuiModelData.Name}" />
</GroupBox>
<GroupBox Margin="2" Header="Group 2">
<TextBlock Text="{Binding GuiModelData.LastName}" />
</GroupBox>
</StackPanel>
</UserControl>

اکنون فرض کنید که می‌خواهیم Group 1 و Group 2 را جهت مدیریت ساده‌تر View اصلی در دو user control مجزا قرار دهیم؛ مثلا:

<UserControl x:Class="SplittingViewsInMvvm.Views.Group1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<GroupBox Margin="2" Header="Group 1">
<TextBlock Text="{Binding GuiModelData.Name}" />
</GroupBox>
</Grid>
</UserControl>
و
<UserControl x:Class="SplittingViewsInMvvm.Views.Group2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<GroupBox Margin="2" Header="Group 2">
<TextBlock Text="{Binding GuiModelData.LastName}" />
</GroupBox>
</Grid>
</UserControl>

اکنون اگر این دو را مجددا در همان View اصلی ساده شده قبلی قرار دهیم (بدون اینکه در هر user control به صورت جداگانه data context را تنظیم کنیم):
<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<V:Group1 />
<V:Group2 />
</StackPanel>
</UserControl>

باز هم .... برنامه همانند سابق کار خواهد کرد و ViewModel وهله سازی شده در user control فوق به صورت یکسانی در اختیار هر دو View اضافه شده قرار می‌گیرد و نهایتا یک View یکپارچه را در زمان اجرا می‌توان مورد استفاده قرار داد. علت هم بر می‌گردد به مقدار دهی خودکار DataContext هر View اضافه شده به بالاترین DataContext موجود در Visual tree که ذکر آن الزامی نیست:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<V:Group1 DataContext="{Binding}" />
<V:Group2 DataContext="{Binding}"/>
</StackPanel>
</UserControl>


بنابراین به صورت خلاصه زمانیکه از MVVM استفاده ‌می‌کنید لازم نیست کار خاصی را جهت خرد کردن یک View به چند Sub View انجام دهید! فقط این‌ها را در چند User control جدا کنید و بعد مجددا به کمک فضای نامی که تعریف خواهد (مثلا V در اینجا) در همان View اصلی تعریف کنید. بدون هیچ تغییر خاصی باز هم برنامه همانند سابق کار خواهد کرد.


نظرات مطالب
EF Code First #9
 public class Person:BaseEntity
    {
        public int PersonId { get; set; }        
        [StringLength(100)]
        public string Title { get; set; }
        public PersonType PersonType { get; set; }
        public virtual ICollection<PrivacyPolicy> PrivacyPolicies { get; set; }


        public override string ToString()
        {
            return Title;
        }
    }
کلاس دوم
  [Table("Organs")]
    public class Organ:Person
    {
        [StringLength(100)]
        public string FullName { get; set; }
        [StringLength(1000)]
        public string Address { get; set; }

        public override string ToString()
        {
            return FullName;
        }
        public Organ()
        {
            PersonType = PersonType.Organ;
        }
    }
تو این مثال از Discriminator استفاده کرده ولی بعضی وقتها کوئری‌ها به این شکل ایجاد میکنه
CASE WHEN (( NOT (([UnionAll2].[C7] = 1) AND ([UnionAll2].[C7] IS NOT NULL))) AND ( NOT (([UnionAll2].[C8] = 1) AND ([UnionAll2].[C8] IS NOT NULL))) AND ( NOT (([UnionAll2].[C9] = 1) AND ([UnionAll2].[C9] IS NOT NULL)))) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll2].[C9] = 1) AND ([UnionAll2].[C9] IS NOT NULL)) THEN [UnionAll2].[C2] WHEN (([UnionAll2].[C8] = 1) AND ([UnionAll2].[C8] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) END AS [C2], 
CASE WHEN (( NOT (([UnionAll2].[C7] = 1) AND ([UnionAll2].[C7] IS NOT NULL))) AND ( NOT (([UnionAll2].[C8] = 1) AND ([UnionAll2].[C8] IS NOT NULL))) AND ( NOT (([UnionAll2].[C9] = 1) AND ([UnionAll2].[C9] IS NOT NULL)))) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll2].[C9] = 1) AND ([UnionAll2].[C9] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll2].[C8] = 1) AND ([UnionAll2].[C8] IS NOT NULL)) THEN [UnionAll2].[C3] END AS [C3], 
CASE WHEN (( NOT (([UnionAll2].[C7] = 1) AND ([UnionAll2].[C7] IS NOT NULL))) AND ( NOT (([UnionAll2].[C8] = 1) AND ([UnionAll2].[C8] IS NOT NULL))) AND ( NOT (([UnionAll2].[C9] = 1) AND ([UnionAll2].[C9] IS NOT NULL)))) THEN CAST(NULL AS int) WHEN (([UnionAll2].[C9] = 1) AND ([UnionAll2].[C9] IS NOT NULL)) THEN CAST(NULL AS int) WHEN (([UnionAll2].[C8] = 1) AND ([UnionAll2].[C8] IS NOT NULL)) THEN [UnionAll2].[C4] END AS [C4],
زمان اجرای کوئری پایین هست ، ولی حجم کد تولید شده بالاهست.
مطالب
فعال سازی سطح دوم کش در Fluent NHibernate

سطح اول کش در NHibernate در یک تراکنش معنا پیدا می‌کند (+)؛ اما نتایج حاصل از اعمال سطح دوم (+) آن، در اختیار تمام تراکنش‌های جاری برنامه خواهند بود. در ادامه قصد داریم نحوه فعال سازی سطح دوم کش NHibernate را توسط Fluent NHibernate بررسی کنیم.

الف) دریافت کش پروایدر
برای این منظور به صفحه اصلی آن در سایت سورس فورج مراجعه نمائید(+). اگر به علت تحریم‌ها امکان دریافت فایل‌های مرتبط را نداشتید از این برنامه استفاده کنید(+). پس از دریافت، می‌خواهیم نحوه فعال سازی NHibernate.Caches.SysCache.dll را بررسی کنیم (این اسمبلی، در برنامه‌های وب و دسکتاپ بدون مشکل کار می‌کند).

ب) اعمال به قسمت تعاریف اولیه
پس از دریافت اسمبلی NHibernate.Caches.SysCache.dll و افزودن ارجاعی به آن، اکنون نوبت به معرفی آن به تنظیمات Fluent NHibernate‌ می‌باشد. این‌کار هم بسیار ساده است:
...
.ConnectionString(x => x.FromConnectionStringWithKey(...))
.Cache(x => x.UseQueryCache()
.UseMinimalPuts()
.ProviderClass<NHibernate.Caches.SysCache.SysCacheProvider>())
...

ج) تعریف نوع کش در هنگام ایجاد نگاشت‌ها
اگر از ClassMap‌ها برای تعریف نگاشت‌ها استفاده می‌کنید، در انتهای تعاریف یک سطر Cache.ReadWrite را اضافه کنید.
اگر از AutoMapping استفاده می‌کنید، نیاز است تا با استفاده از IAutoMappingOverride (+) سطر یاد شده اضافه گردد؛ برای مثال:
using FluentNHibernate.Automapping.Alterations;

namespace NH3Test.MappingDefinitions.Domain
{
public class AccountOverrides : IAutoMappingOverride<Account>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Account> mapping)
{
mapping.Cache.ReadWrite();
}
}
}
تعریف یک سطر فوق هم مهم است؛ زیرا در غیراینصورت فقط primary key حاصل از بار اول فراخوانی کوئری‌های مرتبط کش می‌شوند؛ نه نتیجه عملیات. هرچند این مورد هم یک قدم مثبت به شمار می‌رود از این لحاظ که برای مثال تهیه نتایج کوئری بر روی فیلدی که ایندکس بر روی آن تعریف نشده است همیشه از حالت تهیه کوئری بر روی فیلد دارای ایندکس کندتر است. اما هدف ما در اینجا این است که پس از بار اول فراخوانی کوئری، بار‌های دوم و بعدی دیگر کوئری خاصی را به بانک اطلاعاتی ارسال نکرده و نتایج از کش خوانده شوند (جهت استفاده عموم کاربران در کلیه تراکنش‌های جاری برنامه).

د) اعمال متد Cacheable به کوئر‌ی‌ها
سه مرحله قبل نحوه برپایی مقدماتی سطح دوم کش را بیان می‌کنند و تنها یکبار نیاز است انجام شوند. در ادامه هر جایی که نیاز داشتیم نتایج کوئری مورد نظر کش شوند (و باید دقت داشت که این کش شدن سطح دوم به معنی در دسترس بودن نتایج آن جهت تمام کاربران برنامه در تمام تراکنش‌های جاری برنامه هستند؛ برای مثال نتایج آمار سایت که دسترسی عمومی دارد) تنها کافی است متد Cacheable را به کوئری مورد نظر اضافه کرد؛ برای مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();

ه) چگونه صحت اعمال سطح دوم کش را بررسی کنیم؟
برای بررسی این امر باید به خروجی SQL نهایی مراجعه کرد (+). سه تراکنش مجزا را تعریف کنید. در تراکنش اول یک insert ساده، در تراکنش دوم کوئری از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراکنش سوم مجددا همان کوئری تراکنش دوم را (به همراه اعمال متد Cacheable) تکرار کنید. حاصل کار تنها باید دو عبارت SQL باشند. یک مورد جهت insert و یک مورد هم select . در تراکنش سوم، از نتایج کش شده تراکنش دوم استفاده خواهد شد؛ به همین جهت دیگری کوئری سومی به بانک اطلاعاتی ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش کنید، سه کوئری را مشاهده خواهید کرد، اما کوئری سوم با کوئری دوم اندکی متفاوت خواهد بود و بهینه‌تر؛ چون به صورت هوشمند بر اساس جستجوی بر روی primary key تغییر کرده است (صرفنظر از اینکه قسمت where کوئری شما چیست).

مطالب
آیا دیتابیس مورد استفاده در NHibernate با نگاشت‌های تعریف شده همخوانی دارد؟

زمانیکه خاصیتی به یکی از کلاس‌های نگاشت‌های تعریف شده اضافه می‌شود یا حذف می‌گردد، دقیقا باید این به روز رسانی در سمت بانک اطلاعاتی هم انجام شود. امکان تهیه و همچنین اعمال اسکریپت نهایی تولید database schema مهیا است، اما ممکن است به هر علتی این کار فراموش شود. اکنون سؤال این است که آیا می‌توان سریع بررسی کرد که دیتابیس مورد استفاده با نگاشت‌های برنامه همخوانی و تطابق دارد؟
جهت پاسخ به این سؤال بهترین راه ایجاد یک کوئری Select بر اساس تمام خواص تعریف شده در یک کلاس است. اگر یکی از خواص یا حتی خود جدول وجود نداشته باشد، انجام این کوئری خودبخود با شکست مواجه شده و یک استثناء صادر خواهد شد. همین ایده را به سادگی می‌توان با NHibernate هم پیاده سازی کرد:
public class ConfirmDatabaseMatchesMappings
{
public static void ValidateDatabaseSchemaAgainstMappings()
{
//در اینجا باید سشن فکتوری سراسری تعریف شده را دریافت و استفاده کرد
using (var session = sessionManager.OpenSession())
{
var allClassMetadata = session.SessionFactory.GetAllClassMetadata();

foreach (var entry in allClassMetadata)
{
session.CreateCriteria(entry.Value.GetMappedClass(EntityMode.Poco))
.SetMaxResults(0).List();
}
}
}
}
برای مثال اگر فیلدی در کلاس‌های برنامه موجود باشد اما در بانک اطلاعاتی خیر، استثنای حاصل شبیه به عبارات ذیل خواهد بود:
NHibernate.Exceptions.GenericADOException was unhandled
Message=could not execute query
...
و اگر کمی سایر اطلاعات این استثناء را بررسی کنیم، به همان عبارات آشنای فلان فیلد یافت نشد یا فلان جدول وجود ندارد، ‌می‌رسیم.

مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت چهارم - اعتبارسنجی Async سمت کلاینت و یا همان Remote Client Side Validation
در قسمت قبل با نحوه‌ی پیاده سازی اعتبارسنجی‌های سفارشی سمت کلاینت مخصوص کتابخانه‌ی Fluent Validation آشنا شدیم. در این قسمت، یک حالت خاص همان نوع اعتبارسنجی‌های سمت کلاینت را که remote validation نام دارد، بررسی می‌کنیم. در این حالت خاص، نیازی به کدنویسی جاوااسکریپتی خاصی نیست. چون زیرساخت آن به همراه unobtrusive jQuery Ajax خود ASP.NET Core ارائه می‌شود. در اینجا فقط نیاز است تا متادیتای خاص آن‌را تولید کنیم. به عبارتی اینبار هدف ما تنها تولید یک چنین تگ HTML ای است:
<input dir="ltr" class="form-control input-validation-error" 
type="email" 
data-val="true" 
data-val-email="'آدرس ایمیل' is not a valid email address." 
data-val-remote="این آدرس ایمیل هم اکنون مورد استفاده‌است" 
data-val-remote-url="/Home/ValidateUniqueEmail" 
data-val-required="'آدرس ایمیل' must not be empty." 
id="Email" 
name="Email" 
>
که به همراه ویژگی‌های data-val ، data-val-remote و data-val-remote-url است. همینقدر که این سه ویژگی وجود داشته باشند، مابقی منطق اعتبارسنجی سمت کلاینت آن توسط unobtrusive jQuery Ajax (ارسال خودکار Ajax ای مقدار ایمیل، به سمت سرور و دریافت پاسخ) و unobtrusive java script validation مدیریت خواهند شد.


افزودن آدرس ایمیل به مدل کاربران

به همان مدل قسمت قبل، قصد داریم خاصیت آدرس ایمیل را هم اضافه کنیم:
using System.ComponentModel.DataAnnotations;

namespace FluentValidationSample.Models
{
    public class UserModel
    {
        [Display(Name = "نام کاربری")]
        public string Username { get; set; }

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

        [Display(Name = "سابقه کار")]
        public int Experience { get; set; }

        [DataType(DataType.EmailAddress)]
        [Display(Name = "آدرس ایمیل")]
        public string Email { get; set; }
    }
}


ایجاد سرویسی برای بررسی منحصربفرد بودن آدرس ایمیل

در ادامه قصد داریم سرویسی را ایجاد کنیم که برای مثال با بانک اطلاعاتی ارتباط برقرار کرده (در اینجا جهت سهولت ارائه، از یک آرایه استفاده شده‌است) و مشخص می‌کند که آیا ایمیل دریافتی پیشتر استفاده شده‌است یا خیر:
using System.Linq;

namespace FluentValidationSample.Services
{
    public interface IUsersService
    {
        bool IsUniqueEmail(string emailAddress);
    }

    public class UsersService : IUsersService
    {
        public bool IsUniqueEmail(string emailAddress)
        {
            string[] registedEmails = {
                "email@site.com",
                "test@gmail.com"
            };
            return !registedEmails.Contains(emailAddress);
        }
    }
}
این سرویس را هم با طول عمر Scoped به برنامه معرفی می‌کنیم؛ چون طول عمر سرویس‌هایی که با بانک اطلاعاتی و DbContext کار می‌کنند، به همین نحو تعیین می‌شوند:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUsersService, UsersService>();


ایجاد اعتبارسنج سمت سرور بررسی منحصربفرد بودن آدرس ایمیل

در ادامه یک PropertyValidator جدید را ایجاد می‌کنیم تا بتوان توسط آن مقدار ایمیل دریافتی را در سمت سرور، تعیین اعتبار کرد:
using FluentValidation.Validators;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public class UniqueEmailValidator : PropertyValidator
    {
        private readonly IUsersService _usersService;

        public UniqueEmailValidator(IUsersService usersService)
                : base("این آدرس ایمیل هم اکنون مورد استفاده‌است")
        {
            _usersService = usersService;
        }

        protected override bool IsValid(PropertyValidatorContext context)
        {
            return context.PropertyValue != null &&
                    _usersService.IsUniqueEmail((string)context.PropertyValue);
        }
    }
}
همانطور که مشاهده می‌کنید، در اینجا تزریق سرویس سفارشی IUsersService به سازنده‌ی کلاس اعتبارسنج، مجاز است و بدون مشکل کار می‌کند.
پس از آن جهت سهولت استفاده‌ی از آن، یک متد الحاقی جدید را نیز به نام UniqueEmail به نحو زیر تعریف می‌کنیم:
using FluentValidation;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public static class CustomValidatorExtensions
    {
        public static IRuleBuilderOptions<T, string> UniqueEmail<T>(
            this IRuleBuilder<T, string> ruleBuilder, IUsersService usersService)
        {
            return ruleBuilder.SetValidator(new UniqueEmailValidator(usersService));
        }
    }
}

یک نکته: اگر دقت کرده باشید، فضای نام این اعتبارسنج در این قسمت FluentValidationSample.ModelsValidations شده‌است:


علت اینجا است که اعتبارسنج تعریف شده نیاز دارد هم از مدل‌ها استفاده کند و هم از سرویس کاربران. سرویس کاربران هم از مدل‌ها استفاده می‌کند. به همین جهت اگر تعاریف اعتبارسنجی را داخل پروژه‌ی مدل‌ها قرار دهیم، یک وابستگی حلقوی رخ خواهد داد (وابستگی مدل‌ها به سرویس‌ها و برعکس). بنابراین بهتر است اعتبارسنج‌ها را به یک پروژه‌ی مجزا منتقل کنیم تا از بروز این cyclic dependency جلوگیری شود.


اعمال اعتبارسنجی منحصربفرد بودن ایمیل دریافتی به اعتبارسنج UserModel

پس از تهیه‌ی متد الحاقی UniqueEmail، آن‌را به RuleFor مخصوص خاصیت ایمیل اضافه می‌کنیم. در اینجا نیز تزریق وابستگی سرویس سفارشی IUsersService به سازنده‌ی کلاس اعتبارسنج مجاز است:
using FluentValidation;
using FluentValidationSample.Models;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public class UserModelValidator : AbstractValidator<UserModel>
    {
        public UserModelValidator(IUsersService usersService)
        {
            RuleFor(x => x.Username).NotNull();
            RuleFor(x => x.Age).NotNull();
            RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull();
            RuleFor(x => x.Email).EmailAddress().NotNull().UniqueEmail(usersService);
        }
    }
}


ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور

در ادامه نیاز است ویژگی‌های data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation;
using FluentValidation.AspNetCore;
using FluentValidation.Internal;
using FluentValidation.Resources;
using FluentValidation.Validators;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace FluentValidationSample.ModelsValidations
{
    public class RemoteClientValidator : ClientValidatorBase
    {
        public string RemoteUrl { set; get; }

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

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

        private string GetErrorMessage(ClientModelValidationContext context)
        {
            var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName());
            string messageTemplate;
            try
            {
                messageTemplate = Validator.Options.ErrorMessageSource.GetString(null);
            }
            catch (FluentValidationMessageFormatException)
            {
                messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>();
            }
            return formatter.BuildMessage(messageTemplate);
        }
    }
}
در این کدها، تنها قسمت مهم آن، متد AddValidation است که کار تعریف و افزودن متادیتاهای unobtrusive java script validation را انجام می‌دهد و برای مثال سبب رندر تگ HTML ای زیر می‌شود:
<input dir="ltr" class="form-control input-validation-error" 
type="email" 
data-val="true" 
data-val-email="'آدرس ایمیل' is not a valid email address." 
data-val-remote="این آدرس ایمیل هم اکنون مورد استفاده‌است" 
data-val-remote-url="/Home/ValidateUniqueEmail" 
data-val-required="'آدرس ایمیل' must not be empty." 
id="Email" 
name="Email" 
>
که به همراه ویژگی‌های data-val، data-val-remote و data-val-remote-url است تا unobtrusive jQuery Ajax validation را فعال کند.


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

پس از تعریف UniqueEmailValidator و RemoteClientValidator، روش افزودن آن‌ها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUsersService, UsersService>();

            services.AddControllersWithViews().AddFluentValidation(
                fv =>
                {
                    fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
                    fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>();
                    fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;

                    fv.ConfigureClientsideValidation(clientSideValidation =>
                    {
                        // ...

                        clientSideValidation.Add(
                            validatorType: typeof(UniqueEmailValidator),
                            factory: (context, rule, validator) =>
                                        new RemoteClientValidator(rule, validator)
                                        {
                                            RemoteUrl = "/Home/ValidateUniqueEmail"
                                        });
                    });
                }
            );
        }
در اینجا یک RemoteUrl را هم مشاهده می‌کنید که به صورت زیر باید تعریف شود:
namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IUsersService _usersService;

        public HomeController(IUsersService usersService)
        {
            _usersService = usersService;
        }

         // ...

        public IActionResult ValidateUniqueEmail(string email)
        {
            return Ok(_usersService.IsUniqueEmail(email));
        }
    }
}
زمانیکه اعتبارسنجی سمت کلاینت رخ می‌دهد، آدرس ایمیل، به اکشن متد فوق ارسال شده و یک true و یا false را دریافت می‌کند که بیانگر موفقیت آمیز بودن و یا شکست اعتبارسنجی از راه دور است.


تعریف کدهای جاوا اسکریپتی مورد نیاز

پیش از هرکاری، اسکریپت‌های فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
در اینجا مداخل jquery، سپس jquery.validate و بعد از آن jquery.validate.unobtrusive را مشاهده می‌کنید. در ادامه نیازی به تکمیل فایل js/site.js جهت افزودن کدهای remote client validation نیست و این کدها جزئی از کتابخانه‌ی jquery.validate.unobtrusive هستند.


آزمایش برنامه

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


برای آزمایش آن اگر برای مثال یکی از آدرس‌های ایمیل از پیش تعریف شده‌ی در متد IsUniqueEmail سرویس کاربران را وارد کنیم، با خطای اعتبارسنجی سمت کلاینت فوق روبرو خواهیم شد.


کدهای کامل این سری را تا این قسمت از اینجا می‌توانید دریافت کنید: FluentValidationSample-part04.zip
مطالب
اصل Command Query separation

در ادامه مطلب قبلی، یکی از مشکلاتی که طراحی Builder از آن رنج می‌برد، نقض کردن قانون command query separation است که در ادامه درباره‌ی این اصل بیشتر بحث خواهیم کرد.

اصل Command query separation یا به اختصار CQS، در کتاب Object-Oriented Software Construction توسط Bertrand Meyer معرفی شد‌ه‌است. بر اساس آن، عملیات‌های سیستم باید یا Command باشند و یا Query و نه هر دوی آن‌ها. وقتی یک کلاینت به امضای یک متد توجه می‌کند، اینکه این متد چه کاری را انجام میدهد Commands نام داشته و به شیء فرمان می‌دهد تا کاری را انجام بدهد. این عملیات وضعیت خود شیء و یا اشیاء دیگر را تغییر می‌دهد. در اینجا Queries به شیء فرمان می‌دهند تا نتیجه‌ی سؤال ( ویا درخواست) را برگرداند.

در آن سوی دیگر، متدهایی را که وضعیت شیء را تغییر می‌‌دهند، به عنوان Command در نظر میگیریم (بدون آنکه مقداری را برگردانند). اگر این نوع متدها، مقداری را برگردانند، باعث سردرگمی کلاینت می‌شوند؛ زیرا کلاینت نمی‌داند این متد باعث تغییر شیء شده‌است و یا Query؟

 همانطور که میدانیم، متد‌ها می‌توانند هر دو کار را با هم انجام دهند؛ یعنی مقداری را برگردانند و همچنین وضعیت شیء را تغییر دهند و همین مورد باعث سردرگمی و نقض می‌شود. وقتی متد‌های Command را از Query جدا میکنیم، ما را به سمت یک طراحی قابل فهم هدایت می‌کند. متدهایی که مقدار  void برمی گردانند، Command و سایر آنهایی که نوعی (type ) را برمی‌گردانند، Query هستند.
به کد زیر توجه فرمایید:
public class FileStore
    {
        public string WorkingDirectory { get; set; }

        public string Save(int id, string message)
        {
            var path = Path.Combine(this.WorkingDirectory + id + ".txt");
            File.WriteAllText(path, message);
            return path;
        }

        public event EventHandler<MessageEventArgs> MessageRead;

        public void Read(int id)
        {
            var path = Path.Combine(this.WorkingDirectory + id + ".txt");
            var msg = File.ReadAllText(path);
            this.MessageRead(this, new MessageEventArgs { Message = msg });    
        }
    }
اولین مشکلی که در طراحی این کلاس وجود مربوط به متد Read است؛ زیرا این متد void برمی‌گرداند. پس درنتیجه از نوع Command است. ولی اگر بیشتر به این متد توجه فرمایید احساس خواهید کرد که متد Read باید به صورت Query باشد. زیرا این متد قرار بوده مقداری را برگرداند؛ ولی اینجا به صورت void پیاده سازی شده‌است. در عوض  متد Save به صورت Query پیاده سازی شده است.
برای حل این مشکل کافی است تا امضای متد Read را به این صورت تغییر دهیم:
 public string Read(int id)
 {
     var path = Path.Combine(this.WorkingDirectory + id + ".txt");
     var msg = File.ReadAllText(path);
     this.MessageRead(this, new MessageEventArgs { Message = msg });
     return msg;
  }
خوب؛ اولین سوالی که پیش می‌آید این است که آیا این Query چیزی را تغییر می‌دهد؟ (تغییر شیء یا اشیایی دیگر) 
در ادامه متوجه خواهید شد این کد باعث فراخواندن یک event می‌شود. حالا آیا این event از نوع Command است یا Query؟ از نوع Command است؛ چون EventHandler  مانند متد‌هایی هستند که مقدار void را بر می‌گردانند و همانطور که میدانید، متدهایی که مقدار void را بر می‌گردانند، از نوع Command میباشند که وضعیت شیء را تغییر می‌دهند و برای اینکه از اصل CQS پیروی کنیم، باید این event را حذف کنیم تا متد Read از نوع Query باشد.
اگر به امضای متد Save  دقت کنید، به صورت یک Query است. ولی اگر به پیاده سازی آن دقت کنید، بیشتر شبیه به یک Command است تا یک Query و مهمترین ویژگی یک Command این است که مقدار void را بر می‌گرداند و برای حل این مشکل، متد Save را به صورت زیر تغییر می‌دهیم:
public void Save(int id, string message)
{
    var path = Path.Combine(this.WorkingDirectory + id + ".txt");
    File.WriteAllText(path, message);
}
همانطور که متوجه شدید، با این تغییر دیگر ما دسترسی به  مقدار path نخواهیم داشت و شاید مقدار path برای کلاینت مهم باشد. برای حل این مشکل متد جدیدی را به نام GetFileName به کلاس اضافه می‌کنیم؛ تا کلاینت به مقدار Path دسترسی داشته باشد. توجه داشته باشید که امضای متد GetFileName به صورت query پیاده سازی شده‌است.
public class FileStore
    {
        public string WorkingDirectory { get; set; }

        public void Save(int id, string message)
        {
            var path = GetFileName(id);  //ok to query from Command
            File.WriteAllText(path, message);            
        }

        public string Read(int id)
        {
            var path = GetFileName(id);
            var msg = File.ReadAllText(path);
            return msg;
        }
     
        public string GetFileName(int id)
        {
            return Path.Combine(this.WorkingDirectory , id + ".txt");     
        }
    }
تنها نکته‌ای که در اینجا بد نیست به آن اشاره کنیم این است که متدهایی که از نوع command هستند، می‌توانند بدون هیچگونه مشکلی متد‌های query را فراخوانی کنند. زیرا مهمترین ویژگی query‌ها این هستند که وضعیت شیء را تغییر نمی‌دهند و در نتیجه در هر بار فراخوانی، همان نتیجه را بازگشت می‌دهند.

چکیده:

هدف اصلی از طراحی نرم افزار، غالب شدن بر پیچیدگی‌ها می‌باشد. اصل CQS متد‌ها را به دو دسته‌ی Command و Query تقسیم می‌کند که Query ، اطلاعاتی را از وضعیت سیستم بر می‌گرداند، ولی command  وضعیت سیستم را تغییر می‌دهد و مهمترین دستاورد CQS ما را به سمت کدی تمیز‌تر و با قابلیت درک بهتر می‌رساند.

مطالب
شروع به کار با DNTFrameworkCore - قسمت 5 - مکانیزم Eventing و استفاده از سرویس‌های موجودیت‌ها
در قسمت‌های قبل سعی شد یک دید کلی از نحوه استفاده از این زیرساخت ارائه شود؛ در این قسمت علاوه بر بررسی مکانیزم Eventing، با جزئیات بیشتری به استفاده از سرویس‌های پیاده‌سازی شده پرداخته خواهد شد.
‌‌‌‌‌

مکانیزم Eventing

‌‌
استفاده از رخ‌دادها، یکی از راه‌حل‌های رسیدن به  طراحی با Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) می‌باشد؛ همچنین برای حذف چرخه در فرآیند وابستگی مولفه‌های سیستم نیز مورد استفاده قرار میگیرد. در این زیرساخت برای Application Layer مبتنی‌بر CRUD، مکانیزم BusinessEvent با هدف در معرض دید قراردادن یکسری نقاط قابل گسترش توسط سایر بخش‌های سیستم، تعبیه شده است. برای استفاده از این مکانیزم لازم است بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore
‌‌‌
سپس امکان این را خواهید داشت که مشترک رخ‌دادهای مرتبط با عملیات CUD متناظر با موجودیت‌های سیستم، شوید. به عنوان مثال، برای اینکه بتوان مشترک رخ‌داد ویرایش مرتبط با موجودیت Task شد، باید به شکل زیر عمل کرد:
public class TaskEditingBusinessEventHandler : BusinessEventHandler<EditingBusinessEvent<TaskModel, int>>
{
    private readonly ILogger<TaskEditingBusinessEventHandler> _logger;

    public TaskEditingBusinessEventHandler(ILogger<TaskEditingBusinessEventHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public override Task<Result> Handle(EditingBusinessEvent<TaskModel, int> @event)
    {
        foreach (var model in @event.Models)
        {
            _logger.LogInformation($"Title changed from: {model.OriginalValue.Title} to: {model.NewValue.Title}");
        }

        return Task.FromResult(Ok());
    }
}

کار با پیاده‌سازی واسط جنریک IBusinessEventHandler یا ارث‌بری از کلاس جنریک BusinessEventHandler آغاز می‌شود؛ سپس نیاز است Type Parameter متناظر را نیز مشخص کنیم. برای این منظور در تکه کد بالا از رخ‌داد جنریک EditingBusinessEvent استفاده شده است. همچنین همانطور که ملاحظه می‌کنید، نیاز است نوع Model مورد نظر نیز مشخص شده باشد؛ در اینجا از TaskModel به عنوان Model/DTO عملیات CUD موجودیت Task استفاده شده است.

‌‌‌‌رخ‌دادهای Creating/Created/Deleting/Deleted دارای خصوصیتی بنام Models هستند که نوع آن ‎IEnumerable<TModel>‎ می‌باشد. ولی این خصوصیت در رخ‌دادهای Editing/Edited از نوع ‎IEnumerable<ModifiedModel<TModel>> ‎ می‌باشد؛ در این صورت به مقادیر موجود در بانک اطلاعاتی و همچنین مقادیری که توسط استفاده کننده از سرویس جاری به عنوان آرگومان به متد ویرایش ارسال شده است، دسترسی خواهیم داشت.

public class ModifiedModel<TValue>
{
    public TValue NewValue { get; set; }
    public TValue OriginalValue { get; set; }
}
‌‌‌‎‌‌‌‎‌
نکته: همانطور که در قسمت‌های قبل اشاره شد، Application Layer مدنظر ما با یک Model/DTO برای عملیات CUD کار می‌کند؛ از این جهت، منطق تجاری و همچنین قواعد تجاری برفراز همان Model/DTO اجرا خواهند شد و به‌تبع آن، اگر سایر بخش‌های سیستم نیز قصد گسترش منطق تجاری مرتبط با یک موجودیت را دارند، باید با همان Model/DTO کار کنند.
‎‎‌‌
چه زمانی استفاده از مکانیزم BusinessEvent مطرح شده توصیه می‌شود؟
‌‎‎‌‎
به طور کلی محدودیتی در استفاده از آن وجود ندارد؛ در مواردی مشابه اگر قصد اعمال یکسری قواعد تجاری توسط سایر مولفه‌های سیستم را دارید و قصد ندارید ارجاعی به آن مولفه در مولفه جاری وجود داشته باشد یا بدلیل ایجاد چرخه، این امکان وجود ندارد، می‌توان از این مکانیزم بهره برد. برای مثال زمانی که یکسری قواعد تجاری جدید قرار است از سمت مولفه فروش بر روی مولفه مرتبط با مدیریت محصولات اعمال شود. 
‌‌‌
نکته: اگر قصد ارائه یک رخ‌داد سفارشی را دارید، می‌توانید واسط IEventBus را تزریق کرده و از متد TriggerAsync آن استفاده کنید.
‌‌‌‌

استفاده از سرویس‌های موجودیت‌ها

‌‌
OOP : Everything is an object
CRUD-based thinking : Everything is CRUD

استفاده از سرویس‌های موجودیت‌ها به تولید CrudController مرتبط ختم نمی‌شود و در تفکر مبتنی‌بر CRUD، تمام عملیات مرتبط با یک موجودیت از یک تونل واحد عبور خواهند کرد. مسئول این تونل در ابتدا متد Create می‌باشد و در ادامه توسط متد Edit مدیریت می‌شود. به عنوان مثال، اگر امروز در یک سیستم رستورانی با نحوه‌های فروش مختلف، قرار باشد در زمان ثبت گروه کالایی جدید و براساس تنظیمات سیستم، آن گروه کالایی به صورت خودکار به لیست گروه‌های کالایی مرتبط با تمام نحوه‌های فروش اضافه شود، این امر باید از طریق منطق تجاری توسعه داده شده برای نحوه ‌فروش، انجام پذیرد. قرار نیست شما منطق تجاری مرتبط با نحوه فروش را دور بزنید و به صورت دستی شروع به ثبت این اطلاعات در بانک اطلاعاتی کنید. در این شرایط می‌بایست با استفاده از مکانیزم BusinessEvent به شکل زیر عمل کرد:

public class ItemCategoryCreatedBusinessEventHandler : IBusinessEventHandler<CreatedBusinessEvent<ItemCategoryModel, int>>
{
    private readonly ISaleMethodService _saleMethodService;

    public TaskEditingBusinessEventHandler(ISaleMethodService saleMethodService)
    {
        _saleMethodService = saleMethodService ?? throw new ArgumentNullException(nameof(saleMethodService));
    }

    public override Task<Result> Handle(CreatedBusinessEvent<ItemCategoryModel, int> @event)
    {
        var methods = _saleMethodService.FindAsnc();

        foreach (var method in methods)
        {
            foreach (var model in @event.Models)
            {
                method.ItemCategories.Add(new SaleMethodItemCategoryModel
                {
                    ItemCategoryId = model.Id,
                    TrackingState = TrackingState.Added;
            });
        }
    }

     return _saleMethodService.EditAsync(methods);
}
‌‌‌‌‌
این آبونه شدن به رخ‌داد Created مرتبط با گروه کالایی، از سمت مولفه فروش انجام گرفته است. در بدنه متد Handle، ابتدا لیست نحوه‌های فروش موجود در سیستم توسط متد FindAsync بدون پارامتر واکشی شده و سپس با پیمایش خصوصیت Models مرتبط با رخ‌داد مدنظر، به‌ازای تک‌تک گروه‌های کالایی ثبت شده، یک وهله از SaleMethodItemCategoryModel به عنوان Detail موجودیت SaleMethod اضافه می‌شود. سپس با استفاده از متد EditAsync لیست این نحوه‌های فروش را ویرایش خواهیم کرد.
 
نکته: در حد امکان این هندلرها را به صورت تک مسئولیتی طراحی کرده و توسعه دهید؛ این قضیه برای نوشتن آزمون‌های واحد مرتبط با هندلرها، حیاتی می‌باشد.

نکته مهم: در مطلب «معرفی قالب پروژه Web API مبتنی‌بر ASP.NET Core Web API و زیرساخت DNTFrameworkCore» در رابطه با موضوع آزمون جامعیت سرویس‌ها بحث شد؛ توجه داشته باشید که اگر این هندلرها در فرآیند آزمون واحد سرویس‌ها وارد شوند، نگهداری داده‌های تست به‌شدت سخت و طاقت‌فرسا خواهد بود. راهکار پیشنهادی، استفاده از یک StubEventBust و جایگزینی آن با پیاده‌ساز پیش‌فرض، می‌باشد. از این طریق، فراخوانی هندلرهای مرتبط با رخ‌دادها را از فرآیند اصلی متدها حذف کرده‌ایم.