LINQ, select with only the first element of child item - c#

With LINQ, how to do a select and only return the first associated child item?
I have:
[table: Report]
GeneratedDate
PerformanceMetric 1 -----> N [table PerformanceMetric]
Bios Timestamp
... X
Y
..... (there is a good dozen other fields)
Currently, I do:
Report = _ctx.Reports.SingleorDefault(rpt => rpt.bios == mySerial);
This returns the report with all the PerformanceMetric associated. I would like to only have the first occurrence of PerformanceMetric returned.
Does LINQ provide a built-in way of doing so?
EDIT: In fact, I am looking for the Report, but the PerformanceMetric should only contain the first element. (Report.PerformanceMetrics.Count <= 1 )
sry for not being clear..

Hard to tell exactly the full structure based on your XML, but something like:
var firstMetric = _ctx.Reports
.Where(rpt => rpt.bios == mySerial)
.Select(rpt => rpt.PerformanceMetrics) // assuming name here...
.FirstOrDefault();
This returns the first performance metric of the report matching mySerial, or default (null for classes) if the where clause didn't find any reports or no performance metrics existed.
Update: based on the update in the question, you want something different. You want the report, but with only 1 of the performance metrics. This involves mutating the original report (not sure of all the members), which can be kinda hairy because you don't want to manipulate your original data in LINQ.
So you have two options:
You can create a new instance of Report and copy all the properties and just one metric.
You can create an anonymous type with the report and the property together.
I'd probably prefer #2 since returning a modified Report might be confusing. So you could do:
var firstMetric = _ctx.Reports
.Where(rpt => rpt.bios == mySerial)
.Select(new { Report = rpt, Metric = rpt.PerformanceMetrics.FirstOrDefault() })
.FirstOrDefault();
This will scan the Reports for the one where bios == mySerial, create a new anonymous type with a Report member == to the original report, and a Metric member == the first metric in the Report list.
If no metrics exist but report exists, you'll get an anonymous type with Report = your report and Metric = null. If no report exists matching condition, returns null.
If you do really want #1, you could do this:
var firstMetric = _ctx.Reports
.Where(rpt => rpt.bios == mySerial)
.Select(new Report
{
PerformanceMetrics = rpt.PerformanceMetrics.Take(1),
// copy all other Report fields here...
})
.FirstOrDefault();

Are you looking for something like this?
var reportMetric = (from r in _ctx.Reports select r.PerformanceMetric where r.Bios == mySerial).FirstOrDefault();

Related

Value cannot be null, r nparameter name source

I am confused. I have two statements that are the same and one works and the other receives the error - value cannot be null. r nparameter name source. From what I read I am receiving the error because something is null in my linq expression. However from what I can tell nothing is null.
The first if statement works. When a person selects 'Select' from a list of Burn Project a list of BurnPiles is displayed below the BurnProject list. (this works). Then when a person selects 'Select' from the list of BurnPiles a list of RequestedBurns is display below it. This gives me the null error. At one time it did work now it doesn't.
I am at a loss of what went wrong. I do know the RequestedBurn Table starts at record #2 but that shouldn't have anything to do with it. The BurnPile records that I have been using have associated RequestedBurns.
//when 'Select' is chosen from the list of burn projects the list of burn piles
//associated with that specific burn project is display below it.
if (burnerID != null)
{
ViewBag.BurnerID = burnerID.Value;
viewModel.BurnPiles = viewModel.BurnProjects.Where(
b => b.BurnerID == burnerID.Value).Single().BurnPiles;
}
//when 'Select' is chosen from the list of burn piles the list of requested
//burns associated with that specific burn pile is displayed below it.
if (burnPileID != null)
{
ViewBag.BurnPilesID = burnPileID.Value;
viewModel.RequestedBurns = viewModel.BurnPiles.Where(
x => x.BurnPilesID == burnPileID).Single().RequestedBurns;
}
If you look at documentation for Where or Single, you would see that source is the name of the parameter that represents your collection. So, it looks like you are trying to call a method on null reference, which would be the case if viewModel.BurnProjects = null or viewModel.BurnPiles = null.
viewModel.BurnPiles = viewModel.BurnProjects.Where(
b => b.BurnerID == burnerID.Value).Single().BurnPiles;
could be setting viewModel.BurnPiles to null.
or
viewModel.BurnPiles.Where(
x => x.BurnPilesID == burnPileID).Single()
is returning nothing so when you try and access RequestedBurns then it throws an exception.
SingleOrDefault also has an overload where you can simplify the expression a bit more. You can also combine it with the null conditional operator (if using at least C# 6).
if (burnerID != null)
{
ViewBag.BurnerID = burnerID.Value;
viewModel.BurnPiles = viewModel.BurnProjects.SingleOrDefault(b => b.BurnerID == burnerID.Value)?.BurnPiles;
}

Using variables to build a LinQ query?

I don't think is possible but wanted to ask to make sure. I am currently debugging some software someone else wrote and its a bit unfinished.
One part of the software is a search function which searches by different fields in the database and the person who wrote the software wrote a great big case statement with 21 cases in it 1 for each field the user may want to search by.
Is it possible to reduce this down using a case statement within the Linq or a variable I can set with a case statement before the Linq statement?
Example of 1 of the Linq queries: (Only the Where is changing in each query)
var list = (from data in dc.MemberDetails
where data.JoinDate.ToString() == searchField
select new
{
data.MemberID,
data.FirstName,
data.Surname,
data.Street,
data.City,
data.County,
data.Postcode,
data.MembershipCategory,
data.Paid,
data.ToPay
}
).ToList();
Update / Edit:
This is what comes before the case statement:
string searchField = txt1stSearchTerm.Text;
string searchColumn = cmbFirstColumn.Text;
switch (cmbFirstColumn.SelectedIndex + 1)
{
The cases are then done by the index of the combo box which holds the list of field names.
Given that where takes a predicate, you can pass any method or function which takes MemberDetail as a parameter and returns a boolean, then migrate the switch statement inside.
private bool IsMatch(MemberDetail detail)
{
// The comparison goes here.
}
var list = (from data in dc.MemberDetails
where data => this.IsMatch(data)
select new
{
data.MemberID,
data.FirstName,
data.Surname,
data.Street,
data.City,
data.County,
data.Postcode,
data.MembershipCategory,
data.Paid,
data.ToPay
}
).ToList();
Note that:
You may look for a more object-oriented way to do the comparison, rather than using a huge switch block.
An anonymous type with ten properties that you use in your select is kinda weird. Can't you return an instance of MemberDetail? Or an instance of its base class?
How are the different where statements handled, are they mutually excluside or do they all limit the query somehow?
Here is how you can have one or more filters for a same query and materialized after all filters have been applied.
var query = (from data in dc.MemberDetails
select ....);
if (!String.IsNullOrEmpty(searchField))
query = query.Where(pr => pr.JoinDate.ToString() == searchField);
if (!String.IsNullOrEmpty(otherField))
query = query.Where(....);
return query.ToList();

How do I make a streaming LINQ expression that delivers the filtered out items as well as the filtered items?

I am transforming an Excel spreadsheet into a list of "Elements" (this is a domain term). During this transformation, I need to skip the header rows and throw out malformed rows that cannot be transformed.
Now comes the fun part. I need to capture those malformed records so that I can report on them. I constructed a crazy LINQ statement (below). These are extension methods hiding the messy LINQ operations on the types from the OpenXml library.
var elements = sheet
.Rows() <-- BEGIN sheet data transform
.SkipColumnHeaders()
.ToRowLookup()
.ToCellLookup()
.SkipEmptyRows() <-- END sheet data transform
.ToElements(strings) <-- BEGIN domain transform
.RemoveBadRecords(out discard)
.OrderByCompositeKey();
The interesting part starts at ToElements, where I transform the row lookup to my domain object list (details: it's called an ElementRow, which is later transformed into an Element). Bad records are created with just a key (the Excel row index) and are uniquely identifiable vs. a real element.
public static IEnumerable<ElementRow> ToElements(this IEnumerable<KeyValuePair<UInt32Value, Cell[]>> map)
{
return map.Select(pair =>
{
try
{
return ElementRow.FromCells(pair.Key, pair.Value);
}
catch (Exception)
{
return ElementRow.BadRecord(pair.Key);
}
});
}
Then, I want to remove those bad records (it's easier to collect all of them before filtering). That method is RemoveBadRecords, which started like this...
public static IEnumerable<ElementRow> RemoveBadRecords(this IEnumerable<ElementRow> elements)
{
return elements.Where(el => el.FormatId != 0);
}
However, I need to report the discarded elements! And I don't want to muddy my transform extension method with reporting. So, I went to the out parameter (taking into account the difficulties of using an out param in an anonymous block)
public static IEnumerable<ElementRow> RemoveBadRecords(this IEnumerable<ElementRow> elements, out List<ElementRow> discard)
{
var temp = new List<ElementRow>();
var filtered = elements.Where(el =>
{
if (el.FormatId == 0) temp.Add(el);
return el.FormatId != 0;
});
discard = temp;
return filtered;
}
And, lo! I thought I was hardcore and would have this working in one shot...
var discard = new List<ElementRow>();
var elements = data
/* snipped long LINQ statement */
.RemoveBadRecords(out discard)
/* snipped long LINQ statement */
discard.ForEach(el => failures.Add(el));
foreach(var el in elements)
{
/* do more work, maybe add more failures */
}
return new Result(elements, failures);
But, nothing was in my discard list at the time I looped through it! I stepped through the code and realized that I successfully created a fully-streaming LINQ statement.
The temp list was created
The Where filter was assigned (but not yet run)
And the discard list was assigned
Then the streaming thing was returned
When discard was iterated, it contained no elements, because the elements weren't iterated over yet.
Is there a way to fix this problem using the thing I constructed? Do I have to force an iteration of the data before or during the bad record filter? Is there another construction that I've missed?
Some Commentary
Jon mentioned that the assignment /was/ happening. I simply wasn't waiting for it. If I check the contents of discard after the iteration of elements, it is, in fact, full! So, I don't actually have an assignment problem. Unless I take Jon's advice on what's good/bad to have in a LINQ statement.
When the statement was actually iterated, the Where clause ran and temp filled up, but discard was never assigned again!
It doesn't need to be assigned again - the existing list which will have been assigned to discard in the calling code will be populated.
However, I'd strongly recommend against this approach. Using an out parameter here is really against the spirit of LINQ. (If you iterate over your results twice, you'll end up with a list which contains all the bad elements twice. Ick!)
I'd suggest materializing the query before removing the bad records - and then you can run separate queries:
var allElements = sheet
.Rows()
.SkipColumnHeaders()
.ToRowLookup()
.ToCellLookup()
.SkipEmptyRows()
.ToElements(strings)
.ToList();
var goodElements = allElements.Where(el => el.FormatId != 0)
.OrderByCompositeKey();
var badElements = allElements.Where(el => el.FormatId == 0);
By materializing the query in a List<>, you only process each row once in terms of ToRowLookup, ToCellLookup etc. It does mean you need to have enough memory to keep all the elements at a time, of course. There are alternative approaches (such as taking an action on each bad element while filtering it) but they're still likely to end up being fairly fragile.
EDIT: Another option as mentioned by Servy is to use ToLookup, which will materialize and group in one go:
var lookup = sheet
.Rows()
.SkipColumnHeaders()
.ToRowLookup()
.ToCellLookup()
.SkipEmptyRows()
.ToElements(strings)
.OrderByCompositeKey()
.ToLookup(el => el.FormatId == 0);
Then you can use:
foreach (var goodElement in lookup[false])
{
...
}
and
foreach (var badElement in lookup[true])
{
...
}
Note that this performs the ordering on all elements, good and bad. An alternative is to remove the ordering from the original query and use:
foreach (var goodElement in lookup[false].OrderByCompositeKey())
{
...
}
I'm not personally wild about grouping by true/false - it feels like a bit of an abuse of what's normally meant to be a key-based lookup - but it would certainly work.

IEnumerable<T>.Union(IEnumerable<T>) overwrites contents instead of unioning

I've got a collection of items (ADO.NET Entity Framework), and need to return a subset as search results based on a couple different criteria. Unfortunately, the criteria overlap in such a way that I can't just take the collection Where the criteria are met (or drop Where the criteria are not met), since this would leave out or duplicate valid items that should be returned.
I decided I would do each check individually, and combine the results. I considered using AddRange, but that would result in duplicates in the results list (and my understanding is it would enumerate the collection every time - am I correct/mistaken here?). I realized Union does not insert duplicates, and defers enumeration until necessary (again, is this understanding correct?).
The search is written as follows:
IEnumerable<MyClass> Results = Enumerable.Empty<MyClass>();
IEnumerable<MyClass> Potential = db.MyClasses.Where(x => x.Y); //Precondition
int parsed_key;
//For each searchable value
foreach(var selected in SelectedValues1)
{
IEnumerable<MyClass> matched = Potential.Where(x => x.Value1 == selected);
Results = Results.Union(matched); //This is where the problem is
}
//Ellipsed....
foreach(var selected in SelectedValuesN) //Happens to be integer
{
if(!int.TryParse(selected, out parsed_id))
continue;
IEnumerable<MyClass> matched = Potential.Where(x => x.ValueN == parsed_id);
Results = Results.Union(matched); //This is where the problem is
}
It seems, however, that Results = Results.Union(matched) is working more like Results = matched. I've stepped through with some test data and a test search. The search asks for results where the first field is -1, 0, 1, or 3. This should return 4 results (two 0s, a 1 and a 3). The first iteration of the loops works as expected, with Results still being empty. The second iteration also works as expected, with Results containing two items. After the third iteration, however, Results contains only one item.
Have I just misunderstood how .Union works, or is there something else going on here?
Because of deferred execution, by the time you eventually consume Results, it is the union of many Where queries all of which are based on the last value of selected.
So you have
Results = Potential.Where(selected)
.Union(Potential.Where(selected))
.Union(potential.Where(selected))...
and all the selected values are the same.
You need to create a var currentSelected = selected inside your loop and pass that to the query. That way each value of selected will be captured individually and you won't have this problem.
You can do this much more simply:
Reuslts = SelectedValues.SelectMany(s => Potential.Where(x => x.Value == s));
(this may return duplicates)
Or
Results = Potential.Where(x => SelectedValues.Contains(x.Value));
As pointed out by others, your LINQ expression is a closure. This means your variable selected is captured by the LINQ expression in each iteration of your foreach-loop. The same variable is used in each iteration of the foreach, so it will end up having whatever the last value was. To get around this, you will need to declare a local variable within the foreach-loop, like so:
//For each searchable value
foreach(var selected in SelectedValues1)
{
var localSelected = selected;
Results = Results.Union(Potential.Where(x => x.Value1 == localSelected));
}
It is much shorter to just use .Contains():
Results = Results.Union(Potential.Where(x => SelectedValues1.Contains(x.Value1)));
Since you need to query multiple SelectedValues collections, you could put them all inside their own collection and iterate over that as well, although you'd need some way of matching the correct field/property on your objects.
You could possibly do this by storing your lists of selected values in a Dictionary with the name of the field/property as the key. You would use Reflection to look up the correct field and perform your check. You could then shorten the code to the following:
// Store each of your searchable lists here
Dictionary<string, IEnumerable<MyClass>> DictionaryOfSelectedValues = ...;
Type t = typeof(MyType);
// For each list of searchable values
foreach(var selectedValues in DictionaryOfSelectedValues) // Returns KeyValuePair<TKey, TValue>
{
// Try to get a property for this key
PropertyInfo prop = t.GetProperty(selectedValues.Key);
IEnumerable<MyClass> localSelected = selectedValues.Value;
if( prop != null )
{
Results = Results.Union(Potential.Where(x =>
localSelected.Contains(prop.GetValue(x, null))));
}
else // If it's not a property, check if the entry is for a field
{
FieldInfo field = t.GetField(selectedValues.Key);
if( field != null )
{
Results = Results.Union(Potential.Where(x =>
localSelected.Contains(field.GetValue(x, null))));
}
}
}
No, your use of union is absoloutely correct.
The only thing to keep in mind is it excludes duplicates as based on the equality operator. Do you have sample data?
Okay, I think you are are haveing a problem because Union uses deferred execution.
What happens if you do,
var unionResults = Results.Union(matched).ToList();
Results = unionResults;

C# lambda extract a single row string value

Want to extract the text value from a lookup table's column in a db. EL is the entity for my db. Current code :
var QTypes = EL.ElogQueryType.Where<ElogQueryType>( eqt=> eqt.ID == queryTypeID);
string qType = QTypes.First().QueryType;
I get a list when I just pull .Select(... so something is wrong.
You should be able to just do if you know you will be just getting one item:
var QTypes = EL.ElogQueryType.Where(eqt=> eqt.ID == queryTypeID).Single().QueryType;
If you are not sure if you will get one or nothing use SingleOrDefault().
If you just want the first since you are expecting many records do:
var QTypes = EL.ElogQueryType.First(eqt=> eqt.ID == queryTypeID).QueryType;
Same applies if you don't know if you will get anything, use FirstOrDefault.
It's not clear what's wrong, as your current query should give you what you're after. However, you can also use the overload of First which takes a predicate:
string qType = EL.ElogQueryType.First(eqt => eqt.ID == queryTypeID)
.QueryType;
You say you "get a list when [you] pull .Select(" but it's not really clear what you mean. You haven't said what's wrong with the code you've already specified.
(As Kelsey says, there are alternatives to First: FirstOrDefault, SingleOrDefault, Single and even Last should you wish.)

Categories