Developer Advocate
Ir al canal en Telegram
1 035
Suscriptores
Sin datos24 horas
Sin datos7 días
-130 días
Archivo de publicaciones
1 035
مایکروسافت در این مقاله به معرفی قابلیت جدید شخصیسازی پاسخهای هوش مصنوعی در GitHub Copilot پرداخته است. حالا کاربران میتوانند تجربه برنامهنویسی خود را به کمک پروفایلهای مختلف در کوپایلوت سفارشیسازی کنند. با این قابلیت، میتوانید سبک کدنویسی، زبان برنامهنویسی ترجیحی، فریمورک یا ابزارهای مورد علاقه خود را به Copilot معرفی کنید تا پاسخهای دریافتی دقیقتر و متناسب با نیازهای شما باشد. همچنین امکان فعالسازی چندین پروفایل بر اساس نوع پروژه یا تسک وجود دارد تا Copilot با توجه به هر سناریو، پیشنهاد مناسبی ارائه دهد. این شخصیسازیها باعث افزایش بهرهوری و بهبود کیفیت کد شما میشود. مایکروسافت اعلام کرده قابلیت Personalization بهتدریج برای کاربران و سازمانها فعال خواهد شد و توسعهدهندگان میتوانند از طریق تنظیمات Copilot آن را مدیریت کنند. نتیجه این قابلیت، تولید کدهای کاربردیتر، مرتبطتر و نزدیکتر به سبک و نیازهای خاص هر برنامهنویس است.
https://devblogs.microsoft.com/dotnet/customize-ai-responses-from-github-copilot
@DeveloperAdvocate 🥑
1 035
سه پیوند (Three Links)
در این مقاله، مارتین فاولر درباره یک الگوی مهم در معماری نرمافزار به نام «سه پیوند» صحبت میکند. او توضیح میدهد که امروزه محصولات نرمافزاری معمولاً سه نوع پیوند (لینک) مهم در ساختار خود دارند:
1. پیوند سلسلهمراتبی (Hierarchical Link): این پیوند مسیر دادهها یا ساختار والد-فرزندی (مثل پوشهها و فایلها) را مشخص میکند. مثال واضح آن ساختار دایرکتوری سیستمعامل یا منوهای تو در تو است.
2. پیوند زمانبندی (Chronological Link): این پیوند نشاندهنده ترتیب زمانی رخدادها یا تغییرات است. مانند تاریخ و زمان ایجاد یا ویرایش یک محتوا، یا ثبت تسکهای انجامشده بهصورت تایملاین.
3. پیوند ارجاعی (Referential Link): این پیوند برای ارجاع دادن یک عنصر به عنصر دیگر استفاده میشود. مثل هایپرلینکها در صفحات وب یا رفرنس به یک تسک دیگر در سیستم مدیریت پروژه.
فاولر تاکید میکند که هرکدام از این پیوندها نقش مجزایی در محصولات پیچیده دارند و نباید با هم ادغام شوند. اغلب سیستمها برای راحتی، چند نوع پیوند را با هم ترکیب میکنند (مثلاً ترکیب ساختار سلسلهمراتبی با ترتیب زمانی)، اما این کار باعث سردرگمی کاربران و محدودیت توسعهی آینده میشود.
مثالها:
- در سیستمهای مدیریت محتوا، مقالات معمولاً بر اساس پوشه (سلسلهمراتبی)، تاریخ انتشار (زمانی) و ارجاعات متقابل (ارجاعی) سازماندهی میشوند.
- در ابزارهای مدیریت پروژه، تسکها میتوانند به تسکهای دیگر ارجاع داده شوند، در یک فاز یا فولدر قرار گیرند و هرکدام زمان خود را داشته باشند.
او توصیه میکند که طراحی نرمافزارها باید به گونهای باشد که هر یک از این پیوندها به طور مجزا و شفاف در محصول حفظ و مدیریت شوند تا هم توسعهدهندگان و هم کاربران بیشترین بهرهوری و درک را داشته باشند.
در پایان، فاولر هشدار میدهد که اگرچه در ابتدا ترکیب این پیوندها ساده و سریع بهنظر میرسد، اما در بلندمدت باعث بروز مشکلات پیچیده در نگهداری و گسترش سیستم و همچنین سردرگمی کاربران خواهد شد. پس بهتر است از همان ابتدا، معماری را بر مبنای «سه پیوند» اصولی طراحی کنیم.
https://martinfowler.com/articles/202507-three-links.html
@DeveloperAdvocate 🥑
1 035
اگر معماریتان را با کانتینرهای .NET Core/ASP.NET میسازید، آگاهی نسبت به اکوسیستم Managed Container Orchestration آمازون (ECS و Fargate) میتواند تحولی جدی در تحویل و نگهداری سرویسهای شما ایجاد کند. Fargate نیاز به مدیریت سرور و Capacity Planning را حذف میکند؛ یعنی واقعاً میتوانید روی کد تمرکز کنید، نه زیرساخت.
یک ترفند مهم در دپلوی کانتینرهای .NET روی Fargate: حتماً dotenv و Configuration Management را بر مبنای SecretsManager یا Parameter Store طراحی کنید تا Cloud-Native واقعی بسازید. برای هندل کردن Configuration در زمان اجرا میتوانید از Amazon.Extensions.Configuration.SystemsManager استفاده کنید:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddSystemsManager(
$"/myapp/{env.EnvironmentName}/",
optional: true
);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
نکته: با این کار میتوانید Configها و Secrets حساس را در AWS نگه دارید و فقط هنگام اجرا در محیط Fargate—و نه در Pipelineهای CI/CD یا حتی سورس کنترل—بازیابی کنید.
برای orchestrate کردن چند کانتینر (مثلاً سرویس Identity جدا از API)، از definition چندگانه در ECS Task استفاده کنید و با استفاده از AWS Service Discovery، یکدیگر را پیدا کنید. این قابلیت به شدت معماری microservices را تقویت میکند.
در نظر داشته باشید بهترین راه اتوماسیون تعریف سرویسها و تسکها، به کمک ابزارهایی نظیر AWS CDK (با پروژهای مثل L2 Construct «ApplicationLoadBalancedFargateService») است تا به جای YAMLنویسی خستهکننده، همه چیز را به صورت Infrastructure-as-Code (IAC) و با دیسیپلین بسازید.
مطالعه عمیقتر:
Deploy ASP.NET Core Applications to AWS Fargate using Amazon ECS
Cloud Computing (Azure/AWS/GCP) & Serverless
@DeveloperAdvocate 🥑1 035
در سیستمهای توزیعشده، عبور درخواست از سرویسهای مختلف بدون ردیابی دقیق تقریباً تضمین میکند که اشکالزدایی یک کابوس خواهد بود. "Correlation ID" راه حل کلیدی است—یک شناسه یکتا را به هر درخواست تخصیص دهید و آن را در سراسر کل اکوسیستم حمل و لاگ کنید.
🔹 ایجاد Correlation ID و انتقال بین سرویسها
بهترین حالت، اگر یک correlation id از کلاینت دریافت شد، حفظش کنید—اگر نه، خودتان بسازید:
public static class CorrelationIdMiddleware
{
private const string HeaderName = "X-Correlation-ID";
public static async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!context.Request.Headers.TryGetValue(HeaderName, out var correlationId))
correlationId = Guid.NewGuid().ToString();
context.Items[HeaderName] = correlationId;
context.Response.Headers[HeaderName] = correlationId;
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await next(context);
}
}
}
🔹 انتقال Correlation ID هنگام فراخوانی سرویس دیگر
public class CorrelatingHttpClientHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (HttpContext.Current?.Items["X-Correlation-ID"] is string correlationId)
request.Headers.Add("X-Correlation-ID", correlationId);
return await base.SendAsync(request, cancellationToken);
}
}
🔹 ثبت Correlation ID در لاگها (مثلاً با Serilog):
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {CorrelationId} {Message}{NewLine}{Exception}")
.CreateLogger();
نکات پیشرفته:
- همیشه Correlation ID را به کلاینت بازگردانید تا در پیگیری درخواست مفید باشد.
- Header اختصاصی (مانند X-Correlation-ID) استفاده کنید و مستندسازی را فراموش نکنید.
- مراقب باشید مقدار header را در همه سرویسها propagate کنید—حتی در صفهای پیام یا ایونتها.
- ابزارهایی مثل Application Insights یا OpenTelemetry را برای ترسیم chain درخواستها و تحلیل عمیقتر استفاده کنید.
مطالعه دقیقتر:
https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/log-tracing-correlation-ids
Microservices Patterns (Advanced)
@DeveloperAdvocate 🥑1 035
🌐 راهبردهای Replication در دیتابیس: Master-Slave، Master-Master و Group Replication
افزایش دسترسپذیری (High Availability) و مقیاسپذیری خواندن (Read Scalability) همواره دغدغه سیستمهای mission-critical بوده است. انتخاب استراتژی مناسب replication پیشران رسیدن به این اهداف است.
1️⃣ Master-Slave Replication:
در این الگو، یک نود master مسئول پذیرفتن و انجام همه writeهاست. سایر نودها (slaves) تنها replication دادهها را از master دریافت و صرفاً خواندن انجام میدهند. مزایا: سادگی پیادهسازی و consistency بالا. نقطه ضعف؟ Master single-point-of-failure است و مقیاسپذیری نوشتن ندارد.
2️⃣ Master-Master Replication:
اینجا هر نود میتواند write و replication را انجام دهد و معمولاً conflict resolution اهمیت مییابد. این مدل برای read-heavy plus write-heavy workload راهحل مناسبی میشود، اما مدیریت consistency و همگامسازی (مثلاً با ترفندهایی مثل write conflict avoidance یا last-write-win) چالشها را افزایش میدهد.
3️⃣ Group Replication:
الگویی پیشرفتهتر (مانند MySQL Group Replication یا Galera Cluster) که مفهوم consensus و quorum را وارد پیکربندی میکند. در این مدل، هر نود عضو گروه، قابلیت vote برای تایید تراکنش جدید را دارد و replication مبتنی بر پروتکلهایی مانند Paxos یا Raft پیاده میشود. نتیجه؟ بالاترین سطح availability و no single-point-of-failure، البته هزینه latency و پیچیدگی بیشتر.
💡 نکته عملیاتی: در بسیاری از پروژههای enterprise .NET، کافی است عملیاتهای حیاتی (مانند invoice generation) فقط روی master اجرا شوند و خواندن گزارشات از slaves، یا حتی طراحی abstractionی ساده برای انتخاب رپلیکا مناسب کافی باشد:
public interface IReplicaSelector
{
string GetReadConnectionString();
string GetWriteConnectionString();
}
// نمونهی ساده جهت load-balancing روی خواندن از رپلیکای تصادفی
public class RandomReplicaSelector : IReplicaSelector
{
private readonly string[] _readReplicas;
private readonly string _writeReplica;
private readonly Random _random = new();
public RandomReplicaSelector(string[] readReplicas, string writeReplica)
{
_readReplicas = readReplicas;
_writeReplica = writeReplica;
}
public string GetReadConnectionString() =>
_readReplicas[_random.Next(_readReplicas.Length)];
public string GetWriteConnectionString() => _writeReplica;
}
🔗 مطالعه بیشتر و مثالهای دقیقتر:
https://martinfowler.com/articles/patterns-of-distributed-systems/replication.html
Advanced Database Concepts
@DeveloperAdvocate 🥑1 035
🚀 React Suspense: تحول مدیریت لودینگ و تقسیم کد در اپلیکیشنهای ریاکت
تا حالا با Stateهای متعدد، Skeletonها، و useEffectهای تودرتو برای مدیریت وضعیت لودینگ دست و پنجه نرم کردید؟ React Suspense رو شاید بیشتر به چشم "ابزاری برای تقسیم کد" (Code Splitting) دیده باشید، اما در نسخههای جدید، Suspense یک abstraction قوی برای Data Fetching هم هست؛ خصوصاً وقتی با ابزارهایی مثل React Query، SWR یا حتی کتابخانه سرورمحور React Server Components ترکیب میشه.
۱. تقسیم کد (Code Splitting) با lazy و Suspense:
import React, { Suspense, lazy } from 'react';
const UserProfile = lazy(() => import('./UserProfile'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
);
}
👍 هر کامپوننت فقط موقع نیاز لود میشه، اپلیکیشن سریعتر بالا میاد و حس بهتری منتقل میکنه.
۲. Data Fetching با Suspense: رویکرد Future-Proof
در قالبهای بسیار مدرن، دیتافچینگ میتونه بهصورت Declarative با Suspense مدیریت بشه. این کارها رو ابزارهایی مثل Relay، React Query و حتی ویژگی Server Components سادهتر و تمیزتر انجام میدن:
// ⚡️ فرض کنید از React Query یا کتابخانه مشابه استفاده میکنید
<Suspense fallback={<Skeleton />}>
<UserList />
</Suspense>
داخل UserList کسی دیگه لازم نیست شرط لودینگ مدیریت بشه. Suspense مستقیماً وضعیت رو کنترل میکنه. نتیجه؟ جدایی بهتر منطق دیتا و UI، کد تمیزتر و هموارتر شدن تجربه کاربری.
۳. چند سطوح Suspense برای بهینهسازی پیشرفته:
ترکیب چند Suspense با fallbackهای متفاوت باعث میشه بخشهای مختلف صفحه مستقل از هم لود شوند.
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</Suspense>
در نتیجه، بالاترین بخش با Skeleton خاص خودش و پایینترها با Skeleton خودشان نمایش داده میشوند. کنترل ریزبینانهتر روی UX و مقیاسپذیری لودینگ!
✅ نکات معماری سطح بالا:
- Suspense مثل یک قرارداد بین data source و UI عمل میکنه؛ data fetching باید Promise-based باشه!
- در اکوسیستم ریاکت مدرن، جهت مدیریت async rendering و SSR با Server Components و Streaming Suspense متحد میشن.
- مراقب باشید بعضی ابزارها هنوز با Suspense for Data Fetching Production-ready نیستند؛ اما future-proof و مدرن فکر کنید.
مطالعه بیشتر (انگلیسی):
React Docs: Introducing Suspense for Data Fetching
#React #FrontendArchitecture #Suspense #BestPractices
Advanced React Concepts
@DeveloperAdvocate 🥑1 035
👀 Mutation Observer API: نظارت حرفهای بر تغییرات DOM با رویکرد مهندسی
در پروژههایی که UI پویا دارند—مخصوصاً صفحات با ماژولهای مستقل، SPAها یا افزونههای مرورگر—تشخیص و واکنش به تغییرات دقیق DOM اهمیت کلیدی دارد. Mutation Observer به شما امکان میدهد که هرگونه اضافه، حذف یا ویرایش در DOM را عمیقاً رصد و لاگ کنید یا برای همگامسازی وضعیتهای بیرونی، اجرای A/B تستها و حتی باگیابی دیباگرهای پیشرفته، ورود کنید.
مثالی از استفاده ترکیبی با JSInterop در Blazor را ببینید؛ به محض اضافه شدن یک المان خاص به DOM، یک متد C# فراخوانی میشود:
// Blazor C#: ثبت callback در .NET برای دریافت نوتیفیکیشنها
[JSInvokable]
public static Task NotifyElementAddedAsync(string elementId)
{
Console.WriteLine($"Element added: {elementId}");
// یا هر منطق دلخواه!
return Task.CompletedTask;
}
// JavaScript: راهاندازی MutationObserver و فراخوانی متد داتنت
const observer = new MutationObserver(mutations => {
for (let mutation of mutations) {
for (let node of mutation.addedNodes) {
if (node.id === 'target-element') {
DotNet.invokeMethodAsync('AssemblyName', 'NotifyElementAddedAsync', node.id);
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
چرا این مهم است؟
- میتوانید در راندریگ اجزاء Third-party بوتاسترپ یا Material Design، المانهای تزریقی را رصد و کنترل کنید.
- در اپلیکیشنهای پیچیده، MutationObserver نقطه طلایی برای دیباگینگ race conditionها و memory leakهاست.
- با JSInterop کنترل عمیقتر و واکنش Real-Time به رویدادهایی که در لایه داتنت قابل مشاهده نیستند ممکن است.
📚 مطالعه بیشتر:
MDN Web Docs – MutationObserver API
Advanced JavaScript/TypeScript
@DeveloperAdvocate 🥑1 035
در معماریهای مدرن مبتنی بر ابر، تلفیق مشاهدهپذیری (Observability) با گردش کامل دادههای لاگ، متریک و ترِیس، کلید موفقیت در مانیتورینگ سرویسهاست—بهویژه وقتی زیرساخت چندابر (Multi-Cloud) و میکروسرویس داریم. سه ابزار کلیدی این حوزه: Azure Monitor، AWS CloudWatch و Grafana هستند که هرکدام مزایای خاص خود را دارند.
۱. Azure Monitor:
با استفاده از Log Analytics، متریکها و لاگهای منابع Azure تجمیع و به شکلی پیشرفته قابل کوئری و تحلیلاند. در اپلیکیشنهای .NET Core میتوانید از SDK رسمی Application Insights بهصورت زیر بهره ببرید:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"]);
}
این کد مستقیماً ترِیسها، درخواستها، لاگها و حتی دِپندنسیها را به Azure ارسال میکند؛ با کوئری KQL میتوانید داشبرد پیشرفته بسازید.
۲. AWS CloudWatch:
CloudWatch در AWS امکان جمعآوری، پایش و آلارم بر اساس لاگ و متریک را فراهم میکند. Integrate شدن .NET با CloudWatch Logs Agent:
using Amazon.CloudWatchLogs;
using Amazon.CloudWatchLogs.Model;
var client = new AmazonCloudWatchLogsClient();
await client.PutLogEventsAsync(new PutLogEventsRequest
{
LogGroupName = "my-app-logs",
LogStreamName = "production",
LogEvents = new List<InputLogEvent>
{
new InputLogEvent
{
Message = "Critical error occurred.",
Timestamp = DateTime.UtcNow
}
}
});
۳. Grafana:
Grafana با Datasourceهای متنوع (مانند Loki برای لاگ، Prometheus برای متریک) میتواند هاب مرکزی مشاهدهپذیری پروژههای Multicloud یا Kubernetes شما باشد. اتصال سرویس اپلیکیشن به Grafana معمولاً از طریق exporterها یا push gateway انجام میشود:
- برای .NET: استفاده از `prometheus-net` برای متریک:
// Program.cs
app.UseMetricServer(); // exposes /metrics endpoint
🔗 مطالعهی بیشتر: Monitoring and logging in Azure – best practices
نکته معماری: طراحی سیستمهای مشاهدهپذیر باید با نگاه به تجمیع دادهها، کوئریهای بلادرنگ و تحلیل cross-source انجام شود—ترکیب چند ابزار بالا با رویکرد بیرونکشیدن دادهها (Pull/Push)، یک SIEM ساده و سریع برای معماری ابری فراهم میکند.
Cloud Computing (Azure/AWS/GCP) & Serverless
@DeveloperAdvocate 🥑1 035
در TypeScript، انواع Utility، شرطی (Conditional) و مبتنی بر نگاشت (Mapped) ابزار قدرتمندی برای ساختن Contractهای منعطف و قابل اطمینان هستند—چیزی که معماریهای مدرن .NET نیز به شدت نیاز دارند. بیایید معادلهایی از این مفاهیم را در داتنت بررسی کنیم تا الهام بگیرید که چطور میتوان این تکنیکهای سطح بالا را در TypeScript با رویکردهای مشابه در C# ترکیب کرد.
Mapped Types در TypeScript معادل Reflection و Expression-based Mapping در C# است. فرض کنید میخواهیم از یک DTO، مدل بهروزرسانی (Patch) بسازیم که همه پراپرتیها Nullable شوند:
public static Type MakeAllPropertiesNullable(Type type)
{
var nullableTypeBuilder = ... // استفاده از Reflection.Emit یا Source Generators
// برای هر پراپرتی، نوع Nullable<T> را تعریف کن
}
در TypeScript، شبیه این کار را با Partial<T> انجام میدهیم:
type Patch<T> = {
[P in keyof T]?: T[P];
}
Conditional Types در TypeScript مثل Exclude<T, U> یا Extract<T, U> هستند که معادل آنها در C# معمولا Generic Constraints پیچیدهتر است، اما میتوان با ترکیب LINQ و Reflection به صورت runtime مشابهش را ساخت:
public IEnumerable<PropertyInfo> GetPropertiesExcept<T, TExclude>()
{
return typeof(T).GetProperties()
.Where(p => p.PropertyType != typeof(TExclude));
}
در TypeScript:
type Without<T, U> = T extends U ? never : T;
Utility Types مثل Readonly<T>, Record<K, T> و ... اغلب معادل Data Annotationها یا Wrapperهای سفارشی در C# هستند (مثلا Immutable Objectها). اما در اکوسیستم داتنت، این مفاهیم را با استفاده از init-only properties و Immutable Collections پیاده میکنیم:
public class Person
{
public string Name { get; init; }
public int Age { get; init; }
}
جمعبندی: الگوهای پیشرفتهی Typing مانند Mapped و Conditional Types در TypeScript، الهامبخش نوآوریهای معماری در .NET هم هستند. تقاطع این رویکردها به شما امکان میدهد سیستمهایی بسازید که علاوه بر چابکی، از نظر استحکام و maintainability بسیار قدرتمند شوند.
مقاله پیشنهادی:
Advanced TypeScript Types: Conditional, Mapped, and More (Medium)
Advanced JavaScript/TypeScript
@DeveloperAdvocate 🥑1 035
🔹 Data Mesh: آینده معماری داده در سازمانهای پیچیده
در مواجهه با حجم زیاد داده و تیمهای متعدد، معماریهای سنتی (مانند Data Lake) با چالشهای مقیاسپذیری، مالکیت و چابکی روبهرو هستند. Data Mesh یک تحول فکری است که با تکیه بر اصول زیر، معماری داده شما را بازتعریف میکند:
1️⃣ مالکیت غیرمتمرکز داده (Decentralized Ownership): هر تیم فیچر مستقل، مسئول توسعه، نگهداری و کیفیت محصولات دادهای خود است، دقیقاً مانند سرویسها در معماری Microservices.
2️⃣ تفکر محصولی داده (Data as a Product): داده دیگر یک "خروجی جانبی" نیست، بلکه باید مانند یک API با SLA، مستندسازی، مشاهدهپذیری و امکان کشف ارائه شود.
3️⃣ پلتفرم خودسرویس (Self-Serve Data Platform): تیمها باید بتوانند با حداقل وابستگی به تیم مرکزی، داده خود را منتشر و مصرف کنند.
4️⃣ Governance فدرال (Federated Governance): استانداردسازی و امنیت بهشکل توزیعشده اما کنترلشده تضمین میشوند.
یک مثال ساده از پیادهسازی Product Thinking در داده، استفاده از strongly-typed Data Contractها است. در .NET میتوانید چنین Contractی را با Swagger/OpenAPI برای سرویسهای داده فراهم کنید:
public class CustomerPurchaseDto
{
public Guid CustomerId { get; set; }
public List<OrderDto> Orders { get; set; }
public DateTime LastUpdated { get; set; }
}
public class OrderDto
{
public Guid OrderId { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; }
}
انتشار Contractها در یک Data Catalog مرکزی (برای مثال OpenMetadata یا Azure Purview) امکان کشف، تضمین کیفیت و تعامل سیستمی به صورت ایمن را برای تیمهای مختلف فراهم میکند.
بیشتر بخوانید:
How to Move Beyond a Monolithic Data Lake to a Distributed Data Mesh – MartinFowler.com
Modern Architectural Patterns
@DeveloperAdvocate 🥑1 035
📦 IndexedDB: راهکاری قدرتمند برای ذخیرهسازی داده ساختیافته سمت کلاینت
در توسعه اپلیکیشنهای مدرن وب، نیاز به ذخیرهسازی دادههای بزرگ و ساختیافته در سمت کلاینت اجتنابناپذیر است، به خصوص زمانی که بخواهید دادهها حتی در حالت آفلاین هم در دسترس کاربر باشند. IndexedDB این نیاز را بهصورت تا حد زیادی مشابه یک NoSQL ساده، برطرف میکند و میتواند دادههایی با ساختار پیچیده را درون مرورگر نگهداری کند.
اگر با Blazor WebAssembly یا سایر فریمورکهای SPA کار میکنید، بهرهبرداری از IndexedDB در کنار C# تجربهای متفاوت و در عین حال موثر برای توسعهدهندگان .NET فراهم میآورد. در این سناریو، توصیه میشود از JS Interop برای تعامل مستقیم با API مبتنی بر Promise مرورگر استفاده کنید.
نمونه پیادهسازی پایه برای افزودن داده به IndexedDB از طریق JS Interop در Blazor WebAssembly:
// JsInteropHelper.cs
public class IndexedDbInterop
{
private readonly IJSRuntime _jsRuntime;
public IndexedDbInterop(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task SetItemAsync(string key, object value)
{
await _jsRuntime.InvokeVoidAsync("myIndexedDb.setItem", key, JsonSerializer.Serialize(value));
}
public async Task<T> GetItemAsync<T>(string key)
{
var result = await _jsRuntime.InvokeAsync<string>("myIndexedDb.getItem", key);
return result == null ? default : JsonSerializer.Deserialize<T>(result);
}
}
و در جاوااسکریپت (که باید در فایل wwwroot/index.html یا یک فایل js مجزا گنجانده شود):
window.myIndexedDb = {
setItem: async (key, value) => {
let db = await openDb();
let tx = db.transaction('store', 'readwrite');
tx.objectStore('store').put(value, key);
await tx.complete;
},
getItem: async (key) => {
let db = await openDb();
let tx = db.transaction('store', 'readonly');
let val = await tx.objectStore('store').get(key);
await tx.complete;
return val;
}
};
// تابع openDb (براساس idb library) باید قبلاً تعریف شده باشد.
مزایای اصلی IndexedDB:
- ذخیره ایمن مگابایتها داده حتی در حالت آفلاین
- مدل مبتنی بر object store و index برای دیتای ساختیافته
- سازگاری با اکثر مرورگرهای مدرن و امنیت sandbox
🎯 نکته مهم: به دلیل ماهیت async و Promise-driven بودن IndexedDB، حتماً لایه abstraction مناسبی برای ارتباط #Blazor با مقادیر ایجاد کنید؛ زیرا خطاهای runtime در تعاملات بین زبانها بسیار حساس هستند.
برای آشنایی عمیقتر با معماری و بهترین شیوههای استفاده از IndexedDB مخصوصاً در پروژههای Blazor WebAssembly، مطالعه این مقاله توصیه میشود:
https://devblogs.microsoft.com/dotnet/in-process-blazor-durable-state-persistence-with-indexeddb/
Advanced JavaScript/TypeScript
@DeveloperAdvocate 🥑1 035
🛡️ RBAC vs. ABAC در معماری مجوزدهی: فراتر از نقشها برای انعطاف واقعی
در طراحی سیستمهای Enterprise، انتخاب مدل مناسب برای کنترل دسترسی، تعیینکننده قابلیت توسعه و نگهداری است. دو رویکرد پرکاربرد:
- RBAC (Role-Based Access Control): مبتنی بر نقش (Role) کاربران است؛ ساده، اما گاهی محدود.
- ABAC (Attribute-Based Access Control): مبتنی بر ویژگیها (Attributes) است؛ انعطافپذیرتر اما پیچیدهتر.
در مثال زیر، یک PolicyHandler برای ABAC در ASP.NET Core نوشتهایم که بهجای نقش، روی ویژگی «سطح حساسیت» داده و «دپارتمان» کاربر تصمیم میگیرد:
public class DocumentAccessRequirement : IAuthorizationRequirement
{
public int RequiredSensitivityLevel { get; }
public DocumentAccessRequirement(int requiredLevel) => RequiredSensitivityLevel = requiredLevel;
}
public class DocumentAccessHandler : AuthorizationHandler<DocumentAccessRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DocumentAccessRequirement requirement,
Document resource)
{
var userDepartment = context.User.FindFirst("department")?.Value;
var userSensitivity = int.Parse(context.User.FindFirst("sensitivityLevel")?.Value ?? "0");
if (resource.Department == userDepartment && resource.SensitivityLevel <= userSensitivity)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
ABAC را میتوان برای بیان سیاستهایی مثل "کاربرانی که مالکیت پروژه دارند یا عضو دپارتمان خاصی هستند و حساسیت داده زیر مقدار معینی است" بهکار برد؛ چیزی که در RBAC دشوار میشود.
نکات معماری:
- RBAC برای اکثر سازمانها تا زمانی که نیازمندیها سادهاند قابل توصیه است.
- ABAC زمانی ارزشمند است که نیازمند سیاستهای دقیق، پیادهسازی Separation of Duty، یا Multi-tenancy پویا باشید.
- ترکیب RBAC و ABAC (Hybrid Access Control) در پروژههای مدرن رایج است.
مطالعه بیشتر (Microsoft Docs):
https://learn.microsoft.com/en-us/azure/active-directory/architecture/authz-access-control
Application Security (AppSec)
@DeveloperAdvocate 🥑1 035
در معماری سرویسمحور ASP.NET Core، درک عمیق چرخه عمر سرویسها (DI lifetimes) و نحوه استفاده صحیح از آنها کلید پایداری و مقیاسپذیری است. اشتباه ساده در اختیارگذاری یا مصرف نادرست Scopedها به عنوان Singleton یا Inject مستقیم DbContext، میتواند باعث Memory Leak، Race Condition یا حتی اختلال جدی در اپلیکیشن شود.
💡 خلاصه چرخهها:
- Singleton: سرویس فقط یک بار در طول حیات اپلیکیشن نمونهسازی میشود.
- Scoped: برای هر درخواست HTTP یک نمونه جدید ساخته میشود.
- Transient: هر بار درخواست، نمونهای تازه.
ضدالگو رایج: وابستگی مستقیم Scoped به Singleton
مثال: Inject کردن
IDbContext (که Scoped است) در سرویس Singleton. نتیجه؟ context share بین درخواستها! راه حل: از ServiceFactory یا Scoped Service Resolver استفاده کنید.
public class MySingletonService
{
private readonly IServiceProvider _serviceProvider;
public MySingletonService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void DoSomething()
{
using (var scope = _serviceProvider.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
// استفاده امن از DbContext داخل اسکوپ
}
}
}
🎛 در مواقع نیاز به ساخت داینامیک سرویسها یا اعمال پارامترهای runtime، Service Factory را توصیه میکنم (مثلاً هنگام تزریق کانفیگهای پیچیده یا وابستگی حلقوی).
services.AddScoped<IMyService>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
return new MyService(config["MyCustomSetting"]);
});
نکات پیشرفته:
- از Inject مستقیم IServiceProvider مگر برای Factory Pattern یا advanced scenarios اجتناب کنید (anti-pattern: Service Locator).
- در تستها و background serviceها به scope دقت کنید، مخصوصاً اگر با HostedService کار میکنید.
- از تزریق Transient در Singleton بپرهیزید، چون چرخه عمر سرویس ترازی نیست و قطعی حفظ نمیشود.
مطالعه بیشتر:
Dependency injection in ASP.NET Core — Microsoft Docs
ASP.NET Core (Advanced)
@DeveloperAdvocate 🥑1 035
Powerful, high-performance, durable state machine implementations for Microsoft Orleans.
https://github.com/ledjon-behluli/DurableStateMachines
https://www.nuget.org/packages/DurableStateMachines
#dotnet #aspnetcore #state #durable
@DeveloperAdvocate 🥑
1 035
⭐ استراتژیهای Replication پایگاهداده: Master-Slave، Master-Master و Group Replication
در معماریهای توزیعشده که نیاز به High Availability و Read Scalability داریم، انتخاب استراتژی Replication صحیح بسیار حیاتیست. نگاه عمیقتر:
🔹 Master-Slave (Primary-Replica): تنها یک master عملیات write را انجام میدهد و یک یا چند slave فقط خواندن را از master کپی میکنند. مناسب برای Workloadهایی با حجم بالای read و write محدود. چالش بزرگ: lag بین Master و Slaves و نقطه تکنفری شکست (SPOF).
🔹 Master-Master (Multi-primary): چندین node میتوانند write را انجام دهند. Replication دوطرفه اما پیادهسازی conflict resolution پیچیده میشود (مثلاً update همزمان یک رکورد). برای Split-brain و Conflict باید راهکار داشته باشید. پیشنهاد: فقط در سناریوهایی با writeهای کم و توزیعشده.
🔹 Group Replication (یا Multi-leader Sync): تمامی nodeها توانایی write دارند و تغییرات با voting/consensus synchronize میشوند (مانند Paxos یا Raft). بسیار مناسب برای resiliency و zero-downtime اما پیچیدگی عملیاتی (مانند handling transaction ordering یا network partition) بالاست.
برای stack داتنت، توصیه میشود Replication را خارج از Business Logic پیادهسازی کنید (مثلاً با Database middleware یا قابلیت native engine). اگر eventual consistency را میپذیرید، استفاده از الگوهایی مانند CQRS + Read Replicas بسیار کارآمد است.
نمونه pseudo-code ساده setup یک Replication-aware Repository:
public class ReadWriteRepository
{
private readonly IDbConnection _writeDb;
private readonly IReadOnlyList<IDbConnection> _readReplicas;
private readonly Random _random = new Random();
public ReadWriteRepository(IDbConnection writeDb, IReadOnlyList<IDbConnection> readReplicas)
{
_writeDb = writeDb;
_readReplicas = readReplicas;
}
public T Read<T>(string sql, object param)
{
var replica = _readReplicas[_random.Next(_readReplicas.Count)];
return replica.QuerySingle<T>(sql, param);
}
public void Write(string sql, object param)
{
_writeDb.Execute(sql, param);
}
}
مطالعه تکمیلی در مقاله عالی Martin Fowler درباره Replication Patterns:
https://martinfowler.com/articles/patterns-of-distributed-systems/replication.html
Advanced Database Concepts
@DeveloperAdvocate 🥑1 035
🧪 Testability: طراحی کد برای افزایش قابلیت تست (Unit/Integration)
نگاه معماری به تستپذیری فراتر از Mock کردن ساده است. با رعایت چند اصل ساده و اعمال انتخابهای درست در طراحی، میتوانیم کدهایی بسازیم که تست آنها لذتبخش و قابل اعتماد باشد:
1️⃣ وابستگیها را کنترل کنید
مستقیم ساختن وابستگیها با
new دشمن تست است. SRP و IoC با هم موجب جدایی concerns و کاهش hard-coupling میشوند. استفاده از Constructor Injection و تعریف Interface برای وابستگیها، هسته تستپذیری را میسازد.
public interface IEmailSender
{
Task SendAsync(string to, string body);
}
public class UserService
{
private readonly IEmailSender _emailSender;
public UserService(IEmailSender emailSender) => _emailSender = emailSender;
public async Task Register(string email)
{
// Logic ...
await _emailSender.SendAsync(email, "Welcome!");
}
}
2️⃣ کدهای Pure بنویسید
توابع pure و بدون عوارض جانبی (Side effect) تست را تبدیل به یک بازیابی ساده از ورودی/خروجی میکنند. منطق تجاری (Domain Logic) هرچه بیشتر از زیرساخت جدا باشد، نوشتن تستهای آن آسانتر و سریعتر خواهد بود.
3️⃣ مرزبندی شفاف (Clear Boundaries)
هرجایی که به دنیای بیرون (دیتابیس، فایل سیستم، پیامرسان) متصل میشوید را با abstraction جدا کنید. این نقاط باید جداشده، تست شوند (Test Double)، و بهروشنی از core logic فاصله بگیرند.
4️⃣ کوچک نگهداشتن واحدها (Small Units)
متدها و کلاسهای بزرگ مثل Black Box هستند. واحدهای کوچک با یک مسئولیت، هم تست سادهتری دارند هم نتایج تستشان گویاست.
5️⃣ تابعیت از SOLID، خصوصاً Open/Closed
تغییر رفتار با افزونهها (Extension) یا پالسیها (Policy) بهجای تغییر کدهای موجود، احتمال شکست تستهای قدیمی را کاهش میدهد.
6️⃣ اجتناب از Singleton State و Static Data
Static state دشمن تستپذیری و مانع Parallel test execution است. Scoped dependencies و life-cycle management توسط DI container به شما آزادی تست را میدهد.
یک نکته نهایی:
در .NET، Integration Testها را به کمک In-Memory گزینههایی همچون TestServer در ASP.NET Core و دیتابیسهایی مثل SQLite میتوانید بهشکلی امن و سریع بنویسید.
مطالعه بیشتر:
Martin Fowler – UnitTest
(مطالب عمیق در مورد تعریف درست تست واحد و حوزه مسئولیت تستها)
Quality Attributes & Cross-Cutting Concerns
@DeveloperAdvocate 🥑1 035
الگوهای پیشرفتهی API Gateway مثل Ocelot و YARP فراتر از پروکسی ساده هستند؛ این ابزارها بستری قوی برای تجمیع درخواست (Aggregation)، مسیریابی هوشمند (Smart Routing) و مدیریت نگرانیهای مقطعی (Cross-Cutting Concerns) مانند امنیت و مشاهدهپذیری فراهم میکنند.
🔹 Request Aggregation:
یکی از الگوهای اساسی ترکیب چندین میکروسرویس در یک پاسخ است. فرض کنید یک API نیاز دارد اطلاعات کاربر را از دو سرویس مختلف جمعآوری کند. با استفاده از Ocelot میتوانید با تعریف یک مسیر aggregator، چند downstream به صورت همزمان فراخوانی شوند و نتیجهی نهایی در API Gateway تجمیع گردد:
public class UserAggregator : IDefinedAggregator
{
public async Task<DownstreamResponse> Aggregate(List<HttpContext> responses)
{
var profile = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync();
var orders = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync();
var merged = $"{{\"profile\":{profile},\"orders\":{orders}}}";
return new DownstreamResponse(new StringContent(merged, Encoding.UTF8, "application/json"),
HttpStatusCode.OK, new List<Header>(), "OK");
}
}
در فایل ocelot.json تنها کافی ست یک route از نوع aggregator تعریف کنید.
🔹 Intelligent Routing:
YARP با middewareهای پیشرفته امکان مسیریابی پویا براساس سربرگها، احراز هویت یا load balancing بر مبنای تخصیص سفارشی متریکها را ایجاد میکند. یک نمونه:
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapReverseProxy(proxyPipeline =>
{
proxyPipeline.Use(async (context, next) =>
{
var region = context.Request.Headers["X-Region"];
context.Items["Destination"] = region switch
{
"eu" => "cluster-eu",
"us" => "cluster-us",
_ => "cluster-default"
};
await next().ConfigureAwait(false);
});
});
});
اکنون مسیریابی براساس موقعیت جغرافیایی، بسیار ساده و منعطف انجام میشود!
🔹 Cross-Cutting Concerns (امنیت، مشاهدهپذیری، و غیره):
API Gateway نقطهی مرکزی پیادهسازی سیاستهایی مثل JWT validation، rate limiting، API key validation و ابزارهای tracing (مثلاً OpenTelemetry) است. برای مثال افزودن احراز هویت JWT در Ocelot فقط نیاز به یک middleware دارد:
services.AddAuthentication()
.AddJwtBearer("GatewayBearer", options =>
{
options.Authority = "https://login.example.com";
options.Audience = "api-gateway";
});
✅ مهم: به کمک API Gateway میتوانید نگرانیهای مشترک را تنها یک بار، در یک نقطهی مرکزی پیادهسازی کنید و سرعت توسعه، مدیریت و مقیاسپذیری سرویسها را بهبود دهید.
مطالعهی بیشتر در مقالهی رسمی مایکروسافت:
https://learn.microsoft.com/en-us/azure/architecture/patterns/api-gateway
Microservices Patterns (Advanced)
@DeveloperAdvocate 🥑1 035
یکی از چالشهای عمیق در سیستمهای توزیعشده .NET، آسیبپذیری در برابر «Insecure Deserialization» است؛ یعنی زمانی که دادههای سریالایزشده از منبع نامعتبر یا غیرقابل اعتماد deserialize میشوند و مهاجم میتواند کد مخرب اجرا کند. بهطور ویژه، استفاده از BinaryFormatter یا ObjectStateFormatter میتواند بسیار خطرناک باشد؛ چرا که امکان تزریق اشیای ساختگی با payload مخرب به runtime را فراهم میکند.
✔️ نمونه حمله:
فرض کنید API شما، شیء دریافتشده را با BinaryFormatter بازمیسازد:
using (var ms = new MemoryStream(byteArray))
{
var formatter = new BinaryFormatter();
var obj = formatter.Deserialize(ms);
}
مهاجم میتواند payload حاوی شیئی خاص بسازد که در هنگام deserialization متدهای خاصی را اجرا میکند.
✋ بهترین راهکارها:
۱. هرگز داده سریالایزشده را از کاربران یا منابع غیرقابلاعتماد بدون اعتبارسنجی deserialize نکنید.
۲. بههیچوجه از BinaryFormatter، NetDataContractSerializer یا ObjectStateFormatter استفاده نکنید؛ اینها deprecated و ناامن هستند.
۳. اگر لازم است فقط به deserialization اعتماد کنید، از serializerهای امنتر مثل Newtonsoft.Json، System.Text.Json یا MessagePack با مدل contract-based استفاده کنید.
۴. در Newtonsoft.Json حتما از TypeNameHandling اجتناب کنید یا فقط برای نوعهای مشخصشده مجاز کنید:
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None // یا Auto با محدودیت نوعهای مجاز
};
JsonConvert.DeserializeObject(jsonData, settings);
۵. در صورت نیاز به polymorphic serialization، لیست typeهای مجاز (trusted types) را explicit تعریف کنید.
۶. همیشه Principle of Least Privilege را رعایت کنید: سرویسهایی که داده سریالایزشده میگیرند، باید محدودترین سطح دسترسی را داشته باشند.
مقاله پیشنهادی (Martin Fowler):
https://martinfowler.com/articles/201701/theres-a-hole-in-my-deserialization.html
Application Security (AppSec)
@DeveloperAdvocate 🥑1 035
در معماریهایی که بر eventual consistency تکیه دارند (مثل سیستمهای توزیعشده یا microservices با Event Sourcing و CQRS)، یکی از بزرگترین چالشها طراحی UI است که تجربه کاربری مطلوب را باوجود تاخیرهای ذاتی در نهایت همگرایی دادهها حفظ کند. کاربران انتظار دارند عملیاتشان فوری دیده شود، اما در backend دادهها با کمی تاخیر نهایی میشوند. چند نکته و الگوی کلیدی:
🔹 بازخورد خوشساخت به کاربر: حتما پس از واکنش به رویدادها (مثل ثبت سفارش یا تغییر آدرس) از optimistic UI استفاده کنید؛ بلافاصله تغییر را در UI به کاربر نشان دهید، اما با کامپوننتی نظیر Snackbar یا Notification توضیح دهید «در حال انجام…». درصورت خطا، تغییر rollback و پیام مناسب نمایش دهید.
🔹 بازنگری داده پس از نهایی شدن: وقتی دادهها eventual consistent هستند، UI باید پس از تایید نهایی از سمت backend (مثلا دریافت پیام تایید یا آپدیت داده) داده را دوباره واکشی یا بروزرسانی کند. برای این کار، استفاده از الگوهایی مانند Poll-Until-Consistent یا WebSocket-based push بسیار مفید است.
🔹 مدیریت optimistic update در state management: اگر از state containerهایی مثل Redux, Fluxor یا حتی ViewModel در MVVM استفاده میکنید، حتما state مربوط به عملیات pending و موفق/ناموفق بودن را track کنید تا واکنش UI سریع و دقیق باشد.
مثال ساده C# (برای MVVM در اپلیکیشن WPF یا MAUI):
public class OrderViewModel : INotifyPropertyChanged
{
public ICommand PlaceOrderCommand { get; }
private readonly IOrderService _orderService;
private bool _isSaving;
private string _userMessage;
public bool IsSaving
{
get => _isSaving;
set { _isSaving = value; OnPropertyChanged(); }
}
public string UserMessage
{
get => _userMessage;
set { _userMessage = value; OnPropertyChanged(); }
}
public OrderViewModel(IOrderService orderService)
{
_orderService = orderService;
PlaceOrderCommand = new AsyncRelayCommand(PlaceOrderAsync);
}
private async Task PlaceOrderAsync()
{
IsSaving = true;
UserMessage = "در حال ثبت سفارش...";
try
{
await _orderService.PlaceOrderAsync();
UserMessage = "سفارش ثبت شد، منتظر تایید نهایی...";
// در اینجا میتوانید با push/poll پیغام تایید نهایی یا آپدیت را مدیریت کنید
}
catch (Exception ex)
{
UserMessage = "خطا در ثبت سفارش: " + ex.Message;
}
finally
{
IsSaving = false;
}
}
}
🔹 شفافیت و آموزش: هیچگاه کاربر را گیج نکنید؛ واضح نمایش دهید دادهها ممکن است تغییر کرده یا هنوز تایید نشدهاند. حتی میتوانید از نشانههای بصری (icon یا رنگ متفاوت) استفاده کنید تا بخشهایی که هنوز قطعی نشدهاند مشخص باشند.
برای مطالعه بیشتر، مقالهی زیر از Martin Fowler دربارهی eventual consistency و تاثیر آن بر UX توصیه میشود:
https://martinfowler.com/articles/patterns-of-distributed-systems/eventual-consistency.html
Microservices Patterns (Advanced)
@DeveloperAdvocate 🥑1 035
وقتی مشکل پیچیدهای پیش میآید، معمولاً اولین ابزار برنامهنویسها، همان
Console.WriteLine() است—اما این روش برای سناریوهای چندریسمانی، رفتار غیرقطعی، یا وابستگیهای پیچیده به اندازه کافی قدرتمند نیست. بیایید چند تکنیک پیشرفتهتر را بررسی کنیم:
✅ Debugger Breakpoints و Conditions
در محیط Visual Studio، بجای Breakpoint ساده، از Breakpointهای شرطی بهره بگیرید تا فقط وقتی وضعیت دادهها به انفجار نزدیک میشود گیر بیفتید:
// روی خط زیر یک breakpoint با شرط i == 42 قرار دهید
for (int i = 0; i < 100; i++)
{
SomeProcess(i);
}
✅ Tracepoints
آیا میخواهید یک پیام دیباگ بدون توقف برنامه، درست شبیه Console بنویسید؟ Tracepointها همان Breakpointهایی هستند که فقط پیغام سفارشی چاپ میکنند. کافی است Breakpoint را تنظیم و بجای اکشن "Break" گزینه "Print Message" را انتخاب کنید.
✅ Live Visualizerهای Inspection
با ابزارهایی چون Debugger Display یا Watch/Immediate Window میتوانید وضعیت آبجکتهای پیچیده را در لحظه ببینید. مثال:
[DebuggerDisplay("User: {UserName}, IsActive: {IsActive}")]
public class User { /*...*/ }
✅ Performance Profiling & IntelliTrace
از Performance Profiler یا ابزار IntelliTrace در Visual Studio برای track کردن call stack و tracing state استفاده کنید—اینها ریشهٔ bottleneck یا خطای منطقی را با دقت بیشتری نمایان میکنند.
✅ Dynamic Logging با Serilog یا ILogger
جای شکستهای موقت و ناامن، لاگگیری ساختارمند و سطحبندیشده (مثلاً با Serilog یا Microsoft.Extensions.Logging) را پیادهسازی کنید تا بتوانید براساس کانتکست و severity تصمیمگیری کنید:
_logger.LogWarning("Invalid value: {Value}", someValue);
=> مطالعه بیشتر:
Advanced Debugging in Visual Studio (Microsoft Docs)
Productivity & Self-Improvement
@DeveloperAdvocate 🥑