[C#] Updating Data Without Loading from the Database (Attach / Entry) in EF Core

目次

Overview

This is a performance-oriented implementation pattern in Entity Framework Core (EF Core) that allows you to perform an UPDATE without first executing a SELECT query. If the primary key (ID) is already known, you can use the Attach method to track the entity and update only specific properties. This reduces the number of round-trips to the database.

Specifications (Input/Output)

  • Input: The ID of the record to update and the new value (e.g., a new email address).
  • Output: Completion of the update process.
  • Prerequisites: .NET 6.0 or higher, Microsoft.EntityFrameworkCore.InMemory (for testing).
  • Behavior: Updates the record if the ID exists. If the ID does not exist, a DbUpdateConcurrencyException may occur.

Basic Usage

Create a new object, set its ID, connect it to the context using Attach, and then change the desired values.

// 1. Create a "stub" object and set the ID
var stub = new Member { Id = targetId };

// 2. Attach to the context (starts tracking)
// At this point, the state is "Unchanged"
context.Members.Attach(stub);

// 3. Change properties
// Only modified properties are marked as "Modified"
stub.Email = "new_address@example.com";

// 4. Save (only the changed columns are included in the UPDATE SQL)
await context.SaveChangesAsync();

Full Code Example

The following console application updates a member’s email address by ID without loading the record from the database. You will need the Microsoft.EntityFrameworkCore.InMemory package.

dotnet add package Microsoft.EntityFrameworkCore.InMemory
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

public class Program
{
    public static async Task Main()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase("MemberDb")
            .Options;

        // --- 1. Prepare Initial Data ---
        using (var context = new AppDbContext(options))
        {
            if (!await context.Members.AnyAsync())
            {
                context.Members.Add(new Member 
                { 
                    Id = 101, 
                    Name = "Alice", 
                    Email = "alice@old.com", 
                    Points = 500 
                });
                await context.SaveChangesAsync();
            }
        }

        // --- 2. Execute Update Without Loading ---
        using (var context = new AppDbContext(options))
        {
            var worker = new DisconnectedUpdater(context);
            
            // Change only the email for ID: 101
            // Name and Points are not retrieved, so they remain unchanged in the DB
            await worker.UpdateEmailWithoutLoadingAsync(101, "alice@new-domain.com");
        }

        // --- 3. Verify the Result ---
        using (var context = new AppDbContext(options))
        {
            var member = await context.Members.FindAsync(101);
            Console.WriteLine($"[Verify] ID: {member.Id}");
            Console.WriteLine($"       Name: {member.Name}");
            Console.WriteLine($"       Email: {member.Email}"); // This should be changed
            Console.WriteLine($"       Points: {member.Points}");
        }
    }
}

// Business Logic Class
public class DisconnectedUpdater
{
    private readonly AppDbContext _context;

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

    public async Task UpdateEmailWithoutLoadingAsync(int memberId, string newEmail)
    {
        // No SELECT query is sent to the database
        // Create a "stub" object representing the record
        var stub = new Member 
        { 
            Id = memberId 
        };

        try
        {
            // Start tracking with the Attach method
            // EF Core treats this as an existing record in the DB
            _context.Members.Attach(stub);

            // Change the property
            // Only this change will be included in the SET clause of the UPDATE statement
            stub.Email = newEmail;

            // Save changes
            await _context.SaveChangesAsync();
            
            Console.WriteLine($"Successfully updated ID {memberId}.");
        }
        catch (DbUpdateConcurrencyException)
        {
            // Occurs if the specified ID does not exist in the DB
            Console.WriteLine($"Error: ID {memberId} does not exist or has been deleted.");
        }
    }
}

// Entity Definition
public class Member
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public int Points { get; set; }
}

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

    public DbSet<Member> Members => Set<Member>();
}

Example Output

Successfully updated ID 101.
[Verify] ID: 101
       Name: Alice
       Email: alice@new-domain.com
       Points: 500

Customization Points

  • Updating All Columns: If you want to mark the entire object as changed rather than just specific properties, use the Entry method.C#var entity = new Member { Id = 1, Name = "New Name", Email = "New Email" ... }; // Marks all properties for update (Note: unset properties will be overwritten with null or defaults) context.Entry(entity).State = EntityState.Modified;
  • Optimistic Concurrency: If you have a row version (Timestamp) column, you can set that version on the stub object to enable conflict detection.

Important Notes

  • Data Existence: Attach assumes the ID exists in the database. If the ID is missing, SaveChangesAsync will throw a DbUpdateConcurrencyException.
  • Risk of Uninitialized Properties: When using EntityState.Modified (full update) instead of the Attach pattern (partial update), any property not set in the object (null or 0) will be saved to the database. Be careful not to lose data accidentally.
  • Context Duplication: If the same DbContext instance is already tracking an entity with the same ID, calling Attach will throw an exception. Use a short-lived context or call ChangeTracker.Clear() to reset tracking.

Advanced Application

If you are using EF Core 7.0 or later, the ExecuteUpdateAsync method is the most modern and fastest approach. It issues a SQL command directly without any object tracking.

// EF Core 7.0+ (Fastest approach)
await context.Members
    .Where(m => m.Id == 101)
    .ExecuteUpdateAsync(s => s
        .SetProperty(m => m.Email, "alice@new-domain.com"));

Conclusion

Using Attach allows you to skip the SELECT query and issue only an UPDATE command efficiently. This is very useful in scenarios like Web API update operations where you only have the ID and the values you want to change. Choose between a full update (State = Modified) and a partial update (Attach + property change) to prevent accidental data overwrites.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次