使用 Entity Framework 拦截器让模型摆脱视图强制的限制






3.86/5 (4投票s)
为 Entity Framework 实现业务逻辑、日志记录和验证。
引言
你好,非常感谢你阅读我关于Entity Framework的探索。我希望我在这里写的一些内容对你有用。
现在,我仍然很享受学习的过程,最近,我关注的是 Entity Framework,这大概是在 NET 3.5 SP1 发布的时候。在探索它的可能性时,我遇到了一些有趣的问题,并四处打听寻求建议。由于我找不到太多信息,我想和你分享我的发现,并寻求你专业的意见。
我最终为 Entity Framework 编写了一些拦截器。它还有些粗糙,但也许对你有用,或者能给你一些如何让事情变得更简单的想法。如果你喜欢这里提出的想法,或者在实际中使用它们,我很乐意知道!
背景
在本文中,我旨在简要探讨如何在 Entity Framework 中实现业务逻辑,尝试设计一种更通用的方法来管理模型背后的逻辑,并尝试分离各个方面。
Entity Framework 中的业务逻辑
在我所知,在 Entity Framework 本身中,你可以在四个地方实现业务逻辑:
实体类的 On<Property>Changing 部分方法
当属性更改时会调用此事件。该部分方法由代码生成器生成。你只需输入“partial”,它就会出现。当设置属性时会调用此函数。 http://msdn.microsoft.com/en-us/library/cc716747.aspx。
对象上下文的 SavingChanges 事件
当更改持久化到数据库或存储时,会调用对象上下文的 SavingChanges
事件,你可以在其中使用 GetObjectStateEntries()
检查已更改的 ObjectStateEntry
,并进行日志记录、验证或其他处理。你可以将其用作 LINQ to SQL 部分方法的替代,但所有这些都会一次性以一个大列表的形式出现。 http://msdn.microsoft.com/en-us/library/cc716714.aspx。
LINQ to SQL 的部分方法在哪里?
与 LINQ to SQL 不同,Entity Framework *不* 提供部分实例方法 Insert<table>
、Update<table>
或 Delete<table>
来处理单个实例的删除,似乎是这样。 http://weblogs.asp.net/scottgu/archive/2007/07/11/linq-to-sql-part-4-updating-our-database.aspx。
EntityReference 的 AssociationChanged 事件
你可以使用指向另一个实体(集合)的 EntityReference
来注册关联或关系更改。当引用发生更改(属性更改)时,会调用该事件。例如,如果你有一个名为“Client”的属性,你还有一个名为“ClientReference”的属性,可以通过向 AssociationChanged
事件添加事件处理程序来监视其更改。 http://msdn.microsoft.com/en-us/library/cc716754.aspx。
关系/关联更改和 SavingChanges 事件
对象上下文的 SavingChanges
事件可以通过 GetObjectStateEntries()
检索更改,其中包括对关联所做的更改;在这种情况下,ObjectStateEntry
的 IsRelationship
属性应为“true”。这将允许你将关联更改作为“进行中”处理,并只拦截最终结果。
加载前的初始化和处理状态跟踪的移除
Entity 对象可以在被上下文存储之前,或者从存储中移除之前被拦截。这与添加到数据库或从数据库中移除不同,而是指对象被添加到上下文正在跟踪更改的对象集合中,或者从集合中移除。例如,你可以在这里为每个新对象的键提供一个新的 GUID。ObjectStateManager
的 ObjectStateManagerChanged
事件是为此目的进行拦截的事件。 http://msdn.microsoft.com/en-us/library/system.data.objects.objectstatemanager.objectstatemanagerchanged.aspx。
模型外的附加业务逻辑
据我所知,还有另外两个地方原生支持插入附加逻辑:
- ADO.NET Data Services 具有拦截器。 http://msdn.microsoft.com/en-us/library/cc668788.aspx。
- ASP.NET Dynamic Data 使用元数据属性进行自动(UI 端?)验证。 http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.aspx。
回顾
所有这些都可以组合使用,但这样会导致业务逻辑分布在六个不同的逻辑位置,对于复杂的模型来说,这可能会导致一个难以管理的类组合。
属性更改的部分方法非常有用,例如,可以禁止明显的错误用户输入。在任何模型中都是必需的。ObjectContext
和 ObjectStateManager
的事件可能需要大量的处理和 if/else。我希望那里有一个解决方案,保持其整洁和单一职责。Dynamic Data 和 Data Services 是建立在模型之上的技术,如果你只想保留逻辑但将其用于其他技术,它们会限制重用性。
需要拦截机制
这一切都运行良好,甚至很棒,Entity Framework 是一项伟大的技术,入门门槛非常低,可以让开发人员在深入研究 XML 时逐渐变得更高级。但我仍然觉得我遗漏了什么。理想情况下,我想:
- 将相关的逻辑(方面)分组到单一职责的类中,尽可能地将日志记录和验证的关注点与上下文和实体的代码分离
- 避免用数百个
if
或case
语句使我的上下文类变得混乱 - 轻松地向我的管道添加或删除逻辑,包括硬编码和配置,这样我就不必重新部署
- 释放我的模型免受视图的限制,并使其可重用于许多视图(管理视图、编辑器视图、客户端视图、Web 视图等)
- 为我的实体提供细粒度的控制,以及对日志记录的相同控制
- 使我的实体更容易保持无参构造函数,这样生成的网站和视图(例如 Dynamic Data)可以更好地处理它们,但仍然无法执行我不希望发生的操作。这增加了 Dynamic Data 作为模型功能测试的用途,因为它完全支持继承(截至撰写本文时,Dynamic Data vNext 在派生类添加导航属性方面仍存在问题)。
我快速编写了一些类来满足我的大部分需求;如果你觉得实体验证和日志记录的标准选项有点麻烦,我鼓励你尝试这个项目,并给我一些关于如何更好地设计它的想法。
Entity Framework 拦截器
设计
我在设计拦截器时还考虑了以下几点:
- 在对象层次结构中,保持一个类的单一职责是困难的。你可以选择构建自己的拦截器层次结构,使用继承。这很可能与模型中的继承树非常相似。你也可以选择让引擎(“调度器”)对所有可分配的类型运行所有验证程序。我仍然不确定我最喜欢哪种方式,所以我保留了选择的选项。
- 我希望能够选择运行时是否会尝试拦截我未指定要查看的类型,并检查是否指定了它知道的基础类型或接口,或者只处理我用指定的拦截器指定的类型。
- 我希望能够使用属性、属性上的属性、配置,或者直接在构造函数中传递相关的拦截器,并能够将拦截器分组到一个逻辑名称中,以便我知道我添加了什么。
- 我将拦截器添加到上下文中。它们拦截上下文事件,所以我认为将它们添加到那里是合乎逻辑的。上下文是实体对象所处环境的最终负责人,所以对我来说在那里也是有意义的。此外,我可以想象在另一个上下文中,同一个实体可能表现不同,甚至可能无效。
它们如何工作
ObjectContextInterceptorDispatcher
处理对象上下文的 SavingChanges
事件,以及 ObjectStateManager
的 ObjectStateManagerChanged
事件。从那里开始,每次有东西被加载到存储中或持久化到数据库时,相关的实体类型就会被指定的任何拦截器拦截(如果你的设置如此,也会被兼容的拦截器拦截)。在这些拦截过程中,你可以进行验证、日志记录、分配 ID 或添加在拦截过程中未填写的必需值。如果你在拦截过程中抛出异常,保存应该会被上下文中止,从而使你的存储保持一致状态。
Using the Code
初始化一个 ObjectContextInterceptorDispatcher 到一个 Context 类型
下面的代码示例说明了如何创建一个可以被拦截影响的实体类:
public partial class Entities
{
//this class will handle all the events
//and move dispatch them to the configured
//interceptors
private ObjectContextInterceptorDispatcher _dispatcher;
partial void OnContextCreated()
{
//override settings found in config
_dispatcher = new ObjectContextInterceptorDispatcher(this,
new ObjectContextInterceptorDispatcherSettings()
{
InheritanceBasedOnEntityTypes = true,
InterceptUnmappedTypes = true,
});
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
//the dispatcher is IDisposable so dispose of it
//when the context is disposed of
_dispatcher.Dispose();
_dispatcher = null;
}
base.Dispose(disposing);
}
}
从那时起,无论是由属性还是 app.config 添加的任何策略或拦截器,都将自动加载,对于派生自你设计的对象上下文的类也是如此。
创建拦截器
有几种类型的拦截器,遵循不同的接口。我可以选择一个通用接口以获得更好的编译时检查,但它们没有太大的共同点。有五种类型的拦截器:
- 在实体保存时拦截的拦截器(
IEntitySaveInterceptor
) - 监视关联更改的拦截器(
IAssociationSaveInterceptor
) - 处理跟踪中更改的拦截器(
IEntityTrackingInterceptor
) - 原始事件处理程序作为拦截器(
IContextSaveInterceptor
和IContextTrackingInterceptor
),允许在其他三种类型之前或之后调用拦截器;例如,这有助于进行一些后验证。
实体保存
继承抽象类 EntitySaveInterceptor<T>
以获得强类型访问,或者自己输入类型并使用 IEntitySaveInterceptor
接口,允许一个类拦截多个实体类型。
public class ClientSaveInterceptor : EntitySaveInterceptor<Client>
{
public override void InterceptEntityInsert(Client entity)
{
//add logic here
}
public override void InterceptEntityDelete(Client entity)
{
//add logic here
}
public override void InterceptEntityUpdate(Client entity,
IExtendedDataRecord originalvalues,
IEnumerable<string> changedproperties)
{
//add logic here
}
}
关联保存
继承抽象类 AssociationSaveInterceptor<PKTYPE, FKTYPE>
以获得强类型访问。或者,IAssociationSaveInterceptor
接口提供非类型化访问。
public class FK_ProductRegistration_Client_NoUpdateRule :
AssociationSaveInterceptor<Client, ProductRegistration>
{
public FK_ProductRegistration_Client_NoUpdateRule()
{
Console.WriteLine("Intercepting FK_ProductRegistration_Client");
}
public override void InterceptAssociationSaveInsert
(ObjectContext context, AssociationType association,
Client pk, ProductRegistration fk)
{
//add logic here
}
public override void InterceptAssociationSaveRemove
(ObjectContext context, AssociationType association,
Client pk, ProductRegistration fk)
{
//if the fk has a new reference to an association, it was updated
if (fk.Client != null)
throw new UnauthorizedAccessException("Product registrations " +
"cannot change clients. Remove the registration and add a new one.");
}
}
实体加载到跟踪或从跟踪中卸载
实现 IEntityTrackingInterceptor
接口
public class IdSetterInterceptor : IEntityTrackingInterceptor{
public void InterceptAddToTracking
(System.Data.Objects.ObjectContext context, object obj)
{
//tries to set a new Guid to every 'Id' property on objects it handles
try
{
PropertyInfo pi = obj.GetType().GetProperty("Id");
if (pi != null)
{
Guid guid = (Guid)pi.GetValue(obj, null);
if (guid.Equals(Guid.Empty))
{
guid = Guid.NewGuid();
pi.GetSetMethod().Invoke(obj, new object[1] { guid });
}
}
} catch { }
}
public void InterceptRemoveFromTracking
(System.Data.Objects.ObjectContext context, object obj)
{ ; }
}
原始事件:上下文保存
实现 IContextSaveInterceptor
接口以处理来自 ObjectContext
的 SavingChanges
的原始事件。
public class AlwaysBeforeContextSaveInterceptor
: IContextSaveInterceptor
{
public InterceptTime InterceptTime
{
get
{
return InterceptTime.Before;
}
set
{
;
}
}
public void InterceptSave
(System.Data.Objects.ObjectContext context)
{
//this is like adding the interceptor functions
//to the object context event handler
//add logic here
}
}
原始事件:从数据库加载/从跟踪中卸载
实现 IContextTrackingInterceptor
接口以处理来自处理对象状态跟踪的 ObjectStateManager
的事件。
public class AlwaysBeforeContextTrackingInterceptor : IContextTrackingInterceptor{
public InterceptTime InterceptTime
{
get
{
return InterceptTime.Before;
}
set
{
;
}
}
public void InterceptTracking(System.Data.Objects.ObjectContext context,
System.ComponentModel.CollectionChangeAction action, object obj)
{
//this is similar to handling the raw ObjectStateManager event
//add logic here
}
}
向 Context 类型添加属性
你可以添加属性或使用 app.config。两者都可以组合使用,但重复的拦截器不会被检测到,所以如果你添加了两次相同的日志记录拦截器,你将得到两个日志条目。
InterceptEntitySaveAttribute
添加实体保存拦截器
[InterceptEntitySave(
InterceptedType = typeof(Client),
InterceptorType = typeof(ClientInterceptor))]
InterceptAssociationSaveAttribute
[InterceptAssociationSave(
EdmName = "DataModel.FK_ProductRegistration_Client",
InterceptorType = typeof(FK_ProductRegistration_Client_NoUpdateRule))]
InterceptEntityTrackingAttribute
[InterceptEntityTracking(
InterceptedType = typeof(Person),
InterceptorType = typeof(IdSetterInterceptor))]
InterceptTrackingAttribute
处理原始 ObjectStateManager
事件
[InterceptTracking(
InterceptTime = InterceptTime.Before,
InterceptorType = typeof(TrackingInterceptor))]
InterceptSaveAttribute
处理原始 ObjectContext
事件
[InterceptSave(
InterceptTime = InterceptTime.Before,
InterceptorType = typeof(SaveInterceptor))]
属性可以放在 ObjectContext
派生类上,调度器将加载相关的拦截器。它们也可以放在其他属性上,形成策略(见下文)。
在 app.config 中添加条目
如果很多人使用它,我将为其创建一个 XSD 架构。
示例节
<configSections>
<section name="context-interception"
type="LibEntityIntercept.Config.InterceptorPoliciesSection,
LibEntityIntercept, Version=0.1.0.0, Culture=neutral,
PublicKeyToken=null" allowLocation="true"
allowDefinition="Everywhere"
allowExeDefinition="MachineToApplication"
restartOnExternalChanges="true"
requirePermission="true" />
</configSections>
<context-interception>
<policies>
<add policy-name="Example7Policy" policy-type="">
<entity-save-interceptors>
<add intercept="EntityValidationSample3.Person,
EntityValidationSample3"
handler="EntityValidationSample3.Example7.Person_NoSpaceInLastNameRule,
EntityValidationSample3" />
<add intercept="EntityValidationSample3.Employee,
EntityValidationSample3"
handler="EntityValidationSample3.Example7.Employee_WageBoundariesRule,
EntityValidationSample3" />
<add intercept="EntityValidationSample3.KeyAccountManager,
EntityValidationSample3"
handler="EntityValidationSample3.Example7.KeyAccountManager_NoDeleteRule,
EntityValidationSample3" />
</entity-save-interceptors>
<association-interceptors>
<add association="DataModel.FK_ProductRegistration_Client"
handler="EntityValidationSample3.Example7.
FK_ProductRegistration_Client_NoUpdateRule,
EntityValidationSample3"/>
</association-interceptors>
</add>
</policies>
<contexts>
<add context-type="EntityValidationSample3.Example7.InterceptedEntities,
EntityValidationSample3">
<policies>
<clear />
<add policy-name="Example7Policy" />
</policies>
</add>
</contexts>
<settings intercept-unmapped="true"
build-inheritance="true" no-delete="false"
no-insert="false" no-update="false"/>
</context-interception>
在 LibEntityIntercept 项目文件夹中还有一个 Example.conf 文件。
在构造函数中用拦截器初始化调度器
你也可以通过将一些拦截器设置添加到构造函数调用中来硬编码它们。
_dispatcher = new ObjectContextInterceptorDispatcher(this,
new ObjectContextInterceptorDispatcherSettings()
{
InheritanceBasedOnEntityTypes = true,
InterceptUnmappedTypes = true,
}, new CodePolicy());
你可以将策略或拦截器设置传递给构造函数,它们将被添加。以上所有三种方法都将结合使用。但是,对于调度器设置,只能有一个,所以存在优先级:代码覆盖配置覆盖属性。
其他属性:策略(又名拦截器组)、访问控制
ControlledEntityAccessAttribute
此属性可防止某些类型被加载到 ObjectStateManager
中。
[ControlledEntityAccess(
ControlledEntityAccessInterceptor.ControlledEntityAccessMode.DenySpecified,
typeof(KeyAccountManager))]
InterceptorPolicyAttribute
你可以使用 InterceptorPolicyAttribute
和 AttributedInterceptorPolicyAttribute
将拦截器分组到一个逻辑名称中。例如:
[InterceptEntityTracking(
InterceptedType = typeof(Person),
InterceptorType = typeof(IdSetterInterceptor))]
public class CodePolicy : AttributedInterceptorPolicyAttribute
{
}
请注意,你可以将属性放在 AttributedInterceptorPolicyAttribute
类上。或者,你可以全部通过代码完成并实现抽象类 InterceptorPolicyAttribute
。
调度器设置
|
|
Effect |
|
|
仅使用指定的拦截器处理指定的类型 |
|
|
如果类型本身被拦截,则拦截器将拦截所有可分配的类型 |
|
|
指定的拦截器将拦截所有类型;如果存在精确匹配,将使用该拦截器;否则,将检查类型并使用所有接口和基类进行拦截 |
|
|
拦截器将拦截所有可分配的类型 |
示例
我包含了三个示例项目,共计 7 个示例,可以帮助你开始尝试我刚才介绍的内容。我试图说明如何结合使用属性、配置和构造函数参数来组合拦截器以记录和验证更改。该解决方案附带了我编写的小型库,包括源代码供你随意使用。查看程序 Main
来开始。在运行任何内容之前,你可能需要为每个项目中的 app.config 中的数据库连接字符串进行调整。
注意事项
关于关联的注意事项
继承以及指向另一个实体对象的属性引用都是关联。因此,当你创建继承对象时,你将收到关联更改;你也可以拦截这些更改,但我不太确定我会如何使用它们。
关于自动添加 ID 的注意事项
请记住,如果你在数据库表中忘记指定主键 (PK),并且意外关闭了 ID 分配,保存时可能会发生错误,并且更改仍将被持久化!你将无法再次从数据库加载实体,因为主键将产生两条记录。是的,我以亲身经历告诉你。我学会了始终确保 PK 在数据库级别也是唯一的,除非你想自己通过代码来确保这一点。
理想功能
- 一个更好、更漂亮的配置节(像 WCF 那样)
- 强制执行
DataAnnotation
和ComponentModel
命名空间属性,就像DynamicData
所做的那样
另请阅读
历史
- 发布日期:2009-05-02。