Bald Bearded Builder
csharp/easy-linq-tips-every-developer-should-know

Easy LINQ Tips Every Developer Should Know!

Practical LINQ techniques improve performance and readability by avoiding repeated work, unnecessary allocations, unstable pagination, and accidental client-side processing.

Loads from YouTube when you press play

Transcript 8 topics

Avoid repeated query work

0:00

Today, we're talking about link queries, and I'm gonna give you several battle tested techniques to make your queries faster, easier to read, and kinder to your future self. You're gonna understand not only what these tips are, but why they're important. Think of them as a beard trimmer for your query fuzz. First up, if you're not capturing an intermediate value, you may be causing your query to be computed more than once. What's an intermediate value?

Let's show an example of a bad one. In this case, we're using the first name and last name to create a full name, but we're not really calling it a full name. We're just computing it twice. Now, that gets converted into SQL or whatever other query language we're turning it into, that could cause us to run that computation more than once in our query. Now in memory, we're just talking about extra CPU cycles.

But if we're hitting a database, we could potentially run into multiple executions or at least slower performance. And I know you're looking at that saying, Michael, that's that weird way of writing link. I hated it stupid and weird. Okay. Okay.

Don't think because you like writing it fluent, you're off the hook. Check this out. I'm gonna paste in some code here that shows you the exact same thing using where and select. In both cases, we're computing that field in a where and a select that's gonna get translated to essentially the same SQL. And heck, if users are denied queryable, you may be hitting the database multiple times depending on when you materialize that query.

So what's the way to fix this? Well, let's modify this query to use a let. We're gonna go back to the other way of writing this. And you'll notice this line that says, let full name equal that first name and last name. Now, in this case, we're kind of creating that at one time and then we can reuse it in any kind of where statements or any kind of projection we wanna do at the end.

Now that let gives us an intermediate value that we can then reuse. And if you prefer a more fluent syntax, we can do that too. All we need to do is project our query so that we create that full name and then use that object for any subsequent filtering or projections we wanna do. But either way, what we're saying is name the thing you want to reuse.

Use built-in aggregations

2:17

Next up is aggregate. It's a great tool for custom folds, but it's not always the fastest and it's probably the wrong choice if you're working with strings. For instance, check out this code where we're just concatenating a string to make a sentence. Now that works, but holy free holy, is it bad? Why?

Because every plus sign up there creates a new allocation of a string. That means your performance is gonna tank hard as you have more and more strings. Second, it hides your intent. Like someone really looking at this code has to really like think through what are they doing here just to realize they're just joining some strings. So what's the answer for this?

String join. I mean, it's literally made for this. I mean, look at the name, string join. It's almost like they thought that up on purpose. But why is it better?

Well, one, it only allocates once. It's crazy easier to read and understand, and it is optimized to the moon and back. You should really only use aggregate when you're trying to do custom behavior. For instance, if you need a count or a sum or an average, use those built ins. But if you're doing other kind of logic that doesn't really fit in that, something like computing a checksum or merging objects, or maybe you're even creating new state objects.

Those are better choices for aggregate. You have to remember aggregate is kind of links. I can do anything, but using it in some cases is like me taking a Swiss army knife and using its scissors to trim my beard. I could do it, but it's awful. I gotta tell you in my twenty five years of engineering, I've written so many reports and compared to the old days, LingQ does make that a lot easier.

But if you're assuming there's a right and a wrong way to do that with link, you may be clairvoyant. Let's look at an example of grouping things and totaling them. And it works, but man, is that hard to read? And we're even we're in grouping twice. That's just awful.

And in fact, if we're converting this over to SQL, this is gonna be some really inefficient SQL that's getting written out of this query. Luckily, is a better way to do this. Let's use the group by method where we can group by two or more columns as we need and then project the totals and all that kind of stuff based off of that.

Use GroupBy for totals

4:29

This is gonna get converted into really good SQL. In fact, SQL is actually gonna have one group by SQL group by statement that includes all that stuff for us. Much more efficient to the database, much easier to read and it's a win win for everybody. Speaking of route by, sometimes we use it when we shouldn't or try to do something similar that causes problems for us. For instance, let's check out this code.

We've got these users and for those that are in that HR department, we're trying to filter off a certain key or a field. We're actually gonna execute that twice in each of those four loops. A better choice here would be to use a to lookup method. It materializes once and gives o performance. Let's see what I'm talking about here.

I'm gonna replace this with people to look up with that department. That buy department is gonna materialize once and it's immediate unlike group buy, which is delayed. And then once we have that, we can go through and do our for each on that lookup that basically has created this warmed up dictionary for us to look through. Next up, do you have nested collections?

Flatten nested collections

5:49

Probably so if you're using EF Core, it's kind of a relationship kind of deal. But nested collections are kind of like Russian nesting dolls. They're really pretty. Sometimes they're fun to play with. But if you need the one in the bottom and the inside, then it's a pain in the butt to like go through them all to get what you want.

For instance, we could like drill down through this array of videos and get the tags, but now we've got like two, four loops. We've got a temporary list of tags and we're having to manually dedup it. That's no way to live, I tell you. But luckily there's a better way with select many. Let's replace this code with select many and a distinct, And now we can get just the tags out of those videos and then distinct them to get a unique list.

But what happens if we needed to keep something from the original video? Well, that's cool too. We can do it. We just need to, let's say project A and then tags. And then we'll say, give me a new with a dot title and tags.

And now we get not just the tags that are coming out, but a new object that's gonna have the title of the video and the tags. Pretty nice link. Well done. I mean, link is pretty awesome, especially when you're working with EF Core until it isn't.

Treat AsEnumerable as boundary

7:06

Until you throw it a curveball that it doesn't know how to handle. Check out this code real quick to see what I mean. In this instance, we're getting the products where the price is greater than zero and then we're calling some kind of super fancy is special method that's in C. Now at best, we're gonna get a runtime error, but at worse, it's gonna try and pull that whole dataset into memory and execute that C sharp for every record in the dataset. I mean, that's like ordering the entire menu just to eat a taco.

But you know, sometimes we can't get around that. There's some logic that's in C sharp that's gonna have to stay in C sharp. In that case, we need as innumerable to come to our rescue. And what do I mean by to our rescue? I don't mean to our rescue because it doesn't prevent a lot of that.

But if we put as innumerable on there, it provides us a break mentally to say, hey, listen, at this point, it's going to pull everything above it that we've mentioned where price is greater than zero into memory. And then it's gonna iterate through it for that method is special. But please don't lean into doing this all the time. It's a great pivot when you need to debug or there's just logic you have to have. But when you use it, do your best to do some kind of filtering beforehand so that that data set is smaller.

Otherwise, your RAM is gonna start to cry. You have to remember that as innumerable is a scalpel, not a sledgehammer. You know, link queries are a lot like brewing coffee with Keurig pods. You have to put in the water, put in your pod, wait for it to brew. And then if you decide you want a second cup, you gotta start that whole process over again.

Check this out. We're creating an IQueryable here and then we pass it into two methods. What's gonna happen is it's gonna enumerate that thing once and then enumerate it for the second method again, twice again, twice. You get it. Words are hard, but the point is this means multiple trips to the database and slower performance.

But what if I know I'm gonna want more than one cup of coffee before I get started? Is there a link way to brew a whole pot?

Use ToList deliberately

9:16

You bet there is, and it's called to list. Check this out. We're gonna replace that, and now we've got that expensive query still. But on the next line, var cached, we're actually gonna implement to list, which is gonna execute that query, pull all that data into memory. And when we pass cached into those subsequent methods, it's all there.

No more calls to the database. But a red flag should go up. We just said it goes in memory, which means we could put a big old dataset in there. We need to make sure that we're doing any kind of filtering and that sort of thing beforehand to limit what we're doing to our RAM. Alright.

That's several of these and we've still got a few left. But I'm curious, do you have any of these link horror stories that you've seen in code bases that are just, oh my gosh, I can't believe somebody did that. I love those. So let me know in the comments below. I read every one.

I try to respond to every one of them. And also, I mean, join our Discord where we talk about this kind of stuff all the time. It's super fun. But let's move on to the next one. If you are doing pagination with take and skip and you're not using order by, oh, sweet mother friend.

Use keyset pagination

10:18

You gotta know there's a better way. I mean, not using order by with take and skip is basically like playing poker and shuffling the deck every time you have a hand. You never know what cards you're gonna get. And in fact, you might get the same cards over and over again. I mean, when we do this, the database is free to return rows in any order it sees fit, which means like page two today may not be page two tomorrow.

You may get duplicate records and the heck, you may even miss records completely. But luckily all we have to do is add an order by to this. And if we do say order by you, not you anti weirdo, you. Id. Now we're gonna get more stable pagination.

And honestly, without it, your search results are like a box of chocolates. You never know what you're gonna sorry. I was reaching there. I was trying to it was a reach. I apologize.

But regardless, for that, I'll give you a bonus one. If you have really deep results, one thing that you can do to help with this is add something else. Let's say, int key equals 100. We can change this to say where, where, and then w. Id is greater than 100.

Not 100 you weirdo key. There you go. Look at me. Now we'll take that out. We don't have to skip anymore.

Now, when we send this over, the database doesn't have to scan those lines and skip records. It can just start where it needs to start and then give you the results it needs back. This is great for when you're doing like APIs where you're either maintaining that key yourself and you know where it starts and stops, like what the next one is, or if you're passing that back in an API and they send you a key and you know, okay, well that's, that means we need to pick up at this record. This is great for that. The next tip is a banger.

It is my absolute favorite thing about LingQ and I implement this in every single project I do that has any kind of size to it. Have you ever had to copy and paste the same gnarly filter into multiple queries? Like in the case of this code where we're saying, hey, this person's active and they've been around the place for in the last thirty days, that's in two queries. Now, if we have more than two queries, we have to paste that in multiple places. And then if something ever changes to that rule that says, no, no, no, it's active if you do this and this, then we've got to go replace that in every place.

It's just no way to live, I tell you. But if we'll load that into a reusable expression tree, our lives are going to be much easier.

Reuse filters with expressions

12:53

Check this out. I'm going to paste in this code. Now I've got an expression called is active recently that is that expression. And when I pass it into that filter, it's Okay. Link's gonna pick that up and say, yeah.

Yeah. Yeah. You're an expression. I can translate that into SQL. All is well with the world.

Now when that changes, I go to one place. I make the change. In fact, in really big projects, you may want, like, a filter library with all of these things for like soft deletes or tenant isolation. There's just so many use cases for this sort of thing where you can reuse these filters and make sure they're stable across your entire application. So, yeah, keep these tips in your tool belt, and they're gonna make your link more readable and more scalable.

In fact, if you've got a code base with lots of link, these changes could be the seasoning that makes your project a meal. Thanks for sticking around. Until next time.

Sign in to join in. Reading needs nothing.