uz
Feedback
Developer Advocate

Developer Advocate

Kanalga Telegram’da o‘tish
Buy Ad
1 035
Obunachilar
Ma'lumot yo'q24 soatlar
Ma'lumot yo'q7 kun
-130 kun
Postlar arxiv
🎯 Source Generator پیشرفته در C#: کاهش boilerplate به شیوه‌ای مدرن در پروژه‌های روزمره، فراوان خطوط تکراری مثل Propertyها با Notifying و توابع ToString دستی تکرار می‌شوند. Source Generatorها بازی را عوض می‌کنند: مثل یک meta-programmer در اختیار خودتان! فرض کنید می‌خواهیم برای هر کلاس decorate‌شده با [AutoToString]، یک متد ToString قوی بسازیم. یک Generator از صفر: ۱. پکیج‌های لازم را اضافه کنید: - پکیج Microsoft.CodeAnalysis.CSharp و Microsoft.CodeAnalysis.Analyzers را به پروژه Source Generator اضافه کنید. ۲. تعریف Attribute:
namespace DemoGenerators
{
    [System.AttributeUsage(System.AttributeTargets.Class)]
    public sealed class AutoToStringAttribute : System.Attribute
    {
    }
}
۳. اصلی‌ترین بخش: Source Generator
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

[Generator]
public class AutoToStringGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // No initialization needed
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var syntaxTrees = context.Compilation.SyntaxTrees;

        foreach (var tree in syntaxTrees)
        {
            var semanticModel = context.Compilation.GetSemanticModel(tree);

            var classDeclarations = tree
                .GetRoot()
                .DescendantNodes()
                .OfType<Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax>();

            foreach (var classDecl in classDeclarations)
            {
                var symbol = semanticModel.GetDeclaredSymbol(classDecl);
                if (symbol == null) continue;
                if (!symbol.GetAttributes().Any(ad =>
                    ad.AttributeClass?.ToDisplayString() == "DemoGenerators.AutoToStringAttribute"))
                    continue;

                var toStringBody = new StringBuilder("$\"");

                foreach (var member in symbol.GetMembers().OfType<IPropertySymbol>())
                {
                    toStringBody.Append($"{member.Name}: {{{member.Name}}}, ");
                }
                toStringBody.Append("\"");

                var source = $@"
namespace {symbol.ContainingNamespace}
{{
    partial class {symbol.Name}
    {{
        public override string ToString()
            => {toStringBody.ToString()};
    }}
}}";

                context.AddSource($"{symbol.Name}_AutoToString.g.cs", SourceText.From(source, Encoding.UTF8));
            }
        }
    }
}
۴. نحوه مصرف:
using DemoGenerators;

[AutoToString]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// اکنون Person به طور خودکار متد ToString به صورت زیر دارد:
// Name: ..., Age: ...
⭐️ این الگو را می‌توانید به راحتی توسعه دهید (مثلاً برای تولید INotifyPropertyChanged یا سایر الگوهای تکرارشونده). Source Generatorها قدرت معماری تمیز و کد قدرتمند را در مقیاس enterprise به شما می‌دهند. همیشه اصل SOLID و separation of concern را رعایت کنید! @DeveloperAdvocate 🥑

در معماری‌های مبتنی بر eventual consistency (مثل CQRS + Event Sourcing یا Microservices)، UI باید طوری طراحی شود که تأخیر در انتشار داده نهایی را به کاربر منتقل کند، اما تجربه کاربری را خراب نکند. فرض کنید کاربر یک سفارش ثبت می‌کند؛ درخواستش ابتدا به سرویس Command می‌رود، سپس رخدادها منتشر و Replicaها (Read Modelها) به‌روزرسانی می‌شوند. بین تایید اولیه و مشاهده سفارش معمولاً چند ثانیه فاصله است. یک رویکرد در UI، نمایش optimistic update به‌همراه بازخورد منطقی است: 1. نمایش وضعیت درخواست: بلافاصله پس از ثبت سفارش، آیتم جدید را با وضعیت "در حال ثبت" به لیست اضافه کنید، اما آن را به صورت gray یا با آیکن چرخان نمایش دهید. 2. Polling / SignalR: به‌محض sync شدن read model، آیتم به وضعیت نهایی می‌رود. 3. handle failure: اگر بعد از زمان مناسب، سفارش در read model ظاهر نشد (مثلاً ۳۰ ثانیه)، پیام خطا نمایش دهید یا «امکان ثبت سفارش نبود. دوباره تلاش کنید.» را پیشنهاد دهید. نمونه ساده کد MVVM در WPF:
public class OrderViewModel : INotifyPropertyChanged
{
    public ObservableCollection<OrderItem> Orders { get; } = new();

    public async Task PlaceOrderAsync(OrderRequest request)
    {
        var pendingOrder = new OrderItem { ..., Status = OrderStatus.Pending };
        Orders.Add(pendingOrder);

        await _orderService.PlaceOrderAsync(request);

        // فرض: PollStatusAsync وضعیت سفارش را بررسی می‌کند (مثلاً از طریق مدت‌دار یا SignalR)
        var finalOrder = await PollStatusAsync(pendingOrder.Id, timeout: TimeSpan.FromSeconds(30));
        if (finalOrder != null)
        {
            ReplaceOrder(pendingOrder, finalOrder); // آیتم آپدیت‌شده را جایگزین کن
        }
        else
        {
            pendingOrder.Status = OrderStatus.Failed;
        }
    }
}
در XAML: - اگر Status==Pending، آیتم به شکل شفاف‌تر (Opacity پایین) یا با Spinner از آیتم‌های قطعی متمایز شود. - در صورت Failed، با رنگ قرمز یا با دکمه Retry امکان تلاش مجدد بدهید. در نهایت UI شجاعانه تأخیر eventual consistency را به‌جای پنهان‌کردن، شفاف مدیریت می‌کند. این رویکرد نه‌تنها تجربه کاربری را لطمه نمی‌زند، بلکه اعتماد و درک فنی کاربران حرفه‌ای (و خودتان به‌عنوان تیم توسعه!) را افزایش می‌دهد. @DeveloperAdvocate 🥑

# 🎯 ژرفای Generics در #CSharp: تفاوت Covariance و Contravariance با نمونه‌ی واقعی یکی از بخش‌های پیچیده اما حیاتی در طراحی APIهای قوی و extensible، فهم covariance و contravariance در جنریک‌هاست. این مفاهیم به ما اجازه می‌دهند رابط‌ها و delegateهای خود را هوشمندانه‌تر بسازیم—به‌طوری که کدهایی مثل IEnumerable<Parent> به صورت type-safe قابلیت گرفتن ICollection<Child> داشته باشند. ⛓️ Covariance (کووارینس): اجازه می‌دهد یک نوع جنریک که با پارامتر نوعی «پایه» تعریف شده، با آبجکت‌هایی از نوع «مشتق» نیز کار کند (مخصوص پارامترهای خروجی).
IEnumerable<Animal> animals = new List<Dog>();
در این مثال، چون IEnumerable<T> پارامتر نوع خروجی دارد و با کلیدواژه‌ی out تعریف شده:
interface IEnumerable<out T> { ... }
می‌توانید لیست سگ‌ها را به متغیر enumerable از حیوان نگاشت کنید. ⛓️ Contravariance (کُنتراوارینس): در جهت عکس، برای پارامترهای ورودی—اجازه می‌دهد interface با پارامتر پایه را به جایی که interface با پارامتر مشتق می‌خواهد بدهید.
Action<Dog> dogHandler = (Dog d) => Console.WriteLine(d.Name);
Action<Animal> animalHandler = dogHandler;
animalHandler(new Cat()); // البته با دقت به نوع پارامتر اجرا شود!
چرا مجاز است؟ چون Action<T> پارامتر ورودی دارد و با کلیدواژه‌ی in مشخص شده:
interface IAction<in T> { ... }
👨‍💻 مثال واقعی: سرویس تبدیل notification برای سیستم‌های پیام‌رسان فرض کنید انواع مختلف notification داریم:
class Notification { ... }
class EmailNotification : Notification { ... }
یک تبدیل‌گر برای پردازش notificationها تعریف می‌کنیم:
public interface INotificationHandler<in T>
{
    void Handle(T notification);
}
حال یک handler می‌نویسیم که فقط با Notification کار می‌کند:
class NotificationLogger : INotificationHandler<Notification>
{
    public void Handle(Notification notification)
    {
        // لاگ و مانیتورینگ...
    }
}
با contravariance، می‌توانیم این handler را برای EmailNotification هم استفاده کنیم:
INotificationHandler<EmailNotification> handler = new NotificationLogger();
handler.Handle(new EmailNotification());
#نکته: در صورت استفاده اشتباه از نوع‌ها، استثنا رخ می‌دهد. اما type system از لحاظ signature امن است. 🔬 جمع‌بندیِ عملیاتی - همیشه از out (کووارینس) برای پارامتر خروجی و in (کُنتراوارینس) برای پارامتر ورودی در interfaceها بهره بگیرید تا API منعطف و extendable بسازید. - خوب به این ظرافت توجه کنید: IEnumerable<T> فقط کووارینت است (خواندنی). IList<T> چون هم خواندنی و هم نوشتنی است، نه کووارینت است و نه کنتراوارینت! #CSharp #Generics #Advanced #DesignPatterns @DeveloperAdvocate 🥑

📌 API Gateway؛ نقش و یک پیکربندی ساده با Ocelot در .NET Core در معماری‌های مدرن Microservices، الگوی API Gateway نقطه ورود واحد برای کلاینت‌هاست؛ در عمل، درخواست‌های ورودی را دریافت، اعتبارسنجی، احراز هویت، مسیر‌یابی (Routing) و بعضاً تجمیع پاسخ‌ها را ارائه می‌دهد. این ساختار مزایایی مانند پنهان‌سازی توپولوژی داخلی، سادگی در پیاده‌سازی امنیت و محدودسازی نرخ درخواست‌ها را به ارمغان می‌آورد. در اکوسیستم دات‌نت، Ocelot یکی از محبوب‌ترین و مینیمال‌ترین لایبرری‌ها برای راه‌اندازی یک API Gateway مبتنی بر API Routes است. فلسفه‌اش سادگی در پیکربندی است و می‌تواند Auth، Rate Limiting، و QoS را هم پوشش دهد. فرض کنید دو سرویس داریم: product و order که هر دو API جداگانه‌ای ارائه می‌دهند. یک پیکربندی ساده Ocelot به این صورت خواهد بود:
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/products/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        { "Host": "localhost", "Port": 6001 }
      ],
      "UpstreamPathTemplate": "/gateway/products/{everything}",
      "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ]
    },
    {
      "DownstreamPathTemplate": "/api/orders/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        { "Host": "localhost", "Port": 6002 }
      ],
      "UpstreamPathTemplate": "/gateway/orders/{everything}",
      "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:8000"
  }
}
در این پیکربندی: - درخواست‌هایی مانند /gateway/products/123 را به سرویس محصول روی پورت 6001 هدایت می‌کنیم و همه‌ی متدهای HTTP را پشتیبانی می‌کنیم. - همین منطق برای سرویس سفارش روی پورت 6002 نیز اعمال شده است. در پروژه ASP.NET Core کافی است NuGet زیر را نصب کنید:
dotnet add package Ocelot
و در Program.cs:
using Ocelot.DependencyInjection;
using Ocelot.Middleware;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOcelot();

var app = builder.Build();
await app.UseOcelot();
app.Run();
از اینجا به بعد، مدیریت متمرکز مسیرها، پیاده‌سازی auth، و اعمال سیاست‌های cross-cutting concern به سادگی یک فایل پیکربندی خواهد بود؛ دقیقاً همان چیزی که یک معمار حرفه‌ای نیاز دارد برای تحمل‌پذیری تغییرات و مقیاس‌پذیری اکوسیستم میکروسرویس‌ها. 👌 @DeveloperAdvocate 🥑

درک عمیق GitOps Workflow با ArgoCD در دپلوی .NET اپلیکیشن‌ها روی Kubernetes وقتی صحبت از تحویل کد مطمئن و تکرارپذیر در محیط کلاستر Kubernetes برای معماری‌های Enterprise می‌شود، GitOps به همراه ArgoCD عملاً طلایی‌ترین رویکرد است. اجازه دهید workflow بهینه را برای یک پروژه مدرن .NET (مثلاً یک Web API مبتنی بر ASP.NET Core) مرور کنیم: ۱. ساخت ایمیج و Push به Registry: در مرحله CI (مثلاً GitHub Actions یا Azure Pipelines)، پروژه را build می‌کنیم، تست‌ها را اجرا می‌کنیم و ایمیج Docker را به Registry (مثلاً Azure Container Registry یا GitHub Container Registry) push می‌کنیم.
# بخش کلیدی از GitHub Actions pipeline
- name: Build & Push Docker image
  run: |
    docker build -t registry.example.com/app:$(git rev-parse --short HEAD) .
    docker push registry.example.com/app:$(git rev-parse --short HEAD)
۲. بروزرسانی manifests در GitOps repo: یک repository جداگانه به عنوان single source of truth تعریف کنید (infrastructure as code). هر بار که نسخه جدیدی منتشر می‌شود، image tag دپلویمنت را در manifestها بروزرسانی می‌کنیم.
# deployment.yaml
spec:
  containers:
  - name: app
    image: registry.example.com/app:07cfa1b
    # بقیه تنظیمات
آپدیت manifests را می‌توانید به صورت automation (مثلاً با کد یا ابزار kustomize/kpt/sed) انجام دهید. ۳. ArgoCD به‌عنوان موتور تحویل: ArgoCD با Repository manifests سینک می‌کند و هر تغییری را به کلایستر K8s اعمال می‌نماید. این کار از طریق declarative infrastructure management انجام می‌شود و وضعیت actual vs. desired همیشه شفاف است. ۴. Sync و Promotion: -approved: توسعه‌دهندگان تنها Pull Request را روی GitOps repo merge می‌کنند — نه مستقیم روی K8s! ArgoCD به صورت خودکار وضعیت اپ را به روزترین و مطابق با تعریف Git نگاه می‌دارد. مثال Application manifest:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: dotnet-api
spec:
  project: default
  source:
    repoURL: 'https://github.com/company/dotnet-api-gitops'
    targetRevision: main
    path: kubernetes
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
نکته تکمیلی معمارانه: ۱. Manifestها را با kustomize لایه‌بندی کنید (dev/staging/prod). ۲. از RBAC و Audit Log خود آرگو سی‌دی جهت انطباق امنیتی سازمان بهره ببرید. ۳. health checks و hooks آرگو سی‌دی را جهت rollbacks هوشمند فعال کنید. 🔹 در نتیجه، کد، manifest و history کاملاً Git-based بوده و تحویل تولید شما reproducible، قابل اتکا و کاملاً traceable می‌شود. همین الگو راز زیرساخت‌های بزرگ دنیاست! @DeveloperAdvocate 🥑

در معماری‌های مبتنی بر RAG (Retrieval-Augmented Generation) اتصال مستقیم به دیتابیس‌های برداری مثل Pinecone، Qdrant یا Milvus اهمیت بالایی دارد. این دیتابیس‌ها معمولاً API مبتنی بر REST یا gRPC دارند و هنوز کلاینت‌های دات‌نت پایداری برای همه آن‌ها موجود نیست، اما اصول کار یکسان است: ارسال بردار کوئری و گرفتن نزدیک‌ترین آیتم‌ها. در این مثال، فرض کنیم از Qdrant با REST API استفاده می‌کنیم و قصد داریم ۵ بردار نزدیک به بردار کوئری خود پیدا کنیم:
using System.Net.Http.Json;
using System.Text.Json;

var httpClient = new HttpClient();
string endpoint = "http://localhost:6333/collections/my-collection/points/search";

var queryVector = new float[] { 0.12f, 0.43f, -0.09f, 0.57f, 0.34f }; // بردار کوئری

var payload = new
{
    vector = queryVector,
    limit = 5 // تعداد نزدیک‌ترین آیتم‌ها
};

var response = await httpClient.PostAsJsonAsync(endpoint, payload);
response.EnsureSuccessStatusCode();

using var responseStream = await response.Content.ReadAsStreamAsync();
using var doc = await JsonDocument.ParseAsync(responseStream);

// استخراج نتایج
foreach (var pointResult in doc.RootElement.GetProperty("result").EnumerateArray())
{
    var score = pointResult.GetProperty("score").GetDouble();
    var payloadObj = pointResult.GetProperty("payload");

    // دسترسی به مقادیر کلیدهای موجود در payload
    var docId = payloadObj.GetProperty("document_id").GetString();

    Console.WriteLine($"DocumentId: {docId}, Score: {score:F4}");
}
🔹 نکته معماری: در سطح حرفه‌ای، پیشنهاد می‌شود مدیریت HttpClient را به شیوه singleton انجام دهید و مدل serialization/deserialization را به strongly-typed records یا classes بسپارید تا کنترل بهتری روی داده‌های برداری و payload داشته باشید. همچنین برای حجم بالای کوئری‌ها، استفاده از Connection Pooling و تنظیم Timeout مناسب برای شبکه اهمیت بالایی دارد. 📌 اگر تجربه اتصال به سایر Vector Databaseها مثل Milvus یا Weaviate را دارید، می‌توانید با تغییر endpoint و payload براساس مستندات آن‌ها، این الگو را به‌راحتی تطبیق دهید. قدرت اصلی این معماری در decoupling ماژول بازیابی و LLM نهفته است. @DeveloperAdvocate 🥑

🚦 زیرساخت را مثل کد تست کنید! (Terratest و Beyond) در دنیای DevOps، زیرساخت به عنوان کد (IaC) استاندارد شده — اما آیا زیرساخت‌های خود را تست می‌کنید، یا فقط به اجرای بی‌دردسر آن‌ها اتکا دارید؟ درست مثل کدی که می‌نویسیم، تعریف منابع زیرساخت (Terraform, Pulumi, Bicep و...) هم باید تست‌پذیر و مقاوم در برابر regressions باشد. اینجا جایی است که Terratest وارد میدان می‌شود. Terratest یک framework متن‌باز (به زبان Go) است که تست‌های end-to-end برای زیرساخت شما فراهم می‌کند. روال کار بدین صورت است: 1. زیرساخت را Provision می‌کنید. 2. با کد Go صحت منابع Provision شده (مثلاً وجود VM، اتصال شبکه و...) را بررسی می‌کنید. 3. در انتها، همه چیز را Destroy می‌کنید تا cost اضافی نداشته باشید. یک مثال ساده (به فرض تست یک resource group در Azure با Terraform):
func TestAzureResourceGroup(t *testing.T) {
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/azure/resource-group",
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // صحت وجود resource group را چک می‌کنیم
    groupName := terraform.Output(t, terraformOptions, "resource_group_name")
    exists := azure.ResourceGroupExists(t, groupName, "")
    assert.True(t, exists)
}
مزایای اصلی این رویکرد: - اطمینان از عدم بوجود آمدن خطاهای زیرساختی در deploymentها - مستندسازی رفتار واقعی زیرساخت در production-like conditions - فراهم‌کردن پایه‌ای برای CD pipelineهای قوی‌تر اگر پروژه‌های IaC Enterprise دارید یا در migrationهای جدی ابری فعال هستید، کنار کد application، کد زیرساخت‌تان را هم با Terratest و ابزارهای مشابه پوشش تستی بدهید! ➡️ تجربه یا سوالی درباره تست زیرساخت با .NET و ابزارهایی مثل Terratest دارید؟ زیر همین پست به اشتراک بگذارید. @DeveloperAdvocate 🥑

➖ DevOps Insight: یافتن سریع Commit مخرب با git bisect تصور کنید یک باگ مخفی در لایه کاربردی سیستم، پس از چندین commit وارد شده و سورس کد، بالغ بر چند هزار تغییر داشته است. بازگشت دستی به هر commit—تقریباً نشدنی! اینجا جادوی git bisect وارد می‌شود. ### سناریو: سرویس شما تا یک هفته قبل بی‌نقص تست می‌شد. حالا یک تست Unit روی متد زیر fail شده:
public decimal CalculateOrderTotal(Order order)
{
    return order.Items.Sum(i => i.Price * i.Quantity) * order.TaxRate;
}
شک دارید که کدام commit این مشکل را وارد کرده. ### گام‌های عملی: ۱. شروع bisect
git bisect start
۲. مشخص کردن وضعیت‌ها: فرض کنید: - commit فعلی باگ دارد (bad) - سه commit قبل بی‌عیب بوده (good)
git bisect bad
git bisect good HEAD~3
۳. git به صورت خودکار به midpoint می‌رود کد را build و تست خود را اجرا کنید:
dotnet test
اگر تست fail شد:
git bisect bad
اگر pass شد:
git bisect good
۴. تکرار خودکار تا پیدا شدن culprit commit بعد از چند مرحله، git دقیقاً همان commit مشکل‌ساز را معرفی می‌کند. ۵. پاک‌سازی: پس از اتمام:
git bisect reset
### نکته پیشرفته: برای تست‌های اتوماتیک از همین الگوریتم می‌توانید استفاده کنید:
git bisect run dotnet test
در این صورت bisect تا یافتن commit مشکل‌زا، خودش اجرا می‌شود. --- استفاده از git bisect برای تیم‌هایی که چرخه CI/CD سریع دارند، باعث صرفه‌جویی چشمگیر در زمان دیباگ می‌شود—ویژگی که هر معمار DevOps باید در جعبه ابزار خود داشته باشد! @DeveloperAdvocate 🥑

🔹 معماری ۱۲-فاکتوره: عمیق در «Config» (پیکربندی) عامل «Config» از اصول ۱۲-Factor App می‌گوید: تنظیمات باید جدا از کد و از طریق متغیر محیطی مدیریت شوند. این اصل اهمیت فوق‌العاده‌ای در نگه‌داشت پذیر بودن، تست‌پذیری و تحویل مستمر سرویس‌های .NET دارد. در اپلیکیشن‌های مدرن .NET (خصوصاً ASP.NET Core)، مدیریت کانفیگ با سیستم قوی Configuration Providerها انجام می‌شود، ولی اغلب شاهد آنتی‌پترن‌هایی مثل commit کردن appsettings.Development.json به ریپازیتوری، استفاده از secrets محلی، یا rely به KeyVault فقط در production هستیم. ✅ الگوی پیشنهادی: ۱. همه کانفیگ‌ها مبتنی بر محیط (Environment Variable) باشد؛ نه فایل‌های استاتیک خام. مقداردهی اولیه تمام پارامترهای کلیدی (کانکشن‌استرینگ، کلیدها، endpointها) باید از محیط خوانده شود. ۲. در کنترل کد فقط مقادیر پیشفرض یا مقادیر غیرحساس باشد. حتی در appsettings.json نباید هیچ مقدار فعالی از موارد حساس یا environment-specific وجود داشته باشد. ۳. تست و اجرا با مقادیر محیطی شبیه‌سازی شود؛ حتی در local development. هرگز نباید تست‌های integration به فایل‌های local وابسته باشند. 🔸 نمونه تمیز خواندن کانفیگ از محیط در ASP.NET Core:
builder.Configuration.AddEnvironmentVariables();

string connectionString = builder.Configuration
    .GetValue<string>("Db__ConnectionString");

// تا جای ممکن بخش‌بندی امن (مانند prefixهای متعدد محیط) را رعایت کنید.
🔸 مدیریت کلیدهای پیچیده (مثل JSON یا رشته‌های طولانی): فضاهای ابری (Azure AppConfig, AWS Parameter Store) و ابزارهایی مثل Docker Secrets یا Vault با مدیریت مرکزی، تمام مقادیر را به عنوان متغیر محیطی inject می‌کنند. در صورت ضروریت، custom IConfigurationProvider بنویسید و صراحتاً inputs را لاگ نکنید. 🔸 یک anti-pattern مهم: عدم خواندن تنظیمات confidential در Startup، بلکه تزریق IConfiguration در لایه Service و حتی ترجیح به IOptionsSnapshot در صورت نیاز به به‌روزرسانی Runtime:
public class MyService
{
    private readonly string _apiKey;
    public MyService(IConfiguration cfg)
    {
        _apiKey = cfg["ApiKeys__Google"];
        // بد: به جای خواندن مستقیم، الگوی options و validate را ترجیح دهید.
    }
}
🔸 الگوی برتر: IOptions با DataAnnotation Validation
public class MyServiceOptions
{
    [Required]
    public string GoogleApiKey { get; set; }
}

services.Configure<MyServiceOptions>(builder.Configuration.GetSection("ApiKeys"))
        .PostConfigure<MyServiceOptions>(options =>
        {
            Validator.ValidateObject(options, new ValidationContext(options), validateAllProperties: true);
        });
🔸 مسیر حرفه‌ای تر: در CI/CD، از ابزارهایی مانند DotNetEnv + Docker Swarm/Rancher/K8s Secrets استفاده کنید تا محیط dev/prod/test با یک مکانیسم واحد مقداردهی شوند؛ هرگز مقادیر secrets را روی فایل سیستم یا ریپازیتوری رها نکنید. ⏳ خلاصه: Config باید محیط‌محور، غیر-استاتیک، و جدا از کد بماند؛ رازها فقط از محیط می‌آیند و Pipelineها باید مسئول مقداردهی سالم باشند—این راز مقیاس‌پذیری، تحویل سریع، و امنیت سرویس‌هاست. @DeveloperAdvocate 🥑

یکی از بهترین رویکردها برای نوشتن کد TypeScript قابل نگهداری و ایمن، استفاده حداکثری از type inference به جای انواع صریح (explicit types) است. هرجا که می‌توانید اجازه دهید TypeScript نوع متغیر یا تابع را خودش استنتاج کند؛ این کار باعث کاهش duplication و ریسک drift بین type و مقدار واقعی می‌شود. جالب‌تر اینکه برای exposed API‌ها یا interfaceهای عمومی، دقیق‌ترین نوع ممکن را تعریف کنید و حتی‌المقدور نوع را توسط ترکیب mapped types و conditional types توصیف نمایید. به عنوان مثال، فرض کنید می‌خواهید یک متد generic بنویسید که فقط پراپرتی‌های کلیددار یک آبجکت را انتخاب کند:
type PickByKeys<T, K extends keyof T> = {
  [P in K]: T[P];
};
این نوع تعریف، به شدت از type inference و mapped types بهره می‌برد و همزمان robust و قابل توسعه است. توجه داشته باشید که avoid any یا استفاده بیش‌ از حد از type assertions (مثل as any) اکوسیستم type-safety را تضعیف می‌کنند. در پروژه‌های بزرگ‌تر، حتماً type utilityهای تان را در یک لایه مجزا نگهداری کنید تا قابل تست و reuse باشند؛ این الگو را می‌توانید مشابه extension methods در C# ببینید. یک مثال برای جلوگیری از اشتباه در APIها:
type DeepReadonly<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};
این تکنیک‌ها باعث می‌شوند کد TypeScript شمارا همانند کد C# با strong typing و سطح بالای الگوپذیری پیش ببرد و مانع بروز خطا در refactorهای آینده شود. @DeveloperAdvocate 🥑

🔹 اصول برنامه‌نویسی تابعی: تغییرناپذیری (Immutability) در #CSharp وقتی از تغییرناپذیری صحبت می‌کنیم، منظور این است که پس از ساخته‌شدن یک شیء، وضعیت داخلی آن دیگر قابل تغییر نیست. این اصل، برخلاف روش‌های شی‌گرای سنتی و الگوهای مبتنی بر mutable state، مزایای حیاتی برای توسعه حرفه‌ای به ارمغان می‌آورد: - کاهش باگ‌های هم‌زمانی (Race Conditions): اشیای تغییری‌ناپذیر به طور ذاتی thread-safe هستند، چرا که state آن‌ها هرگز تغییر نمی‌کند. - سادگی reasoning و تست: وقتی داده‌های شما تغییر نمی‌کنند، اثرات جانبی صفر می‌شود و کدها آسان‌تر قابل دنبال‌کردن و تست هستند. - پشتیبانی از معماری‌های مدرن: در الگوهایی مانند CQRS، Event Sourcing و سیستم‌های توزیع‌شده، تغییرناپذیری یک اصل پایه است. مثال ساده:
public record Person(string FirstName, string LastName);
نوع record در C# 9+ به صورت پیش‌فرض immutable است (propertyها init-only هستند). کاربرد پیشرفته‌تر در انواع معمولی:
public sealed class Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Different currencies!");
        return new Money(Amount + other.Amount, Currency);
    }
}
در اینجا حتی عملیات ریاضی نیز یک نمونه‌ی جدید بازمی‌گرداند؛ هیچ وقت state داخلی Money عوض نمی‌شود. یک تمرین برای حرفه‌ای‌ها: فرض کنید در یک domain پیچیده مثل محاسبات مالی، تمامی value objectها و aggregateهای اصلی خود را به‌صورت immutable طراحی کنید. چطور الگوهای تغییرات (مانند state transitions) را مدیریت خواهید کرد؟ این موضوع کلیدی را در معماری آینده خود جدی بگیرید. #DesignPatterns #Immutability #ThreadSafety #CSharpAdvanced @DeveloperAdvocate 🥑

تفاوت معماری Event-Driven و Request-Driven را با نگاه عمیق بررسی کنیم: 🔵 Request-Driven Architecture: در این معماری، تعامل میان سیستم‌ها به صورت هم‌زمان (Synchronous) و بر پایه درخواست/پاسخ انجام می‌شود. سیستم منتظر پاسخ می‌ماند تا ادامه دهد. 📊 Diagram (درخواست‌محور):
Client ──> [ API ] ──> [ Service ] ──> [ DB ]
   <───────────────────────────────────────
// Request-Driven Example (API to Service)
[HttpPost]
public IActionResult Order([FromBody] OrderRequest order)
{
    var result = orderService.Process(order);
    return Ok(result);
}
در این الگو، فراخوانی‌ها هم‌زمان بوده و tightly coupled هستند. --- 🟢 Event-Driven Architecture: در این الگو، ماژول‌های سیستم با ارسال/دریافت رویداد با یکدیگر تعامل دارند. سیستم‌ها decoupled بوده و رویدادها به طور ناهم‌زمان (Asynchronous) پردازش می‌شوند. 📊 Diagram (رویدادمحور):
[ Producer ]
     │
     ▼
 [ Event Bus ]
     │
 ┌───┴─────────┬──────────┐
 ▼             ▼          ▼
[ ServiceA ] [ ServiceB ][ ServiceC ]
// Event-Driven Example (Publishing an event)
// Producer:
public void PlaceOrder(Order order)
{
    var @event = new OrderPlacedEvent(order);
    eventBus.Publish(@event);
}

// Consumer:
public class InventoryHandler : IEventHandler<OrderPlacedEvent>
{
    public Task Handle(OrderPlacedEvent @event)
    {
        // Update inventory asynchronously
        return Task.CompletedTask;
    }
}
در معماری رویدادمحور، سرویس‌ها loosely coupled بوده و با scalability و resiliency بیشتری کار می‌کنند. --- ⚡️ نکته: - Request-Driven مناسب برای عملیات‌های کوتاه و نیازمند پاسخ سریع است. - Event-Driven برای پردازش‌های غیرهمزمان، گسترش‌پذیری بالا و decoupling معماری ایده‌آل است. انتخاب مناسب، کلید موفقیت معماری شماست. @DeveloperAdvocate 🥑

ترفند DevContainers برای پروژه‌های Full-Stack .NET و React اغلب تیم‌های حرفه‌ای که روی پروژه‌های full-stack با .NET (مثلاً WebAPI) و React کار می‌کنند، با چالش همگام‌سازی محیط توسعه مواجه‌اند. DevContainers راه‌حلی عالی برای تضمین consistency محیط Node و .NET میان اعضا است—مخصوصاً اگر نیاز به نسخه خاصی از SDK، tooling یا قابلیت‌هایی مثل hot reload دارید. نمونه یک فایل پیشرفته devcontainer.json برای یک پروژه با ساختار زیر: - /src/backend (پروژه ASP.NET Core) - /src/frontend (پروژه React) کافیست devcontainer را در ریشه پروژه قرار دهید 👇
{
  "name": "Fullstack .NET & React Dev",
  "image": "mcr.microsoft.com/devcontainers/dotnet:8.0-node", // dotnet 8 + Node 18
  "workspaceFolder": "/workspace",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "customizations": {
    "vscode": {
      "settings": {
        "terminal.integrated.defaultProfile.linux": "bash",
        "editor.formatOnSave": true
      },
      "extensions": [
        "ms-dotnettools.csharp",
        "esbenp.prettier-vscode",
        "dbaeumer.vscode-eslint",
        "ms-azuretools.vscode-docker"
      ]
    }
  },
  "postCreateCommand": "dotnet restore ./src/backend && cd ./src/frontend && npm install",
  "forwardPorts": [
    5173, // React (Vite) پیش‌فرض
    3000, // React (Create React App) پیش‌فرض
    5000, // ASP.NET Core
    5001  // ASP.NET Core (HTTPS)
  ],
  "mounts": [
    "source=${localEnv:HOME}/.aspnet/https,target=/root/.aspnet/https,type=bind"
  ],
  "remoteUser": "vscode"
}
امتیاز حرفه‌ای: - تصویر پایه فوق (dotnet:8.0-node) هر دو محیط dotnet و node را در اختیار می‌گذارد. - با forwardPorts می‌توانید بدون زحمت hot reload هر دو سمت را در محیط لوکال ببینید. - با استفاده از VSCode Extensions توسعه هر دو stack ساده‌تر می‌شود. - اگر تست‌های داکرایز شده یا microservice دارید، فیچر docker-in-docker را فعال کنید. برای سناریوهای پیشرفته‌تر (مثلاً multiple containers یا وابستگی به دیتابیس) می‌توانید از فایل‌های docker-compose.yml و تنظیمات multi-container devcontainer هم بهره ببرید. #DevOps #DotNet #React #VSCode #BestPractices @DeveloperAdvocate 🥑

اهمیت مستندسازی مناسب کد (Code Comments و XML Documentation) در سامانه‌های C# را هرگز دست‌کم نگیرید. کدی که امضای ماست، باید همان‌قدر که تمیز و زیباست، قابل فهم و تفسیر هم باشد—نه فقط برای تیم امروز، بلکه برای سال‌ها بعد. کامنت خوب توضیح چرا است، نه چی یا چطور. توضیح مسائلی نظیر دلیل انتخاب الگوریتم خاص، اشاره به تصمیم‌های معماری، یا هر نکته‌ای که بدون آن تحلیل آینده مشکل خواهد شد، ارزشمند است. از کامنت‌های زائد پرهیز کنید تا نویز اطلاعاتی نداشته باشیم. مستندات XML اما برای APIهای عمومی و حتی داخلی حیاتی است—اکوسیستم tooling دات‌نت (مثل Intellisense، DocFX و Sandcastle) مستندات XML را به تجربهٔ توسعه‌دهنده بدل می‌کند. مثال صحیح:
/// <summary>
/// محاسبهٔ نرخ بهره مرکب سالانه.
/// </summary>
/// <param name="principal">مبلغ اولیهٔ سرمایه‌گذاری</param>
/// <param name="rate">نرخ بهره به درصد</param>
/// <param name="years">تعداد سال‌ها</param>
/// <returns>موجودی نهایی پس از اجرا</returns>
public static double CalculateCompoundInterest(
    double principal, double rate, int years)
{
    // نرخ سالانه را به نسبت تبدیل می‌کنیم؛ مثال: 5% → 0.05
    var r = rate / 100;

    // بهره مرکب بر مبنای تعداد سال محاسبه می‌شود
    return principal * Math.Pow(1 + r, years);
}
با همین الگو: - مستندات XML را برای هر API که ممکن است reuse شود (public/internal/protected) بنویسید. - هرگونه shortcut منطق، workaround خاص یا الزام بیزینسی را با کامنت توضیح دهید تا برای بازنگری یا Refactor آینده گمراه نشوید. - مستندات XML را بخشی از Definition of Done تیم کنید. در نهایت: اگر کامنتی نوشتید که می‌تواند به بقیه عمق و روشنی بدهد، آینده‌نگری شما را مهندسین سال‌های بعد تحسین خواهند کرد! @DeveloperAdvocate 🥑

تفاوت مانیتورینگ و Observability یکی از مفاهیم اساسی در طراحی سیستم‌های مدرن است که عموماً به‌اشتباه معادل فرض می‌شوند. مانیتورینگ (Monitoring) یعنی دریافت داده‌های از پیش تعیین‌شده و بررسی آنها برای شناسایی مشکلات یا روندهای خاص. برای مثال جمع‌آوری metrics مثل زمان پاسخ HTTP، مصرف RAM یا تعداد درخواست‌ها با Prometheus، و نمایش آنها با ابزارهایی مثل Grafana. مانیتورینگ معمولاً به سؤال «آیا سیستم سالم است؟» جواب می‌دهد و عمدتاً روی داده‌های کم‌حجم و شمارنده‌ها (counters)، نرخ‌ها (rates) و سنجه‌ها (gauges) تمرکز دارد. Observability اما فراتر می‌رود: یعنی درجه‌ای که می‌توان از طریق داده‌های خروجی، وضعیت داخلی و علت رخداد یک مشکل را شناخت. این مفهوم بر پایۀ جمع‌آوری و تحلیل جامع‌تر داده‌های متنوع مثل logs ساختاریافته (بعنوان روایت وقایع)، traces (زنجیرۀ پراکندگی درخواست‌ها)، metrics و حتی context (برچسب‌ها، exceptionها و ... ) استوار است. Observability می‌پرسد: «وقتی رفتار ناشناخته‌ای رخ می‌دهد، آیا داده کافی برای یافتن علت داریم؟» در معماری مدرن (به‌ویژه Microservices)، تنها با metricها نمی‌توان متوجه ریشه مشکلات شد؛ باید tracing (مثلاً با OpenTelemetry) و logging هوشمند هم مکمل باشند. تصور کنید: در فلو زیر بررسی‌کنید آیا requestها بیش از حد زمان‌بر است و کدام سرویس علت است.
// فرض کنید یک middleware برای لاگ گرفتن و ارسال metric به Prometheus:
public async Task Invoke(HttpContext context) {
    var sw = Stopwatch.StartNew();
    await _next(context);
    sw.Stop();
    _metrics.Record("http_request_duration_seconds", sw.Elapsed.TotalSeconds, 
        new { Path = context.Request.Path, Status = context.Response.StatusCode }
    );
    _logger.LogInformation("Request {Path} finished in {Duration} ms", context.Request.Path, sw.ElapsedMilliseconds);
}
در این رویکرد، مانیتورینگ فقط الارم می‌دهد که تأخیر بالا رفته، اما observability (با داشتن لاگ‌ جزیی و trace) به تیم کمک می‌کند مکانیزم بروز مشکل را کشف کند. ابزارهایی مثل Prometheus (جمع‌آوری metrics)، Grafana (visualization)، و OpenTelemetry (tracing/log collection) با هم “پایه” یک stack observability کامل را می‌سازند؛ فقط وقتی سیستم شما observable است که بتوان هر اتفاق و رفتار نابه‌هنجار را با داده‌های واقعی ریشه‌یابی و تریاژ کرد—not just monitor it. 🔹 مهارت در observability یعنی طراحی داده‌های قابل کشف، نه فقط آلارم‌نویسی ساده. @DeveloperAdvocate 🥑

در پروژه‌های فول‌استک با چندین فرنـت‌اند و بک‌اند (مثلاً React + .NET + Shared Libraries)، استفاده از ابزارهایی مثل Nx یا Turborepo برای مدیریت monorepo به طرز چشمگیری معماری، زمان‌بندی و کیفیت توسعه را ارتقا می‌دهد. چند مزیت کلیدی برای معماران و تیم‌های بالغ: ۱. Dependency Graph هوشمند: هر time تغییر یا build فقط ماژول‌ها و وابستگی‌های مستقیم آن بازسازی می‌شوند. این موضوع با cache و hashing هوشمند، زمان CI/CD را از چندین ساعت به چند دقیقه کاهش می‌دهد. ۲. اشتراک‌گذاری کتابخانه‌ها: فرض کنید مدل‌های DTO یا تست‌های E2E را همزمان برای .NET backend و TypeScript frontend لازم دارید. با Nx می‌توانید یک پروژه‌ی shared بسازید که هر دو محیط مصرف می‌کنند و از duplication جلوگیری می‌شود. ۳. Code Consistency و Enforce قوانین سازمانی: با قابلیت‌هایی نظیر code generators، linting سازمانی و enforce dependency constraints (مثلاً جلوگیری از call لایه UI به لایه Data)، سطح maintainability بالاتر می‌رود. مثال: به‌راحتی اجازه نمی‌دهید یک پروژه‌ی front به صورت مستقیم به DAL وصل شود. ۴. Scaling تیم‌های متعدد: هر تیم می‌تواند یک domain یا bounded context مستقل را مدیریت کند؛ بدون درگیری با context دیگر، اما همچنان publish/release یکپارچه باشد. ۵. تسهیل تست‌های یکپارچه و E2E: orchestration pipeline تست‌ها در monorepo به نسبت انرژی بسیار کمتری می‌خواهد و امکان ردیابی impact تغییر هر ماژول را به سادگی فراهم می‌کند. پیشنهاد حرفه‌ای: اگر microserviceهای .NET خود را به صورت node-based در Nx معرفی کنید (مثلاً با nx-dotnet)، می‌توانید ساختار solutionهای Visual Studio را با dependency graph خیلی غنی‌تر و scale-friendly ترکیب کنید:
// فرض: لایه SharedModels فقط به SharedUtilities وابسته شود
# در nx.json:
"implicitDependencies": {
  "libs/SharedModels": ["libs/SharedUtilities"]
}
این معماری همزمان قدرت flexibility تیم‌ها و کنترل عمیق معماری را ممکن می‌سازد—و migration تدریجی از ساختار repoهای قدیمی به monorepo مدرن را تبدیل به یک فرایند خوشایند می‌کند. @DeveloperAdvocate 🥑

اگه اپ استورتون بالا نمیاد میتونید این کار رو کنید🛒 1️⃣به یه وایفای وصل بشید (هاتسپات یا مودم فرقی نمی‌کنه)🛜 2️⃣تو تنظیمات اتصال به وایفای (More Setting) DNS رو هر جفتش 1.1.1.1 وارد کنید.⚙️ ➖➖➖➖➖➖➖➖➖➖ یه لیست از DNS های تحریم شکن های داخلی خارجی که ممکنه جواب بدن :📝 شکن
178.22.122.100 185.51.200.2
گوگل
8.8.8.8
8.8.4.4
Yandex
77.88.8.8
77.88.8.1
الکترو
78.157.42.100
78.157.42.101
شکن
185.51.200.2
178.22.122.10

🔹 تغییر پارادایم از «ارشد» به «تک‌لید»: مسیر رشد حرفه‌ای بزرگ‌ترین شفت ذهنی برای عبور از یک توسعه‌دهنده‌ی ارشد (Senior) به تک‌لید (Tech Lead)، فراتر رفتن از «تحویل کد عالی» به «رهبر تیم و معمار ارزش» بودن است. دیگر فقط بی‌نقص‌ بودن کد شما مهم نیست؛ باید توانایی رشد دیگران، توازن بده‌بستدهای فنی و ارتباط موثر با ذینفعان را در خود توسعه دهید. 🔸 چند محور کلیدی در این مسیر: ۱. مالکیت سیستمی، نه صرفاً ماژولی: باید کل سیستم را End-to-End ببینید و به جای بهینه‌سازی صرف یک بخش، پیوستگی و انسجام کل راهکار را ایجاد کنید. ۲. تصمیم‌گیری آگاهانه حول بده‌بستدها: هر تصمیم فنی (از طراحی معماری تا انتخاب الگوهای Refactoring) باید بر اساس ارزش تجاری، هزینه نگه‌داری، و سرعت توسعه سنجیده شود—not فقط شیک‌تر بودن تکنیکی. ۳. انتقال ذهنیت و تعاملات: - Pair Programming و Code Review باید به رشد تیم منجر شود، نه صرفاً اصلاح کد. - راهبری فنی یعنی هدایت بحث‌ها به سوی راه‌حل‌هایی که «ساده، پایدار و توسعه‌پذیر» باشند. 🔸 مثال واقعی: فرض کنید تصمیم گرفته‌اید در پروژه یک لایه Caching اضافه کنید. به جای یک PoC سریع، به مواردی مانند Consistency، Scalability، Testability و حتی تجربه Run-time توجه می‌کنید و با تیم به یک Design Doc یا ADR شفاف می‌رسید:
public interface ICacheProvider
{
    Task<T?> GetAsync<T>(string key);
    Task SetAsync<T>(string key, T item, TimeSpan ttl);
}

public class DistributedCacheProvider : ICacheProvider
{
    // وابستگی‌ها را طوری می‌چینید که به راحتی Mock/Test شوند
    // و بتوان روی Fault Tolerance مانور داد
}
⚡️ خود را «ضرب‌کننده توان تیم» کنید، نه فقط بهترین فرد در تحویل. نقطه بلوغ وقتی است که موفقیت شما، تولید و رشد دیگران را چندبرابر می‌کند—نه فقط الگانت‌ترین کلاس‌هایتان! @DeveloperAdvocate 🥑

Repost from NetBlocks
⚠️ Update: Internet connectivity has again collapsed in #Iran following a brief period when residents could exchange messages
⚠️ Update: Internet connectivity has again collapsed in #Iran following a brief period when residents could exchange messages with the outside world; Iran's nation-scale internet shutdown remains in effect, limiting access to information as the conflict with Israel continues

کاربرد حرفه‌ای Delegates و Events در #CSharp مدرن یکی از سناریوهای روزمره، event-driven کردن ماژول‌هایی مانند سیستم مانیتورینگ یا notification است. فرض کنید می‌خواهیم یک سرویس سلامت که به طور ادواری وضعیت سلامتی سیستم را بررسی می‌کند پیاده‌سازی کنیم و سایر سرویس‌ها بتوانند به تغییرات این وضعیت واکنش نشان دهند—به لطف دلیگیت‌ها و ایونت‌ها این سناریو elegant و loosely-coupled پیاده می‌شود:
public enum HealthStatus { Healthy, Degraded, Down }

public class HealthChangedEventArgs : EventArgs
{
    public HealthStatus OldStatus { get; }
    public HealthStatus NewStatus { get; }

    public HealthChangedEventArgs(HealthStatus oldStatus, HealthStatus newStatus)
    {
        OldStatus = oldStatus;
        NewStatus = newStatus;
    }
}

public class HealthMonitor
{
    private HealthStatus _currentStatus = HealthStatus.Healthy;
    public event EventHandler<HealthChangedEventArgs>? HealthChanged;

    public void CheckHealth()
    {
        // فرض کنید اینجا منطق چک سطح سلامت سیستم اجرا می‌شود
        var observedStatus =
            (DateTime.UtcNow.Second % 3 == 0) ? HealthStatus.Degraded : HealthStatus.Healthy;
        
        if (observedStatus != _currentStatus)
        {
            OnHealthChanged(new HealthChangedEventArgs(_currentStatus, observedStatus));
            _currentStatus = observedStatus;
        }
    }

    protected virtual void OnHealthChanged(HealthChangedEventArgs e)
        => HealthChanged?.Invoke(this, e);
}

// مصرف‌کننده‌ای که با event فوق decouple است:
public class AlertingService
{
    public void Subscribe(HealthMonitor monitor)
    {
        monitor.HealthChanged += OnHealthChanged;
    }

    private void OnHealthChanged(object? sender, HealthChangedEventArgs e)
    {
        Console.WriteLine($"[Alert] وضعیت سلامت تغییر کرد: {e.OldStatus} → {e.NewStatus}");
    }
}

// استفاده:
var monitor = new HealthMonitor();
var alerting = new AlertingService();
alerting.Subscribe(monitor);

// در یک تایمر یا حلقه تست
while (true)
{
    monitor.CheckHealth();
    Thread.Sleep(2000);
}
✅ نکته‌ها: - تعریف استاندارد event با EventHandler<T> و ارسال state قدیم/جدید در EventArgs ایده‌آل است. - مصرف‌کنندگان صرفاً به رویداد متصل می‌شوند و از خود implementation مانیتورینگ ایزوله‌اند (Single Responsibility + Decoupling). - استفاده از الگوهای مشابه بسیار در domain-driven design (DDD) و reactive programming کاربرد دارد. این pattern را می‌توانید برای هر use-case قابل event-driven، حتی با ترکیب Delegateها برای composability بیشتر، گسترش دهید. @DeveloperAdvocate 🥑