EF Core Transactions

Transactions in Entity Framework Core

A transaction in any database system represents a logical unit of work, a set of operations that must succeed or fail together. When an application performs multiple related operations (e.g., creating an order, inserting order items, updating stock, creating payment records), these operations must be treated as one atomic process.

In the real world, especially in e-commerce, banking, finance, ERP, and ticket booking systems, transactions ensure data correctness, reliability, and consistency even in the presence of errors, system crashes, or concurrency conflicts.

Entity Framework Core (EF Core) provides powerful transaction management features to control how data is saved, rolled back, or committed. In this session, we will explore Transactions in Entity Framework Core using real-world examples, covering both implicit (automatic) and explicit (manual) transaction management with SQL Server and a .NET 8 Web API.

What is a Transaction in EF Core?

A transaction is one of the most fundamental concepts in relational databases. It represents a single, logical of work that may contain one or more database operations such as:

  • INSERT
  • UPDATE
  • DELETE
  • Or a combination of the above

When these operations are wrapped in a transaction, they are treated as a single atomic operation, meaning they must all succeed or fail together. It means:

  • If all operations succeed → the transaction commits.
  • If any operation fails → the transaction rolls back (undoes all operations).

This ensures that the database never enters a partial, broken, or inconsistent state. To ensure this behaviour, every transaction must adhere to the well-known ACID Principles:

A – Atomicity

Atomicity guarantees that:

  • Either all statements inside the transaction succeed, or
  • None of them is applied to the database

In case of any error (network failure, constraint violation, exception), the database automatically rolls back all changes made inside the transaction, returning the database to the state before the transaction started.

Real-world Example

During online shopping:

  • Order is created
  • Order items are inserted
  • Payment is captured

If payment fails, none of these actions should be saved. Atomicity ensures you never end up with:

  • An order without payment
  • Deducted stock without an order
C – Consistency

Consistency ensures that after the transaction:

  • The data remains valid
  • All constraints, rules, and relationships remain intact

This includes:

  • Primary keys
  • Foreign keys
  • Unique constraints
  • Stock must not go negative
  • Payment amount must match the total order amount

Example: If your Orders table has a foreign key to Customers, the transaction ensures an order is never saved for a non-existent customer. Even if the application fails or crashes, the database must never enter an invalid or corrupted state.

I – Isolation

Isolation ensures that:

  • Multiple transactions can run at the same time
  • But they do not interfere with each other

Each transaction operates on the data as if it were the only one executing.

Why is isolation necessary?

Imagine two users buying the last item:

  • Transaction A is checking the stock
  • Transaction B is checking the stock

Without proper isolation, both may believe stock = 1 and proceed to buy. Isolation ensures that only one transaction succeeds and the other rolls back, maintaining stock accuracy.

D – Durability

Durability means:

  • Once a transaction is committed, the changes become permanent
  • Even power failures, crashes, or restarts cannot undo these changes

This is typically achieved through database-level logging and recovery mechanisms.

Example: Durability guarantees that once the user receives “Order Confirmed,” the system will never lose that record.

Why Use Transactions in EF Core?

Imagine a customer is ordering a laptop from an e-commerce application. The following actions must happen successfully:

  1. Create an order
  2. Insert order item
  3. Reduce stock from the Product table
  4. Capture payment
  5. Update order status

If any one step fails, the entire sequence must be undone.

Without transactions

You may end up with:

  • Order created, but payment not captured
  • Payment captured, but order not created
  • Inventory reduced, butthe  order was not stored
  • Order stored, but itemsare  missing

This leads to:

  • Financial loss
  • Inventory mismatch
  • Duplicate payments
  • Unshipped or ghost orders
  • Bad customer experience
With transactions

All operations behave like one complete package:

  • Everything succeeds → COMMIT
  • Any step fails → ROLLBACK

This creates a guarantee of correctness and consistency.

Types of Transactions in Entity Framework Core

EF Core supports two major transaction mechanisms:

  1. Implicit Transactions: Automatically created by EF Core around each SaveChanges().
  2. Explicit Transactions: Created manually using BeginTransaction(), Commit(), and Rollback(). We control commit/rollback.

Example to Understand Transaction: E-Commerce

When a customer places an order in a real-time e-commerce system, the following steps must execute 100% atomically:

Order Placement Workflow

  1. Creating an Order
  2. Adding Order Items
  3. Capturing Payment
  4. Reducing Product Inventory
  5. Updating Order Status
  6. Creating Order History Logs

If any step fails:

  • Payment should not be captured
  • Order items should not exist
  • Inventory should not be reduced

Hence, Transactions are mandatory.

Entity Framework Core Transaction in a Real-Time E-Commerce Application

Please create a new ASP.NET Core Web API project named ECommerceApp. Run the following commands in Visual Studio Package Manager Console to install EF Core Packages:

  • Install-Package Microsoft.EntityFrameworkCore
  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools

Domain Enums

Create a folder Enums.

OrderStatus Enum

Create a class file named OrderStatus.cs within the Enums folder, and copy-paste the following code. This enum represents the different stages an order goes through in the e-commerce workflow. It allows the system to track whether an order is still pending, whether payment has been received, and whether the order is being processed, shipped, delivered, or cancelled. Using an enum avoids invalid status values and ensures that all services (Order, Payment, Notification, Reporting) interpret order states consistently.

namespace ECommerceApp.Enums
{
    public enum OrderStatus
    {
        Pending = 1,         // Order created but payment not yet confirmed
        PaymentReceived = 2, // Payment captured successfully
        Processing = 3,      // Seller/Warehouse is preparing the order (packing, quality check)
        Shipped = 4,         // Order has left the warehouse and is in transit with the courier
        Delivered = 5,       // Order has been delivered successfully to the customer
        Cancelled = 6        // Order cancelled (by customer, admin, or due to payment/stock issues)
    }
}
PaymentStatus Enum

Create a class file named OrderStatus.cs within the Enums folder, and copy-paste the following code. This enum defines the life cycle of a payment transaction. It indicates whether a payment is still pending, completed, failed, or refunded. This is essential for financial correctness, payment gateway reconciliation, and ensuring orders are not processed without a valid payment.

namespace ECommerceApp.Enums
{
    public enum PaymentStatus
    {
        Pending = 1,      // Payment initiated
        Paid = 2,         // Payment completed
        Failed = 3,       // Payment failed due to any reason
        Refunded = 4      // Refund processed
    }
}

Creating Entities

Create a new folder: Entities.

Product Entity

Create a class file named Product.cs within the Entities folder, and copy-paste the following code. This class represents a sellable item in the e-commerce platform. It stores product details such as name, SKU, price, discount, final price, and stock, and also maintains relationships with categories and order items. It is essential for inventory management, price calculations, and catalog display.

using Microsoft.EntityFrameworkCore;
namespace ECommerceApp.Entities
{
    public class Product
    {
        public int Id { get; set; }                      // Primary key
        public string Name { get; set; } = null!;
        public string SKU { get; set; } = null!;         // Unique product code
        public string? Description { get; set; }
        [Precision(18, 2)]
        public decimal Price { get; set; }
        [Precision(18, 2)]
        public decimal Discount { get; set; }
        [Precision(18, 2)]
        public decimal FinalPrice { get; set; }          // Price - Discount
        public int StockQuantity { get; set; }
        public bool IsActive { get; set; } = true;
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        public DateTime? UpdatedAt { get; set; }

        // Navigation
        public ICollection<ProductCategory> ProductCategories { get; set; }
            = new List<ProductCategory>();

        public ICollection<OrderItem> OrderItems { get; set; }
            = new List<OrderItem>();
    }
}
Category Entity

Create a class file named Category.cs within the Entities folder, and copy-paste the following code. This class represents a product category such as Electronics, Clothing, or Books. Categories help organize products, improve search and filtering, and build a hierarchical product catalog. It participates in a many-to-many relationship with products.

namespace ECommerceApp.Entities
{
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public string? Description { get; set; }
        public bool IsActive { get; set; } = true;

        // Many-to-Many
        public ICollection<ProductCategory> ProductCategories { get; set; }
            = new List<ProductCategory>();
    }
}
ProductCategory (Many-to-Many Bridge)

Create a class file named ProductCategory.cs within the Entities folder, and copy-paste the following code. This join entity models the many-to-many relationship between products and categories. It allows a product to belong to multiple categories and vice versa. It also captures additional metadata, such as when the link was created. This is critical for flexible product classification.

namespace ECommerceApp.Entities
{
    // Join entity for many-to-many between Product and Category
    public class ProductCategory
    {
        public int ProductId { get; set; }
        public int CategoryId { get; set; }

        public DateTime LinkedAt { get; set; } = DateTime.UtcNow;

        // Navigation
        public Product Product { get; set; } = null!;
        public Category Category { get; set; } = null!;
    }
}
Customer Entity

Create a class file named Customer.cs within the Entities folder, and copy-paste the following code. This represents the user who places orders on the platform. It stores profile information such as name, contact info, and customer number. It also maintains navigation properties to addresses and orders. This entity serves as the foundation for personalization, order history tracking, and customer-specific operations.

namespace ECommerceApp.Entities
{
    public class Customer
    {
        public int Id { get; set; }
        public string CustomerNumber { get; set; } = null!; // Business identifier
        public string FirstName { get; set; } = null!;
        public string LastName { get; set; } = null!;
        public string Email { get; set; } = null!;
        public string Phone { get; set; } = null!;
        public bool IsActive { get; set; } = true;

        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

        // Navigation
        public ICollection<Address> Addresses { get; set; }
            = new List<Address>();

        public ICollection<Order> Orders { get; set; }
            = new List<Order>();
    }
}
Address Entity

Create a class file named Address.cs within the Entities folder, and copy-paste the following code. This class stores customer address information for billing and shipping. Each address belongs to a single customer and includes fields such as street, city, state, and zip code. It also tracks whether the address is the customer’s default billing or shipping address. It is essential for checkout, delivery, and taxation.

namespace ECommerceApp.Entities
{
    public class Address
    {
        public int Id { get; set; }

        public int CustomerId { get; set; }
        public Customer Customer { get; set; } = null!;

        public string Line1 { get; set; } = null!;
        public string? Line2 { get; set; }
        public string City { get; set; } = null!;
        public string State { get; set; } = null!;
        public string Country { get; set; } = null!;
        public string ZipCode { get; set; } = null!;
        public bool IsActive { get; set; } = true;
        public bool IsDefaultShipping { get; set; }
        public bool IsDefaultBilling { get; set; }
    }
}
Order Entity

Create a class file named Order.cs within the Entities folder, and copy-paste the following code. This represents a customer’s purchase request. It stores order number, customer details, status, total amount, payment info, and address snapshots. It is the root entity of the order domain and links to OrderItems, Payment, and OrderHistory, enabling a complete transactional workflow.

using ECommerceApp.Enums;
using Microsoft.EntityFrameworkCore;

namespace ECommerceApp.Entities
{
    public class Order
    {
        public long Id { get; set; }
        public string OrderNumber { get; set; } = null!;
        public int CustomerId { get; set; }

        [Precision(18, 2)]
        public decimal TotalAmount { get; set; }
        public OrderStatus OrderStatus { get; set; } = OrderStatus.Pending;
        public DateTime OrderDate { get; set; } = DateTime.UtcNow;

        public string ShippingAddress { get; set; } = null!;
        public string BillingAddress { get; set; } = null!;

        // Navigation
        public Customer Customer { get; set; } = null!;
        public ICollection<OrderItem> OrderItems { get; set; }
            = new List<OrderItem>();
        public Payment? Payment { get; set; }
        public ICollection<OrderHistory> History { get; set; }
            = new List<OrderHistory>();
    }
}
OrderItem Entity

Create a class file named OrderItem.cs within the Entities folder, and copy-paste the following code. This class represents each product within an order. It stores product ID, quantity, final unit price, and total line price. It acts as a snapshot of the product information at the time of purchase, preserving historical pricing even if the product is later updated.

using Microsoft.EntityFrameworkCore;

namespace ECommerceApp.Entities
{
    public class OrderItem
    {
        public long Id { get; set; }

        public long OrderId { get; set; }
        public Order Order { get; set; } = null!;

        public int ProductId { get; set; }
        public Product Product { get; set; } = null!;

        public int Quantity { get; set; }
        [Precision(18, 2)]
        public decimal UnitPrice { get; set; }
        [Precision(18, 2)]
        public decimal TotalPrice { get; set; }
    }
}
Payment Entity

Create a class file named Payment.cs within the Entities folder, and copy-paste the following code. This entity represents the financial transaction related to an order. It stores the amount, payment method, payment status, and transaction identifier. It provides a direct link between the order system and the payment gateway, enabling payment verification and refund operations.

using ECommerceApp.Enums;
using Microsoft.EntityFrameworkCore;
namespace ECommerceApp.Entities
{
    public class Payment
    {
        public long Id { get; set; }

        public long OrderId { get; set; }
        public Order Order { get; set; } = null!;
        [Precision(18, 2)]
        public decimal Amount { get; set; }
        public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Pending;

        public string PaymentMethod { get; set; } = null!;
        public string? TransactionId { get; set; }
        public DateTime PaymentDate { get; set; } = DateTime.UtcNow;
    }
}
OrderHistory Entity

Create a class file named OrderHistory.cs within the Entities folder, and copy-paste the following code. This entity records the status transitions of an order. Each entry tracks the old and new statuses, remarks, and timestamps. It creates a complete audit trail of how an order progressed (e.g., Pending → PaymentReceived → Processing → Shipped → Delivered). This is important for customer transparency and administrative debugging.

using ECommerceApp.Enums;
namespace ECommerceApp.Entities
{
    public class OrderHistory
    {
        public long Id { get; set; }

        public long OrderId { get; set; }
        public Order Order { get; set; } = null!;

        public OrderStatus OldStatus { get; set; }
        public OrderStatus NewStatus { get; set; }

        public string? Remarks { get; set; }
        public DateTime ChangedAt { get; set; } = DateTime.UtcNow;
    }
}
FailureLog Entity

Create a class file named FailureLog.cs within the Entities folder, and copy-paste the following code. The FailureLog entity stores any error, exception, or failure that occurs anywhere in the application. It helps developers and support teams debug issues more easily by keeping a central history of system failures along with optional references to the related Order, User, or Product. This entity plays a crucial role in auditing, troubleshooting, and monitoring the application’s health.

namespace ECommerceApp.Entities
{
    public class FailureLog
    {
        public long Id { get; set; }

        // Optional helper references
        public long? OrderId { get; set; }
        public long? CustomerId { get; set; }
        public long? ProductId { get; set; }
        public long? PaymentId { get; set; }

        // Failure where occurred
        public string MethodName { get; set; } = string.Empty;   // E.g., "PlaceOrder", "ApplyCoupon"
        public string ClassName { get; set; } = string.Empty;  

        // Technical details
        public string ErrorMessage { get; set; } = string.Empty;    // Short description of error
        public string StackTrace { get; set; } = string.Empty;      // Full stack trace for debugging

        // Optional data for debugging / investigation
        public string? RequestPayload { get; set; }                 // JSON payload that caused failure (optional)
        public string Severity { get; set; } = "Error";             // Info / Warning / Error / Critical
        public string? CorrelationId { get; set; }                  // For tracing across services
        public string Environment { get; set; } = "Production";     // Or Development / Staging

        public DateTime LoggedAt { get; set; } = DateTime.UtcNow;
    }
}
Create the DbContext Class:

First, create a folder named Data at the project root directory. Then, create a class file named ECommerceDBContext in the Data folder and paste the following code. This class is the gateway between the e-commerce application and the SQL Server database. It defines DbSet properties for all entities and configures table mappings, relationships, enum string conversions, and seed data. This is the core data layer for the entire system.

using ECommerceApp.Entities;
using Microsoft.EntityFrameworkCore;

namespace ECommerceApp.Data
{
    public class ECommerceDBContext : DbContext
    {
        public ECommerceDBContext(DbContextOptions<ECommerceDBContext> options)
            : base(options)
        {
        }

        // ----------------- DbSet Properties -----------------

        public DbSet<Product> Products { get; set; } = null!;
        public DbSet<Category> Categories { get; set; } = null!;
        public DbSet<ProductCategory> ProductCategories { get; set; } = null!;
        public DbSet<Customer> Customers { get; set; } = null!;
        public DbSet<Address> Addresses { get; set; } = null!;
        public DbSet<Order> Orders { get; set; } = null!;
        public DbSet<OrderItem> OrderItems { get; set; } = null!;
        public DbSet<Payment> Payments { get; set; } = null!;
        public DbSet<OrderHistory> OrderHistories { get; set; } = null!;
        public DbSet<FailureLog> FailureLogs => Set<FailureLog>();

        // ----------------- Model Creation -----------------

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Order>()
                .Property(o => o.OrderStatus)
                .HasConversion<string>()
                .HasMaxLength(30);

            modelBuilder.Entity<Payment>()
                .Property(p => p.PaymentStatus)
                .HasConversion<string>()
                .HasMaxLength(30);

            modelBuilder.Entity<OrderHistory>()
                .Property(h => h.OldStatus)
                .HasConversion<string>()
                .HasMaxLength(30);

            modelBuilder.Entity<OrderHistory>()
                .Property(h => h.NewStatus)
                .HasConversion<string>()
                .HasMaxLength(30);

            ConfigureProductCategoryMapping(modelBuilder);
            SeedInitialData(modelBuilder);
        }

        private void ConfigureProductCategoryMapping(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ProductCategory>()
                .HasKey(pc => new { pc.ProductId, pc.CategoryId });

            modelBuilder.Entity<ProductCategory>()
                .HasOne(pc => pc.Product)
                .WithMany(p => p.ProductCategories)
                .HasForeignKey(pc => pc.ProductId);

            modelBuilder.Entity<ProductCategory>()
                .HasOne(pc => pc.Category)
                .WithMany(c => c.ProductCategories)
                .HasForeignKey(pc => pc.CategoryId);
        }

        private void SeedInitialData(ModelBuilder modelBuilder)
        {
            // 1. Seed Categories
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 1, Name = "Electronics", Description = "Electronic gadgets and devices", IsActive = true },
                new Category { Id = 2, Name = "Books", Description = "Books and study materials", IsActive = true },
                new Category { Id = 3, Name = "Clothing", Description = "Men and Women apparel", IsActive = true }
            );

            // 2. Seed Products
            modelBuilder.Entity<Product>().HasData(
                new Product
                {
                    Id = 1,
                    Name = "Samsung Smartphone",
                    SKU = "SM-001",
                    Description = "Latest model smartphone",
                    Price = 20000,
                    Discount = 2000,
                    FinalPrice = 18000,
                    StockQuantity = 50,
                    IsActive = true,
                    CreatedAt = new DateTime(2024, 01, 01)
                },
                new Product
                {
                    Id = 2,
                    Name = "Dell Laptop",
                    SKU = "LP-101",
                    Description = "High performance laptop",
                    Price = 60000,
                    Discount = 5000,
                    FinalPrice = 55000,
                    StockQuantity = 20,
                    IsActive = true,
                    CreatedAt = new DateTime(2024, 01, 01)
                },
                new Product
                {
                    Id = 3,
                    Name = "Python Programming Book",
                    SKU = "BK-777",
                    Description = "Beginner to Advanced Python",
                    Price = 800,
                    Discount = 100,
                    FinalPrice = 700,
                    StockQuantity = 100,
                    IsActive = true,
                    CreatedAt = new DateTime(2024, 01, 01)
                }
            );

            // 3. Seed ProductCategory (many-to-many)
            modelBuilder.Entity<ProductCategory>().HasData(
                new ProductCategory { ProductId = 1, CategoryId = 1, LinkedAt = new DateTime(2024, 01, 01) },
                new ProductCategory { ProductId = 2, CategoryId = 1, LinkedAt = new DateTime(2024, 01, 01) },
                new ProductCategory { ProductId = 3, CategoryId = 2, LinkedAt = new DateTime(2024, 01, 01) }
            );

            // 4. Seed Customers
            modelBuilder.Entity<Customer>().HasData(
                new Customer
                {
                    Id = 1,
                    CustomerNumber = "CUST-0001",
                    FirstName = "Pranaya",
                    LastName = "Rout",
                    Email = "pranaya@example.com",
                    Phone = "9999999999",
                    IsActive = true,
                    CreatedAt = new DateTime(2024, 01, 01)
                },
                new Customer
                {
                    Id = 2,
                    CustomerNumber = "CUST-0002",
                    FirstName = "John",
                    LastName = "Doe",
                    Email = "john@example.com",
                    Phone = "8888888888",
                    IsActive = true,
                    CreatedAt = new DateTime(2024, 01, 01)
                }
            );

            // 5. Seed Addresses
            modelBuilder.Entity<Address>().HasData(
                new Address
                {
                    Id = 1,
                    CustomerId = 1,
                    Line1 = "123 Main Street",
                    City = "Bhubaneswar",
                    State = "Odisha",
                    Country = "India",
                    ZipCode = "751024",
                    IsDefaultBilling = true,
                    IsDefaultShipping = true
                },
                new Address
                {
                    Id = 2,
                    CustomerId = 2,
                    Line1 = "MG Road",
                    City = "Bangalore",
                    State = "Karnataka",
                    Country = "India",
                    ZipCode = "560001",
                    IsDefaultBilling = true,
                    IsDefaultShipping = true
                }
            );
        }
    }
}

DTOs for Order Placement:

Create a folder named DTOs and add the following DTO classes.

OrderItemCreateDTO

Create a class file named OrderItemCreateDTO.cs within the DTOs folder, and copy-paste the following code. This DTO represents the product item details sent by the client when placing an order. It includes the product ID and quantity and ensures validation rules, such as minimum quantity. It prevents exposing the internal OrderItem structure to the API consumers.

using System.ComponentModel.DataAnnotations;
namespace ECommerceApp.DTOs
{
    public class OrderItemCreateDTO
    {
        [Required(ErrorMessage = "ProductId is required.")]
        public int ProductId { get; set; }

        [Required]
        [Range(1, 1000, ErrorMessage = "Quantity must be at least 1.")]
        public decimal Quantity { get; set; }
    }
}
PaymentCreateDTO

Create a class file named PaymentCreateDTO.cs within the DTOs folder, and copy-paste the following code. This DTO carries the client’s payment details, payment method, and an optional transaction ID. It ensures proper input validation before creating a Payment entity. It also allows flexibility in supporting COD, Razorpay, Stripe, or other gateways.

using System.ComponentModel.DataAnnotations;
namespace ECommerceApp.DTOs
{
    public class PaymentCreateDTO
    {
        [Required(ErrorMessage = "Payment Method is required.")]
        [StringLength(50, ErrorMessage = "Payment Method is too long.")]
        public string PaymentMethod { get; set; } = null!;   // e.g., "COD", "Razorpay", "Stripe"

        [StringLength(100, ErrorMessage = "TransactionId is too long.")]
        public string? TransactionId { get; set; }   // Optional (we will generate if not provided)
    }
}
OrderCreateDTO

Create a class file named OrderCreateDTO.cs within the DTOs folder, and copy-paste the following code. This is the complete request model for placing an order. It contains customer ID, selected shipping/billing addresses, a list of order items, and payment details. It supports a testing flag, MakeSecondSaveFail, to simulate failures. This DTO enforces clean API input for the checkout operation.

using System.ComponentModel.DataAnnotations;
namespace ECommerceApp.DTOs
{
    public class OrderCreateDTO
    {
        [Required(ErrorMessage = "CustomerId is required.")]
        public int CustomerId { get; set; }

        [Required(ErrorMessage = "ShippingAddressId is required.")]
        [Range(1, int.MaxValue, ErrorMessage = "ShippingAddressId must be a positive value.")]
        public int ShippingAddressId { get; set; }

        [Required(ErrorMessage = "BillingAddressId is required.")]
        [Range(1, int.MaxValue, ErrorMessage = "BillingAddressId must be a positive value.")]
        public int BillingAddressId { get; set; }

        [Required(ErrorMessage = "At least one order item is required.")]
        [MinLength(1, ErrorMessage = "At least one order item is required.")]
        public List<OrderItemCreateDTO> Items { get; set; } = new();

        [Required(ErrorMessage = "Payment details are required.")]
        public PaymentCreateDTO Payment { get; set; } = null!;

        // Use this only for testing implicit transaction behavior
        public bool MakeSecondSaveFail { get; set; } = false;
    }
}
OrderResponseDTO

Create a class file named OrderResponseDTO.cs within the DTOs folder, and copy-paste the following code. This DTO defines the minimal response structure returned after a successful order. It includes order ID, order number, and a success message. It ensures that sensitive internal domain details never leak into the API response.

namespace ECommerceApp.DTOs
{
    public class OrderResponseDTO
    {
        public long OrderId { get; set; }
        public string OrderNumber { get; set; } = null!;
        public string Message { get; set; } = null!;
    }
}

Configure Database Connection String

Please add the database connection string in the appsettings.json file as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}

Configure DbContext in Program.cs Class

Please modify the Program class as follows.

using ECommerceApp.Data;
using Microsoft.EntityFrameworkCore;

namespace ECommerceApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // Keep JSON property names exactly as defined in the C# models (no camelCase conversion).
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // Register ECommerceDBContext with the dependency injection container
            // and configure it to use SQL Server with the "DefaultConnection" connection string.
            builder.Services.AddDbContext<ECommerceDBContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}

Generate and Apply the Migration:

Please execute the following command in the Package Manager Console to generate the Migration and apply the Migration to sync our codebase with the database.

  • Add-Migration Mig1
  • Update-Database

It should create the following database with the required tables.

EF Core Transactions

What Are Implicit Transactions in Entity Framework Core?

In Entity Framework Core, an Implicit Transaction is a transaction that EF Core automatically creates behind the scenes every time we call SaveChanges() or SaveChangesAsync(). Developers do not manually begin, commit, or roll back these transactions; instead, EF Core takes full responsibility for ensuring that the set of operations persisted within that SaveChanges call.

Think of implicit transactions as EF Core’s built-in protection mechanism. Whenever we modify tracked entities and call SaveChanges, EF Core bundles all pending insert, update, and delete commands into one transaction. This ensures that either all changes are saved successfully or none are saved if an error occurs.

EF Core must ensure that the database adheres to the ACID properties (Atomicity, Consistency, Isolation, Durability). This is why even the simplest SaveChanges() implicitly wraps our SQL operations inside:

What Are Implicit Transactions in Entity Framework Core?

We don’t see any of this in our C# code because EF Core automatically manages it. Implicit transactions are ideal for simple operations that naturally fit into one SaveChanges call, where there is no need for manual control, partial rollbacks, or multi-step workflows involving external systems or multiple DbContexts.

Characteristics of Implicit Transactions

Let us expand each characteristic with theoretical clarity and real-world impact.

Automatically Created by EF Core

Whenever SaveChanges executes, EF Core evaluates all tracked changes and wraps them in a transaction without requiring programmer intervention. This offers the developer:

  • Simplicity
  • Reduced code repetition
  • Automatic safety

The developer focuses only on business logic, not transaction mechanics.

Automatically committed if SaveChanges succeeds.

If all database commands execute without any SQL or connection errors, EF Core commits the transaction automatically. Commit ensures:

  • The entire change set becomes permanent
  • No other transaction can undo these changes
  • The business operation is marked as successful

This “all-or-nothing” commit guarantees atomicity.

Automatically rolled back if SaveChanges fails.

If at least one SQL statement fails (Insert, Update, Delete), EF Core rolls back the entire transaction. Rollback means:

  • All changes inside the SaveChanges are discarded
  • The database remains untouched
  • The system returns to the last stable state

This is crucial for data integrity. Example rollback triggers:

  • Violated foreign key constraints
  • Duplicate keys
  • Required column missing
  • SQL timeout
  • Lost database connection

Implicit rollback prevents inconsistent or corrupted data.

One SaveChanges = One Transaction

Implicit transactions are scoped to a single SaveChanges call. This means a single call can perform multiple operations, but multiple SaveChanges calls cannot be implicitly merged into a single transaction. This is a limitation of the implicit model.

Cannot span across multiple SaveChanges calls

If you write:

  • SaveChanges(); // Transaction A
  • SaveChanges(); // Transaction B

Then you get two completely separate transactions, each committed independently. If Transaction B fails, Transaction A cannot be undone. This is why implicit transactions are unsafe for multi-step workflows.

Implementing Order Placement Using EF Core Implicit Transactions

Because EF Core uses ONE transaction per SaveChanges, we must try to complete the workflow with a single SaveChanges() call. This means:

  • We create an Order
  • Populate OrderItems
  • Update Product Stock
  • Create Payment
  • Create OrderHistory

Everything is added to the DbContext before calling SaveChanges once. This is the correct way to use Implicit Transactions.

OrdersController – Using EF Core Implicit Transactions

Create an API Empty Controller named Orders Controller within the Controllers folder, then copy and paste the following code. This controller relies on EF Core’s automatic transaction handling for each SaveChanges() call, meaning each SaveChanges () call acts like a small, independent atomic operation.

using ECommerceApp.Data;
using ECommerceApp.DTOs;
using ECommerceApp.Entities;
using ECommerceApp.Enums;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

namespace ECommerceApp.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class OrdersController : ControllerBase
    {
        private readonly ECommerceDBContext _context;

        public OrdersController(ECommerceDBContext context)
        {
            _context = context;
        }

        // PLACE ORDER USING IMPLICIT TRANSACTIONS
        [HttpPost("Place")]
        public async Task<IActionResult> PlaceOrderAsync([FromBody] OrderCreateDTO dto)
        {
            try
            {
                // STEP 1: Validate incoming request body
                if (!ModelState.IsValid)
                {
                    var errors = ModelState.Values
                        .SelectMany(v => v.Errors)
                        .Select(e => e.ErrorMessage);

                    return BadRequest(new
                    {
                        Success = false,
                        Message = "Validation failed.",
                        Details = string.Join(" | ", errors)
                    });
                }

                // STEP 2: Load Customer + Address details
                var customer = await _context.Customers
                    .Include(c => c.Addresses)
                    .FirstOrDefaultAsync(c => c.Id == dto.CustomerId && c.IsActive == true);

                if (customer == null)
                {
                    return BadRequest(new
                    {
                        Success = false,
                        Message = "Customer not found or inactive."
                    });
                }

                // Get shipping & billing addresses from customer's saved address list
                var shippingAddress = customer.Addresses.FirstOrDefault(a =>
                    a.Id == dto.ShippingAddressId && a.IsActive == true);

                var billingAddress = customer.Addresses.FirstOrDefault(a =>
                    a.Id == dto.BillingAddressId && a.IsActive == true);

                if (shippingAddress == null || billingAddress == null)
                {
                    return BadRequest(new
                    {
                        Success = false,
                        Message = "Invalid shipping or billing address."
                    });
                }

                // STEP 3: Load all required products in ONE database call
                var productIds = dto.Items.Select(i => i.ProductId).Distinct().ToList();

                var products = await _context.Products
                    .Where(p => productIds.Contains(p.Id) && p.IsActive == true)
                    .ToListAsync();

                if (products.Count != productIds.Count)
                {
                    return BadRequest(new
                    {
                        Success = false,
                        Message = $"Some products do not exist."
                    });
                }

                // STEP 4: Validate stock & prepare OrderItem list
                decimal orderTotal = 0;
                List<OrderItem> orderItems = new();

                foreach (var dtoItem in dto.Items)
                {
                    var product = products.First(p => p.Id == dtoItem.ProductId);

                    if (product.StockQuantity < dtoItem.Quantity)
                    {
                        return BadRequest(new
                        {
                            Success = false,
                            Message = $"Insufficient stock for product: {product.Name}"
                        });
                    }

                    // Calculate price for each line item
                    var lineTotal = product.FinalPrice * dtoItem.Quantity;
                    orderTotal += lineTotal;

                    orderItems.Add(new OrderItem
                    {
                        ProductId = product.Id,
                        Quantity = (int)dtoItem.Quantity,
                        UnitPrice = product.FinalPrice,
                        TotalPrice = lineTotal
                    });

                    // Reduce stock in-memory (DB update happens during SaveChanges)
                    product.StockQuantity -= (int)dtoItem.Quantity;
                }

                // STEP 5: Build Order entity
                var order = new Order
                {
                    CustomerId = customer.Id,
                    OrderNumber = Guid.NewGuid().ToString()[..8].ToUpper(),
                    OrderDate = DateTime.UtcNow,
                    TotalAmount = orderTotal,
                    OrderStatus = OrderStatus.Pending,
                    ShippingAddress =
                        $"{shippingAddress.Line1}, {shippingAddress.City}, {shippingAddress.State}, {shippingAddress.Country}, {shippingAddress.ZipCode}",
                    BillingAddress =
                        $"{billingAddress.Line1}, {billingAddress.City}, {billingAddress.State}, {billingAddress.Country}, {billingAddress.ZipCode}"
                };

                await _context.Orders.AddAsync(order);

                // Attach order items to order
                foreach (var item in orderItems)
                {
                    item.Order = order;
                    await _context.OrderItems.AddAsync(item);
                }

                // STEP 6: Create Payment entry (initially Pending)
                var payment = new Payment
                {
                    Order = order,
                    Amount = orderTotal,
                    PaymentStatus = PaymentStatus.Pending,
                    PaymentMethod = dto.Payment.PaymentMethod,
                    TransactionId = string.IsNullOrWhiteSpace(dto.Payment.TransactionId)
                        ? Guid.NewGuid().ToString()
                        : dto.Payment.TransactionId,
                    PaymentDate = DateTime.UtcNow
                };

                await _context.Payments.AddAsync(payment);

                // STEP 7: Add initial OrderHistory (Pending → Pending)
                await _context.OrderHistories.AddAsync(new OrderHistory
                {
                    Order = order,
                    OldStatus = OrderStatus.Pending,
                    NewStatus = OrderStatus.Pending,
                    Remarks = "Order created successfully"
                });

                // STEP 8: FIRST SaveChanges() → IMPLICIT TRANSACTION #1
                await _context.SaveChangesAsync();

                // STEP 9: Update payment + order status after payment confirmation
                var oldOrderStatus = order.OrderStatus;

                payment.PaymentStatus = PaymentStatus.Paid;
                order.OrderStatus = OrderStatus.PaymentReceived;

                await _context.OrderHistories.AddAsync(new OrderHistory
                {
                    OrderId = order.Id,
                    OldStatus = oldOrderStatus,
                    NewStatus = OrderStatus.PaymentReceived,
                    Remarks = "Payment verified successfully"
                });

                // STEP 10: Teaching scenario — simulate failure
                if (dto.MakeSecondSaveFail)
                {
                    throw new Exception("Simulated failure after first SaveChanges.");
                }

                // STEP 11: SECOND SaveChanges() → IMPLICIT TRANSACTION #2
                await _context.SaveChangesAsync();

                // FINAL SUCCESS RESPONSE
                return Ok(new OrderResponseDTO
                {
                    OrderId = order.Id,
                    OrderNumber = order.OrderNumber,
                    Message = "Order placed successfully using Implicit Transaction."
                });
            }
            catch (Exception ex)
            {
                // STEP 12: Log failure details into database
                try
                {
                    var logEntry = new FailureLog
                    {
                        OrderId = null,
                        MethodName = "PlaceOrderAsync",
                        ClassName = "OrdersController",
                        ErrorMessage = ex.Message,
                        StackTrace = ex.StackTrace ?? "",
                        RequestPayload = JsonSerializer.Serialize(dto),
                        Severity = "Error",
                        CustomerId = dto.CustomerId
                    };

                    _context.FailureLogs.Add(logEntry);
                    await _context.SaveChangesAsync();
                }
                catch
                {
                    // If logging fails we never throw — avoid recursive exceptions
                }

                // STEP 13: Send error response to client
                return StatusCode(500, new
                {
                    Success = false,
                    Message = "An unexpected error occurred while placing the order.",
                    Details = ex.Message
                });
            }
        }
    }
}
Testing the Order Placed Flow:
Request Body for Success Scenario
{
  "CustomerId": 1,
  "ShippingAddressId": 1,
  "BillingAddressId": 1,
  "MakeSecondSaveFail": false,
  "Items": [
    {
      "ProductId": 1,
      "Quantity": 2
    }
  ],
  "Payment": {
    "PaymentMethod": "Razorpay",
    "TransactionId": "PAY_SUCCESS_1001"
  }
}
Expected:
  • First SaveChanges → Success
  • Second SaveChanges → Success
  • Order created
  • Order status updated
  • Both histories inserted
  • Response returns success
Response in Swagger:

Using EF Core Implicit Transactions

Request Body for Failure Scenario
{
  "CustomerId": 1,
  "ShippingAddressId": 1,
  "BillingAddressId": 1,
  "MakeSecondSaveFail": true,
  "Items": [
    {
      "ProductId": 1,
      "Quantity": 1
    }
  ],
  "Payment": {
    "PaymentMethod": "Razorpay",
    "TransactionId": "PAY_FAIL_2001"
  }
}
Expected:
  • First SaveChanges → Success (Order, OrderItems, Payment, History persisted)
  • Exception thrown before second SaveChanges()
  • Order.Status stays Pending
  • NO “PaymentReceived” history written
  • Response returns failure
  • Partial changes persisted → Demonstrating implicit transaction behaviour
Response in Swagger:

Using EF Core Implicit Transactions

When to Use EF Core Implicit Transactions?

Implicit transactions are suitable when your operation is simple, self-contained, and safe to run within a single SaveChanges. Use implicit transactions when:

Your entire business operation fits inside one SaveChanges call

If you can perform all required changes in memory and persist them in a single transaction, implicit transactions are ideal. Examples:

  • Registering a new user (insert one record)
  • Adding a product to the catalog
  • Updating inventory count
  • Deleting a single review

All changes are sent to the database at once and kept atomic automatically.

You do NOT need manual control

If you do NOT need:

  • to decide when to commit
  • to decide when to roll back
  • to coordinate multi-step workflows
  • to combine SaveChanges across multiple scopes

…then implicit transactions are sufficient.

Performance is essential, and operations are simple

Implicit transactions:

  • Create fewer transaction scopes
  • Avoid multiple round-trip
  • Execute faster
  • Reduce overhead

For performance-sensitive, non-critical, one-step operations, implicit transactions are the most efficient option.

When Not to Use Implicit Transactions

Implicit transactions are not suitable for scenarios like:

  • Order processing workflows
  • Payment + order + inventory deduction
  • Multi-SaveChanges operations
  • Multi-table workflow requiring consistency
  • External API calls that must be atomically combined with DB updates

For those, we need to use Explicit Transactions.

What Are Explicit Transactions in EF Core?

Explicit Transactions in EF Core represent a developer-controlled transactional boundary that wraps one or more database operations (and often one or more SaveChanges calls) into a single logical atomic unit. Unlike implicit transactions, where EF Core automatically handles the transaction lifecycle, explicit transactions require you, the developer, to explicitly declare:

  • When the transaction begins
  • Which operations belong inside the boundary
  • When the transaction should be committed
  • When it should be rolled back
  • What additional logic (including external non-DB operations) must be included within the atomic unit

This level of control is essential whenever your workflow involves multiple sequential steps, external system interactions, multi-table consistency requirements, or scenarios where failure at any point must undo the entire sequence.

Why Do Explicit Transactions Matter?

Explicit transactions allow us to construct a transactional workflow that mirrors the real-world business operation. A complete order placement workflow includes

  1. Writing Order
  2. Writing OrderItems
  3. Deducting Inventory
  4. Writing Payment
  5. Confirming Payment
  6. Updating Order Status
  7. Writing OrderHistory

If any step fails:

  • Inventory must be restored
  • The order must not exist
  • The payment must not be captured
  • The system must remain fully consistent

Implicit transactions cannot coordinate all these sequential steps across multiple SaveChanges calls, which is why Explicit Transactions become essential.

The Syntax and Its Meaning
Begin Transaction

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

This creates a new database transaction. From this moment on, every EF Core SQL operation participates in this transaction until commit or rollback.

Commit Transaction

await transaction.CommitAsync();

Commit means:

  • All accumulated changes become permanent
  • The database transitions from an intermediate state to a durable state
  • Locks are released
  • No rollback is possible after commit

Business-wise, this means: The transaction is successful.

Rollback Transaction

await transaction.RollbackAsync();

Rollback means:

  • Undo all changes since the transaction started
  • Restore the database to the state it was in before the transaction
  • Ensure no partial changes corrupt business logic

Business-wise, this means: The transaction failed safely.

OrdersController – Explicit Transaction using EF Core

Please modify the OrdersController as follows. This controller manually controls the transaction so that both SaveChanges calls belong to the same atomic operation. If any step fails, the entire order workflow rolls back completely.

using ECommerceApp.Data;
using ECommerceApp.DTOs;
using ECommerceApp.Entities;
using ECommerceApp.Enums;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

namespace ECommerceApp.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class OrdersController : ControllerBase
    {
        private readonly ECommerceDBContext _context;

        private readonly IServiceScopeFactory _scopeFactory;

        public OrdersController(ECommerceDBContext context, IServiceScopeFactory scopeFactory)
        {
            _context = context;
            _scopeFactory = scopeFactory;
        }

        // PLACE ORDER USING EXPLICIT TRANSACTION
        [HttpPost("Place")]
        public async Task<IActionResult> PlaceOrderAsync([FromBody] OrderCreateDTO dto)
        {
            // STEP 1: Validate input model before starting any DB operation
            if (!ModelState.IsValid)
            {
                var errorMessages = ModelState.Values
                    .SelectMany(v => v.Errors)
                    .Select(e => e.ErrorMessage);

                return BadRequest(new
                {
                    Success = false,
                    Message = "Validation failed.",
                    Details = string.Join(" | ", errorMessages)
                });
            }

            // STEP 2: Begin explicit transaction so all steps behave as ONE atomic unit
            using var transaction = await _context.Database.BeginTransactionAsync();

            try
            {
                // STEP 3: Load customer and their address details
                var customer = await _context.Customers
                    .Include(c => c.Addresses)
                    .FirstOrDefaultAsync(c => c.Id == dto.CustomerId && c.IsActive);

                if (customer == null)
                {
                    return BadRequest(new
                    {
                        Success = false,
                        Message = "Customer not found or inactive."
                    });
                }

                // STEP 4: Validate shipping & billing addresses
                var shippingAddress = customer.Addresses
                    .FirstOrDefault(a => a.Id == dto.ShippingAddressId && a.IsActive);

                var billingAddress = customer.Addresses
                    .FirstOrDefault(a => a.Id == dto.BillingAddressId && a.IsActive);

                if (shippingAddress == null || billingAddress == null)
                {
                    return BadRequest(new
                    {
                        Success = false,
                        Message = "Invalid shipping or billing address."
                    });
                }

                // STEP 5: Load all requested products in a single DB call
                var productIds = dto.Items.Select(i => i.ProductId).Distinct().ToList();

                var products = await _context.Products
                    .Where(p => productIds.Contains(p.Id) && p.IsActive)
                    .ToListAsync();

                if (products.Count != productIds.Count)
                {
                    return BadRequest(new
                    {
                        Success = false,
                        Message = "Invalid Products"
                    });
                }

                // STEP 6: Validate stock & build OrderItems list
                decimal orderTotal = 0;
                List<OrderItem> orderItems = new();

                foreach (var dtoItem in dto.Items)
                {
                    var product = products.First(p => p.Id == dtoItem.ProductId);

                    if (product.StockQuantity < dtoItem.Quantity)
                    {
                        return BadRequest(new
                        {
                            Success = false,
                            Message = $"Insufficient stock for product: {product.Name}"
                        });
                    }

                    var lineTotal = product.FinalPrice * dtoItem.Quantity;
                    orderTotal += lineTotal;

                    orderItems.Add(new OrderItem
                    {
                        ProductId = product.Id,
                        Quantity = (int)dtoItem.Quantity,
                        UnitPrice = product.FinalPrice,
                        TotalPrice = lineTotal
                    });

                    // Reduce inventory (actual DB update happens during SaveChanges)
                    product.StockQuantity -= (int)dtoItem.Quantity;
                }

                // STEP 7: Create Order with initial status = Pending
                var order = new Order
                {
                    CustomerId = customer.Id,
                    OrderNumber = Guid.NewGuid().ToString()[..8].ToUpper(),
                    TotalAmount = orderTotal,
                    OrderDate = DateTime.UtcNow,
                    OrderStatus = OrderStatus.Pending,
                    ShippingAddress =
                        $"{shippingAddress.Line1}, {shippingAddress.City}, {shippingAddress.State}, {shippingAddress.Country}, {shippingAddress.ZipCode}",
                    BillingAddress =
                        $"{billingAddress.Line1}, {billingAddress.City}, {billingAddress.State}, {billingAddress.Country}, {billingAddress.ZipCode}"
                };

                await _context.Orders.AddAsync(order);

                // Attach OrderItems to Order
                foreach (var item in orderItems)
                {
                    item.Order = order;
                    await _context.OrderItems.AddAsync(item);
                }

                // STEP 8: Create Payment with initial status = Pending
                var payment = new Payment
                {
                    Order = order,
                    Amount = orderTotal,
                    PaymentStatus = PaymentStatus.Pending,
                    PaymentMethod = dto.Payment.PaymentMethod,
                    TransactionId = string.IsNullOrWhiteSpace(dto.Payment.TransactionId)
                        ? Guid.NewGuid().ToString()
                        : dto.Payment.TransactionId,
                    PaymentDate = DateTime.UtcNow
                };

                await _context.Payments.AddAsync(payment);

                // STEP 9: Insert first OrderHistory entry
                await _context.OrderHistories.AddAsync(new OrderHistory
                {
                    Order = order,
                    OldStatus = OrderStatus.Pending,
                    NewStatus = OrderStatus.Pending,
                    Remarks = "Order created successfully"
                });

                // FIRST SAVE (Order + Items + Payment + History)
                await _context.SaveChangesAsync();

                // STEP 10: Simulated payment success for teaching purpose
                var oldOrderStatus = order.OrderStatus;

                payment.PaymentStatus = PaymentStatus.Paid;
                order.OrderStatus = OrderStatus.PaymentReceived;

                await _context.OrderHistories.AddAsync(new OrderHistory
                {
                    OrderId = order.Id,
                    OldStatus = oldOrderStatus,
                    NewStatus = OrderStatus.PaymentReceived,
                    Remarks = "Payment verified successfully"
                });

                // Simulated Failure
                if (dto.MakeSecondSaveFail)
                {
                    throw new Exception("Simulated failure occurred before second SaveChanges.");
                }

                // SECOND SAVE (status updates)
                await _context.SaveChangesAsync();

                // STEP 11: Commit the entire explicit transaction
                await transaction.CommitAsync();

                return Ok(new OrderResponseDTO
                {
                    OrderId = order.Id,
                    OrderNumber = order.OrderNumber,
                    Message = "Order placed successfully using Explicit Transaction."
                });
            }
            catch (Exception ex)
            {
                // STEP 12: Rollback entire workflow on ANY exception
                await transaction.RollbackAsync();

                // Dispose transaction before using another DbContext
                await transaction.DisposeAsync();

                // STEP 13: Log the exception into FailureLog table
                try
                {
                    using var scope = _scopeFactory.CreateScope();
                    var loggingContext = scope.ServiceProvider.GetRequiredService<ECommerceDBContext>();

                    var logEntry = new FailureLog
                    {
                        OrderId = null,                 // Order may not exist
                        CustomerId = dto.CustomerId,    // Helps investigation
                        ClassName = "OrdersController",
                        MethodName = "PlaceOrderAsync",
                        ErrorMessage = ex.Message,
                        StackTrace = ex.StackTrace ?? "",
                        RequestPayload = JsonSerializer.Serialize(dto),
                        Severity = "Error",
                        Environment = "Production"
                    };

                    loggingContext.FailureLogs.Add(logEntry);
                    await loggingContext.SaveChangesAsync();
                }
                catch
                {
                    // Never throw again — logging failures should never break API
                }

                return StatusCode(500, new
                {
                    Success = false,
                    Message = "Transaction failed.",
                    Details = ex.Message
                });
            }
        }
    }
}
How Explicit Transactions Work Here

Both SaveChanges() calls belong to ONE transaction because we wrapped them inside:

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

If ANY step fails → entire workflow rolls back

Meaning:

  • Order will NOT be created
  • The payment record will NOT exist
  • Stock will NOT reduce
  • History will NOT persist

Ensures full atomicity

  • This is exactly how Amazon, Flipkart, Ajio, and Meesho ensure consistency.
Example Request Bodies
Success Scenario
{
  "CustomerId": 1,
  "ShippingAddressId": 1,
  "BillingAddressId": 1,
  "MakeSecondSaveFail": false,
  "Items": [
    {
      "ProductId": 1,
      "Quantity": 1
    }
  ],
  "Payment": {
    "PaymentMethod": "Razorpay",
    "TransactionId": "TXN12345"
  }
}
Failure Scenario (Rollback)
{
  "CustomerId": 1,
  "ShippingAddressId": 1,
  "BillingAddressId": 1,
  "MakeSecondSaveFail": true,
  "Items": [
    {
      "ProductId": 1,
      "Quantity": 1
    }
  ],
  "Payment": {
    "PaymentMethod": "Razorpay",
    "TransactionId": "TXN54321"
  }
}
Expected Result:
  • The entire transaction is rolled back
  • No order, no items, no payment, no history is saved
  • You get a clean JSON error response

Transactions in EF Core ensure that all data we save to the database is saved safely and consistently. Whether we rely on implicit transactions for simple operations or use explicit transactions for multi-step workflows, the goal is the same: to prevent partial updates and protect our data. By grouping related actions into a single unit of work, transactions help maintain reliability and ensure our application behaves correctly even when something goes wrong.

Leave a Reply

Your email address will not be published. Required fields are marked *