Check out a free preview of the full Building APIs with C# and ASP.NET Core course
The "Creating a Custom Filter" Lesson is part of the full, Building APIs with C# and ASP.NET Core course featured in this preview video. Here's what you'd learn in this lesson:
Spencer creates a custom filter that eliminates the need to repeat boilerplate validation logic. The PUT request is refactored with a new validator and the tests are updated so they are using valid requests.
Transcript from the "Creating a Custom Filter" Lesson
[00:00:00]
>> Spencer Schneidenbach: Let's write our own filter, because as much as I don't mind too much boilerplate, if I can reduce it in a way that Developers like and are really useful to developers then I'm gonna do that. Let's take this piece of code here. First things first, I'm gonna go ahead and I am going to close this folder and I'm gonna open up the next folder here, which is middleware-and-filters-3-D, perfect.
[00:00:28]
And I'm gonna look specifically at this validate async method, right? And we're gonna look at it where it's used inside of our employee controller here. I'm gonna scroll down to my post request, my create employee, and I'm gonna see that, I'm gonna repeat this code quite a bit.
[00:00:47]
You'll remember that if you're using the built in stuff for ASP.NET Core, and you're using their built-in validators, if you have the API controller attribute on top of your controller, like we do here. Right there in our base class, that it will automatically do validation for you. Well, we don't have that option because we're using async validation.
[00:01:08]
So there's no way that we're using fluent validation and async validation. So there's no way for ASP.NET Core to know that we have something to validate, so we have to do it manually. Cuz it's better than not doing it at all, that's for sure. But we can get around that by adding a filter, we can actually take it after that model has been bound.
[00:01:26]
We can actually figure out what the model is, what the type of it is at runtime, and then see if we can validate that object. We want to add in this fluent validation filter. Let's see, I'm actually going to,
>> Spencer Schneidenbach: Yeah, we'll add it, and we'll go ahead and copy this in like so, and then we're gonna go through this line by line.
[00:01:49]
This is actually a fairly complex file, I really wanna demonstrate the functionality. It's not so important that we know everything that this thing is doing, but only that it's available to us. cs, so I'm gonna paste that in here. I'm gonna start importing things like that. I'm gonna import that, I'm gonna import FluentValidation.Results.
[00:02:16]
And I am going to import FluentValidation. And a thing called ProblemDetailsFactory, which we will touch on. And so what this is actually doing, this filter is actually spinning through all of the parameters that we've specified on our controller method. And it's determining, do any of these parameters match an existing validator?
[00:02:39]
It's using reflection to determine to say, this is the shape of that IValidator type, and do we have one in our dependency injection pipeline that we can use to actually validate this incoming object? Again, it's not super important that, I don't expect anybody here to say, wow, okay, that's a lot of stuff.
[00:02:59]
Cuz this is a lot, this is rather advanced programming. But this is where reflection and the power of reflection, as I explained in my previous course comes in. We can do things at runtime and create repeatable patterns such that we can add additional behaviors and do different things, even if we don't know the type ahead of time.
[00:03:22]
>> Spencer Schneidenbach: Let me see validationResults.AddToModelState, yes, I know why that's not showing up, we'll go there in a second. Okay, so I don't wanna necessarily, we'll step through this here in a second. I do wanna point out that this thing is missing, and that is because we need one new get package, which is, do Ctrl + P.
[00:03:39]
And open up Nuget, and then we are going to FluentValidation.AspNetCore, go ahead and add that.
>> Spencer Schneidenbach: And then we'll use that and then we'll import it. That's just an extension method that takes our validation result and adds it to our existing model state which is available inside of this context.
[00:04:03]
One last thing to actually use the filter is that in our AddController method call, we actually have to say, hey, make sure that this filter is part of all of our controllers. So without this, ASP.Net Core just won't use it, they just see a class. It just sees a class as part of the solution, and it doesn't do anything with it, so we do need to instruct ASP.Net Core to use it.
[00:04:25]
So we're gonna go to our program file, we're gonna go down here. We're gonna replace our AddControllers method here with this one, that's gonna add our FluentValidationFilter. And now we're saying, again, it's like micro middleware, right? So we're taking it and we're saying, hey, we've gotten to the controller but before that controller executes, we're gonna send it through this filter.
[00:04:44]
So you can do it actually one of two ways, you can send it before the controller actually gets the request to actually handle it, or you could do it after. You can modify the aspects of that response after the controller's done executing it. And you can see that here because we have OnActionExecution and OnActionExecuted, so there we go.
[00:05:07]
So we're gonna play this, debug, boom.
>> Spencer Schneidenbach: So I'm gonna go ahead and hit that, and you're gonna see that now that we've established that our filter. I'm gonna move this out of the way, that our filter is actually part of our controller, we can actually see that it is executing.
[00:05:29]
So we're gonna take a little bit and just take a look at some of these objects and go through. Again, I don't expect you to memorize it. The point is, just know that this is available, because this may be useful in a probably will be further down the line as in your .Net and your ASP.Net Core journey.
[00:05:47]
So I wanna call out this parameters thing specifically, which is this parameter descriptor. And you see this name is employeeRequest, which is exactly the name of our variable here. So, because it's already gotten to ASP.Net Core's web API handlers, it knows that this is something that should be in the incoming request.
[00:06:11]
It's actually already gone through the trouble of binding it for us too. So if we go here and spin through, we can see that if we try to get the value, we can actually see that we get our CreateEmployeeRequest object. We can then read the type of that object at runtime and then use reflection to make a reference to that generic type.
[00:06:33]
So you go here validatorType, and you can see our validatorType is IValidator of CreateEmployeeRequest. And then you can go through and then ask the dependency injection system, does this thing exist, does this validator exist? And if so, I would like to use it. And that's when you would go through and do the logic that you would expect, which is that if validation result is valid, you use the AddToModelState to just add it to the modelState context again using the FluentValidation ASP.Net Core package.
[00:07:04]
And then I think the strangest part of this, all of it feels pretty straightforward except for this ProblemDetailsFactory. This is actually an abstraction that ASP.Net Core uses for some reason to actually create the validation problem details object, this is just basically a factory class for creating those. So we have to use those, because it's defined in some other part of ASP.Net Core for us.
[00:07:32]
And that's when we can set the result of our request, which is normally if we don't have this set, then it would just be handled. But at that point, we can set it to BadObjecRequestResult, and you can see that we get our 400 bad request. Now I don't believe I removed this validation logic here, let's see, what are you complaining about?
[00:08:01]
We'll go ahead and kill that by, it's saying that we don't have anything async await in here. We can await on a already CompletedTask in order to kill that, or we could simply convert this to a non-async method. I'm just gonna take the little bit of a Kluge code here and just let it do that.
[00:08:19]
But to just show you that, we don't call validate async anymore, but we still get our validation problem details. You can see that we send it and we still get our validation problem details. And because we were smart, we thought ahead of time, and we wanted to write some tests, we're gonna go ahead and run our tests.
[00:08:35]
We're gonna see that everything works just fine. Now, we're gonna go ahead and do something else, which is we're gonna add our validator to our PUT endpoint. At this point ignored our PUT endpoint. But our PUT endpoint is doing a mutation of its own, which is it is changing aspects of our system.
[00:08:54]
And we do wanna validate that those things are happening. We wanna validate that object is valid before we alter a part of our system. So we're gonna create a slightly more complex validator. I'm gonna copy it here, open this up, and I'm just gonna put it under my updateEmployeeRequest class, just reduce the number of files that we have, and then I'm gonna close this so we can look at this.
[00:09:22]
I'm gonna do an import, import FluidValidation, command.
>> Spencer Schneidenbach: And import that and let's talk about what this is doing. First of all, we've defined our AbstractValidator here of an updateEmployeeRequest and we've given it a little bit of context, a little bit of things that it needs. We wanna be able to get the Http context, we'll show you why in here a second.
[00:09:48]
So this HttpContextAccessor object is an object that you can use. It's part of the dependency injection system to downstream in another service, get access to the Http context, which is really critical. Multiple because we need to read values from that Http context, I'll show you what that looks like here in a second.
[00:10:06]
And then, of course, we have our repository object that we need to get. And we have some very custom, very interesting logic right here. Which is that for address, we wanna say that this must not be empty if it's already set on an employee. In other words, if the employee object already has an address, then we wanna make sure that nobody can remove that address.
[00:10:31]
>> Spencer Schneidenbach: Why did we need the Http context to do this? Well, we actually needed it so we could read the Routevalue of the Id from the request. The validator doesn't have direct access to say, we're updating employee one, two, three, four. Thus, we give it the httpContext in order to say, in order to get access to the request, and then get that Id out of that Routevalue.
[00:10:52]
So we know what the employee Id is, and that's relevant, because we need to reach into our employee repository. We need to GetById, we need to get that employee and then say, if the employee address does not equal null and the string IsNotNullOrWhiteSpace, then we need to return false, otherwise, return true.
[00:11:13]
And this is a little bit of a different syntax. I think we saw it a little bit earlier, but this is establishing a validation rule for our address one field. Where we say, must (NotBeEmptyIfItIsSetOnAnEmployeeAlreadyAsync). Which in a fluent API, you typically try to write it as close to English as you possibly can and then say with message.
[00:11:35]
Address1 must not be empty and you could even be more specific, an address was already set on the employee, you make it whatever message you want. So this is a very custom piece of behavior. And believe me when I say this, example might look maybe a bit strange.
[00:11:53]
I mean, Ken you should be able to zero out an address or null out an address if it already exists. Maybe, but you will use things like this a ton as you're building APIs and you're validating them. If we try to run our tests, they will break. And the reason that they will break is that, well, I'll show you.
[00:12:11]
If we did everything correctly we should see that, yep we have a problem, which is that the dependency injection system cannot find our HttpContextAccessor. That's because we need to register it manually, which is fine. I actually think that you used to have to register it manually, and then Microsoft made it so you didn't have to register it.
[00:12:32]
And then they made it so you had to register it again, not sure why, I think that's how it went. AddHttpContextAccessor.
>> Spencer Schneidenbach: pContextAccessor, hit Tab, (). And that goes through, and it actually adds in our IHttpContextAccessor. So we can get access to our HTTP context. So we can get the Id from the incoming request, so we can know what employee they're trying to modify.
[00:12:58]
So that we can validate our incoming object.
>> Spencer Schneidenbach: So boom, boom, now we rerun our tests. Ctrl + Shift +tilde dotnet test, we get everything passing, which is exactly what we want, okay. Lastly, we can update our tests to say that yes, when address is set, we can have a test out there that says, if the address on that employee is set, if we wanted to test that behavior.
[00:13:27]
Because it's really mission critical, cuz again, limited time and resources, we can't test every single thing. We do wanna test this, so we say, for this employee, we'll say employeeId, what does this say? Doesn't exist, all right, we'll just say 1, invalidEmployee. And we'll say, if this employee already has an address that we want to make sure that we're getting a bad request back.
[00:13:55]
Let me just make sure, because of the way we've set up our repo, we should see that our test will fail.
>> Spencer Schneidenbach: And then once we add in our address, we've already defined an address, and we don't wanna be able to zero it out, still failing, let's see.
[00:14:10]
And the reason is, is we already have some employees in our repo. So we actually need to capture this employee's employeeId, which we will do. Go create a new variable employee capture it here.
>> Spencer Schneidenbach: And then we will say employeeIdForAddressTest, will get repo.GetAll().First().Id. And then we will create this private field here, and then we will copy it down here, add it here.
[00:14:52]
And now we should see because we already have an address on our employee. [LAUGH] That's the way it goes sometimes. I didn't copy this other request, which actually does have a valid. Okay, cut all that. It's because this test was failing, not this test, okay, that makes total sense.
[00:15:11]
So we'll copy these here, we'll hit Tab + Over. We'll see that we make our valid PUT request valid by making sure that we add our address1 here, 123 Main St. to make sure that this becomes a valid request. We will run all of our tests again, and now we see that they pass.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops