2

我們遇到了一些實體軟實體框架的軟刪除功能問題。這個想法是使用知道EF上下文的存儲庫。在存儲庫的層次上,我們實現了一個插件系統,只要在存儲庫上完成一個操作,就會執行這些插件。例如,當我們調用Repository.GetQuery<Relation>()時,插件會被執行。其中一個插件是LogicalDeletePlugin,這個插件應該爲select中的每個表添加一個Where(x => x.IsDeleted)語句。我們的想法是使用ExpressionVisitor實現這個IsDeleted插件,該插件訪問linq表達式並查找所有「table」select語句並添加IsDeleted條件。ExpressionVisitor軟刪除

爲了澄清問題/問題,我將使用一些代碼示例來解釋問題。

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

上面的C#代碼將導致下面的SQL代碼:

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 

正如你可以在生成的SQL查詢語句IsDeleted被添加到TestWorks = x.Bonus.Where(y => !y.IsDeleted)「中選擇」代碼見。這就是TestVisitor目前正在做的事情。但現在的問題是我們如何在其他選擇上實現這一點,x => !x.IsDeleted不會被添加到Test = x.Bonus部分。

ExpressionVisitor是完成此操作的正確方法,還是應該使用其他解決方案?所有幫助表示讚賞!如果解釋不夠清楚,請告訴我,我會嘗試提供一些其他信息!

編輯:

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。現在,當我取消註釋返回Expression.MamkeMemberaccess代碼時,會拋出異常,因爲它不指望MemberExpression或其他東西。

以下是我想出瞭解決方案:

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

QueryConditional是持有Expression<Func<T, bool>>類型的表達式的類。

InjectconditionVisitor可以與InterceptWith(QueryInterceptor NuGet包)結合使用,如query.InterceptWith(new InjectConditionVisitor(new QueryConditional(x => x.Deleted == true))

+0

我不明白你的解決方案。你說'QueryConditional'是一個類,我假設,'Conditional'屬性是'Expression >'。但是'T'從哪裏來?當你說'QueryConditional(x => x.Deleted == true))'定義了哪裏'x''的類型?看起來'T'必須是特定的基類或接口,或者'QueryConditional'必須是一個泛型類'QueryConditional '。另外什麼是'ParameterRebinder'? – xr280xr

回答

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-clause。

您可能需要重寫非常通用的ExpressionVisitor.Visit方法,以訪問所有表達式,而不僅僅是二進制表達式。

你很有可能會遇到你還沒有想過的特殊情況。這將是困難的,但有趣:)

+0

感謝您的回覆。我沒有很多時間來繼續解決這個問題。我碰到的最後一件事是,當你看linq代碼時,x.Bonus.Where(x => X ...)與x.Bonus代碼是不同的類型。試圖將x.Bonus轉換爲x.Bonus.Where,但沒有任何運氣。你可能有一個想法如何做到這一點? – JTI

+0

我想你想要的是'(x.Bonus.IsDeleted?null:x.Bonus)''。這可能在SQL方面很慢,因爲EF可能會將其轉換爲低效的形式。 – usr

-1

勝利!今天,我創建了一個ExpressionVisitor,它將IsDeleted where子句添加到每個選擇中,即使在導航屬性中也是如此!

+1

我試圖解決這個完全相同的問題。你能否更新你的答案來包含你提出的最終代碼? –

+0

答案更新 – JTI

+0

嗨用戶1725275,我試着按照你的示例代碼,但沒有得到相同的結果。我已經發布了一個新的問題與我的代碼和更多信息[這裏](http://stackoverflow.com/questions/17532393/use-expressionvisitor-to-exclude-soft-deleted-records-in-joins)。你可以看一下嗎? –