🔬 Isolation Transaction در ASP.NET Core (آموزش عمیق)

Isolation transaction asp .net core همه سطوح
parsakarimidev.ir

🔬 Isolation Transaction در ASP.NET Core (آموزش عمیق)

Isolation transaction asp .net coreفریم‌ورک سطح: همه سطوح 1405/07/03

Isolation Transaction در ASP.NET Core

⚡ خلاصه سریع — اصل کاری‌ها

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

  • نکته ۱: Isolation Level یعنی سطح ایزولیشن مشخص می‌کنه تراکنش‌های هم‌زمان چقدر داده‌های یکدیگر رو ببینن.
  • نکته ۲: در EF Core با context.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted) یک تراکنش با سطح ایزولیشن مشخص شروع می‌شه.
  • نکته ۳: سطح پیش‌فرض SQL Server یعنی ReadCommitted فقط جلوی Dirty Read رو می‌گیره.
  • نکته ۴: سطح Serializable قوی‌ترین ایزولیشن است، ولی Locking و Deadlock بیشتری ایجاد می‌کنه.
  • نکته ۵: همیشه از await using برای تراکنش استفاده کن و CommitAsync یا RollbackAsync رو فراخوانی کن.
  • نکته ۶: Savepoint روی یک تراکنش امکان Rollback جزئی بدون از بین بردن کل تراکنش رو می‌ده.
  • نکته ۷: تراکنش رو تا جای ممکن کوتاه نگه دار؛ هیچ I/O خارجی یا عملیات طولانی داخل تراکنش انجام نده.
  • نکته ۸: برای چند DbContext یا چند دیتابیس، معمولاً باید از TransactionScope استفاده کنی.

۳ اشتباه رایج که باید ازشون پرهیز کنی:

  1. فراموش کردن CommitAsync یا RollbackAsync و رها کردن تراکنش باز.
  2. استفاده از Serializable برای همه تراکنش‌ها به‌طور پیش‌فرض.
  3. باز نگه داشتن تراکنش هنگام انتظار برای پاسخ یک API یا یک عملیات سنگین.

اگر فقط ۵ دقیقه وقت داری، این‌ها رو یاد بگیر:

  1. BeginTransactionAsync برای شروع تراکنش با سطح ایزولیشن مشخص.
  2. CommitAsync و RollbackAsync برای پایان تراکنش.
  3. انتخاب درست IsolationLevel بر اساس نوع Anomaly که باید جلوش رو بگیری.

۱. مقدمه

این موضوع چیه؟

وقتی چند کاربر به‌طور هم‌زمان روی یک دیتابیس کار می‌کنن، ممکنه یکی از اون‌ها در حال تغییر داده‌ها باشه و دیگری همون لحظه بخواد همون داده‌ها رو بخونه یا تغییر بده.
Isolation Transaction یعنی تعیین این‌که هر تراکنش چقدر از تغییرات تراکنش‌های دیگر رو ببینه و چه قفل‌هایی روی داده‌ها اعمال بشه.

در ASP.NET Core و وقتی با Entity Framework Core کار می‌کنی، می‌تونی برای هر تراکنش یک سطح ایزولیشن مشخص کنی. این سطح مستقیماً تعیین می‌کنه:

  • چه خطاهای هم‌زمانی مثل Dirty Read رخ بده یا نده.
  • چه قفل‌هایی روی ردیف‌ها یا محدوده‌ها اعمال بشه.
  • برنامه چقدر بتونه هم‌زمانی و کارایی بالایی داشته باشه.

چرا باید یادش بگیری؟

اگر سطح ایزولیشن رو درست انتخاب نکنی، یکی از این اتفاق‌ها می‌افته:

  • کاربر A موجودی حساب رو می‌خونه، کاربر B هم همون لحظه برداشت می‌کنه و A هنوز با مقدار قدیمی کار می‌کنه.
  • یک گزارش‌گیری دو بار پشت‌سرهم تعداد ردیف‌های مختلفی نشون می‌ده.
  • یک محصول با موجودی صفر به فروش می‌رسه چون دو خرید هم‌زمان هر دو فکر کردن موجودی کافیه.
  • دیتابیس به‌خاطر قفل‌های زیاد قفل می‌شه و Deadlock رخ می‌ده.

یادگیری این موضوع باعث می‌شه هم‌زمانی امن داشته باشی، یعنی نه داده غلط بخونی و نه دیتابیس از کار بیفته.

کجا استفاده می‌شه؟

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

۲. پیش‌نیازها

قبل از شروع، بهتره این‌ها رو بدونی:

  • زبان C#: آشنایی با کلاس‌ها، async/await
  • ASP.NET Core: ایجاد Web API یا MVC
  • Entity Framework Core: مفاهیم DbContext، مدل‌ها، SaveChanges
  • SQL و تراکنش‌ها: آشنایی اولیه با BEGIN TRANSACTION، COMMIT، ROLLBACK
  • مفاهیم ACID: بدون این‌که عمیق باشی، بدونی تراکنش باید Atomic باشه

ابزارهای لازم

  • .NET 8 یا .NET 9 SDK
  • Visual Studio یا VS Code
  • SQL Server یا PostgreSQL (مثال‌ها با SQL Server است)
  • پکیج‌های زیر:
    • Microsoft.EntityFrameworkCore
    • Microsoft.EntityFrameworkCore.SqlServer یا متناسب با دیتابیس
    • Microsoft.EntityFrameworkCore.Tools برای Migration

۳. نصب و راه‌اندازی

گام ۱: ایجاد پروژه

dotnet new webapi -n TransactionDemo
cd TransactionDemo

گام ۲: نصب پکیج‌ها

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

گام ۳: ساختن مدل و DbContext

فایل Entities/Account.cs:

namespace TransactionDemo.Entities;

public class Account
{
    public int Id { get; set; }
    public string Owner { get; set; } = string.Empty;
    public decimal Balance { get; set; }
}

فایل Data/AppDbContext.cs:

using Microsoft.EntityFrameworkCore;
using TransactionDemo.Entities;

namespace TransactionDemo.Data;

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

    public DbSet<Account> Accounts => Set<Account>();
}

گام ۴: ثبت DbContext در Program.cs

using Microsoft.EntityFrameworkCore;
using TransactionDemo.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

app.MapControllers();
app.Run();

و در appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=TransactionDemo;Trusted_Connection=True;TrustServerCertificate=True"
  }
}

گام ۵: اجرای Migration

dotnet ef migrations add Init
dotnet ef database update

حالا پروژه آماده استفاده از تراکنش‌هاست.

۴. مفاهیم پایه

تراکنش چیست؟

تراکنش یک واحد логиکی از کار هست که یا باید کاملاً اجرا بشه یا کاملاً برگرده.
به این خاصیت Atomicity می‌گن.

ACID چیست؟

  • Atomicity: همه عملیات یا موفق می‌شن یا هیچ‌کدوم.
  • Consistency: تراکنش داده‌ها رو از یک حالت معتبر به حالت معتبر دیگه می‌بره.
  • Isolation: تراکنش‌های هم‌زمان نباید همدیگه رو خراب کنن.
  • Durability: بعد از Commit، داده حتی با قطع برق باقی می‌مونه.

Isolation Level یعنی چه؟

هر تراکنش می‌تونه داده‌هایی رو که تراکنش‌های دیگر در حال تغییرش هستن ببینه یا نبینه.
به این «میزان دیدن» یا «درجه ایزولیشن» می‌گن.

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

۱. Dirty Read (خواندن کثیف)

تراکنش B قبل از Commit شدن تراکنش A، داده‌های A رو می‌خونه.
اگر A برگشت بخوره، B یک مقدار ناموجود رو خونده.

۲. Non-Repeatable Read (خواندن غیرقابل‌تکرار)

در یک تراکنش، یک رکورد دو بار خونده می‌شه.
بار اول مقدار X بود، بار دوم مقدار Y شده چون یک تراکنش دیگه همون ردیف رو تغییر داده و Commit کرده.

۳. Phantom Read (خواندن فانتوم)

یک Query در یک تراکنش دوبار اجرا می‌شه.
بار اول ۵ ردیف برمی‌گردونه، بار دوم ۶ ردیف می‌شه چون یک تراکنش دیگه یک ردیف جدید اضافه یا کم کرده.

سطح‌های Isolation در SQL Server

سطح Isolation Dirty Read Non-Repeatable Read Phantom Read قفل‌گذاری
ReadUncommitted بله بله بله خیلی کم
ReadCommitted خیر بله بله متوسط
RepeatableRead خیر خیر بله زیاد
Serializable خیر خیر خیر خیلی زیاد
Snapshot خیر خیر خیر Optimistic

شروع تراکنش با EF Core

using System.Data;
using Microsoft.EntityFrameworkCore;

await using var transaction = await context.Database.BeginTransactionAsync(
    IsolationLevel.ReadCommitted);

// ... عملیات روی context ...

await context.SaveChangesAsync();
await transaction.CommitAsync();

اگر خطایی رخ بده و CommitAsync صدا زده نشه، await using به‌صورت خودکار تراکنش را Rollback می‌کنه.

مقدار IsolationLevel از کجا میاد؟

از enum زیر:

using System.Data;

IsolationLevel.ReadUncommitted
IsolationLevel.ReadCommitted
IsolationLevel.RepeatableRead
IsolationLevel.Serializable
IsolationLevel.Snapshot

۵. مثال‌های کد ساده

مثال ۱: تراکنش ساده با ReadCommitted

سناریو: کم کردن موجودی یک محصول.

using System.Data;
using Microsoft.EntityFrameworkCore;

public async Task DecreaseStockAsync(AppDbContext context, int productId, int quantity)
{
    await using var transaction = await context.Database
        .BeginTransactionAsync(IsolationLevel.ReadCommitted);

    try
    {
        var product = await context.Products.FirstAsync(p => p.Id == productId);

        if (product.Stock < quantity)
            throw new InvalidOperationException("Stock is not enough");

        product.Stock -= quantity;
        await context.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

توضیح:

  • تراکنش با ReadCommitted شروع می‌شه.
  • محصول خونده می‌شه و موجودی به‌روزرسانی می‌شه.
  • اگر خطایی رخ بده، Rollback می‌شه.
  • این کد جلوی Dirty Read رو می‌گیره ولی ممکنه Lost Update یا Non-Repeatable Read پیش بیاد.

مثال ۲: تراکنش Serializable برای جلوگیری از Phantom

public async Task<List<Order>> GetPendingOrdersWithLockAsync(AppDbContext context)
{
    await using var transaction = await context.Database
        .BeginTransactionAsync(IsolationLevel.Serializable);

    var orders = await context.Orders
        .Where(o => o.Status == "Pending")
        .ToListAsync();

    // تا زمانی که تراکنش باز است، تراکنش‌های دیگر نمی‌توانند
    // ردیف جدیدی در محدوده این شرط اضافه کنند.

    await transaction.CommitAsync();

    return orders;
}

توضیح:

  • با Serializable، قفل‌های Range اعمال می‌شن.
  • تراکنش‌های دیگر نمی‌تونن یک ردیف جدید با همان وضعیت اضافه کنن.
  • این سطح برای گزارش‌گیری‌های حساس مناسبه، ولی قفل زیاد می‌زنه.

مثال ۳: استفاده از Snapshot

public async Task<Product> GetProductSnapshotAsync(AppDbContext context, int productId)
{
    await using var transaction = await context.Database
        .BeginTransactionAsync(IsolationLevel.Snapshot);

    var product = await context.Products.FirstAsync(p => p.Id == productId);

    await transaction.CommitAsync();

    return product;
}

توضیح:

  • Snapshot قفل نمی‌زنه و نسخه‌سازی ردیف انجام می‌ده.
  • این سطح Optimistic Concurrency هست؛ ممکنه در Update با خطای conflict مواجه بشی.
  • برای استفاده از Snapshot روی SQL Server باید تنظیم دیتابیس فعال باشه:
    ALTER DATABASE YourDb SET ALLOW_SNAPSHOT_ISOLATION ON;
    

مثال ۴: Savepoint برای Rollback جزئی

public async Task ProcessWithSavepointAsync(AppDbContext context)
{
    await using var transaction = await context.Database
        .BeginTransactionAsync(IsolationLevel.ReadCommitted);

    try
    {
        context.Logs.Add(new Log { Step = "Step1" });
        await context.SaveChangesAsync();

        await transaction.CreateSavepointAsync("AfterStep1");

        context.Logs.Add(new Log { Step = "Step2" });
        await context.SaveChangesAsync();

        // فرض کن Step2 نباید انجام می‌شد
        await transaction.RollbackSavepointAsync("AfterStep1");

        context.Logs.Add(new Log { Step = "Step3" });
        await context.SaveChangesAsync();

        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

نتیجه:

  • Step1 و Step3 ثبت می‌شن.
  • Step2 حذف می‌شه بدون این‌که کل تراکنش Rollback بشه.

۶. مثال واقعی و کاربردی

پروژه کوچک: انتقال وجه بین دو حساب

این سناریو واقعی‌ترین مثال برای تراکنش و ایزولیشنه.

مدل

public class Account
{
    public int Id { get; set; }
    public string Owner { get; set; } = string.Empty;
    public decimal Balance { get; set; }
}

سرویس انتقال وجه

using System.Data;
using Microsoft.EntityFrameworkCore;

public class TransferService
{
    private readonly AppDbContext _context;

    public TransferService(AppDbContext context)
    {
        _context = context;
    }

    public async Task TransferAsync(int sourceId, int destinationId, decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");

        // در SQL Server، UPDLOCK از Lost Update جلوگیری می‌کنه.
        await using var transaction = await _context.Database
            .BeginTransactionAsync(IsolationLevel.Serializable);

        try
        {
            var source = await _context.Accounts
                .FromSqlInterpolated($"SELECT * FROM Accounts WITH (UPDLOCK) WHERE Id = {sourceId}")
                .SingleAsync();

            var destination = await _context.Accounts
                .FromSqlInterpolated($"SELECT * FROM Accounts WITH (UPDLOCK) WHERE Id = {destinationId}")
                .SingleAsync();

            if (source.Balance < amount)
                throw new InvalidOperationException("Insufficient balance");

            source.Balance -= amount;
            destination.Balance += amount;

            await _context.SaveChangesAsync();
            await transaction.CommitAsync();
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

توضیح:

  • IsolationLevel.Serializable قفل‌های Range اعمال می‌کنه.
  • WITH (UPDLOCK) مطمئن می‌شه که ردیف‌ها برای Update قفل شدن و تراکنش دیگر نمی‌تونه هم‌زمان تغییرش بده.
  • اگر موجودی کافی نباشه، Rollback انجام می‌شه.
  • اگر خطای دیتابیس یا Cancellation رخ بده، کل عملیات برمی‌گرده.

ثبت سرویس

builder.Services.AddScoped<TransferService>();

استفاده در Controller

[ApiController]
[Route("api/[controller]")]
public class TransfersController : ControllerBase
{
    private readonly TransferService _transferService;

    public TransfersController(TransferService transferService)
    {
        _transferService = transferService;
    }

    [HttpPost]
    public async Task<IActionResult> Transfer(
        [FromQuery] int sourceId,
        [FromQuery] int destinationId,
        [FromQuery] decimal amount,
        CancellationToken cancellationToken)
    {
        try
        {
            await _transferService.TransferAsync(sourceId, destinationId, amount);
            return Ok("Transfer completed");
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

۷. مباحث پیشرفته

۱. انتخاب سطح ایزولیشن بر اساس مشکل

مشکل سطح مناسب
فقط خواندن گزارش بدون نگرانی از داده کثیف ReadUncommitted
عملیات عادی با جلوگیری از Dirty Read ReadCommitted
نیاز به قفل روی ردیف‌ها در طول تراکنش RepeatableRead
جلوگیری از Phantom و تضمین کامل Serializable
خواندن زیاد و نوشتن کم با هم‌زمانی بالا Snapshot

۲. Snapshot Isolation روی SQL Server

برای فعال‌سازی:

ALTER DATABASE YourDb SET ALLOW_SNAPSHOT_ISOLATION ON;

برای فعال‌سازی ReadCommitted Snapshot (برای همه کوئری‌ها):

ALTER DATABASE YourDb SET READ_COMMITTED_SNAPSHOT ON;

تفاوت مهم:

  • ALLOW_SNAPSHOT_ISOLATION فقط برای تراکنش‌هایی هست که صریحاً IsolationLevel.Snapshot می‌گیرند.
  • READ_COMMITTED_SNAPSHOT رفتار سطح ReadCommitted را به‌صورت Snapshot تغییر می‌ده.

۳. TransactionScope برای Multi-Database

اگر چند DbContext یا حتی چند دیتابیس مختلف داری، EF Core به تنهایی تراکنش توزیع‌شده نمی‌سازه.
باید از TransactionScope استفاده کنی:

using System.Transactions;

using var scope = new TransactionScope(
    TransactionScopeOption.Required,
    new TransactionOptions
    {
        IsolationLevel = IsolationLevel.Serializable,
        Timeout = TimeSpan.FromSeconds(30)
    },
    TransactionScopeAsyncFlowOption.Enabled);

await using var context1 = new AppDbContext(options1);
await using var context2 = new AppDbContext(options2);

// عملیات روی context1
// عملیات روی context2

scope.Complete();

نکته‌ها:

  • حتماً TransactionScopeAsyncFlowOption.Enabled بذار، مخصوصاً وقتی async/await داری.
  • TransactionScope ممکنه تراکنش توزیع‌شده رو فعال کنه که نیازمند تنظیمات خاص روی دیتابیس و DTC هست.

۴. Savepoint در تراکنش

Savepoint برای Rollback جزئی عالیه:

await transaction.CreateSavepointAsync("Savepoint1");
// ...
await transaction.RollbackSavepointAsync("Savepoint1");
await transaction.ReleaseSavepointAsync("Savepoint1");

۵. استفاده از DbTransaction خارجی

اگر قبلاً یک DbTransaction از SqlConnection ساخته‌ای، می‌تونی EF Core رو بهش متصل کنی:

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var dbTransaction = await connection.BeginTransactionAsync(
    IsolationLevel.ReadCommitted);

await using var context = new AppDbContext(options);
await context.Database.UseTransactionAsync(dbTransaction);

// ... عملیات ...

await dbTransaction.CommitAsync();

۸. اشتباهات رایج

۱. فراموش کردن Commit یا Rollback

// ❌ اشتباه
var transaction = await context.Database.BeginTransactionAsync();

context.Products.Add(new Product());
await context.SaveChangesAsync();

// فراموش شده: transaction.CommitAsync()

✅ درست:

await using var transaction = await context.Database.BeginTransactionAsync();
context.Products.Add(new Product());
await context.SaveChangesAsync();
await transaction.CommitAsync();

۲. استفاده از TransactionScope بدون Async Flow

// ❌ اشتباه در ASP.NET Core با async/await
using var scope = new TransactionScope();
await SomeAsyncMethod();

✅ درست:

using var scope = new TransactionScope(
    TransactionScopeAsyncFlowOption.Enabled);
await SomeAsyncMethod();

۳. تراکنش بسیار طولانی

این یک اشتباه جدی است:

  • قفل‌ها روی دیتابیس نگه داشته می‌شوند.
  • کاربران دیگر معطل می‌شوند.
  • احتمال Deadlock بالا می‌رود.

✅ همیشه تراکنش را کوتاه کن؛ عملیات I/O خارجی، ارسال ایمیل، صدا زدن API را بیرون از تراکنش انجام بده.

۴. استفاده از Serializable برای همه چیز

قوی‌ترین سطح همیشه بهترین نیست.
اگر فقط یک Update ساده داری، ReadCommitted معمولاً کافیه.

۵. استفاده از ReadUncommitted برای داده مالی

این سطح اجازه خواندن داده کثیف را می‌ده.
برای موجودی حساب، گزارش مالی، رزرو، هرگز ReadUncommitted نده.

۶. تصور Nested Transaction در EF Core

EF Core از Nested Transaction پشتیبانی نمی‌کنه.
اگر بخوای یک تراکنش داخل تراکنش دیگر باز کنی، معمولاً یا خطا می‌گیری یا رفتار نامشخصی رخ می‌ده.
به‌جاش از Savepoint استفاده کن.

۷. فراموش کردن Rollback در Catch

// ❌ اشتباه
try
{
    // ...
    await transaction.CommitAsync();
}
catch
{
    // Rollback فراموش شده
}

✅ درست:

try
{
    // ...
    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

۹. بهترین روش‌ها (Best Practices)

۱. همیشه از await using استفاده کن

await using var transaction = await context.Database.BeginTransactionAsync();

این کار باعث می‌شه اگر CommitAsync فراموش بشه، Dispose تراکنش را Rollback کنه.

۲. سطح ایزولیشن را آگاهانه انتخاب کن

سناریو سطح پیشنهادی
CRUD ساده ReadCommitted
گزارش‌گیری سریع با احتمال خطای کم ReadUncommitted
خواندن چندباره همان ردیف RepeatableRead
رزرو، انتقال مالی Serializable
خواندن زیاد، نوشتن کم Snapshot

۳. تراکنش را تا جای ممکن کوتاه کن

  • قبل از تراکنش، Validation انجام بده.
  • داخل تراکنش فقط عملیات دیتابیس انجام بده.
  • عملیات Logging غیرضروری داخل تراکنش نگذار.

۴. از Async API استفاده کن

در ASP.NET Core همیشه از BeginTransactionAsync، CommitAsync، RollbackAsync استفاده کن تا Thread Pool مسدود نشه.

۵. برای Lost Update از Lock Hint یا Concurrency Token استفاده کن

در SQL Server:

var account = await context.Accounts
    .FromSqlInterpolated($"SELECT * FROM Accounts WITH (UPDLOCK) WHERE Id = {id}")
    .SingleAsync();

یا از ConcurrencyToken و RowVersion استفاده کن.

۶. Deadlock را مدیریت کن

  • همیشه ترتیب یکسان برای قفل‌کردن منابع داشته باش.
  • زمان تراکنش را کوتاه کن.
  • از Retry Policy استفاده کن.

۷. برای هر Case تست هم‌زمانی بنویس

با Task.WhenAll یا مشابه، هم‌زمانی را تست کن:

var tasks = Enumerable.Range(0, 10)
    .Select(i => transferService.TransferAsync(1, 2, 100));

await Task.WhenAll(tasks);

۱۰. منابع و ادامه مسیر

اگه می‌خوای عمیق‌تر بشی:

  • کتاب SQL Server Internals: Transaction Logging and Recovery
  • دوره‌های Performance Tuning در SQL Server
  • مقالات درباره Concurrency در EF Core با Pessimistic و Optimistic Concurrency

۱۱. مرجع کامل توابع و متدها (API Deep Dive)

در این بخش، متدهای اصلی مرتبط با Isolation Transaction در EF Core را با جزئیات کامل بررسی می‌کنیم.

📌 متد: BeginTransaction(IsolationLevel)

امضای متد (Signature):

public static IDbContextTransaction BeginTransaction(
    this DatabaseFacade databaseFacade,
    IsolationLevel isolationLevel
);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
databaseFacade DatabaseFacade بله (گیرنده Extension) دسترسی به دیتابیس از طریق context.Database context.Database
isolationLevel System.Data.IsolationLevel بله سطح ایزولیشن تراکنش IsolationLevel.ReadCommitted

مقدار برگشتی (Return Value):

  • نوع: IDbContextTransaction
  • شیء تراکنش که می‌تونی Commit یا Rollback رو روی آن صدا بزنی.
  • اگر BeginTransaction ناموفق باشد، Exception پرتاب می‌شود نه null.

کاری که انجام می‌ده (گام به گام):

۱. بررسی می‌کند که این DbContext به یک Provider متصل است.
۲. بررسی می‌کند که قبلاً تراکنش باز وجود ندارد یا اینکه Provider از تراکنش پشتیبانی می‌کند.
۳. از DbConnection مربوط به Provider، تراکنش با سطح ایزولیشن مشخص را آغاز می‌کند.
۴. تراکنش را در یک شیء IDbContextTransaction می‌پیچد و برمی‌گرداند.

مثال ساده و قابل اجرا:

using System.Data;
using Microsoft.EntityFrameworkCore;

using var context = new AppDbContext(options);

using var transaction = context.Database
    .BeginTransaction(IsolationLevel.ReadCommitted);

var product = context.Products.First(p => p.Id == 1);
product.Stock -= 5;
context.SaveChanges();

transaction.Commit();

مثال واقعی و کاربردی:

public void TransferMoney(int sourceId, int destinationId, decimal amount)
{
    using var transaction = _context.Database
        .BeginTransaction(IsolationLevel.Serializable);

    try
    {
        var source = _context.Accounts
            .FromSqlRaw("SELECT * FROM Accounts WITH (UPDLOCK) WHERE Id = {0}", sourceId)
            .Single();

        var destination = _context.Accounts
            .FromSqlRaw("SELECT * FROM Accounts WITH (UPDLOCK) WHERE Id = {0}", destinationId)
            .Single();

        if (source.Balance < amount)
            throw new InvalidOperationException("Insufficient balance");

        source.Balance -= amount;
        destination.Balance += amount;

        _context.SaveChanges();
        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
InvalidOperationException تراکنش قبلی باز است، یا Provider نصب نشده است اطمینان از بسته‌بودن تراکنش قبلی یا ثبت Provider
ArgumentException مقدار IsolationLevel نامعتبر است استفاده از enum معتبر
NotSupportedException Provider از سطح ایزولیشن پشتیبانی نمی‌کند تغییر سطح به پشتیبانیشده
DbException خطا در اتصال به دیتابیس بررسی Connection String و در دسترس بودن دیتابیس

Overloadها:

// تراکنش با سطح ایزولیشن پیش‌فرض
IDbContextTransaction BeginTransaction();

// با سطح ایزولیشن مشخص
IDbContextTransaction BeginTransaction(IsolationLevel isolationLevel);

اشتباهات رایج:

  • ❌ باز کردن تراکنش بعد از بستن Connection → ✅ همیشه Connection باید باز باشد.
  • ❌ استفاده از BeginTransaction در متد async → ✅ از BeginTransactionAsync استفاده کن.
  • ❌ فراموش کردن Commit → ✅ از using یا try/finally استفاده کن.

متدهای مرتبط:

  • BeginTransactionAsync
  • Commit
  • Rollback
  • UseTransaction

📌 متد: BeginTransactionAsync(IsolationLevel, CancellationToken)

امضای متد (Signature):

public static Task<IDbContextTransaction> BeginTransactionAsync(
    this DatabaseFacade databaseFacade,
    IsolationLevel isolationLevel,
    CancellationToken cancellationToken = default
);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
databaseFacade DatabaseFacade بله (گیرنده Extension) دسترسی به دیتابیس context.Database
isolationLevel IsolationLevel بله سطح ایزولیشن تراکنش IsolationLevel.Snapshot
cancellationToken CancellationToken خیر سیگنال لغو عملیات HttpContext.RequestAborted

مقدار برگشتی (Return Value):

  • نوع: Task<IDbContextTransaction>
  • پس از await کردن، IDbContextTransaction بازگشت داده می‌شود.
  • اگر Operation لغو شود، OperationCanceledException پرتاب می‌شود.

کاری که انجام می‌ده (گام به گام):

۱. بررسی CancellationToken برای لغو.
۲. بررسی Provider و اتصال دیتابیس.
۳. شروع غیرهمزمان تراکنش روی DbConnection با سطح ایزولیشن مشخص.
۴. بازگرداندن Task<IDbContextTransaction>.

مثال ساده و قابل اجرا:

await using var context = new AppDbContext(options);

await using var transaction = await context.Database
    .BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);

var order = new Order { TotalPrice = 250 };
context.Orders.Add(order);
await context.SaveChangesAsync(cancellationToken);

await transaction.CommitAsync(cancellationToken);

مثال واقعی و کاربردی:

public async Task ReserveSeatAsync(int seatId, CancellationToken cancellationToken)
{
    await using var transaction = await _context.Database
        .BeginTransactionAsync(IsolationLevel.Serializable, cancellationToken);

    try
    {
        var seat = await _context.Seats
            .FirstAsync(s => s.Id == seatId, cancellationToken);

        if (seat.IsReserved)
            throw new InvalidOperationException("Seat already reserved");

        seat.IsReserved = true;
        await _context.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);
    }
    catch
    {
        await transaction.RollbackAsync(cancellationToken);
        throw;
    }
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
OperationCanceledException لغو از طریق CancellationToken مدیریت لغو در Controller
InvalidOperationException تراکنش باز موجود یا Provider نامعتبر عدم باز کردن تراکنش دوم
DbException خطای دیتابیس بررسی اتصال
NotSupportedException سطح ایزولیشن پشتیبانی نمی‌شود تغییر سطح

Overloadها:

Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
Task<IDbContextTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default);

اشتباهات رایج:

  • ❌ استفاده از using به جای await using → ✅ برای Async Dispose باید await using بذاری.
  • ❌ عدم ارسال CancellationToken از Controller → ✅ همیشه cancellationToken را پاس بده.
  • ❌ فراخوانی BeginTransactionAsync با Connection بسته → ✅ ابتدا Connection را باز کن.

متدهای مرتبط:

  • BeginTransaction
  • CommitAsync
  • RollbackAsync
  • CreateSavepointAsync

📌 متد: Commit()

امضای متد (Signature):

public void Commit();

ورودی‌ها (Parameters):

نام پارامتر نوع اجباری؟ توضیح دقیق مثال مقدار
— — — هیچ پارامتری ندارد —

مقدار برگشتی (Return Value):

  • نوع: void
  • چیزی برنمی‌گرداند. در صورت موفقیت، تغییرات روی دیتابیس دائمی می‌شوند.

کاری که انجام می‌ده (گام به گام):

۱. بررسی می‌کند که تراکنش هنوز فعال است و Commit/Rollback نشده است.
۲. گرفتن DbTransaction زیرین.
۳. فراخوانی Commit بر روی DbTransaction ADO.NET.
۴. انتشار تغییرات و پایان تراکنش.

مثال ساده و قابل اجرا:

using var transaction = context.Database.BeginTransaction();

var user = new User { Name = "Ali" };
context.Users.Add(user);
context.SaveChanges();

transaction.Commit();

مثال واقعی و کاربردی:

public void AddOrder(Order order)
{
    using var transaction = _context.Database.BeginTransaction();

    try
    {
        _context.Orders.Add(order);
        _context.SaveChanges();

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
InvalidOperationException تراکنش قبلاً Commit یا Rollback شده باشد فقط یک‌بار Commit/Rollback کن
ObjectDisposedException تراکنش Dispose شده باشد از Commit قبل از Dispose استفاده کن
DbException خطای دیتابیس هنگام Commit مدیریت خطا و Rollback

Overloadها:

  • دارد: فقط نسخه Sync.

اشتباهات رایج:

  • ❌ صدا زدن Commit بعد از Dispose → ✅ Commit قبل از Dispose.
  • ❌ فرض اینکه بعد از Commit باز هم می‌شه Rollback کرد → ✅ پس از Commit تراکنش تمام شده است.

متدهای مرتبط:

  • CommitAsync
  • Rollback
  • BeginTransaction

📌 متد: CommitAsync(CancellationToken)

امضای متد (Signature):

public Task CommitAsync(CancellationToken cancellationToken = default);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
cancellationToken CancellationToken خیر سیگنال لغو عملیات Commit cancellationToken

مقدار برگشتی (Return Value):

  • نوع: Task
  • عملیات غیرهمزمان Commit را اجرا می‌کند. در صورت لغو، OperationCanceledException پرتاب می‌شود.

کاری که انجام می‌ده (گام به گام):

۱. بررسی می‌کند که تراکنش فعال است.
۲. گرفتن DbTransaction زیرین.
۳. فراخوانی CommitAsync بر روی DbTransaction با همان CancellationToken.
۴. برگرداندن Task برای await.

مثال ساده و قابل اجرا:

await using var transaction = await context.Database
    .BeginTransactionAsync();

context.Products.Add(new Product { Name = "Book" });
await context.SaveChangesAsync();

await transaction.CommitAsync();

مثال واقعی و کاربردی:

public async Task CompleteOrderAsync(int orderId, CancellationToken cancellationToken)
{
    await using var transaction = await _context.Database
        .BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);

    var order = await _context.Orders.FirstAsync(o => o.Id == orderId, cancellationToken);
    order.Status = "Completed";
    await _context.SaveChangesAsync(cancellationToken);

    await transaction.CommitAsync(cancellationToken);
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
OperationCanceledException لغو از طریق CancellationToken مدیریت لغو
InvalidOperationException تراکنش قبلاً پایان یافته فقط یک‌بار Commit
DbException خطای Commit در دیتابیس Rollback در Catch

Overloadها:

Task CommitAsync();
Task CommitAsync(CancellationToken cancellationToken);

اشتباهات رایج:

  • ❌ استفاده از Commit() در متد async → ✅ از CommitAsync استفاده کن.
  • ❌ نگه نداشتن await در مقابل CommitAsync → ✅ همیشه await کن.

متدهای مرتبط:

  • Commit
  • RollbackAsync
  • BeginTransactionAsync

📌 متد: Rollback()

امضای متد (Signature):

public void Rollback();

ورودی‌ها (Parameters):

نام پارامتر نوع اجباری؟ توضیح دقیق مثال مقدار
— — — هیچ پارامتری ندارد —

مقدار برگشتی (Return Value):

  • نوع: void
  • تمام تغییرات انجام‌شده در تراکنش جاری را بازمی‌گرداند.

کاری که انجام می‌ده (گام به گام):

۱. بررسی می‌کند که تراکنش هنوز فعال است.
۲. گرفتن DbTransaction زیرین.
۳. فراخوانی Rollback بر روی DbTransaction.
۴. لغو تمام عملیات و همه Savepointها.

مثال ساده و قابل اجرا:

using var transaction = context.Database.BeginTransaction();

try
{
    context.Users.Add(new User { Name = "Ali" });
    context.SaveChanges();

    throw new InvalidOperationException("Simulated error");
}
catch
{
    transaction.Rollback();
    throw;
}

مثال واقعی و کاربردی:

public void UpdateInventory(int productId, int quantity)
{
    using var transaction = _context.Database.BeginTransaction();

    try
    {
        var product = _context.Products.First(p => p.Id == productId);
        product.Stock -= quantity;
        _context.SaveChanges();

        if (product.Stock < 0)
            throw new InvalidOperationException("Negative stock");

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
InvalidOperationException تراکنش قبلاً Commit/Rollback شده فقط یک‌بار Rollback کن
ObjectDisposedException تراکنش Dispose شده Rollback قبل از Dispose
DbException خطا در دیتابیس مدیریت خطا

Overloadها:

  • فقط نسخه Sync.

اشتباهات رایج:

  • ❌ استفاده از Rollback بعد از Commit → ✅ پس از Commit نمی‌شه Rollback کرد.
  • ❌ ننوشتن throw بعد از Rollback در Catch → ✅ همیشه throw کن تا خطا بالا برود.

متدهای مرتبط:

  • RollbackAsync
  • Commit
  • RollbackSavepoint

📌 متد: RollbackAsync(CancellationToken)

امضای متد (Signature):

public Task RollbackAsync(CancellationToken cancellationToken = default);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
cancellationToken CancellationToken خیر سیگنال لغو عملیات Rollback cancellationToken

مقدار برگشتی (Return Value):

  • نوع: Task
  • عملیات غیرهمزمان Rollback را اجرا می‌کند.

کاری که انجام می‌ده (گام به گام):

۱. بررسی می‌کند که تراکنش فعال است.
۲. گرفتن DbTransaction زیرین.
۳. فراخوانی RollbackAsync بر روی DbTransaction.
۴. بازگرداندن Task برای await.

مثال ساده و قابل اجرا:

await using var transaction = await context.Database
    .BeginTransactionAsync();

try
{
    context.Users.Add(new User { Name = "Ali" });
    await context.SaveChangesAsync();

    throw new InvalidOperationException("Simulated error");
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

مثال واقعی و کاربردی:

public async Task ProcessPaymentAsync(Payment payment, CancellationToken cancellationToken)
{
    await using var transaction = await _context.Database
        .BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);

    try
    {
        _context.Payments.Add(payment);
        await _context.SaveChangesAsync(cancellationToken);

        // فرض کن یک سرویس خارجی خطا می‌دهد
        if (payment.Amount <= 0)
            throw new InvalidOperationException("Invalid amount");

        await transaction.CommitAsync(cancellationToken);
    }
    catch
    {
        await transaction.RollbackAsync(cancellationToken);
        throw;
    }
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
OperationCanceledException لغو از طریق CancellationToken مدیریت لغو
InvalidOperationException تراکنش قبلاً پایان یافته فقط یک‌بار Rollback
DbException خطای دیتابیس مدیریت خطا

Overloadها:

Task RollbackAsync();
Task RollbackAsync(CancellationToken cancellationToken);

اشتباهات رایج:

  • ❌ استفاده از Rollback() در متد async → ✅ از RollbackAsync استفاده کن.
  • ❌ حذف throw پس از Rollback → ✅ حتماً throw کن.

متدهای مرتبط:

  • Rollback
  • CommitAsync
  • RollbackSavepointAsync

📌 متد: CreateSavepoint(string)

امضای متد (Signature):

public void CreateSavepoint(string name);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
name string بله نام Savepoint برای Rollback جزئی "AfterInsert"

مقدار برگشتی (Return Value):

  • نوع: void
  • چیزی برنمی‌گرداند. یک نقطه ذخیره داخل تراکنش ایجاد می‌کند.

کاری که انجام می‌ده (گام به گام):

۱. بررسی می‌کند که تراکنش جاری فعال است.
۲. بررسی می‌کند که Provider از Savepoint پشتیبانی می‌کند (SupportsSavepoints).
۳. صدور دستور SAVE TRANSACTION name (در SQL Server) روی اتصال جاری.
۴. ثبت Savepoint برای استفاده بعدی.

مثال ساده و قابل اجرا:

using var transaction = context.Database.BeginTransaction();

context.Users.Add(new User { Name = "A" });
context.SaveChanges();

transaction.CreateSavepoint("AfterA");

context.Users.Add(new User { Name = "B" });
context.SaveChanges();

transaction.RollbackSavepoint("AfterA"); // افزودن B لغو می‌شود
transaction.Commit(); // فقط A ثبت می‌شود

مثال واقعی و کاربردی:

public void CreateOrderWithOptionalLogging(Order order, bool addLog)
{
    using var transaction = _context.Database.BeginTransaction();

    _context.Orders.Add(order);
    _context.SaveChanges();

    transaction.CreateSavepoint("OrderSaved");

    if (addLog)
    {
        _context.Logs.Add(new Log { Message = "Order created" });
        _context.SaveChanges();
    }

    // اگر تصمیم به حذف Log بگیریم:
    if (!addLog)
    {
        transaction.RollbackSavepoint("OrderSaved");
    }

    transaction.Commit();
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
InvalidOperationException تراکنش فعال نیست ابتدا تراکنش را Begin کن
ArgumentException name خالی یا null است نام معتبر بده
NotSupportedException Provider از Savepoint پشتیبانی نمی‌کند بررسی SupportsSavepoints

Overloadها:

void CreateSavepoint(string name);
Task CreateSavepointAsync(string name, CancellationToken cancellationToken = default);

اشتباهات رایج:

  • ❌ استفاده از Savepoint بدون بررسی SupportsSavepoints → ✅ ابتدا بررسی کن.
  • ❌ نام تکراری مبهم → ✅ نام یکتا و معنادار بده.

متدهای مرتبط:

  • RollbackSavepoint
  • ReleaseSavepoint
  • BeginTransaction

📌 متد: RollbackSavepoint(string)

امضای متد (Signature):

public void RollbackSavepoint(string name);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
name string بله نام Savepoint برای Rollback به آن نقطه "OrderSaved"

مقدار برگشتی (Return Value):

  • نوع: void
  • تغییرات بعد از Savepoint را بازمی‌گرداند، اما کل تراکنش را Rollback نمی‌کند.

کاری که انجام می‌ده (گام به گام):

۱. بررسی می‌کند که تراکنش جاری فعال است.
۲. بررسی می‌کند که Savepoint با نام داده‌شده وجود دارد.
۳. اجرای دستور ROLLBACK TRANSACTION name (در SQL Server).
۴. حفظ تغییرات قبل از Savepoint و لغو تغییرات پس از آن.

مثال ساده و قابل اجرا:

using var transaction = context.Database.BeginTransaction();

context.Users.Add(new User { Name = "A" });
context.SaveChanges();

transaction.CreateSavepoint("AfterA");

context.Users.Add(new User { Name = "B" });
context.SaveChanges();

transaction.RollbackSavepoint("AfterA"); // B حذف می‌شود
transaction.Commit();

مثال واقعی و کاربردی:

public void SetupUserAccount(User user)
{
    using var transaction = _context.Database.BeginTransaction();

    _context.Users.Add(user);
    _context.SaveChanges();

    transaction.CreateSavepoint("UserSaved");

    try
    {
        _context.Accounts.Add(new Account { UserId = user.Id, Balance = 0 });
        _context.SaveChanges();
    }
    catch
    {
        // اگر ساخت Account خطا خورد، فقط Account برگردد، نه User
        transaction.RollbackSavepoint("UserSaved");
    }

    transaction.Commit();
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
InvalidOperationException تراکنش فعال نیست یا Savepoint وجود ندارد بررسی نام و تراکنش
ArgumentException name خالی یا null است نام معتبر بده
NotSupportedException Provider از Savepoint پشتیبانی نمی‌کند بررسی SupportsSavepoints

Overloadها:

void RollbackSavepoint(string name);
Task RollbackSavepointAsync(string name, CancellationToken cancellationToken = default);

اشتباهات رایج:

  • ❌ Rollback به Savepointای که هرگز ساخته نشده → ✅ ابتدا CreateSavepoint کن.
  • ❌ فراموش کردن ReleaseSavepoint پس از اتمام کار → ✅ در صورت نیاز Release کن.

متدهای مرتبط:

  • CreateSavepoint
  • ReleaseSavepoint
  • Rollback

۱۲. مقایسه کلاس‌ها/توابع مشابه

مقایسه Database.BeginTransaction با SqlConnection.BeginTransaction و TransactionScope

ویژگی Database.BeginTransaction (EF Core) SqlConnection.BeginTransaction (ADO.NET) TransactionScope
سطح انتزاع بالا، روی DbContext پایین، روی DbConnection بالاتر، روی TransactionManager
استفاده آسان بله متوسط بله برای Multi-Database
پشتیبانی Async بله بله بله (با Enabled)
تراکنش توزیع‌شده خیر خیر بله
Commit/Rollback دستی بله بله خیر (خودکار با Complete)
Nested Transaction پشتیبانی نمی‌شود؛ Savepoint پشتیبانی نمی‌شود؛ Savepoint گاهی پشتیبانی نسبی
مناسب برای عملیات روی یک DbContext کنترل دقیق Connection چند DbContext یا چند دیتابیس
نیاز به پکیج فقط EF Core Provider System.Data.SqlClient یا Microsoft.Data.SqlClient System.Transactions

مقایسه سطح‌های ایزولیشن

سطح جلوگیری از Dirty Read جلوگیری از Non-Repeatable جلوگیری از Phantom قفل‌گذاری کارایی
ReadUncommitted ❌ ❌ ❌ خیلی کم خیلی بالا
ReadCommitted ✅ ❌ ❌ متوسط بالا
RepeatableRead ✅ ✅ ❌ زیاد متوسط
Serializable ✅ ✅ ✅ خیلی زیاد پایین
Snapshot ✅ ✅ ✅ Optimistic بالا

۱۳. سناریوهای واقعی (Real-World Scenarios)

سناریو ۱: انتقال وجه بین دو حساب

نیاز:
اگر پول از یک حساب کم شد و به حساب دیگر اضافه نشد، نباید هیچ تغییری ثبت شود.

سطح ایزولیشن: Serializable یا RepeatableRead همراه با UPDLOCK.

راه‌حل:

await using var transaction = await _context.Database
    .BeginTransactionAsync(IsolationLevel.Serializable);

var source = await _context.Accounts
    .FromSqlRaw("SELECT * FROM Accounts WITH (UPDLOCK) WHERE Id = {0}", sourceId)
    .SingleAsync();

var destination = await _context.Accounts
    .FromSqlRaw("SELECT * FROM Accounts WITH (UPDLOCK) WHERE Id = {0}", destinationId)
    .SingleAsync();

source.Balance -= amount;
destination.Balance += amount;

await _context.SaveChangesAsync();
await transaction.CommitAsync();

سناریو ۲: مدیریت موجودی انبار

نیاز:
اگر ۱۰ کاربر هم‌زمان ۱۰ محصول خریدن و موجودی واقعی ۵ است، فقط ۵ خرید باید موفق باشد.

سطح ایزولیشن: RepeatableRead یا Serializable.

راه‌حل:

await using var transaction = await _context.Database
    .BeginTransactionAsync(IsolationLevel.RepeatableRead);

var product = await _context.Products
    .FromSqlRaw("SELECT * FROM Products WITH (UPDLOCK) WHERE Id = {0}", productId)
    .SingleAsync();

if (product.Stock < quantity)
    throw new InvalidOperationException("Not enough stock");

product.Stock -= quantity;
await _context.SaveChangesAsync();
await transaction.CommitAsync();

سناریو ۳: رزرو صندلی

نیاز:
یک صندلی فقط یک‌بار رزرو شود.

سطح ایزولیشن: Serializable یا RepeatableRead با بررسی وضعیت.

راه‌حل:

await using var transaction = await _context.Database
    .BeginTransactionAsync(IsolationLevel.Serializable);

var seat = await _context.Seats.SingleAsync(s => s.Id == seatId);

if (seat.IsReserved)
    throw new InvalidOperationException("Seat already reserved");

seat.IsReserved = true;
await _context.SaveChangesAsync();
await transaction.CommitAsync();

سناریو ۴: چند دیتابیس یا چند Context

نیاز:
عملیات روی دو DbContext یا دو دیتابیس باید اتمیک باشد.

راه‌حل: TransactionScope

using var scope = new TransactionScope(
    TransactionScopeOption.Required,
    new TransactionOptions
    {
        IsolationLevel = IsolationLevel.ReadCommitted,
        Timeout = TimeSpan.FromMinutes(1)
    },
    TransactionScopeAsyncFlowOption.Enabled);

await using var firstContext = new AppDbContext(firstOptions);
await using var secondContext = new AppDbContext(secondOptions);

// عملیات روی firstContext
// عملیات روی secondContext

scope.Complete();

۱۴. کارایی و Performance

چرا انتخاب Isolation Level روی کارایی اثر می‌گذارد؟

  • سطوح بالاتر مثل Serializable قفل‌های بیشتر و طولانی‌تری اعمال می‌کنند.
  • قفل‌های بیشتر یعنی Blocking بیشتر و در نتیجه انتظار بیشتر برای سایر تراکنش‌ها.
  • هرچه تراکنش طولانی‌تر باشد، احتمال Deadlock بالا می‌رود.

جدول تقریبی هزینه

سطح Isolation Locking Blocking Deadlock Speed
ReadUncommitted خیلی کم خیلی کم خیلی کم خیلی بالا
ReadCommitted کم کم کم بالا
RepeatableRead متوسط متوسط متوسط متوسط
Serializable زیاد زیاد زیاد پایین
Snapshot Optimistic کم کم بالا (با هزینه TempDB)

نکات بهینه‌سازی

۱. کمترین سطح ممکن را انتخاب کن

برای عملیات ساده CRUD، ReadCommitted معمولاً کافی است.
فقط وقتی واقعاً به قوی‌ترین سطح نیاز داری از Serializable استفاده کن.

۲. تراکنش را کوتاه نگه دار

// ❌ بد
await using var transaction = await context.Database.BeginTransactionAsync();

await SendEmailAsync(); // طولانی و غیرضروری داخل تراکنش
var product = await context.Products.FirstAsync();
await transaction.CommitAsync();
// ✅ خوب
await SendEmailAsync(); // قبل از تراکنش

await using var transaction = await context.Database.BeginTransactionAsync();
var product = await context.Products.FirstAsync();
await transaction.CommitAsync();

۳. از Index استفاده کن

اگر WHERE یا JOIN روی ستون‌های بدون Index اجرا شود، دیتابیس Table Scan می‌کند و قفل‌های اضافی می‌زند.
برای ستون‌های مورد استفاده در تراکنش‌های حساس، Index بساز.

۴. Snapshot برای کاهش Blocking

در سیستم‌های خواندن‌زیاد/نوشتن‌کم، Snapshot می‌تواند هم‌زمانی را بالا ببرد، اما:

  • از فضای tempdb استفاده می‌کند.
  • باید ALLOW_SNAPSHOT_ISOLATION را فعال کنی.
  • ممکن است Update conflict رخ بدهد.

۵. مدیریت Deadlock با Retry

همیشه Deadlock امکان دارد. یک Retry Policy ساده:

public async Task ExecuteWithRetryAsync(Func<Task> operation, int maxRetries = 3)
{
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            await operation();
            return;
        }
        catch (SqlException ex) when (ex.Number == 1205 && attempt < maxRetries)
        {
            await Task.Delay(TimeSpan.FromMilliseconds(attempt * 200));
        }
    }
}

۱۵. چک‌لیست یادگیری

مفاهیم:

  • تفاوت ACID با Isolation Level را می‌دانم.
  • سه Anomaly یعنی Dirty Read، Non-Repeatable Read و Phantom Read را می‌شناسم.
  • همه مقادیر IsolationLevel را می‌دانم.
  • می‌دانم هر سطح کدام Anomaly را کنترل می‌کند.

مهارت‌های کدنویسی:

  • می‌توانم با BeginTransactionAsync تراکنش شروع کنم.
  • می‌توانم CommitAsync و RollbackAsync را درست به کار ببرم.
  • می‌توانم Savepoint بسازم و Rollback جزئی کنم.
  • می‌توانم در Catch تراکنش را Rollback کنم.
  • می‌توانم با TransactionScope چند Context را تراکنش بدهم.

انتخاب درست:

  • برای عملیات مالی، سطح ایزولیشن مناسب را انتخاب می‌کنم.
  • برای گزارش‌گیری سبک، ReadUncommitted را فقط وقتی قابل قبول است استفاده می‌کنم.
  • در سیستم‌های خواندن‌زیاد، Snapshot را بررسی می‌کنم.
  • می‌دانم Serializable چه زمانی ضروری است و چه زمانی فقط سربار اضافه ایجاد می‌کند.

تست:

  • یک تست هم‌زمانی با Task.WhenAll نوشته‌ام.
  • رفتار Lost Update را با UPDLOCK یا Concurrency Token تست کرده‌ام.
  • Deadlock و Retry Policy را امتحان کرده‌ام.

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.