نظرات مطالب
EF Code First #9
برای حالت TPH فقط باید این یک تعریف را داشته باشید public DbSet<Person> People { set; get; } و دسترسی به سایر مشتقات Person، از طریق کوئری‌های ()<db.People.OfType<Coach انجام می‌شود و نه ارجاع مستقیمی به DbSet ایی که نباید داشته باشند. وجود DbSet در Context، یعنی الزام به ساخت جدول معادل.
مطالب
Minimal API's در دات نت 6 - قسمت دوم - ایجاد مدل‌ها و Context برنامه
در قسمت قبل، ساختار ابتدایی یک پروژه‌ی Minimal API's مبتنی بر معماری Vertical slices را تشکیل دادیم. در ادامه موجودیت‌ها و DbContext آن‌را تشکیل می‌دهیم.


ایجاد مدل‌ها و موجودیت‌های برنامه‌ی Minimal Blog

در مثال این سری، هر نویسنده می‌تواند بلاگ خاص خودش را داشته باشد و در هر بلاگ، تعدادی مقاله منتشر کند. هر مقاله هم می‌تواند تعدادی تگ یا گروه مرتبط را داشته باشد.

ساختار ابتدایی موجودیت نویسندگان مطالب بلاگ

این موجودیت‌ها در پوشه‌ی جدیدی به نام Model پروژه‌ی MinimalBlog.Domain اضافه خواهند شد:
namespace MinimalBlog.Domain.Model;

public class Author
{
    public int Id { get; set; }
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;

    public string FullName => FirstName + " " + LastName;
    public DateTime DateOfBirth { get; set; }
    public string? Bio { get; set; }
}
در اینجا تعاریفی مانند !default را هم مشاهده می‌کنید که مرتبط است با فعال بودن nullable reference types در این پروژه که در فایل Directory.Build.props به صورت سراسری به تمام پروژه‌ها اعمال می‌شود. با تعریف !default به کامپایلر اعلام می‌کنیم که این خواص هیچگاه نال نخواهند بود. هدف اصلی از nullable reference types، بالا بردن قابلیت پیش بینی نال بودن، یا نبودن آن‌ها است؛ تا برنامه نویس در قسمت‌های مختلف برنامه بداند که آیا واقعال نیاز است هنگام کار با خاصیت FirstName، نال بودن آن‌را پیش از استفاده بررسی کند یا خیر؟

ساختار ابتدایی موجودیت مقالات بلاگ
namespace MinimalBlog.Domain.Model;

public class Article
{
    public Article()
    {
        Categories = new List<Category>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = default!;
    public string? Subtitle { get; set; }
    public string Body { get; set; } = default!;

    public int AuthorId { get; set; }
    public Author Author { get; set; } = default!;
    public DateTime DateCreated { get; set; }
    public DateTime LastUpdated { get; set; }
    public int NumberOfLikes { get; set; }
    public int NumberOfShares { get; set; }
    public ICollection<Category> Categories { get; set; }
}

ساختار ابتدایی بلاگ‌های اختصاصی قابل تعریف
namespace MinimalBlog.Domain.Model;

public class Blog
{
    public Blog()
    {
        Contributors = new List<Author>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
    public DateTime CreatedDate { get; set; }

    public int AuthorId { get; set; }
    public Author Owner { get; set; } = default!;

    public ICollection<Author> Contributors { get; }
}

ساختار ابتدایی گروه‌های مقالات بلاگ
namespace MinimalBlog.Domain.Model;

public class Category
{
    public Category()
    {
        Articles = new List<Article>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
    public ICollection<Article> Articles { get; set; }
}


معرفی موجودیت‌های تعریف شده به لایه‌ی Dal

لایه‌ی Dal جایی است که DbContext برنامه تعریف می‌شود و موجودیت‌ها فوق توسط DbSetها در معرض دید EF-Core قرار می‌گیرند. به همین جهت ابتدا فایل MinimalBlog.Dal.csproj را به صورت زیر تغییر می‌دهیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\MinimalBlog.Domain\MinimalBlog.Domain.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
در اینجا در ابتدا ارجاعی به پروژه‌ی MinimalBlog.Domain.csproj اضافه شده‌است. سپس بسته‌های مورد نیاز از EF-Core نیز جهت تعریف DbSetها و اجرای Migrations، اضافه شده‌اند.
به علاوه تعاریفی مانند ImplicitUsings و Nullable را هم مشاهده می‌کنید که با توجه به استفاده‌ی از فایل Directory.Build.props غیرضروری و تکراری هستند؛ چون این فایل مخصوص به همراه این تعاریف سراسری نیز هست.

سپس به پروژه‌ی Dal، کلاس جدید MinimalBlogDbContext را به صورت زیر اضافه می‌کنیم تا مدل‌های برنامه را در معرض دید EF-Core قرار دهد:
using Microsoft.EntityFrameworkCore;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Dal;

public class MinimalBlogDbContext : DbContext
{
    public MinimalBlogDbContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Article> Articles { get; set; } = default!;
    public DbSet<Category> Categories { get; set; } = default!;
    public DbSet<Author> Authors { get; set; } = default!;
    public DbSet<Blog> Blogs { get; set; } = default!;
}
در اینجا نیز تعاریف !default را مشاهده می‌کنید که خاصیت راهنمای کامپایلر را در حالت فعال بودن <Nullable>enable</Nullable> دارند و هدف از آن، اعلام نال نبودن این خواص، در حین استفاده‌ی از آن‌ها در قسمت‌های مختلف برنامه است.


ایجاد و اجرای Migrations

پس از تعریف MinimalBlogDbContext، اکنون نوبت به ایجاد کلاس‌های Migrations و اجرای آن‌ها جهت ایجاد بانک اطلاعاتی متناظر با مدل‌های برنامه است. برای این منظور در ابتدا به پروژه‌ی Api مراجعه کرده و فایل appsettings.json را به صورت زیر تکمیل می‌کنیم:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=MinimalBlog;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  }
}
در اینجا یک رشته‌ی اتصالی جدید را از نوع SQL Server LocalDB، تعریف کرده‌ایم. سپس نیاز است تا این رشته‌ی اتصالی را به پروایدر SQL Server مخصوص EF-Core اضافه کنیم. برای اینکار ابتدا باید تغییرات زیر را به فایل MinimalBlog.Api.csproj اعمال کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\MinimalBlog.Domain\MinimalBlog.Domain.csproj" />
    <ProjectReference Include="..\MinimalBlog.Dal\MinimalBlog.Dal.csproj" />
  </ItemGroup>
</Project>
که در آن ارجاعاتی به پروژه‌های Domain و Dal اضافه شده‌است و همچنین بسته‌های نیوگت EF-Core نیز افزوده شده‌اند تا بتوان در فایل Program.cs، تنظیم زیر را اضافه کرد:
using Microsoft.EntityFrameworkCore;
using MinimalBlog.Dal;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var connectionString = builder.Configuration.GetConnectionString("Default");
builder.Services.AddDbContext<MinimalBlogDbContext>(opt => opt.UseSqlServer(connectionString));
در اینجا با استفاده از متد الحاقی AddDbContext، کلاس Context برنامه به مجموعه‌ی سرویس‌های آن اضافه شده و همچنین رشته‌ی اتصالی با کلید Default هم از تنظیمات برنامه، دریافت و به متد UseSqlServer جهت استفاده ارسال شده‌است.

پس از این تنظیمات به پوشه‌ی MinimalBlog.Dal وارد شده و فایل _01-add_migrations.cmd را اجرا می‌کنیم (این فایل به همراه پیوست قسمت اول، ارائه شده‌است). محتوای این فایل به صورت زیر است:
For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b)
For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b)
dotnet tool update --global dotnet-ef --version 6.0.2
dotnet build
dotnet ef migrations --startup-project ../MinimalBlog.Api/ add V%mydate%_%mytime% --context MinimalBlogDbContext
pause
ابتدا، آخرین نگارش ابزار dotnet-ef را نصب کرده و سپس یکبار هم پروژه را build می‌کند تا اگر مشکلی باشد، در همینجا با اطلاعات بیشتری نمایش داده شود. سپس با اجرای دستور dotnet ef migrations، کلاس‌های Migrations در پوشه‌ای به همین نام تشکیل می‌شوند.
البته چون کدهای تولید شده‌ی به صورت خودکار الزاما با Roslyn analyzers برنامه سازگاری ندارند، آن‌ها را توسط فایل مخصوص editorconfig. قرار گرفته‌ی در پوشه‌ی MinimalBlog.Dal\Migrations، از لیست آنالیزهای برنامه خارج می‌کنیم. محتوای این فایل ویژه به صورت زیر است:
[*.cs]
generated_code = true
که سبب خواهد شد تا این پوشه‌ی ویژه به عنوان کدهای به صورت خودکار تولید شده تشخیص داده شود و دیگر آنالیز نشود؛ چون کیفیت آن، مشکل ما نیست!

پس از ایجاد کلاس‌های Migration، اکنون نوبت به اجرای آن‌ها بر روی بانک اطلاعاتی است که در رشته‌ی اتصالی مشخص کردیم. برای اینکار فایل MinimalBlog.Dal\_02-update_db.cmd را اجرا می‌کنیم که چنین محتویاتی دارد:
dotnet tool update --global dotnet-ef --version 6.0.2
dotnet build
dotnet ef --startup-project ../MinimalBlog.Api/ database update --context MinimalBlogDbContext
pause
در اینجا نیز ابتدا از نصب آخرین نگارش ابزارهای EF اطمینان حاصل می‌شود. یکبار دیگر نیز پروژه بر اساس فایل‌های جدیدی که به آن اضافه شده، کامپایل شده و سپس کلاس‌های مهاجرت‌ها، اجرا و اعمال می‌شوند.
نظرات مطالب
ایندکس منحصر به فرد با استفاده از Data Annotation در EF Code First
در EF 6.2 امکان تعریف ایندکس‌ها توسط Fluent API هم (علاوه بر Attributes) میسر شده‌است:
public class MyContext: DbContext
{
        public DbSet<Person> People { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Person>().Property(p => p.Name).HasMaxLength(100);
            modelBuilder.Entity<Person>().HasIndex(p => p.Name).IsUnique();
        }
}
نظرات مطالب
ایجاد ایندکس منحصربفرد در EF Code first
در EF 6.2 امکان تعریف ایندکس‌ها توسط Fluent API هم (علاوه بر Attributes) میسر شده‌است:
public class MyContext: DbContext
{
        public DbSet<Person> People { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Person>().Property(p => p.Name).HasMaxLength(100);
            modelBuilder.Entity<Person>().HasIndex(p => p.Name).IsUnique();
        }
}
نظرات مطالب
معماری لایه بندی نرم افزار #3
بعد از پیاده سازی UOW و لایه‌بندی نرم‌افزار به این این شکل که در مطلب فعلی توضیح داده شد،
 فرض کنید دو ویومدل زیر را داریم:
 public class PersonFormViewModel
    {
        public long Id { get; set; }
        public long RequestId { get; set; }
        [DisplayName("نام کاربری"), Required(ErrorMessage = "نام کاربری الزامی می‌باشد.")]
        public string Username { get; set; }
        public bool Accepted { get; set; }
        [DisplayName("مدل")]
        public string DeviceModel { get; set; }
        public DateTime? ExpireDate { get; set; }
        public RequestViewModel RequestViewModel { get; set; }
    }
public class RequestViewModel
    {
        public long Id { get; set; }
        public string Username { get; set; }
        [DisplayName("توضیحات")]
        [DataType(DataType.MultilineText)]
        public string Description { get; set; }
        public DateTime CreateDate { get; set; }
        public Nullable<long> DeviceId { get; set; }
        public Nullable<long> ParentId { get; set; }
        public long RequestTypeId { get; set; }
        public bool IsFinalized { get; set; }
        public virtual  PersonFormViewModel PersonFormViewModel { get; set; }
    }
سناریو به این شکل است که ما فرمی داریم برای ایحاد یک درخواست (RequestViewModel) که با ایجاد آن در واقع اطلاعات شخص (PersonFormViewModel) را نیز دریافت می‌کنیم.
برای افزودن RequestٰViewModel به دیتابیس این دو روش قابل پیاده‌سازی است:
روش اول: 
تنها RequestViewModel را از طریق RequestService اضافه می‌کنیم و به دلیل وجود PersonFormViewModel داخل RequestViewModel اطلاعات شخص به خودی خود داخل entity مربوطه اضافه می‌شود:
_requestService.Add(requestViewModel);
_uow.SaveChanges();
روش دوم:
ابتدا RequestViewModel را از طریق سرویس مربوطه اضافه می‌کنیم و بعد به طور جداگانه PersonViewModel را از طریق سرویس PersonFormService اضافه می‌کنیم:
var addedRequest = _requestService.Add(requestViewModel ); 
var personViewModel = requestViewModel .PersonFormViewModel; 
_personFormService.Add(personViewModel);
_uow.SaveChanges();
در حال حاضر روش درست کدام است؟
مطالب
بررسی مقدمات کتابخانه‌ی JSON.NET
چرا JSON.NET؟
JSON.NET یک کتابخانه‌ی سورس باز کار با اشیاء JSON در دات نت است. تاریخچه‌ی آن به 8 سال قبل بر می‌گردد و توسط یک برنامه نویس نیوزیلندی به نام James Newton King تهیه شده‌است. اولین نگارش آن در سال 2006 ارائه شد؛ مقارن با زمانی که اولین استاندارد JSON نیز ارائه گردید.
این کتابخانه از آن زمان تا کنون، 6 میلیون بار دانلود شده‌است و به علت کیفیت بالای آن، این روزها پایه اصلی بسیاری از کتابخانه‌ها و فریم ورک‌های دات نتی می‌باشد؛ مانند RavenDB تا ASP.NET Web API و SignalR مایکروسافت و همچنین گوگل نیز از آن جهت تدارک کلاینت‌های کار با API خود استفاده می‌کنند.
هرچند دات نت برای نمونه در نگارش سوم آن جهت مصارف WCF کلاسی را به نام DataContractJsonSerializer ارائه کرد، اما کار کردن با آن محدود است به فرمت خاص WCF به همراه عدم انعطاف پذیری و سادگی کار با آن. به علاوه باید درنظر داشت که JSON.NET از دات نت 2 به بعد تا مونو، Win8 و ویندوز فون را نیز پشتیبانی می‌کند.

برای نصب آن نیز کافی است دستور ذیل را در کنسول پاورشل نیوگت اجرا کنید:
 PM> install-package Newtonsoft.Json

معماری JSON.NET

کتابخانه‌ی JSON.NET از سه قسمت عمده تشکیل شده‌است:
الف) JsonSerializer
ب) LINQ to JSON
ج) JSON Schema


الف) JsonSerializer
کار JsonSerializer تبدیل اشیاء دات نتی به JSON و برعکس است. مزیت مهم آن امکانات قابل توجه تنظیم عملکرد و خروجی آن می‌باشد که این تنظیمات را به شکل ویژگی‌های خواص نیز می‌توان اعمال نمود. به علاوه امکان سفارشی سازی هر کدام نیز توسط کلاسی به نام JsonConverter، پیش بینی شده‌است.
یک مثال:
 var roles = new List<string>
{
   "Admin",
   "User"
};
string json = JsonConvert.SerializeObject(roles, Formatting.Indented);
در اینجا نحوه‌ی استفاده از JSON.NET را جهت تبدیل یک شیء دات نتی، به معادل JSON آن مشاهده می‌کنید. اعمال تنظیم Formatting.Indented سبب خواهد شد تا خروجی آن دارای Indentation باشد. برای نمونه اگر در برنامه‌ی خود قصد دارید فرمت JSON تو در تویی را به نحو زیبا و خوانایی نمایش دهید یا چاپ کنید، همین تنظیم ساده کافی خواهد بود.
و یا در مثال ذیل استفاده از یک anonymous object را مشاهده می‌کنید:
 var jsonString = JsonConvert.SerializeObject(new
{
   Id =1,
   Name = "Test"
}, Formatting.Indented);
به صورت پیش فرض تنها خواص عمومی کلاس‌ها توسط JSON.NET تبدیل خواهند شد.


تنظیمات پیشرفته‌تر JSON.NET

مزیت مهم JSON.NET بر سایر کتابخانه‌ها‌ی موجود مشابه، قابلیت‌های سفارشی سازی قابل توجه آن است. در مثال ذیل نحوه‌ی معرفی JsonSerializerSettings را مشاهده می‌نمائید:
var jsonData = JsonConvert.SerializeObject(new
{
   Id = 1,
   Name = "Test",
   DateTime = DateTime.Now
}, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   Converters =
   {
      new JavaScriptDateTimeConverter()
   }
});
در اینجا با استفاده از تنظیم JavaScriptDateTimeConverter، می‌توان خروجی DateTime استانداردی را به مصرف کنندگان جاوا اسکریپتی سمت کاربر ارائه داد؛ با خروجی ذیل:
 {
  "Id": 1,
  "Name": "Test",
  "DateTime": new Date(1409821985245)
}


نوشتن خروجی JSON در یک استریم

خروجی متد JsonConvert.SerializeObject یک رشته‌است که در صورت نیاز به سادگی توسط متد File.WriteAllText در یک فایل قابل ذخیره می‌باشد. اما برای رسیدن به حداکثر کارآیی و سرعت می‌توان از استریم‌ها نیز استفاده کرد:
using (var stream = File.CreateText(@"c:\output.json"))
{
    var jsonSerializer = new JsonSerializer
   {
      Formatting = Formatting.Indented
   };
   jsonSerializer.Serialize(stream, new
   {
     Id = 1,
     Name = "Test",
     DateTime = DateTime.Now
   });
}
کلاس JsonSerializer و متد Serialize آن یک استریم را نیز جهت نوشتن خروجی می‌پذیرند. برای مثال response.Output برنامه‌های وب نیز یک استریم است و در اینجا نوشتن مستقیم در استریم بسیار سریعتر است از تبدیل شیء به رشته و سپس ارائه خروجی آن؛ زیرا سربار تهیه رشته JSON از آن حذف می‌گردد و نهایتا GC کار کمتری را باید انجام دهد.


تبدیل JSON رشته‌ای به اشیاء دات نت

اگر رشته‌ی jsonData ایی را که پیشتر تولید کردیم، بخواهیم تبدیل به نمونه‌ای از شیء User ذیل کنیم:
public class User
{
   public int Id { set; get; }
   public string Name { set; get; }
   public DateTime DateTime { set; get; }
}
خواهیم داشت:
 var user = JsonConvert.DeserializeObject<User>(jsonData);
در اینجا از متد DeserializeObject به همراه مشخص سازی صریح نوع شیء نهایی استفاده شده‌است.
البته در اینجا با توجه به استفاده از JavaScriptDateTimeConverter برای تولید jsonData، نیاز است چنین تنظیمی را نیز در حالت DeserializeObject مشخص کنیم:
var user = JsonConvert.DeserializeObject<User>(jsonData, new JsonSerializerSettings
{
   Converters = {  new JavaScriptDateTimeConverter() }
});


مقدار دهی یک نمونه یا وهله‌ی از پیش موجود

متد JsonConvert.DeserializeObject یک شیء جدید را ایجاد می‌کند. اگر قصد دارید صرفا تعدادی از خواص یک وهله‌ی موجود، توسط JSON.NET مقدار دهی شوند از متد PopulateObject استفاده کنید:
 JsonConvert.PopulateObject(jsonData, user);


کاهش حجم JSON تولیدی

زمانیکه از متد JsonConvert.SerializeObject استفاده می‌کنیم، تمام خواص عمومی تبدیل به معادل JSON آن‌ها خواهند شد؛ حتی خواصی که مقدار ندارند. این خواص در خروجی JSON، با مقدار null مشخص می‌شوند. برای حذف این خواص از خروجی JSON نهایی تنها کافی است در تنظیمات JsonSerializerSettings، مقدار NullValueHandling = NullValueHandling.Ignore مشخص گردد.
var jsonData = JsonConvert.SerializeObject(object, new JsonSerializerSettings
{
   NullValueHandling = NullValueHandling.Ignore,
   Formatting = Formatting.Indented
});
مورد دیگری که سبب کاهش حجم خروجی نهایی خواهد شد، تنظیم DefaultValueHandling = DefaultValueHandling.Ignore است. در این حالت کلیه خواصی که دارای مقدار پیش فرض خودشان هستند، در خروجی JSON ظاهر نخواهند شد. مثلا مقدار پیش فرض خاصیت int مساوی صفر است. در این حالت کلیه خواص از نوع int که دارای مقدار صفر می‌باشند، در خروجی قرار نمی‌گیرند.
به علاوه حذف Formatting = Formatting.Indented نیز توصیه می‌گردد. در این حالت فشرده‌ترین خروجی ممکن حاصل خواهد شد.


مدیریت ارث بری توسط JSON.NET

در مثال ذیل کلاس کارمند و کلاس مدیر را که خود نیز در اصل یک کارمند می‌باشد، ملاحظه می‌کنید:
public class Employee
{
    public string Name { set; get; }
}

public class Manager : Employee
{
    public IList<Employee> Reports { set; get; }
}
در اینجا هر مدیر لیست کارمندانی را که به او گزارش می‌دهند نیز به همراه دارد. در ادامه نمونه‌ای از مقدار دهی این اشیاء ذکر شده‌اند:
 var employee = new Employee { Name = "User1" };
var manager1 = new Manager { Name = "User2" };
var manager2 = new Manager { Name = "User3" };
manager1.Reports = new[] { employee, manager2 };
manager2.Reports = new[] { employee };
با فراخوانی
 var list = JsonConvert.SerializeObject(manager1, Formatting.Indented);
یک چنین خروجی JSON ایی حاصل می‌شود:
{
  "Reports": [
    {
      "Name": "User1"
    },
    {
      "Reports": [
        {
          "Name": "User1"
        }
      ],
      "Name": "User3"
    }
  ],
  "Name": "User2"
}
این خروجی JSON جهت تبدیل به نمونه‌ی معادل دات نتی خود، برای مثال جهت رسیدن به manager1 در کدهای فوق، چندین مشکل را به همراه دارد:
- در اینجا مشخص نیست که این اشیاء، کارمند هستند یا مدیر. برای مثال مشخص نیست User2 چه نوعی دارد و باید به کدام شیء نگاشت شود.
- مشکل دوم در مورد کاربر User1 است که در دو قسمت تکرار شده‌است. این شیء JSON اگر به نمونه‌ی معادل دات نتی خود نگاشت شود، به دو وهله از User1 خواهیم رسید و نه یک وهله‌ی اصلی که سبب تولید این خروجی JSON شده‌است.

برای حل این دو مشکل، تغییرات ذیل را می‌توان به JSON.NET اعمال کرد:
var list = JsonConvert.SerializeObject(manager1, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   TypeNameHandling = TypeNameHandling.Objects,
   PreserveReferencesHandling = PreserveReferencesHandling.Objects
});
با این خروجی:
{
  "$id": "1",
  "$type": "JsonNetTests.Manager, JsonNetTests",
  "Reports": [
    {
      "$id": "2",
      "$type": "JsonNetTests.Employee, JsonNetTests",
      "Name": "User1"
    },
    {
      "$id": "3",
      "$type": "JsonNetTests.Manager, JsonNetTests",
      "Reports": [
        {
          "$ref": "2"
        }
      ],
      "Name": "User3"
    }
  ],
  "Name": "User2"
}
- با تنظیم TypeNameHandling = TypeNameHandling.Objects سبب خواهیم شد تا خاصیت اضافه‌ای به نام $type به خروجی JSON اضافه شود. این نوع، در حین فراخوانی متد JsonConvert.DeserializeObject جهت تشخیص صحیح نگاشت اشیاء بکار گرفته خواهد شد و اینبار مشخص است که کدام شیء، کارمند است و کدامیک مدیر.
- با تنظیم PreserveReferencesHandling = PreserveReferencesHandling.Objects شماره Id خودکاری نیز به خروجی JSON اضافه می‌گردد. اینبار اگر به گزارش دهنده‌ها با دقت نگاه کنیم، مقدار $ref=2 را خواهیم دید. این مورد سبب می‌شود تا در حین نگاشت نهایی، دو وهله متفاوت از شیء با Id=2 تولید نشود.

باید دقت داشت که در حین استفاده از JsonConvert.DeserializeObject نیز باید JsonSerializerSettings یاد شده، تنظیم شوند.


ویژگی‌های قابل تنظیم در JSON.NET

علاوه بر JsonSerializerSettings که از آن صحبت شد، در JSON.NET امکان تنظیم یک سری از ویژگی‌ها به ازای خواص مختلف نیز وجود دارند.
- برای نمونه ویژگی JsonIgnore معروفترین آن‌ها است:
public class User
{
   public int Id { set; get; }

   [JsonIgnore]
   public string Name { set; get; }

   public DateTime DateTime { set; get; }
}
JsonIgnore سبب می‌شود تا خاصیتی در خروجی نهایی JSON تولیدی حضور نداشته باشد و از آن صرفنظر شود.

- با استفاده از ویژگی JsonProperty اغلب مواردی را که پیشتر بحث کردیم مانند NullValueHandling، TypeNameHandling و غیره، می‌توان تنظیم نمود. همچنین گاهی از اوقات کتابخانه‌های جاوا اسکریپتی سمت کاربر، از اسامی خاصی که از روش‌های نامگذاری دات نتی پیروی نمی‌کنند، در طراحی خود استفاده می‌کنند. در اینجا می‌توان نام خاصیت نهایی را که قرار است رندر شود نیز صریحا مشخص کرد. برای مثال:
[JsonProperty(PropertyName = "m_name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { set; get; }
همچنین در اینجا امکان تنظیم Order نیز وجود دارد. برای مثال مشخص کنیم که خاصیت X در ابتدا قرار گیرد و پس از آن خاصیت Y رندر شود.

- استفاده از ویژگی JsonObject به همراه مقدار OptIn آن به این معنا است که از کلیه خواصی که دارای ویژگی JsonProperty نیستند، صرفنظر شود. حالت پیش فرض آن OptOut است؛ یعنی تمام خواص عمومی در خروجی JSON حضور خواهند داشت منهای مواردی که با JsonIgnore مزین شوند.
[JsonObject(MemberSerialization.OptIn)]
public class User
{
    public int Id { set; get; }

    [JsonProperty]
    public string Name { set; get; }
 
    public DateTime DateTime { set; get; }
}

- با استفاده از ویژگی JsonConverter می‌توان نحوه‌ی رندر شدن مقدار خاصیت را سفارشی سازی کرد. برای مثال:
[JsonConverter(typeof(JavaScriptDateTimeConverter))]
public DateTime DateTime { set; get; }


تهیه یک JsonConverter سفارشی

با استفاده از JsonConverterها می‌توان کنترل کاملی را بر روی اعمال serialization و deserialization مقادیر خواص اعمال کرد. مثال زیر را در نظر بگیرید:
public class HtmlColor
{
   public int Red { set; get; }
   public int Green { set; get; }
   public int Blue { set; get; }
}

var colorJson = JsonConvert.SerializeObject(new HtmlColor
{
  Red = 255,
  Green = 0,
  Blue = 0
}, Formatting.Indented);
در اینجا علاقمندیم، در حین عملیات serialization، بجای اینکه مقادیر اجزای رنگ تهیه شده به صورت int نمایش داده شوند، کل رنگ با فرمت hex رندر شوند. برای اینکار نیاز است یک JsonConverter سفارشی را تدارک دید:
    public class HtmlColorConverter : JsonConverter
    {

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(HtmlColor);
        }

        public override object ReadJson(JsonReader reader, Type objectType,
                                        object existingValue, JsonSerializer serializer)
        {
            throw new NotSupportedException();
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var color = value as HtmlColor;
            if (color == null)
                return;

            writer.WriteValue("#" + color.Red.ToString("X2")
                + color.Green.ToString("X2") + color.Blue.ToString("X2"));
        }
    }
کار با ارث بری از کلاس پایه JsonConverter شروع می‌شود. سپس باید تعدادی از متدهای این کلاس پایه را بازنویسی کرد. در متد CanConvert اعلام می‌کنیم که تنها اشیایی از نوع کلاس HtmlColor را قرار است پردازش کنیم. سپس در متد WriteJson منطق سفارشی خود را می‌توان پیاده سازی کرد.
از آنجائیکه این تبدیلگر صرفا قرار است برای حالت serialization استفاده شود، قسمت ReadJson آن پیاده سازی نشده‌است.

در آخر برای استفاده از آن خواهیم داشت:
var colorJson = JsonConvert.SerializeObject(new HtmlColor
{
  Red = 255,
  Green = 0,
  Blue = 0
},  new JsonSerializerSettings
    {
      Formatting = Formatting.Indented,
      Converters = { new HtmlColorConverter() }
    });   
نظرات مطالب
نرمال سازی (قسمت سوم: Third Normal Form)
خوب، اگر این سه قسمت رو بخواهیم با EF Code first مدل کنیم: 
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

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

    //هر دانشجو چند ترم در دانشگاه خواهد بود
    public virtual ICollection<Semester> Semesters { set; get; }
    //هر دانشجو چندین واحد دارد
    public virtual ICollection<Unit> Units { set; get; }
}

public class Semester
{
    public int Id { set; get; }
    public string Name { set; get; }
    public int Average { set; get; }

    [ForeignKey("StudentId")]
    public virtual Student Student { set; get; }
    public int StudentId { set; get; }
}

public class Unit
{
    public int Id { set; get; }
    public string Name { set; get; }
    public string UnitType { set; get; }
    public int NumberOfUnits { set; get; }

    [ForeignKey("StudentId")]
    public virtual Student Student { set; get; }
    public int StudentId { set; get; }
}
به نظر می‌رسه که خاصیت Average جاش در کلاس Semester نیست. حتی به Unit هم نباید به صورت مستقیم ارتباط پیدا کنه. نیاز به یک کلاس دیگر هست که بتونه به ازای هر دانشجو، ترم و واحد، نمره ثبت کرد. میانگین، یک خاصیت آماری است که می‌تونه اصلا لحاظ نشه و در گزارشات محاسبه بشه.
و یا هر ترم یک سری واحد داره. اینطوری چطور؟ چون الان مشخص نیست در هر ترم چه واحدهایی برداشته.
مطالب
تقسیم جدول در Entity Framework Code First
سناریو هایی هستند که در آن ها، تعداد ستون‌های یک جدول، بیش از اندازه زیاد می‌شوند و یا آن جدول حاوی فیلدهایی هست که منابع زیادی مصرف می‌کنند، به مانند فیلدهای متنی طولانی یا عکس. معمولا متوجه می‌شویم که در اکثر مواقع، به هنگام واکشی اطلاعات آن جدول، احتیاجی به داده‌های آن فیلد‌ها نداریم و با واکشی بی مورد آن ها، سربار اضافه ای به سیستم تحمیل می‌کنیم، چرا که این داده‌ها ، منابع حافظه ای ما را به هدر می‌دهند.
برای مثال، جدول Post مدل بلاگ را در نظر بگیرید که در آن دو فیلد Body و Image تعریف شده اند.فیلد Body از نوع nvarchar max و فیلد Image از نوع varbinary max است و بدیهی است که این دو داده، به هنگام واکشی حافظه‌ی زیادی مصرف می‌کنند.موارد بسیاری وجود دارند که ما به اطلاعات این دو فیلد احتیاجی نداریم از جمله: نمایش پست‌های پر بازدید، پسته هایی که اخیرا ارسال شده اند و اصولا ما فقط به چند فیلد جدول Post احتیاج داریم  و نه همه‌ی آن ها.
namespace SplittingTableSample.DomainClasses
{
    public class Post
    {
        public virtual int Id { get; set; }
        public virtual string Title { get; set; }
        public virtual DateTime CreatedDate { get; set; }
        public virtual string  Body { get; set; }
        public virtual byte[] Image { get; set; }
    }
}

دلیل اینکه در مدل فوق، تمامی خواص به صورت virtual تعریف شده اند، فعال سازی پروکسی‌های ردیابی تغییر است. اگر دستور زیر را برای واکشی اطلاعات post با id=1 انجام دهیم: 

            using (var context = new MyDbContext())
            {
                var post = context.Posts.Find(1);
            }
خروجی زیر را در SQL Server Profiler مشاهده خواهید کرد:

exec sp_executesql N'SELECT TOP (2) 
[Extent1].[Id] AS [Id], 
[Extent1].[Title] AS [Title], 
[Extent1].[CreatedDate] AS [CreatedDate], 
[Extent1].[Body] AS [Body], 
[Extent1].[Image] AS [Image]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1

همان طور که مشاهده می‌کنید، با اجرای دستور فوق تمامی فیلد‌های جدول Posts که id آن‌ها برابر 1 بود واکشی شدند، ولی من تنها به فیلدهای Id و Title آن احتیاج داشتم. خب شاید بگویید که من به سادگی با projection، این مشکل را حل می‌کنم و تنها از فیلد هایی که به آن‌ها احتیاج دارم، کوئری می‌گیرم. همه‌ی این‌ها درست، اما projection هم مشکلات خود را دارد،به صورت پیش فرض، نوع بدون نام بر می‌گرداند و اگر بخواهیم این گونه نباشد، باید مقادیر آن را به یک کلاس(مثلا viewmodel) نگاشت کنیم و کلی مشکل دیگر.
راه حل دیگری که برای حل این مشکل ارائه می‌شود و برای نرمال سازی جداول نیز کاربرد دارد این است که، جدول Posts را به دو جدول مجزا که با یکدیگر رابطه‌ی یک به یک دارند تقسیم کنیم، فیلد‌های پر مصرف را در یک جدول و فیلدهای حجیم و کم مصرف را در جدول دیگری تعریف کنیم و سپس یک رابطه‌ی یک به یک بین آن دو برقرار می‌کنیم.
به  طور مثال این کار را بر روی جدول Posts ، به شکل زیر انجام شده است:
 
namespace SplittingTableSample.DomainClasses
{
    public class Post
    {
        public virtual int Id { get; set; }
        public virtual string Title { get; set; }
        public virtual DateTime CreatedDate { get; set; }
        public virtual PostMetaData PostMetaData { get; set; }
    }
}
namespace SplittingTableSample.DomainClasses
{
    public class PostMetaData
    {
        public virtual int PostId { get; set; }
        public virtual string Body { get; set; }
        public virtual byte[] Image { get; set; }
        public virtual Post Post { get; set; }
    }
}
همان طور که می‌بینید، خواص حجیم به جدول دیگری به نام PostMetaData منتقل شده و با تعریف خواص راهبری ارجاعی در هر دو کلاس،رابطه‌ی یک به یک بین آن‌ها برقرار شده است.جز الزامات تعریف روابط یک به یک این است که، با استفاده از Fluent API یا Data Annotations ، طرف‌های Depenedent و Principal، صریحا به EF معرفی شوند.

namespace SplittingTableSample.DomainClasses
{
    public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData>
    {
        public PostMetaDataConfig()
        {
            HasKey(x => x.PostId);
            HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData);
        }
    }
}
اولین نکته ای که باید به آن توجه شود، این است که در کلاس PostMetaData، قوانین پیش فرض EF برای تعیین کلید اصلی نقض شده است و به همین دلیل، صراحتا با استفاده از متد HasKey ، کلید اصلی به EF معرفی شده است. نکته‌ی مهم دیگری که به آن باید توجه شود این است که هر دو سر رابطه به صورت Required تعریف شده است. دلیل این موضوع هم با توجه به مطلبی که قرار است گفته شود،کمی جلوتر خواهید فهمید. حال اگر تعاریف DbSet‌ها را نیز اصلاح کنیم و دستور زیر را اجرا کنیم:

var post = context.Posts.Find(1);
خروجی sql زیر را مشاهده خواهید کرد:

exec sp_executesql N'SELECT TOP (2) 
[Extent1].[Id] AS [Id], 
[Extent1].[Title] AS [Title], 
[Extent1].[CreatedDate] AS [CreatedDate]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
خیلی خوب! دیگر خبری از  فیلدهای اضافی Body و Image نیست. دلیل اینکه در اینجا join  بین دو جدول مشاهده نمی‌شود، قابلیت lazy loading است، که با virtual تعریف کردن خواص راهبری حاصل شده است. پس lazy loading در اینجا واقعا مفید است.
اما راه حل ذکر شده نیز کاملا بدون ایراد نیست. مشکل اساسی آن تعدد تعداد جداول آن است. آیا جدول Post ، واقعا احتیاج به چنین سطح نرمال سازی و تبدیل آن به دو جدول مجزا را داشت؟ مطمئنا خیر. آیا واقعا راه حلی وجود دارد که ما در سمت کد‌های خود با دو موجودیت مجزا کار کنیم، در صورتی که در دیتابیس این دو موجودیت، ساختار یک جدول را تشکیل دهند. در اینجا روشی مطرح می‌شود به نام تقسیم جدول (Table Splitting).
برای انجام این کار فقط چند تنظیم ساده لازم است:
1) فیلد‌های موجودیت مورد نظر را به موجودیت‌های کوچکتر، نگاشت می‌کنیم.
2) بین موجودیت‌های کوچک تر، رابطه‌ی یک به یک که هر دو سر رابطه Required هستند، رابطه برقرار می‌کنم.
3) با استفاده از Fluent API یا DataAnnotations، تمامی موجودیت‌ها را به یک نام در دیتابیس نگاشت می‌کنیم.
برای مثال، تنظیمات Fluent برای کلاس Post و PostMetaData که رابطه‌ی بین آن‌ها یک به یک است را مشاهده می‌کنید:
 
namespace SplittingTableSample.DomainClasses
{
    public class PostConfig : EntityTypeConfiguration<Post>
    {
        public PostConfig()
        {
            ToTable("Posts");
        }
    }
}
namespace SplittingTableSample.DomainClasses
{
    public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData>
    {
        public PostMetaDataConfig()
        {
            ToTable("Posts");
            HasKey(x => x.PostId);
            HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData);
        }
    }
}
نکته مهم این است که در هر دو کلاس(حتی کلاس Post) باید با استفاده از متد ToTable، کلاس‌ها را به یک نام در دیتابیس نگاشت کنیم. در نتیجه با استفاده از متد ToTable در هر دو موجودیت، آنها در دیتابیس به جدولی به نام  Posts نگاشت خواهند شد. تصویر زیر پس از اجرای برنامه، بیان گر این موضوع خواهد بود.

اگر دستورات زیر را اجرا کنید:


var post = context.Posts.Find(1);
Console.WriteLine(post.PostMetaData.Body);
خروجی زیر را در  SQL Server Profiler مشاهده خواهید کرد:
برای متد Find خروجی زیر:
 
exec sp_executesql N'SELECT TOP (2) 
[Extent1].[Id] AS [Id], 
[Extent1].[Title] AS [Title], 
[Extent1].[CreatedDate] AS [CreatedDate]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
و برای post.PostMetaData.Body دستور sql زیر را مشاهده می‌کنید:

exec sp_executesql N'SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Body] AS [Body], 
[Extent1].[Image] AS [Image]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[Id] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1
دلیل این که در اینجا ،دو دستور sql به دیتابیس ارسال شده است، فعال بودن ویژگی lazy loading ،به دلیل virtual  تعریف کردن خواص راهبری موجودیت‌ها است.
حال اگر بخواهیم با یک رفت و آمد به دیتابیس کلیه اطلاعات را واکشی کنیم، می‌توانیم از Eager Loading استفاده کنیم:
 
var post = context.Posts.Include(x => x.PostMetaData).SingleOrDefault(x => x.Id == 1);
که خروجی sql آن نیز به شکل زیر است:

SELECT 
[Limit1].[Id] AS [Id], 
[Limit1].[Title] AS [Title], 
[Limit1].[CreatedDate] AS [CreatedDate], 
[Extent2].[Id] AS [Id1], 
[Extent2].[Body] AS [Body], 
[Extent2].[Image] AS [Image]
FROM   (SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate]
FROM [dbo].[Posts] AS [Extent1]
WHERE 1 = [Extent1].[Id] ) AS [Limit1]
LEFT OUTER JOIN [dbo].[Posts] AS [Extent2] ON [Limit1].[Id] = [Extent2].[Id]
در نتیجه با کمک این تکنیک توانستیم، با چند موجودیت، در قالب یک جدول رفتار کنیم و از مزیت‌های آن همچون lazy loading، نیز بهره مند شویم.

دریافت کد‌های این بخش: SplittingTable-Sample.rar 
مطالب
بهبود کارآیی استفاده از 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)) => ...;
    }
}
مطالب
پیاده سازی پروژه‌ای مبتنی بر CQRS و ES
در قسمت قبلی با معماری CQRS و Event Sourcing بصورت مختصر آشنا شدیم. برای درک بیشتر مطلب پیشین، احتیاج به پیاده سازی آن به صورت عملیاتی و نه فقط تئوری محض میباشد و در این مرحله قصد پیاده سازی این مدل را به ساده‌ترین صورت ممکن داریم.
برای مطالعه‌ی ادامه‌ی این مقاله، نیاز به آشنایی با مباحث مطرح شده در قسمت قبل وجود دارد. پس از توضیحات اضافه بر روی قسمت‌های زیر گذشته و فرض بر آن است که آشنایی با این قسمت‌ها وجود دارد.
از این مدل میتوان در زبان‌های مختلف برنامه نویسی و همچنین سیستم‌های مختلف اعم از وب اپلیکیشن و ... استفاده نمود. همچنین برای استفاده از این مدل نیاز قطعی به استفاده از فریم ورک خاصی نیست. در صورت نیاز میتوانید پیاده سازی سفارشی خاص خود را داشته باشید. اما برای ساده‌تر شدن و هرچه سریعتر شدن مراحل از فریمورک SimpleCqrs استفاده میکنیم. هر چند بر خلاف نامش امکانات فراوانی را در اختیار برنامه نویسان قرار میدهد و حتی در پروژه‌های واقعی نیز میتوان از آن استفاده نمود.
برای سریعتر شدن کار میخواهیم پیاده سازی این مدل را در یک پروژه‌ی Console انجام دهیم و همچنین پس از ایجاد، پکیج‌های زیر را نصب مینماییم:
Unity, SimpleCqrs, SimpleCqrs.Unity
میخواهیم طبق مراحل گفته شده‌ی در قسمت قبل، به پیاده سازی این مدل بپردازیم و هدف، اضافه کردن یک Account به سیستم خواهد بود.
ابتدا باید DomainObject مورد نظر نوشته شود:
using System;
using SimpleCqrs.Domain;

namespace CqrsPattern.Cqrs.Command
{
    public class Account : AggregateRoot
    {
        public Account(Guid id)
        {
            Apply(new AccountCreatedEvent { AggregateRootId = id });
        }

        public void SetName(string firstName, string lastName)
        {
            Apply(new AccountNameSetEvent { FirstName = firstName, LastName = lastName });
        }

        public void OnAccountCreated(AccountCreatedEvent evt)
        {
            Id = evt.AggregateRootId;
        }
    }
}
نکته: میخواهیم عملیات اضافه کردن یک Account، با استفاده از دو event مربوطه به نام AccountCreatedEvent و مقدار دهی آن با استفاده از AccountNameSetEvent انجام شود.
eventهای فوق را در ادامه اضافه خواهیم داد (از توضیحات بیشتر صرفنظر شده و به مقاله‌ی قسمت قبل رجوع شود).
حال احتیاج به پیاده سازی Command مربوطه برای انجام وظیفه‌ی خود داریم که هدف آن، اضافه کردن یک Account  به سیستم مورد نظر میباشد.
فرض کنید برای اضافه شدن Account، پراپرتی‌های FirstName و LastName باید مقدار دهی شوند:
using SimpleCqrs.Commanding;

namespace CqrsPattern.Cqrs.Command
{
    public class CreateAccountCommand : ICommand
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}

حال CommandHandler که وظیفه‌ی تفسیر کردن Command مربوطه را به عهده دارد، پیاده سازی خواهد شد:
using System;
using SimpleCqrs.Commanding;
using SimpleCqrs.Domain;

namespace CqrsPattern.Cqrs.Command
{
    public class CreateAccountCommandHandler : CommandHandler<CreateAccountCommand>
    {
        private readonly IDomainRepository repository;

        public CreateAccountCommandHandler(IDomainRepository repository)
        {
            this.repository = repository;
        }

        public override void Handle(CreateAccountCommand command)
        {
            var account = new Account(Guid.NewGuid());
            account.SetName(command.FirstName, command.LastName);

            repository.Save(account);
        }
    }
}
نکته: از طریق account.SetName فراخوانی Event مربوطه انجام شده‌است و همچنین repository.Save به raise کردن EventHandler میپردازد.
event مربوط به اضافه شدن Account را به صورت زیر پیاده سازی مینماییم:
using SimpleCqrs.Eventing;

namespace CqrsPattern.Cqrs.Command
{
    public class AccountCreatedEvent : DomainEvent { }
}
و همچنین event مربوط به مقدار دهی پراپرتی‌ها نیز به صورت زیر خواهد بود:
using SimpleCqrs.Eventing;

namespace CqrsPattern.Cqrs.Command
{
    public class AccountNameSetEvent : DomainEvent
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
در این بخش، پیاده سازی EventHandler را خواهیم داشت. طبق مطلب پیشین هر Domain باید EventHnadler ی داشته باشد که از Event هایش ارث بری کرده و هر کدام از Event‌ها عملا در قسمت Handle مربوط به خودش پردازش خواهد شد.
using System.Linq;
using SimpleCqrs.Eventing;
using CqrsPattern.Cqrs.Db;

namespace CqrsPattern.Cqrs.Command
{
    public class AccountEventHandler : IHandleDomainEvents<AccountCreatedEvent>,
                                             IHandleDomainEvents<AccountNameSetEvent>
    {
        private readonly FakeAccountTable accountTable;

        public AccountEventHandler(FakeAccountTable accountTable)
        {
            this.accountTable = accountTable;
        }

        public void Handle(AccountCreatedEvent domainEvent)
        {
            accountTable.Add(new FakeAccountTableRow { Id = domainEvent.AggregateRootId });
        }

        public void Handle(AccountNameSetEvent domainEvent)
        {
            var account = accountTable.Single(x => x.Id == domainEvent.AggregateRootId);
            account.Name = domainEvent.FirstName + " " + domainEvent.LastName;
        }
    }
}
نکته: از آنجاییکه پیاده سازی ذخیره کردن Account با استفاده از دو event فوق انجام شده، بعد از Raise شدن EventHandler هر دو متد Handle، وظیفه‌ی Command مربوطه را به عهده دارند (بنابراین وظیفه‌ی هر Command میتواند با استفاده از event‌های مختلفی انجام شود).
برای اینکه نخواهیم وارد فاز‌های مربوط به دیتابیس شویم، موقتا یک db به صورت fake شده را پیاده سازی مینماییم؛ به صورت زیر:
using System.Collections.Generic;

namespace CqrsPattern.Cqrs.Db
{
    public class FakeAccountTable : List<FakeAccountTableRow>
    { }
}
using System;

namespace CqrsPattern.Cqrs.Db
{
    public class FakeAccountTableRow
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
}

و همچنین نیاز به ServiceLocator برای نمونه گرفتن از RunTime ی که از آن ارث بری کرده است داریم (برای سادگی کار از الگوی ServiceLocator استفاده میکنیم، ServiceLocator جز Anti-Pattern  ها محسوب میشود و معمولا در پروژه‌های واقعی از آن استفاده نمیشود)
using SimpleCqrs;
using SimpleCqrs.Unity;

namespace CqrsPattern
{
    public class SampleRunTime : SimpleCqrsRuntime<UnityServiceLocator> { }
}
حال احتیاج به پیاده سازی قسمت Queryداریم به همراه ReadModel و سرویسی برای فراخوانی آن
using System;

namespace CqrsPattern.Cqrs.Query
{
    public class AccountReadModel
    {
        public string Name { get; set; }
        public Guid Id { get; set; }
    }
}
using CqrsPattern.Cqrs.Db;
using System.Collections.Generic;
using System.Linq;

namespace CqrsPattern.Cqrs.Query
{
    public class AccountReportReadService
    {
        private FakeAccountTable fakeAccountDb;

        public AccountReportReadService(FakeAccountTable fakeAccountDb)
        {
            this.fakeAccountDb = fakeAccountDb;
        }

        public IEnumerable<AccountReadModel> GetAccounts()
        {
            return from a in fakeAccountDb
                   select new AccountReadModel { Id = a.Id, Name = a.Name };
        }
    }
}

در قسمت Main نرم افزار نیاز به register کردن FakeTable خود داریم و همانطور که ملاحظه میکنید Command مورد نظر را نمونه سازی کرده و آن را روی CommandBus قرار میدهیم تا مراحل پیاده سازی شده در قسمت‌های فوق انجام شود و همچنین بعد از اتمام command ارسال شده از طریق Service مورد نظر اطلاعات ذخیره شده بازگردانی میشود
using System;
using SimpleCqrs.Commanding;
using CqrsPattern.Cqrs.Query;
using CqrsPattern.Cqrs.Command;

namespace CqrsPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var runtime = new SampleRunTime();

            runtime.Start();

            var fakeAccountTable = new FakeAccountTable();
            runtime.ServiceLocator.Register(fakeAccountTable);
            runtime.ServiceLocator.Register(new AccountReportReadService(fakeAccountTable));
            var commandBus = runtime.ServiceLocator.Resolve<ICommandBus>();

            var cmd = new CreateAccountCommand { FirstName = "Ali", LastName = "Kh" };

            commandBus.Send(cmd);

            var accountReportReadModel = runtime.ServiceLocator.Resolve<AccountReportReadService>();

            Console.WriteLine("Accounts in database");
            Console.WriteLine("####################");
            foreach (var account in accountReportReadModel.GetAccounts())
            {
                Console.WriteLine(" Id: {0} Name: {1}", account.Id, account.Name);
            }

            runtime.Shutdown();

            Console.ReadLine();
        }
    }
}
اینگونه کل عملیات‌های لازم انجام خواهد شد.

خلاصه:
1) Command مربوطه را نمونه سازی کرده و روی CommandBus قرار میدهیم.
2) CommandHandler فراخوانی شده و فانکشن Handle آن باعث نمونه سازی از AggregateRoot میشود.
public override void Handle(CreateAccountCommand command)
        {
            var account = new Account(Guid.NewGuid()); //line 1
            account.SetName(command.FirstName, command.LastName); //line 2
            repository.Save(account); //line 3
        }
در خط نخست Constructor کلاس Account باعث Apply شدن event مربوطه میشود.
public Account(Guid id)
        {
            Apply(new AccountCreatedEvent { AggregateRootId = id });
        }
و در خط دوم account.SetName  برای Apply شدن event مربوط به مقدار دهی property‌ها میباشد.
public void SetName(string firstName, string lastName)
        {
            Apply(new AccountNameSetEvent { FirstName = firstName, LastName = lastName });
        }
و همچنین در خط  سوم و پس از repository.Save باعث میشود event‌های pending شده Raise شده و توسط متد Handle مربوط به EventHandler پردازش شده و عملیات‌های زیر انجام شوند:
public void Handle(AccountCreatedEvent domainEvent)
        {
            accountTable.Add(new FakeAccountTableRow { Id = domainEvent.AggregateRootId });
        }

        public void Handle(AccountNameSetEvent domainEvent)
        {
            var account = accountTable.Single(x => x.Id == domainEvent.AggregateRootId);
            account.Name = domainEvent.FirstName + " " + domainEvent.LastName;
        }
رکورد مورد نظر ثبت شده و event بعدی، پراپرتی‌هایش را مقدار دهی مینماید  و بصورت InMemory درون FakeAccountTable ذخیره میشود (پر واضح است که در یک پروژه‌ی واقعی به جای ذخیره شدن در یک Collection باید درون دیتایس واقعی ذخیره سازی شود).
و پس از اتمام عملیات انجام شده، بصورت زیر در Main برنامه اطلاعات ذخیره شده بازگردانده خواهد شد:
var accountReportReadModel = runtime.ServiceLocator.Resolve<AccountReportReadService>();
var accounts = accountReportReadModel.GetAccounts();

در ادامه برای مطالعه بیشتر میتوان به Scale out کردن این سیستم و استفاده از فریمورک‌های  messaging چون Redis یا Kafka پرداخت و همچنین اعمال Load Balancing را در اینگونه سیستم‌ها انجام داد.
نکته: Cqrs-Pattern را میتوانید از اینجا clone نمایید