آموزش ASP.NET Core؛ راهنمای جامع از ساختار پروژه تا معماری حرفه‌ای

ASP.NET Core قلب توسعه وب مدرن در دات‌نت است. در این راهنما، به‌جای مرور سطحی، وارد جزئیاتی می‌شویم که تفاوت یک برنامه‌نویس مبتدی و یک توسعه‌دهنده حرفه‌ای را می‌سازند: خط لوله میان‌افزار، تزریق وابستگی، پیکربندی محیط‌ها، مسیریابی، اعتبارسنجی، دسترسی به داده، احراز هویت و مدیریت خطا.

ASP.NET Core دقیقاً چیست؟

ASP.NET Core یک چارچوب متن‌باز، چندسکویی و پرکارایی برای ساخت برنامه‌های تحت وب، وب‌سرویس‌ها و برنامه‌های بلادرنگ است. برخلاف نسل قدیمی که یک بسته یکپارچه و وابسته به ویندوز بود، این نسل از ماژول‌های مستقل ساخته شده و شما فقط قطعاتی را که واقعاً لازم دارید فعال می‌کنید. همین ماژولار بودن، هم مصرف حافظه را پایین می‌آورد و هم زمان راه‌اندازی برنامه را کوتاه می‌کند.

ذهنیتی که باید از همان ابتدا بسازید این است: یک برنامه ASP.NET Core در واقع یک برنامه کنسول است که یک وب‌سرور را میزبانی می‌کند. نقطه شروع آن همان متد Main کلاسیک است که امروز به شکل فایل Program.cs با سینتکس ساده‌شده نوشته می‌شود. این وب‌سرور داخلی Kestrel نام دارد، درخواست‌های شبکه را می‌گیرد و آن‌ها را وارد یک زنجیره پردازشی می‌کند که به آن «خط لوله» می‌گوییم.

اگر هنوز تفاوت این نسل با ASP.NET قدیمی برایتان روشن نیست، پیشنهاد می‌کنیم ابتدا بخش مربوطه را در صفحه آموزش ASP.NET بخوانید و سپس به این صفحه برگردید.

نصب، ساخت پروژه و ساختار پوشه‌ها

برای شروع فقط به دو چیز نیاز دارید: .NET SDK و یک ویرایشگر. اگر ویندوز دارید، Visual Studio تجربه کامل‌تری می‌دهد؛ روی لینوکس و مک، VS Code با افزونه C# Dev Kit انتخاب استانداردی است. پس از نصب، این دستور باید نسخه نصب‌شده را نشان دهد:

dotnet --version
dotnet --list-sdks

ساخت پروژه‌ها با قالب‌های آماده انجام می‌شود:

dotnet new mvc      -o Shop.Web       # پروژه MVC
dotnet new webapi   -o Shop.Api       # وب‌سرویس
dotnet new razor    -o Shop.Pages     # Razor Pages
dotnet new blazor   -o Shop.Client    # Blazor
dotnet new sln      -n Shop           # فایل راه‌حل
dotnet sln add Shop.Web/Shop.Web.csproj

ساختار پوشه‌ها و نقش هر بخش

  • Program.cs — نقطه ورود برنامه؛ ثبت سرویس‌ها و ساخت خط لوله.
  • appsettings.json — پیکربندی برنامه: رشته اتصال، سطح لاگ، کلیدها.
  • Controllers/ — کنترلرها؛ نقطه دریافت درخواست و بازگرداندن پاسخ.
  • Models/ — کلاس‌های داده و مدل‌های نمایشی (ViewModel).
  • Views/ — قالب‌های Razor با پسوند .cshtml.
  • wwwroot/ — فایل‌های ایستا: CSS، جاوااسکریپت، تصویر و فونت.
  • Properties/launchSettings.json — تنظیمات اجرای محلی و پورت‌ها.
  • *.csproj — فایل پروژه: نسخه هدف و بسته‌های NuGet.
نکته مهم: هر چیزی که داخل wwwroot باشد از طریق مرورگر قابل دسترسی عمومی است. هرگز فایل حساس، پشتیبان پایگاه داده یا فایل پیکربندی را آنجا نگذارید.

خط لوله میان‌افزار؛ مهم‌ترین مفهومی که باید بفهمید

هر درخواستی که وارد برنامه می‌شود، از یک زنجیره از اجزای کوچک عبور می‌کند که هرکدام «میان‌افزار» نام دارند. هر میان‌افزار می‌تواند سه کار انجام دهد: چیزی را پیش از ادامه مسیر انجام دهد، درخواست را به میان‌افزار بعدی بسپارد، و پس از بازگشت پاسخ دوباره دخالت کند. اگر میان‌افزاری تصمیم بگیرد درخواست را جلوتر نفرستد، زنجیره همان‌جا قطع می‌شود و پاسخ برمی‌گردد؛ به این کار «مدار کوتاه» می‌گویند.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

این ترتیب تصادفی نیست و جابه‌جا کردن خطوط، رفتار برنامه را عوض می‌کند. چند نمونه از خطاهایی که مستقیماً از ترتیب اشتباه ناشی می‌شوند:

  • اگر UseAuthentication بعد از UseAuthorization بیاید، کاربر هرگز شناسایی نمی‌شود و همه‌چیز غیرمجاز به نظر می‌رسد.
  • اگر UseStaticFiles بعد از احراز هویت بیاید، هر فایل CSS هم وارد فرآیند احراز هویت می‌شود و بی‌دلیل کند می‌شود.
  • اگر UseRouting پیش از میان‌افزارهای وابسته به مسیر نیاید، اطلاعات نقطه پایانی در دسترس نخواهد بود.

نوشتن میان‌افزار سفارشی

ساده‌ترین شکل، یک میان‌افزار درجا برای اندازه‌گیری زمان پاسخ:

app.Use(async (context, next) =>
{
    var sw = System.Diagnostics.Stopwatch.StartNew();
    await next();               // ادامه زنجیره
    sw.Stop();

    context.Response.Headers["X-Response-Time-ms"] =
        sw.ElapsedMilliseconds.ToString();
});

برای منطق پیچیده‌تر، بهتر است میان‌افزار را در یک کلاس مستقل بنویسید:

public class RequestLogMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLogMiddleware> _logger;

    public RequestLogMiddleware(RequestDelegate next, ILogger<RequestLogMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        _logger.LogInformation("درخواست {Method} {Path}",
            context.Request.Method, context.Request.Path);

        await _next(context);

        _logger.LogInformation("پاسخ {Status}", context.Response.StatusCode);
    }
}

// ثبت در خط لوله
app.UseMiddleware<RequestLogMiddleware>();

تزریق وابستگی و طول عمر سرویس‌ها

تزریق وابستگی (DI) در ASP.NET Core یک افزونه اختیاری نیست؛ ستون فقرات فریم‌ورک است. ایده ساده است: کلاس‌ها وابستگی‌های خود را نمی‌سازند، بلکه آن‌ها را از بیرون و از طریق سازنده دریافت می‌کنند. نتیجه این است که کد شما قابل تست، قابل جایگزینی و قابل نگهداری می‌شود.

public interface IPriceCalculator
{
    decimal CalculateFinalPrice(decimal basePrice, int quantity);
}

public class PriceCalculator : IPriceCalculator
{
    public decimal CalculateFinalPrice(decimal basePrice, int quantity)
    {
        var total = basePrice * quantity;
        return quantity >= 10 ? total * 0.9m : total;   // تخفیف عمده
    }
}

// ثبت سرویس
builder.Services.AddScoped<IPriceCalculator, PriceCalculator>();

// استفاده در کنترلر
public class OrderController : Controller
{
    private readonly IPriceCalculator _calculator;

    public OrderController(IPriceCalculator calculator) => _calculator = calculator;

    public IActionResult Total(decimal price, int qty)
        => Ok(_calculator.CalculateFinalPrice(price, qty));
}

سه طول عمر و تفاوت واقعی آن‌ها

طول عمررفتارکاربرد مناسب
Transientهر بار درخواست سرویس، نمونه تازه ساخته می‌شودسرویس‌های سبک و بدون حالت
Scopedیک نمونه برای هر درخواست HTTPDbContext، سرویس‌های تجاری، واحد کار
Singletonیک نمونه برای کل عمر برنامهکش درون‌حافظه‌ای، تنظیمات ثابت
خطای پرتکرار: تزریق یک سرویس Scoped (مثل DbContext) داخل یک سرویس Singleton. نتیجه‌اش یک شیء منقضی‌شده است که در طول عمر برنامه زنده می‌ماند و خطاهای عجیب و غیرقابل بازتولید تولید می‌کند. اگر واقعاً به این کار نیاز دارید، از IServiceScopeFactory استفاده کنید.

پیکربندی، محیط‌ها و مدیریت اسرار

سامانه پیکربندی ASP.NET Core لایه‌ای است: مقادیر از چند منبع خوانده می‌شوند و منابع بعدی مقادیر قبلی را بازنویسی می‌کنند. ترتیب معمول از پایین به بالا چنین است: appsettings.json، سپس appsettings.{Environment}.json، سپس User Secrets در محیط توسعه، سپس متغیرهای محیطی و در نهایت آرگومان‌های خط فرمان.

{
  "ConnectionStrings": {
    "Default": "Server=.;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True"
  },
  "SiteSettings": {
    "Title": "فروشگاه نمونه",
    "PageSize": 20
  },
  "Logging": {
    "LogLevel": { "Default": "Information" }
  }
}

بهترین روش خواندن تنظیمات، الگوی Options است — یعنی نگاشت بخشی از فایل پیکربندی به یک کلاس:

public class SiteSettings
{
    public string Title { get; set; } = "";
    public int PageSize { get; set; } = 10;
}

builder.Services.Configure<SiteSettings>(
    builder.Configuration.GetSection("SiteSettings"));

// استفاده
public class HomeController : Controller
{
    private readonly SiteSettings _settings;
    public HomeController(IOptions<SiteSettings> options) => _settings = options.Value;
}

درباره اسرار یک قانون بی‌استثنا وجود دارد: هیچ رمز، کلید یا رشته اتصال تولیدی نباید در مخزن Git قرار بگیرد. در محیط توسعه از dotnet user-secrets استفاده کنید و در محیط تولید از متغیرهای محیطی سرور. جزئیات تنظیم آن روی سرور را در صفحه هاست و انتشار پروژه آورده‌ایم.

مسیریابی و پیوند مدل

مسیریابی تعیین می‌کند کدام آدرس به کدام قطعه کد می‌رسد. دو سبک اصلی وجود دارد: مسیریابی قراردادی (که در MVC با الگوی {controller}/{action}/{id?} تعریف می‌شود) و مسیریابی مبتنی بر ویژگی که روی خود متد نوشته می‌شود و کنترل دقیق‌تری می‌دهد.

[Route("products")]
public class ProductController : Controller
{
    [HttpGet("")]                       // /products
    public IActionResult Index() => View();

    [HttpGet("{id:int:min(1)}")]        // /products/12
    public IActionResult Details(int id) => View(id);

    [HttpGet("category/{slug:alpha}")]  // /products/category/laptop
    public IActionResult ByCategory(string slug) => View(slug);
}

قیدهای مسیر (int، alpha، min، guid) نه‌تنها از خطا جلوگیری می‌کنند، بلکه بخشی از اعتبارسنجی ورودی را پیش از رسیدن به کد شما انجام می‌دهند. از نگاه سئو هم آدرس‌های خوانا و معنادار — مثل /products/category/laptop به‌جای /p?c=17 — ارزش بیشتری دارند و در نتایج جست‌وجو بهتر عمل می‌کنند.

پیوند مدل (Model Binding)

فریم‌ورک به‌طور خودکار داده‌ها را از مسیر، رشته پرس‌وجو، بدنه درخواست، هدرها و فرم استخراج و به پارامترهای متد شما تبدیل می‌کند. اگر می‌خواهید منبع را صریح تعیین کنید، از ویژگی‌هایی مثل [FromBody]، [FromQuery]، [FromRoute] و [FromForm] استفاده کنید. تعیین صریح منبع در وب‌سرویس‌ها یک عادت حرفه‌ای است چون ابهام را حذف می‌کند.

MVC و Razor Pages در عمل

الگوی MVC مسئولیت‌ها را به سه بخش تقسیم می‌کند: مدل داده و منطق را نگه می‌دارد، نما فقط نمایش می‌دهد و کنترلر میان این دو هماهنگی می‌کند. کلید نوشتن MVC خوب یک جمله است: کنترلر باید لاغر بماند.

public class ArticleController : Controller
{
    private readonly IArticleService _service;

    public ArticleController(IArticleService service) => _service = service;

    public async Task<IActionResult> Index(int page = 1)
    {
        var model = await _service.GetPagedAsync(page, pageSize: 20);
        return View(model);
    }

    public async Task<IActionResult> Details(int id)
    {
        var article = await _service.FindAsync(id);
        if (article is null) return NotFound();
        return View(article);
    }
}

و نمای متناظر با سینتکس Razor:

@model IEnumerable<Article>
@{ ViewData["Title"] = "فهرست مقالات"; }

<h1>فهرست مقالات</h1>

@foreach (var item in Model)
{
    <article>
        <h2><a asp-action="Details" asp-route-id="@item.Id">@item.Title</a></h2>
        <p>@item.Summary</p>
    </article>
}

تگ هلپرها

تگ هلپرها به شما اجازه می‌دهند به‌جای نوشتن آدرس‌های ثابت، از عناصر HTML آشنا با ویژگی‌های ویژه استفاده کنید. مزیت اصلی این است که اگر مسیریابی تغییر کند، آدرس‌ها خودبه‌خود به‌روز می‌شوند و لینک شکسته نمی‌سازید — که هم برای کاربر و هم برای موتور جست‌وجو اهمیت دارد.

Razor Pages چه فرقی دارد؟

در Razor Pages هر صفحه یک فایل .cshtml و یک کلاس همراه .cshtml.cs دارد. متدهای OnGet و OnPost جای اکشن‌های کنترلر را می‌گیرند. برای سایت‌های صفحه‌محور، این ساختار خواناتر است چون همه‌چیزِ مربوط به یک صفحه کنار هم قرار می‌گیرد، نه پخش‌شده در سه پوشه مختلف.

Web API و Minimal API

وب‌سرویس‌ها امروز ستون ارتباط بین بک‌اند و هر مصرف‌کننده دیگری هستند: اپ موبایل، فرانت‌اند تک‌صفحه‌ای یا سرویس‌های داخلی. کنترلرهای API با ویژگی [ApiController] رفتار هوشمندتری دارند؛ برای مثال اعتبارسنجی مدل به‌صورت خودکار انجام می‌شود و در صورت خطا پاسخ استاندارد ۴۰۰ برمی‌گردد.

[ApiController]
[Route("api/products")]
public class ProductsApiController : ControllerBase
{
    private readonly AppDbContext _db;
    public ProductsApiController(AppDbContext db) => _db = db;

    [HttpGet]
    public async Task<ActionResult<IEnumerable<Product>>> GetAll(
        [FromQuery] int page = 1, [FromQuery] int size = 20)
    {
        var items = await _db.Products
            .AsNoTracking()
            .OrderBy(p => p.Id)
            .Skip((page - 1) * size)
            .Take(size)
            .ToListAsync();

        return Ok(items);
    }

    [HttpPost]
    public async Task<IActionResult> Create(ProductDto dto)
    {
        var product = new Product { Name = dto.Name, Price = dto.Price };
        _db.Products.Add(product);
        await _db.SaveChangesAsync();

        return CreatedAtAction(nameof(GetAll), new { id = product.Id }, product);
    }
}

Minimal API

اگر سرویس شما کوچک است، نیازی به کنترلر ندارید. Minimal API همان قابلیت‌ها را با کد بسیار کمتر ارائه می‌دهد و برای میکروسرویس‌ها و توابع سبک ایده‌آل است:

var group = app.MapGroup("/api/tags");

group.MapGet("/", async (AppDbContext db) =>
    await db.Tags.AsNoTracking().ToListAsync());

group.MapGet("/{id:int}", async (int id, AppDbContext db) =>
    await db.Tags.FindAsync(id) is Tag t ? Results.Ok(t) : Results.NotFound());

group.MapDelete("/{id:int}", async (int id, AppDbContext db) =>
{
    var tag = await db.Tags.FindAsync(id);
    if (tag is null) return Results.NotFound();

    db.Tags.Remove(tag);
    await db.SaveChangesAsync();
    return Results.NoContent();
});

چند اصل طراحی API که حرفه‌ای بودن شما را نشان می‌دهد

  • از اسم جمع برای منابع استفاده کنید (/api/products) نه فعل (/api/getProducts).
  • کد وضعیت درست برگردانید: ۲۰۰ برای موفقیت، ۲۰۱ برای ساخت، ۲۰۴ برای حذف، ۴۰۴ برای نیافتن، ۴۲۲ برای خطای اعتبارسنجی.
  • هرگز موجودیت پایگاه داده را مستقیم برنگردانید؛ از DTO استفاده کنید تا فیلدهای حساس فاش نشوند.
  • خروجی فهرستی را همیشه صفحه‌بندی کنید.
  • نسخه‌بندی را از روز اول در نظر بگیرید (/api/v1/...).
  • مستندسازی خودکار با OpenAPI را فعال کنید تا مصرف‌کننده سرویس معطل نماند.

اعتبارسنجی مدل

هرگز به ورودی کاربر اعتماد نکنید. اعتبارسنجی سمت مرورگر فقط برای تجربه کاربری است و هر مهاجمی می‌تواند آن را دور بزند؛ اعتبارسنجی واقعی باید در سمت سرور انجام شود.

public class ProductDto
{
    [Required(ErrorMessage = "نام محصول الزامی است")]
    [StringLength(120, MinimumLength = 3,
        ErrorMessage = "نام باید بین ۳ تا ۱۲۰ نویسه باشد")]
    public string Name { get; set; } = "";

    [Range(1000, 1_000_000_000, ErrorMessage = "قیمت خارج از محدوده مجاز است")]
    public decimal Price { get; set; }

    [EmailAddress(ErrorMessage = "ایمیل معتبر نیست")]
    public string? ContactEmail { get; set; }
}

در کنترلرهای MVC، وضعیت اعتبارسنجی را با ModelState.IsValid بررسی می‌کنید. در کنترلرهای علامت‌گذاری‌شده با [ApiController] این بررسی خودکار است. برای قواعد پیچیده‌تر — مثلاً وابستگی بین چند فیلد — یا ویژگی سفارشی بنویسید یا از کتابخانه FluentValidation استفاده کنید که قواعد را در کلاسی جدا و قابل تست نگه می‌دارد.

دسترسی به داده با Entity Framework Core

EF Core لایه نگاشت شیء به رابطه در دات‌نت است. با آن، جدول‌ها به کلاس و رکوردها به شیء تبدیل می‌شوند و کوئری‌ها را با LINQ می‌نویسید. اما استفاده حرفه‌ای از آن چند نکته دارد که تفاوت یک برنامه سریع و یک برنامه کند را رقم می‌زند.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public Category? Category { get; set; }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> o) : base(o) { }

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();

    protected override void OnModelCreating(ModelBuilder b)
    {
        b.Entity<Product>(e =>
        {
            e.Property(p => p.Name).HasMaxLength(120).IsRequired();
            e.Property(p => p.Price).HasPrecision(18, 2);
            e.HasIndex(p => p.Name);

            e.HasOne(p => p.Category)
             .WithMany(c => c.Products)
             .HasForeignKey(p => p.CategoryId)
             .OnDelete(DeleteBehavior.Restrict);
        });
    }
}

مهاجرت‌ها

dotnet tool install --global dotnet-ef
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet ef migrations remove          # حذف آخرین مهاجرت اعمال‌نشده

پنج خطای رایج در EF Core

  1. مسئله N+1: دسترسی به داده مرتبط داخل حلقه، به‌ازای هر رکورد یک رفت‌وبرگشت به پایگاه داده می‌سازد. راه‌حل: Include یا Select هدفمند.
  2. فراموش کردن AsNoTracking: در کوئری‌های فقط‌خواندنی، ردیابی تغییرات هزینه اضافی است.
  3. کشیدن کل جدول: فراخوانی ToList() بدون فیلتر و صفحه‌بندی، روی جدول بزرگ فاجعه است.
  4. ارزیابی سمت کلاینت: استفاده از متدی که به SQL ترجمه نمی‌شود باعث می‌شود داده اضافی به حافظه بیاید.
  5. استفاده از یک DbContext در چند نخ: این کلاس ایمن برای دسترسی همزمان نیست؛ آن را Scoped نگه دارید.

احراز هویت و مجوزدهی

این دو مفهوم را نباید با هم اشتباه گرفت. احراز هویت یعنی «تو کی هستی؟» و مجوزدهی یعنی «اجازه انجام این کار را داری؟». ASP.NET Core برای هر دو راهکار داخلی دارد.

ASP.NET Core Identity

برای برنامه‌های وبی که خودشان کاربر ثبت می‌کنند، Identity کل چرخه را پوشش می‌دهد: ثبت‌نام، ورود، خروج، تأیید ایمیل، بازیابی رمز، قفل شدن حساب پس از تلاش‌های ناموفق و احراز هویت دو مرحله‌ای. رمزها با الگوریتم‌های استاندارد و نمک‌گذاری‌شده ذخیره می‌شوند؛ هرگز این بخش را دستی پیاده نکنید.

JWT برای وب‌سرویس‌ها

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

مجوزدهی مبتنی بر نقش و سیاست

[Authorize(Roles = "Admin")]
public IActionResult Dashboard() => View();

// سیاست سفارشی
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdultOnly", policy =>
        policy.RequireClaim("Age").RequireAssertion(ctx =>
            int.TryParse(ctx.User.FindFirst("Age")?.Value, out var age) && age >= 18));
});

[Authorize(Policy = "AdultOnly")]
public IActionResult Restricted() => View();

مدیریت خطا و لاگ‌برداری

در محیط توسعه، صفحه استثنای توسعه‌دهنده جزئیات کامل خطا را نشان می‌دهد. در محیط تولید هرگز نباید این کار انجام شود، چون جزئیات پشته و ساختار داخلی برنامه اطلاعات ارزشمندی به مهاجم می‌دهد. راه درست، یک کنترل‌کننده سراسری خطاست:

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseStatusCodePagesWithReExecute("/Home/StatusCode/{0}");
    app.UseHsts();
}

برای وب‌سرویس‌ها، پاسخ خطای استاندارد بر پایه ProblemDetails رویکرد درست است:

app.UseExceptionHandler(handler =>
{
    handler.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/problem+json";

        await context.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Title  = "خطای داخلی سرور",
            Status = 500,
            Detail = "درخواست پردازش نشد. لطفاً بعداً دوباره تلاش کنید."
        });
    });
});

برای لاگ‌برداری، ILogger<T> را در سازنده کلاس تزریق کنید. همیشه از لاگ ساختاریافته با پارامتر استفاده کنید (_logger.LogInformation("کاربر {UserId} وارد شد", id)) نه الحاق رشته؛ چون این شکل در ابزارهای تحلیل لاگ قابل جست‌وجو و فیلتر است. و مراقب باشید هرگز داده حساس مثل رمز، توکن یا شماره کارت را لاگ نکنید.

تست‌نویسی؛ تفاوت کد قابل اعتماد و کد امیدوارانه

یکی از بزرگ‌ترین مزیت‌های تزریق وابستگی این است که کد شما را قابل تست می‌کند. وقتی یک کنترلر به‌جای ساختن مستقیم DbContext، یک اینترفیس سرویس دریافت می‌کند، شما می‌توانید در تست به‌جای پایگاه داده واقعی یک پیاده‌سازی ساختگی بدهید و رفتار منطق را در چند میلی‌ثانیه بسنجید. در دات‌نت سه سطح تست رایج است.

تست واحد کوچک‌ترین قطعه منطق را جدا می‌سنجد؛ سریع است و باید بیشترین تعداد را داشته باشد. تست یکپارچگی چند جزء را با هم و معمولاً با یک پایگاه داده واقعی یا درون‌حافظه‌ای می‌آزماید و مطمئن می‌شود قطعات درست به هم وصل شده‌اند. تست سرتاسری کل برنامه را از دید کاربر بررسی می‌کند؛ ارزشمند اما کند و شکننده است، بنابراین تعدادش باید محدود بماند. تناسب درست میان این سه سطح، همان چیزی است که به آن «هرم تست» می‌گویند.

فریم‌ورک محبوب در دات‌نت xUnit است و برای ساختن اشیاء ساختگی معمولاً از کتابخانه‌هایی مثل Moq یا NSubstitute استفاده می‌شود. برای تست یکپارچگی هم WebApplicationFactory کل برنامه را در حافظه بالا می‌آورد تا بتوانید درخواست‌های واقعی HTTP بفرستید بدون آنکه سروری اجرا شود. نکته عملی مهم: تست‌ها را طوری بنویسید که به ترتیب اجرا وابسته نباشند و هر تست وضعیت خودش را بسازد؛ تست‌های وابسته به هم، اعتماد تیم به مجموعه تست را از بین می‌برند.

Blazor و SignalR؛ وقتی صفحه باید زنده باشد

دو فناوری در اکوسیستم ASP.NET Core وجود دارد که مرز میان سرور و مرورگر را جابه‌جا می‌کنند. Blazor به شما اجازه می‌دهد رابط کاربری تعاملی را با سی‌شارپ بنویسید، نه جاوااسکریپت. در حالت سمت سرور، منطق روی سرور اجرا می‌شود و تنها تفاوت‌های رابط کاربری از طریق یک اتصال دائمی به مرورگر فرستاده می‌شود؛ حجم دانلود اولیه کم است اما به اتصال پایدار نیاز دارد. در حالت WebAssembly، کد در خود مرورگر اجرا می‌شود و برنامه پس از بارگذاری اولیه مستقل از سرور کار می‌کند، اما بارگذاری نخست سنگین‌تر است.

SignalR ابزار ارتباط بلادرنگ دوطرفه است. به‌جای آنکه مرورگر مدام از سرور بپرسد «خبر جدیدی هست؟»، سرور هر وقت لازم شد خودش به کلاینت پیام می‌فرستد. کاربردهای رایج آن شامل چت، اعلان‌های زنده، داشبورد لحظه‌ای قیمت، نمایش پیشرفت عملیات طولانی و همکاری همزمان چند کاربر روی یک سند است. SignalR در پس‌زمینه ابتدا سراغ WebSockets می‌رود و اگر در دسترس نبود، به‌طور خودکار به روش‌های جایگزین برمی‌گردد؛ یعنی شما لازم نیست نگران سازگاری زیرساخت باشید.

توصیه ما به تازه‌کارها این است که این دو را در مرحله اول یادگیری کنار بگذارید. ابتدا MVC یا Razor Pages و Web API را کامل کنید؛ وقتی چرخه درخواست و پاسخ برایتان کاملاً جا افتاد، ورود به دنیای بلادرنگ بسیار ساده‌تر و بامعناتر خواهد بود.

معماری لایه‌ای و سازمان‌دهی کد

وقتی پروژه از چند فایل فراتر می‌رود، ساختار اهمیت پیدا می‌کند. رایج‌ترین تقسیم‌بندی عملی در پروژه‌های دات‌نت چهار لایه دارد:

Domain

موجودیت‌ها و قواعد کسب‌وکار. هیچ وابستگی به فریم‌ورک ندارد و خالص‌ترین لایه است.

Application

سرویس‌ها، سناریوهای کاربردی، DTOها و اینترفیس‌ها؛ منطق هماهنگ‌کننده اینجاست.

Infrastructure

پیاده‌سازی دسترسی به داده، ارسال ایمیل و پیامک، کش و سرویس‌های بیرونی.

Presentation

کنترلرها، نماها یا نقاط پایانی API؛ نازک‌ترین لایه که فقط ورودی و خروجی را مدیریت می‌کند.

قاعده طلایی این معماری: جهت وابستگی همیشه به سمت داخل است. لایه بیرونی به درونی وابسته می‌شود، نه برعکس. رعایت همین یک قاعده باعث می‌شود بتوانید پایگاه داده یا حتی کل لایه نمایش را عوض کنید بدون اینکه منطق کسب‌وکار دست بخورد.

البته زیاده‌روی هم آفت است. برای یک سایت کوچک با ده صفحه، ساختن چهار پروژه مجزا فقط سربار می‌سازد. با ساختاری ساده شروع کنید و وقتی پیچیدگی واقعی ظاهر شد، لایه اضافه کنید. معماری خوب آن است که متناسب با اندازه مسئله باشد، نه آنکه بیشترین تعداد پوشه را داشته باشد.

پرسش‌های متداول

ترتیب میان‌افزارها واقعاً مهم است؟

بله و یکی از رایج‌ترین منابع باگ در پروژه‌های تازه‌کار همین است. میان‌افزارها دقیقاً به همان ترتیب ثبت اجرا می‌شوند. اگر احراز هویت بعد از مجوزدهی بیاید یا فایل‌های ایستا بعد از احراز هویت قرار بگیرند، رفتار برنامه اشتباه یا غیربهینه خواهد بود.

تفاوت Scoped و Transient و Singleton را ساده توضیح دهید.

Transient هر بار نمونه تازه می‌سازد، Scoped یک نمونه برای هر درخواست HTTP نگه می‌دارد و Singleton یک نمونه برای کل عمر برنامه. برای DbContext همیشه Scoped درست است.

Minimal API را انتخاب کنم یا کنترلر؟

برای سرویس‌های کوچک و میکروسرویس‌ها Minimal API کد کمتری می‌خواهد و سریع‌تر نوشته می‌شود. برای پروژه‌های بزرگ با ده‌ها نقطه پایانی، فیلترها و منطق مشترک، کنترلرها سازمان‌دهی بهتری می‌دهند.

آیا برای هر پروژه باید Clean Architecture پیاده کنم؟

خیر. معماری ابزار حل پیچیدگی است، نه نشانه حرفه‌ای بودن. برای پروژه کوچک، یک ساختار ساده و منظم بهتر از چهار لایه با ده اینترفیس بی‌استفاده است.

پس از این صفحه چه چیزی یاد بگیرم؟

اگر مبانی زبان را کامل نکرده‌اید، به آموزش سی‌شارپ برگردید. اگر پروژه‌تان آماده است، سراغ انتشار و هاست بروید و آن را روی سرور واقعی بالا بیاورید.

جمع‌بندی

ASP.NET Core مجموعه‌ای از قطعات مستقل اما هماهنگ است: خط لوله میان‌افزار مسیر درخواست را می‌سازد، تزریق وابستگی کد را قابل تست نگه می‌دارد، سامانه پیکربندی رفتار برنامه را در محیط‌های مختلف تنظیم می‌کند، مسیریابی آدرس‌ها را به کد وصل می‌کند و EF Core داده را مدیریت می‌کند. اگر هر کدام از این‌ها را به‌جای حفظ کردن، واقعاً بفهمید، خطایابی برایتان به یک فرآیند منطقی تبدیل می‌شود و می‌توانید هر مسئله تازه‌ای را خودتان حل کنید.

پیشنهاد عملی: یک پروژه کوچک بردارید — مثلاً سامانه مدیریت مقالات — و همه مفاهیم این صفحه را در آن پیاده کنید. مفهومی که در پروژه واقعی به کار برود، دیگر فراموش نمی‌شود.