In this lesson, you will learn how to maintain data consistency when your LINQ queries run against live data sources that are subject to simultaneous updates.
The Challenge of Live Data
When you use IQueryable to fetch data from a database, your LINQ query translates into a SQL statement that runs at a specific moment in time. If another process modifies the database after your query starts but before it completes, or if you perform multiple steps that assume the data hasn't changed, you run into "Dirty Reads" or "Non-repeatable reads."
For example, imagine a banking app:
- You fetch an account balance:
var balance = db.Accounts.Where(a => a.Id == 1).Select(a => a.Balance).FirstOrDefault(); - You perform some logic based on that balance.
- You save the new balance.
If the balance was updated by another thread between steps 1 and 3, your application might perform an calculation based on stale data, leading to a race condition.
Strategies for Consistency
To handle these issues, you must define how "isolated" your read needs to be. In LINQ-to-Entities, we rely on Transaction Isolation Levels. By wrapping your multi-step LINQ execution in a TransactionScope, you tell the database to lock rows or use snapshots to ensure consistency:
using (var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
{
var account = db.Accounts.Single(a => a.Id == 1);
// Because of RepeatableRead, the database ensures this
// data cannot be modified by others until this scope completes.
account.Balance += 100;
db.SaveChanges();
scope.Complete();
}
Preventing Dirty Reads
A "Dirty Read" occurs if you read data that has been modified but not yet committed by another transaction. To prevent this, you ensure your isolation level is at least ReadCommitted. If you need absolute certainty that the data won't change while you are looking at it, RepeatableRead or Serializable are the standards.
Remember: higher isolation levels provide better consistency but reduce concurrency by forcing other processes to wait.
Key Takeaway: Use transaction isolation levels to wrap your multi-step LINQ queries, ensuring that the data you read remains consistent and valid throughout the entire life of your operation.