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 inApplyTopicsCompletedFilter
function, I receive internal data error. Of course if I do that, I also have to doTopicExpr(....).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.