3

I am trying to re-use an expression in a LINQ query but I get an exception saying "Code supposed to be unreachable". I'm not very much versed with PredicateBuilder. So pardon me if I look stupid somewhere. Below are the code snippets.

The expression that I am building via PredicateBuilder for re-use:

private static Expression <Func <DiscussionTopic , bool >> TopicExpr(Guid topicId, TaskMember taskMember, bool invert = false)
{
    var predicate = PredicateBuilder.True<DiscussionTopic >();
    predicate = predicate.And(topic => topic.TopicType == TopicType.Topic && topic.DiscussionTopicId == topicId);
    predicate = predicate.And(ExpressionStore.IsTopicAnsweredExpression(taskMember.PersonId));
    predicate = predicate.And(topic => (topic.TopicGroup == null || topic.TopicGroup.TopicVisibility == TopicVisibilityType .AllParticipants ||
                    (topic.TopicGroup.TopicVisibilityTags.Any(tv => taskMember.Person.PeopleTags.Any(pt => pt.ContentTagId == tv.ContentTagId) || taskMember.TaskMemberTags.Any(tmt => tmt.ContentTagId == tv.ContentTagId)))));
    predicate = predicate.And(topic => (topic.TopicVisibility == TopicVisibilityType.AllParticipants ||
                    (topic.TopicVisibilityTags.Any(tv => taskMember.Person.PeopleTags.Any(pt => pt.ContentTagId == tv.ContentTagId) || taskMember.TaskMemberTags.Any(tmt => tmt.ContentTagId == tv.ContentTagId)))));

    return predicate;
}

Next, I am trying to use the above expression in a nested query as below:

public static IQueryable <TaskMember > ApplyTopicsCompletedFilter(this IQueryable<TaskMember > input, Guid[] topicIds, bool invert = false)
{
    var predicate = PredicateBuilder.True<TaskMember >();
    predicate = predicate.And(taskMember => taskMember.TaskMembership.Discussions.Any(discussion => topicIds.All(topicId =>
            discussion.DiscussionTopics.AsQueryable().Any(TopicExpr(topicId, taskMember, invert)))));
        input = input.Where(predicate);
    return input;
}

Some points to mention which are skipped for brevity:

  • The ApplyTopicsCompletedFilter function is an extension method that receives an already constructed query as input by the caller. Therefore, AsExpandable() operator is applied in the caller and not here.
  • ExpressionStore.IsTopicAnsweredExpression in line 5 in TopicExpr is an external query that has a complex logic for checking if a member has answered a topic in a discussion. I have skipped the definition here but it's a normal expression with signature Expression<Func<DiscussionTopic, bool> returned by a function that takes a PersonId as a parameter.
  • If I remove AsQueryable() from line 4 in ApplyTopicsCompletedFilter function, I receive internal data error. Of course if I do that, I also have to do TopicExpr(....).Compile() in the same line otherwise the code won't compile.

Please help as I have wasted almost a day trying to find a nice way of sharing business logic across different functions in our application. Our code is quite complex and uses a lot of sub-queries as well. That's why I am looking for a way where I can share a piece of logic no matter if it appears in a parent query or a sub-query inside a parent query.

Edit: I just commented everything inside TopicExpr and now it just looks like this:

private static Expression<Func<DiscussionTopic, bool>> TopicExpr(Guid topicId, TaskMember taskMember, bool invert = false)
{
    var predicate = PredicateBuilder.True<DiscussionTopic>();
    predicate = predicate.And(topic => topic.TopicType == TopicType.Topic && topic.DiscussionTopicId == topicId);
    return predicate;
}

I have done this to make the query simple to test. Here is the complete flow now:

[Route("api/accounts/testPredicate")]
[HttpGet]
public IQueryable<apiPerson> TestPredicate()
{
    return this.TestPredicate(this.CurrentAccountId, new[] { MembershipStatusType.Member }, new apiFilterData { TopicsCompleted = new[] { Guid.Parse("<some guid>") } }).Select(tm => tm.Person).ToApiObjects();
}

public IQueryable<TaskMember> TestPredicate(Guid currentAccountId, MembershipStatusType[] statusTypes = null, apiFilterData filterData = null)
{
    var query = this.Db.Set<TaskMember>().Where(t => t.Person.AccountId == currentAccountId).AsExpandable();

    if (statusTypes != null)
        query = query.Where(t => statusTypes.Contains(t.Status));

    if (filterData.TopicsCompleted != null && filterData.TopicsCompleted.Length > 0)
        query = query.ApplyTopicsCompletedFilterWithPredicate(filterData.TopicsCompleted);

    if (filterData.TopicsNotCompleted != null && filterData.TopicsNotCompleted.Length > 0)
        query = query.ApplyTopicsCompletedFilterWithPredicate(filterData.TopicsNotCompleted, true);

    return query;
}

private static Expression<Func<DiscussionTopic, bool>> TopicExpr(Guid topicId, TaskMember taskMember, bool invert = false)
{
    var predicate = PredicateBuilder.True<DiscussionTopic>();
    predicate = predicate.And(topic => topic.TopicType == TopicType.Topic && topic.DiscussionTopicId == topicId);

    return predicate;
}

public static IQueryable<TaskMember> ApplyTopicsCompletedFilterWithPredicate(this IQueryable<TaskMember> input, Guid[] topicIds, bool invert = false)
{
    var predicate = PredicateBuilder.True<TaskMember>();
    predicate = predicate.And(taskMember => taskMember.TaskMembership.Discussions.AsQueryable().Any(discussion => topicIds.All(topicId =>
    discussion.DiscussionTopics.AsQueryable().Any(TopicExpr(topicId, taskMember, invert)))));
    input = input.Where(predicate);
    return input;
 }

 public static IQueryable<apiTopicSimple> ToApiObjects(this IQueryable<DiscussionTopic> items, Guid currentUserId, MembershipStatusType membershipStatus, bool stagingSite)
    {
        return from t in items
               let ms = membershipStatus
               let uId = currentUserId
               let sS = stagingSite
               select new apiTopicSimple
        {
            //Property initialization
        };
    }

PS: These functions are in different files inside the project but I have put them here at one place for definition purposes so that someone can go through the complete flow.

  • Can you reproduce the problem with a simpler code? Maybe that way I can help – tede24 Jan 23 '16 at 22:10
  • I just simplified the code. Please have a look at the edit. Thx –  Jan 24 '16 at 08:02
  • I'm out of computer now so can't try it, but a question: are you sure the problem is caused by the TopicExpr method? I mean, have you tried to run the code without calling the method but writing the expr inline? Just to be sure the problem is the method and nothing else – tede24 Jan 24 '16 at 17:54
  • Yes I have tried that already and it works flawlessly. Infact everything was combined into single query before I decided to split the logic because the idea was to re-use expressions as I am sensing serious maintenance issues by not doing that in near future. My other idea is to use SQL views for shared logic but for that, I will need to do several changes in the code base, even in the areas written by other team members. So I have reserved that option as a last resort until I explore all the other options. There's no hurry. You can test and reply as per your convenience. –  Jan 24 '16 at 18:57
  • Ok, so I know what's happening. Not how to solve.. :(. The problem is you are building and Expression which contains a call to TopicExpr in the expression tree. You are not actually calling TopicExpr and use its expression result as part of you predicate. It'll never work. I still don't find an alternative to make it work – tede24 Jan 25 '16 at 14:11
  • I am ok if this approach doesn't work in case you can suggest something else in LINQ using Code First EF model that can allow me to re-use complex logic. I'm already using extension methods for extending IQueryables but that sometimes doesn't work as the logic is sometimes in the parent query while other times it's in a sub-query. Any other ideas? –  Jan 25 '16 at 19:26

0 Answers0