🌐 چگونه با NET. یک کوتاهکنندهٔ لینک بسازیم؟
یک کوتاهکنندهٔ URL ابزاری ساده اما قدرتمند است که لینکهای طولانی را به نسخههای کوتاهتر و قابلمدیریتتری تبدیل میکند. این ابزار بهویژه در پلتفرمهایی که محدودیت کاراکتر دارند یا برای بهبود تجربهٔ کاربری و کاهش شلوغی لینکها بسیار مفید است. دو نمونهٔ محبوب از کوتاهکنندههای لینک عبارتاند از Bitly و TinyURL. طراحی چنین سیستمی چالشی جالب است که مسائلی سرگرمکننده برای حل کردن دارد.
اما چطور میتوان یک کوتاهکنندهٔ URL را با NET. ساخت؟
⚙️ عملکردهای اصلی در یک URL Shortener
یک کوتاهکنندهٔ لینک معمولاً دو قابلیت اصلی دارد:
1️⃣ تولید یک کد منحصربهفرد برای هر URL
2️⃣ هدایت (Redirect) کاربر به آدرس اصلی هنگام دسترسی به لینک کوتاهشده
🧩 طراحی سیستم کوتاهکنندهٔ لینک (URL Shortener System Design)
در سطح بالا، سیستم ما شامل دو endpoint است:
• یکی برای کوتاه کردن URLهای بلند
• دیگری برای Redirect کاربران بر اساس لینک کوتاهشده
در این مثال، لینکهای کوتاهشده در یک پایگاه داده PostgreSQL ذخیره میشوند.
برای بهبود عملکرد خواندن (Read Performance)، میتوان از کش توزیعشدهای مانند Redis نیز در سیستم استفاده کرد. ⚡️
🔢 تولید کد منحصربهفرد برای لینکها
ابتدا باید اطمینان حاصل کنیم که سیستم قادر به تولید تعداد زیادی لینک کوتاه است.
برای این منظور، به هر URL بلند یک کد منحصربهفرد اختصاص میدهیم و از آن برای ساخت لینک کوتاه استفاده میکنیم.
طول این کد و مجموعهٔ کاراکترهایی که از آن استفاده میکنیم، تعیینکنندهٔ تعداد لینکهای کوتاهی است که سیستم میتواند تولید کند.
🎲 استراتژی تولید کد تصادفی
ما از استراتژی تولید کد تصادفی (Random Code Generation) استفاده خواهیم کرد.
این روش پیادهسازی سادهای دارد و نرخ برخورد (Collision) آن قابلقبول است.
البته در ازای این سادگی، افزایش اندک در زمان تأخیر (Latency) را بهعنوان معاوضه خواهیم داشت.
بااینحال، در ادامه گزینههای دیگری را نیز برای بهینهسازی بررسی خواهیم کرد. 🔍
🧱 مدل داده (Data Model) در ساخت URL Shortener با NET.
برای شروع، باید مشخص کنیم چه دادههایی را در پایگاه داده ذخیره خواهیم کرد.
مدل دادهای که نیاز داریم بسیار ساده است. ما یک کلاس ShortenedUrl داریم که نمایانگر لینکهایی است که در سیستم ذخیره میشوند:
public class ShortenedUrl
{
public Guid Id { get; set; }
public string LongUrl { get; set; } = string.Empty;
public string ShortUrl { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public DateTime CreatedOnUtc { get; set; }
}
🔍 این کلاس شامل ویژگیهای زیر است:
• LongUrl: آدرس اصلی (بلند)
• ShortUrl: آدرس کوتاهشده
• Code: کد منحصربهفردی که نمایانگر لینک کوتاهشده است
• Id و CreatedOnUtc نیز برای اهداف دیتابیس و ردیابی (tracking) استفاده میشوند.
کاربران با ارسال مقدار Code به سیستم ما، باعث میشوند برنامه بهدنبال LongUrl متناظر بگردد و آنها را به آدرس اصلی هدایت کند. 🚀
🧩 پیکربندی دیتابیس با Entity Framework Core
برای مدیریت ارتباط با دیتابیس، ما یک کلاس ApplicationDbContext ایجاد میکنیم.
این کلاس وظیفهٔ تنظیم موجودیتها (Entities) و پیکربندی context پایگاه داده را برعهده دارد.
در این مرحله دو کار برای بهبود عملکرد انجام میدهیم:
1️⃣ تعیین حداکثر طول فیلد Code با استفاده از HasMaxLength
2️⃣ تعریف یک ایندکس یکتا (Unique Index) روی ستون Code
این ایندکس یکتا مانع از بروز تکرار در مقدار Code میشود،
بنابراین هیچ دو لینک کوتاهی در دیتابیس مقدار مشابهی نخواهند داشت.
همچنین محدود کردن طول رشته باعث صرفهجویی در فضای ذخیرهسازی میشود و برای ایندکسگذاری ستونهای متنی در برخی دیتابیسها ضروری است.
⚠️ نکته: برخی از دیتابیسها رشتهها را بهصورت غیر حساس به حروف کوچک و بزرگ (case-insensitive) در نظر میگیرند.
این موضوع میتواند تعداد لینکهای قابل تولید را بهشدت کاهش دهد.
بنابراین باید دیتابیس را طوری پیکربندی کنید که Code بهصورت case-sensitive ذخیره و بررسی شود.
🧠 پیادهسازی ApplicationDbContext
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<ShortenedUrl> ShortenedUrls { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ShortenedUrl>(builder =>
{
builder
.Property(shortenedUrl => shortenedUrl.Code)
.HasMaxLength(ShortLinkSettings.Length);
builder
.HasIndex(shortenedUrl => shortenedUrl.Code)
.IsUnique();
});
}
}
🎯 تولید کد منحصربهفرد (Unique Code Generation) در URL Shortener
یکی از مهمترین بخشهای سیستم کوتاهکننده لینک، تولید کد منحصربهفرد برای هر URL است.
الگوریتمهای مختلفی برای پیادهسازی این بخش وجود دارد، اما هدف ما این است که کدها بهصورت یکنواخت در میان تمام مقادیر ممکن توزیع شوند تا احتمال برخورد (collision) کاهش یابد. ⚖️
⚙️ رویکرد انتخابی ما
در این پیادهسازی از تولید کد تصادفی (Random Unique Code Generator) با استفاده از یک الفبای از پیش تعریفشده (Predefined Alphabet) استفاده میکنیم.
این روش ساده است و احتمال برخورد در آن بسیار پایین است — هرچند راهحلهای بهینهتر و سریعتری هم وجود دارد که بعداً به آنها اشاره خواهیم کرد.
🧩 تعریف تنظیمات کوتاهسازی لینک
ابتدا یک کلاس به نام ShortLinkSettings تعریف میکنیم که شامل دو مقدار ثابت (constant) است:
یکی برای تعیین طول کد کوتاه و دیگری برای الفبایی که قرار است از آن کاراکترها انتخاب شوند.
public static class ShortLinkSettings
{
public const int Length = 7;
public const string Alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
}
🔢 این الفبا شامل ۶۲ کاراکتر است (۲۶ حرف بزرگ + ۲۶ حرف کوچک + ۱۰ عدد).
بنابراین، تعداد ترکیبهای ممکن برابر خواهد بود با:
62⁷ = 3,521,614,606,208
یا بهصورت خوانا:
سه تریلیون و پانصد بیست و یک میلیارد و ششصد و چهارده میلیون و ششصد و شش هزار و دویست و هشت ترکیب منحصربهفرد! 😮
این مقدار بهراحتی برای اکثر سیستمهای URL Shortener کافی است.
🧠 پیادهسازی سرویس تولید کد (UrlShorteningService)
در ادامه، یک کلاس به نام UrlShorteningService پیادهسازی میکنیم که وظیفهی تولید کد تصادفی و بررسی یکتایی آن در دیتابیس را برعهده دارد.
public class UrlShorteningService(ApplicationDbContext dbContext)
{
private readonly Random _random = new();
public async Task<string> GenerateUniqueCode()
{
var codeChars = new char[ShortLinkSettings.Length];
const int maxValue = ShortLinkSettings.Alphabet.Length;
while (true)
{
for (var i = 0; i < ShortLinkSettings.Length; i++)
{
var randomIndex = _random.Next(maxValue);
codeChars[i] = ShortLinkSettings.Alphabet[randomIndex];
}
var code = new string(codeChars);
if (!await dbContext.ShortenedUrls.AnyAsync(s => s.Code == code))
{
return code;
}
}
}
}
🔍 در این کد:
برای هر کاراکتر از کد کوتاه، یک مقدار تصادفی از Alphabet انتخاب میشود.
سپس با دیتابیس بررسی میکنیم که آیا این کد قبلاً استفاده شده است یا خیر.
اگر کد منحصربهفرد بود، آن را برمیگردانیم؛ در غیر این صورت، مجدداً تلاش میکنیم.
⚠️ نقاط ضعف و بهبودهای احتمالی
1️⃣ افزایش زمان پاسخ (Latency):
در حال حاضر، هر بار باید با دیتابیس چک کنیم که آیا کد تکراری است یا خیر.
✅ راهحل: میتوان کدهای منحصربهفرد را پیشاپیش در دیتابیس تولید و ذخیره کرد تا در لحظه نیازی به بررسی نباشد.
2️⃣ حلقه بینهایت:
اگر برخوردهای متوالی اتفاق بیفتند، این پیادهسازی تا بینهایت تکرار خواهد شد.
✅ راهحل: بهجای while (true)، از یک تعداد تکرار ثابت استفاده کنید و در صورت تکرار زیاد، Exception پرتاب کنید تا سیستم کنترلشدهتر عمل کند.
🚀 طراحی و پیادهسازی URL Shortener در NET. — بخش نهایی
در این بخش، بعد از آمادهسازی منطق اصلی سیستم، میخواهیم دو Endpoint اصلی را پیادهسازی کنیم:
1️⃣ کوتاهسازی لینک (URL Shortening)
2️⃣ ریدایرکت کاربر به لینک اصلی (URL Redirection)
🔗 کوتاهسازی لینک (URL Shortening)
در ابتدا، با استفاده از یک Minimal API ساده، یک Endpoint برای کوتاهسازی URL ایجاد میکنیم.
این Endpoint یک URL را از کاربر میگیرد، آن را اعتبارسنجی میکند، سپس با کمک سرویس UrlShorteningService، یک کد منحصربهفرد تولید کرده و لینک کوتاهشده را در دیتابیس ذخیره میکند. در نهایت، لینک کوتاهشده به کاربر بازگردانده میشود.
public record ShortenUrlRequest(string Url);
app.MapPost("shorten", async (
ShortenUrlRequest request,
UrlShorteningService urlShorteningService,
ApplicationDbContext dbContext,
HttpContext httpContext) =>
{
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out _))
{
return Results.BadRequest("The specified URL is invalid.");
}
var code = await urlShorteningService.GenerateUniqueCode();
var httpRequest = httpContext.Request;
var shortenedUrl = new ShortenedUrl
{
Id = Guid.NewGuid(),
LongUrl = request.Url,
Code = code,
ShortUrl = $"{httpRequest.Scheme}://{httpRequest.Host}/{code}",
CreatedOnUtc = DateTime.UtcNow
};
dbContext.ShortenedUrls.Add(shortenedUrl);
await dbContext.SaveChangesAsync();
return Results.Ok(shortenedUrl.ShortUrl);
});
📌 نکته:
در اینجا احتمال اندکی از Race Condition وجود دارد — چون ابتدا کد تولید میشود و بعد در دیتابیس ذخیره میگردد. ممکن است دو درخواست همزمان، یک کد مشابه تولید کنند.
اما از آنجا که در دیتابیس Unique Index تعریف کردهایم، جلوی درج مقادیر تکراری گرفته میشود.
🔁 ریدایرکت به URL اصلی (URL Redirection)
در سناریوی دوم، وقتی کاربر روی لینک کوتاه کلیک میکند، سیستم باید او را به لینک اصلی هدایت کند.
برای این کار یک Endpoint دیگر ایجاد میکنیم که کد کوتاهشده را از مسیر (route parameter) میگیرد و در دیتابیس جستوجو میکند.
اگر لینک پیدا شد، کاربر به آدرس اصلی Redirect میشود.
app.MapGet("{code}", async (string code, ApplicationDbContext dbContext) =>
{
var shortenedUrl = await dbContext
.ShortenedUrls
.SingleOrDefaultAsync(s => s.Code == code);
if (shortenedUrl is null)
{
return Results.NotFound();
}
return Results.Redirect(shortenedUrl.LongUrl);
});📨 در صورت موفقیت، پاسخ HTTP با کد وضعیت 302 (Found) بازگردانده میشود که نشاندهندهٔ ریدایرکت موقت است.
⚡️ نقاط قابل بهبود در سیستم کوتاهکننده لینک
اگرچه پیادهسازی فعلی کاملاً قابلاستفاده است، اما میتوان با چند بهبود ساده آن را مقیاسپذیرتر و حرفهایتر کرد:
1️⃣ Caching 🧠
استفاده از Redis برای کش کردن لینکها و کاهش بار دیتابیس.
2️⃣ Horizontal Scaling ⚙️
طراحی سیستم برای مقیاسپذیری افقی و مدیریت ترافیک بالا.
3️⃣ Data Sharding 🧩
تقسیم دادهها بین چند دیتابیس برای بهبود کارایی و توزیع بار.
4️⃣ Analytics 📊
افزودن تحلیلها برای رصد تعداد کلیکها، موقعیت کاربران و نرخ استفاده.
5️⃣ User Accounts 👤
امکان ایجاد حساب کاربری برای مدیریت لینکهای کوتاهشده توسط هر کاربر.
✅ حالا شما یک سیستم کامل URL Shortener با NET. دارید!
میتوانید این پروژه را گسترش دهید و با افزودن قابلیتهای بالا، آن را به یک راهکار مقیاسپذیر و قدرتمند در سطح تولید (Production) تبدیل کنید.
🔖هشتگها:
#URLShortener #SystemDesign
⚖️ متعادلسازی Cross-Cutting Concerns در Clean Architecture
Cross-cutting concerns
جنبههایی از نرمافزار هستند که بر کل برنامه تأثیر میگذارند.
اینها قابلیتهایی در سطح کل برنامهاند که در چندین لایه و بخش تکرار میشوند.
نکتهی کلیدی در مدیریت این دغدغهها این است که باید در یک نقطهی مرکزی متمرکز شوند.
این کار از تکرار کد جلوگیری کرده و اتصال شدید (Tight Coupling) میان اجزای سیستم را کاهش میدهد.
🧩 نمونههایی از Cross-Cutting Concerns
🔐 احراز هویت و مجوزدهی (Authentication & Authorization)
🧠 ثبت وقایع و ردیابی (Logging & Tracing)
🚨 مدیریت استثناها (Exception Handling)
✅ اعتبارسنجی (Validation)
⚡️ ذخیرهسازی در حافظه یا کش (Caching)
🏗 Cross-Cutting Concerns در Clean Architecture
در معماری تمیز، Cross-Cutting Concerns نقش مهمی در حفظ قابلیت نگهداری (Maintainability) و مقیاسپذیری (Scalability) سیستم دارند.
در حالت ایدهآل، این دغدغهها باید جدا از منطق اصلی کسبوکار (Core Business Logic) پیادهسازی شوند.
این کار کاملاً با اصول Clean Architecture همراستا است، چرا که بر تفکیک وظایف (Separation of Concerns) و ماژولار بودن سیستم (Modularity) تأکید دارد.
با این کار، قوانین دامنهی شما تمیز و بدون آلودگی باقی میمانند و معماریتان نیز منعطف و قابل گسترش خواهد بود.
🧱 جایگاه مناسب برای Cross-Cutting Concerns
بهترین محل برای پیادهسازی Cross-Cutting Concerns، لایهی Infrastructure است.
در محیط ASP.NET Core میتوانید از Middlewareها، Decoratorها یا Pipeline Behaviorهای MediatR استفاده کنید.
فرقی نمیکند کدام روش را انتخاب کنید — اصل راهنما این است که این دغدغهها باید جدا از منطق اصلی و در یک نقطهی مرکزی مدیریت شوند.
✳️ مثالهایی از رویکردها
🔹 Middleware ها:
مناسب برای کنترلهای سراسری مانند Logging، Authorization یا Exception Handling.
🔹 Decorator ها:
مناسب برای افزودن رفتارهایی مثل Caching یا Logging به سرویسها بدون تغییر در کد اصلی.
🔹 MediatR Pipeline Behavior:
برای اعمال Validation، Logging یا Caching در سطح درخواستها و دستورها در معماری CQRS.
به این ترتیب، Cross-Cutting Concerns بهصورت ساختیافته و قابل مدیریت در کل سیستم توزیع میشوند،
در حالی که منطق اصلی برنامه از آنها جدا و تمیز باقی میماند.
⚙️ Cross-Cutting Concern شماره ۱ - لاگگیری (Logging)
لاگگیری یکی از جنبههای بنیادین توسعهٔ نرمافزار است که به شما اجازه میدهد رفتار یک برنامه را بررسی کنید.
این قابلیت برای دیباگ کردن (Debugging)، پایش سلامت سیستم (Monitoring Application Health) و ردیابی فعالیت کاربران و ناهنجاریهای سیستم (Tracking User Activities and System Anomalies) حیاتی است.
در زمینهٔ Clean Architecture، پیادهسازی لاگگیری باید به گونهای انجام شود که اصل تفکیک وظایف (Separation of Concerns) حفظ شود.
🧠 رویکرد ظریف با MediatR’s IPipelineBehavior
یکی از راههای زیبا برای رسیدن به این هدف، استفاده از IPipelineBehavior در MediatR است.
با قرار دادن منطق لاگگیری درون یک Pipeline Behavior، اطمینان حاصل میکنیم که لاگگیری بهعنوان یک دغدغهٔ مجزا و مستقل از منطق تجاری در نظر گرفته میشود.
این روش به ما امکان میدهد اطلاعات دقیقی از درخواستهایی که در سراسر برنامه جریان دارند ثبت کنیم.
📋 ویژگیهای یک سیستم لاگگیری مؤثر
لاگگیری مؤثر باید یکنواخت (Consistent)، دارای زمینه (Context-Rich) و غیرمزاحم (Non-Intrusive) باشد.
با استفاده از قابلیتهای Structured Logging در Serilog، میتوانیم لاگهایی بسازیم که نهتنها گویا هستند بلکه بهراحتی قابل جستوجو نیز هستند.
این ویژگی برای درک وضعیت برنامه در هر لحظه از زمان ضروری است.
اگر این کار بهدرستی انجام شود، Structured Logging بینشهای بسیار ارزشمندی دربارهٔ وضعیت برنامه ارائه میدهد —
بدون آنکه منطق اصلی سیستم را شلوغ کند.
این کار نوعی تعادل میان جزئیات و شفافیت (Balance of Granularity and Clarity) است که اطمینان میدهد لاگها ابزاری مفید باشند نه منبعی از نویز.
🧩 نمونه کد
using Serilog.Context;
internal sealed class RequestLoggingPipelineBehavior<TRequest, TResponse>(
ILogger<RequestLoggingPipelineBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : class
where TResponse : Result
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
string requestName = typeof(TRequest).Name;
logger.LogInformation(
"Processing request {RequestName}",
requestName);
TResponse result = await next();
if (result.IsSuccess)
{
logger.LogInformation(
"Completed request {RequestName}",
requestName);
}
else
{
using (LogContext.PushProperty("Error", result.Error, true))
{
logger.LogError(
"Completed request {RequestName} with error",
requestName);
}
}
return result;
}
}
🧩 Cross-Cutting Concern شماره ۲ — اعتبارسنجی (Validation)
اعتبارسنجی یکی از دغدغههای اساسی (Critical Cross-Cutting Concerns) در مهندسی نرمافزار است.
این بخش درواقع اولین خط دفاعی سیستم در برابر ورود دادههای نادرست به برنامه محسوب میشود.
اعتبارسنجی از بروز وضعیتهای ناسازگار داده (Inconsistent Data States) و آسیبپذیریهای امنیتی احتمالی (Security Vulnerabilities) جلوگیری میکند.
⚙️ اعتبارسنجی در Clean Architecture
در مثال زیر، یک Validation Pipeline Behavior ایجاد میکنیم.
این ساختار به ما اجازه میدهد تا منطق اعتبارسنجی را از منطق تجاری جدا نگه داریم —
بهعبارتی دیگر، اعتبارسنجی قبل از رسیدن درخواست به بخش اصلی پردازش انجام میشود.
این الگو موجب پاکسازی لایهٔ منطق تجاری (Business Logic) از قوانین تکراری و اطمینان از یکپارچگی دادهها میشود.
🧠 انواع اعتبارسنجی
در هنگام طراحی اعتبارسنجی، باید بین دو نوع مهم تمایز قائل شوید:
اعتبارسنجی ورودی (Input Validation)
اعتبارسنجی قوانین تجاری (Business Rule Validation)
🔹 Input Validation
برای بررسی درستی و قالب دادهها به کار میرود — مثلاً طول رشته، محدودهٔ اعداد یا فرمت تاریخ.
هدف از آن اطمینان از این است که داده قبل از ورود به منطق سیستم، معیارهای پایه را رعایت کرده باشد.
🔹 Business Rule Validation
بررسی میکند که دادهها با قوانین و منطق خاص دامنهٔ شما (Domain Logic) سازگار هستند یا نه.
🧩 نقش اعتبارسنجی در پایداری سیستم
اجرای مؤثر اعتبارسنجی به مقاومت (Resilience) و قابلیت اطمینان (Reliability) بالاتر برنامه منجر میشود.
با اعمال قوانین اعتبارسنجی میتوانید:
کیفیت دادهها را در سطح بالا حفظ کنید 🧾
و تجربهٔ کاربری بهتری ارائه دهید 💡
💻 نمونهکد Pipeline Behavior
internal sealed class ValidationPipelineBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : class
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
ValidationFailure[] validationFailures = await ValidateAsync(request);
if (validationFailures.Length != 0)
{
throw new ValidationException(validationFailures);
}
return await next();
}
private async Task<ValidationFailure[]> ValidateAsync(TRequest request)
{
if (!validators.Any())
{
return [];
}
var context = new ValidationContext<TRequest>(request);
ValidationResult[] validationResults = await Task.WhenAll(
validators.Select(validator => validator.ValidateAsync(context)));
ValidationFailure[] validationFailures = validationResults
.Where(validationResult => !validationResult.IsValid)
.SelectMany(validationResult => validationResult.Errors)
.ToArray();
return validationFailures;
}
}
⚡️ Cross-Cutting Concern شماره ۳ — کشینگ (Caching)
کشینگ یکی از دغدغههای کلیدی در توسعه نرمافزار است که هدف اصلی آن افزایش کارایی (Performance) و مقیاسپذیری (Scalability) سیستم میباشد.
کشینگ بهطور خلاصه یعنی ذخیره موقت دادهها در یک لایه با دسترسی سریعتر تا نیاز به واکشی یا محاسبهی مکرر همان اطلاعات کاهش یابد.
🧠 الگوی Cache Aside
رفتار Pipeline مربوط به کشینگ که در پایین مشاهده میکنی، الگوی Cache Aside را پیادهسازی میکند.
در این الگو، ابتدا کش بررسی میشود تا ببینیم آیا داده از قبل موجود است یا خیر.
در صورتی که داده وجود نداشته باشد، درخواست پردازش شده و نتیجهی جدید در کش ذخیره میشود.
این الگو به دلیل سادگی و کارایی بالا، یکی از محبوبترین استراتژیهای کشینگ به شمار میرود.
📺 (اگر بخواهی نحوهی پیادهسازی آن را ببینی، در مقالهی اصلی نویسنده ویدیو آموزشیاش را قرار داده است.)
⚙️ نکات کلیدی در پیادهسازی کشینگ
در هنگام طراحی و پیادهسازی کشینگ، باید به سه عامل مهم توجه کنی:
چه چیزی را کش کنیم؟
دادههایی را انتخاب کن که واکشی یا محاسبهٔ آنها پرهزینه است و از ثبات کافی برای ذخیره موقت برخوردارند.
ابطال کش (Cache Invalidations)
تصمیم بگیر چه زمانی و چگونه دادههای کششده باید از بین بروند یا بهروزرسانی شوند.
پیکربندی کش (Cache Configuration)
پارامترهایی مثل زمان انقضا (Expiration) و اندازهٔ حافظه را بهدرستی تنظیم کن.
✨ اجرای مؤثر کشینگ باعث بهبود چشمگیر زمان پاسخدهی و کاهش بار روی سیستم میشود —
در نتیجه، به استراتژیای حیاتی برای ساخت برنامههای مقیاسپذیر در NET. تبدیل میگردد.
💻 نمونهکد: Query Caching Pipeline Behavior
internal sealed class QueryCachingPipelineBehavior<TRequest, TResponse>(
ICacheService cacheService,
ILogger<QueryCachingPipelineBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ICachedQuery
where TResponse : Result
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
TResponse? cachedResult = await cacheService.GetAsync<TResponse>(
request.CacheKey,
cancellationToken);
string requestName = typeof(TRequest).Name;
if (cachedResult is not null)
{
logger.LogInformation("Cache hit for {RequestName}", requestName);
return cachedResult;
}
logger.LogInformation("Cache miss for {RequestName}", requestName);
TResponse result = await next();
if (result.IsSuccess)
{
await cacheService.SetAsync(
request.CacheKey,
result,
request.Expiration,
cancellationToken);
}
return result;
}
}
با بهکارگیری تکنیکهای جداسازی وابستگیها (Decoupling) که در این مجموعه مرور کردیم، میتونی مطمئن باشی پروژههای NET. تو قابل نگهداری، توسعهپذیر و پایدار خواهند بود.
هر قدمی که برای بهبود مدیریت این دغدغههای مشترک برمیداری،
در واقع گامی است بهسوی معماری نرمافزاری بهتر 🧱
اگر علاقهمند به راهنمای گامبهگام و عمیق در این زمینه هستی، مطالعهٔ کتاب
📗 Pragmatic Clean Architecture
رو از دست نده.
به یاد داشته باش 💬
زیبایی توسعهٔ نرمافزار در تکامل مداوم و جستوجوی بیپایان برای بهبود نهفته است.
👋 این هم از این بخش
امیدوارم برات مفید بوده باشه.
🔖 هشتگها:
#CrossCuttingConcerns
#SoftwareArchitecture
#Scalability #DotNetCore
💡اگر منتظر شنبهای، یعنی هنوز جدی نیستی.
شنبه یه bug توی سیستم ذهنته، fixش کن و run بزن.
شنبه یه bug توی سیستم ذهنته، fixش کن و run بزن.
سوال مصاحبهای که نحوه تفکر من درباره طراحی سیستم را تغییر داد 📝💡
<Milan Jovanović>
حدود شش یا هفت سال پیش، در یک مصاحبه برای یک موقعیت سطح میانی NET. شرکت کردم.
💡یکی از سوالها از آن زمان تا الان با من مانده است:
یک کاربر روی یک دکمه در UI کلیک میکند تا یک گزارش Excel یا PDF تولید شود. تولید گزارش حدود پنج دقیقه طول میکشد (زمان میتواند دلخواه باشد). کاربر باید منتظر بماند تا فرایند کامل شود. شما چگونه این جریان را بهینه میکنید؟
در آن زمان، تمرکز من روی چیزی بود که بیشترین تسلط را داشتم: performance ⏱️. شروع به فکر کردن درباره بهینهسازی تولید گزارش کردم. شاید بتوانم SQL queries را بهینه کنم، تبدیل دادهها را کاهش دهم یا بخشهایی از نتیجه را cache کنم. اگر میتوانستم فرایند را از پنج دقیقه به یک دقیقه برسانم، احساس موفقیت بزرگی میکردم.
اما حتی اگر پنج برابر سریعتر هم میشد، کاربر همچنان مجبور بود منتظر بماند. اگر browser کرش میکرد، همه چیز از دست میرفت. اگر شبکه قطع میشد، فرایند متوقف میشد. اگر تب را میبستند، تمام پیشرفت از بین میرفت.
در واقع این یک مشکل performance نبود، بلکه یک مشکل design بود. 🛠
چیزی که آن زمان از دست دادم ⚠️
با نگاه به گذشته، متوجه میشوم که ذهنم درگیر این بود که "کد را سریعتر کنم". نه که اشکالی داشته باشد، بهینهسازی عملکرد یک مهارت ارزشمند است. آنچه که آن موقع به وضوح نمیدیدم، مشکل بزرگتر بود. برنامه همه این کارها را synchronously انجام میداد و کاربر را تا پایان فرایند در حالت انتظار نگه میداشت. نهایتاً با چند راهنمایی از مصاحبهکننده متوجه شدم.
سوال بهتر این نبود که "چگونه میتوانم سریعتر کنم؟"
سوال واقعی این بود که: "چرا کاربر در وهله اول منتظر است؟" 🤔
اگر چیزی دقایق (یا حتی ساعتها و روزها) طول میکشد، نباید کاربر را بلاک کند. این کار باید در background و خارج از جریان اصلی درخواست انجام شود، در حالی که کاربر به کارهای خود ادامه میدهد.
نکته مهم 🔑
باز هم فراموش نکنید که کد را بهینه کنید. Database queries، پردازش دادهها و تولید فایل همگی اهمیت دارند. شاید indexی جا افتاده باشد، حلقهای ناکارآمد باشد یا کتابخانه بهتری برای ایجاد Excel وجود داشته باشد. اما این بهینهسازیها فقط بخشی از راهحل هستند، نه کل آن.
چگونه امروز این مشکل را حل میکنم ⚡️🖥
امروز، همچنان با همان دکمه UI شروع میکنم. کاربر روی «Generate Report» کلیک میکند، اما به جای انتظار، backend درخواست را میپذیرد، آن را جایی ذخیره میکند (مثلاً به عنوان یک رکورد job در دیتابیس) و بلافاصله پاسخ میدهد. این جوهرهی ساخت asynchronous APIs است. سپس job توسط یک background worker پردازش میشود.
این worker میتواند یک hosted service، یک Quartz job، یا حتی یک AWS Lambda Function فعالشده توسط پیام صف (queue message) باشد. این وظیفه را انجام میدهد: دادهها را جمعآوری میکند، فایل را میسازد و آن را در فضایی مثل S3 یا Azure Blob آپلود میکند. 📂☁️
زمانی که گزارش آماده شد، worker وضعیت job را به «completed» بهروزرسانی میکند و به کاربر اطلاع میدهد. این میتواند یک ایمیل با لینک دانلود یا پیام real-time SignalR باشد که در برنامه نمایش داده میشود. لینک به فایل ذخیرهشده اشاره دارد و از طریق backend به صورت امن ارائه میشود.
مزیت این روش 🌟
اکنون کاربر منتظر یک HTTP request طولانی نیست. سرور connections را برای دقیقهها باز نگه نمیدارد. اگر چیزی خراب شد، میتوان آن را به صورت خودکار retry کرد. همچنین امکان ردیابی پیشرفت یا لغو job وجود دارد. اگر صد کاربر به طور همزمان درخواست گزارش بدهند، سیستم بدون قفل شدن scale میشود.
تجربه برای کاربر سریعتر به نظر میرسد، حتی اگر زمان واقعی تولید گزارش تغییر نکرده باشد. زیرا در نهایت، کاربران به performance metrics اهمیت نمیدهند، بلکه responsiveness برایشان مهم است. ⚡️
چرا هنوز از این سوال استفاده میکنم 🧐
چند سال بعد، شروع به استفاده از همین سوال در مصاحبه با توسعهدهندگان دیگر کردم. نه برای فریب کسی، بلکه چون نشان میدهد افراد چگونه فکر میکنند.
برخی کاندیداها مستقیم به بهینهسازی کد و queries میروند، دقیقاً مثل من در گذشته. این نشان میدهد که آنها با performance tuning آشنا هستند. سپس میتوانم به سوالات فنی پیشرفته درباره algorithms، data structures یا database optimization بپردازم.
برخی لحظهای مکث میکنند و درباره تجربه کاربر، پردازش background و fault tolerance فکر میکنند. اینجاست که گفتگوی واقعی شروع میشود: queues، retries، اطلاعرسانی، اشتراک امن فایل و غیره. این سناریو میتواند به بحثهای گستردهتری درباره system design منجر شود.
هیچ پاسخ واحدی درست نیست. اما تفاوت بزرگی بین کسی که فقط روی کد تمرکز میکند و کسی که میتواند یک سیستم scalable طراحی کند وجود دارد.
درس مهم 🎓
وقتی اولین بار این سوال را شنیدم، به فکر سریعتر کردن کد بودم. اکنون به فکر بهتر کردن تجربه کاربر هستم.
بهینهسازی یک query یا حلقه میتواند کمک کند، اما مشکل انتظار، خطاها یا مقیاسپذیری را حل نمیکند. اگر کاربران زیادی همزمان همان گزارش را شروع کنند، طراحی synchronous به سرعت شکست میخورد. جریان asynchronous سیستم را پاسخگو و مقاوم نگه میدارد، بدون توجه به بار کاری.
این تغییر از بهینهسازی توابع به طراحی سیستمهای scalable تفاوت بین یک توسعهدهنده خوب و یک توسعهدهنده عالی است. 🏆
امیدوارم مفید بوده باشد.🤍
🧩 چالش برنامهنویسی امروز — خروجی چی میشه؟
What will be the output?
A) C
B
B
B) B
B
B
C) A
B
B
C) B
B
D
<Link>
🌐 مدل TCP/IP
مدل TCP/IP یک چارچوب (framework) است که برای مدلسازی ارتباطات در شبکه استفاده میشود. این مدل در اصل مجموعهای از پروتکلهای شبکه است که در لایههای مختلف سازماندهی شدهاند تا نحوهی ارتباطات در شبکه را مدل کنند.
این مدل شامل چهار لایه است:
🔹 Application
🔹 Transport
🔹 Network/Internet
🔹 Network Access
در حالی که مدل OSI دارای هفت لایه است، مدل چهار لایهای TCP/IP سادهتر بوده و امروزه در اینترنت و سیستمهای شبکهای بهطور گسترده استفاده میشود.
🎯 نقش TCP/IP
یکی از اهداف اصلی TCP/IP این است که اطمینان حاصل کند دادهای که از سمت فرستنده ارسال میشود، بهصورت صحیح و کامل به گیرنده میرسد.
برای انجام این کار، داده قبل از ارسال به بخشهای کوچکتری به نام Packet تقسیم میشود. هر Packet بهصورت جداگانه در شبکه سفر میکند و زمانی که به مقصد رسید، دوباره به ترتیب درست بازسازی (Reassemble) میشود.
🧩 نکته: این فرآیند باعث میشود احتمال خطا کاهش یابد و پیام نهایی بهطور کامل و دقیق به مقصد برسد.
🧱 لایههای مدل TCP/IP
1️⃣ Application Layer
بالاترین لایه در مدل TCP/IP است و نزدیکترین لایه به کاربر محسوب میشود.
در این لایه، تمام برنامههایی که ما استفاده میکنیم — مثل مرورگر وب، کلاینت ایمیل یا ابزارهای اشتراک فایل — به شبکه متصل میشوند.
این لایه مثل یک پل ارتباطی بین نرمافزارهای کاربر (مثل Chrome، Gmail یا WhatsApp) و لایههای پایینتر شبکه عمل میکند که واقعاً دادهها را ارسال و دریافت میکنند.
این لایه از پروتکلهای مختلفی پشتیبانی میکند مانند:
HTTP برای وبسایتها 🌍
FTP برای انتقال فایلها 📁
SMTP برای ارسال ایمیلها ✉️
DNS برای یافتن آدرس وبسایتها 🌐
همچنین وظایفی مانند قالببندی دادهها (Data Formatting)، رمزنگاری (Encryption) برای حفظ امنیت دادهها، و مدیریت نشستها (Session Management) برای کنترل ارتباطات فعال را بر عهده دارد.
⚙️ 2️⃣ Transport Layer
مسئول اطمینان از ارسال مطمئن و مرتب دادهها بین دستگاههاست. این لایه بررسی میکند که دادههایی مانند پیامها، فایلها یا ویدیوها که ارسال میشوند، بهصورت کامل و بدون خطا به مقصد برسند.
این لایه از دو پروتکل اصلی استفاده میکند: TCP و UDP — بسته به اینکه ارتباط نیاز به قابلیت اطمینان (Reliability) داشته باشد یا سرعت.
🔹️TCP (Transmission Control Protocol)
زمانی استفاده میشود که داده باید دقیق و کامل باشد؛ مانند زمانی که در حال بارگذاری یک صفحهی وب یا دانلود یک فایل هستیم.
خطاها را بررسی میکند، بخشهای گمشده را دوباره ارسال میکند و ترتیب دادهها را حفظ مینماید.
🔹️UDP (User Datagram Protocol)
سریعتر است اما تحویل داده را تضمین نمیکند.
این ویژگی باعث میشود برای مواردی مثل پخش زندهی ویدیو یا بازیهای آنلاین که سرعت مهمتر از دقت صددرصدی است، مناسب باشد.
🌍 3️⃣ Internet Layer
وظیفه دارد بهترین مسیر را برای انتقال دادهها در میان شبکههای مختلف پیدا کند تا داده بتواند به مقصد درست برسد.
این لایه مانند یک کنترلکنندهی ترافیک شبکه عمل میکند و کمک میکند Packetها از یک شبکه به شبکهی دیگر حرکت کنند تا به دستگاه مورد نظر برسند.
این لایه از Internet Protocol (IP) برای اختصاص آدرس منحصربهفرد (IP Address) به هر دستگاه استفاده میکند تا مشخص شود داده باید به کجا ارسال شود.
وظیفهی اصلی این لایه مسیریابی (Routing) است؛ یعنی تصمیمگیری دربارهی بهترین مسیر برای حرکت دادهها.
همچنین کارهایی مانند Packet Forwarding (انتقال داده از یک نقطه به نقطهی دیگر)، Fragmentation (شکستن دادههای بزرگ به بخشهای کوچکتر) و Addressing را نیز انجام میدهد.
📡 4️⃣ Network Access Layer
پایینترین لایه در مدل TCP/IP است. این لایه با اتصال فیزیکی واقعی بین دستگاههای موجود در یک شبکهی محلی (LAN) سر و کار دارد — مثل کامپیوترهایی که از طریق کابل یا Wi-Fi به هم متصل هستند.
این لایه اطمینان حاصل میکند که داده بتواند از طریق سختافزارهایی مانند سیمها، سوئیچها یا سیگنالهای بیسیم منتقل شود.
همچنین وظایف مهمی بر عهده دارد مانند:
• استفاده از MAC Address برای شناسایی دستگاهها 🖥
• ایجاد Frameها (قالب داده برای انتقال در لینک فیزیکی)
• بررسی خطاهای ابتدایی در هنگام ارسال داده
⚙️ نحوهٔ کار مدل TCP/IP
📤 زمانی که داده ارسال میشود (از Sender به Receiver)
Application Layer 🧩:
دادههای کاربر را با استفاده از پروتکلهایی مانند HTTP، FTP یا SMTP آماده میکند.
Transport Layer (TCP/UDP) 🚦:
داده را به بخشهای کوچکتر (Segments) تقسیم کرده و اطمینان حاصل میکند که انتقال یا قابل اعتماد (TCP) باشد یا سریعتر (UDP).
Internet Layer (IP) 🌍:
آدرسهای IP را اضافه میکند و بهترین مسیر را برای هر Packet انتخاب میکند.
Link Layer (Network Access Layer) 🔗:
Packet
ها را به Frame تبدیل کرده و از طریق شبکهی فیزیکی ارسال میکند.
📥 زمانی که داده دریافت میشود (در مقصد)
Link Layer ⚡️:
بیتها را از شبکه دریافت کرده و Frameها را بازسازی میکند تا به لایهی بعدی منتقل شوند.
Internet Layer 🌐:
آدرس IP را بررسی کرده، IP Header را حذف میکند و داده را به لایهی Transport میفرستد.
Transport Layer 📦:
Segments
را دوباره ترکیب میکند، خطاها را بررسی کرده و از کامل بودن داده مطمئن میشود.
Application Layer 🖥:
دادهی نهایی را به اپلیکیشن مناسب تحویل میدهد (برای مثال، نمایش یک صفحه وب در مرورگر).
💡 مقایسه TCP/IP و OSI — چرا TCP/IP پرکاربردتر است؟
مدل TCP/IP نسبت به OSI سادهتر، عملیتر و برای شبکههای واقعی و اینترنت بسیار رایجتر است.
🧠 دلیل | 💬 توضیح
━━━━━━━━━━━━━━━
⚙️ ساختار سادهتر (Simpler Structure)
🔹 فقط ۴ لایه دارد (در مقابل ۷ لایه در OSI)، بنابراین پیادهسازی و درک آن در سیستمهای واقعی بسیار سادهتر است.
🧩 طراحی مبتنی بر پروتکل (Protocol-Driven Design)
🔹 مدل TCP/IP بر اساس پروتکلهای واقعی ساخته شده است، در حالی که مدل OSI بیشتر یک چارچوب نظری و آموزشی است.
🚀 انعطافپذیری و مقاومت بالا (Flexibility & Robustness)
🔹 بهخوبی با سختافزارها و شبکههای مختلف سازگار میشود و قابلیتهایی مانند Error Handling, Routing و Congestion Control را پشتیبانی میکند.
🌍 استاندارد باز (Open Standard)
🔹 یک استاندارد آزاد و عمومی است که توسط هیچ سازمان خاصی کنترل نمیشود و به همین دلیل در سراسر جهان پذیرفته شده است.
🧱 کاربرد واقعی در مقابل مدل مفهومی (Actual Use vs Conceptual Model)
🔹 مدل OSI برای آموزش مفید است، اما در دنیای واقعی، این TCP/IP است که واقعاً در شبکهها بهکار میرود.