How can I get the next appropriate value from a generic list using LINQ?

I have a class:

public class CounselPoints
{
    public int CounselNum { get; set; }
    public String CounselPoint { get; set; }
    public bool BR { get; set; }
    public bool ICRVBS { get; set; }
}

...and a generic list of that class:

List<CounselPoints> counselPoints = AYttFMConstsAndUtils.DeserializeCounselPointsFile();

I want to retrieve the next appropirate CounselPoint from that generic list with logic that could be expressed in SQL like so:

SELECT TOP 1 CounselNumber
FROM COUNSELPOINTSLU
WHERE BR IS TRUE
AND CounselNumber > @LastCounselPoint;

I call this method to try to get that value, and am flailing away at it like so:

public static int GetNextBibleReadingCounselPoint(int LastCounselPoint)
{
    List<CounselPoints> counselPoints = AYttFMConstsAndUtils.DeserializeCounselPointsFile();
    return counselPoints.FirstOrDefault(i => i.CounselNum).Where(j => j.BR).Where(k => k.CounselNum > LastCounselPoint);
}

In "English," I'm trying to say, "Get me the first CounselNum that is a greater number than the passed-in arg (LastCounselPoint) and where BR is true"

For example, if I pass in a 17, and the counselPoint "record" with a CounselNum val of 18 also has BR set to true, it should return 18. Simple, no?

But my apparently hideous, or at best awkward, attempt, doesn't even compile; I get:

Error CS0029 Cannot implicitly convert type 'int' to 'bool'

...and:

Error CS1662 Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type

I don't know why it thinks I'm trying to convert into to bool, and I definitely don't grok the second error.

If I change the code to this:

return counselPoints.FirstOrDefault(i => i.CounselNum > LastCounselPoint).Where(j => j.BR).Select(k => k.CounselNum );

I get:

Error CS1061 'CounselPoints' does not contain a definition for 'Where' and no extension method 'Where' accepting a first argument of type 'CounselPoints' could be found (are you missing a using directive or an assembly reference?)

...which also only results in me telling the compiler, "All down but nine, pard; set 'em up on the other alley."

What is the key to unlocking this conundrum?

Upvotes: 1

Views: 785

Answers (4)

Chris Moutray
Chris Moutray

Reputation: 18379

I see in your example (and also in other anwsers) you've written first-default like this: .FirstOrDefault(i => i.CounselNum) but I don't think you need this.

Think of the first-default as a filter rather than a select.

So instead you need to write like this

return counselPoints
  .Where(cp => cp.BR && cp.CounselNum > LastCounselPoint)  // where clause
  .Select(cp => cp.CounselNum) // select part
  .FirstOrDefault(); // get top 1 only

*update > exists

As @Jannik pointed out the above returns zero instead of null when there are no matches.

If it is important to test existence at the same time then you could write it like this instead.

CounselPoint cp = counselPoints
  .Where(cp => cp.BR && cp.CounselNum > LastCounselPoint)  // where clause
  .FirstOrDefault(); // get top 1 object of type CounselPoint

if (cp == null)
  return null;

return cp.CounselNum;

Note the return type of your method has to be a nullable int. See the ? added below.

public static int? GetNextBibleReadingCounselPoint(int LastCounselPoint)

... and then I guess your calling code to check the result

int? result = GetNextBibleReadingCounselPoint(lastCounselPoint);

if (result.HasValue) {
  // something was returned
} else {
  // nothing was returned
  int num = result.Value; 
}

*update2 > order

Thinking about your original SQL - I wonder if you're not considering order??

Perhaps you should have an order by added to the bottom:

SELECT TOP 1 CounselNumber
FROM COUNSELPOINTSLU
WHERE BR IS TRUE
AND CounselNumber > @LastCounselPoint
ORDER BY CounselNumber; // ascending

In which case you can add the order-by before the where clause in your linq statement; like this

return counselPoints
  .OrderBy(cp => cp.CounselNum) // order by ascending
  .Where( ... rest of code

Upvotes: 4

linqer
linqer

Reputation: 21

Try this....

counselPoints
    .Where(j => j.BR && j.CounselNum > LastCounselPoint)
    .FirstOrDefault();

Upvotes: 2

dotnetom
dotnetom

Reputation: 24916

When using query, you first need to use filtering conditions, and only then call FirstOrDefault:

return counselPoints
    .Where(j => j.BR)
    .Where(k => k.CounselNum > LastCounselPoint)
    .Select(i => i.CounselNum)
    .FirstOrDefault();

There are 2 types of LINQ query operators - immediate and deferred. Where is the example of deferred operator and FirstOrDefault is immediate opereator.

In your code you used FirstOrDefault on unordered list, so you immediately filtered the initial list.

In the sample code I provided I used deferred operators to define the filters first, and only then I used immediate function to retrieve the results.

Upvotes: 5

Jannik
Jannik

Reputation: 2429

I think dotnetom & Chris Moutray already answered this question, I just want to give a small side note:

Next time you have problems with LINQ-statements, try to split them into seperate statements. Doing that you will see that the FirstOrDefault() doesn't make any sense before the Where(), because it will return a single object and you can't use where on a single object, right? :)

Upvotes: 2

Related Questions