2

Nous rencontrons quelques problèmes lors de la mise en œuvre de la fonctionnalité de suppression logicielle avec framework d'entité. L'idée est d'utiliser un référentiel qui connaît le contexte EF. Au niveau du référentiel, nous avons implémenté un système de plugin, ces plugins sont exécutés dès qu'une action est effectuée sur le référentiel. Par exemple quand nous appelons Repository.GetQuery<Relation>() les plugins sont exécutés. L'un des plugins est un LogicalDeletePlugin, ce plugin devrait ajouter une instruction Where(x => x.IsDeleted) à chaque table qui est dans la sélection. L'idée était d'implémenter ce plugin IsDeleted en utilisant un ExpressionVisitor qui visite l'expression linq et trouve toutes les instructions de sélection "table" et ajoute la condition IsDeleted.ExpressionVisitor soft delete

Pour clarifier la question/problème, je vais expliquer le problème en utilisant des exemples de code.

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

ci-dessus C# code entraînera dans le code SQL suivant:

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 

Comme vous pouvez le voir dans la requête SQL résultant des IsDeleted déclarations est ajouté au TestWorks = x.Bonus.Where(y => !y.IsDeleted) code "sélectionner". C'est ce que fait actuellement le TestVisitor. Mais la question est maintenant de savoir comment nous pouvons également mettre en œuvre cela sur les autres sélections, le x => !x.IsDeleted ne pas être ajouté sur la partie Test = x.Bonus.

Est-ce que ExpressionVisitor est la bonne approche pour y arriver ou devrais-je utiliser une autre solution? Toute aide est appréciée! Si l'explication n'était pas assez claire, faites-le moi savoir et j'essaierai de donner quelques infos supplémentaires!

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

Ce qui précède est ce que je l'ai ajouté à la ExpressionVisitor. Maintenant, lorsque je décommente le code Expression.MamkeMemberaccess de retour une exception est levée car il ne s'attend pas à une MemberExpression ou quelque chose.

Après est la solution que je suis venu avec:

/// <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 est une classe qui détient une expression de type Expression<Func<T, bool>>. Le InjectconditionVisitor peut être utilisé avec InterceptWith (paquet QueryInterceptor NuGet) comme query.InterceptWith(new InjectConditionVisitor(new QueryConditional(x => x.Deleted == true)).

+0

Je ne comprends pas votre solution. Vous dites 'QueryConditional' est une classe où, je suppose, la propriété' Conditional' est une 'Expression >'. Mais d'où vient T? Quand vous dites 'QueryConditional (x => x.Deleted == true))' où est le type de 'x' défini? Il semble que 'T' devrait être une classe de base ou une interface spécifique ou' QueryConditional' devrait être une classe générique 'QueryConditional '. Aussi qu'est-ce que 'ParameterRebinder'? – xr280xr

Répondre

0

Oui, l'utilisation de ExpressionVisitor est la bonne approche. Vous devez transformer x.Bonus en x.Bonus.Where(x => !x.IsDeleted). Je vous suggère de faire la même chose pour x.Bonus.Where(y => y.bonID == 100). Transformer en x.Bonus.Where(x => !x.IsDeleted).Where(y => y.bonID == 100)

Cela signifie que vous devez convertir une expression de type IQueryable<Bonus> à une autre expression de type IQueryable<Bonus>, mais avec la clause where en annexe.

Vous devez probablement remplacer la méthode très générale ExpressionVisitor.Visit pour visiter toutes les expressions, et pas seulement les expressions binaires.

Vous êtes très susceptible de rencontrer dans des cas spéciaux ici que vous n'avez pas encore pensé.Cela va être difficile, mais amusant :)

+0

Merci pour la réponse. Je n'ai pas eu beaucoup de temps pour continuer à travailler sur ce problème. La dernière chose contre laquelle je me suis heurté est que lorsque vous regardez le code linq, c'est que x.Bonus.Where (x => X ...) est d'un type différent du code x.Bonus. Été essayer de convertir le x.Bonus à un x.Bonus.Where mais sans aucune chance. Avez-vous peut-être une idée de comment cela peut-il être fait? – JTI

+0

Je pense que ce que vous voulez est '(x.Bonus.IsDeleted? Null: x.Bonus)'. Cela peut être lent sur le côté SQL car EF peut traduire cela en une forme inefficace. – usr

-1

Victoire! Aujourd'hui, j'ai créé un ExpressionVisitor qui ajoute la clause IsDeleted where à chaque sélection, même dans les propriétés de navigation!

+1

J'essaie de résoudre exactement ce même problème. Pouvez-vous mettre à jour votre réponse pour inclure le code final que vous avez trouvé? –

+0

La réponse est mise à jour – JTI

+0

Salut user1725275, j'ai essayé de suivre votre exemple de code, mais je n'obtiens pas le même résultat. J'ai posté une nouvelle question avec mon code et plus d'info [ici] (http://stackoverflow.com/questions/17532393/use-expressionvisitor-to-exclude-soft-deleted-records-in-joins). Pourriez-vous jeter un coup d'œil? –