【问题标题】:NHibernate, expression trees, and eliminating repetitionNHibernate、表达式树和消除重复
【发布时间】:2015-02-11 20:47:29
【问题描述】:

我们在 NHibernate 持久层周围实现了一个安全层,希望防止用户在不应该访问它的情况下甚至从数据库接收回一个对象。该安全层如下所示:

public static IQueryable<T> Secure<T>(this Queryable<T> query){
    //if T does not implement ISecurable, then return query
    //else
        return query.Where(expressionFactory.GetExpression(securityKey));
}

我们基本上通过使用调用 ISession.Query().Secure() 的装饰器包装 ISession 来限制对 ISession 的访问。

所以我们有许多类型返回Expression&lt;Func&lt;T, bool&gt;&gt;,这样我们就可以将它传递给 Where():

public class DocumentSecurityExpressionFactory : ISecurityExpressionFactory<Document> {
    public Expression<Func<Document, bool>> GetExpression(SecurityKey key) {
        return doc => doc.MasterDocument.Compartments.Where(c => c.AssociatedCompartment.Type != ProgramTypes.AccessGroup) //Look at non-access group compartments for access
                        .All(c => key.Compartments.Contains(c.AssociatedCompartment.ID))
                    && (
            //person has to be either NTK 
                        doc.MasterDocument.NeedToKnowAccessList.Count() == 0
                        || doc.MasterDocument.NeedToKnowAccessList.Any(p => p.PersonID == key.PersonID)
                        || doc.MasterDocument.NeedToKnowAccessList.Any(p => key.AccessGroups.Contains(p.CompartmentID))
                        );
    }
}

public class DocumentSummarySecurityExpressionFactory : ISecurityExpressionFactory<DocumentSummary> {
    public Expression<Func<DocumentSummary, bool>> GetExpression(SecurityKey key) {
        return doc => doc.MasterDocument.Compartments.Where(c => c.AssociatedCompartment.Type != ProgramTypes.AccessGroup)
                        .All(c => key.Compartments.Contains(c.AssociatedCompartment.ID))
                        && (
                          doc.MasterDocument.NeedToKnowAccessList.Count() == 0
                        || doc.MasterDocument.NeedToKnowAccessList.Any(p => p.PersonID == key.PersonID)
                        || doc.MasterDocument.NeedToKnowAccessList.Any(p => key.AccessGroups.Contains(p.CompartmentID))
                           );
    }
}

public class LatestDocumentVersionSecurityExpressionFactory : ISecurityExpressionFactory<LatestDocumentVersion> {
    public Expression<Func<LatestDocumentVersion, bool>> GetExpression(SecurityKey key) {
        return version => version.BaseDocument.MasterDocument.Compartments.Where(c => c.AssociatedCompartment.Type != ProgramTypes.AccessGroup)
                            .All(c => key.Compartments.Contains(c.AssociatedCompartment.ID))
                            && (
                                version.BaseDocument.MasterDocument.NeedToKnowAccessList.Count() == 0
                                || version.BaseDocument.MasterDocument.NeedToKnowAccessList.Any(p => p.PersonID == key.PersonID)
                                || version.BaseDocument.MasterDocument.NeedToKnowAccessList.Any(p => key.AccessGroups.Contains(p.CompartmentID))
                            );
    }
}

实际上还有更多的不同类型看起来像这样。

这里的问题应该很清楚:我们每个执行此操作的实体本质上都是相同的。它们每个都有对 MasterDocument 对象的引用,所有逻辑都在该对象上完成。重复这段代码完全糟透了(而且它们都放在一个文件中,所以如果他们这样做的话,它们都可以一起改变)。

我觉得我应该能够告诉一个方法如何从类型 T 获取 MasterDocument,然后有一个通用的方法来构建表达式。像这样的:

public static class ExpressionFactory {
    public static Expression<Func<T, bool>> Get<T>(Expression<Func<T, MasterDocument>> mdSource, SecurityKey key) {
        return t => {
             var md = mdSource.Compile()(t);
             return md.Compartments.Where(c => c.AssociatedCompartment)...
        };
    }
}

然后这样称呼它:

public class DocumentSecurityExpressionFactory : ISecurityExpressionFactory<Document> {
    public Expression<Func<Document, bool>> GetExpression(SecurityKey key) {
        return ExpressionFactory.Get<Document>(doc => doc.MasterDocument, key);
    }
}

现在,我明白为什么这段代码不起作用了。我想不通的是如何正确构建这个表达式树以极大地简化我们的代码。我想我可以像这样传入Expression&lt;Func&lt;T, MasterDocument&gt;&gt; mdSource,然后使用Expression API 用MemberAccessExpressions 等来构建它,但我预计会出现这样的混乱情况,我不确定哪个会更小邪恶的。

非常感谢任何帮助。

【问题讨论】:

    标签: c# linq nhibernate linq-to-sql expression-trees


    【解决方案1】:

    您可以做的是使用Compose 方法,该方法可以将一个表达式与另一个表达式组合:

    public static Expression<Func<TFirstParam, TResult>>
        Compose<TFirstParam, TIntermediate, TResult>(
        this Expression<Func<TFirstParam, TIntermediate>> first,
        Expression<Func<TIntermediate, TResult>> second)
    {
        var param = Expression.Parameter(typeof(TFirstParam), "param");
    
        var newFirst = first.Body.Replace(first.Parameters[0], param);
        var newSecond = second.Body.Replace(second.Parameters[0], newFirst);
    
        return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
    }
    

    它使用以下方法将一个表达式的所有实例替换为另一个:

    public static Expression Replace(this Expression expression,
        Expression searchEx, Expression replaceEx)
    {
        return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
    }
    internal class ReplaceVisitor : ExpressionVisitor
    {
        private readonly Expression from, to;
        public ReplaceVisitor(Expression from, Expression to)
        {
            this.from = from;
            this.to = to;
        }
        public override Expression Visit(Expression node)
        {
            return node == from ? to : base.Visit(node);
        }
    }
    

    现在你可以写了:

    public static class ExpressionFactory 
    {
        public static Expression<Func<T, bool>> Get<T>(
             Expression<Func<T, MasterDocument>> mdSource, SecurityKey key) 
        {
            return mdSource.Compose(document => 
                document.Compartments.Where(c => c.AssociatedCompartment.Type != ProgramTypes.AccessGroup)
                        .All(c => key.Compartments.Contains(c.AssociatedCompartment.ID))
                        && (
                          doc.MasterDocument.NeedToKnowAccessList.Count() == 0
                        || doc.MasterDocument.NeedToKnowAccessList.Any(p => p.PersonID == key.PersonID)
                        || doc.MasterDocument.NeedToKnowAccessList.Any(p => key.AccessGroups.Contains(p.CompartmentID))
                           );
    
        }
    }
    

    【讨论】:

    • 像魅力一样工作!非常感谢。顺便说一句:对于其中一个表达式,MasterDocument 的属性向下嵌套了几个属性(例如 doc.PropA..PropB.MasterDocument),在这种情况下,存在 PropA 为空的可能性。通常,如果 PropA 为空,我们会返回 true,我认为我必须以某种方式将其处理到表达式中。但实际上,事实证明没有必要。即使我可能期望出现 NullReferenceException,它也可以在没有检查的情况下工作。对此有何解释?
    • @MarcChu SQL 以不同方式处理空值。当您尝试对空值执行操作而不是崩溃时,它只是传播空值。当您尝试获取null 值的PropB 值时,它只会返回null,而不是抛出。如果代码作为 C# 代码执行,而不是转换为 SQL,您将不得不显式处理 null
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-22
    相关资源
    最近更新 更多