2

У нас возникли проблемы с реализацией функциональности мягкого удаления с инфраструктурой сущности. Идея состоит в том, чтобы использовать репозиторий, который знает контекст EF. На уровне репозитория мы реализовали систему плагинов, эти плагины выполняются всякий раз, когда действие выполняется в репозитории. Например, когда мы вызываем Repository.GetQuery<Relation>(), плагины выполняются. Один из плагинов - это LogicalDeletePlugin, этот плагин должен добавить инструкцию Where(x => x.IsDeleted) к каждой таблице, которая находится в списке. Идея состояла в том, чтобы реализовать этот плагин IsDeleted, используя ExpressionVisitor, который посещает выражение linq и находит все операторы выбора таблицы и добавляет условие IsDeleted.ExpressionVisitor soft delete

Чтобы уточнить вопрос/проблему, я объясню проблему, используя некоторые примеры кода.

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 правильным подходом для этого, или я должен пойти с другим решением? Вся помощь приветствуется! Если объяснение не было достаточно ясным, просто дайте мне знать, и я попытаюсь дать дополнительную информацию!

Edit:

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' является выражением >'. Но откуда берется «Т»? Когда вы говорите '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>, но с где-п добавленным.

Вы, вероятно, должны переопределить самый общий метод 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 clause к каждому select, даже в свойствах навигации!

+1

Я пытаюсь решить эту ту же проблему. Можете ли вы обновить свой ответ, чтобы включить окончательный код, с которым вы столкнулись? –

+0

Ответ обновлен – JTI

+0

Привет user1725275, я пробовал следовать вашему образцу кода, но я не получаю тот же результат. Я отправил новый вопрос с моим кодом и дополнительной информацией [здесь] (http://stackoverflow.com/questions/17532393/use-expressionvisitor-to-exclude-soft-deleted-records-in-joins). Не могли бы вы взглянуть? –

Смежные вопросы