Forwarded from TondTech (مسعود بیگی)
اگر دنبال خلق ارزش با AI هستید مثل من و خیلی دیگه از بچه ها میتونید این کتاب رو دانلود کنید رایگان. یا نسخه چاپی شو با کمتر از یک پول پیتزا از ما بخرید
https://refhub.ir/fa/refrence_detail/ai-value-creators-beyond-the-generative-ai-user-mindset/
لایک و شیر و کامنت کنید برسه دست کسی که نیازش داره
https://refhub.ir/fa/refrence_detail/ai-value-creators-beyond-the-generative-ai-user-mindset/
لایک و شیر و کامنت کنید برسه دست کسی که نیازش داره
💡 بهترین شیوهها برای تست یکپارچهسازی با Testcontainers در NET.
تستهای یکپارچهسازی با Testcontainers قدرتمند هستند، اما اگر از الگوهای صحیح پیروی نکنید، به سرعت میتوانند به یک کابوس نگهداری تبدیل شوند. 😫
من تیمهایی را دیدهام که با تستهای شکننده (flaky)، مجموعه تستهای کند، و سردردهای پیکربندی دست و پنجه نرم میکنند که با رعایت شیوههای بهتر از همان ابتدا، قابل اجتناب بودند.
امروز الگوهایی را به شما نشان خواهم داد که تستهای Testcontainers را قابل اعتماد، سریع و آسان برای نگهداری میکنند.
Testcontainers چگونه تست یکپارچهسازی را تغییر میدهد؟
تستهای یکپارچهسازی سنتی اغلب به دیتابیسهای تست اشتراکی یا جایگزینهای درون-حافظهای (in-memory) تکیه میکنند که با رفتار محیط پروداکشن مطابقت ندارند. شما یا با آلودگی تست بین اجراها سر و کار دارید یا واقعگرایی را فدای سرعت میکنید.
تست کانتینرها این مشکل را با بالا آوردن کانتینرهای واقعی Docker 🐳 برای وابستگیهای شما حل میکند. تستهای شما در برابر PostgreSQL، Redis یا هر سرویس دیگری که در پروداکشن استفاده میکنید، اجرا میشوند. وقتی تستها کامل شدند، کانتینرها از بین میروند و هر بار یک محیط تمیز در اختیار شما قرار میدهند.
این جادو 🪄 از طریق API داکر اتفاق میافتد. Testcontainers کل چرخه حیات را مدیریت میکند: دریافت ایمیجها، شروع کانتینرها، انتظار برای آماده شدن، و پاکسازی. کد تست شما فقط باید بداند چگونه به آنها متصل شود.
پیشنیازها 📦
اول، مطمئن شوید که پکیجهای لازم را دارید:
Install-Package Microsoft.AspNetCore.Mvc.Testing
Install-Package Testcontainers.PostgreSql
Install-Package Testcontainers.Redis
ساختن کانتینرهای تست 🏗
در اینجا نحوه راهاندازی کانتینرهای خود با پیکربندی مناسب آمده است:
PostgreSqlContainer _postgresContainer = new PostgreSqlBuilder()
.WithImage("postgres:17")
.WithDatabase("devhabit")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
RedisContainer _redisContainer = new RedisBuilder()
.WithImage("redis:latest")
.Build();
برای شروع و توقف تمیز کانتینرها در سراسر مجموعه تست خود، IAsyncLifetime را در WebApplicationFactory خود پیادهسازی کنید:
public sealed class IntegrationTestWebAppFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
// ... تعریف کانتینرها ...
public async Task InitializeAsync()
{
await _postgresContainer.StartAsync();
await _redisContainer.StartAsync();
// وابستگیهای دیگر را اینجا شروع کنید
}
public async Task DisposeAsync()
{
await _postgresContainer.StopAsync();
await _redisContainer.StopAsync();
}
}
این کار تضمین میکند که کانتینرها قبل از اجرای تستها آماده و پس از آن پاکسازی شوند. این یعنی هیچ وضعیت باقیمانده از داکر یا شرایط رقابتی (race conditions) وجود نخواهد داشت.
📌 نکته: نسخههای ایمیج خود را پین کنید (مانند postgres:17) تا از غافلگیریهای ناشی از تغییرات بالادستی جلوگیری کنید.
انتقال پیکربندی به اپلیکیشن شما ✅
بزرگترین اشتباهی که میبینم، هاردکد کردن connection stringها است. Testcontainers پورتهای داینامیک اختصاص میدهد. هیچ چیز را هاردکد نکنید.
در عوض، مقادیر را از طریق WebApplicationFactory.ConfigureWebHost تزریق کنید:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseSetting("ConnectionStrings:Database", _postgresContainer.GetConnectionString());
builder.UseSetting("ConnectionStrings:Redis", _redisContainer.GetConnectionString());
}
📍نکته کلیدی استفاده از متد UseSetting برای انتقال داینامیک connection stringها است. این کار همچنین از هرگونه شرایط رقابتی یا تداخل با تستهای دیگری که ممکن است به صورت موازی اجرا شوند، جلوگیری میکند.
اشتراکگذاری تنظیمات پرهزینه با xUnit Collection Fixtures
💡فیکسچر تست (test fixture) چیست؟
یک فیکسچر یک زمینه اشتراکی برای تستهای شماست که به شما اجازه میدهد منابع پرهزینه مانند دیتابیسها یا message brokerها را یک بار راهاندازی کرده و در چندین تست از آنها استفاده مجدد کنید.
اینجاست که اکثر تیمها دچار مشکل میشوند. انتخاب بین class و collection fixtures بر عملکرد و ایزولهسازی تست تأثیر میگذارد.
Class Fixture 🏠 :
یک کانتینر برای هر کلاس تست:
زمانی استفاده کنید که تستها وضعیت سراسری را تغییر میدهند یا دیباگ کردن تعاملات تست دشوار میشود.
public class AddItemToCartTests : IClassFixture<DevHabitWebAppFactory>
{
// ...
}
Collection Fixture 🏢 :
یک کانتینر مشترک بین چندین کلاس تست:
زمانی استفاده کنید که تستهای شما وضعیت اشتراکی را تغییر نمیدهند یا میتوانید به طور قابل اعتماد بین تستها پاکسازی انجام دهید.
[CollectionDefinition(nameof(IntegrationTestCollection))]
public sealed class IntegrationTestCollection : ICollectionFixture<DevHabitWebAppFactory> {}
// سپس آن را به کلاسهای تست خود اعمال کنید:
[Collection(nameof(IntegrationTestCollection))]
public class AddItemToCartTests : IntegrationTestFixture
{
// ...
}
چه زمانی از کدام استفاده کنیم:🤔
🔹️Class fixtures
وقتی به ایزولهسازی کامل بین کلاسهای تست نیاز دارید (کندتر اما امنتر).
🔹️Collection fixtures
وقتی کلاسهای تست با یکدیگر تداخل ندارند (سریعتر اما نیازمند نظم).
نوشتن تستهای یکپارچهسازی قابل نگهداری ✍️
با پیکربندی صحیح زیرساخت، تستهای واقعی شما باید روی منطق بیزینس تمرکز کنند:
[Fact]
public async Task Should_ReturnFailure_WhenNotEnoughQuantity()
{
//Arrange
Guid customerId = await Sender.CreateCustomerAsync(Guid.NewGuid());
// ...
//Act
Result result = await Sender.Send(command);
//Assert
result.Error.Should().Be(TicketTypeErrors.NotEnoughQuantity(Quantity));
}
توجه کنید که چگونه تستها به جای دغدغههای زیرساختی، روی قوانین بیزینس تمرکز دارند. شما PostgreSQL یا Redis را mock نمیکنید، شما رفتار واقعی را تست میکنید.
نتیجهگیری 👍
تست کانتینرها تست یکپارچهسازی را با دادن اطمینانی که از تست در برابر وابستگیهای واقعی حاصل میشود، متحول میکند.
ساده شروع کنید: یک تست یکپارچهسازی را که در حال حاضر از mockها یا دیتابیسهای درون-حافظهای استفاده میکند، انتخاب کرده و آن را برای استفاده از Testcontainers تبدیل کنید. بلافاصله تفاوت در اطمینان را هنگامی که آن تست پاس میشود، متوجه خواهید شد.
Forwarded from Brain bytes
🧠✨ Brain bytes
Channel: t.me/brain_bytes
# Programming Paradigms vs Programming Model
## 1) What Is a Programming Paradigm?
A programming paradigm is a conceptual style or philosophy for thinking about problems and structuring solutions in code.
It answers questions like:
- How do we view data? (objects, pure values, sequences, events)
- How do we express behavior? (methods, functions, queries, handlers)
- How do system parts interact? (method calls, message passing, events, streams)
Paradigms guide how you mentally model the domain. They are about “How should I think and organize?” not about specific syntax.
Examples: Imperative, Object-Oriented (OOP), Functional, Declarative, Event-Driven, Asynchronous, Reactive.
## 2) What Is a Programming Model?
A programming model is the concrete set of language mechanisms, runtime behavior, keywords, and APIs that let you implement one or more paradigms.
In C#, the programming model provides:
- OOP constructs:
- Functional/declarative tools: LINQ (
- Asynchronous constructs:
- Event-driven features:
- Memory/execution semantics: managed runtime, garbage collection, value vs reference types
In short:
Paradigm = conceptual lens (thinking model)
Programming model = toolbox + mechanics enabling that lens
---
## 3) Major Paradigms with C# Examples
### A) Imperative Paradigm
Focus: Explicit step-by-step instructions and mutable state.
Characteristics: loops, assignments, control flow (
---
### B) Object-Oriented Programming (OOP)
Model the world as objects combining state + behavior.
Core concepts: Encapsulation, Inheritance, Polymorphism, Abstraction.
Polymorphism example:
---
### C) Functional Style (in C#)
Emphasis on pure transformations, minimized mutation, composability.
Traits: Lambdas, LINQ pipelines, declarative transformations, easier testing.
---
### D) Declarative Paradigm
Describe WHAT you want, not HOW to iterate.
You specify filters and projections; LINQ decides iteration strategy (and can translate to SQL via EF Core).
---
Channel: t.me/brain_bytes
# Programming Paradigms vs Programming Model
## 1) What Is a Programming Paradigm?
A programming paradigm is a conceptual style or philosophy for thinking about problems and structuring solutions in code.
It answers questions like:
- How do we view data? (objects, pure values, sequences, events)
- How do we express behavior? (methods, functions, queries, handlers)
- How do system parts interact? (method calls, message passing, events, streams)
Paradigms guide how you mentally model the domain. They are about “How should I think and organize?” not about specific syntax.
Examples: Imperative, Object-Oriented (OOP), Functional, Declarative, Event-Driven, Asynchronous, Reactive.
## 2) What Is a Programming Model?
A programming model is the concrete set of language mechanisms, runtime behavior, keywords, and APIs that let you implement one or more paradigms.
In C#, the programming model provides:
- OOP constructs:
class, interface, abstract, virtual, override - Functional/declarative tools: LINQ (
Where, Select, query syntax), lambdas, delegates (Func<>, Action), expression trees, record types - Asynchronous constructs:
async, await, Task, ValueTask, CancellationToken - Event-driven features:
event, EventHandler, delegates - Memory/execution semantics: managed runtime, garbage collection, value vs reference types
In short:
Paradigm = conceptual lens (thinking model)
Programming model = toolbox + mechanics enabling that lens
---
## 3) Major Paradigms with C# Examples
### A) Imperative Paradigm
Focus: Explicit step-by-step instructions and mutable state.
int sum = 0;
int[] numbers = { 3, 5, 7, 9 };
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
Console.WriteLine(sum);
Characteristics: loops, assignments, control flow (
if, for, while).---
### B) Object-Oriented Programming (OOP)
Model the world as objects combining state + behavior.
public class Car
{
public string Brand { get; }
public int Speed { get; private set; }
public Car(string brand, int speed)
{
Brand = brand;
Speed = speed;
}
public void Accelerate(int amount) => Speed += amount;
public override string ToString() => $"{Brand} => {Speed} km/h";
}
var car = new Car("BMW", 120);
car.Accelerate(30);
Console.WriteLine(car); // BMW => 150 km/h
Core concepts: Encapsulation, Inheritance, Polymorphism, Abstraction.
Polymorphism example:
public abstract class Shape
{
public abstract double Area();
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double r) => Radius = r;
public override double Area() => Math.PI * Radius * Radius;
}
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double w, double h) { Width = w; Height = h; }
public override double Area() => Width * Height;
}
Shape[] shapes = { new Circle(3), new Rectangle(4, 5) };
foreach (var s in shapes)
Console.WriteLine(s.Area());
---
### C) Functional Style (in C#)
Emphasis on pure transformations, minimized mutation, composability.
int[] numbers = { 3, 5, 7, 9 };
int sum = numbers.Aggregate((a, b) => a + b);
var squaredEvens = numbers
.Where(n => n % 2 == 0)
.Select(n => n * n)
.ToList();
Console.WriteLine(sum);
Console.WriteLine(string.Join(", ", squaredEvens));Traits: Lambdas, LINQ pipelines, declarative transformations, easier testing.
---
### D) Declarative Paradigm
Describe WHAT you want, not HOW to iterate.
var products = new[]
{
new { Name = "Laptop", Price = 45000 },
new { Name = "Mouse", Price = 400 },
new { Name = "Monitor", Price = 9000 },
};
var expensive =
from p in products
where p.Price > 5000
orderby p.Price descending
select p.Name;
Console.WriteLine(string.Join(" | ", expensive));
You specify filters and projections; LINQ decides iteration strategy (and can translate to SQL via EF Core).
---
Forwarded from Brain bytes
### E) Event-Driven Paradigm
Flow controlled by events rather than sequential commands.
Used in UI, messaging systems, reactive pipelines.
---
### F) Asynchronous Paradigm
Handle I/O or long-running tasks without blocking threads.
Combines
---
### G) Reactive (Optional Extension)
Treat data as streams (e.g. with IObservable): react declaratively to change over time.
---
## 4) How C# Programming Model Enables These Paradigms
| Paradigm | Model Elements (C#) |
|---------------|---------------------|
| Imperative | loops, mutable locals, branching (
| OOP |
| Functional | lambdas, delegates (
| Declarative | LINQ query syntax, attributes, expression trees |
| Event-Driven |
| Asynchronous |
| Reactive | observables (via libraries), continuation chains |
C# is multi-paradigm: mix domain modeling (OOP) + data transformations (functional/declarative) + async I/O + events.
---
## 5) Choosing a Paradigm (Quick Guide)
| Situation | Best Fit |
|-----------|----------|
| Rich domain entities, lifecycle | OOP |
| Data querying/filtering | Declarative (LINQ) |
| Composable transformations, testability | Functional style |
| UI interactions, user input | Event-Driven |
| Network / file / database latency | Asynchronous |
| Simple noscripts / procedural tasks | Imperative |
| Live data streams / continuous updates | Reactive |
Often you combine several in one solution.
---
## 6) Summary
Paradigm = How you think and structure the solution conceptually.
Programming Model = The language/runtime mechanisms enabling those structures.
C# provides a unified model supporting multiple paradigms seamlessly.
---
## 7) Glossary (English Term + Persian Meaning)
| Term | Persian |
|------|--------|
| Programming Paradigm | پارادایم برنامهنویسی / سبک مفهومی |
| Programming Model | مدل برنامهنویسی / مکانیزم اجرایی |
| Imperative | دستوری |
| Declarative | اعلامی / деклараتی |
| Object-Oriented (OOP) | شیءگرا |
| Encapsulation | کپسولهسازی |
| Inheritance | ارثبری |
| Polymorphism | چندریختی |
| Abstraction | انتزاع |
| Class | کلاس |
| Object | شیء |
| Method | متد |
| State | حالت |
| Functional Programming | برنامهنویسی تابعی |
| Pure Function | تابع خالص |
| Side Effect | عارضهٔ جانبی |
| Immutability | تغییرناپذیری |
| Lambda Expression | عبارت لامبدا |
| Delegate | نماینده (تابع اشارهای) |
| LINQ | چارچوب کوئری یکپارچه |
| Query Syntax | نحوهٔ نگارش کوئری |
| Expression Tree | درخت بیان |
| Event | رویداد |
| Event-Driven | رویدادمحور |
| Asynchronous / Async | ناهمگام |
| Concurrency | همزمانی |
| Task | وظیفهٔ ناهمگام |
| CancellationToken | توکن لغو |
| Record | رکورد (نوع دادهٔ مختصر) |
| Virtual | مجازی (قابل بازنویسی) |
| Override | بازنویسی |
| Abstract | انتزاعی |
| Interface | اینترفیس / قرارداد |
| Reactive | واکنشی |
| Multi-Paradigm | چندپارادایمی |
| API | رابط برنامهنویسی کاربردی |
| Design Pattern | الگوی طراحی |
| Refactoring | بازآرایی کد |
| Maintainability | نگهداشتپذیری |
| Scalability | مقیاسپذیری |
| Deferred Execution | اجرای مؤخر |
| DSL | زبان دامنهمحور |
| Idempotent | ایدمپوتنت (تکرار بدون تغییر نتیجه) |
---
## 8) Tags
#programming #paradigm #programming_model #CSharp #OOP #Functional #Declarative #LINQ #Async #EventDriven #Reactive #DotNet #SoftwareDesign #CleanCode #BrainBytes #Architecture #Coding #Developer
Flow controlled by events rather than sequential commands.
public class Button
{
public event Action? Click;
public void OnClick() => Click?.Invoke();
}
var button = new Button();
button.Click += () => Console.WriteLine("Button clicked!");
button.OnClick();
Used in UI, messaging systems, reactive pipelines.
---
### F) Asynchronous Paradigm
Handle I/O or long-running tasks without blocking threads.
public async Task<string> FetchAsync()
{
using var http = new HttpClient();
return await http.GetStringAsync("https://example.com");
}
var data = await FetchAsync();
Console.WriteLine(data);
Combines
async/await with Task for clearer concurrency.---
### G) Reactive (Optional Extension)
Treat data as streams (e.g. with IObservable): react declaratively to change over time.
---
## 4) How C# Programming Model Enables These Paradigms
| Paradigm | Model Elements (C#) |
|---------------|---------------------|
| Imperative | loops, mutable locals, branching (
if, switch) || OOP |
class, interface, abstract, virtual, override, access modifiers || Functional | lambdas, delegates (
Func<>, Action), LINQ, record types || Declarative | LINQ query syntax, attributes, expression trees |
| Event-Driven |
event, delegates, EventHandler, UI frameworks || Asynchronous |
async, await, Task, CancellationToken, ValueTask || Reactive | observables (via libraries), continuation chains |
C# is multi-paradigm: mix domain modeling (OOP) + data transformations (functional/declarative) + async I/O + events.
---
## 5) Choosing a Paradigm (Quick Guide)
| Situation | Best Fit |
|-----------|----------|
| Rich domain entities, lifecycle | OOP |
| Data querying/filtering | Declarative (LINQ) |
| Composable transformations, testability | Functional style |
| UI interactions, user input | Event-Driven |
| Network / file / database latency | Asynchronous |
| Simple noscripts / procedural tasks | Imperative |
| Live data streams / continuous updates | Reactive |
Often you combine several in one solution.
---
## 6) Summary
Paradigm = How you think and structure the solution conceptually.
Programming Model = The language/runtime mechanisms enabling those structures.
C# provides a unified model supporting multiple paradigms seamlessly.
---
## 7) Glossary (English Term + Persian Meaning)
| Term | Persian |
|------|--------|
| Programming Paradigm | پارادایم برنامهنویسی / سبک مفهومی |
| Programming Model | مدل برنامهنویسی / مکانیزم اجرایی |
| Imperative | دستوری |
| Declarative | اعلامی / деклараتی |
| Object-Oriented (OOP) | شیءگرا |
| Encapsulation | کپسولهسازی |
| Inheritance | ارثبری |
| Polymorphism | چندریختی |
| Abstraction | انتزاع |
| Class | کلاس |
| Object | شیء |
| Method | متد |
| State | حالت |
| Functional Programming | برنامهنویسی تابعی |
| Pure Function | تابع خالص |
| Side Effect | عارضهٔ جانبی |
| Immutability | تغییرناپذیری |
| Lambda Expression | عبارت لامبدا |
| Delegate | نماینده (تابع اشارهای) |
| LINQ | چارچوب کوئری یکپارچه |
| Query Syntax | نحوهٔ نگارش کوئری |
| Expression Tree | درخت بیان |
| Event | رویداد |
| Event-Driven | رویدادمحور |
| Asynchronous / Async | ناهمگام |
| Concurrency | همزمانی |
| Task | وظیفهٔ ناهمگام |
| CancellationToken | توکن لغو |
| Record | رکورد (نوع دادهٔ مختصر) |
| Virtual | مجازی (قابل بازنویسی) |
| Override | بازنویسی |
| Abstract | انتزاعی |
| Interface | اینترفیس / قرارداد |
| Reactive | واکنشی |
| Multi-Paradigm | چندپارادایمی |
| API | رابط برنامهنویسی کاربردی |
| Design Pattern | الگوی طراحی |
| Refactoring | بازآرایی کد |
| Maintainability | نگهداشتپذیری |
| Scalability | مقیاسپذیری |
| Deferred Execution | اجرای مؤخر |
| DSL | زبان دامنهمحور |
| Idempotent | ایدمپوتنت (تکرار بدون تغییر نتیجه) |
---
## 8) Tags
#programming #paradigm #programming_model #CSharp #OOP #Functional #Declarative #LINQ #Async #EventDriven #Reactive #DotNet #SoftwareDesign #CleanCode #BrainBytes #Architecture #Coding #Developer
🆕 تازههای EF Core 10: عملگرهای LeftJoin و RightJoin در LINQ
اگر تا به حال با پایگاههای داده کار کرده باشی، حتماً با LEFT JOIN (و بهصورت مشابه RIGHT JOIN) آشنا هستی. این یکی از اون چیزهاییست که تقریباً همیشه ازش استفاده میکنیم.
اما در Entity Framework Core، انجام یک Left Join همیشه کمی دردسر داشت 😅
من از joinهایی خوشم میاد که دقیقاً مثل کاری که انجام میدن خونده بشن.
متأسفانه تا قبل از این، در LINQ راه مستقیمی برای بیان Left/Right Join وجود نداشت. باید از ترکیب GroupJoin و DefaultIfEmpty استفاده میکردی، که باعث میشد کدت پیچیدهتر و سختتر برای خواندن و نگهداری بشه.
اما خبر خوب اینه که 🎉 در 10 NET. این مشکل بالاخره برطرف شده، با معرفی متدهای جدید LeftJoin و RightJoin.
❓Left Join چیه؟ (به زبان ساده)
یک LEFT JOIN تمام ردیفها از سمت چپ (Left side) و ردیفهای منطبق از سمت راست رو برمیگردونه.
اگه تطابقی وجود نداشته باشه، سمت راست مقدار null میگیره.
🔹 دلیل استفاده: برای نگه داشتن دادههای اصلی حتی زمانی که دادههای مرتبط وجود ندارن.
مثلاً نمایش تمام محصولات، حتی اگر بعضی از اونها هیچ Reviewای نداشته باشن.
🕰 روش قدیمی (GroupJoin + DefaultIfEmpty)
قبل از 10 NET. ، برای انجام یک Left Join در LINQ باید از ترکیب GroupJoin و DefaultIfEmpty استفاده میکردی تا ردیفهای سمت چپ بدون تطابق هم حفظ بشن.
این روش کار میکرد، اما مفهوم اصلی وسط کدهای اضافی گم میشد 😩
دو روش برای نوشتن این نوع query وجود داشت: Query Syntax و Method Syntax
🔹 Query Syntax
var query =
from product in dbContext.Products
join review in dbContext.Reviews on product.Id equals review.ProductId into reviewGroup
from subReview in reviewGroup.DefaultIfEmpty()
orderby product.Id, subReview.Id
select new
{
ProductId = product.Id,
product.Name,
product.Price,
ReviewId = (int?)subReview.Id ?? 0,
Rating = (int?)subReview.Rating ?? 0,
Comment = subReview.Comment ?? "N/A"
};
🧩 SQL
تولیدشده توسط EF Core برای query بالا:
SELECT
p."Id" AS "ProductId",
p."Name",
p."Price",
COALESCE(r."Id", 0) AS "ReviewId",
COALESCE(r."Rating", 0) AS "Rating",
COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Products" AS p
LEFT JOIN "Reviews" AS r ON p."Id" = r."ProductId"
ORDER BY p."Id", COALESCE(r."Id", 0)
⚙️ روش Method Syntax
var query = dbContext.Products
.GroupJoin(
dbContext.Reviews,
product => product.Id,
review => review.ProductId,
(product, reviewList) => new { product, subgroup = reviewList })
.SelectMany(
joinedSet => joinedSet.subgroup.DefaultIfEmpty(),
(joinedSet, review) => new
{
ProductId = joinedSet.product.Id,
joinedSet.product.Name,
joinedSet.product.Price,
ReviewId = (int?)review!.Id ?? 0,
Rating = (int?)review!.Rating ?? 0,
Comment = review!.Comment ?? "N/A"
})
.OrderBy(result => result.ProductId)
.ThenBy(result => result.ReviewId);
🧩 چرا این روش کار میکند:
• GroupJoin
ردیفهای دو مجموعه را بر اساس شرط تطبیق میدهد،
• DefaultIfEmpty
زمانی که هیچ تطابقی وجود ندارد، یک مقدار پیشفرض (null) را درج میکند،
در نتیجه ردیف سمت چپ همچنان حفظ میشود.
سپس با SelectMany دادهها را تخت (flatten) میکنیم.
اما بیایید صادق باشیم 😅 —
برای چیزی بهسادگی یک Left Join، این حجم از کد واقعاً زیادیه!
🚀 روش جدید در EF Core 10: استفاده از LeftJoin
الان میتونیم دقیقاً چیزی که مد نظر داریم رو بنویسیم.
LeftJoin
یک قابلیت رسمی در LINQ است و EF Core به طور خودکار آن را به LEFT JOIN در SQL ترجمه میکند.
var query = dbContext.Products
.LeftJoin(
dbContext.Reviews,
product => product.Id,
review => review.ProductId,
(product, review) => new
{
ProductId = product.Id,
product.Name,
product.Price,
ReviewId = (int?)review.Id ?? 0,
Rating = (int?)review.Rating ?? 0,
Comment = review.Comment ?? "N/A"
})
.OrderBy(x => x.ProductId)
.ThenBy(x => x.ReviewId);`
🧠 SQL تولیدشده توسط EF Core در این حالت دقیقاً مشابه مثال قبلی است.
✅ چرا این روش بهتره؟
🔹 قصد (Intent) واضح است: وقتی LeftJoin را میبینی، دقیقاً میدانی چه اتفاقی میافتد.
🔹 کد کمتر، اجزای کمتر: دیگه خبری از GroupJoin، DefaultIfEmpty یا SelectMany نیست.
🔹 همان نتیجه: همهی محصولات حفظ میشن، حتی اگر بعضی از آنها هیچ Reviewای نداشته باشن.
💡 نکته:
در زمان نگارش این مقاله، C# Query Syntax (ساختار from … select …) هنوز کلیدواژههای مخصوص left join یا right join رو نداره.
پس فعلاً باید از Method Syntax استفاده کنی که در مثال بالا نشون داده شده.
🆕 همچنین جدید در RightJoin
EF Core 10:
•RightJoin
تمام ردیفهای سمت راست را حفظ میکند و فقط ردیفهای منطبق از سمت چپ را نگه میدارد.
EF Core
این متد را به RIGHT JOIN در SQL ترجمه میکند.
این روش زمانی کاربرد دارد که سمت دوم دادهها (Right Side) برای ما بخش «ضروری برای حفظ» باشد.
💡 بهصورت مفهومی:
var query = dbContext.Reviews
.RightJoin(
dbContext.Products,
review => review.ProductId,
product => product.Id,
(review, product) => new
{
ProductId = product.Id,
product.Name,
product.Price,
ReviewId = (int?)review.Id ?? 0,
Rating = (int?)review.Rating ?? 0,
Comment = review.Comment ?? "N/A"
});
🧠 چرا از RightJoin استفاده کنیم؟
وقتی گزارشهایت از جدول Reviews شروع میشن (و میخوای همهشون حفظ بشن)،
در عین حال میخوای Products مرتبط رو هم بیاری اگر وجود داشته باشن.
📊 SQL تولیدشده توسط EF Core:
SELECT
p."Id" AS "ProductId",
p."Name",
p."Price",
COALESCE(r."Id", 0) AS "ReviewId",
COALESCE(r."Rating", 0) AS "Rating",
COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Reviews" AS r
RIGHT JOIN "Products" AS p ON r."ProductId" = p."Id"
🧩 جمعبندی
به این فکر کن که چقدر left joinها در پروژههایت استفاده میشوند:
• نمایش تمام کاربران حتی اگر تنظیمات پروفایل نداشته باشند
• نمایش همهی محصولات حتی اگر هیچ Reviewای برایشان ثبت نشده باشد
• نمایش همهی سفارشها حتی اگر هنوز اطلاعات ارسال (Shipping Info) نداشته باشند
تقریباً همهجا باهاش سروکار داریم! 🚀
پیش از این، توسعهدهندهها گاهی برای دور زدن پیچیدگی GroupJoin و DefaultIfEmpty، دو Query جداگانه مینوشتند 😩
یا بدتر از آن، از Inner Join استفاده میکردند و دادههایی را از دست میدادند.
اما حالا دیگه هیچ بهانهای نیست ✨
LeftJoin و RightJoin
درست مثل هر متد LINQ دیگهای ساده و خوانا هستند.
⚙️ چند نکته سریع برای نوشتن Queryهای LINQ:
✅ در Projectionها، سمت Nullable را با ?? محافظت کن:
review.Comment ?? "N/A"
✅ Projection
ها را کوچک نگه دار تا دادههای اضافی از دیتابیس نخوانی.
✅ روی ستونهای Join (کلیدهای اتصال) ایندکس بگذار تا Query Plan بهینه شود.
💡 حالا با اضافه شدن LeftJoin و RightJoin،
کدی که مینویسی دقیقاً با مدل ذهنیات منطبق است — واضح، تمیز، و قابل نگهداری.
🔖هشتگها:
#EntityFrameworkCore #DotNet10 #EFCore #LINQ #LeftJoin #RightJoin
Forwarded from thisisnabi.dev [Farsi]
A friendly reminder to all of us building tech: power ≠ usability.
Daniel De Laney’s post “Normal” is going viral in tech — and for good reason.
He shows a TV remote with most of its buttons covered in tape. Only the essentials remain. It’s absurdly simple — and perfect for the person using it.
That image captures what’s wrong with most software: too many buttons, too much flexibility, too little empathy. Users don’t want optionality; they want clarity. They don’t want to “learn a system”; they just want it to work.
If you’re building for non-experts, design for the taped-over remote first. Hide complexity. Reveal it only when someone asks for it.
Software wins when it feels obvious. Everything else is just noise.
https://www.linkedin.com/posts/mariustreitz_a-friendly-reminder-to-all-of-us-building-activity-7389702679670796288-UvVU?utm_source=share&utm_medium=member_android&rcm=ACoAABdqDr0BJIj7gy7oW3facT7ro7bITsW3Ay0
Daniel De Laney’s post “Normal” is going viral in tech — and for good reason.
He shows a TV remote with most of its buttons covered in tape. Only the essentials remain. It’s absurdly simple — and perfect for the person using it.
That image captures what’s wrong with most software: too many buttons, too much flexibility, too little empathy. Users don’t want optionality; they want clarity. They don’t want to “learn a system”; they just want it to work.
If you’re building for non-experts, design for the taped-over remote first. Hide complexity. Reveal it only when someone asks for it.
Software wins when it feels obvious. Everything else is just noise.
https://www.linkedin.com/posts/mariustreitz_a-friendly-reminder-to-all-of-us-building-activity-7389702679670796288-UvVU?utm_source=share&utm_medium=member_android&rcm=ACoAABdqDr0BJIj7gy7oW3facT7ro7bITsW3Ay0
🧩 چگونه با استفاده از Domain Events سیستمهای Loosely Coupled بسازیم
در مهندسی نرمافزار، واژهٔ coupling (وابستگی) به این معناست که بخشهای مختلف یک سیستم نرمافزاری تا چه اندازه به یکدیگر وابستهاند.
اگر اجزا tightly coupled باشند، تغییر در یک بخش میتواند بر قسمتهای زیادی از سیستم تأثیر بگذارد.
اما اگر loosely coupled باشند، تغییر در یک بخش، باعث بروز مشکلات بزرگ در سایر قسمتها نخواهد شد.
✨Domain Events
یکی از الگوهای تاکتیکی
در Domain-Driven Design (DDD) هستند که به ما کمک میکنند تا سیستمهایی با وابستگی کم بین اجزا بسازیم.
میتوان یک Domain Event را از داخل Domain منتشر کرد که در واقع نمایانگر یک رخداد واقعی است.
سایر کامپوننتها در سیستم میتوانند به این Event مشترک شوند (subscribe) و آن را بر اساس منطق خودشان مدیریت (handle) کنند.
⚙️ Domain Event چیست؟
یک Event چیزی است که در گذشته اتفاق افتاده است.
✅ یعنی یک واقعیت (Fact) است.
✅ و غیرقابلتغییر (Unchangeable) است.
یک Domain Event چیزی است که در درون Domain رخ داده و سایر بخشهای Domain باید از آن آگاه شوند.
🎯 Domain Events
به شما این امکان را میدهند که Side Effectها را بهصورت صریح بیان کنید
و Separation of Concerns بهتری در Domain داشته باشید.
این الگو یک روش عالی برای تحریک (Trigger) کردن Side Effectها در میان چند Aggregate مختلف در درون Domain است.
⚠️ اما یک نکته مهم:
شما به عنوان توسعهدهنده باید مطمئن شوید که انتشار (Publishing) یک Domain Event بهصورت تراکنشی (Transactional) انجام میشود.
در ادامه خواهیم دید که چرا این کار به این سادگیها هم نیست! 😅
⚖️ Domain Events در مقابل Integration Events
ممکن است قبلاً دربارهٔ Integration Events شنیده باشید و اکنون این سؤال برایتان پیش آمده باشد که تفاوت آنها با Domain Events در چیست.
از نظر معنایی (Semantically)، هر دو یکی هستند:
نمایانگر چیزیاند که در گذشته اتفاق افتاده است.
اما هدف (Intent) آنها متفاوت است — و درک این تفاوت اهمیت زیادی دارد.
🧩 Domain Events:
• درون یک Domain واحد منتشر و مصرف (Consume) میشوند.
• از طریق یک in-memory message bus ارسال میشوند.
• میتوانند بهصورت synchronous یا asynchronous پردازش شوند.
🔗 Integration Events:
• توسط زیرسیستمها (مانند microservices یا Bounded Contexts) مصرف میشوند.
• از طریق message broker و روی queue ارسال میشوند.
• بهصورت کاملاً asynchronous پردازش میشوند.
💡 بنابراین، اگر نمیدانید چه نوع Eventی باید منتشر کنید، به هدف (Intent) و اینکه چه کسی باید آن را مدیریت کند فکر کنید.
در واقع، Domain Events میتوانند برای تولید Integration Events نیز مورد استفاده قرار گیرند - رویدادهایی که از مرز (boundary) Domain خارج میشوند.
🏗 پیادهسازی Domain Events
روشی که من برای پیادهسازی Domain Events ترجیح میدهم، ایجاد یک abstraction به نام IDomainEvent و پیادهسازی آن از MediatR.INotification است.
مزیت این کار این است که میتوان از قابلیت publish-subscribe در MediatR برای انتشار یک Notification به یک یا چند Handler استفاده کرد.
using MediatR;
public interface IDomainEvent : INotification
{
}
اکنون میتوانید یک Domain Event مشخص (Concrete) را پیادهسازی کنید.
در هنگام طراحی Domain Events، چند نکتهٔ مهم را باید در نظر بگیرید 👇
⚙️ نکات کلیدی طراحی Domain Events:
🔹️Immutability (تغییرناپذیری) :
چون Domain Event یک واقعیت (Fact) است، باید غیرقابلتغییر باشد.
🔹️Fat vs Thin Domain Events :
چقدر اطلاعات لازم دارید؟ (Eventها را بیش از حد سنگین یا بیش از حد سبک طراحی نکنید.)
🔹️نامگذاری با زمان گذشته :
برای نام Event از past tense استفاده کنید.
🔸 مثال:
public class CourseCompletedDomainEvent : IDomainEvent
{
public Guid CourseId { get; init; }
}
🚀 Raising Domain Events
پس از اینکه Domain Eventهای خود را ایجاد کردید، باید بتوانید آنها را از درون Domain فراخوانی (raise) کنید.
روش پیشنهادی من این است که یک کلاس پایه به نام Entity بسازید،
چون فقط Entityها مجازند Domain Event ایجاد کنند.
برای کپسولهسازی (Encapsulation) بهتر، متد RaiseDomainEvent را میتوان protected تعریف کرد تا فقط از داخل کلاس یا زیرکلاسها قابل فراخوانی باشد.
در این پیادهسازی، ما Domain Eventها را در یک لیست داخلی (internal collection) نگهداری میکنیم تا هیچ بخش دیگری از سیستم نتواند مستقیماً به آن دسترسی داشته باشد.
متد GetDomainEvents یک snapshot از لیست داخلی برمیگرداند،
و متد ClearDomainEvents برای پاکسازی لیست داخلی استفاده میشود.
public abstract class Entity : IEntity
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> GetDomainEvents()
{
return _domainEvents.ToList();
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
protected void RaiseDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
}
اکنون Entityهای شما میتوانند از این کلاس پایه ارثبری کرده و Domain Eventها را raise کنند 👇
public class Course : Entity
{
public Guid Id { get; private set; }
public CourseStatus Status { get; private set; }
public DateTime? CompletedOnUtc { get; private set; }
public void Complete()
{
Status = CourseStatus.Completed;
CompletedOnUtc = DateTime.UtcNow;
RaiseDomainEvent(new CourseCompletedDomainEvent { CourseId = this.Id });
}
}
✅ حالا تنها کاری که باقی مانده، انتشار (Publish) کردن Domain Eventها است.
🚀 نحوهٔ انتشار (Publish) Domain Eventها با EF Core
یک راهحل زیبا برای انتشار Domain Eventها استفاده از EF Core است.
از آنجا که EF Core مانند یک Unit of Work عمل میکند،
میتوانید از آن برای جمعآوری تمام Domain Eventهای موجود در تراکنش فعلی و انتشار آنها استفاده کنید.
من دوست ندارم این فرآیند را پیچیده کنم،
بنابراین به سادگی متد SaveChangesAsync را override میکنم تا بعد از ذخیره شدن تغییرات در پایگاه داده، Domain Eventها منتشر شوند.
البته میتوانید از interceptor نیز استفاده کنید.
public class ApplicationDbContext : DbContext
{
public override async Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default)
{
// چه زمانی باید Domain Eventها را منتشر کنید؟
//
// 1. قبل از فراخوانی SaveChangesAsync
// - Domain Eventها بخشی از همان تراکنش هستند
// - تضمین immediate consistency
// 2. بعد از فراخوانی SaveChangesAsync
// - Domain Eventها در تراکنش جداگانه اجرا میشوند
// - eventual consistency
// - احتمال خطا در Handlerها
var result = await base.SaveChangesAsync(cancellationToken);
await PublishDomainEventsAsync();
return result;
}
}
مهمترین تصمیمی که در این مرحله باید بگیرید این است که چه زمانی Domain Eventها منتشر شوند.
من شخصاً ترجیح میدهم بعد از فراخوانی SaveChangesAsync آنها را منتشر کنم،
یعنی پس از آنکه تغییرات در پایگاه داده ذخیره شدند.
این کار البته معایب و مزایایی دارد 👇
✅ Eventual consistency:
چون پیامها پس از پایان تراکنش اصلی پردازش میشوند.
⚠️ ریسک ناسازگاری دادهها (Database inconsistency): چون ممکن است اجرای Domain Eventها با خطا مواجه شود.
با eventual consistency میتوانم کنار بیایم،
اما ریسک inconsistency در پایگاه داده موضوع مهمی است.
برای حل این مشکل میتوان از الگوی Outbox استفاده کرد 💡
در این الگو، تغییرات در پایگاه داده بهصورت اتمیک (Atomic) به همراه Domain Eventها (به عنوان پیامهای Outbox) ذخیره میشوند.
سپس Domain Eventها به صورت ناهمزمان (asynchronously) توسط یک background job پردازش میشوند.
اگر میخواهید بدانید داخل متد PublishDomainEventsAsync چه اتفاقی میافتد 👇
private async Task PublishDomainEventsAsync()
{
var domainEvents = ChangeTracker
.Entries<Entity>()
.Select(entry => entry.Entity)
.SelectMany(entity =>
{
var domainEvents = entity.GetDomainEvents();
entity.ClearDomainEvents();
return domainEvents;
})
.ToList();
foreach (var domainEvent in domainEvents)
{
await _publisher.Publish(domainEvent);
}
}
🧩 نحوهٔ Handle کردن Domain Eventها
با تمام زیرساختی که تا اینجا ایجاد کردیم،
اکنون آمادهایم که Handler مربوط به Domain Eventها را پیادهسازی کنیم.
خوشبختانه این مرحله سادهترین بخش کار است ✨
کافی است کلاسی بسازید که <INotificationHandler<Tرا پیادهسازی کند
و نوع Domain Event خود را به عنوان پارامتر generic مشخص نمایید.
در مثال زیر، یک Handler برای CourseCompletedDomainEvent داریم
که پس از وقوع Domain Event، یک CourseCompletedIntegrationEvent منتشر میکند
تا سایر سیستمها را مطلع سازد 👇
public class CourseCompletedDomainEventHandler
: INotificationHandler<CourseCompletedDomainEvent>
{
private readonly IBus _bus;
public CourseCompletedDomainEventHandler(IBus bus)
{
_bus = bus;
}
public async Task Handle(
CourseCompletedDomainEvent domainEvent,
CancellationToken cancellationToken)
{
await _bus.Publish(
new CourseCompletedIntegrationEvent(domainEvent.CourseId),
cancellationToken);
}
}
🧠 جمعبندی
Domain Event
ها به شما کمک میکنند تا سیستمی loosely coupled بسازید.
میتوانید از آنها برای جدا کردن منطق اصلی دامنه از اثرات جانبی (side effects) استفاده کنید،
اثراتی که میتوانند به صورت asynchronous مدیریت شوند.
لازم نیست برای پیادهسازی Domain Eventها از صفر شروع کنید؛
میتوانید از ترکیب EF Core و MediatR استفاده کنید تا این قابلیت را بهسادگی بسازید.
باید تصمیم بگیرید که چه زمانی میخواهید Domain Eventها را منتشر کنید:
قبل یا بعد از ذخیره شدن تغییرات در پایگاه داده — هرکدام مزایا و معایب خاص خود را دارند.
من شخصاً ترجیح میدهم بعد از ذخیرهسازی تغییرات Domain Eventها را منتشر کنم
و برای اطمینان از تراکنش اتمیک، از الگوی Outbox استفاده میکنم.
این رویکرد باعث ایجاد eventual consistency میشود،
اما در عین حال قابل اعتمادتر است.
امیدوارم این مطلب برایتان مفید بوده باشد 🙌
🔖هشتگها:
#DomainEvents #EFCore #OutboxPattern #EventDriven #DesignPatterns #LooselyCoupled
«استاد، من کتابهای زیادی خواندهام… اما بیشترشان را فراموش کردهام. پس فایدهی خواندن چیست؟»
این پرسشِ شاگردی کنجکاو بود از استادش.
استاد پاسخی نداد، فقط در سکوت به او نگاه کرد.
چند روز بعد، کنار رودخانهای نشسته بودند.
پیرمرد ناگهان گفت:
«تشنهام. برایم کمی آب بیاور… اما با آن آبکش قدیمی که آنجاست.»
شاگرد متعجب شد. درخواست عجیبی بود — چطور میشد با آبکشِ پر از سوراخ، آب آورد؟
اما جرئت نکرد مخالفت کند.
آبکش را برداشت و تلاش کرد.
یکبار… دوبار… بارها و بارها…
سریعتر دوید، زاویهاش را عوض کرد، حتی سعی کرد سوراخها را با انگشتهایش بپوشاند.
هیچکدام کارساز نشد. نتوانست حتی یک قطره آب نگه دارد.
خسته و ناامید، آبکش را کنار پای استاد انداخت و گفت:
«متأسفم، نتوانستم. غیرممکن بود.»
استاد با مهربانی به او نگریست و گفت:
«تو شکست نخوردی. به آبکش نگاه کن.»
شاگرد نگاهی انداخت… و چیزی دید.
آبکشِ قدیمی و سیاه و کثیف، حالا میدرخشید.
آب، هرچند در آن نمانده بود، اما بارها و بارها از آن گذشته و شسته بودش تا براق شده بود.
استاد ادامه داد:
«خواندن هم همینگونه است.
مهم نیست اگر هر جزئیات را به خاطر نسپاری،
مهم نیست اگر دانستههایت مثل آب از ذهنت بیرون میروند…
زیرا در هنگام خواندن، ذهنت تازه میشود،
روحت نیرو میگیرد،
افکارت نفس میکشند،
و حتی اگر بلافاصله متوجه نشوی، درونت در حال دگرگونی است.»
این است معنای واقعیِ خواندن —
نه برای پُر کردن حافظه،
بلکه برای شستن و غنیساختنِ روح.
این پرسشِ شاگردی کنجکاو بود از استادش.
استاد پاسخی نداد، فقط در سکوت به او نگاه کرد.
چند روز بعد، کنار رودخانهای نشسته بودند.
پیرمرد ناگهان گفت:
«تشنهام. برایم کمی آب بیاور… اما با آن آبکش قدیمی که آنجاست.»
شاگرد متعجب شد. درخواست عجیبی بود — چطور میشد با آبکشِ پر از سوراخ، آب آورد؟
اما جرئت نکرد مخالفت کند.
آبکش را برداشت و تلاش کرد.
یکبار… دوبار… بارها و بارها…
سریعتر دوید، زاویهاش را عوض کرد، حتی سعی کرد سوراخها را با انگشتهایش بپوشاند.
هیچکدام کارساز نشد. نتوانست حتی یک قطره آب نگه دارد.
خسته و ناامید، آبکش را کنار پای استاد انداخت و گفت:
«متأسفم، نتوانستم. غیرممکن بود.»
استاد با مهربانی به او نگریست و گفت:
«تو شکست نخوردی. به آبکش نگاه کن.»
شاگرد نگاهی انداخت… و چیزی دید.
آبکشِ قدیمی و سیاه و کثیف، حالا میدرخشید.
آب، هرچند در آن نمانده بود، اما بارها و بارها از آن گذشته و شسته بودش تا براق شده بود.
استاد ادامه داد:
«خواندن هم همینگونه است.
مهم نیست اگر هر جزئیات را به خاطر نسپاری،
مهم نیست اگر دانستههایت مثل آب از ذهنت بیرون میروند…
زیرا در هنگام خواندن، ذهنت تازه میشود،
روحت نیرو میگیرد،
افکارت نفس میکشند،
و حتی اگر بلافاصله متوجه نشوی، درونت در حال دگرگونی است.»
این است معنای واقعیِ خواندن —
نه برای پُر کردن حافظه،
بلکه برای شستن و غنیساختنِ روح.
💡Value Objects در .NET (مبانی DDD)
Value Object
ها یکی از اجزای اصلی Domain-Driven Design (DDD) هستند.DDD یک رویکرد در توسعهٔ نرمافزار است که برای حل مسائل در دامنههای پیچیده (complex domains) استفاده میشود.
Value Object
ها مجموعهای از مقادیر اولیه (primitive values) و قوانین یا محدودیتهای مرتبط (invariants) را در خود نگه میدارند.
چند نمونهٔ رایج از Value Objectها:
💰 Money (که شامل amount و currency است)
📅 DateRange (که شامل start date و end date است)
🧠 Value Objectها چه هستند؟
بیایید با تعریف اصلی از کتاب Domain-Driven Design شروع کنیم:
شیئی که نمایانگر جنبهای توصیفی از دامنه باشد و هیچ هویت مفهومی (conceptual identity) نداشته باشد، Value Object نامیده میشود.
این اشیاء برای نمایش عناصری از طراحی استفاده میشوند که برای ما فقط به خاطر ماهیتشان مهماند، نه به خاطر اینکه چه کسی یا کدام نمونه هستند.
— Eric Evans
Value Object
ها با Entityها تفاوت دارند؛
زیرا هیچ مفهوم هویتی (identity) ندارند.
در واقع، آنها نوعهای اولیه (primitive types) را در دامنه کپسوله میکنند و از primitive obsession جلوگیری میکنند.
✨ ویژگیهای اصلی Value Objectها
دو ویژگی کلیدی در همهٔ Value Objectها وجود دارد:
Immutable بودن :
پس از ایجاد، مقدار آنها دیگر تغییر نمیکند.
نداشتن Identity : هویت مستقل یا کلید یکتا ندارند.
یک ویژگی مهم دیگر نیز وجود دارد:
⚖️ برابری ساختاری (Structural Equality)
دو Value Object زمانی برابر محسوب میشوند که مقادیر درونی آنها یکسان باشد.
البته در عمل، این ویژگی کماهمیتتر از دو مورد قبلی است،
اما در بعضی موقعیتها لازم است که تنها برخی از مقادیر داخلی در مقایسهٔ برابری در نظر گرفته شوند.
🧱 پیادهسازی Value Objectها
مهمترین ویژگی Value Objectها تغییرناپذیری (immutability) است. مقادیر یک Value Object پس از ساخته شدن، دیگر نمیتوانند تغییر کنند.
اگر بخواهید یکی از مقادیر را تغییر دهید، باید کل Value Object را جایگزین کنید.
در مثال زیر، یک موجودیت به نام Booking داریم که شامل مقادیر ابتدایی (primitive values) برای آدرس و تاریخ شروع و پایان رزرو است:
public class Booking
{
public string Street { get; init; }
public string City { get; init; }
public string State { get; init; }
public string Country { get; init; }
public string ZipCode { get; init; }
public DateOnly StartDate { get; init; }
public DateOnly EndDate { get; init; }
}
میتوانیم این مقادیر اولیه را با دو Value Object به نامهای Address و DateRange جایگزین کنیم:
public class Booking
{
public Address Address { get; init; }
public DateRange Period { get; init; }
}
اما چطور باید Value Objectها را پیادهسازی کرد؟
⚙️ استفاده از C# Records
در #C میتوان از recordها برای نمایش Value Objectها استفاده کرد. Recordها بهصورت پیشفرض immutable هستند و از برابری ساختاری (structural equality) پشتیبانی میکنند — دو ویژگیای که دقیقاً برای Value Objectها نیاز داریم.
برای مثال، میتوانیم یک Value Object به نام Address را با استفاده از record و primary constructor تعریف کنیم:
public record Address(
string Street,
string City,
string State,
string Country,
string ZipCode);
مزیت این روش در اختصار و سادگی آن است.
اما زمانی که بخواهید قوانین (invariants) را هنگام ساخت Value Object اعمال کنید، باید سازندهٔ خصوصی (private constructor) تعریف کنید — که در این حالت مزیت اختصار از بین میرود.
مشکل دیگر در استفاده از recordها، امکان دور زدن این قوانین از طریق عبارت with است.
در مثال زیر، پیادهسازی کاملتر Address را میبینید که در آن یک متد factory برای ایجاد نمونهها استفاده شده است:
روش جایگزین برای پیادهسازی Value Objectها، استفاده از یک کلاس پایه به نام ValueObject است.
این کلاس، وظیفهٔ مقایسهٔ ساختاری (structural equality) را از طریق متد انتزاعی GetAtomicValues بر عهده دارد.
کلاسهای مشتقشده از ValueObject باید این متد را پیادهسازی کنند و اجزایی را که در مقایسهٔ برابری مؤثر هستند، مشخص نمایند.
صراحت (Explicitness): بهوضوح مشخص است که کدام کلاسها در دامنه (domain) شما نقش Value Object دارند.
کنترل بر اجزای برابری: میتوانید دقیقاً تعیین کنید چه مقادیری باید در مقایسهٔ برابری در نظر گرفته شوند.
در زیر یک پیادهسازی نمونه از کلاس پایهٔ ValueObject را مشاهده میکنید که در بسیاری از پروژهها کاربرد دارد:
در ادامه، Address را به عنوان یک Value Object پیادهسازی کردهایم که از ValueObject ارثبری میکند و متد GetAtomicValues را برای مقایسهٔ اجزای داخلی بازنویسی میکند:
public record Address
{
private Address(
string street,
string city,
string state,
string country,
string zipCode)
{
Street = street;
City = city;
State = state;
Country = country;
ZipCode = zipCode;
}
public string Street { get; init; }
public string City { get; init; }
public string State { get; init; }
public string Country { get; init; }
public string ZipCode { get; init; }
public static Result<Address> Create(
string street,
string city,
string state,
string country,
string zipCode)
{
// بررسی معتبر بودن آدرس
return new Address(street, city, state, country, zipCode);
}
}
🏗 کلاس پایه (Base Class)
روش جایگزین برای پیادهسازی Value Objectها، استفاده از یک کلاس پایه به نام ValueObject است.
این کلاس، وظیفهٔ مقایسهٔ ساختاری (structural equality) را از طریق متد انتزاعی GetAtomicValues بر عهده دارد.
کلاسهای مشتقشده از ValueObject باید این متد را پیادهسازی کنند و اجزایی را که در مقایسهٔ برابری مؤثر هستند، مشخص نمایند.
✨ مزایای استفاده از کلاس پایه
صراحت (Explicitness): بهوضوح مشخص است که کدام کلاسها در دامنه (domain) شما نقش Value Object دارند.
کنترل بر اجزای برابری: میتوانید دقیقاً تعیین کنید چه مقادیری باید در مقایسهٔ برابری در نظر گرفته شوند.
در زیر یک پیادهسازی نمونه از کلاس پایهٔ ValueObject را مشاهده میکنید که در بسیاری از پروژهها کاربرد دارد:
public abstract class ValueObject : IEquatable<ValueObject>
{
public static bool operator ==(ValueObject? a, ValueObject? b)
{
if (a is null && b is null)
{
return true;
}
if (a is null || b is null)
{
return false;
}
return a.Equals(b);
}
public static bool operator !=(ValueObject? a, ValueObject? b) =>
!(a == b);
public virtual bool Equals(ValueObject? other) =>
other is not null && ValuesAreEqual(other);
public override bool Equals(object? obj) =>
obj is ValueObject valueObject && ValuesAreEqual(valueObject);
public override int GetHashCode() =>
GetAtomicValues().Aggregate(
default(int),
(hashcode, value) =>
HashCode.Combine(hashcode, value.GetHashCode()));
protected abstract IEnumerable<object> GetAtomicValues();
private bool ValuesAreEqual(ValueObject valueObject) =>
GetAtomicValues().SequenceEqual(valueObject.GetAtomicValues());
}
🧩 نمونه پیادهسازی Value Object آدرس
در ادامه، Address را به عنوان یک Value Object پیادهسازی کردهایم که از ValueObject ارثبری میکند و متد GetAtomicValues را برای مقایسهٔ اجزای داخلی بازنویسی میکند:
public sealed class Address : ValueObject
{
public string Street { get; init; }
public string City { get; init; }
public string State { get; init; }
public string Country { get; init; }
public string ZipCode { get; init; }
protected override IEnumerable<object> GetAtomicValues()
{
yield return Street;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
}
}
🧠 چه زمانی از Value Objectها استفاده کنیم؟
من از Value Objectها برای حل مشکل primitive obsession و کپسولهسازی (encapsulation) قوانین دامنه (domain invariants) استفاده میکنم.
کپسولهسازی بخش مهمی از هر مدل دامنه است. شما نباید بتوانید یک Value Object را در حالت نامعتبر ایجاد کنید.
Value Object
ها همچنین type safety را فراهم میکنند. به امضای متد زیر دقت کنید:
public interface IPricingService
{
decimal Calculate(Apartment apartment, DateOnly start, DateOnly end);
}
حالا به امضای متد زیر نگاه کنید که در آن از Value Objectها استفاده شده است. میتوانید ببینید که این نسخه از IPricingService بسیار واضحتر (explicit) است. همچنین از مزیت type safety برخوردار میشوید. هنگام کامپایل کد، Value Objectها احتمال بروز خطا را کاهش میدهند.
public interface IPricingService
{
PricingDetails Calculate(Apartment apartment, DateRange period);
}
در ادامه چند نکته دیگر را ببینید که باید هنگام تصمیمگیری برای استفاده از Value Objectها در نظر بگیرید:
• پیچیدگی قوانین (invariants): اگر نیاز به اعمال قوانین پیچیده دارید، استفاده از Value Object را در نظر بگیرید.
• تعداد مقادیر اولیه (primitives): زمانی که تعداد زیادی مقدار اولیه وجود دارد، استفاده از Value Object منطقی است.
• تعداد تکرارها: اگر نیاز دارید قوانین را فقط در چند نقطه از کد اعمال کنید، میتوانید بدون Value Object هم آن را مدیریت کنید.
💾 ذخیرهسازی Value Objectها با EF Core
Value Object
ها بخشی از موجودیتهای دامنه (domain entities) هستند و باید در پایگاه داده ذخیره شوند.
در اینجا نحوه استفاده از EF Owned Types و Complex Types برای ذخیرهسازی Value Objectها آورده شده است.
🧱 Owned Types
Owned Type
ها را میتوان با فراخوانی متد OwnsOne در هنگام پیکربندی موجودیت تنظیم کرد.
این کار به EF اعلام میکند که باید Value Objectهای Address و Price را در همان جدول موجودیت Apartment ذخیره کند.
Value Object
ها با ستونهای اضافی در جدول apartments نمایش داده میشوند.
public void Configure(EntityTypeBuilder<Apartment> builder)
{
builder.ToTable("apartments");
builder.OwnsOne(property => property.Address);
builder.OwnsOne(property => property.Price, priceBuilder =>
{
priceBuilder.Property(money => money.Currency)
.HasConversion(
currency => currency.Code,
code => Currency.FromCode(code));
});
}
چند نکته در مورد Owned Typeها:
• Owned Typeها دارای یک کلید پنهان هستند.
• از نوع اختیاری (nullable) پشتیبانی نمیکنند.
• مجموعههای متعلق پشتیبانی میشوند و با OwnsMany قابل پیکربندی هستند.
•Table splitting
به شما اجازه میدهد Owned Typeها را در جدول جداگانه ذخیره کنید.
🧩 Complex Types
ویژگی جدیدی در EF هستند که در 8 NET. در دسترساند.
آنها با مقدار کلید (key value) شناسایی یا پیگیری نمیشوند.
اینها باید بخشی از نوع موجودیت (entity type) باشند.Complex Type ها برای نمایش Value Objectها در EF مناسبتر هستند.
در اینجا نحوه پیکربندی Address به عنوان یک Complex Type را میبینید:
public void Configure(EntityTypeBuilder<Apartment> builder)
{
builder.ToTable("apartments");
builder.ComplexProperty(property => property.Address);
}
چند محدودیت برای Complex Typeها وجود دارد:
🔹️ از مجموعهها (collections) پشتیبانی نمیکنند.
🔹️ از مقادیر اختیاری (nullable values) پشتیبانی نمیکنند.
🧾 نتیجهگیری
Value Object
ها به شما کمک میکنند تا یک مدل دامنه غنی طراحی کنید.
میتوانید از آنها برای حل مشکل primitive obsession و کپسولهسازی قوانین دامنه استفاده کنید.Value Objectها با جلوگیری از ایجاد اشیای دامنه نامعتبر، احتمال خطا را کاهش میدهند.
شما میتوانید برای نمایش Value Objectها از record یا کلاس پایه ValueObject استفاده کنید.انتخاب بین آنها باید بر اساس نیازها و پیچیدگی دامنه شما باشد.
من معمولاً بهصورت پیشفرض از record استفاده میکنم مگر زمانی که به ویژگیهای خاص کلاس پایه نیاز داشته باشم.
برای مثال، کلاس پایه زمانی مفید است که بخواهید اجزای برابری (equality components) را کنترل کنید.
🔖هشتگها:
#DomainDrivenDesign #ValueObject #CleanCode #EntityVsValueObject #ImmutableObjects