c# Lambda referencing class object in Select method - c#

I've written this code which works:
var uniqueCustomerIdList = services
.GroupBy(x => x.CustomerId)
.Select(cl => new Customer
{
CustomerId = cl.First().CustomerId,
CustomerName = cl.First().CompanyName,
PdfServices = services.Where(x => x.CustomerId == cl.First().CustomerId).ToList(),
PdfServiceLines = services.Where(x => x.CustomerId == cl.First().CustomerId).ToList()
.GroupBy(l => l.ServiceDescription)
.Select(cy => new PdfServiceLine
{
ServiceName = cy.First().ServiceDescription,
Quantity = cy.Count(),
UnitPrice = cy.First().PlanCharge,
ServiceCharges = services.Where(x => x.CustomerId == cl.First().CustomerId && x.ServiceDescription == cy.First().ServiceDescription).Sum(y => y.TotalBill),
UsageCharges = usage.Where(x => x.CustomerId == cl.First().CustomerId && x.ServiceDescription == cy.First().ServiceDescription).Sum(y => y.Charge),
Total = cy.Sum(c => c.PlanCharge),
}).ToList(),
PdfUsages = usage.Where(x => x.CustomerId == cl.First().CustomerId).ToList()
})
.ToList();
I wanted to know if it's possible to reference values from the outer Select statement in the inner statement? As it looks rather clunky at the moment.
For instance in the outer Customer select I use PdfServices - can I use that in the inner select where I have ServiceCharges?
ServiceCharges = PdfServices.Where(s => s.ServiceDescription == cy.First().ServiceDescription).Sum(y => y.TotalBill)
instead of
ServiceCharges = services.Where(x => x.CustomerId == cl.First().CustomerId && x.ServiceDescription == cy.First().ServiceDescription).Sum(y => y.TotalBill),
Thanks,
Lee.

The results that you want to re-use are members of an anonymous object, not variables. As such, you cannot expect them to be available, like a variable would be, to the inner lambda. If you re-wrote your statement, you could store them in intermediate variables:
var uniqueCustomerIdList = services
.GroupBy(x => x.CustomerId)
.Select(cl =>
{
var pdfServices = services.Where(x => x.CustomerId == cl.First().CustomerId).ToList();
return new Customer
{
CustomerId = cl.First().CustomerId,
CustomerName = cl.First().CompanyName,
PdfServices = pdfServices,
PdfServiceLines = pdfServices
.GroupBy(l => l.ServiceDescription)
.Select(cy => new PdfServiceLine
{
ServiceName = cy.First().ServiceDescription,
Quantity = cy.Count(),
UnitPrice = cy.First().PlanCharge,
ServiceCharges = services.Where(x => x.CustomerId == cl.First().CustomerId && x.ServiceDescription == cy.First().ServiceDescription).Sum(y => y.TotalBill),
UsageCharges = usage.Where(x => x.CustomerId == cl.First().CustomerId && x.ServiceDescription == cy.First().ServiceDescription).Sum(y => y.Charge),
Total = cy.Sum(c => c.PlanCharge),
}).ToList(),
PdfUsages = usage.Where(x => x.CustomerId == cl.First().CustomerId).ToList()
};
})
.ToList();
Note that the lamda body now uses curly braces and a return statement. This is not convertible to an expression tree, and some ORM frameworks, like Entity Framework, will not be able to translate the lamda into SQL.

Related

Linq variable inside select

I have a following query
return _context.Table1
.Where(x => x.ContactId == contactKey.Id)
.Include(x => x.Table2)
.Include(x => x.Table3.Table4)
.Select(a =>
new MyReadModel
{
PriorityAssignment = true,
LastContactedDate = (a.Table3.Table4 != null && a.Table3.Table4.FirstOrDefault(h =>
h.Id == a.Table2.FkId
) != null ?
a.Table3.Table4.FirstOrDefault(h => && h.Id == a.Table2.FkId
).LastContactedDatetime : default
)
}
)
.ToListAsync();
What i wants is to simplify LastContactedDate assignment with in select. I think we can assign
a.Table3.Table4.FirstOrDefault(h =>
h.Id == a.Table2.FkId
)
to some variable but can't able to do it
can someone identify what is needed
With EF Core you don't have to check for null, LINQ Translator do not execute your code, but uses it for translation to the SQL. Also Includes is not needed if you do not retrieve whole objects.
return await _context.Table1
.Where(x => x.ContactId == contactKey.Id)
.Select(a => new MyReadModel
{
PriorityAssignment = true,
LastContactedDate = (DateTime?)a.Table3.Table4.Where(h => h.Id == a.Table2.FkId)
.OrderByDescending(h => LastContactedDatetime)
.Select(h => LastContactedDatetime)
.FirstOrDefault()
}).ToListAsync();
you can use it like this example
List<string> someList= new List<string>();
someList= someList.Select(x =>
{
var newVariable= "newVariable";
return newVariable;
}).ToList();
in your case
return _context.Table1
.Where(x => x.ContactId == contactKey.Id)
.Include(x => x.Table2)
.Include(x => x.Table3.Table4)
.Select(a => {
Table4 newVariable = null;
if(a.Table3.Table4 != null)
newVariable = a.Table3.Table4.FirstOrDefault(h => h.Id == a.Table2.FkId;
var result = new MyReadModel
{
PriorityAssignment = true,
LastContactedDate = (newVariable != null ? newVariable.LastContactedDatetime : default
)
};
}
) .ToListAsync();

Understanding EF core subquery [duplicate]

This question already has answers here:
Multiple aggregate functions in one query using Linq to SQL [duplicate]
(1 answer)
Return multiple aggregate columns in LINQ
(4 answers)
Closed last year.
I have the following linq statements which I am hoping to cut down from multiple round trips to my database to one using subquery.
This is my code before refactoring:
IQueryable<Customers> customers= this.BaseQuery.Where(o => o.EmailAddress == emailAddress);
int totalCustomersCount = await customers.CountAsync();
int totalDisabledCustomersCount = await customers.Where(o => CustomerState.Disabled == o.CurrentState).CountAsync();
int totalCancelledCustomersCount = await customers.Where(o => CustomerState.Cancelled == o.CurrentState).CountAsync();
int totalExpiredCustomersCount = await customers.Where(o => CustomerState.Archived == o.CurrentState).CountAsync();
int totalPremiumCustomerCount = await customers.Where(o => CustomerState.Premium== o.CurrentState).CountAsync();
CustomerStatistics customerStatistics = new CustomerStatistics()
{
TotalCustomersCount = totalCustomersCount,
TotalDisabledCustomersCount = totalDisabledCustomersCount,
TotalCancelledCustomersCount = totalCancelledCustomersCount,
TotalExpiredCustomersCount= totalExpiredCustomersCount,
TotalPremiumCustomerCount= totalPremiumCustomerCount,
};
and I have reworked this code to look like this
IQueryable<Customers>? query = this.Context.Customers.Where(o => o.EmailAddress == emailAddress);
IQueryable<CustomerStatistics >? myQuery= query.Select(o => new CustomerStatistics()
{
TotalCustomersCount = query.Count(),
TotalDisabledCustomersCount = query.Where(o => o.CurrentState == CustomerState.Disabled).Count(),
TotalCancelledCustomersCount = query.Where(o => o.CurrentState == CustomerState.Cancelled).Count(),
TotalExpiredCustomersCount= query.Where(o => o.CurrentState == CustomerState.Archived).Count(),
TotalPremiumCustomerCount= query.Where(o => o.CurrentState == CustomerState.Premium).Count(),
});
Is this along the right lines? To me, it still looks like it will be makign round trips to the database to get each count.
Update to the example query:
IQueryable<Order> orders = this.BaseQuery.Where(o => o.CustomerContactId == customerContactId);
int totalOrderCount = await orders.CountAsync();
int totalInProgressOrderCount = await orders.Where(o => OrderState.Fulfilment == o.CurrentState).CountAsync();
int totalCancelledOrderCount = await orders.Where(o => OrderState.Cancelled == o.CurrentState).CountAsync();
int totalExpiredOrderCount = await orders.Where(o => OrderState.Archived == o.CurrentState).CountAsync();
int totalQuoteCount = await orders.Where(o => OrderState.Initial == o.CurrentState || OrderState.QuoteIssued == o.CurrentState || OrderState.ReadyToPlace == o.CurrentState || OrderState.CheckGeometry == o.CurrentState).CountAsync();
int totalIssuedOrderCount = await orders.Where(o => OrderState.QuoteIssued == o.CurrentState).CountAsync();
CustomerContactOrderStatistics customerContactOrderStatistics = new CustomerContactOrderStatistics()
{
TotalOrderCount = totalOrderCount,
TotalInProgressOrderCount = totalInProgressOrderCount,
TotalCancelledOrderCount = totalCancelledOrderCount,
TotalExpiredOrderCount = totalExpiredOrderCount,
TotalQuoteCount = totalQuoteCount,
TotalIssuedOrderCount = totalIssuedOrderCount,
};
You can do grouping by constant. It is special case which is handled by EF to make aggregation query:
vra query = this.Context.Customers
.Where(o => o.EmailAddress == emailAddress);
var statistics = = query.GroupBy(x => 1)
.Select(g => new CustomerStatistics()
{
TotalCustomersCount = g.Count(),
TotalDisabledCustomersCount = g.Count(o => o.CurrentState == CustomerState.Disabled),
TotalCancelledCustomersCount = g.Count(o => o.CurrentState == CustomerState.Cancelled),
TotalExpiredCustomersCount = g.Count(o => o.CurrentState == CustomerState.Archived),
TotalPremiumCustomerCount = g.Count(o => o.CurrentState == CustomerState.Premium),
});
For EF Core versions which do not support conditional Count, it can be emulated by Sum
var statistics = = query.GroupBy(x => 1)
.Select(g => new CustomerStatistics()
{
TotalCustomersCount = g.Count(),
TotalDisabledCustomersCount = g.Sum(o => o.CurrentState == CustomerState.Disabled ? 1 : 0),
TotalCancelledCustomersCount = g.Sum(o => o.CurrentState == CustomerState.Cancelled ? 1 : 0),
TotalExpiredCustomersCount= g.Sum(o => o.CurrentState == CustomerState.Archived ? 1 : 0),
TotalPremiumCustomerCount= g.Sum(o => o.CurrentState == CustomerState.Premium ? 1 : 0),
});

How to use if else statement inside select new

I have a list of FlightTicketRequestDocument used for both Patients and their Escorts requesting a plane ticket. Theses are grouped with a GUID named GroupId.
I'm trying to group them inside this object:
public class FlightTicketRequestDocumentGroup
{
public Guid GroupId { get; set; }
public FlightTicketRequestDocument PrincipalDocument { get; set; }
public List<FlightTicketRequestDocument> GroupDocuments { get; set; }
}
PrincipalDocument holds the Patient if there's one. (If there isn't we just want the first Escort)
GroupDocuments holds the Escorts. (Except the first one that is placed in PrincipalDocument)
I'm attempting to use different expressions to set my values inside a Select New statement depending if there's a Patient or not but I'm getting the error:
The nested query is not supported. Operation1='Case' Operation2='VarRef'
Here's the code (Note: StayEscortId is null when the person is a Patient.):
var indexGroups = await _db.FlightTicketRequests
.OrderBy(f => f.FirstName)
.ProjectTo<FlightTicketRequestDocument>()
.GroupBy(f => f.GroupId)
.Select(g => new FlightTicketRequestDocumentGroup
{
GroupId = g.Key,
PrincipalDocument = g.Any(f => f.StayEscortId == null)
? g.FirstOrDefault(f => f.StayEscortId == null)
: g.FirstOrDefault(),
GroupDocuments = g.Any(f => f.StayEscortId == null)
? g.Where(c => c.StayEscortId != null).ToList()
: g.Where(c => c.StayEscortId != null).OrderBy(f => f.FullName).Skip(1).ToList()
})
.ToListAsync(cancellationToken);
What can I use instead of the conditional operator ?: ?
The solution that I ended up using:
As specified by #mjwills, since I simply want the values where StayEscordId is null first, all I need to do is to sort as a boolean (false values end up at the top).
For the rest of the group, I do the same operation while Skipping 1 (the first one I chose).
Modified code:
var indexGroups = await _db.FlightTicketRequests
.OrderBy(f => f.FirstName)
.ProjectTo<FlightTicketRequestDocument>()
.GroupBy(f => f.GroupId)
.Select(g => new FlightTicketRequestDocumentGroup
{
GroupId = g.Key,
PrincipalDocument = g.OrderBy(f => f.StayEscortId != null).ThenBy(f => f.FullName).FirstOrDefault(),
GroupDocuments = g.OrderBy(f => f.StayEscortId != null).ThenBy(f => f.FullName).Skip(1).ToList()
})
.ToListAsync(cancellationToken);
Here's what the query would look like if you were to use if/else instead of the ternary conditions. You would just use a code block inside of the .Select() to return the results:
var flightTicketRequestDocuments = await _db.FlightTicketRequests
.OrderBy(f => f.FirstName)
.ProjectTo<FlightTicketRequestDocument>()
.GroupBy(f => f.GroupId)
.ToListAsync(cancellationToken);
var indexGroups = flightTicketRequestDocuments
.Select(g =>
{
FlightTicketRequestDocument principalDocument;
List<FlightTicketRequestDocument> groupDocuments;
if (g.Any(f => f.StayEscortId == null))
{
principalDocument = g.FirstOrDefault(f => f.StayEscortId == null);
groupDocuments = g.Where(c => c.StayEscortId != null).ToList();
}
else
{
principalDocument = g.FirstOrDefault();
groupDocuments = g.Where(c => c.StayEscortId != null).OrderBy(f => f.FullName).Skip(1).ToList();
}
return new FlightTicketRequestDocumentGroup
{
GroupId = g.Key,
PrincipalDocument = principalDocument,
GroupDocuments = groupDocuments
};
})
.ToList();
Note that this approach requires enumerating the IQueryable before performing the .Select() so that it's applied against an IEnumerable.
Needs a check or two (or a tweak or two) but if i remember correctly expression trees accept null coalescence operator you may try to swap in the place of the ternary.
Starting (and BASING) from your example, maybe something like the following?
var indexGroups = await _db.FlightTicketRequests
.OrderBy(f => f.FirstName)
.ProjectTo<FlightTicketRequestDocument>()
.GroupBy(f => f.GroupId)
.Select(g => new FlightTicketRequestDocumentGroup
{
GroupId = g.Key,
PrincipalDocument = g.FirstOrDefault(f => f.StayEscortId == null) ?? g.OrderBy(f => f.FullName).FirstOrDefault(),
GroupDocuments = g.Where(c => c.StayEscortId != null).OrderBy(f => f.FullName).Skip(Math.Max(x.Count(y => y.StayEscortId == null), 1)).ToList()
})
.ToListAsync(cancellationToken);

Lambda query previous parameter access

Is possible to access a parameter variable of a previous condition?
How to do this?
var result = collectionA
.First(a => a.id == 1)
.CollectionB
.SelectMany(b => b.CollectionC)
.Select(c => new { propA = a.id, propC = c.id });
You can write
var result = collectionA
.Where(q => q.id == 1).Take(1) //it will be collection of 1 element
.Select(a => a
.CollectionB
.SelectMany(b => b.CollectionC)
.Select(c => new { propA = a.id, propC = c.id })).First();
or
(new [] { collectionA.First(q => q.id == 1) })
.Select(a => a
....
or just
var a = collectionA.First(q => q.id = 1);
a.Select( .....
The solution based on #Artem's answer:
var result = CollectionA
.Where(a => a.id == 1)
.SelectMany(b =>
b.CollectionB
.SelectMany(c => c.CollectionC)
.Select(c => new { b.id, c.id })
);

OrderByDescending doesn't work in nested linq statement

In Linqpad, i can see the correct list. But in code, after putting in the list collection, order by doesn't work for BeginDate. If i use BeginDate with Max, it works. I don't understand where i am wrong?
var templist = contentRepository
.Get(q => (q.Status == (int)StatusEnum.Active) &&
(q.CategoryId == category.GetHashCode() || q.Category.ParentId == category.GetHashCode())
&& q.MinorVersion == 0
&& q.MajorVersion > 0)
.GroupBy(q => q.VersionId)
.OrderByDescending(q => q.Key)
.Select(q => new
{
VersionId = q.Key,
Id = q.Max(x => x.Id),
MajorVersion = q.Max(x => x.MajorVersion),
UpdatedAt = q.Max(x => x.UpdatedAt),
//BeginDate = q.Max(x=>x.BeginDate),
BeginDate = (q.OrderByDescending(x => x.Id).Take(1).Select(x=>x.BeginDate)).First(),
Title = (q.OrderByDescending(x => x.Id).Take(1).Select(x => x.Title)).First(),
ShowOnHomePage = (q.OrderByDescending(x => x.Id).Take(1).Select(x=>x.ShowOnHomePage)).First()
})
.OrderByDescending(x => x.BeginDate)
.Take(maxItemCount)
.ToList();
List<ContentEntity> contents = new List<ContentEntity>();
templist.ForEach(q => contents.Add(
contentRepository
.Get(x => x.VersionId == q.VersionId && x.MajorVersion == q.MajorVersion && x.MinorVersion == 0)
.FirstOrDefault()
));
return contents.Where(q => q.ShowOnHomePage == true)
.OrderByDescending(q => q.MajorVersion)
.OrderByDescending(q => q.BeginDate)
.Take(maxItemCount)
.ToList();
You are ordering by Id, not by BeginDate. Equivalent code for
q.Max(x => x.BeginDate)
Will be
q.OrderByDescending(x => x.BeginDate).Take(1).Select(x => x.BeginDate).First()
Or simplified
q.OrderByDescending(x => x.BeginDate).First().BeginDate

Categories

Resources