Attaching detached POCO to EF DbContext - simple and fast

Introduction


Recently I was playing around with Entity Framework (EF) and evaluating it
for some projects. I had a very hard time figuring out how to attach detached
object graph to DBContext in fast and reliable way. Here I am
sharing simple AttachByIdValue() method implementation that can do
this for you. If you are not interested in full explanation of the problem jump
straight to method implementation
and start attaching your objects.

The Problem

Let’s say we are using EF in web app to implement page for managing Order and
Order Lines. So we have parent-child relation (Order and Order Lines) and some
referential data that is displayed but won’t be updated (Customer and
Products).

We would typically query above object graph from database (DB) using EF and
send it to client (browser). When client sends this object graph back to server
we would like to persist it and in order to do so we must first attach it to
DbContext.

The question is how to attach this detached graph without reloading
it form DB and applying changes
. Reloading form DB is performance hit
and it is invasive. If I couldn’t do it without reloading I would discard EF
because this is very basic task that I expect my ORM to solve easily. Luckily I
found the solution after lot of digging.

Add() or Attach()

There are two methods for attaching detached objects, Add()
and Attach(), and they receive graph root object (Order).
Add() method attaches all objects in graph and
marks them as Added, while Attach() also
attaches all objects in graph but marks them as
Unchanged
.

Since our object group will usually have new, modified and unchanged data our
only option is to use one of these two methods to attach the full graph and then
traverse the graph and correct state of each entry.

So which method should we choose?

Well actually Attach is not an option because attach can cause key conflicts
due to duplicate key values for same object types. If we have Order with two new
Order Lines, those Order Lines would probably have Id = 0. Attaching this Order
with Attach method would break because Attach will mark these two Order Lines as
Unchanged and EF insists that all existing entities should have unique 
primary keys. This is why we will be using Add method for
attaching
.

Resolving new and modified data by Id value

The question is how will we know the state of each object in graph
(New/Modified/Unchanged/Deleted)? Because detached objects are not tracked the
only reliable way would be to reload the object graph form DB, and as I stated
before I don’t want to do that because of the performance.

We can use simple convention. If Id > 0 object is modified, and if Id = 0
then object is new. This is pretty simple convention but with 
drawbacks:

  • We can’t detect unchanged objects so we will be saving to DB unchanged
    data. On the bright side these object graphs should not be that big so this
    should not be performance issue.

  • Deleting objects must be handled with custom logic. E.g. having something
    like Order.DeletedOrderLines collection.

In order to read Id value when attaching objects, all entities will implement
IEntity interface.


Collapse | Copy
Code

public interface IEntity
{
long Id { get; }
}

Ignoring referent data

Each object graph can contain referential (read-only) data. In our case when
we are saving Order, we might have Products and Customer objects in graph but we
know that we don’t want to save them in DB. We know that we should save only
Order and Order Lines. On the other hand EF doesn’t know that. This is way
AttachByIdValue accepts array of Child types that should be attached for saving
along with Order. All objects in graph that are not root nor are of Child Type
will be attached to context, but will be marked as  Unchanged so they won’t
be saved to DB.

To save only Order (without Order Lines) we should call:


Collapse | Copy
Code

myContext.AttachByIdValue(Order, null);
myContext.SaveChanges();

So to save Order and Order Lines we should call:


Collapse | Copy
Code

myContext.AttachByIdValue(Order, new HashSet<Type>() { typeof(OrderLine) });
myContext.SaveChanges();

Off course above HashSet<Type> can be cached in static field to avoid
calling typeof on every object attaching.


Collapse | Copy
Code

private static readonly HashSet<Type> OrderChildTypes = new HashSet<Type>() { typeof(OrderLine) };
...
myContext.AttachByIdValue(Order, OrderChildTypes);
myContext.SaveChanges();

The
final solution


Collapse | Copy
Code

/// <summary>
/// Attaches entity graph to context using entity id to determinate if entity is new or modified.
/// If Id is zero then entity is treated as NEW and otherwise it is treated as modified.
/// If we want to save more than just root entity than child types must be supplied.
/// If entity in graph is not root nor of child type it will be attached but not saved
/// (it will be treated as unchanged).
/// </summary>
/// <param name="context">The context.</param>
/// <param name="rootEntity">The root entity.</param>
/// <param name="childTypes">The child types that should be saved with root entity.</param>
public static void AttachByIdValue<TEntity>(this DbContext context, TEntity rootEntity, HashSet<Type> childTypes)
where TEntity : class, IEntity
{
// mark root entity as added
// this action adds whole graph and marks each entity in it as added
context.Set<TEntity>().Add(rootEntity);
// in case root entity has id value mark it as modified (otherwise it stays added)
if (rootEntity.Id != 0)
{
context.Entry(rootEntity).State = EntityState.Modified;
}
// traverse all entities in context (hopefully they are all part of graph we just attached)
foreach (var entry in context.ChangeTracker.Entries<IEntity>())
{
// we are only interested in graph we have just attached
// and we know they are all marked as Added
// and we will ignore root entity because it is already resolved correctly
if (entry.State == EntityState.Added && entry.Entity != rootEntity)
{
// if no child types are defined for saving then just mark all entities as unchanged)
if (childTypes == null || childTypes.Count == 0)
{
entry.State = EntityState.Unchanged;
}
else
{
// request object type from context because we might got reference to dynamic proxy
// and we wouldn‘t want to handle Type of dynamic proxy
Type entityType = ObjectContext.GetObjectType(entry.Entity.GetType());
// if type is not child type than it should not be saved so mark it as unchanged
if (!childTypes.Contains(entityType))
{
entry.State = EntityState.Unchanged;
}
else if (entry.Entity.Id != 0)
{
// if entity should be saved with root entity
// than if it has id mark it as modified
// else leave it marked as added
entry.State = EntityState.Modified;
}
}
}
}
}

One gotcha

As I explained earlier, EF insists that all existing entities should have
unique primary keys and this is why you cannot attach to DbContext two unchanged
objects of same type with same Id. This shouldn’t be the case in general but I
have found one edge case where it might occur. Let’s say we are loading Order,
Order Lines and Products and we have two different Order Lines pointing
to same Product
. Normally EF will set reference to same Product object
to these Order Lines unless you are loading your data using AsNoTrackingto get better
performance
in which case each Order Line gets reference to separate Product
object that is equal by all values. I didn’t find documentation of this behavior
anywhere, I have discovered by accident why struggling to attach objects to
DBContext.

Attaching detached POCO to EF DbContext - simple and
fast

时间: 2024-08-11 09:41:42

Attaching detached POCO to EF DbContext - simple and fast的相关文章

EF架构~为EF DbContext生成的实体添加注释(T5模板应用)

相关文章系列 第八回 EF架构~将数据库注释添加导入到模型实体类中 第二十一回  EF架构~为EF DbContext生成的实体添加注释(T4模板应用) 第二十二回EF架构~为EF DbContext生成的实体添加注释(T5模板应用) 嗨,没法说,EF4的TT模版加上注释后,升级到EF5的TT模版后,注释就不通用了,所以,还得再研究一下,然后把操作方法再分享出来,没辙的微软! T4模版可能有些凌乱,这在T5模版里有了不错的改进,但我希望解决的问题在T5里并没有得到解决,那就是TT类文件自动得到E

如何重写EF DBContext 获取链接字符串的方法

public partial class byvarDBFirst: DbContext { //使用自定义连接串 private static string GetEFConnctionString() { //string connString = "metadata=res://*/EFModel_FromDb.csdl|res://*/EFModel_FromDb.ssdl|res://*/EFModel_FromDb.msl;provider=System.Data.SqlClient

EF DbContext 并发执行时可能出现的问题

现在许多Web项目都使用了IOC的DI注入组件.其中对象的生命周期管理是非常重要的. 有时我们为了提高请求的响应,经常在请求线程中执行多个子线程,然而忽略了EF的DbContext的生命周期管理. DbContext并非是线程安全的.子线程A和子线程B 可能同时的对同一个DbContext进行操作,从而导致下面的异常(可能随机抛出其中一个). 所以建议不要共用同一个DbContext. {"已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭."} “Sys

EF dbcontext上下文的处理

,那么我们整个项目里面上下文的实例会有很多个,我们又遇到了多次,当我们在编程的时候遇到多的时候,一般我们就要想想能不能解决多这个问题. (2)这里我要说的是EF上下文怎么管理呢?很简单啦,就是要保证线程内唯一,所以这里我们就要进行修改BaseRepository类了. (3) 在这里BaseRepository仓储的职责是什么?他的职责就是帮我们实现了所有子仓储的公共方法(增删查改),他的职责不包含怎么去管理上下文的实 例,所以我们不能把这种控制上下文实例的线程内唯一的代码放在这个位置,这就是我

为EF DbContext生成的实体添加注释(T5模板应用)[转]

1 先加上类注释 找到这行代码WriteHeader(codeStringGenerator, fileManager): 在它下面加上我们的代码: string summary=string.Empty; foreach (var entity in typeMapper.GetItemsToGenerate<EntityType>(itemCollection)) {     fileManager.StartNewFile(entity.Name + ".cs");

EF DbContext 和 ObjectContext 转换

由 DbContext 获得 ObjectContext的方法: ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext; /// context 是 DbContext 的实例 PS : ObjectContext VS DBContext , 描述了 两者的 区别:

让Ef.DbContext输出SQL

1.在YourDbContext构造函数中配置Database.Log. 1 public partial class XxxEntities: DbContext 2 { 3 public XxxEntities() 4 : base("name=XxxEntities") 5 { 6 System.Diagnostics.Debug.WriteLine("--实例化DbContext--"); 7 Database.Log = x => { System.

EntityFramework Core技术线路(EF7已经更名为EF Core,并于2016年6月底发布)

官方文档英文地址:https://github.com/aspnet/EntityFramework/wiki/Roadmap 历经延期和更名,新版本的实体框架终于要和大家见面了,虽然还有点害羞.请大家多体谅! 下面正式进入主题: Entity Framework Core (EF Core) 下面是EF Core 的计划和技术线路,注意,这些计划是可能发现变化的,因为很多事是很难预测的.即便如此,我们还是尽可能保持计划的公开和透明,以解大家对EF Core期望,以及做出相应的安排. Sched

[转]EntityFramework Core技术线路(EF7已经更名为EF Core,并于2016年6月底发布)

本文转自:http://www.cnblogs.com/VolcanoCloud/p/5572408.html 官方文档英文地址:https://github.com/aspnet/EntityFramework/wiki/Roadmap 历经延期和更名,新版本的实体框架终于要和大家见面了,虽然还有点害羞.请大家多体谅! 下面正式进入主题: Entity Framework Core (EF Core) 下面是EF Core 的计划和技术线路,注意,这些计划是可能发现变化的,因为很多事是很难预测