ExpressionVisitor软删除

时间:2012-10-06 15:05:33

标签: linq entity-framework linq-expressions soft-delete expressionvisitor

我们在使用实体框架实现软删除功能时遇到了一些问题。我们的想法是使用知道EF上下文的存储库。在存储库的级别上,我们实现了一个插件系统,只要在存储库上执行了操作,就会执行这些插件。例如,当我们调用Repository.GetQuery<Relation>()时,插件会被执行。其中一个插件是LogicalDeletePlugin,这个插件应该为select中的每个表添加一个Where(x => x.IsDeleted)语句。我们的想法是使用IsDeleted来实现此ExpressionVisitor插件,该IsDeleted访问linq表达式并查找所有“table”select语句并添加void Main() { var options = new ReadonlyRepositoryOptions() { ConnectionStringDelegate = () => Connection.ConnectionString }; using (var context = new ReadonlyObjectContextRepository<PFishEntities>(options)) { var query = context.GetQuery<Relation>() .Select(x => new { Test = x.Bonus, TestWorks = x.Bonus.Where(y => y.bonID == 100) }); query.InterceptWith(new TestVisitor()).ToList(); } } public class TestVisitor : ExpressionVisitor { private ParameterExpression Parameter { get; set; } protected override Expression VisitBinary(BinaryExpression node) { "VisitBinary".Dump(); Expression left = this.Visit(node.Left); Expression right = this.Visit(node.Right); var newParams = new[] { Parameter }; var condition = (LambdaExpression)new LogicalDeletePlugin().QueryConditionals.First().Conditional; var paramMap = condition.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement); var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, condition.Body); return Expression.MakeBinary(ExpressionType.AndAlso, node, fixedBody, node.IsLiftedToNull, node.Method); } protected override Expression VisitParameter(ParameterExpression expr) { Parameter = expr; return base.VisitParameter(expr); } } void Main() { var options = new ReadonlyRepositoryOptions() { ConnectionStringDelegate = () => Connection.ConnectionString }; using (var context = new ReadonlyObjectContextRepository<PFishEntities>(options)) { var query = context.GetQuery<Relation>() .Select(x => new { Test = x.Bonus, TestWorks = x.Bonus.Where(y => y.bonID == 100) }); query.InterceptWith(new TestVisitor()).ToList(); } } public class TestVisitor : ExpressionVisitor { private ParameterExpression Parameter { get; set; } protected override Expression VisitBinary(BinaryExpression node) { "VisitBinary".Dump(); Expression left = this.Visit(node.Left); Expression right = this.Visit(node.Right); var newParams = new[] { Parameter }; var condition = (LambdaExpression)new LogicalDeletePlugin().QueryConditionals.First().Conditional; var paramMap = condition.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement); var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, condition.Body); return Expression.MakeBinary(ExpressionType.AndAlso, node, fixedBody, node.IsLiftedToNull, node.Method); } protected override Expression VisitParameter(ParameterExpression expr) { Parameter = expr; return base.VisitParameter(expr); } } 条件。

为了澄清问题/问题,我将使用一些代码示例解释该问题。

SELECT 
[UnionAll1].[relID] AS [C1], 
[UnionAll1].[C2] AS [C2], 
[UnionAll1].[C1] AS [C3], 
[UnionAll1].[bonID] AS [C4], 
[UnionAll1].[bonCUSTOMERID] AS [C5], 
[UnionAll1].[bonRELATIONARTICLEBONUSID] AS [C6], 
[UnionAll1].[bonINVOICEID] AS [C7], 
[UnionAll1].[bonSALEROWID] AS [C8], 
[UnionAll1].[bonVALUE] AS [C9], 
[UnionAll1].[bonPERCENTAGE] AS [C10], 
[UnionAll1].[bonMANUAL] AS [C11], 
[UnionAll1].[bonPAID] AS [C12], 
[UnionAll1].[IsDeleted] AS [C13], 
[UnionAll1].[InternalReference] AS [C14], 
[UnionAll1].[ConcurrencyToken] AS [C15], 
[UnionAll1].[Created] AS [C16], 
[UnionAll1].[CreatedBy] AS [C17], 
[UnionAll1].[Updated] AS [C18], 
[UnionAll1].[UpdatedBy] AS [C19], 
[UnionAll1].[DisplayMember] AS [C20], 
[UnionAll1].[ValueMember] AS [C21], 
[UnionAll1].[SearchField] AS [C22], 
[UnionAll1].[CreateDate] AS [C23], 
[UnionAll1].[C3] AS [C24], 
[UnionAll1].[C4] AS [C25], 
[UnionAll1].[C5] AS [C26], 
[UnionAll1].[C6] AS [C27], 
[UnionAll1].[C7] AS [C28], 
[UnionAll1].[C8] AS [C29], 
[UnionAll1].[C9] AS [C30], 
[UnionAll1].[C10] AS [C31], 
[UnionAll1].[C11] AS [C32], 
[UnionAll1].[C12] AS [C33], 
[UnionAll1].[C13] AS [C34], 
[UnionAll1].[C14] AS [C35], 
[UnionAll1].[C15] AS [C36], 
[UnionAll1].[C16] AS [C37], 
[UnionAll1].[C17] AS [C38], 
[UnionAll1].[C18] AS [C39], 
[UnionAll1].[C19] AS [C40], 
[UnionAll1].[C20] AS [C41], 
[UnionAll1].[C21] AS [C42], 
[UnionAll1].[C22] AS [C43]
FROM  (SELECT 
    CASE WHEN ([Extent2].[bonID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1], 
    [Extent1].[relID] AS [relID], 
    1 AS [C2], 
    [Extent2].[bonID] AS [bonID], 
    [Extent2].[bonCUSTOMERID] AS [bonCUSTOMERID], 
    [Extent2].[bonRELATIONARTICLEBONUSID] AS [bonRELATIONARTICLEBONUSID], 
    [Extent2].[bonINVOICEID] AS [bonINVOICEID], 
    [Extent2].[bonSALEROWID] AS [bonSALEROWID], 
    [Extent2].[bonVALUE] AS [bonVALUE], 
    [Extent2].[bonPERCENTAGE] AS [bonPERCENTAGE], 
    [Extent2].[bonMANUAL] AS [bonMANUAL], 
    [Extent2].[bonPAID] AS [bonPAID], 
    [Extent2].[IsDeleted] AS [IsDeleted], 
    [Extent2].[InternalReference] AS [InternalReference], 
    [Extent2].[ConcurrencyToken] AS [ConcurrencyToken], 
    [Extent2].[Created] AS [Created], 
    [Extent2].[CreatedBy] AS [CreatedBy], 
    [Extent2].[Updated] AS [Updated], 
    [Extent2].[UpdatedBy] AS [UpdatedBy], 
    [Extent2].[DisplayMember] AS [DisplayMember], 
    [Extent2].[ValueMember] AS [ValueMember], 
    [Extent2].[SearchField] AS [SearchField], 
    [Extent2].[CreateDate] AS [CreateDate], 
    CAST(NULL AS bigint) AS [C3], 
    CAST(NULL AS bigint) AS [C4], 
    CAST(NULL AS bigint) AS [C5], 
    CAST(NULL AS bigint) AS [C6], 
    CAST(NULL AS bigint) AS [C7], 
    CAST(NULL AS decimal(20,4)) AS [C8], 
    CAST(NULL AS decimal(20,4)) AS [C9], 
    CAST(NULL AS bit) AS [C10], 
    CAST(NULL AS decimal(20,4)) AS [C11], 
    CAST(NULL AS bit) AS [C12], 
    CAST(NULL AS varchar(1)) AS [C13], 
    CAST(NULL AS varbinary(1)) AS [C14], 
    CAST(NULL AS datetimeoffset) AS [C15], 
    CAST(NULL AS varchar(1)) AS [C16], 
    CAST(NULL AS datetimeoffset) AS [C17], 
    CAST(NULL AS varchar(1)) AS [C18], 
    CAST(NULL AS varchar(1)) AS [C19], 
    CAST(NULL AS varchar(1)) AS [C20], 
    CAST(NULL AS varchar(1)) AS [C21], 
    CAST(NULL AS datetime2) AS [C22]
    FROM  [dbo].[Relation] AS [Extent1]
    LEFT OUTER JOIN [dbo].[Bonus] AS [Extent2] ON [Extent1].[relID] = [Extent2].[bonCUSTOMERID]
UNION ALL
    SELECT 
    2 AS [C1], 
    [Extent3].[relID] AS [relID], 
    2 AS [C2], 
    CAST(NULL AS bigint) AS [C3], 
    CAST(NULL AS bigint) AS [C4], 
    CAST(NULL AS bigint) AS [C5], 
    CAST(NULL AS bigint) AS [C6], 
    CAST(NULL AS bigint) AS [C7], 
    CAST(NULL AS decimal(20,4)) AS [C8], 
    CAST(NULL AS decimal(20,4)) AS [C9], 
    CAST(NULL AS bit) AS [C10], 
    CAST(NULL AS decimal(20,4)) AS [C11], 
    CAST(NULL AS bit) AS [C12], 
    CAST(NULL AS varchar(1)) AS [C13], 
    CAST(NULL AS varbinary(1)) AS [C14], 
    CAST(NULL AS datetimeoffset) AS [C15], 
    CAST(NULL AS varchar(1)) AS [C16], 
    CAST(NULL AS datetimeoffset) AS [C17], 
    CAST(NULL AS varchar(1)) AS [C18], 
    CAST(NULL AS varchar(1)) AS [C19], 
    CAST(NULL AS varchar(1)) AS [C20], 
    CAST(NULL AS varchar(1)) AS [C21], 
    CAST(NULL AS datetime2) AS [C22], 
    [Extent4].[bonID] AS [bonID], 
    [Extent4].[bonCUSTOMERID] AS [bonCUSTOMERID], 
    [Extent4].[bonRELATIONARTICLEBONUSID] AS [bonRELATIONARTICLEBONUSID], 
    [Extent4].[bonINVOICEID] AS [bonINVOICEID], 
    [Extent4].[bonSALEROWID] AS [bonSALEROWID], 
    [Extent4].[bonVALUE] AS [bonVALUE], 
    [Extent4].[bonPERCENTAGE] AS [bonPERCENTAGE], 
    [Extent4].[bonMANUAL] AS [bonMANUAL], 
    [Extent4].[bonPAID] AS [bonPAID], 
    [Extent4].[IsDeleted] AS [IsDeleted], 
    [Extent4].[InternalReference] AS [InternalReference], 
    [Extent4].[ConcurrencyToken] AS [ConcurrencyToken], 
    [Extent4].[Created] AS [Created], 
    [Extent4].[CreatedBy] AS [CreatedBy], 
    [Extent4].[Updated] AS [Updated], 
    [Extent4].[UpdatedBy] AS [UpdatedBy], 
    [Extent4].[DisplayMember] AS [DisplayMember], 
    [Extent4].[ValueMember] AS [ValueMember], 
    [Extent4].[SearchField] AS [SearchField], 
    [Extent4].[CreateDate] AS [CreateDate]
    FROM  [dbo].[Relation] AS [Extent3]
    INNER JOIN [dbo].[Bonus] AS [Extent4] ON ([Extent3].[relID] = [Extent4].[bonCUSTOMERID]) AND (100 = [Extent4].[bonID]) AND ([Extent4].[IsDeleted] <> cast(1 as bit))) AS [UnionAll1]
ORDER BY [UnionAll1].[relID] ASC, [UnionAll1].[C1] ASC

上面的c#代码将产生以下SQL代码:

IsDeleted

正如您在生成的SQL查询中所看到的,TestWorks = x.Bonus.Where(y => !y.IsDeleted)语句被添加到TestVisitor“select”代码中。这就是x => !x.IsDeleted目前正在做的事情。但现在的问题是我们如何在其他选项上实现此功能,Test = x.Bonus部分不会添加protected override Expression VisitMember(MemberExpression node) { var test = typeof(bool); if (node.Type != test && node.Type != typeof(string)) { var type = typeof(ArticleVat); var condition = (LambdaExpression)Condition; var newParams = new[] { Expression.Parameter(type, "x") }; var paramMap = condition.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement); var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, condition.Body); condition = Expression.Lambda(fixedBody, newParams); var whereM = whereMethod.MakeGenericMethod(new [] { type }); var expr = Expression.Property(node.Expression, "ArticleVat"); var whereExpr = Expression.Call(whereM, expr, condition); // whereExpr.Dump(); node.Dump(); // return Expression.MakeMemberAccess(whereExpr, node.Expression.Type.GetMember(node.Member.Name).Single()); // return Expression.MakeMemberAccess( // whereExpr, // node.Expression.Type.GetMember(node.Member.Name).Single()); } return base.VisitMember(node); }

ExpressionVisitor是完成此操作的正确方法还是我应该使用其他解决方案?所有帮助表示赞赏!如果解释不够清楚,请告诉我,我会尝试提供一些额外的信息!

编辑:

/// <summary>
/// This visitor will append a .Where(QueryCondition) clause for a given Condition to each Navigation property
/// </summary>
public class InjectConditionVisitor : ExpressionVisitor
{
    private QueryConditional QueryCondition { get; set; }

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

    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.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;
    }
}

以上是我添加到ExpressionVisitor的内容。现在,当我取消注释返回Expression.MamkeMemberaccess代码时,会抛出异常,因为它不期望有MemberExpression或其他东西。

以下是我提出的解决方案:

Expression<Func<T, bool>>

QueryConditional是一个包含类型query.InterceptWith(new InjectConditionVisitor(new QueryConditional(x => x.Deleted == true))的表达式的类。

InjectconditionVisitor可以与InterceptWith(QueryInterceptor NuGet包)结合使用,如{{1}}。

2 个答案:

答案 0 :(得分:0)

是的,使用ExpressionVisitor是正确的方法。

您需要将x.Bonus转换为x.Bonus.Where(x => !x.IsDeleted)。我建议你对x.Bonus.Where(y => y.bonID == 100)做同样的事情。将其转换为x.Bonus.Where(x => !x.IsDeleted).Where(y => y.bonID == 100)

这意味着您需要将IQueryable<Bonus>类型的任何表达式转换为另一个类型为IQueryable<Bonus>的表达式,但附加了where子句。

您可能需要覆盖非常通用的ExpressionVisitor.Visit方法来访问所有表达式,而不仅仅是二进制表达式。

你很可能会遇到一些你还没想过的特殊情况。这将是困难的,但很有趣:)

答案 1 :(得分:-1)

胜利!今天我创建了一个ExpressionVisitor,它将IsDeleted where子句附加到每个选择中,即使在导航属性中也是如此!