Dealing with null values in chained linq-to-sql query expressions - c#

I have a L2S repository query which I'm stuggling to write in a nice way. It looks something like...
_orderRepository
.GetAllByFilter(o => o.CustomerId == id)
.Select(o =>
new CustomerOrderRecord
(
o.Id,
o.PartNumber,
o.Date
// ... etc, more order details
/* Here I need the last DateTime? the customer placed
an order for this item, which might be null.
So I end up with the following horrible part of
the query */
o.Customer.CustomerOrderRecords
.Where(x => x.PartNumber == o.PartNumber)
.OrderByDescending(x => x.Date).FirstOrDefault()
== null ? null :
o.Customer.CustomerOrderRecords
.Where(x => x.PartNumber == o.PartNumber)
.OrderByDescending(x => x.Date).First().Date;
)).ToList();
So hopefully you can see the problem that I'm having to write the whole query chain twice just to do the null check when receiving the LastOrdered value.
This needs to be written in-line (I think) because GetAllByFilter returns an IQueryable.
I tried to use an intermediate variable within the select statement, so I'd have something a bit like the following, but I couldn't get anything like that to compile.
.Select(o =>
new CustomerOrderRecord
(
o.Id,
o.PartNumber,
o.Date
// ... etc, more order details
var last = o.Customer.CustomerOrderRecords
.Where(x => x.PartNumber == o.PartNumber)
.OrderByDescending(x => x.Date).FirstOrDefault()
== null ? null : last.Date;
)).ToList();
Is there a syntax trick available which solves this problem?

Try using Select to fetch the Date member:
o.Customer.CustomerOrderRecords
.Where(x => x.PartNumber == o.PartNumber)
.OrderByDescending(x => x.Date)
.Select(x => (DateTime?)x.Date)
.FirstOrDefault()

Related

Using conditional statements in linq query c#

I have 2 queries. First gets the full name of a committee head if the committee has more that 1 member.
var result = await db.ExpertCommittees
.Where(f => f.Id == committeeId)
.Where(f => f.ExpertCommitteeMembers.Count > 1)
.Select(f => f.ExpertCommitteeMembers
.Where(m => m.IsCommitteeHead)
.FirstOrDefault().Expert.FullName)
.FirstOrDefaultAsync();
The second one gets the full name of the only committee member if the committee has only 1 member
var result2 = await db.ExpertCommittees
.Where(f => f.Id == committeeId)
.Where(f => f.ExpertCommitteeMembers.Count == 1)
.Select(f => f.ExpertCommitteeMembers
.FirstOrDefault().Expert.FullName)
.FirstOrDefaultAsync();
Is it possible to check how many members does the committee have and then return the correct name all in the same query? Or do I first have to check how many members does the committee have and then run the appropriate query seperatly?
If I understand correctly you can try to let condition into inner linqwhere
var result = await db.ExpertCommittees
.Where(f => f.Id == committeeId)
.Select(f => f.ExpertCommitteeMembers.Where(m =>
(m.IsCommitteeHead &&
f.ExpertCommitteeMembers.Count > 1)||f.ExpertCommitteeMembers.Count == 1).FirstOrDefault().Expert.FullName)
.FirstOrDefaultAsync();
you can Create a create anonymous object in linq query select. which will contain FullName and count.
var result = await db.ExpertCommittees
.Where(f => f.Id == committeeId)
.Where(f => f.ExpertCommitteeMembers.Count > 1)
.Select(f => new
{
FullName = f.ExpertCommitteeMembers
.Where(m => m.IsCommitteeHead)
.FirstOrDefault().Expert.FullName,
Count = f.ExpertCommitteeMembers.Count
})
.FirstOrDefaultAsync();
A generated Join, as it is generated from ExpertCommittees to
ExpertCommitteeMembers, with a Navigation-Collection, will always do a Left Join,
what you want is an Inner Join. It will give you only items with entities in both tables.
This will be something like
db.ExpertCommittees.Join(db.ExpertCommittemember, x=>someid, y=>somid,
(comittee, member) => new { comittee, member});
But this will give you one line per member.... with the possibility to filter by "IsComitteeHead" or whatever.
You can append Grouping, or use a GroupJoin directly, to have a List of "Commitees", each with a List of it's members ...and only if there are members.
Without knowing the data structure, ID's, Foreign-Keys and table name, we cannot produce a valid query.

Using two Linq query in a single method

As shown in the below code, the API will hit the database two times to perform two Linq Query. Can't I perform the action which I shown below by hitting the database only once?
var IsMailIdAlreadyExist = _Context.UserProfile.Any(e => e.Email == myModelUserProfile.Email);
var IsUserNameAlreadyExist = _Context.UserProfile.Any(x => x.Username == myModelUserProfile.Username);
In order to make one request to database you could first filter for only relevant values and then check again for specific values in the query result:
var selection = _Context.UserProfile
.Where(e => e.Email == myModelUserProfile.Email || e.Username == myModelUserProfile.Username)
.ToList();
var IsMailIdAlreadyExist = selection.Any(x => x.Email == myModelUserProfile.Email);
var IsUserNameAlreadyExist = selection.Any(x => x.Username == myModelUserProfile.Username);
The .ToList() call here will execute the query on database once and return relevant values
Start with
var matches = _Context
.UserProfile
.Where(e => e.Email == myModelUserProfile.Email)
.Select(e => false)
.Take(1)
.Concat(
_Context
.UserProfile
.Where(x => x.Username == myModelUserProfile.Username)
.Select(e => true)
.Take(1)
).ToList();
This gets enough information to distinguish between the four possibilities (no match, email match, username match, both match) with a single query that doesn't return more than two rows at most, and doesn't retrieve unused information. Hence about as small as such a query can be.
With this done:
bool isMailIdAlreadyExist = matches.Any(m => !m);
bool isUserNameAlreadyExist = matches.LastOrDefault();
It's possible with a little hack, which is grouping by a constant:
var presenceData = _Context.UserProfile.GroupBy(x => 0)
.Select(g => new
{
IsMailIdAlreadyExist = g.Any(x => x.Email == myModelUserProfile.Email),
IsUserNameAlreadyExist = g.Any(x => x.Username == myModelUserProfile.Username),
}).First();
The grouping gives you access to 1 group containing all UserProfiles that you can access as often as you want in one query.
Not that I would recommend it just like that. The code is not self-explanatory and to me it seems a premature optimization.
You can do it all in one line, using ValueTuple and LINQ's .Aggregate() method:
(IsMailIdAlreadyExist, IsUserNameAlreadyExist) = _context.UserProfile.Aggregate((Email:false, Username:false), (n, o) => (n.Email || (o.Email == myModelUserProfile.Email ? true : false), n.Username || (o.Username == myModelUserProfile.Username ? true : false)));

Get complex object using IN equivalent in LINQ

I have a list of type customer. I need to insert all values of the list in the database before checking if a customer with the same customer number exists for that particular client.
For that I am firing a query to get me all customers who are there in the database having customer number equal to ones in the list. The query I am writing is not working, here's the code.
CustomerRepository.Find(x => x.ClientId == clientId)
.Where(x => x.CustomerNumber.Contains(lstCustomersInserted.Select(c => c.CustomerNumber)));
Keep it simple:
var lstCustomerNumbers = lstCustomersInserted.Select(c => c.CustomerNumber);
var res = CustomerRepository.Where(x => x.ClientId == clientId && lstCustomerNumbers.Any(c => c == x.CustomerNumber));
I think you have it backwards. Try reversing the Contains.
Edit: I switched to using the generic predicate Exists instead of Contains based on the comment, so you can match a property.
CustomerRepository.Find(x => x.ClientId == clientId)
.Where(x => lstCustomersInserted.Exists(c => x.CustomerNumber == c.CustomerNumber));
How about an Except?
CustomerRepository.Select(x => x.ClientID)
.Except(lstCustomersInserted.Select(x => x.CustomerID));
This will return the IDs of the objects in the repo that don't exist in your lstCustomersInserted.

Fluent nHibernate Query => QueryOver, Join, Distinct

I try to change that query to QueryOver<> to be able to do the Distinct operation yet inside the (generated sql) query
var result = (from x in Session.Query<Events>()
join o in Session.Query<Receivers>() on x.ID equals o.ID
where x.Owner.ID == 1 //the user is the owner of that Event (not null)
||
x.EVType.ID == 123 //(not null)
||
x.Receivers.Count(y => y.User.ID == 1) > 0 //the user is one of the Event Receivers
select x.StartDate)
.Distinct();
I tried something like that
Events x = null;
List<Receivers> t = null;
var result = Session.QueryOver<Events>(() => x)
.JoinAlias(() => x.Receivers, () => t)
.Where(() => x.Owner.ID == 1
||
x.EVType.ID == 123
||
t.Count(y => y.User.ID == 1) > 0)
.TransformUsing(Transformers.DistinctRootEntity)
.Select(a => a.StartDate)
.List();
but then I got the Value can not be null. Parameter name: source exception. Any ideas how can I fix that query ?
edit
thanks to the xanatos' answer, the final SQL query is correct (I used his 2nd approach):
SELECT distinct this_.StartDate as y0_
FROM Events this_
WHERE
(
this_.UserID = ?
or
this_.EventTypeID = ?
or
exists (SELECT this_0_.ID as y0_
FROM Receivers this_0_
WHERE this_0_.UserID = ?)
)
"In QueryOver, aliases are assigned using an empty variable. The variable can be declared anywhere (but should be empty/default at runtime). The compiler can then check the syntax against the variable is used correctly, but at runtime the variable is not evaluated (it's just used as a placeholder for the alias)." http://nhibernate.info/blog/2009/12/17/queryover-in-nh-3-0.html
Setting List<Receivers> t to empty collection as you did (as you have mentioned in comments) means that you check is event id in local empty collection - doesn't have sense at all.
You can try do your query with subquery (should work but i'm not sure, I wrote it without testing, "by hand"):
Receivers receiversSubQueryAlias = null;
var subquery = session.QueryOver<Events>()
.JoinQueryOver<Receivers>(x => x.Receivers, () => receiversSubqueryAlias, JoinType.Inner)
.Where(()=> receiversSubQueryAlias.UserId == 1)
.Select(x => x.Id)
.TransformUsing(Transformers.DistinctRootEntity);
Events eventsAlias = null;
var mainQueryResults = session.QueryOver<Events>(() => eventsAilas)
.Where(Restrictions.Disjunction()
.Add(() => eventAlias.OwnerId == 1)
.Add(() => eventAlias.EVType.Id == 123)
.Add(Subqueries.WhereProperty<Events>(() => eventAlias.Id).In(subquery))
).Select(x => x.StartDate)
.TransformUsing(Transformers.DistinctRootEntity)
.List();
As written by #fex, you can't simply do a new List<Receivers>. The problem is that you can't mix QueryOver with "LINQ" (the t.Count(...) part). The QueryOver "parser" tries to execute "locally" the t.Count(...) instead of executing it in SQL.
As written by someone else, TransformUsing(Transformers.DistinctRootEntity) is client-side. If you want to do a DISTINCT server-side you have to use Projections.Distinct .
You have to make an explicit subquery. Here there are two variants of the query. the first one is more similar to the LINQ query, the second one doesn't use the Count but uses the Exist (in LINQ you could have done the same by changing the Count(...) > 0 with a Any(...)
Note that when you use a .Select() you normally have to explicitly tell the NHibernate the type of the .List<something>()
Events x = null;
Receivers t = null;
// Similar to LINQ, with COUNT
var subquery2 = QueryOver.Of<Receivers>(() => t)
.Where(() => t.SOMETHING == x.SOMETHING) // The JOIN clause between Receivers and Events
.ToRowCountQuery();
var result2 = Session.QueryOver<Events>(() => x)
.Where(Restrictions.Disjunction()
.Add(() => x.Owner.ID == 1)
.Add(() => x.EVType.ID == 123)
.Add(Subqueries.WhereValue(0).Lt(subquery2))
)
.Select(Projections.Distinct(Projections.Property(() => x.StartDate)))
.List<DateTime>();
// With EXIST
var subquery = QueryOver.Of<Receivers>(() => t)
.Where(() => t.SOMETHING == x.SOMETHING) // The JOIN clause between Receivers and Events
.Select(t1 => t1.ID);
var result = Session.QueryOver<Events>(() => x)
.Where(Restrictions.Disjunction()
.Add(() => x.Owner.ID == 1)
.Add(() => x.EVType.ID == 123)
.Add(Subqueries.WhereExists(subquery))
)
.Select(Projections.Distinct(Projections.Property(() => x.StartDate)))
.List<DateTime>();
Note that you'll have to set "manually" the JOIN condition in the subquery.
Hopefully this answer can help others. This error was being caused by declaring
List<Receivers> t = null;
followed by the query expression
t.Count(y => y.User.ID == 1) > 0
The QueryOver documentation states "The variable can be declared anywhere (but should be empty/default at runtime)." Since in this case, the place holder is a List, you must initialize it as an empty list.
List<Receivers> t = new List<Receivers>();
Otherwise, when you try to reference the Count method, or any other method on the placeholder object, the source (t) will be null.
This however still leaves a problem as #fex and #xanatos, in which it makes no sense to reference Count() from the alias List t, as it won't convert into SQL. Instead you should be creating a subquery. See their answers for more comprehensive answer.

Linq Where/OrderBy string to int

I have seen similar questions on here but none of the answers are working for my linq query.
I am trying to convert a string to integer on the .ThenBy()
dbResults = gaResultDetails.All
.Where(c => c.ContentLink.Id == contentId && c.RequestType.Id == requestTypeId)
.OrderBy(c => c.DateFrom)
.ThenBy(c => int.Parse(c.Data_2)).Take(Take).ToList();
Please note I am using nHibernate for data access and with the above expression get the following error:
[NotSupportedException: Int32 Parse(System.String)]
Help!
Some functions are not supported by the nhibernate linq expression builder, try this:
dbResults = gaResultDetails.All
.Where(c => c.ContentLink.Id == contentId && c.RequestType.Id == requestTypeId)
.AsEnumerable()
.OrderBy(c => c.DateFrom)
.ThenBy(c => int.Parse(c.Data_2))
.Take(Take)
.ToList();
Might not be ideal performance-wise, but should accomplish what you need.
This is just a shot in the dark. If the parse doesn't work in the ThenBy, it probably won't in the let but it's worth a shot. In LINQ syntax, cuz I like it better:
dbResults = (from c in gaResultDetails.All
where c.ContentLink.Id == contentId
&& c.RequestType.Id == requestTypeId
let nData2 = int.Parse(c.Data_2)
orderby c.DateFrom, nData2)
.Take(Take)
.ToList();
It seems like your ORM tries to perform casting on the SQL server side.
Try to evaluate data before casting, e.g. :
dbResults = gaResultDetails.All
.Where(c => c.ContentLink.Id == contentId && c.RequestType.Id == requestTypeId)
.OrderBy(c => c.DateFrom).ToList()
.ThenBy(c => int.Parse(c.Data_2)).Take(Take).ToList();

Categories

Resources