使用ExpressionVisitor以排除软删除的记录在加入 [英] Use ExpressionVisitor to Exclude Soft Deleted Records In Joins

查看:116
本文介绍了使用ExpressionVisitor以排除软删除的记录在加入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有实现了数据库软删除(可空的DateTime称为DeletedDate)的框架。我使用的是存储库来处理像这样的主要实体请求:

  ///<总结> 
///返回实体集合的LINQ的可查询实例。
///< /总结>
公众的IQueryable< T>所有
{
{返回Context.Set< T>()式。(E => e.DeletedDate == NULL); }
}

这个伟大的工程,但我遇到的问题是,当你有导航性能,以及如何确保只有活动的记录查询。有问题的库法开始是这样的:

  ///<总结> 
///返回实体集合的LINQ的可查询实例,允许连接的对象被加载。
///< /总结>
///&下; PARAM NAME =includeProperties>连接对象被包括在结果集中&下; /参数>
///<退货和GT;实体的一个IQueryable收集<​​; /回报>
公众的IQueryable< T> AllIncluding(PARAMS表达式来; Func键< T,对象>> [] includeProperties)
{
&IQueryable的LT; T>查询= Context.Set< T>()式。(E => e.DeletedDate == NULL);

的foreach(在includeProperties VAR includeProperty)
{
=查询query.Include(includeProperty);
}

返回查询;
}



因此​​,如果存储库正在使用的实体称为父具有导航财产被称为儿童的AllIncluding方法将正确地过滤出软删除父记录,但软删除儿童的记录仍然会包括在内。



纵观查询发送到数据库,似乎所有需要做的是添加到SQL加盟条款和Children.DeletedDate IS NULL,而查询将返回正确的结果。



在我的研究,我发现这个帖子这似乎正是我需要的,但我的实现没有得到过的海报相同的结果。 。通过代码步进,似乎没有任何发生在查询的儿童部分。



下面是我当前的相关代码的(注:从的NuGet使用QueryInterceptor):​​



BaseClass的:

 ;使用System.ComponentModel.DataAnnotations 
;
使用System.ComponentModel.DataAnnotations.Schema;

命名空间DomainClasses
{
///<总结>
///作为基类的所有数据模型类
///< /总结>
公共类BaseClass的
{
///<总结>
///默认的构造,设置EntityState以不变。
///< /总结>
公众的BaseClass()
{
this.StateOfEntity = DomainClasses.StateOfEntity.Unchanged;
}

///<总结>
///指示实体的当前状态。没有映射到数据库。
///< /总结>
[NotMapped]
公共StateOfEntity StateOfEntity {搞定;组; }

///<总结>
///实体主键。
///< /总结>
[键,列(令= 0),ScaffoldColumn(假)]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)
公众诠释标识{搞定;组; }

///<总结>
///实体记录的创建日期。更新InsightDb.SaveChanges()方法
///< /总结>
[列(订单= 1,类型名=DATETIME2),ScaffoldColumn(假)]
公众的DateTime ADDDATE {搞定;组; }

///<总结>
///谁创造了实体记录用户的用户名。更新InsightDb.SaveChanges()方法
///< /总结>
[StringLength(56),列(顺序= 2),ScaffoldColumn(假)]
公共字符串ADDUSER {搞定;组; }

///<总结>
///实体记录被修改日期。更新InsightDb.SaveChanges()方法
///< /总结>
[列(顺序= 3,类型名=DATETIME2),ScaffoldColumn(假)]
公众的DateTime修改日期{搞定;组; }

///<总结>
///谁修改了实体记录用户的用户名。
///< /总结>
[StringLength(56),列(订单= 4),ScaffoldColumn(假)]
公共字符串ModUser {搞定;组; }

///<总结>
///允许软删除的记录。
///< /总结>
[列(订单= 5,类型名=DATETIME2),ScaffoldColumn(假)]
公众的DateTime? DeletedDate {搞定;组; }
}
}



父类:

 使用System.Collections.Generic;使用System.ComponentModel.DataAnnotations 
;

命名空间DomainClasses
{
///<总结>
///父实体。
///< /总结>
公共类家长:BaseClass的
{
///<总结>
///父实例化一个新的实例,初始化虚拟场景。
///< /总结>
公共父()
{
this.Children =新的HashSet<儿童>();
}

#区域属性

///<总结>
///母公司的名称
///< /总结>
[StringLength(50),需要的显示(名称=父名)]
公共字符串名称{;组; }

#endregion

#地区的关系
///<总结>
///与儿童的关系,1父=许多儿童。
///< /总结>
公共虚拟的ICollection<儿童>儿童{搞定;组; }

#endregion
}
}



子类:

 使用System.Collections.Generic;使用System.ComponentModel.DataAnnotations 
;
使用System.ComponentModel.DataAnnotations.Schema;

命名空间DomainClasses
{
///<总结>
///子实体。一位家长=许多儿童
///< /总结>
公共类儿童:BaseClass的
{
#区域属性

///<总结>
///儿童名称。
///< /总结>
[必填,StringLength(50),显示(NAME =儿童名称)]
公共字符串名称{;组; }

#endregion

#地区的关系
///<总结>
///父关系。 1父=许多儿童。
///< /总结>
公共虚拟父父{搞定;组; }

#endregion
}
}



上下文类:

 使用DomainClasses; 
使用系统;
使用System.Data这;
使用System.Data.Entity的;
使用System.Linq的;

命名空间的dataLayer
{
公共类DemoContext:的DbContext,IDemoContext
{
///<总结>
/// ActiveSession执行该操作的用户的对象。
///< /总结>
公共ActiveSession ActiveSession {搞定;私人集; }

公共DemoContext(ActiveSession activeSession)
:基地(NAME = demodb的)
{
ActiveSession = activeSession;
this.Configuration.LazyLoadingEnabled = FALSE;
}

#地区分贝映射

公共IDbSet<儿童>儿童{搞定;组; }
公共IDbSet<家长和GT;家长{搞定;组; }

#endregion

公共覆盖INT的SaveChanges()
{
VAR变更= ChangeTracker.Entries<&的BaseClass GT;();

如果(变更!= NULL)
{
的foreach(VAR在changeSet.Where记录(c =>!c.State = EntityState.Unchanged))
{
entry.Entity.ModDate = DateTime.UtcNow;
entry.Entity.ModUser = ActiveSession.UserName;

如果(entry.State == EntityState.Added)
{
entry.Entity.AddDate = DateTime.UtcNow;
entry.Entity.AddUser = ActiveSession.UserName;
}
,否则如果(entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.DeletedDate = DateTime.UtcNow;
}
}
}

返回base.SaveChanges();
}

公开新IDbSet< T>设置< T>()其中T:BaseClass的
{
回报率((的DbContext)这一点)。设置< T>();
}
}
}



仓储类:

 使用DomainClasses; 
使用QueryInterceptor;
使用系统;
使用System.Data.Entity的;
使用System.Linq的;使用System.Linq.Expressions
;

命名空间的dataLayer
{
///<总结>
///实体储存库在业务层中使用。
///< /总结>
公共类EntityRepository< T> :IEntityRepository< T>其中T:BaseClass的
{
公共IDemoContext语境{搞定;私人集; }

///<总结>
///主要构造库。创建DemoContext的实例(从的DbContext派生)。
///< /总结>
///< PARAM NAME =activeSession>执行该操作的用户的用户名和LT; /参数>
公共EntityRepository(ActiveSession activeSession)
:这(新DemoContext(activeSession))
{
}

///<总结>
///构造函数库。允许范围内(即FakeDemoContext)传递中进行测试。
///< /总结>
///< PARAM NAME =上下文> IDemoContext资源库中的使用。即FakeDemoContext< /参数>
公共EntityRepository(IDemoContext上下文)
{
=上下文语境;
}

///<总结>
///返回实体集合的LINQ的可查询实例。
///< /总结>
公众的IQueryable< T>所有
{
{返回Context.Set< T>()式。(E => e.DeletedDate == NULL); }
}

///<总结>
///返回实体集合的LINQ的可查询实例,允许连接的对象被加载。
///< /总结>
///&下; PARAM NAME =includeProperties>连接对象被包括在结果集中&下; /参数>
///<退货和GT;实体的一个IQueryable收集<​​; /回报>
公众的IQueryable< T> AllIncluding(PARAMS表达式来; Func键< T,对象>> [] includeProperties)
{
&IQueryable的LT; T>查询= Context.Set< T>()式。(E => e.DeletedDate == NULL);

InjectConditionVisitor ICV =新InjectConditionVisitor();

的foreach(在includeProperties VAR includeProperty)
{
=查询query.Include(includeProperty);
}

返回query.InterceptWith(ICV);
}

///<总结>
///查找由标识该实体的单个实例。
///< /总结>
///< PARAM NAME =ID>作为实体的主键与LT; /参数>
///<返回>该实体的实例< /回报>
公共找不到(INT ID)
{
返回Context.Set< T>()式(E => e.DeletedDate == NULL)。.SingleOrDefault(E => e.Id == ID);
}

///<总结>
///取得一个单一的实体或实体图和读取明确的状态,然后应用所需的状态变化更新或添加的实体。
///< /总结>
///&下; PARAM NAME =实体>该实体对象与所述; /参数>
公共无效InsertOrUpdate(T实体)
{
如果(entity.StateOfEntity == StateOfEntity.Added)
{
Context.Set< T>()添加(实体);
}
,否则
{
Context.Set< T>()添加(实体);
Context.ApplyStateChanges();
}
}

///<总结>
///删除的实体的实例。
///< /总结>
///< PARAM NAME =ID>该实体的主键与LT; /参数>
公共无效删除(INT ID)
{
VAR实体= Context.Set< T>()式。(E => e.DeletedDate == NULL).SingleOrDefault(E = > e.Id == ID);
entity.StateOfEntity = StateOfEntity.Deleted;
Context.Set< T>()删除(实体);
}

///<总结>
///保存交易。
///< /总结>
公共无效保存()
{
Context.SaveChanges();
}

///<总结>
///部署存储库。
///< /总结>
公共无效的Dispose()
{
Context.Dispose();
}
}
}



InjectConditionVisitor类:

 使用系统; 
使用System.Linq的;使用System.Linq.Expressions
;

命名空间的dataLayer
{
公共类InjectConditionVisitor:ExpressionVisitor
{
私人QueryConditional queryCondition;

公共InjectConditionVisitor(QueryConditional情况):
{
queryCondition =条件;
}

公共InjectConditionVisitor()
{
queryCondition =新QueryConditional(X => x.DeletedDate == NULL);
}

保护覆盖表达VisitMember(MemberExpression前)
{
//只有改变泛型类型=导航属性
//否则只执行正常码。
的回报!ex.Type.IsGenericType? base.VisitMember(前):CreateWhereExpression(queryCondition,EX)? base.VisitMember(除息);
}

///<总结>
///创建具有适应QueryConditional
///<在那里表达/总结​​>
///&下; PARAM NAME =条件>至使用的条件与下/参数>
///< PARAM NAME =EX>我们正在访问<的MemberExpression; /参数>
///<&回报GT;< /回报>
私人表达CreateWhereExpression(QueryConditional条件,表达EX)
{
变种类型= ex.Type; // GetGenericArguments()(第)。
VAR测试= CreateExpression(条件型);
如果(测试== NULL)
返回NULL;
变种listType = typeof运算(IQueryable的&所述;>)MakeGenericType(类型)。
返回Expression.Convert(Expression.Call(typeof运算(可枚举),去哪儿,新类型[] {}型,(表达)前,测试),listType);
}

///<总结>
///适配QueryConditional到我们目前正在参观的成员。
///< /总结>
///&下; PARAM NAME =条件>到适应的条件和所述; /参数>
///&下; PARAM NAME =类型>该类型的当前部件(=导航属性)所述的; /参数>
///<返回>在适应QueryConditional< /回报>
私人LambdaExpression CreateExpression(QueryConditional条件,种类型)
{
VAR波长=(LambdaExpression)condition.Conditional; 。
VAR conditionType = condition.Conditional.GetType()GetGenericArguments()FirstOrDefault();当条件适用于会员
如果(conditionType == NULL)
返回NULL的类型
//只有不断;
如果(conditionType.IsAssignableFrom(型)!)
返回NULL;

变种newParams =新[] {Expression.Parameter(类型,博)};
VAR paramMap = lambda.Parameters.Select((原件,I)=>新建{原创,更换= newParams [I]})ToDictionary(P =方式> p.original,P =指p。替代);
VAR fixedBody = ParameterRebinder.ReplaceParameters(paramMap,lambda.Body);
波长= Expression.Lambda(fixedBody,newParams);

返回的lambda;
}
}
}



QueryConditional类:

 使用DomainClasses; 
使用系统;使用System.Linq.Expressions
;

命名空间的dataLayer
{
公共类QueryConditional
{
公共QueryConditional(表达式来; Func键< BaseClass的,布尔>>除息)
{
=条件前;
}

公共表达式来; Func键< BaseClass的,布尔>>有条件{搞定;组; }
}
}



ParameterRebinder类:

 使用System.Collections.Generic;使用System.Linq.Expressions 
;

命名空间的dataLayer
{
公共类ParameterRebinder:ExpressionVisitor
{
私人只读字典< ParameterExpression,ParameterExpression>地图;

公共ParameterRebinder(词典< ParameterExpression,ParameterExpression>图)
{
this.map =地图?新字典< ParameterExpression,ParameterExpression>();
}

公共静态表达ReplaceParameters(词典< ParameterExpression,ParameterExpression>地图,表达式exp)
{
返回新ParameterRebinder(图).Visit(EXP);
}

保护覆盖表达VisitParameter(ParameterExpression节点)
{
ParameterExpression更换;

如果(map.TryGetValue(节点,置换))
节点=更换;

返回base.VisitParameter(节点);
}
}
}



IEntityRepository接口:

 使用系统; 
使用System.Linq的;使用System.Linq.Expressions
;

命名空间的dataLayer
{
公共接口IEntityRepository< T> :IDisposable的
{
&IQueryable的LT; T>所有{搞定; }
&IQueryable的LT; T> AllIncluding(PARAMS表达式来; Func键< T,对象>> [] includeProperties);
找不到(INT ID);
无效InsertOrUpdate(T实体);
无效删除(INT ID);
无效保存();
}
}



IDemoContext接口:

 使用DomainClasses; 
使用系统;
使用System.Data.Entity的;

命名空间的dataLayer
{
公共接口IDemoContext:IDisposable的
{
ActiveSession ActiveSession {搞定; }

IDbSet<儿童>儿童{搞定; }
IDbSet<家长和GT;家长{搞定; }

INT的SaveChanges();

IDbSet< T>设置< T>()其中T:BaseClass的;
}
}


解决方案

我从来没有能够弄清楚表达客人,已经花了足够的时间就可以了。所以,我最终只是删除记录处理这一个表触发器如果DeletedDate是不为空。



软删除的最初目的是跟踪谁删了记录在应用程序中。我设置的更改保存上下文mod的用户,但在一个缺失这个没有更新,所以没有谁做了删除的审核。



我已经有一个更新后和之后删除为每个表我是审计和为每个表相关的审计表触发器。触发器基本上插入旧记录插入到审计表随时有更新或删除。审计表和触发器通过存储过程创建:

  CREATE PROCEDURE [DBO] [CreateAuditTable(
。 @tablename NVARCHAR(100),
@SchemaName NVARCHAR(50)


/ *
------------- -------------------------------------------------- --------------------------------------
*程序名称:dbo.CreateAuditTable
*作者:约什 - 杰伊
*日期:2013年3月15日
*说明:从现有的表创建一个审计表。
---------------------------------------------- -------------------------------------------------- -----
SL没有修改日期修改通过更改
------- ------------- ---------- ------- ------------------------------------------- ------
1 2013年7月1日乔希杰伊删除表的别名参数,并替换表名称的使用。
2 2013年8月28日乔希杰伊修改更新语句删除行,如果它是一个软删除。
---------------------------------------------- -------------------------------------------------- -----

例:
EXEC dbo.CreateAuditTable
@tablename ='产品',
@SchemaName ='DBO'

* /
BEGIN
DECLARE @IssueCount INT = 0,
@IssueList NVARCHAR(MAX)= NULL,
@LineBreak NVARCHAR(50)= REPLICATE(' - ', 50)
@CreateTableScript NVARCHAR(MAX)= NULL,
@CreateDeleteScript NVARCHAR(MAX)= NULL,
@CreateUpdateScript NVARCHAR(MAX)= NULL,
@ColumnNamesSection NVARCHAR( MAX)= NULL,
@TableObjectId INT,
@msg VARCHAR(1024);

--1)检查表是否存在
如果NOT EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @SchemaName和TABLE_NAME = @tablename)
BEGIN
SET @IssueCount = @IssueCount + 1;
设置@IssueList = ISNULL(@IssueList + CHAR(10),'')+ CONVERT(VARCHAR,@ IssueCount)+')表'+ @SchemaName +。+ @tablename +'不存在';
端;

--2)检查审计表是否存在
如果EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @SchemaName和TABLE_NAME = @tablename +'_Audit')
BEGIN
设置@IssueCount = @IssueCount + 1;
设置@IssueList = ISNULL(@IssueList + CHAR(10),'')+ CONVERT(VARCHAR,@ IssueCount)+')审核表+ @SchemaName +。+ @tablename +'_Audit已经存在。要重新审计表,请删除现有的审计表,然后再试一次。
端;

--3)检查现有的触发
如果不存在(选择1 FROM sys.triggers TR INNER JOIN上tr.parent_id = t.object_id
,其中T SYS.TABLESŤ .schema_id = SCHEMA_ID(@SchemaName)和t.name = @tablename和tr.name LIKE'%tg_%AUDIT_')
BEGIN
设置@IssueCount = @IssueCount + 1;
设置@IssueList = ISNULL(@IssueList + CHAR(10),'')+ CONVERT(VARCHAR,@ IssueCount)+')至少有一个审计触发器存在于'+ @SchemaName +''+ @表名+'表。要重新审计表,请给审计触发器。
端;

--4)打印错误,如果有任何
如果@IssueCount> 0
BEGIN
打印('有'+ CONVERT(VARCHAR,@ IssueCount)+'试图创建审计表时发现的问题,然后重试请更正以下问题。');
打印(@LineBreak);
打印(@IssueList);
返回;
端;

--5)构建脚本
选择
@CreateTableScript =
'CREATE TABLE ['+ SS.name +'] ['+ ST.name + _Audit]'+ CHAR(10)+
'('+ CHAR(10)+
CHAR(9)+'[AuditId] INT IDENTITY(1,1)NOT NULL约束[PK_'+ @SchemaName +。+ @tablename +'_Audit_AuditId] PRIMARY KEY,+ CHAR(10)+
CHAR(9)+'[AuditDate] DATETIME NOT NULL约束[DF_'+ @SchemaName +'。 + @tablename +'_Audit_AuditDate] DEFAULT(GETUTCDATE()),+ CHAR(10)+
CHAR(9)+'[AuditIsDelete] BIT NOT NULL约束[DF_'+ @SchemaName +''+ @表名+'_Audit_AuditIsDelete] DEFAULT((0))',
@CreateDeleteScript =
'CREATE TRIGGER [DBO]。[tg_'+ @SchemaName +。+ @tablename +'_Audit_Delete]'+ CHAR(10)+
'ON ['+ SS.name +'] ['+ ST.name +']'+ CHAR(10)+
+ CHAR删除后(10) +
作为开始'+ CHAR(10)+
CHAR(9)+'IF TRIGGER_NESTLEVEL()> 1'+ CHAR(10)+
CHAR(9)+ CHAR(9)+返回+ CHAR(10)+
CHAR(10)+
CHAR(9)+' INSERT INTO'+ CHAR(10)+
CHAR(9)+ CHAR(9)+'['+ SS.name +'] ['+ ST.name +'_Audit('+ CHAR(10 )+
CHAR(9)+ CHAR(9)+ CHAR(9)+'[AuditIsDelete]',
@CreateUpdateScript =
'CREATE TRIGGER [DBO]。[tg_'+ @的SchemaName +。+ @tablename +'_Audit_Update]'+ CHAR(10)+
'ON ['+ SS.name +'] ['+ ST.name +']'+ CHAR(10) +
+ CHAR(10)更新后+
作为开始'+ CHAR(10)+
CHAR(9)+'IF TRIGGER_NESTLEVEL()> 1'+ CHAR( 10)+
CHAR(9)+ CHAR(9)+返回+ CHAR(10)+
CHAR(10)+
CHAR(9)+'INSERT INTO'+ CHAR (10)+
CHAR(9)+ CHAR(9)+'['+ SS.name +]。[+ ST.name +'_Audit](+ CHAR(10)+
CHAR(9)+ CHAR(9)+ CHAR(9)+从
'[AuditIsDelete]'
SYS.TABLES ST
INNER JOIN
sys.schemas SS ON ST .schema_id = SS.schema_id
,其中
ST.name = @tablename和
ST.type ='U'和
SS.name = @SchemaName

选择
@CreateTableScript = @CreateTableScript +,+ CHAR(10)+ CHAR(9)+'['+ ISC.COLUMN_NAME +']'+ ISC.DATA_TYPE + CASE WHEN ISC.CHARACTER_MAXIMUM_LENGTH IS NOT NULL和ISC.DATA_TYPE<> 'XML',那么'('+ CASE WHEN ISC.CHARACTER_MAXIMUM_LENGTH = -1,那么最大ELSE CONVERT(VARCHAR,ISC.CHARACTER_MAXIMUM_LENGTH)END +')'ELSE''END +'NULL',
@ColumnNamesSection = ISNULL(@ColumnNamesSection,'')+','+ CHAR(10)+ CHAR(9)+ CHAR(9)+ CHAR(9)+'['+ ISC.COLUMN_NAME +]

INFORMATION_SCHEMA.COLUMNS ISC
,其中
ISC.TABLE_NAME = @tablename和
ISC.TABLE_SCHEMA = @SchemaName
ORDER BY
ISC.ORDINAL_POSITION ASC

设置@CreateTableScript = @CreateTableScript + CHAR(10)+');

设置@CreateDeleteScript = @CreateDeleteScript + @ColumnNamesSection + CHAR(10)+
CHAR( 9)+ CHAR(9)+')'+ CHAR(10)+
CHAR(9)+ CHAR(9)+SELECT+ CHAR(10)+
CHAR(9)+ CHAR (9)+ CHAR(9)+'1为[AuditIsDelete]'+
@ColumnNamesSection + CHAR(10)+
CHAR(9)+ CHAR(9)+'FROM'+ CHAR(10 )+
CHAR(9)+ CHAR(9)+ CHAR(9)+'删除'+ CHAR(10)+
'结束;'

设置@CreateUpdateScript = @CreateUpdateScript + @ColumnNamesSection + CHAR(10)+
CHAR(9)+ CHAR(9)+')'+ CHAR(10)+
CHAR(9)+ CHAR(9)+'SELECT + CHAR(10)+
CHAR(9)+ CHAR(9)+ CHAR(9)+'0为[AuditIsDelete]'+
@ColumnNamesSection + CHAR(10)+
CHAR(9)+ CHAR(9)+'FROM'+ CHAR(10)+
CHAR(9)+ CHAR(9)+ CHAR(9)+已删除+ CHAR(10)+
'声明@SoftDelete位,
@Id INT

选择
@SoftDelete =情况下,当i.DeletedDate不是null,则1否则为0结束,
@Id = i.Id从

插入我;

如果@SoftDelete = 1
开始
INSERT INTO
['+ @SchemaName +'] ['+ @tablename +'_Audit(
〔AuditIsDelete]
'+ @ColumnNamesSection +'

选择
1为[AuditIsDelete]
'+ @ColumnNamesSection +'
起价
插入

从'+ @SchemaName +'删除'+ @tablename +'其中Id = @Id
端;+ CHAR(10)+

'结束;'

--6)打印和运行脚本
BEGIN TRY
BEGIN TRANSACTION;

EXEC(@CreateTableScript);

EXEC(@CreateDeleteScript);

EXEC(@CreateUpdateScript);

- 测试尝试捕捉:
- 选择1/0

COMMIT TRANSACTION;

打印('。审计表创建成功)
端TRY
BEGIN CATCH
ROLLBACK TRANSACTION;

组@msg =
'DB_NAME()='+ ISNULL(DB_NAME(),'NULL')+'; ERROR_MESSAGE()='+
ISNULL(ERROR_MESSAGE(),'NULL')+
'; ERROR_PROCEDURE()='+ ISNULL(ERROR_PROCEDURE(),'NULL')+
'; ERROR_LINE()='+ ISNULL(CONVERT(VARCHAR(10),ERROR_LINE()),'NULL')+
'; ERROR_NUMBER()='+ ISNULL(CONVERT(VARCHAR(10),ERROR_NUMBER()),'NULL')+
'; ERROR_SEVERITY()='+ ISNULL(CONVERT(VARCHAR(10),ERROR_SEVERITY()),'NULL')+
'; ERROR_STATE()='+ ISNULL(CONVERT(VARCHAR(10),ERROR_STATE()),'NULL');

打印(CHAR(10)+'创建审计表脚本:');
打印(@LineBreak);
打印(@CreateTableScript);
打印(@LineBreak);

打印(CHAR(10)+'创建审计删除触发器脚本:');
打印(@LineBreak);
打印(@CreateDeleteScript);
打印(@LineBreak);

打印(CHAR(10)+'创建审计更新触发脚本:');
打印(@LineBreak);
打印(@CreateUpdateScript);
打印(@LineBreak);

RAISERROR(@msg,18,1);
端CATCH
端;



虽然触发器是不理想的,他们完成审核谁删除了用户的目标,我不再需要担心的软删除的记录。


I have a framework which implements Soft Deletes in the Database (Nullable DateTime called DeletedDate). I am using a Repository to handle the main entity requests like so:

/// <summary>
/// Returns a Linq Queryable instance of the entity collection.
/// </summary>
public IQueryable<T> All
{
    get { return Context.Set<T>().Where(e => e.DeletedDate == null); }
}

This works great, but the issue I'm having is when you include navigational properties, and how to make sure only Active records are queried. The repository method in question starts like this:

/// <summary>
/// Returns a Linq Queryable instance of the entity collection, allowing connected objects to be loaded.
/// </summary>
/// <param name="includeProperties">Connected objects to be included in the result set.</param>
/// <returns>An IQueryable collection of entity.</returns>
public IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties)
{
    IQueryable<T> query = Context.Set<T>().Where(e => e.DeletedDate == null);

    foreach (var includeProperty in includeProperties)
    {
        query = query.Include(includeProperty);
    }

    return query;
}

So if the Repository is being used by an entity called Parent which has a navigational property called Children, the AllIncluding method would properly filter out the soft deleted Parent records, but the soft deleted Children records would still be included.

Looking at the query sent to the database, it seems all that needs to be done is add to the sql join clause " AND Children.DeletedDate IS NULL " and the query would return the correct results.

During my research, I found the this post which appears to be exactly what I need, however my implementation doesn't get the same results the poster had. Stepping through the code, nothing seems to happen to the Children part of the query.

Here is my current relevant code (Note: Using QueryInterceptor from nuget):

BaseClass:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace DomainClasses
{
    /// <summary>
    /// Serves as the Base Class for All Data Model Classes
    /// </summary>
    public class BaseClass
    {
        /// <summary>
        /// Default constructor, sets EntityState to Unchanged.
        /// </summary>
        public BaseClass()
        {
            this.StateOfEntity = DomainClasses.StateOfEntity.Unchanged;
        }

        /// <summary>
        /// Indicates the current state of the entity. Not mapped to Database.
        /// </summary>
        [NotMapped]
        public StateOfEntity StateOfEntity { get; set; }

        /// <summary>
        /// The entity primary key.
        /// </summary>
        [Key, Column(Order = 0), ScaffoldColumn(false)]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }

        /// <summary>
        /// The date the entity record was created. Updated in InsightDb.SaveChanges() method
        /// </summary>
        [Column(Order = 1, TypeName = "datetime2"), ScaffoldColumn(false)]
        public DateTime AddDate { get; set; }

        /// <summary>
        /// The UserName of the User who created the entity record. Updated in InsightDb.SaveChanges() method
        /// </summary>
        [StringLength(56), Column(Order = 2), ScaffoldColumn(false)]
        public string AddUser { get; set; }

        /// <summary>
        /// The date the entity record was modified. Updated in InsightDb.SaveChanges() method
        /// </summary>
        [Column(Order = 3, TypeName = "datetime2"), ScaffoldColumn(false)]
        public DateTime ModDate { get; set; }

        /// <summary>
        /// The UserName of the User who modified the entity record.
        /// </summary>
        [StringLength(56), Column(Order = 4), ScaffoldColumn(false)]
        public string ModUser { get; set; }

        /// <summary>
        /// Allows for Soft Delete of records.
        /// </summary>
        [Column(Order = 5, TypeName = "datetime2"), ScaffoldColumn(false)]
        public DateTime? DeletedDate { get; set; }
    }
}

Parent Class:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace DomainClasses
{
    /// <summary>
    /// The Parent Entity.
    /// </summary>
    public class Parent : BaseClass
    {
        /// <summary>
        /// Instantiates a new instance of Parent, initializes the virtual sets.
        /// </summary>
        public Parent()
        {
            this.Children = new HashSet<Child>();
        }

        #region Properties

        /// <summary>
        /// The Parent's Name
        /// </summary>
        [StringLength(50), Required, Display(Name="Parent Name")]
        public string Name { get; set; }

        #endregion

        #region Relationships
        /// <summary>
        /// Relationship to Child, 1 Parent = Many Children.
        /// </summary>
        public virtual ICollection<Child> Children { get; set; }

        #endregion
    }
}

Child Class:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace DomainClasses
{
    /// <summary>
    /// The Child entity. One Parent = Many Children
    /// </summary>
    public class Child : BaseClass
    {
        #region Properties

        /// <summary>
        /// Child Name.
        /// </summary>
        [Required, StringLength(50), Display(Name="Child Name")]
        public string Name { get; set; }

        #endregion

        #region Relationships
        /// <summary>
        /// Parent Relationship. 1 Parent = Many Children.
        /// </summary>
        public virtual Parent Parent { get; set; }

        #endregion
    }
}

Context Class:

using DomainClasses;
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;

namespace DataLayer
{
    public class DemoContext : DbContext, IDemoContext
    {
        /// <summary>
        /// ActiveSession object of the user performing the action.
        /// </summary>
        public ActiveSession ActiveSession { get; private set; }

        public DemoContext(ActiveSession activeSession)
            : base("name=DemoDb")
        {
            ActiveSession = activeSession;
            this.Configuration.LazyLoadingEnabled = false;
        }

        #region Db Mappings

        public IDbSet<Child> Children { get; set; }
        public IDbSet<Parent> Parents { get; set; }

        #endregion

        public override int SaveChanges()
        {
            var changeSet = ChangeTracker.Entries<BaseClass>();

            if (changeSet != null)
            {
                foreach (var entry in changeSet.Where(c => c.State != EntityState.Unchanged))
                {
                    entry.Entity.ModDate = DateTime.UtcNow;
                    entry.Entity.ModUser = ActiveSession.UserName;

                    if (entry.State == EntityState.Added)
                    {
                        entry.Entity.AddDate = DateTime.UtcNow;
                        entry.Entity.AddUser = ActiveSession.UserName;
                    }
                    else if (entry.State == EntityState.Deleted)
                    {
                        entry.State = EntityState.Modified;
                        entry.Entity.DeletedDate = DateTime.UtcNow;
                    }
                }
            }

            return base.SaveChanges();
        }

        public new IDbSet<T> Set<T>() where T : BaseClass
        {
            return ((DbContext)this).Set<T>();
        }
    }
}

Repository Class:

using DomainClasses;
using QueryInterceptor;
using System;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;

namespace DataLayer
{ 
    /// <summary>
    /// Entity Repository to be used in Business Layer.
    /// </summary>
    public class EntityRepository<T> : IEntityRepository<T> where T : BaseClass
    {
        public IDemoContext Context { get; private set; }

        /// <summary>
        /// Main Constructor for Repository. Creates an instance of DemoContext (derives from DbContext).
        /// </summary>
        /// <param name="activeSession">UserName of the User performing the action.</param>
        public EntityRepository(ActiveSession activeSession)
            : this(new DemoContext(activeSession))
        {
        }

        /// <summary>
        /// Constructor for Repository. Allows a context (i.e. FakeDemoContext) to be passed in for testing.
        /// </summary>
        /// <param name="context">IDemoContext to be used in the repository. I.e. FakeDemoContext.</param>
        public EntityRepository(IDemoContext context)
        {
            Context = context;
        }

        /// <summary>
        /// Returns a Linq Queryable instance of the entity collection.
        /// </summary>
        public IQueryable<T> All
        {
            get { return Context.Set<T>().Where(e => e.DeletedDate == null); }
        }

        /// <summary>
        /// Returns a Linq Queryable instance of the entity collection, allowing connected objects to be loaded.
        /// </summary>
        /// <param name="includeProperties">Connected objects to be included in the result set.</param>
        /// <returns>An IQueryable collection of entity.</returns>
        public IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties)
        {
            IQueryable<T> query = Context.Set<T>().Where(e => e.DeletedDate == null);

            InjectConditionVisitor icv = new InjectConditionVisitor();

            foreach (var includeProperty in includeProperties)
            {
                query = query.Include(includeProperty);
            }

            return query.InterceptWith(icv);
        }

        /// <summary>
        /// Finds a single instance of the entity by the Id.
        /// </summary>
        /// <param name="id">The primary key for the entity.</param>
        /// <returns>An instance of the entity.</returns>
        public T Find(int id)
        {
            return Context.Set<T>().Where(e => e.DeletedDate == null).SingleOrDefault(e => e.Id == id);
        }

        /// <summary>
        /// Takes a single entity or entity graph and reads the explicit state, then applies the necessary State changes to Update or Add the entities.
        /// </summary>
        /// <param name="entity">The entity object.</param>
        public void InsertOrUpdate(T entity)
        {
            if (entity.StateOfEntity == StateOfEntity.Added)
            {
                Context.Set<T>().Add(entity);
            }
            else
            {
                Context.Set<T>().Add(entity);
                Context.ApplyStateChanges();
            }
        }

        /// <summary>
        /// Deletes the instance of the entity.
        /// </summary>
        /// <param name="id">The primary key of the entity.</param>
        public void Delete(int id)
        {
            var entity = Context.Set<T>().Where(e => e.DeletedDate == null).SingleOrDefault(e => e.Id == id);
            entity.StateOfEntity = StateOfEntity.Deleted;
            Context.Set<T>().Remove(entity);
        }

        /// <summary>
        /// Saves the transaction.
        /// </summary>
        public void Save()
        {
            Context.SaveChanges();
        }

        /// <summary>
        /// Disposes the Repository.
        /// </summary>
        public void Dispose() 
        {
            Context.Dispose();
        }
    }
}

InjectConditionVisitor Class:

using System;
using System.Linq;
using System.Linq.Expressions;

namespace DataLayer
{
    public class InjectConditionVisitor : ExpressionVisitor
    {
        private QueryConditional queryCondition;

        public InjectConditionVisitor(QueryConditional condition)
        {
            queryCondition = condition;
        }

        public InjectConditionVisitor()
        {
            queryCondition = new QueryConditional(x => x.DeletedDate == null);
        }

        protected override Expression VisitMember(MemberExpression ex)
        {
            // Only change generic types = Navigation Properties
            // else just execute the normal code.
            return !ex.Type.IsGenericType ? base.VisitMember(ex) : CreateWhereExpression(queryCondition, ex) ?? base.VisitMember(ex);
        }

        /// <summary>
        /// Create the where expression with the adapted QueryConditional
        /// </summary>
        /// <param name="condition">The condition to use</param>
        /// <param name="ex">The MemberExpression we're visiting</param>
        /// <returns></returns>
        private Expression CreateWhereExpression(QueryConditional condition, Expression ex)
        {
            var type = ex.Type;//.GetGenericArguments().First();
            var test = CreateExpression(condition, type);
            if (test == null)
                return null;
            var listType = typeof(IQueryable<>).MakeGenericType(type);
            return Expression.Convert(Expression.Call(typeof(Enumerable), "Where", new Type[] { type }, (Expression)ex, test), listType);
        }

        /// <summary>
        /// Adapt a QueryConditional to the member we're currently visiting.
        /// </summary>
        /// <param name="condition">The condition to adapt</param>
        /// <param name="type">The type of the current member (=Navigation property)</param>
        /// <returns>The adapted QueryConditional</returns>
        private LambdaExpression CreateExpression(QueryConditional condition, Type type)
        {
            var lambda = (LambdaExpression)condition.Conditional;
            var conditionType = condition.Conditional.GetType().GetGenericArguments().FirstOrDefault();
            // Only continue when the condition is applicable to the Type of the member
            if (conditionType == null)
                return null;
            if (!conditionType.IsAssignableFrom(type))
                return null;

            var newParams = new[] { Expression.Parameter(type, "bo") };
            var paramMap = lambda.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement);
            var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, lambda.Body);
            lambda = Expression.Lambda(fixedBody, newParams);

            return lambda;
        }
    }
}

QueryConditional Class:

using DomainClasses;
using System;
using System.Linq.Expressions;

namespace DataLayer
{
    public class QueryConditional
    {
        public QueryConditional(Expression<Func<BaseClass, bool>> ex)
        {
            Conditional = ex;
        }

        public Expression<Func<BaseClass, bool>> Conditional { get; set; }
    }
}

ParameterRebinder Class:

using System.Collections.Generic;
using System.Linq.Expressions;

namespace DataLayer
{
    public class ParameterRebinder : ExpressionVisitor
    {
        private readonly Dictionary<ParameterExpression, ParameterExpression> map;

        public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
        {
            this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
        }

        public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
        {
            return new ParameterRebinder(map).Visit(exp);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            ParameterExpression replacement;

            if (map.TryGetValue(node, out replacement))
                node = replacement;

            return base.VisitParameter(node);
        }
    }
}

IEntityRepository Interface:

using System;
using System.Linq;
using System.Linq.Expressions;

namespace DataLayer
{
    public interface IEntityRepository<T> : IDisposable
    {
        IQueryable<T> All { get; }
        IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties);
        T Find(int id);
        void InsertOrUpdate(T entity);
        void Delete(int id);
        void Save();
    }
}

IDemoContext Interface:

using DomainClasses;
using System;
using System.Data.Entity;

namespace DataLayer
{
    public interface IDemoContext : IDisposable
    {
        ActiveSession ActiveSession { get; }

        IDbSet<Child> Children { get; }
        IDbSet<Parent> Parents { get; }

        int SaveChanges();

        IDbSet<T> Set<T>() where T : BaseClass;
    }
}

解决方案

I never was able to figure out the expression visitor, and had already spent enough time on it. So I ended up just handling this in a Table Trigger by deleting the record if the DeletedDate was not null.

The original purpose of the soft delete was to track who deleted the record in the application. I was setting the Mod User in the save changes context, but on a deletion this doesn't get updated, so there isn't an audit of who did the deletion.

I already had an "After Update" and "After Delete" trigger for each table I was auditing and an associated audit table for each table. The triggers basically insert the old record into the audit table anytime there is an update or delete. The Audit tables and triggers are created through a stored procedure:

CREATE PROCEDURE [dbo].[CreateAuditTable](
    @TableName NVARCHAR(100),
    @SchemaName NVARCHAR(50)
)
as
/*
-----------------------------------------------------------------------------------------------------
 * Procedure Name   : dbo.CreateAuditTable
 * Author           : Josh Jay
 * Date             : 03/15/2013
 * Description      : Creates an Audit table from an existing table.
-----------------------------------------------------------------------------------------------------
 Sl No      Date Modified       Modified By         Changes 
-------     -------------       -----------------   -------------------------------------------------
  1         07/01/2013          Josh Jay            Removed the table alias parameter and replaced usage with table name.
  2         08/28/2013          Josh Jay            Modified the Update Statement to Delete the Row if it is a Soft Delete.
-----------------------------------------------------------------------------------------------------

Ex:
EXEC dbo.CreateAuditTable
    @TableName = 'Product',
    @SchemaName = 'dbo'

*/
BEGIN
DECLARE @IssueCount INT = 0,
        @IssueList NVARCHAR(MAX) = NULL,
        @LineBreak NVARCHAR(50) = REPLICATE('-',50),
        @CreateTableScript NVARCHAR(MAX) = NULL,
        @CreateDeleteScript NVARCHAR(MAX) = NULL,
        @CreateUpdateScript NVARCHAR(MAX) = NULL,
        @ColumnNamesSection NVARCHAR(MAX) = NULL,
        @TableObjectId INT,
        @msg varchar(1024);

--1) Check if table exists
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @SchemaName AND TABLE_NAME = @TableName)
        BEGIN
            SET @IssueCount = @IssueCount + 1;
            SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') The table ' + @SchemaName + '.' + @Tablename + ' does not exist.';
        END;

--2) Check if audit table exists
    IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @SchemaName AND TABLE_NAME = @TableName + '_Audit')
        BEGIN
            SET @IssueCount = @IssueCount + 1;
            SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') The audit table ' + @SchemaName + '.' + @Tablename + '_Audit already exists. To recreate the audit table, please drop the existing audit table and try again.';
        END;

--3) Check for existing triggers
    IF EXISTS (SELECT 1 FROM sys.triggers tr INNER JOIN sys.tables t on tr.parent_id = t.object_id
                WHERE t.schema_id = SCHEMA_ID(@SchemaName) AND t.name = @TableName AND tr.name LIKE 'tg_%Audit_%')
        BEGIN
            SET @IssueCount = @IssueCount + 1;
            SET @IssueList = ISNULL(@IssueList + CHAR(10),'') + CONVERT(VARCHAR,@IssueCount) + ') At least one audit trigger exists on the ' + @SchemaName + '.' + @Tablename + ' table. To recreate the audit table, please drop the audit triggers.';
        END;

--4) Print errors if there are any
    IF @IssueCount > 0
        BEGIN
            PRINT('There were ' + CONVERT(VARCHAR,@IssueCount) + ' issues found when attempting to create the audit table. Please correct the issues below before trying again.');
            PRINT(@LineBreak);
            PRINT(@IssueList);
            RETURN;
        END;

--5) Build Scripts
    select
        @CreateTableScript = 
            'CREATE TABLE [' + SS.name + '].[' + ST.name + '_Audit]' + CHAR(10) +
            '(' + CHAR(10) +
            CHAR(9) + '[AuditId] INT IDENTITY(1,1) NOT NULL CONSTRAINT [pk_' + @SchemaName + '.' + @Tablename + '_Audit_AuditId] PRIMARY KEY,' + CHAR(10) +
            CHAR(9) + '[AuditDate] DATETIME NOT NULL CONSTRAINT [df_' + @SchemaName + '.' + @Tablename + '_Audit_AuditDate] DEFAULT (getutcdate()),' + CHAR(10) +
            CHAR(9) + '[AuditIsDelete] BIT NOT NULL CONSTRAINT [df_' + @SchemaName + '.' + @Tablename + '_Audit_AuditIsDelete] DEFAULT ((0))',
        @CreateDeleteScript = 
            'CREATE TRIGGER [dbo].[tg_' + @SchemaName + '.' + @Tablename + '_Audit_Delete]' + CHAR(10) +
            'ON [' + SS.name + '].[' + ST.name + ']' + CHAR(10) +
            'After Delete' + CHAR(10) +
            'As Begin' + CHAR(10) +
            CHAR(9) + 'IF TRIGGER_NESTLEVEL() > 1' + CHAR(10) +
            CHAR(9) + CHAR(9) + 'Return' + CHAR(10) +
            CHAR(10) +
            CHAR(9) + 'INSERT INTO' + CHAR(10) +
            CHAR(9) + CHAR(9) + '[' + SS.name + '].[' + ST.name + '_Audit] (' + CHAR(10) +
            CHAR(9) + CHAR(9) + CHAR(9) + '[AuditIsDelete]',
        @CreateUpdateScript = 
            'CREATE TRIGGER [dbo].[tg_' + @SchemaName + '.' + @Tablename + '_Audit_Update]' + CHAR(10) +
            'ON [' + SS.name + '].[' + ST.name + ']' + CHAR(10) +
            'After Update' + CHAR(10) +
            'As Begin' + CHAR(10) +
            CHAR(9) + 'IF TRIGGER_NESTLEVEL() > 1' + CHAR(10) +
            CHAR(9) + CHAR(9) + 'Return' + CHAR(10) +
            CHAR(10) +
            CHAR(9) + 'INSERT INTO' + CHAR(10) +
            CHAR(9) + CHAR(9) + '[' + SS.name + '].[' + ST.name + '_Audit] (' + CHAR(10) +
            CHAR(9) + CHAR(9) + CHAR(9) + '[AuditIsDelete]'
    from
        sys.tables ST
        INNER JOIN
        sys.schemas SS ON ST.schema_id = SS.schema_id
    WHERE
        ST.name = @TableName AND
        ST.type = 'U' AND
        SS.name = @SchemaName

    SELECT
        @CreateTableScript = @CreateTableScript + ',' + CHAR(10) + CHAR(9) + '[' + ISC.COLUMN_NAME + '] ' + ISC.DATA_TYPE + CASE WHEN ISC.CHARACTER_MAXIMUM_LENGTH IS NOT NULL AND ISC.DATA_TYPE <> 'xml' THEN '(' + CASE WHEN ISC.CHARACTER_MAXIMUM_LENGTH = -1 THEN 'MAX' ELSE CONVERT(varchar,ISC.CHARACTER_MAXIMUM_LENGTH) END + ')' ELSE '' END + ' NULL',
        @ColumnNamesSection = ISNULL(@ColumnNamesSection,'') + ',' + CHAR(10) + CHAR(9) + CHAR(9) + CHAR(9) + '[' + ISC.COLUMN_NAME + ']'
    FROM
        INFORMATION_SCHEMA.COLUMNS ISC
    WHERE
        ISC.TABLE_NAME = @TableName AND
        ISC.TABLE_SCHEMA = @SchemaName
    ORDER BY
        ISC.ORDINAL_POSITION ASC

    SET @CreateTableScript = @CreateTableScript + CHAR(10) + ');'

    SET @CreateDeleteScript = @CreateDeleteScript + @ColumnNamesSection + CHAR(10) +
        CHAR(9) + CHAR(9) + ')' + CHAR(10) +
        CHAR(9) + CHAR(9) + 'SELECT' + CHAR(10) +
        CHAR(9) + CHAR(9) + CHAR(9) + '1 as [AuditIsDelete]' +
        @ColumnNamesSection + CHAR(10) +
        CHAR(9) + CHAR(9) + 'FROM' + CHAR(10) +
        CHAR(9) + CHAR(9) + CHAR(9) + 'deleted' + CHAR(10) +
        'End;'

    SET @CreateUpdateScript = @CreateUpdateScript + @ColumnNamesSection + CHAR(10) +
        CHAR(9) + CHAR(9) + ')' + CHAR(10) +
        CHAR(9) + CHAR(9) + 'SELECT' + CHAR(10) +
        CHAR(9) + CHAR(9) + CHAR(9) + '0 as [AuditIsDelete]' +
        @ColumnNamesSection + CHAR(10) +
        CHAR(9) + CHAR(9) + 'FROM' + CHAR(10) +
        CHAR(9) + CHAR(9) + CHAR(9) + 'deleted' + CHAR(10) +
        'declare @SoftDelete bit,
            @Id int

    select
        @SoftDelete = case when i.DeletedDate is not null then 1 else 0 end,
        @Id = i.Id
    from
        inserted i;

    if @SoftDelete = 1
        begin
            INSERT INTO
                [' + @SchemaName + '].[' + @TableName + '_Audit] (
                    [AuditIsDelete]
                    ' + @ColumnNamesSection + '
                )
                SELECT
                    1 as [AuditIsDelete]
                    ' + @ColumnNamesSection + '
                FROM
                    inserted

            delete from ' + @SchemaName + '.' + @TableName + ' where Id = @Id
        end;' + CHAR(10) +

        'End;'

--6) Print and Run Scripts
    BEGIN TRY
        BEGIN TRANSACTION;

        EXEC(@CreateTableScript);

        EXEC(@CreateDeleteScript);

        EXEC(@CreateUpdateScript);

        --Test Try Catch:
        --SELECT 1/0

        COMMIT TRANSACTION;

        PRINT('The audit table was successfully created.')
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;

        set @msg = 
            'db_name()=' + isnull( db_name(), 'NULL' ) + '; ERROR_MESSAGE()=' +
            isnull( ERROR_MESSAGE(), 'NULL' ) +
            '; ERROR_PROCEDURE()=' + isnull( ERROR_PROCEDURE(), 'NULL' ) +
            '; ERROR_LINE()=' + isnull( CONVERT( varchar(10), ERROR_LINE() ), 'NULL' ) +    
            '; ERROR_NUMBER()=' + isnull( CONVERT( varchar(10), ERROR_NUMBER() ), 'NULL' ) +
            '; ERROR_SEVERITY()=' + isnull( CONVERT( varchar(10), ERROR_SEVERITY() ), 'NULL' ) +
            '; ERROR_STATE()=' + isnull( CONVERT( varchar(10), ERROR_STATE() ), 'NULL' );

        PRINT(CHAR(10) + 'Create Audit Table Script:');
        PRINT(@LineBreak);
        PRINT(@CreateTableScript);
        PRINT(@LineBreak);

        PRINT(CHAR(10) + 'Create Audit Delete Trigger Script:');
        PRINT(@LineBreak);
        PRINT(@CreateDeleteScript);
        PRINT(@LineBreak);

        PRINT(CHAR(10) + 'Create Audit Update Trigger Script:');
        PRINT(@LineBreak);
        PRINT(@CreateUpdateScript);
        PRINT(@LineBreak);

        raiserror ( @msg, 18, 1 );
    END CATCH
END;

While the Triggers are not ideal, they accomplish the goals of auditing the user who deleted and I no longer need to worry about the soft deleted records.

这篇关于使用ExpressionVisitor以排除软删除的记录在加入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆