I have a following query, which is causing me performance issues in SQL server.
ctx.Articles
.Where(m => m.Active)
.Where(m => m.PublishDate <= DateTime.Now)
.Where(m => m.Sponsored == false)
.WhereIf(request.ExcludeFirstNews, m => m.PositionForCategory != 1)
.WhereIf(category != null, m => m.RootCategoryId == category.RootCategoryId)
.WhereIf(request.TimeRange != 0 && request.TimeRange == TimeRange.today, m => m.PublishDate.Value >= today)
.WhereIf(request.TimeRange != 0 && request.TimeRange == TimeRange.yesterday, m => m.PublishDate.Value >= yesterday)
.WhereIf(request.TimeRange != 0 && request.TimeRange == TimeRange.week, m => m.PublishDate.Value >= week &&
m.PublishDate.Value <= DateTime.Now)
.WhereIf(request.TimeRange != 0 && request.TimeRange == TimeRange.month, m => m.PublishDate.Value >= month &&
m.PublishDate.Value <= DateTime.Now)
.OrderByDescending(m => m.ArticleViewCountSum.ViewCountSum)
Is there a way to make entity framework to use inner join instead of left outer join for order by syntax part of the lambda expression?
I've checked query in profiler, and it generaters left outer join, which is not necessary in this case, and runs much faster if i change query manually in SQL.
EDIT:
Per questions, in the comments, this is code for WhereIf
public static IQueryable<T> WhereIf<T>(
this IQueryable<T> source,
bool condition,
Expression<Func<T, bool>> predicate)
{
if (condition)
{
return source.Where(predicate);
}
return source;
}
Related
I need to convert this SQL:
SELECT
sb.Id AS id,
sb.name as name,
sb.age AS age,
sb.BirthDay as BirthDay,
su.UnitId AS unitId,
sb.WorkedInDodo AS workedInDodo,
sb.RelativesWorkedInDodo AS relativesWorkedInDodo,
sb.PhoneNumber AS phoneNumber,
sb.Email AS email,
sb.DeliveryCheck AS deliveryCheck,
sb.RestaurantCheck AS restaurantCheck,
sb.StandardsRatingCheck as standardsRatingCheck,
sb.SocialNetwork AS socialNetwork,
sb.SocialNetworkId AS socialNetworkId,
sb.socialNetworkScreenName AS socialNetworkScreenName,
sb.SourceOfInformationAboutSecretBuyers AS SourceOfInformationAboutSecretBuyers,
sb.suitable AS suitable,
sb.SocialNetworkMessagingEnable as socialNetworkMessagingEnable,
sb.IsInGroup as isInGroup,
sb.IsKeyWord as IsKeyWord,
sb.IsBanned as IsBanned,
sb.IsFraud as IsFraud,
sb.LastUnitsUpdateUtcDate as lastUnitsUpdateUtcDate,
sb.Comments as comments,
sb.MessagingApproval as messagingApproval,
sb.ProcessDataApproval as processDataApproval,
sb.CreatedDateTimeUtc AS createdDateTimeUtc,
sb.ModifiedDateTineUTC AS modifiedDateTime,
sb.Country AS country,
sb.PhoneNumberEditedUtc AS PhoneNumberEditedUtc
FROM (select * from
(SELECT s.*,
IF(s.LastSearchMessageDateUtc is null, 0, 1) as searched,
IF(cc.Id is null, 0, 1) as onCheck
FROM secretbuyers s
JOIN secretbuyerunits sbu ON sbu.SecretBuyerId = s.Id
LEFT JOIN outgoingMessageQueue mq ON mq.Type = 1 AND mq.VkUserId = s.SocialNetworkId and mq.State in (1,2)
LEFT JOIN checkcandidates cc ON cc.SecretBuyerId = s.id AND cc.State IN (1,4) AND cc.Date >= #p_checkBeginDateTime AND cc.Date <= #p_checkEndDateTime
WHERE sbu.UnitId = #p_unitId
AND (s.LastSearchMessageDateUtc is null OR s.LastSearchMessageDateUtc < #p_lastSearchMessageDateUtcLimit)
AND ((#p_deliveryCheck = true AND s.DeliveryCheck = true) OR (#p_restaurantCheck = true AND s.RestaurantCheck = true) OR (#p_standardsRatingCheck = true AND s.StandardsRatingCheck = true))
AND s.Suitable = true
AND s.IsRemove = false
AND mq.Id is null
AND s.IsBanned = false
AND s.IsFraud = false
AND s.IsAutoSearchable = true
group by s.Id
Order By onCheck ASC, searched asc, s.LastSearchMessageDateUtc asc) as temp
LIMIT #p_count) AS sb
JOIN secretbuyerunits su ON su.SecretBuyerId = sb.Id;
to EF Core 3.0 linq. The main problem is left joins. At first I've tried to do it like this:
private async Task<IEnumerable<MysteryShopper>> FindMysteryShoppers(
SearchMysteryShoppersParameters searchParameters, CancellationToken ct)
{
var lastSearchEdge = _nowProvider.UtcNow().Date.AddDays(-3);
bool shouldHaveDeliveryCheckMark = ShouldHaveDeliveryCheckMark(searchParameters.SearchType);
bool shouldHaveRestaurantCheckMark = ShouldHaveRestaurantCheckMark(searchParameters.SearchType);
bool shouldHaveStandardsCheckMark = ShouldHaveStandardsCheckMark(searchParameters.SearchType);
var lastCheckupEdgeDate = searchParameters.CheckupDates.Max().AddDays(-30);
return await _ratingsContext.SecretBuyers.AsNoTracking().Where(ms => !ms.IsBanned &&
!ms.IsFraud
&& ms.IsAutoSearchable
&& ms.Suitable
&& !ms.IsRemove
&& ms.SocialNetworkMessagingEnable
&& (ms.LastSearchMessageDateUtc == null
|| ms.LastSearchMessageDateUtc < lastSearchEdge)
)
.Where(ms => !shouldHaveDeliveryCheckMark && ms.DeliveryCheck)
.Where(ms => !shouldHaveRestaurantCheckMark && ms.RestaurantCheck)
.Where(ms => !shouldHaveStandardsCheckMark && ms.StandardsRatingCheck)
.Where(ms => ms.MysteryShopperUnits
.Any(u => searchParameters.UnitIds.Contains(u.UnitId))
)
.GroupJoin(_ratingsContext.Checkups.AsNoTracking().Where(cc => !_cancelledStates.Contains(cc.State)), ms => ms.Id,
cc => cc.SecretBuyerId, (ms, cc) => new
{
MysteryShopper = ms,
LastCheckupDate = cc
.Max(c => c.Date)
}
)
.GroupJoin(_ratingsContext.Messages.AsNoTracking().Where(m =>
m.Type == OutgoingMessageType.CandidateSearch
&& (m.State == OutgoingMessageState.Sending || m.State == OutgoingMessageState.Waiting)),
ms => ms.MysteryShopper.SocialNetworkId,
m => m.VkUserId,
(ms, m) =>
new {ms.MysteryShopper, ms.LastCheckupDate, HasPendingMessages = m.Any()})
.Where(ms => !ms.HasPendingMessages)
.Where(ms => ms.LastCheckupDate < lastCheckupEdgeDate)
.OrderBy(ms => ms.LastCheckupDate != null)
.ThenBy(ms => ms.MysteryShopper.LastSearchMessageDateUtc != null)
.ThenBy(ms => ms.MysteryShopper.LastSearchMessageDateUtc)
.Select(ms => ms.MysteryShopper)
.ToArrayAsync(ct);
}
After running this query I got error:
... by 'NavigationExpandingExpressionVisitor' failed. This may indicate either a bug or a limitation in EF Core. See https://go.microsoft.com/fwlink/?linkid=2101433 for more detailed information.
After that I discovered from this question that you can't make group by queries evaluated on client anymore.
After more searching I've discovered another solution. So i've re-written my method like this:
private async Task<IEnumerable<MysteryShopper>> FindMysteryShoppers(
SearchMysteryShoppersParameters searchParameters, CancellationToken ct)
{
var lastSearchEdge = _nowProvider.UtcNow().Date.AddDays(-3);
bool shouldHaveDeliveryCheckMark = ShouldHaveDeliveryCheckMark(searchParameters.SearchType);
bool shouldHaveRestaurantCheckMark = ShouldHaveRestaurantCheckMark(searchParameters.SearchType);
bool shouldHaveStandardsCheckMark = ShouldHaveStandardsCheckMark(searchParameters.SearchType);
var lastCheckupEdgeDate = searchParameters.CheckupDates.Max().AddDays(-30);
var mysteryShoppersLeftJoinedWithCheckups = from ms in _ratingsContext.Set<MysteryShopper>()
where ms.MysteryShopperUnits
.Any(u => searchParameters.UnitIds.Contains(u.UnitId)
&& !shouldHaveStandardsCheckMark && ms.StandardsRatingCheck
&& !shouldHaveRestaurantCheckMark && ms.RestaurantCheck
&& !shouldHaveDeliveryCheckMark && ms.DeliveryCheck
&& !ms.IsBanned
&& !ms.IsFraud
&& ms.IsAutoSearchable
&& ms.Suitable
&& !ms.IsRemove
&& ms.SocialNetworkMessagingEnable
&& (ms.LastSearchMessageDateUtc == null || ms.LastSearchMessageDateUtc < lastSearchEdge))
join cc in _ratingsContext.Set<Checkup>()
on ms.Id equals cc.SecretBuyerId into checkups
from cc in checkups.DefaultIfEmpty()
where !_cancelledStates.Contains(cc.State)
select new {MysteryShopper = ms, LastCheckupDate = checkups.Max(c => c.Date)};
var mysteryShoppersLeftJoinedWithCheckupsAndMessages =
from mysteryShopperWithCheckup in mysteryShoppersLeftJoinedWithCheckups
join m in _ratingsContext.Set<Message>()
on mysteryShopperWithCheckup.MysteryShopper.SocialNetworkId equals m.VkUserId into messages
from m in messages.DefaultIfEmpty()
where m.Type == OutgoingMessageType.CandidateSearch && (m.State == OutgoingMessageState.Sending ||
m.State == OutgoingMessageState.Waiting)
select new
{
mysteryShopperWithCheckup.MysteryShopper,
mysteryShopperWithCheckup.LastCheckupDate,
HasPendingMessages = messages.Any()
};
var orderedMysteryShoppersWithoutPendingMessage =
from mysteryShopperWithCheckupAndMessage in mysteryShoppersLeftJoinedWithCheckupsAndMessages
where !mysteryShopperWithCheckupAndMessage.HasPendingMessages &&
mysteryShopperWithCheckupAndMessage.LastCheckupDate < lastCheckupEdgeDate
orderby mysteryShopperWithCheckupAndMessage.LastCheckupDate != null,
mysteryShopperWithCheckupAndMessage.MysteryShopper.LastSearchMessageDateUtc != null,
mysteryShopperWithCheckupAndMessage.MysteryShopper.LastSearchMessageDateUtc
select mysteryShopperWithCheckupAndMessage.MysteryShopper;
return await orderedMysteryShoppersWithoutPendingMessage.ToArrayAsync(ct);
}
But now i get another error:
could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().
After some more testing i know for sure that checkups.Max(c => c.Date)} and HasPendingMessages = messages.Any() is the source of this error. How to fix this query? You can suggest completely different solution or approach. The main idea is to bring logic from sql to c#.
PS: Sorry for long question.
Added navigation properties and nugget Z.EntityFramework.Plus (for IncludeFilter) and rewritten query like this:
var query = _ratingsContext.SecretBuyers
.AsNoTracking()
.IncludeFilter(ms => ms.Checkups.Where(cc => !_cancelledStates.Contains(cc.State)))
.IncludeFilter(ms => ms.Messages.Where(m => m.Type == OutgoingMessageType.CandidateSearch &&
(m.State == OutgoingMessageState.Sending ||
m.State == OutgoingMessageState.Waiting)))
.Where(ms => !ms.IsBanned
&& !ms.IsFraud
&& ms.IsAutoSearchable
&& ms.Suitable
&& !ms.IsRemove
&& ms.SocialNetworkMessagingEnable
&& (ms.LastSearchMessageDateUtc == null || ms.LastSearchMessageDateUtc < lastSearchEdge)
)
.Where(ms => !shouldHaveDeliveryCheckMark || ms.DeliveryCheck)
.Where(ms => !shouldHaveRestaurantCheckMark || ms.RestaurantCheck)
.Where(ms => !shouldHaveStandardsCheckMark || ms.StandardsRatingCheck)
.Where(ms => ms.MysteryShopperUnits
.Any(u => searchParameters.UnitIds.Contains(u.UnitId))
)
.Where(ms => !ms.Messages.Any())
.Select(x => new {MysteryShopper = x, LastCheckupDate = x.Checkups.Max(c => c.Date)})
.Where(ms => ms.LastCheckupDate < lastCheckupEdgeDate)
.OrderBy(x => x.LastCheckupDate != null)
.ThenBy(x => x.MysteryShopper.LastSearchMessageDateUtc != null)
.ThenBy(x => x.MysteryShopper.LastSearchMessageDateUtc)
.Select(x => x.MysteryShopper)
.Take(searchParameters.MessagesPerUnit);
I'm trying to gain performance on this query, and I'd like to know if calling a Select() before the Where() I could have some improvement:
public async Task<List<PostValues>> GetValuesToTheDashboard(DataFilter filter, CancellationToken cancellationToken) {
long startTimestanp = Helpers.UnixTimeNow(filter.StartDate);
long endTimestanp = Helpers.UnixTimeNow(filter.EndDate);
return await
_context.CatchDetails.Where(
x => x.Monitoring.Client.Id == filter.CustomerId && x.Data.published >= startTimestanp
&& x.Data.published <= endTimestanp
&& ((filter.Sentiment == Sentiments.ALL) || x.Sentiment_enum == filter.Sentiment)
&& (filter.MonitoringId == 0 || x.Monitoring.id == filter.MonitoringId)
&& (filter.KeywordId == 0 || x.Keyword.Id == filter.KeywordId)
&& (filter.MotiveId == 0 || x.Motive.Id == filter.MotiveId)
&& (filter.SocialNetwork.Count == 0 || filter.SocialNetwork.Any(s => x.Data.social_media == s))
&& (filter.Busca == "" || x.Data.content_snippet.Contains(filter.Busca))
&& (filter.Gender.Count == 0 || filter.Gender.Any(g => x.Data.extra_author_attributes.gender_enum == g)))
.Select(s => new PostValues() {
CatchDetailsId=s.Id,
Monitoring=s.Monitoring.name,
Keyword=s.Keyword.Text,
Motive=s.Motive.Name,
Sentiment=s.Sentiment_enum,
Gender=s.Data.extra_author_attributes.gender_enum,
SocialMedia=s.Data.social_media,
Country=s.Data.extra_author_attributes.world_data.country_code,
State=s.Data.extra_author_attributes.world_data.region,
Published=s.Data.published
}).ToListAsync(cancellationToken);
}
There might be a way how to improve performance, but it won't be with switching Select and Where (as Chetan mentioned in the comment).
You could build the query in a sequence and based on the filter get a simpler query in the end. This would go like this:
var query = _context.CatchDetails.Where(
x => x.Monitoring.Client.Id == filter.CustomerId && x.Data.published >= startTimestanp
&& x.Data.published <= endTimestanp);
if (filter.Sentiment != Sentiments.ALL)
{
query = query.Where(x => x.Sentiment_enum == filter.Sentiment);
}
if (filter.MonitoringId != 0)
{
query = query.Where(x => x.Monitoring.id == filter.MonitoringId);
}
[...]
return await
query.Select(s => new PostValues() {
[...]
}).ToListAsync(cancellationToken);
Do not forget, the variable query is already on memory of the application when the SQL returns data. If there is many results it could throw memory exception.
I suggest that you limit the range of date on that search.
I am doing the below linq query which is costing me a lot and this query is in a loop which I can not avoid and I have to do it in C# which also I can not avoid. I have lot of logic above the linq query and after the query. I wanted to check if I can change anything on the query to improve the performance at least a little bit.
lstDataTable.Where(i => i.Field<int>("ALLL_Snapshot_ID") == 20 &&
i.Field<int>("ALLL_Analysis_Segment_Group_Column_ID") == 5 &&
i.Field<DateTime>("OriginationDate") > startingSnapshotDate &&
i.Field<DateTime>("OriginationDate") <= endingSnapshotDate &&
snapshotDataWithDate.Select(j => j.Field<string>
("MaturityDateBorrowerIdNoteNumberKey")).Contains(i.Field<string>
("MaturityDateBorrowerIdNoteNumberKey")) &&
snapshotDataWithDate.Select(j => j.Field<string>
("OriginationDateBorrowerIdNoteNumberKey")).Contains(i.Field<string>
("OriginationDateBorrowerIdNoteNumberKey")))
.Select(i => i.Field<Decimal>("BalanceOutstanding") + i.Field<Decimal>
("UndisbursedCommitmentAvailability")).Sum();
where lstDataTable and snapshotDataWithDate are IEnumerable of DataRow.
I tried above query using join but it is not joining properly. The difference between the two results is way high. Below is the query I tried using join
(from p in lstDataTable
join t in snapshotDataWithDate on p.Field<string>
("MaturityDateBorrowerIdNoteNumberKey") equals t.Field<string>
("MaturityDateBorrowerIdNoteNumberKey") &&
p.Field<string>("OriginationDateBorrowerIdNoteNumberKey") equals
t.Field<string>("OriginationDateBorrowerIdNoteNumberKey")
where p.Field<int>("ALLL_Analysis_Segment_Group_Column_ID") ==
SegmentGroupCECLSurvivalRateObj.ALLL_Segment_Group_Column_ID &&
p.Field<DateTime>("OriginationDate") > startingSnapshotDate &&
p.Field<DateTime>("OriginationDate") <= endingSnapshotDate
select p.Field<Decimal>("BalanceOutstanding") + p.Field<Decimal>
("UndisbursedCommitmentAvailability")).Sum();
Try this query, I have changed some expressions in where clause.
lstDataTable.Where(i => i.Field<int>("ALLL_Snapshot_ID") == 20 &&
i.Field<int>("ALLL_Analysis_Segment_Group_Column_ID") == 5 &&
i.Field<DateTime>("OriginationDate") > startingSnapshotDate &&
i.Field<DateTime>("OriginationDate") <= endingSnapshotDate &&
snapshotDataWithDate.Any(j => j.Field<string>
("MaturityDateBorrowerIdNoteNumberKey") == i.Field<string>
("MaturityDateBorrowerIdNoteNumberKey")) &&
snapshotDataWithDate.Any(j => j.Field<string>
("OriginationDateBorrowerIdNoteNumberKey") == i.Field<string>
("OriginationDateBorrowerIdNoteNumberKey")))
.Select(i => i.Field<Decimal>("BalanceOutstanding") + i.Field<Decimal>
("UndisbursedCommitmentAvailability")).Sum();
Perhaps pulling out the Field accesses will provide a small amount of optimization?
var snapshotDataConvertedMDB = snapshotDataWithDate.Select(r => r.Field<string>("MaturityDateBorrowerIdNoteNumberKey")).ToList();
var snapshotDataConvertedODB = snapshotDataWithDate.Select(r => r.Field<string>("OriginationDateBorrowerIdNoteNumberKey")).ToList();
var ans = lstDataTable
.Select(r => new {
ALLL_Snapshot_ID = r.Field<int>("ALLL_Snapshot_ID"),
ALLL_Analysis_Segment_Group_Column_ID = r.Field<int>("ALLL_Analysis_Segment_Group_Column_ID"),
OriginationDate = r.Field<DateTime>("OriginationDate"),
MaturityDateBorrowerIdNoteNumberKey = r.Field<string>("MaturityDateBorrowerIdNoteNumberKey"),
OriginationDateBorrowerIdNoteNumberKey = r.Field<string>("OriginationDateBorrowerIdNoteNumberKey"),
BalanceOutstanding = r.Field<Decimal>("BalanceOutstanding"),
UndisbursedCommitmentAvailability = r.Field<Decimal>("UndisbursedCommitmentAvailability")
})
.Where(i => i.ALLL_Snapshot_ID == 20 &&
i.ALLL_Analysis_Segment_Group_Column_ID == 5 &&
i.OriginationDate > startingSnapshotDate &&
i.OriginationDate <= endingSnapshotDate &&
snapshotDataConvertedMDB.Contains(i.MaturityDateBorrowerIdNoteNumberKey) &&
snapshotDataConvertedODB.Contains(i.OriginationDateBorrowerIdNoteNumberKey))
.Select(i => i.BalanceOutstanding + i.UndisbursedCommitmentAvailability)
.Sum();
I am trying to filter a data service using a List, however, I get a few error messages. Is there a way to filter by a List with a DataServiceQuery?
DataServiceQuery<Order> ordersQuery = (from x in entities.Orders
.Expand<DataServiceCollection<Shipment>>(a => a.Shipments)
.Expand<DataServiceCollection<OrderItem>>(a => a.OrderItems)
.Expand<OrderStatus>(a => a.OrderStatus)
.Expand<Customer>(a => a.Customer)
.Expand<Store>(a => a.Store)
.Expand<Marketplace>(a => a.Marketplace)
.Expand("Shipments/Carrier")
where x.OrderStatusID != 4
select x) as DataServiceQuery<Order>;
if (beginDate != null && endDate != null)
ordersQuery = ordersQuery.Where(x => x.CreateDate >= beginDate && x.CreateDate <= endDate) as DataServiceQuery<Order>;
if (statuses != null && statuses.Count() > 0)
ordersQuery = ordersQuery.Where(a => stores.Contains(a.OrderStatus.Name)) as DataServiceQuery<Order>;
if (stores != null && stores.Count() > 0)
ordersQuery = ordersQuery.Where(a => stores.Contains(a.Store.StoreName)) as DataServiceQuery<Order>;
if (!String.IsNullOrWhiteSpace(orderNumber))
ordersQuery = ordersQuery.Where(a => a.OrderNumber == orderNumber) as DataServiceQuery<Order>;
That code fails with
Error translating Linq expression to URI: The method 'Contains' is not supported.
This code also fails:
if (statuses != null && statuses.Count() > 0)
{
var statusFilterList = statuses.Select(title => String.Format("(Name eq {0})", title));
var statusFilter = String.Join(" or ", statusFilterList);
ordersQuery.AddQueryOption("$filter", statusFilter).Execute().ToList();
}
Can't add query option '$filter' because it would conflict with the query options from the translated Linq expression.
My Filter is problematic. My goal is to have the datagrid on load show any with a created person where date > Today's day - 1 month.
I have some filters surname, forename. I want to Display any Person that had some activities within last 3 months. Thats found through this query
(ctx.Interactions.Where(z => z.Attendees.Where(w => w.Person_Id == x.Id).Any() && z.ActivityDate >= recent ).Any())
The issue I'm having is when surname or forename is filled, I want the query to ignore the created date prequisite and the Interaction Prerequisite.
Items.AddRange(ctx.People.
Where(x => (
((Surname.Length == 0) && (Forename.Length == 0)) ?
(x.Created > limit) : true &&
(((Surname.Length == 0) || x.Surname.StartsWith(Surname)) &&
((Forename.Length == 0) || x.Forename.StartsWith(Forename)) &&
(ctx.Interactions.Where(z => z.Attendees.Where(w => w.Person_Id == x.Id).Any() && z.ActivityDate >= recent ).Any())
One thing I did try was to move the Interaction query and had it with the x.created but that ruined the runtime. Currently it runs around 15seconds, with that change it takes about 2 mins. Any tips or suggestions would be great.
recent is today's date - 3 months
You can split the filtering expression:
var filteredPeople = ctx.People; // here var should be replaced with IEnumerable<T> where T is the type of items in People
if (string.IsNullOrEmpty(Surname) && string.IsNullOrEmpty(Forename))
filteredPeople = filteredPeople.Where(x => x.Created > limit);
else
{
if (!string.IsNullOrEmpty(Surname))
filteredPeople = filteredPeople.Where(x => x.Surname.StartsWith(Surname));
if (!string.IsNullOrEmpty(Forename))
filteredPeople = filteredPeople.Where(x => x.Forename.StartsWith(Forename));
filteredPeople = filteredPeople.Where(x => ctx.Interactions.Where(z => z.ActivityDate >= recent)
.Any(z => z.Attendees.Any(w => w.Person_id == p.Id)));
}
Items.AddRange(filteredPeople);
I think your issue may be in the use of your conditional operator. The conditional operator takes the ? and : clauses completely independently. So what you're currently telling it is,
If Surname and Forename are length 0
Then return ALL where x.Created > limit
Else
Then apply all other filters.
I've reworked the query a bit. This should improve performance because it will be retrieving fewer (and more accurate) results.
Items.AddRange(ctx.People.
Where(x => (
((Surname.Length == 0 && Forename.Length == 0) ? x.Created > limit : true)
&&
(
(Surname.Length == 0 || x.Surname.StartsWith(Surname))
&& (Forename.Length == 0 || x.Forename.StartsWith(Forename))
&& (ctx.Interactions.Any(z => z.Attendees.Any(w => w.Person_Id == x.Id) && z.ActivityDate >= recent))
)
Here's another version that doesn't hit the ctx twice, and instead traverses from Person > Attendee > Interaction instead of backwards. This might affect the performance as well:
Items.AddRange(ctx.People.
Where(x => (
((Surname.Length == 0 && Forename.Length == 0) ? x.Created > limit : true)
&&
(
(Surname.Length == 0 || x.Surname.StartsWith(Surname))
&& (Forename.Length == 0 || x.Forename.StartsWith(Forename))
&& x.Attendees.Any(attendee => attendee.Interactions.Any(interaction => interaction.ActivityDate >= recent))
)