In this video, Nate explains how to harness the power of ASP.NET Core MVC's built-in validation functionality to ensure that the sort parameters provided by the client are correct.
- [Instructor] When a request comes in to the API, ASP.NET Core MVC will bind any OrderBy parameters from the query string to the OrderBy property. Because the stored options model implements the IValidatableObject interface, MVC will automatically call the Validate method. We can validate the sort terms here and let MVC know if the parameters passed by the client were valid. To keep the code clean, we'll delegate this to a new class called SortOptionsProcessor.
This also needs to be declared as T and TEntity, and we'll pass in the OrderBy parameter. Now let's go create SortOptionsProcessor. We'll create this in the Infrastructure folder. This will be SortOptionsProcessor(T,TEntity). This class will hold a private reference to the orderBy string array, and it'll except that parameter in the constructor.
First we'll declare a method that returns an IEnumerable of a new class called SortTerm, and we'll call this GetAllTerms(). Before we continue, let's create that SortTerm class. This will also be in Infrastructure, and each term will be represented by a simple class that has a string name and a boolean that represents whether the term should be Descending or not. All right, back to GetAllTerms. We can do a quick sanity check to make sure that orderBy is not a null array.
If it is, we'll just yield break, and return an empty enumerable. If it's not null, we'll use a foreach to loop over every term in the orderBy parameters. If any particular term is null or empty, we'll just continue and skip over it. Each term needs to be split into a pair of tokens on the space character. If there is no space character, then we can simply yield return a new SortTerm, with the name set to the term string, and then we'll continue and move on in the loop.
If there is one or more tokens separated by a space, we need to check whether the second token is the character's d-e-s-c for descending. So we'll do a check here. Make sure tokens.Length > 1, and then that the second token is equal to d-e-s-c, and we'll make sure that this is a case insensitive comparison, we'll ignore case, and then we'll yield return a new SortTerm with the Name as the first token and Descending set to the value of that check.
This code will return all of the sort terms the client specified in the query string, however, not all of these will be valid sortable fields. We need to cross reference it with the fields that have the Sortable attribute on the model. We can get a list of those with reflection. I'm going to declare a new method which is private static. It will also return an IEnumerable of SortTerm, and this will be called GetTermsFromModel(). This will use reflection to look at the type of the model, GetTypeInfo(), which we need to use System.Reflection for, and then we'll look at the DeclaredProperties on that model.
We'll select any properties that have a custom attribute of type SortableAttribute, and we'll need to import link, should be GetCustomAttributes, and we'll do Select each one to a new SortTerm where the Name is the property name. Now we'll use this list of approved terms from the model, to create a list of valid terms for the current request. I'll declare one more method, which is public, and also returns an IEnumerable of SortTerm called GetValidTerms().
Inside the GetValidTerms method, we need to get all the terms from the current query string, so we'll say queryTerms = GetAllTerms, and put that into an array. If there aren't any query terms, we can just yield break and leave the method immediately. Next, we need to get a set of approved terms from the model. So we'll get the declaredTerms using GetTermsFromModel(). Now we'll use a foreach loop to iterate over all of the query terms.
For each term in the current request, we need to check to see if it's also a valid declaredTerm on the model. So we'll say, declaredTerm = declaredTerms.SingleOrDefault() where x.Name.Equals(term.Name), and we'll make sure that this is a case insensitive comparison as well. If that check comes back null, we'll just skip this one and continue, it's not a valid term on the model.
If it is valid, we'll return a new SortTerm, with the Name set to the name of the declaredTerm, and the Descending value set to the original descending value. All right, let's go back to the SortOptions class. In order to use SortOptionsProcessor, we need to import the Infrastructure namespace. Now that we have the processor that can do all the heavy lifting for us, let's simply get the validTerms from the processor, and select these by Name.
We can also make a list of invalidTerms, by getting all of the terms from the processor, selecting those by Name, and then excluding the validTerms in a case insensitive way, StringComparer, not Comparison. Now we can use a foreach method to return a validation result for every invalidTerm. We'll say, foreach term in invalidTerms, we'll yield return a new ValidationResult, which is the return of this method, with the text Invalid sort term, and the name of the term.
We also have to pass the name of the query string parameter that failed validation. In this case, it's the OrderBy parameter. So we'll just say the nameof(OrderBy). All right, we have one more step. Over in the Rooms model, we need to import the Infrastructure namespace in order to use this SortableAttribute. Okay, let's try out our code. I'll compose a request with Postman to the Rooms route, and I'll pass the orderBy parameter with the invalidTerm foo.
I should get an error back saying that this term is invalid. All right, let's try a valid term like rate. If I send a request with orderBy=rate, I get an error message, but it's not because validation failed, this error is because we haven't implemented the Apply method yet. We will implement the Apply method next to dynamically add those sort terms to the database query.
- REST vs. RPC
- Using HTTP methods (aka verbs)
- Returning JSON
- Creating a new API project
- Building a root controller
- Routing to controllers with templates
- Requiring HTTPS for security
- Creating resources and data models
- Returning data and resources from a controller
- Representing links (HREFs)
- Representing collections
- Sorting and searching collections
- Creating forms
- Caching and compression
- Authentication and authorization for RESTful APIs