In this video, we'll build on a previous example where we measure the overhead of using StronglyTypedIds with Dapper. Using BenchmarkDotNet, just how much overhead is being added for the convenience of strongly typed IDs? Let's find out!
View Transcript
We'll just wait a moment. I will have the editor fast forward through all of this so you don't fall asleep. In this video, we're going to look at benchmark results for strongly typed ID usage in Dapper. And the reason for this video is that recently I got to sit in on the .NET community standup. And I was having this conversation about Entity Framework Core usage versus Dapper. And of course, if you know me, I'm on the side of using Dapper. Not because I'm against Entity Framework Core, just because I prefer using Dapper. But one of the questions that came up in the chat was if you're using strongly typed IDs with Dapper, is there some overhead to using that and what does it look like? Now, on that call, I didn't know the answer to that. But I do know that it's not having such
a negative effect in my own application usage that I really had to consider not using it, but I figured that would be a good followup. So, I did make a previous video, which if you haven't seen, you can check it out right up here, that focuses just on strongly typed IDs and the benchmarks. Got to use Visual Studio 2026, use the benchmarking project template, but in this video, we're going to switch over to using Dapper so that we can see truly what this person was asking about. So, let's jump over to Visual Studio. And you can see on my screen, if you've watched the previous video, I'm working in the same solution. So we've already started Visual Studio with the project template again from VS 2026 and what I've done now is I've introduced a new benchmark file. So you can see that I have
really in this class here SQLite Dapper benchmarks. We have couple of classes up at the top which I'll explain in just a moment. And I do have some commented out code because I do want to be able to move easily between using gooids for our strongly typed ids but also using things like integers. And I'll explain this in just a moment. If we look at the global setup which is indicated by the attribute that says global setup, we can see that we have some of these handlers added in. Again, I'm going to go back to these in just a moment to show you the code behind them. But we're going to open up a connection. I'm just using SQLite here. We're going to have an in-memory database. And if we go a little lower, we can see that we're making two tables. And really, we're
going to be using two tables with two sets of queries because one is going to be with strongly typed IDs. And the other table is just going to be using the base type, something that's a very simple value type. In this case, we'll have a GID. And the next variation will use an integer. So we're starting off by inserting some data so that we have something that we can look up by a specific ID. So a simple ID and a strong ID. And then we're going to add in a bunch of other records as well. So that way when we go to query them in some of the benchmarks, we have some data there already. And then there's also this global cleanup. If we go look at the actual benchmark methods, again, please bear with me because I do have some commented code. This is
again just to make it easy when I go to switch over to this. But if we look at the uncommented code, we're going to be creating a simple order or I guess it's called order simple in this case. And it's going to be with just the simple type gooid, not a strongly typed ID. We're going to go insert that into the database. Go down a little bit lower. We're going to be using the strongly typed ID. So order strong. Really, I should jump over to these to show you them if you haven't watched the previous video, but you can see order simple, the ID type is just good. In order strong we have order ID and that's the strongly typed ID which you can see over here. So strongly typed ID where the template is good. Okay. So jump back over to here again. We'll
continue down a little bit lower. In this case we're going to be inserting. We're going to go create the records to go insert and then we will go insert them. But all of these ones in this benchmark method are going to be using order simple. And all of these ones down here are going to be using the strongly typed ID. With a GID, we can actually use order new. And the strongly typed IDs will allow us to create a new one. That doesn't work the exact same when we go to something like an integer. And I believe that's just because again if I jump into this you can see that the code generation has actually created a method that gives us sort of like a new random ID. Let's head back over to the other benchmarks. We have querying. So query single or default. So
we're going to go look it up by the simple ID or the strong ID in this case. And then we can query for many as well. We have these benchmarks. And I did say that I had to go back up and show you some of these other classes up here. And if you're not familiar with strongly typed IDs or Dapper, there's something that we have to consider when it comes to being able to load records out of using Dapper. So Dapper is an OM, which is an object relational mapper. And that means if we go right back down, let's go check out one of the query ones, right? We can see here query single or default. This is order simple and query single or default with order strong. When we go to do this select, what Dapper is going to do for us is go
run that query. But when it pulls the data back given these two values, ID and amount, so these two columns, right? It's able to figure out how to go create an order strong or order simple based on the query. Dapper is going to do that for us. If you're, you know, familiar with Entity Framework Core, Entity Framework Core also is an OM. It does it for you as well. It's just that generally in Entity Framework Core, you don't find yourself often writing direct SQL queries like this. The tricky part with Dapper is that out of the box, it does not support mapping to strongly typed IDs based on what's being queried, which means we have to go add special handlers in, which are what we see right here. So, let me go expand these. And it's also worth noting that this is uh especially important
with SQLite where there's not the uh sort of what I would call traditional types that you have with a SQL database. So for example, there is no GID type in SQLite. You don't have a GID data type that you can assign to a column. You do have strings though. Okay, so we have strings, we have integers, I think floats as well. There's blobs, but you don't have GID as a dedicated type in SQLite. And so that means even for the non strongly typed ID, we have to actually have a mapper in place to get a gooid back. This is the other reason that I also want to show you this using integers where we can get rid of one of the mappers entirely and just show the overhead of having to use one of these mappers. Okay, if we go look at the first one
up at the top, I'll collapse the second one. This one here is saying when we go to set value. So this is going to be writing. We're going to set the DB type to GID and we're going to assign the value. But we're really considering reading in this case and this is where we have to actually take um the object coming in. So it has a parse method but it's object not necessarily just string. If it is a GID then we will just go return a new order ID. That's not going to be the case with SQLite because we have a string column. If you missed it, I'll scroll a little bit lower. You can see that when we create the table, the ID is of type text. Okay, so if I go back up here, means we're really going to be hitting this part
here. Now, if you think about this for a second, every time we go to pull a record out of the database using Dapper as our OM, we need to essentially go parse that string to make it a GID. So every single time we pull one of these back, we have this overhead of parsing to a gooid. Okay, so that's something to keep in mind. And if we go look at the other one, it's going to be very similar actually because it is a gooid. So the only difference is up here I am returning a new order ID because that's what this mapper is for. And down here, this mapper is just for gooids. So, if you're a little bit lost by that, what I'm just trying to call out is that even with our simple data type as the ID and not a strongly typed
ID, we still need a mapper here. We still need a type handler to be able to go from string columns to gooids. It's just that up here, we go one step further and we go create that new order ID. So hopefully that makes sense and we're going to go run these benchmarks, look at the results, and then once we have that and we've gone through them, I'll switch this code to go use integers by uncommenting some of the code that you might see like right here for example. And that way we can go run those benchmarks. And the big difference will be that we actually take out one of these mappers entirely. We won't use a mapper for the simple type for the ID. We'll only use it for the strongly typed ID. and we should probably see a bigger discrepancy in the usage for
strongly typed IDs. So, let's go ahead and run this though. Keep in mind that I have it in release mode. Continue debugging and we'll just wait a moment. I will have the editor fast forward through all of this so you don't fall asleep. Okay, now that we have the results, let's scroll back up. We can see our benchmark data right in front of us here. So it's always going to be simple for the ID then strong. So simple, strong, simple, strong, so on and so forth. But it's the different benchmarks that we'll be going through. So we can see that when we're inserting simple and strong, inserting strong is a little bit quicker it seems in this case. This is what we saw in the previous video. For what it's worth, I actually don't expect that the the strongly typed ID should ever be faster,
but we saw that in the previous video for some things, and we can see it here. Although, what is different is that on the memory that's allocated, we do use a little bit more memory in this case, right? It's not by much, but it's a it's a little bit more. But strongly typed IDs in this case for inserting were a little bit faster. For what it's worth, just for full transparency, when I've run this code before recording this video, these times were actually roughly reversed. So, I did manage to see the um strongly typed IDs be a little bit slower. Just for full transparency, if we go down to inserting in batches of 100, we can actually see that reversal come into play. So we can see that simple ids so just with gooids as the ID versus the strongly typed ID the simple ones
are in fact a little bit faster in this case and of course if we go look at the allocated memory it's a little bit more right not by much but a little bit more for inserting a batch with strongly typed IDs. So if memory is a huge consideration for you in terms of allocations this might be something you want to avoid. In my particular case, this amount of memory because I have similar data types like this isn't something that would really bother me for my particular use case. If we go down to querying, uh this is almost identical, right? 5.965 and 5.951. Technically, you know, strongly typed IDs were faster here, but it's so close that I guess the point is like it's negligible, right? And again, strongly typed IDs taking a little bit more memory in this particular case. If I were to hazard
a guess, I would want to say it's because of the parsing. But actually in both cases, we are parsing strings to gooids in both cases, right? Even for the simple ones because of using SQL light here. We're querying simple versus strong for all of the records. We see that we do have a bit more of a a delta between the two for simple versus strong forgetting all of them. uh pretty large uh discrepancy here on the error column. But if again if we go look at the the allocated memory in this case maybe we can't see it because of the number of decimal places but essentially the memory usage was the same here. So overall like I feel like the amount of overhead we're seeing from the strongly typed IDs in terms of performance it's almost the same. Sometimes it's faster, sometimes it's a little
bit slower. And for the memory, it's almost always allocating a little bit more. But again, this is not in my opinion a great example because in both cases, we're using these mappers on top. So these type handlers to go from a string, a text column in this case to a gooid. So that feels kind of unfair because it's like it's not really like a clearcut, you know, simple ID. It's kind of like we still have to do this extra step in both cases. So, let's go ahead and switch this over to integers. And we will see a very simple data type for the ID versus the strongly typed ID, which is also simple. It's just got a little bit more to it. So, let's see what happens. I'm going to pause here just for a moment as I'm commenting this stuff and basically swapping commented
lines for uncommented lines. But in SQL light, what we will notice is that sometimes the data type that comes back from Dapper will be long versus integer. And so I'm just making sure that I have both in place here. I have seen it come back where even where it comes back as zero, it's still a long. So I just want to make sure that we have that covered. I go over to the strongly typed ID, I will change it from gooid to int. And then if I go over to the order, I need to make sure that this the simple one is now also with an integer. So at this point, this code will compile. We don't need this one at all. So I'm just going to comment out this other one here. And that means we can comment out this one. I kept this
order type order ID type handler. It's still the same one. It's just that I've swapped it from gooids to um integers, right? Let's keep going through. I'm going to put this code in as well. And the reason that I have a little bit of extra code here is because with gooids, we could just generate a new random one. I hinted at this earlier in the video, but for integers, I'm actually just going to be incrementing so that we have something that's unique instead of calling like random next or something like that. We also have to change the schema. So, integer instead of text and integer instead of text. And this is where I'm going to put this in place. So, we'll start at zero and then we'll be incrementing beyond that. Something to keep in mind is that this is a a slight detail or
detour if you will from the performance part, but one of the things that came up on the net community standup when I was I guess there was was like how often does this happen where you're changing the type, right? Like you you'd think that you know one of the the nice things that you get with having strongly typed IDs is like you can just change the backing type of the strongly typed ID and you don't really have to worry too much. But I'd called out that you have to worry sort of at the edges. So if you have the strongly typed ID being used throughout your application, you can change the backing type and most of your application doesn't have to get updated at all. But if you had something like an ASP on a core web app, you might have to change that sort
of at the API level, the web API, and you might have to change it at the complete other end of your application at the database. That's what we're seeing here. Like I literally had to change the schema of the data in order to go from having a gooid text column to an integer. Like technically we could store integers in the text column, but like I think you get what I'm saying. At the edges you have to kind of think about changing things a little bit more. Let's go ahead and get rid of that. And this is where we're going to be incrementing. And then I have to go through these benchmark methods quickly and swap out these lines. Okay, I just did a quick check. It looks like we're able to build. With that said, I think we are good to try this out. Again,
you'll see that I have this last inserted simple ID starting at zero. We'll increment by one. And then when these benchmarks run, you can see that I'm doing this pre-increment here before actually performing the insert. So, with all of that said, we have this hooked up as well. I'm I want to prove to you that I'm not using a handler in the other case. Essentially, we should see that for the uh DTO's or the records that do not use the strongly typed ID, they do not need a type handler at all. What I expect to see when we run this is that we do have better performance characteristics for the simple ID versus the strongly typed ID. And that's again because I think that there's a little bit of extra overhead due to these type handlers. But let's go check it out. Okay, the benchmark
results are in. Let's check things out. So again, same order, simple versus strong, like simple for a strong right after. When it comes to inserting, we do see this flipped around from the original one, right? So, uh, inserting strong is a little bit slower. Not by much, but it is. And we do see again that there is more memory allocated. Memory is very much the same in terms of the the relationship but we do see slower inserting strong seems to be a little faster in this case like again not by much we're talking you know one microscond here so again very very comparable and a little bit more memory used if we go down uh querying strong this happened to be a tiny bit faster these numbers are so close that I'm personally like not trying to tell you oh look look faster. To me,
it's it's basically the same thing. At least for the applications that I'm working with, this would not make that big of a difference at all. So, querying by strongly typed ID really didn't change much. But again, allocated memory a little bit higher for the strongly typed IDs. And finally, when we go to query all of the records, simple was a little bit quicker than strongly typed IDs, but interestingly, we do see like disproportionately more memory use on the strong one. Right? If we go back up to the inserting part, you know, quite quite comparable, little bit more for the strongly type, but when we're querying, we do have uh, you know, more substantial and we actually see Gen One here. So, kind of interesting, something to keep in mind. But again, like I I'm just going to be very transparent for the types of applications
I have been building, this wouldn't be something that deters me from this type of uh usage of strongly typed IDs. It certainly would not be the thing that I am rushing to go optimize because I think for my applications there's probably things like better indexes, caching, things like that before I'm trying to chase this kind of thing down, especially around the memory allocated. But this is hopefully a good starting point for you to think through these things because if you're considering strongly typed IDs now, you can see that mostly they're quite comparable to just their their base types. However, especially with uh memory allocations, there's a little bit more in this case with Dapper. Now, in the previous video that I made, there was basically no difference, but just something to keep in mind before you rush to any conclusions. I do encourage you to
have your own benchmarks, try things out, and understand how it works in your application because these are very simple benchmarks. They might not be representative of what you have going on, but again, hopefully it's a starting point and you can see how these things come together. So, thank you so much for watching and in future videos I will be covering some more different features of VS 2026 because this one was building on top of the benchmark.NET project templates that we have available to us. So, I will see you in the next video. Take care.