LINQ statment looks unreasonable to me: what's my error? - c#

There has to be a better way:
public IList<ApplicationUser> GetProspects()
{
var UsrNames = Roles.GetUsersInRole("prospect");
IList<ApplicationUser> Users = new List<ApplicationUser>();
foreach ( var u in UsrNames)
{
// In theory should return at most one element.
var UserListFromQuery = from usr in (new ApplicationDbContext()).Users where usr.UserName == u select usr;
Users.Add(UserListFromQuery.ElementAtOrDefault(0));
}
return Users;
}
Can you please show me the error of my ways?

This should do what you want:
using (var context = new ApplicationDbContext())
{
var result = Roles.GetUsersInRole("prospect")
.Select(name => context.Users.FirstOrDefault(user => user.UserName == name))
.Where(user => user != null)
.ToList();
}
I've modified your code to utilize a using statement for the context, so as to ensure it is disposed of properly even if there is an exception. Then I have a linq query which does the following:
Get the usernames
For every username, select the first user in Users with a matching username
Remove all nulls from the resulting enumeration. This is necessary because FirstOrDefault returns null if no matching user is found
Turn our final enumeration into a List

I guess you could join it, then group it, then cull the grouping. I'm not sure whether there is an overall advantage of the front-loading that you'd be doing by joining and grouping, so you might want to put a stopwatch to this (and to your original code) and figure that one out.
My suggestion is:
// force a prefetch, rather than potentially slamming the server again and again
var allUsers = (new ApplicationDbContext()).Users.ToList();
// use the prefetched list to join & filter on
var result = from entitled in UsrNames
join user in allUsers
on entitled equals user.UserName
group user by user.UserName into grp
select grp.First();
return result.ToList();
Couple of thoughts:
This is clearly a user-related table. So I'm guessing that you're not going to have 100k records or something along that scale. At most, maybe thousands. So safe to cache in local memory, especially if the data will not change many times throughout the day. If this is true, you might even want to preload the collection earlier on, and store into a singular instance of the data, to be reused later on. But this observation would only hold true if the data changes very infrequently.

Related

How to make a linq-query with multiple Contains()/Any() on possibly empty lists?

I am trying to make a query to a database view based on earlier user-choices. The choices are stored in lists of objects.
What I want to achieve is for a record to be added to the reportViewList if the stated value exists in one of the lists, but if for example the clientList is empty the query should overlook this statement and add all clients in the selected date-range. The user-choices are stored in temporary lists of objects.
The first condition is a time-range, this works fine. I understand why my current solution does not work, but I can not seem to wrap my head around how to fix it. This example works when both a client and a product is chosen. When the lists are empty the reportViewList is obviously also empty.
I have played with the idea of adding all the records in the date-range and then removing the ones that does not fit, but this would be a bad solution and not efficient.
Any help or feedback is much appreciated.
List<ReportView> reportViews = new List<ReportView>();
using(var dbc = new localContext())
{
reportViewList = dbc.ReportViews.AsEnumerable()
.Where(x => x.OrderDateTime >= from && x.OrderDateTime <= to)
.Where(y => clientList.Any(x2 => x2.Id == y.ClientId)
.Where(z => productList.Any(x3 => x3.Id == z.ProductId)).ToList();
}
You should not call AsEnumerable() before you have added eeverything to your query. Calling AsEnumerable() here will cause your complete data to be loaded in memory and then be filtered in your application.
Without AsEnumerable() and before calling calling ToList() (Better call ToListAsync()), you are working with an IQueryable<ReportView>. You can easily compose it and just call ToList() on your final query.
Entity Framework will then examinate your IQueryable<ReportView> and generate an SQL expression out of it.
For your problem, you just need to check if the user has selected any filters and only add them to the query if they are present.
using var dbc = new localContext();
var reportViewQuery = dbc.ReportViews.AsQueryable(); // You could also write IQuryable<ReportView> reportViewQuery = dbc.ReportViews; but I prefer it this way as it is a little more save when you are refactoring.
// Assuming from and to are nullable and are null if the user has not selected them.
if (from.HasValue)
reportViewQuery = reportViewQuery.Where(r => r.OrderDateTime >= from);
if (to.HasValue)
reportViewQuery = reportViewQuery.Where(r => r.OrderDateTime <= to);
if(clientList is not null && clientList.Any())
{
var clientIds = clientList.Select(c => c.Id).ToHashSet();
reportViewQuery = reportViewQuery.Where(r => clientIds.Contains(y.ClientId));
}
if(productList is not null && productList.Any())
{
var productIds = productList.Select(p => p.Id).ToHashSet();
reportViewQuery = reportViewQuery.Where(r => productIds .Contains(r.ProductId));
}
var reportViews = await reportViewQuery.ToListAsync(); // You can also use ToList(), if you absolutely must, but I would not recommend it as it will block your current thread.

ASP.NET MVC C# Select and Where Statements

I'm having trouble understanding .Select and .Where statements. What I want to do is select a specific column with "where" criteria based on another column.
For example, what I have is this:
var engineers = db.engineers;
var managers = db.ManagersToEngineers;
List<ManagerToEngineer> matchedManager = null;
Engineer matchedEngineer = null;
if (this.User.Identity.IsAuthenticated)
{
var userEmail = this.User.Identity.Name;
matchedEngineer = engineers.Where(x => x.email == userEmail).FirstOrDefault();
matchedManager = managers.Select(x => x.ManagerId).Where(x => x.EngineerId == matchedEngineer.PersonId).ToList();
}
if (matchedEngineer != null)
{
ViewBag.EngineerId = new SelectList(new List<Engineer> { matchedEngineer }, "PersonId", "FullName");
ViewBag.ManagerId = new SelectList(matchedManager, "PersonId", "FullName");
}
What I'm trying to do above is select from a table that matches Managers to Engineers and select a list of managers based on the engineer's id. This isn't working and when I go like:
matchedManager = managers.Where(x => x.EngineerId == matchedEngineer.PersonId).ToList();
I don't get any errors but I'm not selecting the right column. In fact the moment I'm not sure what I'm selecting. Plus I get the error:
Non-static method requires a target.
if you want to to select the manager, then you need to use FirstOrDefault() as you used one line above, but if it is expected to have multiple managers returned, then you will need List<Manager>, try like:
Update:
so matchedManager is already List<T>, in the case it should be like:
matchedManager = managers.Where(x => x.EngineerId == matchedEngineer.PersonId).ToList();
when you put Select(x=>x.ManagerId) after the Where() now it will return Collection of int not Collection of that type, and as Where() is self descriptive, it filters the collection as in sql, and Select() projects the collection on the column you specify:
List<int> managerIds = managers.Where(x => x.EngineerId == matchedEngineer.PersonId)
.Select(x=>x.ManagerId).ToList();
The easiest way to remember what the methods do is to remember that this is being translated to SQL.
A .Where() method will filter the rows returned.
A .Select() method will filter the columns returned.
However, there are a few ways to do that with the way you should have your objects set up.
First, you could get the Engineer, and access its Managers:
var engineer = context.Engineers.Find(engineerId);
return engineer.Managers;
However, that will first pull the Engineer out of the database, and then go back for all of the Managers. The other way would be to go directly through the Managers.
return context.Managers.Where(manager => manager.EngineerId == engineerId).ToList();
Although, by the look of the code in your question, you may have a cross-reference table (many to many relationship) between Managers and Engineers. In that case, my second example probably wouldn't work. In that case, I would use the first example.
You want to filter data by matching person Id and then selecting manager Id, you need to do following:
matchedManager = managers.Where(x => x.EngineerId == matchedEngineer.PersonId).Select(x => x.ManagerId).ToList();
In your case, you are selecting the ManagerId first and so you have list of ints, instead of managers from which you can filter data
Update:
You also need to check matchedEngineer is not null before retrieving the associated manager. This might be cause of your error
You use "Select" lambda expression to get the field you want, you use "where" to filter results

Entity Framework Group By Then Order By

I'm generating a query using Entity Framework which uses a group by clause and then attempts to order each of the groups to get specific data. I attempted to optimize the order by to only happen once using a let statement but the results are incorrect but the query still executes.
Concept:
var results =
(from n in noteEntities.NoteLog
where associatedIDs.Contains(n.AssociatedID)
group n by n.AssociatedID into gn
let ogn = gn.OrderByDescending(t => t.CreatedDateTime)
let successNote = ogn.FirstOrDefault(x => x.Type == "Success")
let lastStatusNote = ogn.FirstOrDefault()
select new { Success = successNote, Status = lastStatusNote, AssociatedID = gn.Key }).ToList();
However, the problem is that using, what should be the ordered let variable, ogn in the subsequent let statements is not using an order by descending list and I'm getting the wrong success and status notes. I've also tried changing things up to create a sub-query and reference the result but that doesn't seem to return an ordered list either, ex:
var subQuery =
(from n in noteEntities.NoteLog
where associatedIDs.Contains(n.AssociatedID)
group n by n.AssociatedID into gn
select gn.OrderByDescending(t => t.CreatedDateTime));
var results =
(from s in subQuery
let successNote = s.FirstOrDefault(x => x.Type == "Success")
let lastStatusNote = s.FirstOrDefault()
select new { Success = successNote, Status = lastStatusNote }).ToList();
I can make this work by using OrderByDescending twice in the select statement or let statements for the success and status notes but this becomes very slow, and redundant, when there are a lot of notes. Is there a way to run the order by only once and get the right results back?
In SQL a subquery with Order By must have a TOP statement (yours does not). And when Linq detects that there is no FirstOrDefault or Takestatements with the ordered subquery it just strips the OrderByDescending.
If you are having a performance problem with the query perhaps you should look into indexing the table.

Linq to SQL Updating Data

I am just starting to use linq to sql for data access. It is working fine for read only. But it does not work for update. I have been reading the threads over several forums. It is clear that anonymous types (in my case var) cannot be updated. I cannot find what I should replace the var with and where I find it. I will appreciate any help.
Below is the code. The exception is
Error 1 Property or indexer 'AnonymousType#1.date_last_logon' cannot be assigned to -- it is read only
fmcsaDataContext db = new fmcsaDataContext();
// DataTable _UserTable;
UserModel _UserModel = new UserModel();
var users = from u in db.FMCSA_USERs
where u.USER_NAME == pName && u.ACTIVE == true
select new
{
date_last_logon = u.DATE_LAST_LOGON,
date_current_logon = u.DATE_CURRENT_LOGON,
failed_login = u.FAILED_LOGIN,
};
if (users.Count() == 0)
return null;
foreach (var user in users)
{
user.date_last_logon = user.date_current_logon;
}
This is the case for any ORM tool; you will have to use the entity types that LINQ-to-SQL generates for you when you make your .dbml file if you want to perform CRUD operations.
Also, be aware that your query is being executed twice and is not concurrently safe; calling Count() executes your query with a Count aggregate in the database, then looping over it executes the query again, this time bringing back results. Given what you're doing, this may be better:
var users = (from u in db.FMCSA_USERs
where u.USER_NAME == pName && u.ACTIVE == true
select u).ToList(); // This executes the query and materializes
// the results into a List<FMCSA_USER>
if (users.Count == 0) return null;
foreach (var user in users)
{
user.date_last_logon = user.date_current_logon;
}
db.SaveChanges();
In order to update data, you cannot use anonymous types.
Instead, you can end your query with select u; to select the actual entities.
Table you are trying to update using LINQ, should have Primary key.

Is looping through the entityreference the correct way?

I want to get all users that have a specific role to a list of usernames.
Is using .Include to include all the users, and going through the UsersReference
the best way to loop through all the users that are associated with the role?
I noticed I could not do a foreach(User user in role.Users) but UsersReference seem to work, but is that how it's supposed to be done? Going through the reference?
using (var context = new MyEntities())
{
List<string> users = new List<string>();
Role role = (from r in context.Roles.Include("Users")
where r.RoleName == roleName
select r).FirstOrDefault();
foreach (User user in role.UsersReference)
users.Add(user.UserName);
return users.ToArray();
}
Is it possible that your Role table has a Users property? I would think that it would name the navigation property Users, not UsersReference. I don't use EF, but all the examples I've seen name the property after the table. AFAIK it always implements IEnumerable so you should be able use it in a foreach.
If you have it set up right I think you only need:
using (var context = new MyEntities())
{
return context.Roles
.Where( r => r.RoleName == roleName )
.SelectMany( r => r.Users )
.Select( u => u.UserName )
.ToArray();
}
Try using your original foreach loop with ToList()
foreach(var user in role.Users.ToList()) {...}
Use the .ToArray() helper instead
using (var context = new MyEntities())
{
return (from role in context.Roles
where role.RoleName == roleName
from user in r.Users
select user.UserName).ToArray();
}
Note that there's no need for .Include("Users") if you do it this way. Using r.Users in the query causes it to be in one query without the need for including it, because it's used in an active ObjectContext.
One a side note, I'm not sure what the method signature of this method is, but in this case IEnumerable<string> is probably a better fit than string[], because you can adjust the implementation later without creating arrays from other types of collections.

Categories