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
DbUpdateConcurrencyExceptionmay 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
Entrymethod.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:
Attachassumes the ID exists in the database. If the ID is missing,SaveChangesAsyncwill throw aDbUpdateConcurrencyException. - Risk of Uninitialized Properties: When using
EntityState.Modified(full update) instead of theAttachpattern (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
DbContextinstance is already tracking an entity with the same ID, callingAttachwill throw an exception. Use a short-lived context or callChangeTracker.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.
