In EF/Core, DbSet implements repository pattern. Repositories can centralize data access for applications, and connect between the data source and the business logic. A DbSet instance can be mapped to a database table, which is a repository for data CRUD (create, read, update and delete):
1
namespaceMicrosoft.EntityFrameworkCore
2
{
3
publicabstractclassDbSet<TEntity> : IQueryable<TEntity> // Other interfaces.
DbSet implements IQueryable, so that DbSet can represent the data source to read from. DbSet.Find is also provided to read entity by the primary keys. After reading, the retrieved data can be changed. Add and AddRange methods track the specified entities as to be created in the repository. Remove and RemoveRange methods track the specified entities as to be deleted in the repository.
As fore mentioned, a unit of work is a collection of data operations that should together or fail together as a unit. DbContext implements unit of work pattern:
As the mapping of database, DbContext’s Set method returns the specified entity’s repositories. For example, calling AdventureWorks.Products is equivalent to calling AdventureWorks.Set. The entities tracking is done at the DbContext level, by its ChangeTracker. When DbContext.Submit is called, the tracked changes are submitted to database. When a unit of work is done, DbContext should be disposed.
In EF, the members of DbSet and DbContext have slightly different signatures:
1
namespaceSystem.Data.Entity
2
{
3
publicclassDbSet<TEntity> : DbQuery<TEntity>, IQueryable<TEntity> // Other interfaces.
DbContext.ChangeTracker property returns Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker, which can track entities for the source DbContext:
Each entity’s loading and tracking information is represented by Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry or Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry. The following is the non generic EntityEntry:
Besides the loading information APIs discussed in previous part, EntityEntry also provides rich APIs for entity’s tracking information and state management:
State returns the entity’s tracking state: Detached, Unchanged, Added, Deleted, or Modified.
Entity property returns the tracked entity
Property returns the specified property’s tracking information.
CurrentValues returns the tracked entity’s current property values.
OriginalValues returns the tracked entity’s original property values
GetDatabaseValues instantly execute a SQL query to read entity’s property values from database, without updating current entity’s property values and tracking information.
Reload also executes a SQL query to read the database values, and also update current entity’s property values, and all tracking information
publicclassEntityEntry<TEntity> : EntityEntrywhereTEntity : class
4
{
5
publicvirtualTEntityEntity { get; }
6
7
// Other members.
8
}
9
}
As fore mentioned in data loading part, DbContext.Entry also accepts an entity and return its EntityEntry/EntityEntry.
In EF, the types involved above are named with Db prefix: DbChangeTracker, DbEntityEntry, DbEntityEntry, DbPropertyEntry, DbPropertyValues, with similar members.
The single result from the first LINQ to Entities query is tracked by DbContext. Later, the second query has a single result too. EF/Core identifies both results map to the same data row of the same table, so they are reference to the same entity instance.
If data from repositories are not entities mapping to table rows, they cannot be tracked:
Here data is queries from repositories, and anonymous type instances are constructed on the fly. EF/Core cannot decide if 2 arbitrary instances semantically represent the same piece of data in remote database. This time 2 query results are independent from each other.
Since the tracking is at DbContext scope. Entities of different DbContext instances belong to different units of work, and do not interfere each other:
If an entity is not read from a DbContext instance’s repositories, then it has nothing to do with that unit of work, and apparently is not tracked by that DbContext instance. DbSet provides an Attach method to place an entity to the repository, and the DbContext tracks the entity as the Unchanged state:
The relationship of entities is also tracked. Remember Product’s foreign key ProductSubcategoryID is nullable. The following example reads a subcategory and its products, then delete the relationship. As a result, each navigation property is cleared to empty collection or null. And each related subcategory’s foreign key property value is synced to null, which is tracked:
DbContext’s default behavior is to track all changes automatically. This can be turned off if not needed. To disable tracking for specific entities queried from repository, call the EntityFrameworkQueryableExtensions.AsNoTracking extension method for IQueryable query:
Tracking can also be enabled or disabled at the DbContext scope, by setting the ChangeTracker.AutoDetectChangesEnabled property to true or false. The default value of ChangeTracker.AutoDetectChangesEnabled is true, so usually it is not needed to manually detect changes by calling ChangeTracker.DetectChanges method. The changes are automatically detected when DbContext.SubmitChanges is called. The changes are also automatically detected when tracking information is calculated, for example, when calling ChangeTracker.Entries, DbContext.Entry, etc.
In EF, the switch is DbContext.Configuration.AutoDetectChangesEnabled. And when AutoDetectChangesEnabled is true (by default), DetectChanges is called much more frequently than in EF Core.
If needed, changes and be manually tracked by calling ChangeTracker.DetectChanges method:
To change the data in the database, just create a DbContext instance, change the data in its repositories, and call DbContext.SaveChanges method to submit the tracked changes to the remote database as a unit of work.
To create new entities into the repository, call DbSet.Add or DbSet.AddRange. The following example creates a new category, and a new related subcategory, and add to repositories:
1
internal staticpartial class Changes
2
{
3
internal static ProductCategory Create()
4
{
5
using (AdventureWorks adventureWorks = new AdventureWorks())
6
{
7
ProductCategory category = new ProductCategory() { Name="Create" };
8
ProductSubcategory subcategory = new ProductSubcategory() { Name="Create" };
9
category.ProductSubcategories= new HashSet<ProductSubcategory>() { subcategory };
10
// Equivalent to: subcategory.ProductCategory= category;
Here DbSet.Add is called only once with 1 subcategory entity. Internally, Add triggers change detection, and tracks this subcategory as Added state. Since this subcategory is related with another category entity with navigation property, the related category is also tracked, as the Added state too. So in total there are 2 entity changes tracked. When DbContext.SaveChanges is called, EF/Core translates these 2 changes to 2 SQL INSERT statements:
The category’s key is identity key, with value generated by database, so is subcategory. So in the translated INSERT statements, the new category’s ProductCategoryID and the new subcategory’s ProductSubcategory are ignored. After the each new row is created, a SELECT statement calls SCOPE_IDENTITY metadata function to read the last generated identity value, which is the primary key of the inserted row. As a result, since there are 2 row changes in total, SaveChanges returns 2, And the 2 changes are submitted in a transaction, so that all changes can succeed or fail as a unit.
DbSet.AddRange can be called with multiple entities. AddRange only triggers change detection once for all the entities, so it can have better performance than multiple Add calls,
To update entities in the repositories, just change their properties, including navigation properties. The following example updates a subcategory entity’s name, and related category entity, which is translated to UPDATE statement:
1
internal static void Update(int categoryId, int subcategoryId)
2
{
3
using (AdventureWorks adventureWorks = new AdventureWorks())
The above example first call Find to read the entities with a SELECT query, then execute the UPDATE statement. Here the row to update is located by primary key, so, if the primary key is known, then it can be used directly:
Here a category entity is constructed on the fly, with specified primary key and updated Name. To track and save the changes, ii is attached to the repository. As fore mentioned, the attached entity is tracked as Unchanged state, so just manually set its state to Modified. This time, only one UPDATE statement is translated and executed, without SELECT.
When there is no change to save, SaveChanges does not translate or execute any SQL and returns 0:
1
internalstaticvoidSaveNoChanges(intcategoryId)
2
{
3
using (AdventureWorksadventureWorks=newAdventureWorks())
Here the cascade deletion are translated and executed in the right order. The subcategory is deleted first, then category is deleted.
In EF, untracked entities’ changes cannot to be translated or executed. The following example tries to delete a untracked entity from the repository, it throws InvalidOperationException:
1
internalstaticvoidUntrackedChanges()
2
{
3
using (AdventureWorksadventureWorks=newAdventureWorks())
As discussed above, by default DbContext.SaveChanges execute all data creation, update and deletion in a transaction, so that all the work can succeed or fail as a unit. If the unit of work succeeds, the transaction is committed, if any operation fails, the transaction is rolled back. EF/Core also supports custom transactions.
Transaction with connection resiliency and execution strategy#
If the retry strategy is enabled for connection resiliency for DbContext by default, then this default retry strategy does not work custom transaction. Custom transaction works within a single retry operation, but not cross multiple retries. In EF Core, database façade’s CreateExecutionStrategy method can be called to explicitly specify a single retry operation:
// Single retry operation, which can have custom transactions.
8
});
9
}
10
}
In EF, the default retry strategy must be manually disabled, so that an individual retry logic must be manually created to start a single retry operation. In the object-relational mapping part, an ExecutionStrategy type is defined to turn on/off the default retry strategy. It can be reused to implement this:
EF Core provides Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction to represent a transaction. It can be created by DbContext.Database.BeginTransaction, where the transaction’s isolation level can be optionally specified. The following example executes a entity change and custom SQL with one EF/Core transaction:
EF/Core transaction wraps ADO.NET transaction. When the EF/Core transaction begins, The specified isolation level is written to a packet (represented by System.Data.SqlClient.SNIPacket type), and sent to SQL database via TDS protocol. There is no SQL statement like SET TRANSACTION ISOLATION LEVEL executed, so the actual isolation level cannot be logged by EF/Core, or traced by SQL Profiler. In above example, CurrentIsolationLevel is called to verify the current transaction’s isolation level. It is an extension method of DbContext. It queries the dynamic management view sys.dm_exec_sessions with current session id, which can be retrieved with @@SPID function:
1
public staticpartial class DbContextExtensions
2
{
3
public staticreadonly string CurrentIsolationLevelSql = $@"
4
SELECT
5
CASE transaction_isolation_level
6
WHEN 0 THEN N'{IsolationLevel.Unspecified}'
7
WHEN 1 THEN N'{IsolationLevel.ReadUncommitted}''
8
WHEN 2 THEN N'{IsolationLevel.ReadCommitted}''
9
WHEN 3 THEN N'{IsolationLevel.RepeatableRead}''
10
WHEN 4 THEN N'{IsolationLevel.Serializable}''
11
WHEN 5 THEN N'{IsolationLevel.Snapshot}''
12
END
13
FROM sys.dm_exec_sessions
14
WHERE session_id = @@SPID";
15
16
public static string CurrentIsolationLevel(this DbContext context)
17
{
18
using (DbCommand command =context.Database.GetDbConnection().CreateCommand())
When DbContext.SaveChanges is called to create entity. it detects a transaction is explicitly created with the current DbContext, so it uses that transaction and does not automatically begins a new transaction like all the previous examples. Then DbContext.Database.ExecuteSqlCommnd is called to delete entity. It also detects and uses transaction of the current DbContext. Eventually, to commit the transaction, call IDbContextTransaction.Commit, to rollback the transaction, call IDbContextTransaction.Rollback
In EF has built-in support to execute custom SQL with result of primitive type, so CurrentIsolationLevel can be implemented as:/p>
EF/Core can also use the ADO.NET transaction, represented by System.Data.Common.DbTransaction. The following example execute the same entity change and custom SQL command with one ADO.NET transaction. To use an existing ADO.NET transaction, call DbContext.Database.UseTransaction:
1
internal static void DbTransaction()
2
{
3
using (DbConnection connection= new SqlConnection(ConnectionStrings.AdventureWorks))
4
{
5
connection.Open();
6
using (DbTransaction transaction=connection.BeginTransaction(IsolationLevel.RepeatableRead))
7
{
8
try
9
{
10
using (AdventureWorks adventureWorks = new AdventureWorks(connection))
The EF transaction only work with its source DbContext, and the ADO.NET transaction only work with its source DbConnection. Since EF work with .NET Framework, where System.Transactions.TransactionScope is provided, TransactionScope can be used with EF to have a transaction that work across the lifecycle of multiple DbContext or DbConnection instances:
1
internal static void TransactionScope()
2
{
3
new ExecutionStrategy().Execute(() =>
4
{
5
using (TransactionScope scope = new TransactionScope(