Using two Linq query in a single method - c#

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)));

Related

Merge 2 linq queries, using result from 1 to continue search

Currently I've written this:
public Task<List<BulkmailAnnouncementModel>> GetBulkmailAnnouncementsByPackageTrackingId(string packageTrackingIdentification)
{
var announcement = await Context.BulkmailAnnouncement
.Include(x => x.Tarras)
.Include(x => x.PackageTrackingIdentifications)
.Where(x => x.PackageTrackingIdentifications.Any(y => y.Value == packageTrackingIdentification) &&
x.AuditReportIndicator != true)
.FirstAsync();
return await Context.BulkmailAnnouncement
.Include(x => x.Tarras)
.Include(x => x.PackageTrackingIdentifications)
.Where(x => x.CustomerOrderId == announcement.CustomerOrderId && x.CustomerPartyAccountId == announcement.CustomerPartyAccountId)
.ToListAsync();
}
I'm trying to combine these 2 statements into 1. I want to query the database just once, because having the result of the first query load in memory and then using it for the 2nd is inefficient.
The result I want is a list of BulkmailAnnouncements which all have the same customerOrderId and CustomerPartyId.
In order to find out which customerOrderId and CustomerPartyId, I first need to find 1 bulkmailAnnouncement which has a packageTrackingIdentification equal to the parameter of the method. then use that bulkmailAnnouncement to find all other announcements with the same customerOrderId and CustomerPartyId.
Alas you didn't specify your requirement in words. So I have to look at your code to see what you want.
Apparently you have an input parameter packageTrackingIdentification and a queryable sequence of BulkmailAnnouncements.
Every BulkmailAnnouncement has a Boolean AuditReportIndicator and zero or more PackageTrackingIdentifications.
Your first query, fetches (several properties of) the first BulkmailAnnouncement that has a true AuditReportIndicator and at least one PackageTrackingIdentification that equals your input parameter packageTrackingIdentification
Your 2nd query fetches (several properties of) all BulkmailAnnouncements that have certain properties (CustomerOrderId and CustomerPartyAccountId) equal to the ones you fetched from your first query.
You could group all BulkmailAnnouncements into groups that have the same certain properties. So you know that all BulkmailAnnouncement in a group have the same values for certain properties
Keep the first group that has at least one BulkmailAnnouncement with a true AuditReportIndicator and at least one PackageTrackingIdentification that equals your input parameter packageTrackingIdentification.
Note that the latter part is equal to what you would have selected in your first query. The result is one group. You know that all BulkMailAnnouncements in this group have the same value for certain properties, and there are no BulkMailAnnouncements with the same values for certain properties that are in other groups. Besides you know that the group also contains at least one BulkMailAnnouncement with true AuditReportIndicator and at least one PackageTrackingIdentification that equals your input parameter packageTrackingIdentification. Hence your requested result equals the items you want.
var result = BulkmailAnnouncements.GroupBy(
// Key: make groups with same "certain properties"
announcement => new
{
CustomerOrderId,
CustomerPartyAccountId,
})
// Result: groups of BulkMailAnnouncements with equal "certain properties"
// keep only those groups that have at least one BulkMailAnnouncement
// that has both a true AuditReportIndicator and at least one
// PackageTrackingIdentification that equals packageTrackingIdentification
.Where(groupOfBulkMailAnnouncements =>
groupOfBulkMailAnnouncements.Any(bulkMailAnnouncement =>
bulkMailAnnouncement.AuditReportIndicator &&
bulkmailAnnouncment.PackageTrackingIdentifications
.Any(packageTrackingId == packageTrakcingIdentification)))
// from the remaining groups, take the first or default
.FirstOrDefault(); // or use async version
Other way is to use Concat() method like below
var announcement = await Context.BulkmailAnnouncement
.Where(x => x.PackageTrackingIdentifications.Any(y => y.Value == packageTrackingIdentification) &&
x.AuditReportIndicator != true)
.FirstAsync();
var announcement2 = await Context.BulkmailAnnouncement
.Include(x => x.Tarras)
.Include(x => x.PackageTrackingIdentifications)
.Where(x => x.CustomerOrderId == announcement.CustomerOrderId && x.CustomerPartyAccountId == announcement.CustomerPartyAccountId)
.ToListAsync();
var resultAnnouncement = announcement.Concat(announcement2);
return resultAnnouncement;
Move the Where predicate to the second query:
return await Context.BulkmailAnnouncement
.Include(x => x.Tarras)
.Include(x => x.PackageTrackingIdentifications)
.Where(x => x.PackageTrackingIdentifications != null
&& x.PackageTrackingIdentifications.Any(y => y.Value == packageTrackingIdentification)
&& x.AuditReportIndicator != true)
.ToListAsync();
Or call Include on the first:
var announcement = await Context.BulkmailAnnouncement
.Include(x => x.Tarras)
.Include(x => x.PackageTrackingIdentifications)
.Where(x => x.PackageTrackingIdentifications != null
&& x.PackageTrackingIdentifications.Any(y => y.Value == packageTrackingIdentification)
&& x.AuditReportIndicator != true)
.FirstAsync();
The result i want is a list of BulkmailAnnouncements which all have the same customerOrderId and CustomerPartyId. In order to find out which customerOrderId and CustomerPartyId i first need to find 1 bulkmailAnnouncement which has a packageTrackingIdentification equal to the parameter of the method. then use that bulkmailAnnouncement to find all other announcements with the same customerOrderId and CustomerPartyId.
Then group the results by the CustomerOrderId and CustomerPartyAccountId properties and take the first group, e.g.:
return (await Context.BulkmailAnnouncement
.Include(x => x.Tarras)
.Include(x => x.PackageTrackingIdentifications)
.Where(x => x.PackageTrackingIdentifications != null
&& x.PackageTrackingIdentifications.Any(y => y.Value == packageTrackingIdentification)
&& x.AuditReportIndicator != true)
.GroupBy(x => new { x.CustomerOrderId, x.CustomerPartyAccountId })
.FirstAsync()).ToArray();

How to make LINQ query with Unions more efficient

I inherited the LINQ query below and I feel that the query can be refactored for efficiency. The query currently takes about 6-8 seconds of processing time to return one record to the user on the front-end of the application. LINQ is not my strong suite, so any help would be greatly appreciated.
The query should ultimately produce a distinct list of CA_TASK_VW objects that are tied to a list of distinct CA_OBJECT_ID's obtained from the CA_OBJECT, CA_PEOPLE, and CA_CONTRACTOR tables.
var data = (from a in _db.CA_TASK_VW
where a.TASK_TYPE == "INSPECTION" && a.TASK_AVAILABLE_FLAG == "Y" && a.TARGET_END_DATE == null
select a).AsQueryable();
data = data.Join(_db.CA_OBJECT.Where(o => o.ENTERED_BY == _userId),
o => o.CA_OBJECT_ID, p => p.CA_OBJECT_ID,
(t, p) => t)
.Union(data.Join(_db.CA_PEOPLE.Where(p => p.EMAIL == _email),
t => t.CA_OBJECT_ID, p => p.CA_OBJECT_ID,
(t, p) => t))
.Union(data.Join(_db.CA_CONTRACTOR.Where(c => c.CONTRACTOR.EMAIL == _email),
t => t.CA_OBJECT_ID, c => c.CA_OBJECT_ID,
(t, c) => t));
The code seems to be using Join/Union to execute basically a where predicate on the list of CA_TASK_VW, filtering it step by step to the final result, so what happens if you just specify the where condition directly?
var data = from a in _db.CA_TASK_VW
where a.TASK_TYPE == "INSPECTION" && a.TASK_AVAILABLE_FLAG == "Y" && a.TARGET_END_DATE == null
select a;
data = data.Where(t => _db.CA_OBJECT.Where(o => o.ENTERED_BY == _userId).Select(o => o.CA_OBJECT_ID).Contains(t.CA_OBJECT_ID) ||
_db.CA_PEOPLE.Where(p => p.EMAIL == _email).Select(p => p.CA_OBJECT_ID).Contains(t.CA_OBJECT_ID) ||
_db.CA_CONTRACTOR.Where(c => c.CONTRACTOR.EMAIL == _email).Select(c => c.CA_OBJECT_ID).Contains(t.CA_OBJECT_ID));
You could try using UNION ALL if you don`t really care about duplicates in your query results as it works much faster than UNION

Flattening Complex LINQ to SQL

I have a somewhat complex LINQ to SQL query that I'm trying to optimise (no, not prematurely, things are slow), that goes a little bit like this;
IQueryable<SearchListItem> query = DbContext.EquipmentLives
.Where(...)
.Select(e => new SearchListItem {
EquipmentStatusId = e.EquipmentStatuses.FirstOrDefault(s => s.DateTo == null).Id,
StatusStartDate = e.EquipmentStatuses.FirstOrDefault(s => s.DateTo == null).DateFrom,
...
});
The where clauses aren't important, they don't filter EquipmentStatuses, happy to include if someone thinks they're required.
This is on quite a large set of tables and returns a fairly details object, there's more references to EquipmentStatuses, but I'm sure you get the idea. The problem is that there's quite obviously two sub-queries and I'm sure that (among some other things) is not ideal, especially since they are exactly the same sub-query each time.
Is it possible to flatten this out a bit? Perhaps it's easier to do a few smaller queries to the database and create the SearchListItem in a foreach loop?
Here's my take given your comments, and with some assumptions I've made
It may look scary, but give it a try, with and without the ToList() before the GroupBy()
If you have LinqPad, check the SQL produced, and the number of queries, or just plug in the SQL Server Profiler
With LinqPad you could even put a Stopwatch to measure things precisely
Enjoy ;)
var query = DbContext.EquipmentLives
.AsNoTracking() // Notice this!!!
.Where(...)
// WARNING: SelectMany is an INNER JOIN
// You won't get EquipmentLive records that don't have EquipmentStatuses
// But your original code would break if such a case existed
.SelectMany(e => e.EquipmentStatuses, (live, status) => new
{
EquipmentLiveId = live.Id, // We'll need this one for grouping
EquipmentStatusId = status.Id,
EquipmentStatusDateTo = status.DateTo,
StatusStartDate = status.DateFrom
//...
})
// WARNING: Again, you won't get EquipmentLive records for which none of their EquipmentStatuses have a DateTo == null
// But your original code would break if such a case existed
.Where(x => x.EquipmentStatusDateTo == null)
// Now You can do a ToList() before the following GroupBy(). It depends on a lot of factors...
// If you only expect one or two EquipmentStatus.DateTo == null per EquipmentLive, doing ToList() before GroupBy may give you a performance boost
// Why? GroupBy sometimes confuses the EF SQL generator and the SQL Optimizer
.GroupBy(x => x.EquipmentLiveId, x => new SearchListItem
{
EquipmentLiveId = x.EquipmentLiveId, // You may or may not need this?
EquipmentStatusId = x.EquipmentStatusId,
StatusStartDate = x.StatusStartDate,
//...
})
// Now you have one group of SearchListItem per EquipmentLive
// Each group has a list of EquipmenStatuses with DateTo == null
// Just select the first one (you could do g.OrderBy... as well)
.Select(g => g.FirstOrDefault())
// Materialize
.ToList();
You don't need to repeat the FirstOrDefault. You can add an intermediate Select to select it once and then reuse it:
IQueryable<SearchListItem> query = DbContext.EquipmentLives
.Where(...)
.Select(e => e.EquipmentStatuses.FirstOrDefault(s => s.DateTo == null))
.Select(s => new SearchListItem {
EquipmentStatusId = s.Id,
StatusStartDate = s.DateFrom,
...
});
In query syntax (which I find more readable) it would look like this:
var query =
from e in DbContext.EquipmentLives
where ...
let s = e.EquipmentStatuses.FirstOrDefault(s => s.DateTo == null)
select new SearchListItem {
EquipmentStatusId = s.Id,
StatusStartDate = s.DateFrom,
...
});
There is another problem in your query though. If there is no matching EquipmentStatus in your EquipmentLive, FirstOrDefault will return null, which will cause an exception in the last select. So you might need an additional Where:
IQueryable<SearchListItem> query = DbContext.EquipmentLives
.Where(...)
.Select(e => e.EquipmentStatuses.FirstOrDefault(s => s.DateTo == null))
.Where(s => s != null)
.Select(s => new SearchListItem {
EquipmentStatusId = s.Id,
StatusStartDate = s.DateFrom,
...
});
or
var query =
from e in DbContext.EquipmentLives
where ...
let s = e.EquipmentStatuses.FirstOrDefault(s => s.DateTo == null)
where s != null
select new SearchListItem {
EquipmentStatusId = s.Id,
StatusStartDate = s.DateFrom,
...
});
Given that you don't test for null after calling FirstOrDefault(s => s.DateTo == null) I assume that:
either for each device there is always a status with DateTo == null or
you need to see only devices which have such status
In order to do so you need to join EquipmentLives with EquipmentStatuses to avoid subqueries:
var query = DbContext.EquipmentLives
.Where(l => true)
.Join(DbContext.EquipmentStatuses.Where(s => s.DateTo == null),
eq => eq.Id,
status => status.EquipmentId,
(eq, status) => new SelectListItem
{
EquipmentStatusId = status.Id,
StatusStartDate = status.DateFrom
});
However, if you do want to perform a left join replace DbContext.EquipmentStatuses.Where(s => s.DateTo == null) with DbContext.EquipmentStatuses.Where(s => s.DateTo == null).DefaultIfEmpty().

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.

.Where inside a .Select , how to select only record that match the where clause:-

I have the following two statements :-
var isadminByuser = tms.SecurityRoles.Where(a => a.Name.ToLower() == "administrator")
.Select(a=>a.SecurityRoleUsers.Where(a2 => a2.UserName.ToLower() == user.ToLower()));
if (isadminByuser.Count() >= 1) { return true;}
&
var adminByGroup = tms.SecurityRoles.Where(a => a.Name == "Administrator")
.SingleOrDefault().Groups
.Select(a2 => a2.TMSUserGroups
.Where(a3 => a3.UserName.ToLower() == user.ToLower()));
bool isadminByGroup = adminByGroup.Count() >= 1;
The first var isadminByuser will always have elements (will always be >=1), even if the where clause a3.UserName.ToLower() == user.ToLower())) is false, while the other var will have a count of zero is the where clause (a3.UserName.ToLower() == user.ToLower())) is false. So why will the first var never have a count of zero?
Thanks
The answer to the question asked is that you are selecting an IQueryable<SecurityRoleUsers> when you select
a.SecurityRoleUsers.Where(a2 => a2.UserName.ToLower() == user.ToLower())
which may or may not have a count of 0, but the containing query will return one of these IQueryables for each SecurityRole that matches Name = "administrator", hence the count will always be 1+ if there is at least one matching SecurityRole
Update:
// first get SecurityRoles
bool isadminByuser = tms.SecurityRoles.Where(a => a.Name.ToLower() == "administrator")
// now you're only interested in the related SecurityRoleUsers
.SelectMany( a => a.SecurityRoleUsers )
// now check if any of the SecurityRoleUsers meet your criteria
.Any( sru => sru.UserName.ToLower() == user.ToLower() );
Could you observe sql query differences from ms-sql-profiler
tms.SecurityRoles.Where(a => a.Name.ToLower() == "administrator")
.Select(a=>a.SecurityRoleUsers.Where(a2 => a2.UserName.ToLower() == user.ToLower()));
***
tms.SecurityRoles.Select(a=>a.SecurityRoleUsers.Where(a2 => a2.UserName.ToLower() == user.ToLower())).Where(a => a.Name.ToLower() == "administrator");

Categories

Resources