🛡 ساخت APIهای امن با Role-Based Access Control در ASP.NET Core
🔐 در Authentication به شما میگوید کاربر کیست،
اما Authorization به شما میگوید کاربر چه کاری میتواند انجام دهد.
بیشتر توسعهدهندگان NET. در ابتدا با بررسیهای سادهی نقش (Role-Based Checks) شروع میکنند:
«آیا این کاربر Admin است؟» 👀
اما وقتی برنامه رشد میکند، خیلی زود متوجه میشوید که فقط نقشها کافی نیستند.
به مجوزهای جزئیتر (Granular Permissions) نیاز دارید که بتوان آنها را بهصورت انعطافپذیر ترکیب و تخصیص داد.
اینجاست که Role-Based Access Control (RBAC) میدرخشد. ✨
بهجای اینکه در همهجا در کد نقشها را چک کنید، شما مجوزهای خاص (Permissions) را تعریف میکنید
و نقشها را طوری طراحی میکنید که آن مجوزها را حمل کنند.
ممکن است یک کاربر نقش Manager داشته باشد،
اما چیزی که اهمیت دارد این است که آیا او مجوز users:delete را دارد یا نه. 🧾
بیایید ببینیم چطور میتوانیم یک سیستم احراز مجوز انعطافپذیر و مبتنی بر مجوز در ASP.NET Core بسازیم. 🚀
🧩 درک اجزای اصلی RBAC
مدل RBAC از سه جزء کلیدی تشکیل شده است که با هم کار میکنند:
👤 Users → اختصاص داده میشوند به → 🧑💼 Roles → که شامل هستند از → 🔑 Permissions
🔄 جریان کار به این صورت است:
🔹️Users (کاربران):
افراد واقعی که از سیستم شما استفاده میکنند.
🔹️Roles (نقشها):
گروهی از مجوزهای مرتبط (مثل Admin، Manager، Editor).
🔹️Permissions (مجوزها):
عملیات خاصی که کاربران میتوانند انجام دهند
(مثل users:read، orders:create، reports:delete).
✨ زیبایی RBAC در انعطافپذیری آن است.
میتوانید چندین نقش به یک کاربر اختصاص دهید
و نقشها را بدون تغییر در تخصیص کاربران ویرایش کنید.
به همهی Managerها میخواهید امکان export گزارشها بدهید؟
فقط مجوز reports:export را به نقش Manager اضافه کنید ✅
این روش بسیار قابل نگهداریتر است
تا اینکه بخواهید در کد بررسی کنید که آیا کسی Admin یا Super Manager است.
علاوه بر این، RBAC یک نقطهی گسترش اضافی فراهم میکند:
میتوانید برای برخی کاربران مجوزهای سفارشی ایجاد کنید
بدون اینکه نیاز به ساخت نقشهای جدید باشد. 💡
⚙️ ساخت یک Authorization Handler سفارشی در ASP.NET Core
در سیستم احراز مجوز (Authorization) در ASP.NET Core، مفاهیم اصلی بر پایهی Policyها و Requirementها ساخته میشوند.
در این بخش، میخواهیم یک Handler سفارشی بسازیم که مجوزها (Permissions) را از داخل Claims کاربر بررسی کند. 🔍
🧠 کد پیادهسازی Handler سفارشی
public class PermissionAuthorizationRequirement(params string[] allowedPermissions)
: AuthorizationHandler<PermissionAuthorizationRequirement>, IAuthorizationRequirement
{
public string[] AllowedPermissions { get; } = allowedPermissions;
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
foreach (var permission in requirement.AllowedPermissions)
{
bool found = context.User.FindFirst(c =>
c.Type == CustomClaimTypes.Permission &&
c.Value == permission) is not null;
if (found)
{
context.Succeed(requirement);
break;
}
}
return Task.CompletedTask;
}
}
🧩 در پشت صحنه چه اتفاقی میافتد؟
این کلاس در واقع هر دو نقش Requirement و Handler را با هم ترکیب کرده است —
یعنی هم مشخص میکند چه مجوزهایی لازماند، و هم چگونه باید بررسی شوند.
این کار باعث میشود منطق مرتبط در یک جا نگهداری شود و از تکرار (Boilerplate) جلوگیری کند.
هندلر در میان Claims کاربر جستوجو میکند تا Claimی با نوع Permission پیدا کند
که مقدار آن با یکی از مجوزهای موردنیاز مطابقت داشته باشد.
این عمل به صورت OR است — یعنی اگر کاربر حتی یکی از مجوزهای خواستهشده را داشته باشد، اجازهی دسترسی خواهد داشت ✅
اگر Claim موردنظر یافت شود، با فراخوانی
context.Succeed(requirement)
به ASP.NET Core اطلاع میدهیم که شرط تأیید شده است، و دیگر نیازی به بررسی بقیه نیست.
در صورت نیاز، میتوانید این رفتار را به AND تغییر دهید تا تمام مجوزها الزامی باشند 🔒
🏷 تعریف Claim Type سفارشی
برای شناسایی نوع مجوز در Claims، ابتدا باید نوع Claim خود را تعریف کنید:
public static class CustomClaimTypes
{
public const string Permission = "permission";
}
🪪 افزودن Claims هنگام صدور JWT Token
هنگام ساخت توکن JWT یا تنظیم Claims کاربر، میتوانید مجوزها را از دیتابیس خوانده و در Claims اضافه کنید:
var permissions = await (
from role in dbContext.Roles
join permission in dbContext.RolePermissions on role.Id equals permission.RoleId
where roles.Contains(role.Name)
select permission.Name)
.Distinct()
.ToArrayAsync();
List<Claim> claims =
[
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
..roles.Select(r => new Claim(ClaimTypes.Role, r)),
..permissions.Select(p => new Claim(CustomClaimTypes.Permission, p))
];
var tokenDenoscriptor = new SecurityTokenDenoscriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(configuration.GetValue<int>("Jwt:ExpirationInMinutes")),
SigningCredentials = credentials,
Issuer = configuration["Jwt:Issuer"],
Audience = configuration["Jwt:Audience"]
};
var tokenHandler = new JsonWebTokenHandler();
string accessToken = tokenHandler.CreateToken(tokenDenoscriptor);
✨ ایجاد Clean APIها با استفاده از Extension Methodها در ASP.NET Core
کار با Authorization Policyهای خام درست است ولی معمولاً کد را طولانی و پیچیده میکند.
برای بهبود تجربهٔ توسعهدهنده، میتوانیم از Extension Methodها استفاده کنیم تا کد خواناتر و تمیزتر شود. 🧩
🧠 ایجاد Extension Method برای مجوزها
public static class PermissionExtensions
{
public static void RequirePermission(
this AuthorizationPolicyBuilder builder,
params string[] allowedPermissions)
{
builder.AddRequirements(new PermissionAuthorizationRequirement(allowedPermissions));
}
}
🔹 با این متد، میتوانید بهصورت مستقیم و تمیز در Policyها از ()RequirePermission استفاده کنید.
⚡️ استفاده در Minimal APIها
public static class Permissions
{
public const string UsersRead = "users:read";
public const string UsersUpdate = "users:update";
public const string UsersDelete = "users:delete";
}
app.MapGet("me", async (ApplicationDbContext dbContext, ClaimsPrincipal User) =>
{
var user = await dbContext.Users
.AsNoTracking()
.Where(u => u.Id == int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!))
.Select(u => new UserDto
{
u.Id,
u.Email,
u.FirstName,
u.LastName
})
.SingleOrDefaultAsync();
return Results.Ok(user);
})
.RequireAuthorization(policy => policy.RequirePermission(Permissions.UsersRead));
💡 حالا endpointها تمیز، خوانا و کاملاً منطبق با الگوی Clean Architecture هستند.
🧩 استفاده در MVC Controllerها
برای MVC Controllerها میتوانیم Attribute اختصاصی بسازیم تا کار با مجوزها سادهتر شود:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RequirePermissionAttribute(params string[] permissions) : AuthorizeAttribute
{
public RequirePermissionAttribute(params string[] permissions)
: base(policy: string.Join(",", permissions))
{
}
}
🔧 سپس Policyها را در DI Container ثبت میکنیم:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("users:read", policy => policy.RequirePermission(Permissions.UsersRead))
.AddPolicy("users:update", policy => policy.RequirePermission(Permissions.UsersUpdate));
📦 حالا استفاده از آن در Controller بسیار تمیز و ساده است:
[RequirePermission(Permissions.UsersUpdate)]
public async Task<IActionResult> UpdateUser(int id, UpdateUserRequest request)
{
// Your logic here
}
🚀 Extension Points برای محیط Production
پیادهسازی بالا کاملاً کاربردی است، اما میتوان آن را در محیطهای Production گسترش داد.
دو نقطهٔ کلیدی برای توسعه وجود دارد:
🧱 Type-Safe Permissions با استفاده از Enum
بهجای استفاده از رشتههای جادویی (Magic Strings)، میتوان از Enumها استفاده کرد تا از بررسی در زمان کامپایل (Compile-Time Safety) بهره ببریم:
public enum Permission
{
UsersRead,
UsersUpdate,
UsersDelete,
OrdersCreate,
ReportsExport
}
سپس هنگام صدور JWT Token یا بررسی مجوز،
باید Enum را به string تبدیل کنید و هنگام خواندن Claims مجدداً آن را به Enum برگردانید. 🔁
این کار باعث میشود اشتباهات تایپی و ناسازگاریهای احتمالی حذف شوند،
و سیستم امنتر، خواناتر و مقیاسپذیرتر شود. 💪
✨ Server-Side Permission Resolution در ASP.NET Core
در بسیاری از پروژهها، توسعهدهندگان تمام Permissionها را درون JWT Token ذخیره میکنند.
اما این روش باعث افزایش حجم Token و کاهش امنیت میشود.
راه بهتر این است که مجوزها را در سمت سرور (Server-Side) واکشی کنیم. ⚙️
🧠 استفاده از IClaimsTransformation برای افزودن Permissionها در سمت سرور
public class PermissionClaimsTransformation(IPermissionService permissionService)
: IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (principal.Identity?.IsAuthenticated != true)
{
return principal;
}
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == null)
{
return principal;
}
// واکشی مجوزها از دیتابیس و سپس ذخیره در Cache
// نکته مهم: حتماً نتایج را کش کنید تا در هر درخواست کوئری تکراری به دیتابیس نرود
var permissions = await permissionService.GetUserPermissionsAsync(userId);
var claimsIdentity = (ClaimsIdentity)principal.Identity;
foreach (var permission in permissions)
{
claimsIdentity.AddClaim(new Claim(CustomClaimTypes.Permission, permission));
}
return principal;
}
}
📦 سپس این کلاس را در DI Container ثبت میکنیم:
builder.Services.AddScoped<IClaimsTransformation, PermissionClaimsTransformation>();
🔹 با این روش، JWT شما سبک و امن باقی میماند،
در حالی که Authorization همچنان سریع و مبتنی بر Claims انجام میشود. ⚡️
🧩 جمعبندی (Takeaway)
الگوی RBAC (Role-Based Access Control)
فرآیند Authorization را از یک دردسر نگهداری به یک سیستم منعطف و مقیاسپذیر تبدیل میکند. 🚀
✅ از Permissions شروع کنید، نه Roles
تعریف کنید کاربر چه عملیاتی میتواند انجام دهد، نه اینکه چه نقشی دارد.
✅ Custom Authorization Handlerها
کنترل کامل روی نحوهٔ اعتبارسنجی مجوزها به شما میدهند.
✅ Extension Methodها
کد را تمیز، منسجم و خوانا میکنند.
✅ Type-Safe Enumها + Server-Side Permission Resolution
کد را پایدارتر، Tokenها را سبکتر و سیستم را قابل نگهداریتر میکنند.
✨ نتیجه؟
یک سیستم Authorization تمیز، تستپذیر، و منعطف
که بهسادگی با رشد برنامهٔ شما سازگار میشود. 💪
🔖هشتگها:
#ASPNetCore #RBAC #Authorization #DotNet #CleanArchitecture #CSharp
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