In this video, learn how to harness the power of ASP.NET Core MVC's built-in validation functionality to ensure that the search parameters provided by the client are correct.
- [Instructor] Any search parameters in the query string of incoming requests will be bound to the search array. In the Validate method, we can let ASP.NET Core know whether the parameters the client gave us are valid search terms. To keep this class clean, we'll delegate to a new class called SearchOptionsProcessor. The SearchOptionsProcessor will also need to be bound to the Entity type and the Resource type and will pass in the search terms the client gave us. Now, let's go create that SearchOptionsProcessor in the Infrastructure folder, call this SearchOptionsProcessor T TEntity, and this class will have a private reference to the string array of searchQuery terms, which it will accept in the constructor.
The first method we need to write is a GetAllTerms method. This will return an IEnumerable of SearchTerm, which is a new class we need to create, so let's go create that now. This also go in the Infrastructure folder. The SearchTerm class will represent a search term that's been deconstructed. So that means it'll have a Name, an Operator, and a Value. I'll also add flag that represents whether the syntax was valid.
Back in the SearchOptionsProcessor, we can implement the GetAllTerms method. If the client didn't give us any search terms, we can imply yield break and leave the method; otherwise, we'll use a foreach to loop over all the expressions in the searchQuery. If a particular expression is null or empty, we can skip it. For each expression, we'll split it up into a set of tokens using Split and splitting on the space character.
Each expression will look something like this. There will be a field, an operator, and a value. If their aren't any tokens left over after we split on the space character, we can just return an invalid result. So we'll return a new SearchTerm object with ValidSyntax set to false and name set to the entire expression for error reporting purposes. And then, we need to make sure that we continue to skip to the next iteration of the loop.
If there are some tokens, but not enough of them, in other words, less than three of them, we'll do the same. In this case, we'll set the name to the first token, which should be the field name. If we've gotten this far, we know that we do have valid syntax. So return a new SearchTerm with ValidSyntax set to true, and then the Name and other properties deconstructed from the tokens. The Operator will be the second token, and the Value will be all the remaining tokens rejoined with the space character, minus the first two.
This method will return a list of all the search terms the client specified in the query string. However, not all of those will necessarily be valid searchable fields on the model. We need to cross reference it with the fields that do have the searchable attribute. We can get a list of those with reflection. I'm going to collapse this method for now. This new method will be private static, it will also return an IEnumerable of SearchTerm, and it will be called GetTermsFromModel. In this case, we're going to use reflection to look at the type of the resource, and we'll use GetTypeInfo from system.reflection to get the DeclaredProperties property.
We'll filter this Where, the property has a custom attribute of type SearchableAttribute, and from those, we'll select or project them each to a new search term, with the name being the property name. Now, we'll use this list of declared or approved terms from the model itself to create a list of valid terms for the current request. So we'll create one more IEnumerable of SearchTerm method, which is called GetValidTerms.
And in the GetValidTerms method, the first thing we need to do is pull all of the terms from the query using GetAllTerms. But now, we can filter that on making sure that each one as ValidSyntax, and we'll put all those in an array. If there aren't any with ValidSyntax, we'll simply yield break and return an empty enumerable. If there's at least one, we need to get all the declared terms from the model, and then, we'll iterate over each term from the query.
For each term in the query, I'll try to cross-reference it with a declaredTerm from the model. I'll use SingleOrDefault for the Name Equals the term Name in a case-insensitive StringComparison. If we didn't find a match, in other words, declaredTerm is null, we'll simply continue and skip this one; otherwise, we'll yield return a new SearchTerm, where we can project all the original values from the term, with the exception of Name, which we wanna pull from the declaredTerm.
All right, let's switch back to search options. We'll need to import the Infrastructure namespace to get access to this new class we created. And using the SearchOptionsProcessor, we can get a list of validTerms, and then, project those to select their names. We can also get a list of invalidTerms by getting all the terms in the query, selecting those by name, and then, excluding the validTerms.
We'll make sure that the case comparison is IgnoreCase, and we wanna do this so we can return to ASP.NET Core a list of the invalidTerms for creating an error message. So we'll loop over each term in the invalidTerms list, and for each one, we'll yield return a new ValidationResult, which is the return type of this method, and that ValidationResult will carry an error message that will be returned to the client. So we'll say Invalid search term, and we'll reflect back the name of the term.
And we also need to pass the name of the parameter from the query string, so that's simply the name of Search. All right, let's test out what we have so far using Postman. We've annotated the room model with some searchable attributes. So let's try hitting slash rooms, and we'll add a search term, but for an invalid attribute, so we'll say maybe, foo equals bar. We should get back an error that says that foo is an invalid search term because there is no foo property on the room model with a searchable attribute.
However, we did mark the room name as being searchable. So let's try name equals Oxford Suite. Now, we get an error, but it's a different error. This error is because we haven't implemented the apply method yet. So that means that at least the search term validation code is running properly. Next, we'll implement that apply method and dynamically add the search criteria from the query string into the database query.
- What is RESTful design?
- Building a new API with ASP.NET Core
- Using HTTP methods
- Returning JSON
- Creating RESTful routing with templates
- Securing RESTful APIs with HTTPS
- Representing resources
- Representing links
- Representing collections
- Sorting and searching collections
- Building forms
- Adding caching to an ASP.NET Core API
- Configuring user authentication and authorization