I was just reading a recent question on using conditionals in Linq and it reminded me of an issue I have not been able to resolve. When building Linq to SQL queries programmatically how can this be done when the number of conditionals is not known until runtime?
For instance in the code below the first clause creates an IQueryable that, if executed, would select all the tasks (called issues) in the database, the 2nd clause will refine that to just issues assigned to one department if one has been selected in a combobox (Which has it's selected item bound to the departmentToShow property).
How could I do this using the selectedItems collection instead?
IQueryable<Issue> issuesQuery;
// Will select all tasks
issuesQuery = from i in db.Issues
orderby i.IssDueDate, i.IssUrgency
select i;
// Filters out all other Departments if one is selected
if (departmentToShow != "All")
{
issuesQuery = from i in issuesQuery
where i.IssDepartment == departmentToShow
select i;
}
By the way, the above code is simplified, in the actual code there are about a dozen clauses that refine the query based on the users search and filter settings.
If the number of conditions is unknown then it's easier to use lambda syntax instead of query comprehension, i.e.:
IQueryable<Issue> issues = db.Issues;
if (departmentToShow != "All")
{
issues = issues.Where(i => i.IssDepartment == departmentToShow);
}
issues = issues.OrderBy(i => i.IssDueDate).ThenBy(i => i.IssUrgency);
(Assuming you want the ordering to happen after the filtering, which ought to be the case - I'm not sure if Linq will generate an optimized query if you try to do the ordering first).
If you've got a very large number of optional conditions then you can clean it up with predicates:
List<Predicate<Issue>> conditions = new List<Predicate<Issue>>();
if (departmentToShow != "All")
conditions.Add(i => i.IssDepartment == departmentToShow);
if (someOtherThing)
conditions.Add(anotherPredicate);
// etc. snip adding conditions
var issues = from i in issues
where conditions.All(c => c(i))
orderby i.IssDueDate, i.IssUrgency;
Or just use PredicateBuilder which is probably easier.
Related
I'm trying to add a Where clause to a table joined with a JoinAlias, but there doesn't appear to be a way to specify the JoinAlias on the where clause.
I'm trying to join to the same table multiple times, then add a variable number of where clauses to the join, based on user input:
var userFilterList = new List<Expression<Func<LocationDb, LocationAttributesDateTimeDb, bool>>>();
Expression <Func<LocationDb, LocationAttributesDateTimeDb, bool>> joinPredicate = (loc, ext) =>
loc.LocationId == ext.LocationId && ext.AttributeId == attributeId;
query = query.Join<LocationAttributesDateTimeDb>(joinPredicate, ctx.JoinAlias($"ext{attributeId}"));
foreach (var item in userFilterList)
{
query = query.Where<LocationDb, LocationAttributesDateTimeDb>(item);
}
The main problem is, there doesn't appear to be a way to add the JoinAlias onto the Where clause. If I try to run the query as is, I get an exception regarding the missing alias.
If I try the following code, I get a compile exception:
query = query.Where<LocationDb, LocationAttributesDateTimeDb>(item, ctx.JoinAlias($"ext{attributeId}"));
Is there a way to add the JoinAlias to a where clause without resorting to writing the Where clauses as manual SQL?
Or, is there an alternative method I can use to stitch my multiple requests together into the single Join predicate?
Note that in the latest v5.4.1 pre-release on MyGet JoinAlias() has been deprecated and replaced with TableAlias() which uses a different implementation that substitutes the alias whilst walking the expression tree whilst generating SQL Statements whereas JoinAlias() worked by post string substitution on the generated SQL which was more fragile.
There's no TableAlias() in WHERE statements as it wouldn't be possible to determine where the alias should be used, instead here are some examples of how to use TableAlias in WHERE conditions:
q.Where<Team, Teamuser>((t, u) => t.Id == Sql.TableAlias(u.TeamId, "Leader"));
q.Where<Teamuser>(u => Sql.TableAlias(u.Id, "Leader") == 1);
q.Where<Team, Teamuser>((t, u) => Sql.TableAlias(t.Id, q.DialectProvider.GetQuotedTableName(ModelDefinition<Team>.Definition)) == Sql.TableAlias(u.TeamId, "Leader")); // Workaround, but only works for fields, not constants
q.Where<Team, Teamuser>((user, leader) => Sql.TableAlias(user.Id, "Teamuser") < Sql.TableAlias(leader.Id, "Leader"));
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
I want to query an array of objects "sortedData", where each object has two values (ItemId, Sort), for a specific ItemId and set the 'Sort' value. Like this below but this isn't the correct linq syntax.
var sortedData = db.Fetch<object>("SELECT ItemId, Sort FROM CollectionItems WHERE CollectionId = #0", collectionId);
dataWithSort = db.Fetch<OrganizationForExportWithSort>(TpShared.DAL.StoredProcedures.GetOrganizationsForTargetListUI(clientId, organizationIdList));
foreach(OrganizationForExportWithSort export in dataWithSort)
{
export.Sort = sortedData.Select("Sort").Where(sortedData.ItemId == export.Id);
}
As I understand it, you want the Sort property from the item that matches that particular ID. This being the case, you have a few problems with what you've written:
"Where" and "Select" both take Lambda expressions, not property names and expressions, so the code snippet you provide shouldn't compile.
"Where" and "Select" both return collections (even if there's only one item that actually matches the "Where" filter; in fact, even if no items in the collection match the condition in the "Where" clause it'll still return a collection, albeit an empty one). Think of LINQ Select more in terms of running a transform on a collection and LINQ "Where" as applying a filter to one.
As a general rule for LINQ queries, if possible, you should actually run "where" before "select" (filter first, then apply some kind of transform to the remaining items).
In this case, I think you actually just want one item, so you can actually use "FirstOrDefault" instead of "Where." This will leave you with a single .NET object. This is analogous to the TOP 1 restriction in SQL. Once you have the .NET object you can retrieve the property from the object itself.
Try this:
foreach(OrganizationForExportWithSort export in dataWithSort)
{
export.Sort = sortedData.FirstOrDefault(data => data.ItemId == export.Id)?.Sort;
}
The "?" is a new C# feature that will try to call .Sort on the object if (and only if) the query succeeded in finding an item with that ID. If it doesn't it'll just return null.
Have you tried Linq sorting?
var sortedData = db.Fetch<object>("SELECT ItemId, Sort FROM CollectionItems WHERE CollectionId = #0", collectionId);
dataWithSort = db.Fetch<OrganizationForExportWithSort>(TpShared.DAL.StoredProcedures.GetOrganizationsForTargetListUI(clientId, organizationIdList));
// create a list ordered by fields
var sorted = dataWithSort.OrderBy(o => o.SomeField).ThenBy(o => o.OtherField);
The o in the lambda stands for object...
I will add my voice to the chorus of folks saying to read up on some good linq tutorials. Start Here
I need to identify items from one list that are not present in another list. The two lists are of different entities (ToDo and WorkshopItem). I consider a workshop item to be in the todo list if the Name is matched in any of the todo list items.
The following does what I'm after but find it awkward and hard to understand each time I revisit it. I use NHibernate QueryOver syntax to get the two lists and then a LINQ statement to filter down to just the Workshop items that meet the requirement (DateDue is in the next two weeks and the Name is not present in the list of ToDo items.
var allTodos = Session.QueryOver<ToDo>().List();
var twoWeeksTime = DateTime.Now.AddDays(14);
var workshopItemsDueSoon = Session.QueryOver<WorkshopItem>()
.Where(w => w.DateDue <= twoWeeksTime).List();
var matches = from wsi in workshopItemsDueSoon
where !(from todo in allTodos
select todo.TaskName)
.Contains(wsi.Name)
select wsi;
Ideally I'd like to have just one NHibernate query that returns a list of WorkshopItems that match my requirement.
I think I've managed to put together a Linq version of the answer put forward by #CSL and will mark that as the accepted answer as it put me in the direction of the following.
var twoWeeksTime = DateTime.Now.AddDays(14);
var subquery = NHibernate.Criterion.QueryOver.Of<ToDo>().Select(t => t.TaskName);
var matchingItems = Session.QueryOver<WorkshopItem>()
.Where(w => w.DateDue <= twoWeeksTime &&
w.IsWorkshopItemInProgress == true)
.WithSubquery.WhereProperty(x => x.Name).NotIn(subquery)
.Future<WorkshopItem>();
It returns the results I'm expecting and doesn't rely on magic strings. I'm hesitant because I don't fully understand the WithSubquery (and whether inlining it would be a good thing). It seems to equate to
WHERE WorkshopItem.Name IS NOT IN (subquery)
Also I don't understand the Future instead of List. If anyone would shed some light on those that would help.
I am not 100% sure how to achieve what you need using LINQ so to give you an option I am just putting up an alternative solution using nHibernate Criteria (this will execute in one database hit):
// Create a query
ICriteria query = Session.CreateCriteria<WorkShopItem>("wsi");
// Restrict to items due within the next 14 days
query.Add(Restrictions.Le("DateDue", DateTime.Now.AddDays(14));
// Return all TaskNames from Todo's
DetachedCriteria allTodos = DetachedCriteria.For(typeof(Todo)).SetProjection(Projections.Property("TaskName"));
// Filter Work Shop Items for any that do not have a To-do item
query.Add(SubQueries.PropertyNotIn("Name", allTodos);
// Return results
var matchingItems = query.Future<WorkShopItem>().ToList()
I'd recommend
var workshopItemsDueSoon = Session.QueryOver<WorkshopItem>()
.Where(w => w.DateDue <= twoWeeksTime)
var allTodos = Session.QueryOver<ToDo>();
Instead of
var allTodos = Session.QueryOver<ToDo>().List();
var workshopItemsDueSoon = Session.QueryOver<WorkshopItem>()
.Where(w => w.DateDue <= twoWeeksTime).List();
So that the collection isn't iterated until you need it to be.
I've found that it's helpfull to use linq extension methods to make subqueries more readable and less awkward.
For example:
var matches = from wsi in workshopItemsDueSoon
where !allTodos.Select(it=>it.TaskName).Contains(wsi.Name)
select wsi
Personally, since the query is fairly simple, I'd prefer to do it like so:
var matches = workshopItemsDueSoon.Where(wsi => !allTodos.Select(it => it.TaskName).Contains(wsi.Name))
The latter seems less verbose to me.
Using a linq query, how can i pass parameters into the groupby function? Depend on user selection, it will be grouped differently
var query = (from to in OrderQuery
group to by to.FromDateUtc into groupedDate
let grouped = groupedDate.GroupBy(o => o.Name)
I would like to pass different groupby values. Any idea?
Cheers
Generally speaking, you need different queries for this. The same goes for building up different WHERE or Where(...) clauses based on user selection:
var query = OrderQuery;
if (userSelection == "GroupByName") {
query = from to in query
group to by to.FromDateUtc into groupedDate
let grouped = groupedDate.GroupBy(o => o.Name);
}
else if (userSelection == "GroupBySomethingElse") {
query = from to in query
group to by to.FromDateUtc into groupedDate
let grouped = groupedDate.GroupBy(o => o.SomethingElse);
}
You'll want to prepare as much common logic as possible before you apply the grouping to query. Because LINQ uses deferred execution you can modify query as much as you need before enumerating the result and you will not incur a performance hit.
If you are trying to do something like this
SELECT * FROM <TableName> GROUP BY <Column1>,<Column2>
Then, take a look at this post
Group By Multiple Columns