我们在使用实体框架实现软删除功能时遇到了一些问题。这个想法是使用一个知道 EF 上下文的存储库。在存储库级别上,我们实现了一个插件系统,只要对存储库执行操作,就会执行这些插件。例如,当我们调用Repository.GetQuery<Relation>()
插件时,就会执行。其中一个插件是 a LogicalDeletePlugin
,这个插件应该Where(x => x.IsDeleted)
向 select 中的每个表添加一个语句。IsDeleted
想法是使用ExpressionVisitor
访问 linq 表达式并找到所有“表”选择语句并添加IsDeleted
条件来实现此插件。
为了澄清问题/问题,我将使用一些代码示例来解释这个问题。
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 的类Expression<Func<T, bool>>
。
InjectconditionVisitor 可以与 InterceptWith(QueryInterceptor NuGet 包)结合使用,例如query.InterceptWith(new InjectConditionVisitor(new QueryConditional(x => x.Deleted == true))
.