65.9K
CodeProject 正在变化。 阅读更多。
Home

终于!Entity Framework 在完全分离的 N 层 Web 应用程序中工作

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (38投票s)

2010年5月16日

CPOL

12分钟阅读

viewsIcon

222667

downloadIcon

2934

Entity Framework 是世界上最难用于 N 层应用程序的 ORM。看看我如何使用 Entity Framework 构建了一个 100% 可单元测试的、完全符合 N 层要求的、遵循存储库模式的数据访问层。

下载 EntityFrameworkTest.zip - 3.57 KB - 示例 100% 可单元测试的 ObjectContext 

介绍 

Entity Framework 本应解决 Linq to SQL 的问题,而 Linq to SQL 在 N 层世界中需要无休止的技巧才能正常工作。Entity Framework 不仅没有解决 L2S 的任何问题,反而使其更难在 N 层场景中使用和 hack。它介于完全断开连接的 ORM 和像 Linq to SQL 这样的完全连接的 ORM 之间。Linq to SQL 的一些有用功能已消失 - 例如自动延迟加载。如果您在断开连接的体系结构中尝试进行简单的选择、连接、插入、更新、删除操作,您会发现不仅需要从顶层到底层进行根本性更改,还需要在基本的 CRUD 操作中进行无休止的 hack。在本文中,我将向您展示如何在我自己的开源 Web 2.0 AJAX 门户(Dropthings)中,如何在 EF 的 ObjectContext 之上添加自定义 CRUD 函数,使其最终在完全断开连接的 N 层 Web 应用程序中运行良好,以及我如何构建一个100% 可单元测试的、完全符合 N 层要求的、遵循存储库模式的数据访问层

在 .NET 4.0 中,大多数问题都已解决,但并非所有问题都已解决。因此,即使您使用的是 .NET 4.0,也应该阅读本文。此外,这里提供了足够深入的见解,可以帮助您解决 EF 相关的故障。

您可能会想:“既然 Linq to SQL 已经做得足够好了,为什么还要费力使用 EF 呢?” Linq to SQL 不会再从微软那里获得任何创新。Entity Framework 是 .NET 框架中持久化层的未来。所有的创新都只发生在 EF 世界中,这令人沮丧。EF 4.0 有一个巨大的飞跃。因此,您应该尽快计划将您的 L2S 项目迁移到 EF。

扩展生成的 ObjectContext

首先,您必须扩展添加 ADO.NET Entity Data Model 时生成的 ObjectContext。我通常会创建一个新类,然后继承自生成的类。类似这样:

public class DropthingsDataContext2 : DropthingsDataContext, IDatabase 
{ 

原始的 ObjectContext 称为 DropthingsDataContext。在代码中,您将使用这个扩展的 XXXX2 类而不是原始类。

下一步是为每一个实体设置 MergeOption 为 NoTracking。如果您不这样做,实体默认会附加到 ObjectContextObjectStateManager 中,该管理器会跟踪实体的更改,以便在调用 SaveChanges 时保存这些更改。但这也意味着您的实体与数据访问层的东西绑定在一起,这种绑定非常紧密。只要实体存在,绑定就一直存在。因此,您需要在将实体传递到其他层之前分离实体。但是,当您调用 Detach(entity) 函数时,它不仅分离实体,还会分离整个图。这很好,也在预期之中。但出乎意料的是,所有引用的实体和集合都被设置为 NULL。所以,如果您需要将一个实体传递到另一层,并且需要携带所有引用的实体,那么在 EF 中是无法做到的。这就是我所做的

public DropthingsDataContext2(string connectionString) : base(connectionString) 
{ 
	this.aspnet_Memberships.MergeOption = 
	this.aspnet_Profiles.MergeOption = 
	this.aspnet_Roles.MergeOption = 
	this.aspnet_Users.MergeOption = 
	this.Columns.MergeOption = 
	this.Pages.MergeOption = 
	this.RoleTemplates.MergeOption = 
	this.Tokens.MergeOption = 
	this.UserSettings.MergeOption = 
	this.Widgets.MergeOption = 
	this.WidgetInstances.MergeOption = 
	this.WidgetsInRolesSet.MergeOption = 
	this.WidgetZones.MergeOption = System.Data.Objects.MergeOption.NoTracking; 

我在构造函数中为所有实体设置了 MergeOption 为 NoTracking。这使得所有实体默认是“分离”的,但仍然保留了对子/父实体/集合的引用。它们不会被放入 ObjectStateManager

接下来,我创建一个静态字典,其中包含每个实体的 Attach AddTo 调用。

private static Dictionary<string, Action<DropthingsDataContext, object>> 
    _AddToMethodCache = 
        new Dictionary<string, Action<DropthingsDataContext, object>>();

private static Dictionary<string, Action<DropthingsDataContext, object>> 
    _AttachMethodCache = 
        new Dictionary<string, Action<DropthingsDataContext, object>>();

public DropthingsDataContext2(string connectionString) : base(connectionString) 
{ 
    this.aspnet_Memberships.MergeOption = 
    ... 
    this.WidgetZones.MergeOption = System.Data.Objects.MergeOption.NoTracking;

    if (_AddToMethodCache.Count == 0) 
    { 
        lock (_AddToMethodCache) 
        { 
            if (_AddToMethodCache.Count == 0) 
            { 
                _AddToMethodCache.Add(typeof(aspnet_Membership).Name, 
                    (context, entity) => context.AddToaspnet_Memberships(entity as aspnet_Membership)); 
                _AddToMethodCache.Add(typeof(aspnet_Profile).Name, 
                    (context, entity) => context.AddToaspnet_Profiles(entity as aspnet_Profile)); 
                ... 
            } 
        } 
    }

    if (_AttachMethodCache.Count == 0) 
    { 
        lock (_AttachMethodCache) 
        { 
            if (_AttachMethodCache.Count == 0) 
            { 
                _AttachMethodCache.Add(typeof(aspnet_Membership).Name, 
                    (context, entity) => context.AttachTo("aspnet_Memberships", entity)); 
                _AttachMethodCache.Add(typeof(aspnet_Profile).Name, 
                    (context, entity) => context.AttachTo("aspnet_Profiles", entity)); 
                ... 
            } 
        } 
    } 
}
这些用于在一个通用函数中将实体 Attach Add 到实体集中。

public void Attach<TEntity>(TEntity entity) 
    where TEntity : EntityObject 
{ 
    if (entity.EntityState != EntityState.Detached) 
        return; 
    // Let's see if the same entity with same key values are already there 
    ObjectStateEntry entry; 
    if (ObjectStateManager.TryGetObjectStateEntry(entity, out entry)) 
    { 
    } 
    else 
    { 
        _AttachMethodCache[typeof(TEntity).Name](this, entity); 
    } 
}

public void AddTo<TEntity>(TEntity entity) 
{ 
    _AddToMethodCache[typeof(TEntity).Name](this, entity); 
}

完成这些之后,您现在可以进行常规的 CRUD 操作了。

选择父、子、多对多、远祖先实体

EF 引入了在多个实体之间进行连接的新方法。在 Linq to SQL 中,您会这样做:

from widgetInstance in dc.WidgetInstances
from widgetZone in dc.WidgetZones
join widgetInstance on widgetZone.ID equals widgetInstance.WidgetZoneID
where widgetZone.ID == widgetZoneId
orderby widgetInstance.OrderNo, widgetZone.ID
select widgetInstance 

Widget_WidgetInstance.png

在 EF 中,没有 join。您可以直接访问引用的实体

from widgetInstance in dc.WidgetInstance
where widgetInstance.WidgetZone.ID == widgetZoneId
orderby widgetInstance.OrderNo, widgetInstance.WidgetZone.ID
select widgetInstance
这是一种更简单的语法,尤其在这里您不需要了解 join 的内部工作原理以及两个表中的哪些键参与了 join。它在 ObjectContext 设计中被完全抽象了。这很好!

但是,从多对多 join 获取实体并非易事。例如,以下是 EF 中的一个查询,它根据多对多映射选择一个相关实体。

from widgetInstance in dc.WidgetInstance
where widgetInstance.Id == widgetInstanceId
select widgetInstance.WidgetZone.Column.FirstOrDefault().Page 

实体模型如下:

WidgetZone_Column_Page.png

请注意 select 子句中的 FirstOrDefault() 调用。这个东西对 Column 表执行多对多查询,并映射 Page 实体。

同样,假设您想从一个非常远的祖先实体中选择一个属性。例如,在此图中,您拥有左下角的 WidgetInstance ID 。现在您只想选择祖先实体 User 的 LoweredUserName。从图中可以看出,有一个父对象 WidgetZone ,它通过使用 Column 的多对多映射与 Page 相关联,然后 Page 有一个父对象 User 。您只想选择 User LoweredUserName

Ancestor_Objects_Property.png

对于这种情况,EF 中的查询如下:

from widgetInstance in dc.WidgetInstance
where widgetInstance.Id == widgetInstanceId
select widgetInstance.WidgetZone.Column.FirstOrDefault().Page.aspnet_Users.LoweredUserName  

如果您想用 Linq to SQL 来完成,您最终会得到一个像一篇小博客文章一样长的查询。

插入断开连接的实体

以下是 Insert<> 的通用版本,它可以插入任何断开连接的实体。

public TEntity Insert<TEntity>(TEntity entity) 
            where TEntity : EntityObject 
{ 
    AddTo<TEntity>(entity); 
    this.SaveChanges(true); 
    // Without this, attaching new entity of same type in same context fails. 
    this.Detach(entity); 
    return entity; 
} 

以下是显示其工作原理的小型测试

[Fact] 
public void Creating_new_object_and_insert_should_work() 
{ 
    using (var context = new DropthingsEntities2()) 
    { 
        var newWidgetZone = context.Insert(new WidgetZone 
        { 
            Title = "Hello", 
            UniqueID = Guid.NewGuid().ToString(), 
            OrderNo = 0                    
        });

        Assert.NotEqual(0, newWidgetZone.ID); 
        Assert.Equal<EntityState>(EntityState.Detached, newWidgetZone.EntityState); 
    } 
} 

正如预期的那样,它确认了实体已插入,自动标识已生成并分配给实体,并且实体以断开连接的状态返回,您可以将其传递到其他层。

插入断开连接的子实体

插入断开连接的子实体与包括 Linq to SQL 在内的其他流行 ORM 库相比,差异巨大,如果您有存储库层,请准备好进行大规模重构。插入子实体的通用原理是——首先您必须将父实体附加到上下文中,然后您需要设置父实体和子实体之间的映射(您不能已经存在映射!),然后您需要调用 SaveChanges。代码如下:

public TEntity Insert<TParent, TEntity>(
    TParent parent,
    Action<TParent, TEntity> addChildToParent,
    TEntity entity)
    where TEntity : EntityObject
    where TParent : EntityObject
{
    AddTo<TParent, TEntity>(parent, addChildToParent, entity);
    this.SaveChanges();
    this.AcceptAllChanges();
    // Without this, consequtive insert using same parent in same context fails.
    this.Detach(parent); 
    // Without this, attaching new entity of same type in same context fails.
    this.Detach(entity);
    return entity;
}

private void AddTo<TParent, TEntity>(TParent parent, 
    Action<TParent, TEntity> addChildToParent, 
    TEntity entity) 
    where TEntity : EntityObject
    where TParent : EntityObject
{
    Attach<TParent>(parent);            
    addChildToParent(parent, entity);            
} 

以下是演示如何使用它的测试用例

[Fact]
public void Using_a_stub_parent_insert_a_child_object_should_work()
{
    var userId = default(Guid);
    using (var context = new DropthingsEntities2())
    {
        aspnet_Users existingUser = context.aspnet_Users.OrderByDescending(u => u.UserId).First();
        userId = existingUser.UserId;
    }

    using (var context = new DropthingsEntities2())
    {
        var newPage = new Page
        {
            Title = "Dummy Page",
            VersionNo = 0,
            ...
            ...
            aspnet_Users = new aspnet_Users { UserId = userId },
        };

        var parentUser = newPage.aspnet_Users;
        newPage.aspnet_Users = null;
        context.Insert<aspnet_Users, Page>(
            parentUser,
            (user, page) => page.aspnet_Users = user,
            newPage);

        Assert.NotEqual(0, newPage.ID);
    }

} 

您现在一定已经发现了插入子对象的恐怖和痛苦。假设从某个上层,您得到了一个已经分配了父实体的实体。在插入它之前,您必须先将父实体设置为 null,然后附加它。如果您不这样做,插入就会悄无声息地失败。没有异常,什么都不会发生。您会想——可怜的 EF 程序经理肯定报酬太低了。

如果您环顾四周,您会找到各种替代方案。有些人尝试了 context.SomeSet.AddObject(…) 方法。这对于每个上下文只进行一次插入来说效果很好。但您不能使用该方法插入具有相同父实体的相同类型的另一个实体。我尝试了 4 种不同的方法,这是唯一一种在所有场景中都有效的方法——无论是连接的还是断开连接的,父实体是真实的还是存根的,在同一上下文或不同上下文中插入一个或多个。我实际上编写了上千行测试代码来测试所有可能的插入子实体的方法,以证明它有效。微软的 EF SDET 们,我来了。

插入断开连接的多对多实体

就像子实体一样,您可以插入多对多映射实体。您可以将它们视为拥有两个或多个父实体。例如,如果您查看此图

Many_to_many_entities.png

这里的 Column 是一个多对多映射实体。因此,它有两个外键——一个指向 WidgetZone ,另一个指向 Page。在 EF 世界中,您可以认为 Column 有两个父实体——WidgetZone 和 Page。因此,Insert<> 类似于插入子实体。

[Fact]
public void Many_to_many_entity_should_insert_in_same_context()
{
    var page = default(Page);
    var widgetZone = default(WidgetZone);

    using (var context = new DropthingsEntities2())
    {
        page = context.Page.OrderByDescending(p => p.ID).First();
        widgetZone = context.WidgetZone.OrderByDescending(z => z.ID).First();

        var columnNo = (int)DateTime.Now.Ticks;
        var newColumn1 = new Column
        {
            ColumnNo = columnNo,
            ColumnWidth = 33,
        };

        context.Insert<Page, WidgetZone, Column>(page, widgetZone,
            (p, c) => p.Column.Add(c),
            (w, c) => w.Column.Add(c),
            newColumn1);
        Assert.NotEqual(0, newColumn1.ID);

    }
} 

在这里,您可以看到,就像单个父子插入一样,它执行了双重父子插入。Insert<> 中的代码如下:

public TEntity Insert<TParent1, TParent2, TEntity>(
    TParent1 parent1, TParent2 parent2,
    Action<TParent1, TEntity> addChildToParent1,
    Action<TParent2, TEntity> addChildToParent2,
    TEntity entity)
    where TEntity : EntityObject
    where TParent1 : EntityObject
    where TParent2 : EntityObject
{
    AddTo<TParent1, TParent2, TEntity>(parent1, parent2, addChildToParent1, addChildToParent2, entity);

    this.SaveChanges(true);

    // Without this, consecutive insert using same parent in same context fails.
    this.Detach(parent1);
    // Without this, consecutive insert using same parent in same context fails.
    this.Detach(parent2);
    // Without this, attaching new entity of same type in same context fails.
    this.Detach(entity);
    return entity;
}

private void AddTo<TParent1, TParent2, TEntity>(TParent1 parent1, TParent2 parent2, Action<TParent1, TEntity> addChildToParent1, Action<TParent2, TEntity> addChildToParent2, TEntity entity) 
    where TEntity : EntityObject
    where TParent1 : EntityObject
    where TParent2 : EntityObject
{
    Attach<TParent1>(parent1);
    Attach<TParent2>(parent2);
    addChildToParent1(parent1, entity);
    addChildToParent2(parent2, entity);

    //AddTo<TEntity>(entity);
} 

同样,网上有很多种方法可以进行这种类型的插入,我尝试了很多。大多数方法在尝试使用同一个上下文插入多个子实体时都会失败。这个方法已被证明有效。我有数百行测试代码来支持我的说法。

更新断开连接的实体 

更新不像插入那样直接。首先,您必须附加断开连接的实体和所有引用的实体,同时要记住它们可能已经存在于 ObjectStateManager 中,因此尝试附加实体将导致 dreaded:

An object with the same key already exists in the ObjectStateManager.
The ObjectStateManager cannot track multiple objects with the same key 

在网上找到一个用于更新断开连接实体的常见解决方案是这样的,它在大多数常见场景中都有效,除了一个不太常见的场景。您看到的常见 Update 函数如下所示:

public TEntity Update<TEntity>(TEntity entity) 
    where TEntity : EntityObject 
{            
    Attach<TEntity>(entity); 
    SetEntryModified(this, entity); 
    this.SaveChanges(true);            
    return entity; 
} 

首先,它将实体附加到上下文中。然后,SetEntryModified 函数将遍历实体的所有非键属性,并将其标记为已修改,以便上下文将对象视为已修改,并在调用 SaveChanges 时执行更新。SetEntryModified 如下所示:

static void SetEntryModified(ObjectContext context, object item) 
{ 
    ObjectStateEntry entry = context.ObjectStateManager.GetObjectStateEntry(item); 
    for (int i = 0; i < entry.CurrentValues.FieldCount; i++) 
    { 
        bool isKey = false;

        string name = entry.CurrentValues.GetName(i);

        foreach (var keyPair in entry.EntityKey.EntityKeyValues) 
        { 
            if (string.Compare(name, keyPair.Key, true) == 0) 
            { 
                isKey = true; 
                break; 
            } 
        } 
        if (!isKey) 
        { 
            entry.SetModifiedProperty(name); 
        } 
    } 
} 

当您在一个上下文中加载一个实体,然后在另一个上下文中尝试更新它时,这效果很好。例如,以下测试代码显示了在一个上下文中加载一个实体,然后处理掉该上下文,并在另一个新创建的上下文中尝试更新。

[Fact] 
public void Entity_should_update_loaded_from_another_context() 
{ 
    int someValue = (int)DateTime.Now.Ticks; 
    WidgetInstance wi; 
    using (var context = new DropthingsEntities2()) 
    { 
        wi = context.WidgetInstance.OrderByDescending(w => w.Id).First(); 
    }

    wi.Height = someValue;

    using (var context = new DropthingsEntities2()) 
    { 
        context.Update<WidgetInstance>(wi);

        WidgetInstance wi2 = getWidgetInstance(context, wi.Id).First(); 
        Assert.Equal(wi.Height, wi2.Height); 
    } 
} 

这如预期般运行良好。即使您已加载实体及其所有引用的实体,然后将其通过各个层传递,然后在更新时将其带回,而没有原始引用的实体,它也同样有效。

[Fact] 
public void Entity_should_update_loaded_from_another_context_with_stub_referenced_entities()  
{ 
    int someValue = (int)DateTime.Now.Ticks; 
    WidgetInstance wi; 
    using (var context = new DropthingsEntities2()) 
    { 
        wi = context.WidgetInstance.Include("WidgetZone").Include("Widget").OrderByDescending(w => w.Id).First(); 
    }

    wi.Height = someValue; 
    wi.WidgetZone = new WidgetZone { ID = wi.WidgetZone.ID }; 
    wi.Widget = new Widget { ID = wi.Widget.ID };

    using (var context = new DropthingsEntities2()) 
    { 
        context.Update<WidgetInstance>(wi);

        WidgetInstance wi2 = getWidgetInstance(context, wi.Id).First(); 
        Assert.Equal(wi.Height, wi2.Height); 
    } 
} 

这是一个典型的 N 层场景,其中您有一个实体,没有任何引用的实体。现在您要更新它,并且手头只有外键。因此,您需要为引用的实体创建存根,这样您就不必命中数据库再次读取实体以及所有引用的实体。上面的测试也适用于这种情况,但失败的情况接近于这种情况。我将处理那个。现在再看另一种情况。

在高流量的 N 层应用程序中,您会缓存常用的实体。实体从缓存加载,然后更改并在数据库中更新。类似这样:

[Fact] 
public void Changes_made_to_entity_after_update_should_update_again() 
{ 
    int someValue = (int)DateTime.Now.Ticks; 
    MemoryStream cachedBytes = new MemoryStream(); 
    using (var context = new DropthingsEntities2()) 
    { 
        var wi = context.WidgetInstance.Include("WidgetZone").Include("Widget").OrderByDescending(z => z.Id).First();

        // Load the related entities separately so that they get into ObjectStateManager 
        var widget = context.Widget.Where(w => w.ID == wi.Widget.ID); 
        var widgetzone = context.WidgetZone.Where(zone => zone.ID == wi.WidgetZone.ID);

        wi.Height = someValue;

        var updated = context.Update<WidgetInstance>(wi); 
        Assert.NotNull(updated);

        WidgetInstance wi2 = getWidgetInstance(context, wi.Id).First(); 
        Assert.Equal(someValue, wi2.Height);

        new DataContractSerializer(typeof(WidgetInstance)).WriteObject(cachedBytes, wi2); 
    }

    // Update the same entity again in a different ObejctContext. Simulating 
    // the scenario where the entity was stored in a cache and now retrieved 
    // from cache and being updated in a separate thread. 
    var anotherThread = new Thread(() => 
        { 
            cachedBytes.Position = 0; 
            var wi = new DataContractSerializer(typeof(WidgetInstance)).ReadObject(cachedBytes) as WidgetInstance; 
            Assert.NotNull(wi.Widget); 
            using (var context = new DropthingsEntities2()) 
            { 
                someValue = someValue + 1; 
                wi.Height = someValue;

                var updatedAgain = context.Update<WidgetInstance>(wi); 
                Assert.NotNull(updatedAgain);

                WidgetInstance wi3 = getWidgetInstance(context, wi.Id).First(); 
                Assert.Equal(someValue, wi3.Height); 
            } 
        }); 
    anotherThread.Start(); 
    anotherThread.Join(); 
} 

这里我模拟了一个缓存场景,其中缓存是进程外缓存或分布式缓存。我加载一个实体及其引用的实体,以确保无误,然后更新一些属性。更新后,更改的实体将被序列化。然后在另一个上下文中,更改的实体是从序列化流创建的。这确保了它与原始上下文没有绑定。此外,它证明在分布式缓存中有效,其中对象不存储在内存中,而是始终进行序列化/反序列化。测试证明在这种缓存场景中,更新效果很好。

现在是您期待的时刻。这个特定的场景需要我完全更改 Update 方法,并选择一种完全不同的方法来更新实体。

假设您需要更改其中一个外键。在 EF 中执行此操作的方法是将引用的实体更改为新的存根。类似这样:

[Fact] 
public void Changing_referenced_entity_should_work_just_like_updating_regular_entity() 
{ 
    int someValue = (int)DateTime.Now.Ticks; 
    WidgetInstance wi;

    var newWidget = default(Widget); 
    using (var context = new DropthingsEntities2()) 
    { 
        wi = context.WidgetInstance.Include("WidgetZone").Include("Widget").OrderByDescending(w => w.Id).First(); 
        newWidget = context.Widget.Where(w => w.ID != wi.Widget.ID).First(); 
    }

    wi.Height = someValue; 
    wi.Widget = new Widget { ID = newWidget.ID };

    using (var context = new DropthingsEntities2()) 
    { 
        context.Update<WidgetInstance>(wi);

        WidgetInstance wi2 = getWidgetInstance(context, wi.Id).First(); 
        Assert.Equal(wi.Height, wi2.Height); 
        Assert.Equal(newWidget.ID, wi2.Widget.ID); 
    } 
} 

 

如果您尝试这样做,不会有异常,但更改的外键不会更新。EF 看不到对 Widget 属性所做的更改。我以为我还需要更改 WidgetReference 以反映对 Widget 属性所做的更改。类似这样:

wi.Height = someValue; 
wi.Widget = new Widget { ID = newWidget.ID }; 
wi.WidgetReference = new EntityReference<Widget> { EntityKey = newWidget.EntityKey }; 

但没有成功。实体不会更新。也没有异常。所以,我不得不为更新实体采取一种完全不同的方法。

public TEntity Update<TEntity>(TEntity entity) 
    where TEntity : EntityObject 
{ 
    AttachUpdated(entity); 
    this.SaveChanges(true); 
    return entity; 
}

public void AttachUpdated(EntityObject objectDetached) 
{ 
    if (objectDetached.EntityState == EntityState.Detached) 
    { 
        object currentEntityInDb = null; 
        if (this.TryGetObjectByKey(objectDetached.EntityKey, out currentEntityInDb)) 
        { 
            this.ApplyPropertyChanges(objectDetached.EntityKey.EntitySetName, objectDetached); 
            ApplyReferencePropertyChanges((IEntityWithRelationships)objectDetached, 
                (IEntityWithRelationships)currentEntityInDb); 
        } 
        else 
        { 
            throw new ObjectNotFoundException(); 
        }

    }

}

public void ApplyReferencePropertyChanges( 
    IEntityWithRelationships newEntity, 
    IEntityWithRelationships oldEntity) 
{ 
    foreach (var relatedEnd in oldEntity.RelationshipManager.GetAllRelatedEnds()) 
    { 
        var oldRef = relatedEnd as EntityReference; 
        if (oldRef != null) 
        { 
            var newRef = newEntity.RelationshipManager.GetRelatedEnd(oldRef.RelationshipName, oldRef.TargetRoleName) as EntityReference; 
            oldRef.EntityKey = newRef.EntityKey; 
        } 
    } 
} 

使用这段代码,并进行 WidgetReference 操作,测试通过。请记住,使用此更新代码而不更改 WidgetReference 将导致异常。

failed: System.Data.UpdateException : A relationship is being added or deleted from an AssociationSet 'FK_WidgetInstance_Widget'. With cardinality constraints, a corresponding 'WidgetInstance' must also be added or deleted. 
    at System.Data.Mapping.Update.Internal.UpdateTranslator.RelationshipConstraintValidator.ValidateConstraints() 
    at System.Data.Mapping.Update.Internal.UpdateTranslator.ProduceCommands() 
    at System.Data.Mapping.Update.Internal.UpdateTranslator.Update(IEntityStateManager stateManager, IEntityAdapter adapter) 
    at System.Data.EntityClient.EntityAdapter.Update(IEntityStateManager entityCache) 
    at System.Data.Objects.ObjectContext.SaveChanges(Boolean acceptChangesDuringSave) 
    DropthingsEntities2.cs(278,0): at EntityFrameworkTest.DropthingsEntities2.Update[TEntity](TEntity entity) 
    Program.cs(646,0): at EntityFrameworkTest.Program.Changing_referenced_entity_should_work_just_like_updating_regular_entity() 

因此,您必须使用这个新的 Update<> 方法,并进行 WidgetReference 技巧。

删除连接和断开连接的实体

即使是一个简单的删除在 EF 中也不会如您预期的那样工作。您必须处理几种情况才能使其在常见用例中正常工作。首先是断开连接的删除。假设您从一个上下文中加载了一个实体,然后在另一个上下文中要删除它。类似这样:

[Fact]
public void Should_be_able_to_delete_entity_loaded_from_another_context()
{
    var wi = default(WidgetInstance);
    using (var context = new DropthingsEntities2())
    {
        wi = context.WidgetInstance.Include("WidgetZone").Include("Widget").OrderByDescending(w => w.Id).First();
    }

    using (var context = new DropthingsEntities2())
    {
        context.Delete<WidgetInstance>(wi);

        var deletedWi = getWidgetInstance(context, wi.Id).FirstOrDefault();
        Assert.Null(deletedWi);
    }
} 

这是 N 层应用程序中最常见的断开连接删除场景。为了使其正常工作,您需要创建一个自定义的 Delete 函数。

public void Delete<TEntity>(TEntity entity)
            where TEntity : EntityObject
{
    this.Attach<TEntity>(entity);
    this.Refresh(RefreshMode.StoreWins, entity);
    this.DeleteObject(entity);
    this.SaveChanges(true);            
} 

这将处理断开连接的删除场景。但如果您在一个上下文中加载了一个实体然后尝试在同一个上下文中删除它,它将失败。为此,您需要添加一些额外的检查。

public void Delete<TEntity>(TEntity entity)
    where TEntity : EntityObject
{
    if (entity.EntityState != EntityState.Detached)
        this.Detach(entity);

    if (entity.EntityKey != null)
    {
        var onlyEntity = default(object);
        if (this.TryGetObjectByKey(entity.EntityKey, out onlyEntity))
        {
            this.DeleteObject(onlyEntity);
            this.SaveChanges(true);
        }
    }
    else
    {
        this.Attach<TEntity>(entity);
        this.Refresh(RefreshMode.StoreWins, entity);
        this.DeleteObject(entity);
        this.SaveChanges(true);
    }
} 

这将满足以下测试:

[Fact]
public void Should_be_able_to_delete_entity_loaded_from_same_context()
{
    var wi = default(WidgetInstance);
    using (var context = new DropthingsEntities2())
    {
        wi = context.WidgetInstance.Include("WidgetZone").Include("Widget").OrderByDescending(w => w.Id).First();

        context.Delete<WidgetInstance>(wi);

        var deletedWi = getWidgetInstance(context, wi.Id).FirstOrDefault();
        Assert.Null(deletedWi);
    }
} 

因此,您可以删除那些未断开连接且具有有效 EntityKey 的实体,以及删除断开连接的实体。此外,您可以使用存根进行删除。像这样:

[Fact]
public void Should_be_able_to_delete_entity_using_stub()
{
	var wi = default(WidgetInstance);
	using (var context = new DropthingsEntities2())
	{
		wi = context.WidgetInstance.Include("WidgetZone").Include("Widget").OrderByDescending(w => w.Id).First(); ;

		context.Delete<WidgetInstance>(new WidgetInstance { Id = wi.Id });

		var deletedWi = getWidgetInstance(context, wi.Id).FirstOrDefault();
		Assert.Null(deletedWi);
	}
} 

这也能正常工作。

100% 可单元测试的 ObjectContext

在 Linq to SQL 中使 DataContext 可进行单元测试非常困难。您需要扩展生成的 DataContext 并添加很多东西。EF 并没有更容易。这是我如何创建一个完全可进行单元测试的 ObjectContext 的方法,其中包含了我上面展示的所有代码以及更多内容——完整的类。

    public class DropthingsEntities2 : DropthingsEntities, IDatabase
    {
        private static Dictionary<string, Action<DropthingsEntities2, object>> 
            _AddToMethodCache =
                new Dictionary<string, Action<DropthingsEntities2, object>>();

        private static Dictionary<string, Action<DropthingsEntities2, object>>
            _AttachMethodCache =
                new Dictionary<string, Action<DropthingsEntities2, object>>();

        public DropthingsEntities2() : base()
        {
            this.aspnet_Applications.MergeOption =
                this.aspnet_Membership.MergeOption =
                this.Widget.MergeOption =
                ...
                ...
                this.WidgetZone.MergeOption = System.Data.Objects.MergeOption.NoTracking;

            if (_AddToMethodCache.Count == 0)
            {
                lock (_AddToMethodCache)
                {
                    if (_AddToMethodCache.Count == 0)
                    {
                        _AddToMethodCache.Add(typeof(aspnet_Applications).Name,
                            (context, entity) => context.AddToaspnet_Applications(entity as aspnet_Applications));                        
                        _AddToMethodCache.Add(typeof(aspnet_Membership).Name,
                            (context, entity) => context.AddToaspnet_Membership(entity as aspnet_Membership));
                        ...
                        ...
                    }
                }
            }

            if (_AttachMethodCache.Count == 0)
            {
                lock (_AttachMethodCache)
                {
                    if (_AttachMethodCache.Count == 0)
                    {
                        _AttachMethodCache.Add(typeof(aspnet_Applications).Name,
                            (context, entity) => context.AttachTo("aspnet_Applications", entity));
                        _AttachMethodCache.Add(typeof(aspnet_Membership).Name,
                            (context, entity) => context.AttachTo("aspnet_Membership", entity));
                        ...
                        ...
                    }
                }
            }
        }

        public IQueryable<TReturnType> Query<TReturnType>(Func<DropthingsEntities, IQueryable<TReturnType>> query)
        {
            return query(this);
        }
        public IQueryable<TReturnType> Query<Arg0, TReturnType>(Func<DropthingsEntities, Arg0, IQueryable<TReturnType>> query, Arg0 arg0)
        {
            return query(this, arg0);
        }
        public IQueryable<TReturnType> Query<Arg0, Arg1, TReturnType>(Func<DropthingsEntities, Arg0, Arg1, IQueryable<TReturnType>> query, Arg0 arg0, Arg1 arg1)
        {
            return query(this, arg0, arg1);
        }
        public IQueryable<TReturnType> Query<Arg0, Arg1, Arg2, TReturnType>(Func<DropthingsEntities, Arg0, Arg1, Arg2, IQueryable<TReturnType>> query, Arg0 arg0, Arg1 arg1, Arg2 arg2)
        {
            return query(this, arg0, arg1, arg2);
        }

        public TEntity Insert<TEntity>(TEntity entity)
            where TEntity : EntityObject
        {
            AddTo<TEntity>(entity);
            this.SaveChanges(true);
            // Without this, attaching new entity of same type in same context fails.
            this.Detach(entity); 
            return entity;
        }
        public TEntity Insert<TParent, TEntity>(
            TParent parent,
            Action<TParent, TEntity> addChildToParent,
            TEntity entity)
            where TEntity : EntityObject
            where TParent : EntityObject
        {
            AddTo<TParent, TEntity>(parent, addChildToParent, entity);
            this.SaveChanges();
            this.AcceptAllChanges();
            // Without this, consequtive insert using same parent in same context fails.
            this.Detach(parent); 
            // Without this, attaching new entity of same type in same context fails.
            this.Detach(entity);
            return entity;
        }
        public TEntity Insert<TParent1, TParent2, TEntity>(
            TParent1 parent1, TParent2 parent2,
            Action<TParent1, TEntity> addChildToParent1,
            Action<TParent2, TEntity> addChildToParent2,
            TEntity entity)
            where TEntity : EntityObject
            where TParent1 : EntityObject
            where TParent2 : EntityObject
        {
            AddTo<TParent1, TParent2, TEntity>(parent1, parent2, addChildToParent1, addChildToParent2, entity);

            this.SaveChanges(true);

            // Without this, consequtive insert using same parent in same context fails.
            this.Detach(parent1);
            // Without this, consequtive insert using same parent in same context fails.
            this.Detach(parent2);
            // Without this, attaching new entity of same type in same context fails.
            this.Detach(entity);
            return entity;
        }

        public void InsertList<TEntity>(IEnumerable<TEntity> entities)
            where TEntity : EntityObject
        {
            entities.Each(entity => Attach<TEntity>(entity));
            this.SaveChanges(true);
        }
        public void InsertList<TParent, TEntity>(
            TParent parent,
            Action<TParent, TEntity> addChildToParent,
            IEnumerable<TEntity> entities)
            where TEntity : EntityObject
            where TParent : EntityObject
        {
            entities.Each(entity => AddTo<TParent, TEntity>(parent, addChildToParent, entity));
            this.SaveChanges(true);            
        }
        public void InsertList<TParent1, TParent2, TEntity>(
            TParent1 parent1, TParent2 parent2,
            Action<TParent1, TEntity> addChildToParent1,
            Action<TParent2, TEntity> addChildToParent2,
            IEnumerable<TEntity> entities)
            where TEntity : EntityObject
            where TParent1 : EntityObject
            where TParent2 : EntityObject
        {
            entities.Each(entity => AddTo<TParent1, TParent2, TEntity>(parent1, parent2,
                addChildToParent1, addChildToParent2, entity));

            this.SaveChanges();
            this.AcceptAllChanges();
        }

        private void AddTo<TParent, TEntity>(TParent parent, 
            Action<TParent, TEntity> addChildToParent, TEntity entity) 
            where TEntity : EntityObject
            where TParent : EntityObject
        {
            Attach<TParent>(parent);            
            addChildToParent(parent, entity);            
            //AddTo<TEntity>(entity);
        }

        private void AddTo<TParent1, TParent2, TEntity>(TParent1 parent1, 
            TParent2 parent2, Action<TParent1, TEntity> addChildToParent1, 
            Action<TParent2, TEntity> addChildToParent2, TEntity entity) 
            where TEntity : EntityObject
            where TParent1 : EntityObject
            where TParent2 : EntityObject
        {
            Attach<TParent1>(parent1);
            Attach<TParent2>(parent2);
            addChildToParent1(parent1, entity);
            addChildToParent2(parent2, entity);

            //AddTo<TEntity>(entity);
        }

        public void Attach<TEntity>(TEntity entity)
            where TEntity : EntityObject
        {
            if (entity.EntityState != EntityState.Detached)
                return;
            // Let's see if the same entity with same key values are already there
            ObjectStateEntry entry;            
            if (ObjectStateManager.TryGetObjectStateEntry(entity, out entry))
            {
            }
            else
            {
                _AttachMethodCache[typeof(TEntity).Name](this, entity);
            }
        }
        public void AddTo<TEntity>(TEntity entity)
        {
            _AddToMethodCache[typeof(TEntity).Name](this, entity);
        }

        public TEntity Update<TEntity>(TEntity entity)
            where TEntity : EntityObject
        {
            AttachUpdated(entity);
            this.SaveChanges(true);
            return entity;
        }

        public void UpdateList<TEntity>(IEnumerable<TEntity> entities)
            where TEntity : EntityObject
        {
            foreach (TEntity entity in entities)
            {
                Attach<TEntity>(entity);
                SetEntryModified(this, entity);                
            }

            this.SaveChanges(true);
        }

        public void AttachUpdated(EntityObject objectDetached)
        {
            if (objectDetached.EntityState == EntityState.Detached)
            {
                object currentEntityInDb = null;
                if (this.TryGetObjectByKey(objectDetached.EntityKey, out currentEntityInDb))
                {
                    this.ApplyPropertyChanges(objectDetached.EntityKey.EntitySetName, objectDetached);
                    ApplyReferencePropertyChanges((IEntityWithRelationships)objectDetached, 
                        (IEntityWithRelationships)currentEntityInDb); 
                }
                else
                {
                    throw new ObjectNotFoundException();
                }

            }
        }

        public void ApplyReferencePropertyChanges(
            IEntityWithRelationships newEntity,
            IEntityWithRelationships oldEntity)
        {
            foreach (var relatedEnd in oldEntity.RelationshipManager.GetAllRelatedEnds())
            {
                var oldRef = relatedEnd as EntityReference;
                if (oldRef != null)
                {
                    var newRef = newEntity.RelationshipManager.GetRelatedEnd(oldRef.RelationshipName, oldRef.TargetRoleName) as EntityReference;
                    oldRef.EntityKey = newRef.EntityKey;
                }
            }
        }

        public void Delete<TEntity>(TEntity entity)
            where TEntity : EntityObject
        {
            if (entity.EntityState != EntityState.Detached)
                this.Detach(entity);

            if (entity.EntityKey != null)
            {
                var onlyEntity = default(object);
                if (this.TryGetObjectByKey(entity.EntityKey, out onlyEntity))
                {
                    this.DeleteObject(onlyEntity);
                    this.SaveChanges(true);
                }
            }
            else
            {
                this.Attach<TEntity>(entity);
                this.Refresh(RefreshMode.StoreWins, entity);
                this.DeleteObject(entity);
                this.SaveChanges(true);
            }
        }
    }
}  

您可以直接使用这个类。唯一需要更改的是在构造函数中设置所有实体为 MergeOption = NoTracking。然后,您需要为 ObjectContext 中的每个实体创建 AttachTo AddTo 映射。这是唯一困难的部分。

这是一个使用扩展的 ObjectContext 的存储库进行单元测试的示例:

[Specification]
public void GetPage_Should_Return_A_Page_from_database_when_cache_is_empty_and_then_caches_it()
{
    var cache = new Mock<ICache>();
    var database = new Mock<IDatabase>();
    IPageRepository pageRepository = new PageRepository(database.Object, cache.Object);

    const int pageId = 1;
    var page = default(Page);
    var samplePage = new Page() { ID = pageId, Title = "Test Page", ColumnCount = 3, LayoutType = 3, VersionNo = 1, PageType = (int)Enumerations.PageTypeEnum.PersonalPage, CreatedDate = DateTime.Now };

    database
            .Expect<IQueryable<Page>>(d => d.Query<int, Page>(CompiledQueries.PageQueries.GetPageById, 1))
            .Returns(new Page[] { samplePage }.AsQueryable()).Verifiable();

    "Given PageRepository and empty cache".Context(() =>
    {
        // cache is empty
        cache.Expect(c => c.Get(It.IsAny<string>())).Returns(default(object));
        // It will cache the Page object afte loading from database
        cache.Expect(c => c.Add(It.Is<string>(cacheKey => cacheKey == CacheKeys.PageKeys.PageId(pageId)),
                It.Is<Page>(cachePage => object.ReferenceEquals(cachePage, samplePage)))).Verifiable();
    });

    "when GetPageById is called".Do(() =>
            page = pageRepository.GetPageById(1));

    "it checks in the cache first and finds nothing and then caches it".Assert(() =>
            cache.VerifyAll());

    "it loads the page from database".Assert(() =>
            database.VerifyAll());

    "it returns the page as expected".Assert(() =>
    {
        Assert.Equal<int>(pageId, page.ID);
    });
} 

如您所见,我可以使用 Moq 来模拟所有 Query 调用。同样,我可以模拟 Insert、Update、Delete,从而为存储库生成单元测试。

结论

Entity Framework 在断开连接的环境中使用起来很困难。微软使其比 Linq to SQL 并不容易。就像 Linq to SQL 一样,它需要大量 hack 才能在数据库操作混合了连接和断开连接的实体环境中正常工作。鉴于您无论如何都要对 Linq to SQL 进行 hack,我建议转向 Entity Framework,尽管需要进行这些新的 hack 和处理实体方式的根本性改变,因为如今大部分创新都发生在 EF 领域。

© . All rights reserved.