BrandGhost

Just How Slow Are StronglyTypeIds in C#?

In this video, we'll use VS2026 and Copilot to build benchmarks for the StronglyTypedIds Nuget package! How much overhead do we pay for having strongly typed IDs in our code? Let's find out!
View Transcript
But I wasn't expecting to see that. And there's still use cases for strongly typed IDs that I think are helpful. All right. In this video, we're going to be looking at benchmarking strongly typed IDs inside of Visual Studio 2026. Now, in Visual Studio 2026, we get this project template where we can make benchmark.net projects. And that's pretty awesome because if you haven't used benchmark.net net before it's super powerful and it's really the standard the de facto that we go to for benchmarking things in C. Now I was recently on a net talk and we were going through strongly typed ids the talk was really about entity framework core I was explaining that I like using Dapper and one of the questions that came up in the chat was really the performance implication of strongly typed IDs. Now, I had to be very transparent on that talk that I'm actually not sure they're not bad enough if there is a performance overhead that it's causing me any grief and really the benefit of using them at least in the software that I'm building is outshining any sort of drawback. But I figured, hey, this is going to be a really good opportunity to go actually figure out what type of memory overhead there is, if any, and any performance overhead, if any. In this video though, we'll go through some of the basics of doing that using the new benchmark templates that we have. And in a follow-up video, I'll actually wire this up to Dapper, and we'll see the pros and cons or the trade-offs that we have with using strongly typed IDs. So, let's jump over to Visual Studio here. I have VS 2026 installed. This is the Enterprise Edition, and I just typed in benchmark, almost benchmark, right into the new project wizard that we have here. And we can see that there's a benchmark project. So, I'll go ahead and take that. And then I'm going to put this I'll leave it here for now. We'll move it over, but we'll call it strongly typed benchmarks. And we'll go ahead and do that. I'll press next on that. I'm going to pick .NET 10. I haven't used this yet, but I figured we'll start with this. And maybe as we go through this video, we can see if we're using different framework versions if there is actually a difference. So, let's go ahead and do that. We'll create this. Benchmark name is a new field for this template, I guess. And if I hover over it, it says the name of the benchmark class. Okay, pretty straightforward. Let's go ahead and press create. Full transparency. I have never used the template for creating benchmarks before. So, let me get copilot out of the way. We'll pull this up and we can see we have a program.cs. It looks like pretty standard static void made. If you're not familiar with sort of the benchmark.net setup, I do have other videos on my channel for benchmark.net. You can go ahead and search for anything benchmark.net or benchmarks on dev leader and you'll see other videos on this sort of thing. But this is the entry point, right? And then we can see that we are calling this benchmark runner. And what that's going to do is put something up in the console and it will actually go look for all of the benchmark classes that we have in this particular assembly. So, if you haven't seen this kind of syntax before, all that it's going to be able to do is go look for at runtime the different benchmark classes, likely using reflection. I've not seen under the hood, but that's what I'm expecting. But the benchmark CS class that we have here, this is the one that was automatically generated for us. It looks like it's sort of just set up some initial benchmarks, but this is of course not what we're trying to benchmark. So, I think the first thing that I would like to do is well, talk to Copilot about this. So, I was thinking about this beforehand. I have some ideas for how I would like to think about this benchmark, but I figured this could be a really cool opportunity to see if we can have C-Pilot put this together for us. Right? We're in the age of AI, so we should be using AI to try and help us build these things. So, I'm just going to start cleaning a little bit of this up because I don't think that this really makes sense. Let me keep the global setup. We'll do this. Um, and I cannot stand having name spaces like that. So, we'll get the file scoped name spaces. Um, and I'll remove that stuff up at the top. Cool. Okay. So, empty benchmarks class. If we were to go run this, there are no benchmarks to actually go run. But I want to talk to Copilot about this. I'm going to switch over to GBT5. It's going to be in agent mode. Couple things that we need to think about here. Um, we are going to be using the strongly typed IDs Nougat package. So, that'll be one thing. I want to be clear with it that I want to profile up memory and CPU. I want to give us something that feels like semi-realistic. So I was thinking about this beforehand. And if we do something like a very simple DTO, so data transfer object with a positional record, we could have something like an order, right? Very simple like entity that we could think about and the order could have a unique ID for itself. We probably actually want to explore now that I'm thinking about this. Strongly typed IDs allow for different data types to back the strongly typed ID. So you can use strings, gooids, longs, integers, that sort of thing. And maybe to start off, we go ahead with just like an integer just as a quick example. We'll get that up and running. We'll use Co-Pilot to help us build that and then we can go see about, you know, extending this or having C-Pilot kind of make the other examples for us. So, with that said, let me see what we can do here. I don't know if my mic and foot pedal are hooked up. Let me do a quick test. Co-pilot, can you hear me? Awesome. Okay, so I'm just going to speak this out. I've made other videos explaining this before, but I find it a lot more natural to actually talk to Copilot or Chat GPT or Claude until I have to start putting some code in place. But we don't really have any code right now. Uh, odds are I'm going to have to fix this text up though. So, let's give it a shot. I would like you to create benchmarks that can help us evaluate the memory and CPU usage of the Nougat package strongly typed IDs versus just working with simple value types. In order to exercise this, I would like you to create a simple data transfer object with a positional record and we will call that order. The order will have a unique identifier either with the strongly typed ID or the simple value type and it will also have an amount that is represented by an integer. So this will be the entity or the simple data object that we are using for our benchmarks. We'll see if that looks okay to start with. I think so. And then I think the other thing that I want to mention is sort of the scenarios that I would like to use this in. So what are some good scenarios I think that we could do um entity creation and then I think a common thing is like if we're thinking about this at least for this video we're not going to be working with Dapper directly. I'll have a follow-up for that but we could think about putting these into collections and maybe taking them out of collections. So for the benchmarks I would like you to evaluate the creation of the entity. So the order type and we will compare the strongly typed ID versus just the simple value type ID. And then I would like you to create benchmarks to insert these into dictionaries and looking them up from dictionaries based on their IDs. And then I think I want to give it a little bit more detail around sort of the the benchmarking constraints. So it knows or I've mentioned that I want memory and CPU. It's probably also worth mentioning like maybe I can tell it the baseline and perhaps like simple job type. It's actually a short job type. So for the benchmarks, I would like you to use a short job type so that we can save on some runtime. And I would like you to make the baseline uh scenarios the ones that use the simple value type for the ids. Cool. Let's start with this. See what it comes up with. It might be totally out to lunch. We'll find out soon enough though. Let's run this in agent mode. And as I said, this is in VS 2026. In some of the notes for the IDE, they actually say that the interactions with co-pilot have been improved. Uh, so this I can't comment on yet because I haven't used it enough to see if it actually feels like it's smarter or works with the context better. But at least in my experience working with LLM tooling so far, that is one of the more frustrating things is that I've made plenty of videos about this on my other channel called Code Commute. It actually often feels like the LLM can do very smart things, but also it messes up on some really trivial things. And it's a weird dichotomy between being very smart and seeming very stupid at the same time. We'll see how it does with this though. I actually don't know what I expect. I don't know if it's going to be sort of like smart enough to go add the Nougat package, but we'll find out. I don't think I actually told it. I said of the Nougat package, strongly typed IDs, but we'll see if that gives it enough to to go install that Nougat package. Okay. To start things off, we can see in the chat, I'll add strongly typed ID generator package. So that's good news. Build failed. Okay, so it's going to be iterating on this. It's running in agent mode. If I click over to the dependencies packages, strongly typed ID is added, which is great. If I check it out, it actually says, okay, now it's trying to pick specific versions of it. I guess that was one of the issues in compilation. So it looks like it can resolve it now. So that's good news. I don't think that that is the the name of that parameter though. It's interesting. I think when it does this kind of stuff, it's probably seen this in training data and I don't think that the property name is called backing type. I can't remember what it is, but I think that that's incorrect. So it's probably going to figure out quite quickly that it needs to use something else. But let's look at the DTO's that we have here. So it made two variations, right? One with the strong ID, order with value ID, order with strong ID. Interesting. It actually seems like that's misunderstanding what I was asking for. I would expect one of these to be an integer, a simple value type ID. So, interestingly enough, it's it's uh considering that order value ID is its own strongly typed ID. So, we'll see if this can get to a point where it is compiling, but I will probably delete the code on line 15 and just replace it with an integer unless it does it for us. And I think actually it's going to do that. It's kind of interesting to watch these LLMs iterate through things. But if I go down here on line 50, you can see like it's getting very confused, right? New strong ID. It's making it with a GID. That doesn't make any sense. We're dealing with integers. But let's keep going through here. Create order simple ID. Create order strong ID. Okay, so we have these two. The baseline is set to true for these. There's still compilation issues. Yeah. So like it's almost seeming like it's guessing maybe based on some training data that uh it has these methods to work with. I have seen this outside of VS 2026 using co-pilot with strongly typed IDs actually in some of my own code that I've worked with that it seems to guess at some of the methods. That seems like that might work now. Interesting. It looks like it's enforcing I don't know what this is here. It added another package. Full transparency. I've never heard of this package here. No idea what that is. Okay. Do you want to run the following command? Sure. So, because this is a new install of VS 2026, there's going to be a bunch of stuff that it's asking me for permission to go run. Whereas historically in VS 2022, I've already given it permission to do a lot of this stuff. So, it's trying to build these things. I'm going to go ahead and press keep on this so I can start to actually read what it's doing. I find when it shows that delta view with what's new and what's removed, it's extremely hard to read if it's touching a whole file. So, we can check out the order type that it made. This public read only struck. It made it generic. And this is the one where it has the strongly typed ID. Kind of curious around this. I would have at least personally just kind of hoped that it would have done and I might change this. I don't know if I wanted to use a generic here because it's using a generic because it's trying to reduce the amount of duplicated code. But I actually think that I would like the code to be duplicated for the benchmark. I'd like to see just a normal order and an order with a strongly typed ID, not a generic. So, this is potentially kind of changing a little bit of what we're dealing with just to be able to compare things. And I don't know if I really uh like that. So, okay, let's go check out the benchmarks. Let's keep this as well just so we can read through it. Okay, couple things going on. We have the global setup here, right? It's using simple and strong. Interesting though, it went to use gooids and order ID is a strongly typed ID, but it didn't specify. Very interesting. It's like I wanted it to be an integer. I'm pretty sure I said that in my prompt. I just scroll back up. Maybe I didn't, but strongly typed ID, simple value type. Okay, I want to start with integers at least. So, I'm going to go through this code now. Instead of like arguing with co-pilot to go do it, I'm going to switch this stuff up the way that I think that I would like to see this. So, I would like the order ID to be an integer. I don't want to have this be a generic. So, I'm going to have two. So, order, it's going to keep doing this until I accept it. So, order strong and order simple. And this is going to be an integer ID. And this is going to be an order ID. So, we'll put that in there. Okay. So in these two cases um yes we are like kind of duplicating the type but this is so that I can do what I feel like is a more direct comparison because we wouldn't be using the generic type in production code we would be picking one or the other. Now we have that if we go back to benchmarks this is going to complain because it's not it's not this is order simple if I could spell properly order simple order strong and then we'll do the same thing. So, I'm going to change this everywhere to be order simple. My camera is blocking the the actual screen here, so I can't really see. And then we'll do order strong. We'll get that going. Cool. Now, simple ID up here needs to be an integer. That's not going to work obviously. So, do 1 2 3. Now, these dictionaries are also not going to be correct because I don't want a gooid. Okay, now it wants to preload things. Yikes. I think what was happening was based on how it wanted to go create all of this stuff. It was trying to figure out uh like how it's going to generate unique IDs the whole time. That's super annoying. Okay, let's let's start with gooids perhaps. Now that I'm seeing this, I don't want to go changing all of this logic to go do what it was after. Okay, so we'll get that in place. But order ID needs to be backed by I was jumping to the generated code. We'll put a gooid back here. Okay, so we still have these two. This will need to be a GID. So we still have these two types. The order ID itself, the template is a GID. And this should hopefully compile now. Let's have a quick look through what's going on. So we have insert benchmarks here where this is the baseline. I actually can't recall when we do multiple things with baseline in them. I don't think that that actually works. Let me just go ahead and remove the baseline part because I think that I technically would need to break these out into different classes, but I'd rather just avoid that for now. If you're not familiar with what baseline does in benchmark.net, it allows us to have a comparison from one run to another, but we can just look at that ourselves. Okay, I think that's probably pretty good. Okay, I feel okay about this. Is it perfect? Perhaps not. But I can see that in all cases it's doing a return. So that ideally we don't have compiler optimizations basically say oh you're not actually using the the you know the result of this we don't need to run it but I think overall this should be pretty good. There's some things that I'd like to do like you know the capacity is a th00and or whatever and we have 10,000 up here for these dictionaries. You know I'd like to use constants and stuff like that just to clean it up but I think that should be okay. We can see that we have the CPU usage memory. So copilot got that part right but overall I think this should be okay. Okay. So, I'm going to switch this from debug to release. And I think we can go ahead and run this. It might complain that I am doing this cuz there's going to be a debugger attached. Let's see. Okay, I think we should be okay. Now, there are some other copilot features in Visual Studio that I would like to try. They do have a profiler agent. Perhaps not in this video because we're just doing a bit of comparison for using simple IDs versus a strongly typed ID. I would like to explore in a follow-up video where we can actually use these benchmarks, maybe not these ones specifically, but as co-pilot um in terms of the profiling or optimization agent that they have now to actually go through, run the benchmarks, look at the data, and then suggest the actual changes that need to happen. Okay, so this is all done. Oh, I'm so silly. I actually removed the wrong thing. I removed the whole attribute. Okay, we got to run them again. So, for those of you that caught it, I certainly didn't. I removed the whole thing instead of just the um the baseline part. Run it again. All right. Now that these have completed, let's go look at what's happening here. So, interestingly, I can see just from a quick scan, we have in all cases simple than strong, simple then strong, simple than strong. The create order simple ID takes 63 nanoseconds on average. The strong ID is actually slightly faster. I mean we're talking this is already extremely fast, right? These are tiny tiny tiny differences but it is faster to use the strong ID which is kind of uh interesting. The error and standard deviations also less as well. Okay. So then if we go to the dictionary this is to this is build dictionary. I probably should have checked that more specifically, but build dictionary is actually okay. It's like inserting into the dictionary based on the code. This is what happens when you have AI write your stuff. You're not even sure what it's doing. So when it's inserting things, again, the strongly typed IDs are actually slightly faster in terms of memory usage. It's the exact same. This is actually So maybe not the performance part. I would have assumed maybe the complete opposite for the the numbers. I would expect them to be very very comparable, but perhaps the opposite, like slightly more overhead for the strongly typed ID, but for memory, I actually thought this makes sense. I I don't actually expect more memory usage. And just to explain why, if I jump over to the order ID, if we press F12 on this, if you're not familiar with this library, it uses source generation to be able to create the code, right? So, you can see public partial strct order ID. This is the only code for order ID in our solution, right? But if you look down at the bottom here, I can jump over to this code here. And this is the generated code for this. There's actually literally code that's been generated. And so like we didn't go type all of this. You didn't see me do that, right? But this is code that's been generated for our order ID using source generation. really if we go back up to the top this type my understanding is like the only thing that we're storing is literally the backing type so in this case it's just the gooit right here right just the value we have so in terms of the memory allocation I would assume this to be identical and so that's what we're seeing in the benchmarks which is uh right here right the gen zero gen one allocated part so to me that makes sense and then the lookup again slightly faster with a strong ID. So, interestingly enough, using strongly typed IDs, at least in these benchmarks, show that it is technically faster. Now, let me be very, very, very transparent. I would not recommend to you to go switch everything to strongly typed IDs because you watched this video and you saw that like it was a nancond faster than using normal value types. That is literally not the reason that I would recommend anyone use this. In fact, I wasn't expecting to see that. And there's still use cases for strongly typed IDs that I think are helpful. And for me, like I said in one of the net community calls that I got to join, it helps me kind of think about the different types of data that I'm passing around. There are, yes, some situations where we can make sure that we're not mixing data types that we shouldn't be, right? So passing uh you know typical example that you'll hear about for this kind of stuff is like not accidentally passing a you know name instead of an email versus a phone number kind of thing because those are all strings. That's less the case of how I use this. I have certain situations in some of my code where I might have an access token and a refresh token. I would like to treat those a little bit more specifically or I have some types of IDs for my entities and I want to make sure that I'm using the, you know, the primary key on those entities in certain situations. They might be named very similar and I want to make sure that I'm not accidentally passing them around. Especially when they're like a uniquely generated number or a gooid because it's not like it has something when you're debugging that makes it very very obvious like oh this is very clearly this record type ID and this is very clearly this other record type ID. It's just some list of numbers. It could be any ID and you're like I can't find it anywhere. it's because it's the wrong ID. So, those are some use cases that I like to use it in. I think that that would be my recommendation to you if you're like, hey, this makes my code easier to understand. But again, if we go quick check at the benchmarks, at least in this case, it was a little bit faster. And the same for memory usage. So, if you thought this was interesting and you'd like to see how this measures up when we use something like Dapper and we're actually mapping data from the database into something like this order ID, then you can check out this video next when it's ready. Thanks and I'll see you next time.

Frequently Asked Questions

What are strongly typed IDs and why would I use them in C#?

Strongly typed IDs are a way to create unique identifiers in C# that are type-safe, helping to prevent errors by ensuring that different types of IDs cannot be mixed up. I find them useful for distinguishing between different kinds of data, like access tokens and entity IDs, making my code easier to understand.

How do strongly typed IDs perform compared to simple value types?

In my benchmarks, I found that strongly typed IDs were actually slightly faster than simple value types in terms of both performance and memory usage. However, I want to emphasize that performance should not be the sole reason to use them; their primary benefit is improving code clarity and safety.

Will you be demonstrating how to use strongly typed IDs with Dapper in a future video?

Yes, I plan to create a follow-up video where I will show how strongly typed IDs perform when used with Dapper, particularly in mapping data from a database. Stay tuned for that!

These FAQs were generated by AI from the video transcript.
An error has occurred. This application may no longer respond until reloaded. Reload