Let's see how Copilot was able to do when fixing up and testing code for Needlr, which is my C# dependency injection library. Did it solve the issue properly? Did it get good test coverage? Let's see!
View Transcript
Now, it is going a little bit off the rails. If you're reading in the chat here, it's saying, "Oh, look. There are no tests for Screwdor. Let me go make that." That's not what I asked for at all. Um, Co-Pilot totally saved my butt today. And I figured this would be a great learning experience to walk through so you can see how I'm using Copilot in some of my software development. So, we're going to be looking at Neler, which is an opinionated dependency scanning system that I have where we can look for types across different assemblies, add that to your dependency injection container automatically, and then you can start working with things without having to manually register stuff. So, I was writing some tests and I ran into a bit of a snag. So, let's jump over to Visual Studio and check this out. All
right, so this topic is a little bit more advanced. So, if you're looking on my screen and wondering what I'm doing with all of these service providers and getting services, we are going to be talking about dependency injection. I'm going to be talking about some different lifetime scopes here. So, if this is something that seems a little bit complicated, bear with me because I think that dependency injection is an incredibly important topic. And I do think that if this is a little bit over your head that it's at least something that you might want to follow up on and learn more about dependency injection. In my particular case, what I was doing was trying to write some integration tests. So I have a setup here. If we look right here on line 17 where this is one of the most basic things that we can
do with my Needler framework. So we have this fluent builder pattern. And all that I'm doing is creating a new builder in needler. This is just called a syringe because I'm playing on all of these punny words. And then we do build service provider. So there is literally no other setup. And what this should do is look for types that are in our assembly, automatically register them onto the service provider, and they get registered as their type as well as the interfaces that they implement. So that means if we scroll down a little bit lower, I have a type that is I my automatic service. You can see that right here on line 23. And I have a two of that just to have a second interface that we can look at. But there's also a type that implements it. So I'm going to jump
over to this definition just so I can show you very quickly. This is not really important what these things do. It's just that we need some types to work with. So you can see from 13 to line 30, this is just the definitions of these things. So I have a service. It implements two of these interfaces. And then we have these two really simple interfaces here. Really the do something method doesn't even matter. We could have probably even excluded that for this demonstration. The point is that I have a concrete type and then I have it implement two interfaces in needler. What I should be able to do is by just running that syringe builder pattern, these three types that we have here, I my automatic service, I my automatic service 2, and my automatic service, the class, I should be able to ask the
service provider for all three of these things. And because by default, it registers the types as singletons, it means that I should be able to get the exact same instance back. That means if I ask for this one here on line 13 and then I ask for it by the concrete type, it better be the exact same thing, not just another instance of it. But by default, these things get registered as singletons. It needs to be the same instance. But that's not what was happening. So if we look here on line 35, I have a test that does just that, right? So what I'm doing in the test is I've built up my service provider. I'm just going to scroll back up. If you're not totally familiar the way that XUnit will work before running a fact, which is an individual test, it will run
the constructor before each fact. So that means that this is really where I put setup code that is common across all of these tests. And that means I'm just going to build that service provider. So scrolling back down, you can see I'm asking the service provider for uh the first interface and the second interface. And then I'm using assert that they are the same. Now this test is failing because these are not coming back as the same instance. the other variations of this pass like if I ask for the same interface so you can see here on this test from line 27 to 32 uh specifically line 29 and 30 right here right I'm I'm basically asking for the same thing indeed these do come back as the same instance but this pattern does not work and really I have only one type that's concrete
so my automatic service and when I ask for it by either of the interfaces I expect that it comes back as the same instance which we see right here but it's failing. If I scroll a little bit lower we have a similar test and this is comparing the actual concrete type with one of the interfaces and this test also fails. I have a problem here because I was hoping that I could just go add these tests on for coverage but I ran into this issue which means this is in my current framework. That means I have a problem I have to go fix. It's a way nicer problem when you go to write these tests and you're like, "Oh, the test is failing." And then you realize there's just a silly bug in the test. It's a crappier problem when the actual issue is in
the code. So, this issue was actually very familiar to me. And I want to talk about another library very briefly, which is called Screwdor. And I'm going to go over here because I do have support for Screwdor inside of Needler. And if you're not familiar with what Screwor does, it's very similar in that it has like a scanning system. So you can use this with the built-in dependency injection patterns that we have in like ASP.NET Core and C# in general. And what it does is that you can see like if we look from line 23, uh, basically like just one of these blocks, right? It says like scan from these assemblies, add these classes in. And then I have some filtering and that kind of stuff. So it does do some of that. If you notice on line 31, there is a very special line.
This is where I can register that particular type that we're trying to add in as the type itself as well as the interfaces that it implements. And this is with transient lifetime. If we go down here to line 43, this is with singleton lifetime. So this is really a similar block that I am trying to make the automatic default pattern in needler. Okay. So the issue is that I have one that does not use screwdriver. So if we jump over to this class, this is the implementation that you get in my library by default. If you don't want to add in yet another package like screwdor, if you love using screwdor, you can use them together. So it's totally cool. But I have my own type registration here, right? So this says register type as self with interfaces. It's literally supposed to do the same
type of thing. And the default for me is going to be a singleton lifetime. But you can see that we pass in the lifetime as well. Okay. So just jumping back here to the screwdor one. I ran into this issue in brand ghost which is what I'm building on the side for being able to publish all of my social media content. And the issue that I had was that I didn't write this line of code. Instead, what I wrote, let me just scroll it up a little bit, is I wrote as self, cuz that's also on here. And then I said as implemented interfaces. If you do these two lines, which seem like the exact same thing as line 42, you get the same bug that my tests are showing. It actually will go register as self and it will register as implemented interfaces. But
when you do that combined with the singleton lifetime, what ends up happening is when you resolve these things, you get new instances based on whether or not you ask for the type of the class or the type of the interface. So the fix for it is to do this, right? You combine it into the single line here. This has different behavior because this will give you back the same instance regardless of whether or not you ask for the interface or the type. So, this was a very familiar problem to me because I've ran into this with Screwdor. But jumping back over to my default type registar, this is something that I was like, "Oh crap, I have to go fix this in mine." We can see that right here on line 63, we're registering it as the actual type itself. That's going to be the
class. And then below, I'm going to look for interfaces to add on. Okay, there's a bit of filtering. I'm not going to go through that and explain it, but this is the part where I'm saying, okay, we already got the type registered. I want to make sure that we can resolve it by all of its interfaces as well. Should just stop to remind you that this is an opinionated framework. This is the default pattern. You can add your own type registars in the framework, but this is the way that I build my applications. I'm familiar with these patterns. I work with these patterns. This is the way I like to do it. I'm not here to tell you that this is the right way, the only way. your way is wrong. This is how I do it. This is how I do it by default
in Needler and this is the pattern I use basically in all of my applications. Just a quick reminder, um this is wrong though. And the problem that I have with working with code like this is that especially if you've watched a lot of my older material, I have worked with another dependency injection framework called Autofac for many, many, many years. And I am transitioning myself away from Autofac. So Screwdor has been nice to work with and I'm trying to work more with the built-in dependency injection system innet but now I have a problem because I am not familiar with the different ways that we can register things. Like to me this seemed like the right thing but I can clearly tell that this is causing it to add new instances in. So here's where C-Pilot comes into play. So I'm going to pop open C-pilot
on the side and essentially here's exactly what I did. We'll see if it can reproduce the fix. I don't have the pro. Well, I guess I could pull up the save prompt. I'm going to try again just so that we can do this live. I have a great video editor, so if this totally gets screwed up, I'll do it again and make it work and he'll help me out with that. But what I'm going to do is hit this plus button. I'm going to add the selection because I wanted to focus on this very specifically. In this case, I know the problem is right here in the code. I know that it's here. In other situations, I might kind of refer to the class and say like, "Hey, I got this class. It's having an issue. I might even add the test into the context."
So, just as an example, I would go to files and I could go add the uh the set of tests that I want and pick it from this drop down. But that's not what I'm going to do here. I'm just adding this particular method because I know the issues here. I'm going to use whisper and my foot pedal so I can speak this out to co-pilot, but I'm just going to collect my thoughts so that we can talk through this. So, I want to say something along the lines that this method is mostly doing what it should, but I'm going to explain the problem that it has. And I'm going to speak this into the microphone so it'll type it out, but I'll probably have to go correct it because I find that if I'm referring to code, it kind of makes things a little
bit messier. So I will say this method does mostly what I would expect when it comes to registering a type based on its uh class as well as the interfaces that it implements. However, when it comes to a singleton registration for the lifetime, what's happening is that if I resolve the class, I get an instance and if I resolve by one of the implemented interfaces, I get a different instance. It's worth noting that if I resolve the class multiple times, it does give me the same instance back. And if I resolve the interface multiple times, it gives me the same interface back. But if I resolve the class versus the interface, I am getting two different instances when I expect to only have one when the lifetime is singleton. And this is just a quick reminder that if you're enjoying this video, go ahead and
give it a thumbs up and consider subscribing to the channel for more C.NET and AI programming tutorials. Okay, so I'm just going to do a quick little read through to make sure we got it all right. This method does mostly what I would expect. Okay. However, I think I managed to talk through this without referring to the specific types, which is kind of handy. And I'm going to switch this from ask mode to agent, and we're going to see if this thing can go fix this for us. I think the other way that you could consider doing this is if you have it in ask mode. A lot of the time if it generates some sample code, it will put it into the chat on the side and then there's an apply button. So if you're like, hey, that actually wrote the code that I
think is going to work, you can just press apply. I'm going to have the agent do it. We'll see if it gets over the top or does something completely wrong. But let's go ahead and press send. So to start things off, it says that it understands the issue when registering a singleton type. with both the concrete class and interfaces. You're getting separate singleton instances. Now it's searching for context. Let me look at some tests. What I'm nervous about when having it in agent mode is that it's going to perhaps try to like go change tests or change other things when I know the problem is directly in this file. Perfect. I can see the issue now looking at this test. There's an expectation that when resolving singleton service by its concrete class and by its interface, you should get the same instance. So it's actually
finding the test that is supposed to be exercising this behavior. Let me check the test classes to understand the setup. Now I understand the problem completely. It's registered. So this class here, my automatic service is registered as a singleton by default behavior. And the tests expect that when you resolve it, whatever it's just kind of repeating itself now. It's saying okay it's failing as expected. I mean it's currently failing. Yes. I don't know if I expect that. I don't want it to fail. The solution is to modify the registration strategy so that and this is on the right track. First register the concrete type. Then for each interface register it with a factory that resolves the concrete type from the service provider. For full disclosure, this is how roughly how it solved it when I did it the first time before recording this video. So
I have pretty high degree of confidence that it's going to get this right. I'll pull this back open. You can see in the bottom of my screen, if you're paying close attention, I'll move this up just for now. You can see that it's actually building and trying to run tests and stuff like that. So, let's go ahead build. Now, it's actually checking. And this is what I'm nervous about. It's checking other places in the code. But I'm glad that it checked the screw door one and didn't go, "Oh, I see an issue here." And then go off the rails trying to fix that. Let me Oh, so now it is going a little bit off the rails. If you're reading in the chat here, it's saying, "Oh, look. There are no tests for Screwor. Let me go make that." That's not what I asked for
at all. Um, I simply said that this bit of code right here is what needs to be fixed, but maybe we'll finish this and it will have some screwor tests that kind of prove the behavior as well. So, we'll see. Uh, pull this co-pilot chat back up. Perfect. All tests are passing. We'll see if it's doing one more run here. Great. Success. We've done it. Well, so it says, right? So, I want to go look at the new test that it added. Let's quickly talk about this pattern we see here. And I just Sorry, this keeps closing cuz I have it with the pin. So, it's nice that it calls out specifically what the code changes. I like the summary of the verification. Just a quick note when you're asking AI to do stuff. Hopefully, all of you know this by now, but like go
validate it. Even if it tells me, like I've seen AI do this a lot, like, hey, all these tests pass. I've seen it tell me that the code compiles. It's passing tests and like the code doesn't compile. So, this is great for it to tell you what it's doing. I would still go verify these things. It's nice that here in the bottom part, I can see that it genuinely was running tests. That's really helpful cuz that's real. But, let me go ahead and keep these changes. I'm just going to space this down onto some new lines so that it doesn't hurt my eyes so much. Of course, if you're familiar with AI coding, a lot of the time it adds a lot of comments. You can probably get this tuned up in your C-pilot instructions or claude MD depending on what you're using. But just
to kind of walk through what this is doing, it kept this part at the top the same registering the type as itself. But when it comes to the interfaces, it changes behavior based on the lifetime. If it's a singleton, this is the part that changed. It used to just look like this down here where the middle parameter is just the type. But now what it's doing is saying we're going to use a factory to go basically ask the service provider to go get the concrete type. That means that because this right here is looking up the concrete type and if I go back up to line 63, the concrete type is already registered as a singleton. This way it will just get us back the same instance, which is great. The nice thing about using Copilot this way is that I was familiar with the
concepts. This is something I could probably go figure out if I was to go read the documentation, but the pattern here, if I have to go do that, I have to go kind of looking for the information to pull back the data. So, I'm going to be digging through documentation to find what I need. The nice thing is, I think with AI in this case, I could have done two things. One is that I could have done almost exactly what I did, left it in ask mode and said, "Hey, could you explain how I could go do this or could you explain why this is happening?" I would recommend this more for more junior folks, especially if you're trying to learn how things work because you're building up that uh that foundation, that basis for understanding things. In my case, the second approach I did
was similar except I had it in agent mode and I was basically like, "Go do this." The reason that that works pretty well for me in my experience level is that this is something I could go figure out. When I read this code, I understand why this is fixing it. It's not that I read this and I'm like, "Oh man, this is totally magic or sorcery. I guess I'll just keep going and hope that everything works." Not the case. I understand this type of pattern. I've seen this come up before. I have a high degree of confidence in this type of thing. So, just wanted to kind of walk you through that thought process. But let's jump back to the test, right? So, it didn't touch the test just to prove it. Total changes. You can see that it only touched default type register. That's
what it says. I thought it was saying it was going to write some tests as well for the Screwdor one. So, let me see if it did. So, it didn't actually go write any tests for Screwdor. We can see that this test project is still empty. So, it's kind of nice. I didn't really want it to do that. So, what I'm going to do is just show you if I go back up to the top of this and I right click and I go to run test, we should hopefully get a green check. What's nice is that, and we do get a green check. That's great. This one doesn't though. Did it run it? Oh, it did. Just slow Visual Studio. The fact that it only touched the underlying code and didn't touch the test means that it didn't change both sides of this and
change both the implementation and the expectation. again, leaving it in agent mode. I was a little bit nervous that if I didn't get the prompt right that it would leave the underlying code the same and change the test, like, oh no, you're you're expecting the wrong behavior. And the answer is no, I'm not. I'm expecting this particular behavior. The underlying code's not working. In this case, it was able to fix things up exactly as I was hoping. And I do get to see that we get these green check marks on here. And in fact, all of the tests in here pass. We can go one step further. I can run all of the tests in my solution just to make sure there isn't something else that got broken. And there is not. I do need more of these integration style tests because this is the
type of coverage I want to have in place when I make such a change. Hopefully I have more integration tests to kind of look for uh more holistic sets of uh of problems that can occur. I feel very good about this change. Um I was kind of hinting that it made the same change I've already seen. So I have already committed this to my repository and pushed it up to GitHub. So with that said to wrap up this video, if you're interested in checking out Needler, it is available on GitHub. As I mentioned earlier, it is an opinionated framework. So if this is not how you're interested in writing your dependency injection, no worries at all. This really works for me because I like having really lightweight code that can look something like well this right. So I can have make a new syringe. I
can add anything manually if I need to. I build the service provider and then I can just resolve something and run it. This is all just extra code to print to the console. So it means that my entry points can stay super thin and for me that works really well. So, thank you so much for watching and I will see you in the next video.