ءBind و مقداردهی برای کلاس abstract🔧
ءBind اجازه میدهد که یک کلاس abstract مقداردهی شود.
کد زیر از کلاس abstract زیر استفاده میکند:
namespace ConfigSample.Options;
public abstract class SomethingWithAName
{
public abstract string? Name { get; set; }
}
public class NameTitleOptions(int age) : SomethingWithAName
{
public const string NameTitle = "NameTitle";
public override string? Name { get; set; }
public string Title { get; set; } = string.Empty;
public int Age { get; set; } = age;
}
کد زیر مقداردهی NameTitleOptions را نمایش میدهد:
public class Test33Model : PageModel
{
private readonly IConfiguration Configuration;
public Test33Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
var nameTitleOptions = new NameTitleOptions(22);
Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);
return Content($"Title: {nameTitleOptions.Title} \n" +
$"Name: {nameTitleOptions.Name} \n" +
$"Age: {nameTitleOptions.Age}"
);
}
}
تفاوت Bind و Get ⚖️
Bind:
• امکان مقداردهی یک کلاس abstract را میدهد.
• نیازی به ساخت instance ندارد.
Get<>:
• باید خودش یک instance بسازد.
• بنابراین فقط روی انواع concrete کار میکند.
Options Pattern🔖
روش دیگر هنگام استفاده از Options Pattern این است که سکشن Position را Bind کرده و آن را به Service Container اضافه کنیم.
کد زیر، کلاس PositionOptions را با Configure به DI اضافه میکند:
using ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
var app = builder.Build();
اکنون میتوانیم از طریق DI تنظیمات را بخوانیم:
public class Test2Model : PageModel
{
private readonly PositionOptions _options;
public Test2Model(IOptions<PositionOptions> options)
{
_options = options.Value;
}
public ContentResult OnGet()
{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}
در این روش، تغییرات فایل JSON بعد از شروع برنامه خوانده نمیشوند.
برای پشتیبانی از تغییرات runtime باید از IOptionsSnapshot استفاده کنید. 🔄
Options Interfaces 🔍
1️⃣ IOptions<TOptions>
پشتیبانی نمیکند از:
• خواندن تغییرات configuration بعد از start
• ءNamed options
• ءSingleton است → در تمام طول برنامه ثابت است.
• میتواند در هر service lifetime inject شود.
2️⃣ IOptionsSnapshot<TOptions>
• در سناریوهایی مفید است که تنظیمات باید در هر درخواست مجدداً محاسبه شوند.
• ءScoped است → نمیتوان آن را در یک Singleton inject کرد.
• از Named options پشتیبانی میکند.
• برای خواندن تغییرات Runtime مناسب است. 🔁
3️⃣ IOptionsMonitor<TOptions>
• برای دریافت options و مدیریت Change Notification استفاده میشود.
• ءSingleton است.
پشتیبانی میکند از:
• Reloadable configuration
• Named options
• Selective invalidation (با IOptionsMonitorCache)
4️⃣ IOptionsFactory<TOptions>
مسئول ایجاد instanceهای جدید گزینههاست.
پیکربندیها را (IConfigureOptions و IPostConfigureOptions) اجرا میکند.
5️⃣ IOptionsMonitorCache<TOptions>
• کش داخلی IOptionsMonitor
• میتواند یک گزینه را حذف کند تا مقدار جدید دوباره محاسبه شود.
• امکان اضافهکردن دستی مقدار جدید با TryAdd
• متد Clear برای ریست تمام named options
استفاده از IOptionsSnapshot برای خواندن دادههای بهروزشده ⚙️📄
IOptionsSnapshot<TOptions>🧪:
🔹️تنظیمات (Options) در هر درخواست یک بار محاسبه میشوند و برای مدت زمان همان درخواست کش میشوند.
🔹️چون یک سرویس Scoped است و در هر درخواست دوباره محاسبه میشود، ممکن است باعث هزینهٔ کارایی شود.
🔹️زمانی تغییرات پیکربندی را پس از شروع برنامه میخواند که Provider مربوطه از بارگذاری مجدد پشتیبانی کند.
تفاوت IOptionsMonitor با IOptionsSnapshot🖇:
🔸️ءIOptionsMonitor یک Singleton است و همیشه مقدار لحظهای تنظیمات را ارائه میدهد؛ مناسب برای سرویسهای Singleton.
🔸️ءIOptionsSnapshot یک Scoped است و هنگام ایجاد شدن، یک Snapshot از تنظیمات میگیرد؛ مناسب برای سرویسهای Transient و Scoped.
نمونهٔ استفاده از <IOptionsSnapshot<TOptions🧪
public class TestSnapModel : PageModel
{
private readonly MyOptions _snapshotOptions;
public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
{
_snapshotOptions = snapshotOptionsAccessor.Value;
}
public ContentResult OnGet()
{
return Content($"Option1: {_snapshotOptions.Option1} \n" +
$"Option2: {_snapshotOptions.Option2}");
}
}
ثبت MyOptions در DI 🧩
using SampleApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
var app = builder.Build();
در این حالت، تغییرات فایل JSON پس از شروع برنامه خوانده میشوند.
IOptionsMonitor 🛰
ثبت MyOptions در سرویسها (مانند قبل)
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
نمونهٔ استفاده از <IOptionsMonitor<TOptions 📡
public class TestMonitorModel : PageModel
{
private readonly IOptionsMonitor<MyOptions> _optionsDelegate;
public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
{
_optionsDelegate = optionsDelegate;
}
public ContentResult OnGet()
{
return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
$"Option2: {_optionsDelegate.CurrentValue.Option2}");
}
}
در این حالت نیز، تغییرات JSON بعد از شروع برنامه خوانده میشوند.
استفاده از ConfigurationKeyName برای تعیین کلید سفارشی 🔑✨
بهطور پیشفرض، نام پراپرتی کلاس Options برابر با نام کلید در پیکربندی است.
اگر بین نام پراپرتی و نام کلید تفاوت وجود دارد، میتوان از ConfigurationKeyName استفاده کرد.
مواقع استفاده:
• زمانی که نام کلید در پیکربندی یک شناسهٔ معتبر #C نیست.
• یا زمانی که میخواهید نام متفاوتی در کد داشته باشید.
مثال 🎯
public class PositionOptionsWithConfigurationKeyName
{
public const string Position = "Position";
[ConfigurationKeyName("position-noscript")]
public string Title { get; set; } = string.Empty;
[ConfigurationKeyName("position-name")]
public string Name { get; set; } = string.Empty;
}
فایل appsettings.json
{
"Position": {
"position-noscript": "Editor",
"position-name": "Joe Smith"
}
}
در نتیجه،
Title ← مقدار position-noscript
Name ← مقدار position-name
پشتیبانی از Named Options با استفاده از IConfigureNamedOptions 🎯⚙️
🔹️ءNamed Options چه هستند؟
ءNamed Options زمانی مفید هستند که:
• چند بخش متفاوت از configuration نیاز دارند به یک کلاس مشترک Bind شوند.
• نامگذاریها Case-Sensitive هستند.
• بتوانیم چند نسخهٔ متفاوت از یک Options را با Names مختلف مدیریت کنیم.
مثال فایل appsettings.json 📄
{
"TopItem": {
"Month": {
"Name": "Green Widget",
"Model": "GW46"
},
"Year": {
"Name": "Orange Gadget",
"Model": "OG35"
}
}
}در این مثال، دو بخش داریم:
TopItem:Month
TopItem:Year
بهجای تعریف دو کلاس جداگانه، از یک کلاس مشترک استفاده میکنیم:
کلاس مشترک TopItemSettings 🧱
public class TopItemSettings
{
public const string Month = "Month";
public const string Year = "Year";
public string Name { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
}
پیکربندی Named Options در DI 🛠
اینجا دو Named Options میسازیم:
TopItemSettings.Month
TopItemSettings.Year
استفاده از Named Options با IOptionsSnapshot 🔍📦
در کد بالا:
• ءSnapshot مربوط به Month گرفته میشود.
• ءSnapshot مربوط به Year گرفته میشود.
🔸️تمام Options در واقع Named Instance هستند.
🔹️ء<IConfigureOptions<T> برای Default Name اعمال میشود، یعنی نام خالی ("").
🔸️ء<IConfigureNamedOptions<T علاوهبر آنکه IConfigureOptions را هم پیادهسازی میکند، برای Named Options مخصوص استفاده میشود.
🔹️در <IOptionsFactory<TOptions منطق انتخاب نام وجود دارد:
🔸️اگر نام Null باشد یعنی Configure روی همهٔ Named Options اعمال میشود.
🔹️متدهای ConfigureAll و PostConfigureAll از همین رفتار استفاده میکنند.
این باعث میشود بتوانید:
• یک پیکربندی خاص را برای یک نام خاص تنظیم کنید.
• و یک پیکربندی کلی را برای همه نامها اعمال کنید.
چرا OptionsBuilder؟
ء<OptionsBuilder<TOptions فرایند ساخت Named Options را سادهتر میکند چون:
تنها در یک جای اولیه نام option مشخص میشود:
اما در بقیهٔ متدهای Configure نیازی نیست نام را تکرار کنید.
ءValidation Options فقط با OptionsBuilder قابل انجام است.
متدهایی که Service Dependency دریافت میکنند نیز فقط با OptionsBuilder پشتیبانی میشوند.
نمونهٔ استفاده
در بخش Options Validation به کار میرود.
سرویسها را میتوان هنگام پیکربندی options به دو روش از dependency injection دریافت کرد:
ارسال یک configuration delegate به متد Configure روی <OptionsBuilder<TOptions.
ء<OptionsBuilder<TOptions چندین overload از Configure ارائه میدهد که اجازه میدهد تا حداکثر پنج سرویس برای پیکربندی options استفاده شوند:
ایجاد یک نوع (class) که <IConfigureOptions<TOptions یا <IConfigureNamedOptions<TOptions را پیادهسازی کند و ثبت آن بهعنوان سرویس.
توصیه میشود از روش ارسال configuration delegate به Configure استفاده کنید، زیرا ایجاد یک سرویس پیچیدهتر است. ایجاد نوع دقیقاً معادل کاری است که فریمورک هنگام فراخوانی Configure انجام میدهد.
فراخوانی Configure یک سرویس transient از نوع generic IConfigureNamedOptions<TOptions ثبت میکند که دارای سازندهای است که سرویسهای generic مشخصشده را میپذیرد.
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
اینجا دو Named Options میسازیم:
TopItemSettings.Month
TopItemSettings.Year
استفاده از Named Options با IOptionsSnapshot 🔍📦
public class TestNOModel : PageModel
{
private readonly TopItemSettings _monthTopItem;
private readonly TopItemSettings _yearTopItem;
public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
{
_monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
_yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
}
public ContentResult OnGet()
{
return Content($"Month:Name {_monthTopItem.Name} \n" +
$"Month:Model {_monthTopItem.Model} \n\n" +
$"Year:Name {_yearTopItem.Name} \n" +
$"Year:Model {_yearTopItem.Model} \n");
}
}
در کد بالا:
• ءSnapshot مربوط به Month گرفته میشود.
• ءSnapshot مربوط به Year گرفته میشود.
توضیح ساختار Options و NamedOptions 🧩
🔸️تمام Options در واقع Named Instance هستند.
🔹️ء<IConfigureOptions<T> برای Default Name اعمال میشود، یعنی نام خالی ("").
🔸️ء<IConfigureNamedOptions<T علاوهبر آنکه IConfigureOptions را هم پیادهسازی میکند، برای Named Options مخصوص استفاده میشود.
🔹️در <IOptionsFactory<TOptions منطق انتخاب نام وجود دارد:
🔸️اگر نام Null باشد یعنی Configure روی همهٔ Named Options اعمال میشود.
🔹️متدهای ConfigureAll و PostConfigureAll از همین رفتار استفاده میکنند.
این باعث میشود بتوانید:
• یک پیکربندی خاص را برای یک نام خاص تنظیم کنید.
• و یک پیکربندی کلی را برای همه نامها اعمال کنید.
OptionsBuilder API 🏗✨
چرا OptionsBuilder؟
ء<OptionsBuilder<TOptions فرایند ساخت Named Options را سادهتر میکند چون:
تنها در یک جای اولیه نام option مشخص میشود:
services.AddOptions<TOptions>("MyName")اما در بقیهٔ متدهای Configure نیازی نیست نام را تکرار کنید.
ءValidation Options فقط با OptionsBuilder قابل انجام است.
متدهایی که Service Dependency دریافت میکنند نیز فقط با OptionsBuilder پشتیبانی میشوند.
نمونهٔ استفاده
در بخش Options Validation به کار میرود.
استفاده از سرویسهای DI برای پیکربندی Options ⚙️📦
سرویسها را میتوان هنگام پیکربندی options به دو روش از dependency injection دریافت کرد:
ارسال یک configuration delegate به متد Configure روی <OptionsBuilder<TOptions.
ء<OptionsBuilder<TOptions چندین overload از Configure ارائه میدهد که اجازه میدهد تا حداکثر پنج سرویس برای پیکربندی options استفاده شوند:
builder.Services.AddOptions<MyOptions>("optionalName")
.Configure<Service1, Service2, Service3, Service4, Service5>(
(o, s, s2, s3, s4, s5) =>
o.Property = DoSomethingWith(s, s2, s3, s4, s5));ایجاد یک نوع (class) که <IConfigureOptions<TOptions یا <IConfigureNamedOptions<TOptions را پیادهسازی کند و ثبت آن بهعنوان سرویس.
توصیه میشود از روش ارسال configuration delegate به Configure استفاده کنید، زیرا ایجاد یک سرویس پیچیدهتر است. ایجاد نوع دقیقاً معادل کاری است که فریمورک هنگام فراخوانی Configure انجام میدهد.
فراخوانی Configure یک سرویس transient از نوع generic IConfigureNamedOptions<TOptions ثبت میکند که دارای سازندهای است که سرویسهای generic مشخصشده را میپذیرد.
اعتبارسنجی Options ✅🔍
ءOptions validation امکان اعتبارسنجی مقدارهای options را فراهم میکند.
فایل appsettings.json زیر را در نظر بگیرید:
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}کلاس زیر برای Bind شدن به بخش "MyConfig" استفاده میشود و چند قانون DataAnnotations را اعمال میکند:
public class MyConfigOptions
{
public const string MyConfig = "MyConfig";
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public string Key1 { get; set; }
[Range(0, 1000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int Key2 { get; set; }
public int Key3 { get; set; }
}
کد زیر:
ءAddOptions را فراخوانی میکند تا <OptionsBuilder<MyConfigOptions را دریافت کند که به کلاس MyConfigOptions Bind میشود.
ءValidateDataAnnotations را فراخوانی میکند تا اعتبارسنجی با استفاده از DataAnnotations فعال شود.
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();
متد ValidateDataAnnotations در پکیج Microsoft.Extensions.Options.DataAnnotations قرار دارد. برای web appهایی که از SDK نوع Microsoft.NET.Sdk.Web استفاده میکنند، این پکیج بهطور ضمنی از shared framework مرجع میشود.
کد زیر مقدارهای configuration یا خطاهای validation را نمایش میدهد:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyConfigOptions> _config;
public HomeController(IOptions<MyConfigOptions> config,
ILogger<HomeController> logger)
{
_config = config;
_logger = logger;
try
{
var configValue = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
_logger.LogError(failure);
}
}
}
public ContentResult Index()
{
string msg;
try
{
msg = $"Key1: {_config.Value.Key1} \n" +
$"Key2: {_config.Value.Key2} \n" +
$"Key3: {_config.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(msg);
}
}
کد زیر یک قانون پیچیدهتر اعتبارسنجی را با استفاده از یک delegate اعمال میکند:
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Key2 != 0)
{
return config.Key3 > config.Key2;
}
return true;
}, "Key3 must be > than Key2."); // Failure message.
IValidateOptions<TOptions> و IValidatableObject ✨
کلاس زیر رابط <IValidateOptions<TOptions را پیادهسازی میکند:
public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
public MyConfigOptions _config { get; private set; }
public MyConfigValidation(IConfiguration config)
{
_config = config.GetSection(MyConfigOptions.MyConfig)
.Get<MyConfigOptions>();
}
public ValidateOptionsResult Validate(string name, MyConfigOptions options)
{
string? vor = null;
var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
var match = rx.Match(options.Key1!);
if (string.IsNullOrEmpty(match.Value))
{
vor = $"{options.Key1} doesn't match RegEx \n";
}
if ( options.Key2 < 0 || options.Key2 > 1000)
{
vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
}
if (_config.Key2 != default)
{
if(_config.Key3 <= _config.Key2)
{
vor += "Key3 must be > than Key2.";
}
}
if (vor != null)
{
return ValidateOptionsResult.Fail(vor);
}
return ValidateOptionsResult.Success;
}
}
ءIValidateOptions این امکان را فراهم میکند که کد اعتبارسنجی از فایل Program.cs خارج شده و در یک کلاس قرار گیرد. 🧩
با استفاده از کد بالا، اعتبارسنجی در فایل Program.cs با کد زیر فعال میشود:
using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
MyConfigOptions.MyConfig));
builder.Services.AddSingleton<IValidateOptions
<MyConfigOptions>, MyConfigValidation>();
var app = builder.Build();
اعتبارسنجی Options همچنین از IValidatableObject پشتیبانی میکند. برای انجام اعتبارسنجی سطح کلاس در داخل خود کلاس:
رابط IValidatableObject و متد Validate را در کلاس پیادهسازی کنید.
در Program.cs متد ValidateDataAnnotations را فراخوانی کنید. ✔️
ValidateOnStart 🚀
اعتبارسنجی Options اولین باری که یک نمونه از TOption ساخته میشود اجرا میگردد. این یعنی مثلاً زمانیکه اولین دسترسی به IOptionsSnapshot<TOptions>.Value در pipeline یک درخواست رخ میدهد یا زمانیکه IOptionsMonitor<TOptions>.Get(string) فراخوانی میشود. پس از reload شدن تنظیمات، اعتبارسنجی دوباره اجرا میشود.
زمان اجرا از OptionsCache<TOptions> برای کش کردن نمونه ساختهشده استفاده میکند.
برای اجرای اعتبارسنجی هنگام شروع اپلیکیشن (Eager Validation) باید در Program.cs متد زیر را فراخوانی کنید:
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.ValidateOnStart();
Options post-configuration 🛠
ءPost-configuration را میتوان با <IPostConfigureOptions<TOptions انجام داد. Post-configuration پس از اجرای تمام <IConfigureOptions<TOptions انجام میشود:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
ءPostConfigure برای Named Options نیز قابل استفاده است:
برای post-config کردن تمام نمونههای تنظیمات از PostConfigureAll استفاده میشود:
برای دسترسی به <IOptions<TOptions یا <IOptionsMonitor<TOptions در Program.cs، باید از GetRequiredService روی WebApplication.Services استفاده کنید:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
myOptions.Name = "post_configured_name_value";
myOptions.Model = "post_configured_model_value";
});
var app = builder.Build();
برای post-config کردن تمام نمونههای تنظیمات از PostConfigureAll استفاده میشود:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
Access options in Program.cs 🔍
برای دسترسی به <IOptions<TOptions یا <IOptionsMonitor<TOptions در Program.cs، باید از GetRequiredService روی WebApplication.Services استفاده کنید:
var app = builder.Build();
var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
.CurrentValue.Option1;
🔖هشتگها:
#aspnetcore #dotnet #csharp #optionspattern #configuration #softwarearchitecture #cleanarchitecture
Forwarded from tech-afternoon (Amin Mesbahi)
- ساختار و سازماندهی تولید چیه؟
ساختار و سازماندهی تولید نرمافزار رو من ذیل ۳ مولفه اصلی بیان میکنم. یعنی فرهنگ، فرایند، و ابزار:
۱: ابزار:
ابزار را شاید بشه سادهترین مولفه از بین ۳ عامل دونست (هرچند خیلیها همین رو هم به درستی ندارن). ابزار یعنی هر سرویس سختافزاری و نرمافزاری که به شما کمک میکنه تا چرخه عمر نرمافزار رو بهتر مدیریت و تجربه کنید. مثلا:
پلتفرم مناسب برای مستندسازی که جستجوی خوب، نسخهبندی، امکانات امنیتی، کامپلاینس، پشتیبانی از نمودار (بهصورت کد، نه صرفاً تصویر)، و کوئری پویا از سورسکد یا سیستم مدیریت پروژه رو فراهم کنه.
یا سرور/سرویس Version Control و CI/CD درست و حسابی (منظورم TFS 2012 اونم به صورت شلخته نیست)
یا ابزارهای مدرن نظارت (منظورم observability است نه monitoring)
یا سرویس Secret Manager (منظورم KeePass نیست طبیعتا)
یا...
۲: فرایند:
از ابتدای چرخه، چه توسط تیم بومی، چه توسط پیمانکار، و چه بهصورت خریداری محصول؛ باید فرایند داشت. فرایند برای استفاده از ابزارها، برای مستندسازی، برای نگهداشت اطلاعات محرمانه، برای نظارت و بازبینی روالها، حتی برای اینکه چجوری و با چه تناوبی تغییر ورژن کتابخونهها و ابزارها، آسیبپذیریها، تغییر لایسنسها و غیره رو مطلع شیم و در موردشون تصمیم بگیریم. یا اینکه طی چه فرایندی و توسط کی، تضمین کنیم که فرایندها در طول زمان، فراموش یا ناکارآمد نشن. اینها همه و همه فرایندهایی هستن که تدوین و جا انداختنشون هزینه و زمان نیاز داره.
همچنین دانش مورد نیاز برای تدوینشون بارها پیچیدهتر از یاد گرفتن سینتکس فلان زبانه و گاها گرونتر! چون باید متناسب با بضاعت و استعداد سازمان انجام بشه؛ چون گاها حرف و عمل مدیران و کارشناسان سازمانها در التزام به فرایندهای جدید، یکی نیست و یا مقاومتهای مرئی و نامرئی زیادی تجربه خواهیم کرد.
۳: فرهنگ:
ابزار و فرایند، قابل خریداری و استقرار هستن؛ اما موفقیت اونها منوط به پذیرش و التزام عملی تیم است. تجربه نشون میده که بدون فرهنگ مناسب، بهترین ابزارها هم در عمل نادیده گرفته میشن؛ و بهترین فرایندها به حاشیه رانده میرن.
فرهنگسازی، حتی از تدوین و استقرار فرایند هم سختتره! فرهنگ نیروی انسانی و فرهنگ سازمانی چیزی نیست که یک شبه به دست بیاد، محصول قابل خریداری و استقرار سریع نیست!
فرق سازمان با جامعه اینه که شما در جامعه وقت، زیاد داری! بین رنسانس تا انقلاب صنعتی چند قرن زمان برد، ولی یک سازمان چند قرن یا چند سال زمان نداره! حساب دو دو تا چهار تا است؛ دیر بجنبی زیانده و ورشکسته میشی. حتی اگر دولتی باشی، به خودت میای و میبینی ۲,۲۴۵,۶۲۳ نفر کارمند داری که راهی جز تعدیل و جایگزینی درصد بسیار بالایی از اونها نداری... و دلیل اصلیش: «فرهنگ نیروی انسانی» است
همونطور که در ادبیات Enterprise Architecture اومده "Culture Ensures Sustainability" فرهنگ، پایداری رو تضمین میکنه. حالا فرهنگ مهندسی در اینجا به معنی ترکیب سه عامل است:
۱. شایستگی فنی (Technical Competence):
تسلط بر معماری، الگوهای طراحی، و...
۲. تعهد به فرایند (Process Commitment):
پایبندی به code review، testing، و documentation
۳. مهارت در ابزار (Tool Proficiency):
استفاده مؤثر از CI/CD، و observability tools
بعضی گزارشهای McKinsey میگن حدود ۷۰٪ پروژههای تحول دیجیتال، به دلایل فرهنگی و سازمانی شکست میخورن. تجربه شخصی من در ایران این عدد رو حتی بالاتر، و حدود۹۰٪ نشون میده.
تغییر فرهنگ، کندترین و حیاتیترین بخش transformation است و نیازمند ۳-۵ سال زمان، تعهد مدیریت ارشد، و گاهی جایگزینی تدریجی ۲۰-۴۰٪ نیروی انسانی است.
به فرض، اگر فردا تحریم برداشته بشه، فلان شرکتِ استارتاپی دیروز که امروز نسبتا بالغ شده، شاید خیلی زود بتونه از سرویسهای متنوع کلادی بهره بگیره و پرسنلش کاراتر عمل کنن. ولی فلان سازمان دولتی تا سالها درگیر انواع مقاومتها و مباحثات زمانبر درباره تغییرات هرچند ساده است. پس ابزار، مولفهی سادهتر، فرایند مولفهی دشوار، و فرهنگ رو میتونیم مولفهی حیاتی و بسیار بسیار دشوار برشمریم!
چکیده:
- Tools Provide Acceleration, Not Discipline
- Processes Create Predictability
- Culture Ensures Sustainability
اگر از این مطلب استقبال بشه، در ادامه، موضوعات زیر رو هر کدوم ذیل ۳ مولفهای که در این مقدمه عرض کردم در یک نرمافزار انترپرایز بررسی میکنم:
بخش دوم: الزامات توسعه و نگهداری محصول
بخش سوم: الزامات زیرساخت
بخش چهارم: الزامات امنیت
بخش پنجم: الزامات طراحی و نگهداشت محصول
Please open Telegram to view this post
VIEW IN TELEGRAM
بهبود کیفیت کد در #C با Static Code Analysis 🔍✨
نوشتن کد خوب برای هر پروژه نرمافزاری مهم است. این موضوعی است که من نیز عمیقاً به آن اهمیت میدهم. با این حال، تشخیص مشکلات تنها با خواندن تمام کد میتواند دشوار باشد.
خوشبختانه ابزاری وجود دارد که میتواند کمک کند: static code analysis.
این ابزار مثل یک جفت چشم اضافی است که بهطور خودکار کد شما را بررسی میکند. Static code analysis کمک میکند کدی ایمن، قابل نگهداری و باکیفیت در #C بسازید. 👨💻⚙️
در این مقاله قرار است به موارد زیر بپردازیم:
• Static code analysis
• Static analysis در .NET
• یافتن ریسکهای امنیتی
بیایید ببینیم static code analysis چطور میتواند به بهبود کیفیت کد کمک کند. 🚀
Static Code Analysis چیست؟ 🧠
ءStatic code analysis روشی برای بررسی کد بدون اجرای آن است. این روش هرگونه مشکل مرتبط با امنیت، عملکرد، سبک کدنویسی یا بهترین شیوهها را گزارش میکند.
با static code analysis میتوانید "shift left" انجام دهید؛ یعنی مشکلات را در مراحل اولیه توسعه پیدا و برطرف کنید، زمانی که رفع آنها کمهزینهتر است. 🕓➡️🛠
با نوشتن کد باکیفیت، میتوانید سیستمهایی بسازید که قابلاعتمادتر، مقیاسپذیرتر و در طول زمان آسانتر برای نگهداری باشند. سرمایهگذاری روی کیفیت کد در مراحل بعدی پروژه نتیجه خواهد داد. 💡📈
میتوانید static code analysis را داخل CI pipeline یکپارچه کنید تا یک بازخورد سریع دریافت کنید. همچنین میتوان آن را با آزمونهای معماری (Architecture Testing) ترکیب کرد تا استانداردهای کدنویسی بیشتری اعمال شود. 🧪🏗
Static Code Analysis در .NET 🔍⚙️
ءNET. دارای Roslyn analyzerهای داخلی است که کد #C شما را برای مشکلات مربوط به سبک کدنویسی و کیفیت بررسی میکنند. اگر پروژه شما NET 5. یا بالاتر را هدف قرار دهد، code analysis بهصورت پیشفرض فعال است.
بهترین روشی که من برای پیکربندی static code analysis پیدا کردهام، استفاده از Directory.Build.props است. این یک فایل XML است که در آن میتوانید ویژگیهای مشترک پروژهها را پیکربندی کنید. میتوانید فایل Directory.Build.props را در پوشه ریشه قرار دهید تا برای تمام پروژهها اعمال شود. 📁✨
میتوانید TargetFramework، ImplicitUsings، Nullable (nullable reference types)، و غیره را پیکربندی کنید. اما آنچه برای ما مهم است، پیکربندی static code analysis است.
در ادامه برخی ویژگیهایی که میتوانیم پیکربندی کنیم آورده شده است:
• TreatWarningsAsErrors –
همه warningها را به error تبدیل میکند.
• CodeAnalysisTreatWarningsAsErrors–
هشدارهای کیفیت کد (CAxxxx) را به error تبدیل میکند.
• EnforceCodeStyleInBuild –
قوانین مربوط به تحلیل سبک کد ("IDExxxx") را فعال میکند.
• AnalysisLevel –
مشخص میکند کدام analyzerها فعال شوند. مقدار پیشفرض latest است.
• AnalysisMode –
پیکربندی تحلیل کد پیشفرض را تنظیم میکند.
همچنین میتوانیم بستههای NuGet اضافی را در پروژهها نصب کنیم. SonarAnalyzer.CSharp مجموعهای از analyzerهای اضافی دارد که به ما کمک میکند کدی تمیز، ایمن و قابلاعتماد بنویسیم. این کتابخانه توسط همان شرکتی توسعه یافته که SonarQube را ساخته است. 🧪🔒
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Configure code analysis. -->
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup Condition="'$(MSBuildProjectExtension)' != '.dcproj'">
<PackageReference Include="SonarAnalyzer.CSharp" Version="*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>
runtime; build; native; contentfiles; analyzers; buildtransitive
</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
ءAnalyzerهای داخلی NET. و analyzerهای موجود در SonarAnalyzer.CSharp میتوانند بسیار مفید باشند. اما گاهی اوقات ممکن است با تعداد زیادی هشدار در هنگام build باعث ایجاد سروصدا شوند. ⚠️📢
وقتی با قوانین تحلیل کدی مواجه میشوید که از نظر شما مفید نیستند، میتوانید آنها را غیرفعال کنید. میتوانید قوانین تحلیل کد را به صورت جداگانه در فایل editorconfig. پیکربندی کنید.
# S125: Sections of code should not be commented out
dotnet_diagnostic.S125.severity = none
# S1075: URIs should not be hardcoded
dotnet_diagnostic.S1075.severity = none
# S2094: Classes should not be empty
dotnet_diagnostic.S2094.severity = none
# S3267: Loops should be simplified with "LINQ" expressions
dotnet_diagnostic.S3267.severity = none
یافتن (و رفع کردن) ریسکهای امنیتی 🔍🛡
ءStatic code analysis میتواند به شما کمک کند آسیبپذیریهای امنیتی احتمالی را در کدتان شناسایی کنید.
در این مثال، یک PasswordHasher فقط از 10,000 iterations برای تولید یک password hash استفاده میکند.
قانون S5344 از SonarAnalyzer.CSharp این مشکل را شناسایی کرده و به شما هشدار میدهد. حداقل تعداد iterations پیشنهادی 100,000 است. ⚠️
میتوانید به توضیحات قانون S5344 مراجعه کنید تا بیشتر یاد بگیرید:
ذخیرهسازی رمز عبور با hashing ضعیف یک ریسک امنیتی جدی برای اپلیکیشنها است. 🔐❗️
وقتی TreatWarningsAsErrors فعال باشد، build شما تا زمانی که مشکل را حل نکنید fail خواهد شد.
این کار احتمال ورود ریسکهای امنیتی به محیط production را به شدت کاهش میدهد. 🚫🔓
نتیجهگیری 📌
ءStatic code analysis یک ابزار قدرتمند است که من همیشه در تمام پروژههای #C استفاده میکنم.
این ابزار کمک میکند مشکلات را زودتر پیدا کنم، کد قابلاعتمادتر و امنتری داشته باشم و در نهایت زمان و انرژی کمتری صرف کنم.
هرچند راهاندازی اولیه و تنظیم دقیق قوانین ممکن است کمی زمانبر باشد، اما مزایای بلندمدت آن غیرقابل انکار است. ⏳✔️
به خاطر داشته باشید که static code analysis یک ابزار کمکی است و جایگزین سایر فعالیتهای توسعه نمیشود.
وقتی static code analysis را با تکنیکهایی مثل code review, unit testing, و continuous integration ترکیب کنید،
میتوانید یک فرایند توسعهی قدرتمند بسازید که همیشه خروجی باکیفیت تولید کند. 💡🚀
ءStatic code analysis را جدی بگیرید.
نسخهی آیندهی شما (و تیمتان) از شما تشکر خواهد کرد. 🙌💙
آنچه بازنویسی یک پروژهی ۴۰ ساله به من دربارهی توسعهی نرمافزار آموخت 🧓💻⚙️
«وظیفهی شما این است که این سیستم را بازنویسی کنید. تمام عملیات ما روی آن اجرا میشود.
اوه، و این سیستم با APL نوشته شده.»
با این جمله، سفر من برای بازنویسی یک سیستم legacy آغاز شد. برای کسانی که با APL آشنا نیستند: این زبان برنامهنویسی مربوط به دههی ۱۹۶۰ است و بهخاطر نشانهگذاری ریاضی خاص و تواناییهای قدرتمند در array manipulation شناخته میشود. پیدا کردن توسعهدهندهای که امروز APL بداند تقریباً به سختی پیدا کردن یک floppy disk drive در یک کامپیوتر مدرن است. 🥲💾
این سیستم طی چهار دهه رشد کرده بود. ابتدا یک ابزار سادهی مدیریت موجودی بود، اما کمکم به یک سیستم ERP جامع تبدیل شد.
بیش از 460+ جدول دیتابیس. بیشمار Business rule در دل کد.Integrationهای پیچیده در هر بخش از فرآیند کسبوکار.
این سیستم ستون فقرات یک عملیات تولیدی بود که بیش از ۱۰ میلیون دلار درآمد سالانه ایجاد میکرد. 🏭💰
ماموریت ما واضح اما ترسناک بود:
مدرنسازی این سیستم با استفاده از dotNET,PostgreSQL, و React. ⚡️
اما یک نکتهی حیاتی وجود داشت:
کسبوکار باید بدون کوچکترین وقفه ادامه پیدا میکرد.
بدون downtime.
بدون data loss.
بدون اختلال در عملیات روزانه. ⛔️🕒
این فقط یک چالش فنی نبود. یک درس بزرگ بود دربارهی مدیریت پیچیدگی، فهم فرآیندهای legacy، و هدایت تعاملات سازمانی.
این داستان و درسهای آموختهشدهی آن است.
وضعیت اولیه: درک Legacy 🏛🧠
اولین چالش این بود که بفهمیم این سیستم عظیم چطور کار میکند. این codebase طی ۴۰ سال بهصورت ارگانیک رشد کرده بود و تنها توسط یک تیم توسعهی ثابت نگهداری میشد. اعضای این تیم حالا در دههی ۶۰ زندگی بودند و قصد بازنشستگی داشتند.
وقتی وارد اولین بازبینی کد شدیم، انگار یک کپسول زمان باز کرده باشیم.
سینتکس بسیار فشردهی APL باعث میشد منطق تجاری پیچیده فقط در چند خط نوشته شود.
زیبا، اگر میتوانستید آن را بخوانید.
وحشتناک، اگر نمیتوانستید.
و اکثر ما نمیتوانستیم. 😅📜
تیم اصلی در مرحلهی انتقال دانش، بیقیمت بود. آنها تمام نکتهها، edge caseها، و business ruleهایی را که طی دههها اضافه شده بود، از حفظ بودند. اما تنها بخش محدودی از این دانش را میتوان از طریق گفتگو منتقل کرد. Documentation کم بود. هر آنچه وجود داشت منسوخ شده بود.
مستندات واقعی در ذهن توسعهدهندگان اصلی بود.
ما هفتهها صرف map کردن کارکردهای سیستم کردیم:
• فرآیند اصلی تولید در بیش از 50+ جدول با وابستگیهای پیچیده پخش شده بود
• مدیریت موجودی تقریباً به همهی بخشهای دیگر سیستم وصل شده بود
• ابزارهای گزارشگیری custom طی دههها ساخته شده بودند تا نیازهای خاص را برآورده کنند
• ءIntegration با سیستمهای خارجی از طریق یک هزارتوی stored procedureها انجام میشد
• جدولهایی که با ساختارهای ساده شروع شده بودند اکنون شامل صدها ستون بودند؛ برخی ستونها دیگر استفاده نمیشدند، اما حذفشان ممکن نبود چون شاید یک گزارش نامعلوم هنوز به آنها وابسته باشد
چالش اصلیتر این بود که بین توصیف سادهی فرآیندهای کسبوکار و پیادهسازی فنی عمیقاً پیچیدهی آنها فاصلهی بزرگی وجود داشت.
کسبوکار یک workflow ساده را توضیح میداد، اما نسخهی فنی آن لایههای متعددی از edge caseها و نیازهای اضافهشده طی سالها را آشکار میکرد.
ما به یک رویکرد سیستماتیک برای فهمیدن این «هیولا» نیاز داشتیم.
شروع کردیم به map کردن business processها و پیادهسازی فنی متناظر آنها.
این کار به ما کمک کرد domainهای اصلی را شناسایی کنیم؛ domainهایی که بعدها معماری ماژولار سیستم جدید را شکل دادند.
مهمتر از آن، به ما کمک کرد مقیاس واقعی کاری که در پیش داشتیم را درک کنیم. 🧩📘
تعارض Product و Engineering ⚔️📊
ءManagement بهدنبال quick wins بود. آنها ما را تحت فشار قرار میدادند که از سادهترین کامپوننتها شروع کنیم. این موضوع میان product management و تیم توسعه تنش ایجاد کرد.
دیدگاه product management روشن بود: باید به کسبوکار پیشرفت قابلمشاهده نشان داد. آنها برای توجیه سرمایهگذاری در بازنویسی سیستم، نیاز به خروجیهای قابلنمایش داشتند. کسبوکار پول قابلتوجهی خرج میکرد و میخواست هرچه سریعتر بازدهی ببیند. 💰📈
اما تیم توسعه واقعیت متفاوتی میدید. ما میدانستیم شروع از بخشهای حاشیهای یعنی ساختوساز روی پایههای سست. هستهی منطق کسبوکار در سیستم legacy باقی میماند و این موضوع هر نقطهی integration را پیچیدهتر میکرد. این بدهی فنی در طول زمان انباشته میشد.
بهعنوان یک technical lead، من بهشدت با این رویکرد مخالفت کردم. استدلالم ساده بود: فرآیند اصلی تولید قلب کل سیستم بود. تمام قابلیتهای جانبی به آن وابسته بودند. با بهتعویقانداختن مهاجرت این core، ما یک شبکهی درهمتنیده از وابستگیها میان سیستم جدید و قدیمی ایجاد میکردیم. هر قابلیت جدیدی که مهاجرت میدادیم نیازمند یک همگامسازی پیچیده با core legacy میشد. ما داشتیم روی ماسههای روان ساختوساز میکردیم. 🏜⚠️
من توصیه کردم ابتدا روی core domain تمرکز کنیم. درست است، نمایش اولین نتایج بیشتر طول میکشید. اما یک پایهی محکم برای ادامهی کار ایجاد میشد. کسبوکار باید دیرتر منتظر «پیشرفت قابلرویت» میبود، اما کل فرآیند مهاجرت سریعتر و قابلاتکاتر انجام میشد.
هیچیک از دو طرف در اهداف خود اشتباه نمیکردند. Product management نگرانیهای موجهی دربارهی نمایش progress داشت.
تیم توسعه نگرانیهای موجهی دربارهی پایداری فنی داشت.
اما این عدمهمراستایی باعث بهوجود آمدن مصالحههایی شد که بر timeline پروژه اثر گذاشت.
تا امروز معتقدم اگر از core business logic شروع کرده بودیم، مهاجرت سریعتر تمام میشد. 🕰🔧
معماری نرمافزار: ساختن برای آینده 🏗🔮
در مرحلهی discovery، ما domainهای کسبوکاری متمایز را در سیستم شناسایی کردیم. این موضوع ما را به سمت پیادهسازی یک modular monolith هدایت کرد. هر ماژول self-contained بود اما میتوانست از طریق یک event bus مشترک با دیگر ماژولها ارتباط برقرار کند.
تصمیمات کلیدی معماری: 🧩
🔸️ءModular monolith:
هر ماژول یک business domain مستقل را نمایش میداد. این کار مسیر روشنی برای حرکت احتمالی آینده به سمت microservices ایجاد میکرد.
🔹️ءAsynchronous communication:
ماژولها از طریق eventها و با استفاده از RabbitMQ با یکدیگر ارتباط برقرار میکردند. این کار coupling را کاهش داده و resiliency را افزایش داد.
🔸️ءShared database با مرزبندی مشخص:
تمام ماژولها از یک دیتابیس PostgreSQL استفاده میکردند، اما هر ماژول جدولها و schemaهای مخصوص خود را داشت. این رویکرد جداسازی منطقی را حفظ میکرد.
🔹️ءCloud-ready design:
سیستم با استفاده از containerization روی AWS مستقر شد. یک Jenkins pipeline امکان deployment به چند environment را در چند دقیقه فراهم میکرد. ☁️🚀
چالش Data Sync 🔄💾
پیادهسازی همگامسازی دوطرفهی داده بسیار پیچیدهتر از چیزی بود که در ابتدا تصور میکردیم. دلیل اینکه نتوانستیم از راهکارهای موجودِ Change Data Capture (CDC) مانند Debezium استفاده کنیم اینها بود:
1️⃣ تبدیلهای پیچیده:
بسیاری از جدولهای legacy برای ساخت رکورد جدید نیازمند داده از چندین جدول در سیستم جدید بودند. این یک mapping سادهی یکبهیک —که ابزارهای CDC در آن عالی هستند— نبود. 🧩
2️⃣ اعمال Business Logic در فرآیند Sync:
فرآیند sync باید business ruleهای متعددی را در مرحلهی transformation اعمال میکرد. این فراتر از قابلیتهای ابزارهای replication معمول بود. ⚙️
3️⃣ نیازمندیهای دوطرفه (Bidirectional):
باید داده را در هر دو جهت همگام میکردیم، بدون آنکه دچار loop بینهایت شویم. سیستم legacy تا زمانی که برخی کامپوننتها مهاجرت نکرده بودند، همچنان source of truth باقی میماند. 🔁❗️
ما یک راهکار custom با استفاده از RabbitMQ برای انتقال پیام ساختیم. این راهکار برای نیازهای ما مناسب بود؛ اما درس بزرگ این است:
پیش از ساخت هر راهکار سفارشی، ابزارهای موجود را بهطور کامل ارزیابی کنید.
حتی اگر نتوانید آنها را تماموکمال استفاده کنید، از معماریها و الگوهای آنها چیزهای ارزشمندی خواهید آموخت. 🛠📘
درسهای فنی کلیدی 🧠💡
• معماری ماژولار بازده دارد:
رویکرد modular monolith باعث شد سیستم قابلدرکتر و قابلنگهداریتر باشد. هر ماژول محدوده و مسئولیتهای روشن داشت.
• سرمایهگذاری روی Deployment Automation:
وجود CI/CD pipeline حیاتی بود. امکان استقرارهای سریع و مطمئن را فراهم کرد و ریسک هر تغییر را کاهش داد. 🚀
• یکپارچهسازی مبتنی بر پیام:
ارتباط async میان ماژولها انعطافپذیری لازم برای مهاجرت تدریجی را فراهم کرد.
• پیچیدگی Data Sync:
هرگز پیچیدگی همگامسازی داده را در مهاجرتهای legacy دستکم نگیرید. چه ابزارهای موجود را استفاده کنید، چه راهکار custom بسازید، این بخش یکی از بزرگترین چالشهاست. ⚠️
عامل انسانی 👥
چالشهای فنی تنها بخشی از ماجرا هستند. موفقیت در بازنویسی سیستمهای legacy شدیداً وابسته به مدیریت ذینفعان است:
🔸️ءProduct Management باید progress ببیند
🔸️تیم توسعه نیاز دارد کار را درست انجام دهد
🔸️کسبوکار باید بدون توقف ادامه پیدا کند
🔸️تیم legacy باید انتقال دانش انجام دهد
🔸️یافتن تعادل میان این نیازهای متعارض، کار آسانی نبود.
ما از چند رویکرد مؤثر استفاده کردیم:
🔹️جلسات منظم ذینفعان برای بیان دغدغهها
🔹️ردیابی شفاف پروژه که برای همه قابل مشاهده بود
🔹️توضیح شفاف تصمیمات فنی و تأثیر آنها بر کسبوکار
🔹️جشنگرفتن milestoneهای فنی و تجاری 🎉
مستندسازی دانش فنی و دانش سازمانی 📚
اهمیت مستندسازی تجربهی ۴۰ سالهی تیم legacy را نمیتوانم دستکم بگیرم.
وقتی تیم اصلی بازنشسته شد، ما مجموعهی کاملی از اسناد داشتیم که تکتک business ruleها و edge caseها را توضیح میداد.
دستاوردهای واقعی 🌟📈
چهار سال بعد، سیستم در وضعیت بسیار خوبی قرار دارد.
زیرساخت cloud، قابلیت اطمینان و مقیاسپذیری را فراهم کرده است.
معماری modular monolith سیستم را قابلنگهداری کرده است.Pipelineهای خودکار امکان استقرار سریع را فراهم میکنند.
اما این مسیر به ما آموخت که باید میان فشارهای کسبوکاری و نیازهای فنی تعادل ایجاد کرد. موفقیت در مهاجرت سیستمهای legacy فقط به برتری فنی وابسته نیست — بلکه نیازمند درک حوزهی کسبوکار، مدیریت انتظارات ذینفعان، و اتخاذ تصمیمات معماری واقعگرایانه است.
معماری نرمافزار مهم است، اما عامل انسانی نیز به همان اندازه اهمیت دارد.
برای هر دو برنامهریزی کنید. 🤝🏗
متشکرم از اینکه خواندید.
و همیشه فوقالعاده بمانید! 🚀
اگر EF Core خودش Repository Pattern را ارائه میدهد، چرا باید یک abstraction دیگر روی آن بسازیم؟ 🤔📦یکی از بحثهای همیشگی در جامعهی .NET این است:
آیا هنوز باید از الگوهای Repository و Unit of Work استفاده کنیم، یا این الگوها در برنامههای مدرن منسوخ شدهاند یا حتی مضر هستند؟
بههرحال، اگر روی DbContext در EF Core hover کنید، مایکروسافت بهوضوح میگوید:
A DbContext instance represents a session with the database and is a combination of the Unit of Work and Repository patterns.
این یعنی EF Core همین حالا این الگوها را پیادهسازی میکند:
• ءDbContext نقش Unit of Work را بازی میکند
• ء<DbSet<TEntity نقش Repository را ایفا میکند
پس سؤال طبیعی این است:
وقتی EF Core همین حالا این abstractions را فراهم میکند، چرا باید یک abstraction دیگر روی آن بسازیم — یعنی یک «abstraction روی abstraction»؟ 🧱🧱
بسیاری از توسعهدهندگان همچنین معتقدند نوشتن repositoryهای سفارشی باعث میشود قابلیتهای قدرتمند EF Core از دید پنهان شود، مانند:
• Eager Loading
• AsNoTracking
• Projections
• Query Composition
• Global Query Filters
• Split Queries
بازنویسی همهی اینها داخل Repositoryهای custom معمولاً به یک نتیجه منتهی میشود:
«ما فقط داریم DbContext را دوباره اختراع میکنیم، فقط با قدمهای اضافی.» 🔁🙃
یک استدلال رایج دیگر testability است.
برخی میگویند برای تستهای واحد باید repository داشته باشیم تا بتوانیم DbContext را mock کنیم؛ اما EF Core همین حالا یک InMemory provider عالی دارد که تستها را واقعیتر میکند و نیاز به mock کردن کل لایهی persistence را از بین میبرد. 🧪🚫
و سپس یک ترس دیگر هم وجود دارد:
«شاید یک روز ORM را عوض کنیم.»
اما در ۹۹٪ پروژههای واقعی این اتفاق نمیافتد.
و حتی اگر هم بیفتد، تغییر فقط repository interfaceها را شامل نمیشود، بلکه بخشهای بسیار بیشتری از سیستم را لمس خواهد کرد — پس این استدلال عمدتاً یک نقض آشکارِ YAGNI است. ⚠️🧠
البته هر Repository اضافی که ایجاد میکنید، سربار نگهداری را افزایش میدهد:
کلاسهای بیشتر 📦
اینترفیسهای بیشتر 🧩
ساختارهای اتصال بیشتر 🔌
پیچیدگی بیشتر 🧠
پس… آیا Repository Pattern مضر است؟ یا ما آن را اشتباه استفاده میکنیم؟
واقعیت، ظریفتر از این حرفهاست.
بسیاری از نظرات منفی ناشی از سوءبرداشت از هدف Repository هستند.Repository هرگز قرار نبود تمام قابلیتهای persistence framework را در معرض دید قرار دهد.
هدف آن این نیست که API مربوط به DbContext را بازتولید کند.
وظیفهٔ اصلی آن این است که کنترل کند دامنه چگونه با داده تعامل میکند.
🎯 Repository واقعاً چه هدفی دارد؟
بهجای ارائهٔ یک سطح دسترسی باز و نامحدود به داده، Repository یک سری عملیات Aggregate-focused، صریح، و intention-revealing تعریف میکند.
این کار چند مزیت مهم به همراه دارد:
منطق کوئری قابلپیشبینی و قابل نگهداری
رفتار کوئری ثابت و در طول زمان بهینهتر میشود.
شفافیت برای domain expertها
نام متدها هدف و مفهوم تجاری را بیان میکنند؛ نیازی نیست SQL یا LINQ بلد باشند.
عملیات دامنهمحور بهجای CRUD عمومی
بهجای اینکه هر نوع دستکاری entity را در اختیار بگذارد، رفتارهایی ارائه میدهد که واقعاً برای دامنه معنادار است.
🛡 هدف واقعی: محافظت از Domain Model
ءRepository pattern در درجهٔ اول دربارهٔ اینها نیست:
• آسانتر کردن unit testing
• امکان تعویض Database
• بستهبندی امکانات EF Core
هدف اصلی آن این است که Domain Model را از نشت نگرانیهای persistence محافظت کند.
در غیاب یک مرز Repository، منطق persistence دیر یا زود وارد دامنه میشود، وضوح را کاهش میدهد، coupling را افزایش میدهد، و مدل را تضعیف میکند.
⛔️ چه زمانی نباید از Repository استفاده کنید؟
• زمانی که دامنهٔ شما ساده است
• زمانی که CRUD کافی است
• زمانی که میخواهید از قابلیتهای EF Core بهصورت مستقیم استفاده کنید
در این موارد، Repository فقط پیچیدگی غیرضروری اضافه میکند.
✅ و چه زمانی باید از Repository استفاده کنید؟
• وقتی با یک دامنهٔ پیچیده و غنی از رفتار سروکار دارید
در چنین سیستمهایی، Repository تبدیل به بخشی از دامنه میشود، قدرت بیان مدل را افزایش میدهد و مرزهای Aggregate را تقویت میکند.
✨ جمعبندی
ءRepository pattern ضدالگو نیست
استفادهٔ اشتباه از آن ضدالگوست.
وقتی آگاهانه و در دامنههای غنی و پیچیده بهکار گرفته شود، به ابزاری قدرتمند برای بیان intent تجاری و حفظ یک معماری تمیز و مقاوم تبدیل میشود.
ساده نگهش دارید.
📌Daniel Jajimi
🔖هشتگها:
#DDD #CleanArchitecture #RepositoryPattern #SoftwareArchitecture #AggregateDesign
معماری Vertical Slice: منطق مشترک دقیقاً کجا باید قرار بگیرد؟ 🚀
معماری Vertical Slice Architecture (VSA) وقتی برای اولین بار با آن روبهرو میشوید شبیه یک نسیم تازه است.
دیگر برای افزودن یک فیلد مجبور نیستید بین هفت لایه جابهجا شوید. پروژههای متعدد را از داخل Solution حذف میکنید. احساس آزادی میکنید.
اما وقتی شروع به پیادهسازی قابلیتهای پیچیدهتر میکنید، ترکها شروع به نمایان شدن میکنند. ⚠️
یک Slice برای CreateOrder میسازید. سپس UpdateOrder. بعد GetOrder.
ناگهان متوجه تکرارها میشوید:
• منطق اعتبارسنجی آدرس در سه مکان تکرار شده است.
• الگوریتم قیمتگذاری هم در Cart نیاز است و هم در Checkout.
• احساس میکنید باید یک Common project یا یک SharedServices folder بسازید.
این لحظه، بحرانیترین نقطه در مسیر پذیرش VSA است.
اگر اشتباه انتخاب کنید، همان coupling که قصد داشتید از آن فرار کنید را دوباره برمیگردانید. 🔄
اگر درست انتخاب کنید، استقلالی را حفظ میکنید که VSA را ارزشمند کرده است.
در ادامه توضیح میدهم که چطور من با shared code در Vertical Slice Architecture برخورد میکنم.
گاردریلها در برابر جادهٔ باز 🛣
برای اینکه بفهمیم چرا این موضوع سخت است، باید به چیزی که پشت سر گذاشتهایم نگاه کنیم.Clean Architecture گاردریلهای سختگیرانه ارائه میکند.
کاملاً مشخص میکند که هر کدی دقیقاً کجا زندگی میکند:
• Entities در Domain
• Interfaces در Application
• Implementations در Infrastructure
امن است. جلوی خطاها را میگیرد.
اما همچنین جلوی میانبرهای ضروری را نیز میگیرد.
در مقابل، Vertical Slice Architecture گاردریلها را حذف میکند.
این معماری میگوید:
"کد را بر اساس قابلیتها سازماندهی کن، نه بر اساس دغدغههای تکنیکی."
این کار سرعت و انعطافپذیری به شما میدهد،
اما بار انضباط معماری را بر دوش خودتان میگذارد.
پس چه باید کرد؟ 🤔
تله: کشوی آشفتگی به نام "Common" 🗃
سادهترین مسیر این است که یک پروژه یا فولدر به نامهای Shared, Common, یا Utils بسازید.
این کار تقریباً همیشه یک اشتباه است. ❌
فرض کنید پروژهای دارید به نام Common.Services همراه با یک کلاس OrderCalculationService.
این کلاس:
• یک متد برای جمع Cart دارد (مورد استفادهی Cart)
• یک متد برای درآمد تاریخی دارد (مورد استفادهی Reporting)
• یک Helper برای فرمتکردن فاکتور دارد (مورد استفادهی Invoices)
سه concern کاملاً بیربط.
سه نرخ تغییر متفاوت.
و یک کلاس که همهٔ اینها را به یکدیگر couple کرده است. 🕸
پروژهٔ Common دیر یا زود تبدیل میشود به یک junk drawer،
محلی برای هر چیزی که حوصلهٔ نامگذاری یا جایگذاری درستش را ندارید.
نتیجه؟
یک شبکهٔ پیچیده از وابستگیها که در آن قابلیتهای مستقل، فقط چون یک Helper مشترک استفاده میکنند، به هم گره میخورند.
در واقع coupling که قصد داشتید از آن فرار کنید دوباره بازمیگردد. 🔁
چارچوب تصمیمگیری 🧭
وقتی به یک موقعیت بالقوهٔ اشتراکگذاری (Sharing) میرسم، سه سؤال از خودم میپرسم:
1️⃣ آیا این موضوع Infrastructural است یا Domain؟
موارد Infrastructure مثل database contexts، logging، HTTP clients تقریباً همیشه باید Shared باشند.
اما مفاهیم Domain نیاز به بررسی دقیقتری دارند.
2️⃣ این کانسپت چقدر پایدار است؟
اگر سالی یک بار تغییر میکند → Shared کردن مناسب است.
اگر همراه با هر Feature Request تغییر میکند → محلی نگهش دارید (Local).
3️⃣ آیا از «Rule of Three» عبور کردهام؟
یک بار Duplicate کردن مشکلی ندارد.
دو بار هم قابل تحمل است.
اما سه بار تکرار باید برای شما زنگ خطر باشد.
تا قبل از رسیدن به سه، Abstraction انجام ندهید.
ما اینها را با Refactor کردن حل میکنیم. بیایید مثالها را ببینیم. 🔍
سه سطح اشتراکگذاری
بهجای اینکه فقط دو گزینهٔ «Shared» یا «Not Shared» داشته باشید، در سه سطح فکر کنید.
Tier 1: Technical Infrastructure (کاملاً قابل اشتراک) ⚙️
کدهای Plumbing که تمام Sliceها به یک اندازه از آن بهره میبرند:
• Logging adapters
• Database connection factories
• Auth middleware
•الگوی Result
•Validation pipelines
این موارد را در یک پروژهٔ Shared.Kernel یا Infrastructure قرار دهید.
همچنین میتوانند فقط یک فولدر باشند.
این بخشها بهندرت بهخاطر نیازهای کسبوکار تغییر میکنند.
نمونهٔ مناسب اشتراکگذاری: Technical Kernel
public readonly record struct Result
{
public bool IsSuccess { get; }
public string Error { get; }
private Result(bool isSuccess, string error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Success() => new(true, string.Empty);
public static Result Failure(string error) => new(false, error);
}
Tier 2: Domain Concepts (اشتراکگذاری و انتقال منطق به پایینترین سطح) 🧩
این یکی از بهترین مکانها برای Shared کردن منطق است.
بهجای پخشکردن Business Ruleها در Sliceهای مختلف، منطق را در Entities و Value Objects قرار دهید.
نمونهٔ مناسب: Entity با منطق تجاری
public class Order
{
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
public List<OrderLine> Lines { get; private set; }
public bool CanBeCancelled() => Status == OrderStatus.Pending;
public Result Cancel()
{
if (!CanBeCancelled())
{
return Result.Failure("Only pending orders can be cancelled.");
}
Status = OrderStatus.Cancelled;
return Result.Success();
}
}
حالا Sliceهای زیر همگی دقیقاً از همین قوانین استفاده میکنند:
CancelOrder
GetOrder
UpdateOrder
منطق فقط در یک مکان زندگی میکند.
این نکتهٔ مهم را نشان میدهد:
ءSliceهای مختلف میتوانند Domain Model یکسانی را به اشتراک بگذارند.
Tier 3: Feature-Specific Logic (محلی نگه دارید) 📌
منطق مشترک بین Sliceهای مرتبط — مانند CreateOrder و UpdateOrder — نیازی ندارد که Global شود.
میتوانید یک فولدر کوچک Shared در داخل یک Feature ایجاد کنید:
📂 Features
└──📂 Orders
├──📂 CreateOrder
├──📂 UpdateOrder
├──📂 GetOrder
└──📂 Shared
├──📄 OrderValidator.cs
└──📄 OrderPricingService.cs
این یک مزیت پنهان هم دارد:
اگر یک روز Feature مربوط به Orders را حذف کنید، Shared logic مخصوص آن هم حذف میشود.
هیچ کد مردهای (Zombie Code) باقی نمیماند. 🧟♂️❌
اشتراکگذاری بین Featureهای مختلف 🚦
در Vertical Slice Architecture، اشتراکگذاری کد بین Featureهای نامرتبط چطور انجام میشود؟
ءCreateOrder باید بررسی کند آیا Customer وجود دارد یا نه.GenerateInvoice باید Tax را محاسبه کند.Orders و Customers هر دو باید پیامهای Notification را فرمت کنند.
اینها در یک فولدر Shared مخصوص Feature جا نمیگیرند. پس کجا باید بروند؟
اول، بپرسید: آیا واقعاً نیاز به اشتراکگذاری وجود دارد؟
بیشتر اشتراکگذاریهای cross-feature در واقع Data Access در پوشش جدید هستند.
اگر CreateOrder به اطلاعات Customer نیاز دارد، باید مستقیماً دیتابیس را Query کند.
نباید Feature مربوط به Customers را صدا بزند.
هر Slice مالک دسترسی به دادههای خودش است.Entity مربوط به Customer Shared است (در Domain قرار دارد)،
اما Service اشتراکی بین آنها وجود ندارد.
وقتی واقعاً نیاز به اشتراکگذاری منطق دارید، بپرسید ماهیت آن چیست:
اگر Domain Logic است (Business Rules یا Calculations) → Domain/Services
اگر Infrastructure است (APIهای خارجی، Formatting) → Infrastructure/Services
نمونه:
// Domain/Services/TaxCalculator.cs
public class TaxCalculator
{
public decimal CalculateTax(Address address, decimal subtotal)
{
var rate = GetTaxRate(address.State, address.Country);
return subtotal * rate;
}
}
هم CreateOrder و هم GenerateInvoice میتوانند از آن استفاده کنند بدون اینکه به هم Coupled شوند.
قبل از ساخت هر Service برای اشتراکگذاری cross-feature، بپرسید:
آیا این منطق میتواند روی یک Domain Entity قرار بگیرد؟
بیشتر "shared business logic"ها در واقع:
• ءdata access هستند
• ءdomain logicی هستند که باید روی Entity قرار بگیرند
• ءabstractionهای زودهنگاماند
اگر نیاز دارید یک side effect در Feature دیگری ایجاد کنید، پیشنهاد میشود:
از Messaging و Event استفاده کنید
یا Feature مقصد یک Facade (یک API عمومی) برای این عملیات ارائه کند
زمانی که Duplication انتخاب درست است 🔁
گاهی چیزی Shared به نظر میرسد، اما واقعاً Shared نیست.
هر دو یکساناند.
وسوسهٔ ساخت یک SharedOrderDto شدید است.
مقاومت کنید.
هفتهٔ بعد، GetOrder نیاز به tracking URL پیدا میکند.
اما CreateOrder وقتی اجرا میشود هنوز Shipping انجام نشده؛ پس URL وجود ندارد.
یک property nullable اضافه میشد
نیمی از مواقع خالی بود
و باعث ابهام و Coupling میشد.Duplication ارزانتر از Abstraction اشتباه است.
این ساختاری است که یک پروژهٔ بالغ Vertical Slice Architecture معمولاً دارد:
توضیح بخشها:
🔸️ءFeatures → Sliceهای مستقل. هرکدام صاحب Request/Response خودشاناند.
🔸️ءFeatures/[Name]/Shared → اشتراکگذاری محلی بین Sliceهای مرتبط یک Feature.
🔸️ءDomain → Entities، Value Objects، Domain Services.
🔸️ءInfrastructure → کل نگرش فنی سیستم.
🔸️ءShared → فقط Cross-Cutting Behaviors.
بعد از ساخت چند سیستم با این معماری، به این اصول رسیدهام:
1️⃣ ءFeatures صاحب Request/Response خودشان هستند. بدون استثنا.
2️⃣ منطق تجاری را تا حد ممکن وارد Domain کنید.
ءEntities و ValueObjectها بهترین مکان برای اشتراکگذاری واقعی Business Rules هستند.
3️⃣ اشتراکگذاری در سطح Feature-Family را محلی نگه دارید.
فقط اگر کد فقط در Orderها استفاده میشود → همانجا نگه دارید.
4️⃣ ءInfrastructure بهصورت پیشفرض Shared است.
Persistence، Logging، HTTP Clients
5️⃣ ءRule of Three را رعایت کنید.
تا وقتی سه استفادهٔ واقعی و مشابه ندارید → abstraction نکنید.
ءVertical Slice Architecture از شما میپرسد:
«این کد متعلق به کدام Feature است؟»
سؤال اشتراکگذاری در واقع میپرسد:
«اگر جوابش چند Feature است چه کنم؟»
پاسخ:
پذیرفتن اینکه برخی مفاهیم واقعاً cross-feature هستند،
و دادن یک محل مشخص به آنها بر اساس ماهیتشان: Domain، Infrastructure یا Behavior.
هدف، حذف کامل Duplication نیست.
هدف این است که وقتی نیازها تغییر میکند، تغییر کد ارزان و ساده باشد.
و نیازها همیشه تغییر میکنند.
Thanks for reading.
And stay awesome! ✨
آیا این منطق میتواند روی یک Domain Entity قرار بگیرد؟
بیشتر "shared business logic"ها در واقع:
• ءdata access هستند
• ءdomain logicی هستند که باید روی Entity قرار بگیرند
• ءabstractionهای زودهنگاماند
اگر نیاز دارید یک side effect در Feature دیگری ایجاد کنید، پیشنهاد میشود:
از Messaging و Event استفاده کنید
یا Feature مقصد یک Facade (یک API عمومی) برای این عملیات ارائه کند
زمانی که Duplication انتخاب درست است 🔁
گاهی چیزی Shared به نظر میرسد، اما واقعاً Shared نیست.
// Features/Orders/GetOrder
public record GetOrderResponse(Guid Id, decimal Total, string Status);
// Features/Orders/CreateOrder
public record CreateOrderResponse(Guid Id, decimal Total, string Status);
هر دو یکساناند.
وسوسهٔ ساخت یک SharedOrderDto شدید است.
مقاومت کنید.
هفتهٔ بعد، GetOrder نیاز به tracking URL پیدا میکند.
اما CreateOrder وقتی اجرا میشود هنوز Shipping انجام نشده؛ پس URL وجود ندارد.
اگر DTO مشترک ساخته بودید:
یک property nullable اضافه میشد
نیمی از مواقع خالی بود
و باعث ابهام و Coupling میشد.Duplication ارزانتر از Abstraction اشتباه است.
ساختار عملی 🏗
این ساختاری است که یک پروژهٔ بالغ Vertical Slice Architecture معمولاً دارد:
📂 src
└──📂 Features
│ ├──📂 Orders
│ │ ├──📂 CreateOrder
│ │ ├──📂 UpdateOrder
│ │ └──📂 Shared # اشتراکگذاری مخصوص Orders
│ ├──📂 Customers
│ │ ├──📂 GetCustomer
│ │ └──📂 Shared # اشتراکگذاری مخصوص Customers
│ └──📂 Invoices
│ └──📂 GenerateInvoice
└──📂 Domain
│ ├──📂 Entities
│ ├──📂 ValueObjects
│ └──📂 Services # منطق domain مشترک
└──📂 Infrastructure
│ ├──📂 Persistence
│ └──📂 Services
└──📂 Shared
└──📂 Behaviors
توضیح بخشها:
🔸️ءFeatures → Sliceهای مستقل. هرکدام صاحب Request/Response خودشاناند.
🔸️ءFeatures/[Name]/Shared → اشتراکگذاری محلی بین Sliceهای مرتبط یک Feature.
🔸️ءDomain → Entities، Value Objects، Domain Services.
🔸️ءInfrastructure → کل نگرش فنی سیستم.
🔸️ءShared → فقط Cross-Cutting Behaviors.
قوانین 🧠
بعد از ساخت چند سیستم با این معماری، به این اصول رسیدهام:
1️⃣ ءFeatures صاحب Request/Response خودشان هستند. بدون استثنا.
2️⃣ منطق تجاری را تا حد ممکن وارد Domain کنید.
ءEntities و ValueObjectها بهترین مکان برای اشتراکگذاری واقعی Business Rules هستند.
3️⃣ اشتراکگذاری در سطح Feature-Family را محلی نگه دارید.
فقط اگر کد فقط در Orderها استفاده میشود → همانجا نگه دارید.
4️⃣ ءInfrastructure بهصورت پیشفرض Shared است.
Persistence، Logging، HTTP Clients
5️⃣ ءRule of Three را رعایت کنید.
تا وقتی سه استفادهٔ واقعی و مشابه ندارید → abstraction نکنید.
نتیجهگیری 📌
ءVertical Slice Architecture از شما میپرسد:
«این کد متعلق به کدام Feature است؟»
سؤال اشتراکگذاری در واقع میپرسد:
«اگر جوابش چند Feature است چه کنم؟»
پاسخ:
پذیرفتن اینکه برخی مفاهیم واقعاً cross-feature هستند،
و دادن یک محل مشخص به آنها بر اساس ماهیتشان: Domain، Infrastructure یا Behavior.
هدف، حذف کامل Duplication نیست.
هدف این است که وقتی نیازها تغییر میکند، تغییر کد ارزان و ساده باشد.
و نیازها همیشه تغییر میکنند.
Thanks for reading.
And stay awesome! ✨
🔖هشتگها:
#VerticalSliceArchitecture #SoftwareArchitecture #DotNet #CSharp #ArchitecturePatterns