Getting Rid of Routine While Advanced ASP.NET Core Web API Applications Development

There are a lot of articles telling us how a good REST API should behave. Usually they go with samples. But that samples are too simplified (or I haven’t found any as detailed as I need). How to support multiple nested properties filtering, how to make it possible to filter by property using the “equals”, “greater than” , “less than”, “starts with”, “ends with”, “contains”, and other operators, how to support multiple nested properties sorting with different directions, what about paging, how to include child objects of arbitrary nesting levels? And what syntax should be chosen for all of that? What HTTP response codes and headers should be returned? And some other questions.

I’ve spent some time trying to find the answers and the best solutions to use them in my work. I think that GraphQL or OData are great but too complex for most cases, so usually I prefer to go with a regular ASP.NET Core web API application in my work. Does it mean that I have to write everything myself again and again? (No!)

I realized that I find the following filtering syntax the best:

?category.name.contains=pizza&price.from=100&price.to=200

It allows to describe relatively complex conditions but still looks clear and simple for humans. It doesn’t allow such more advanced things like conditions grouping, “or” logical operator (but as you will see later, both of them are possible to implement using my solution), so it is like compromise between flexibility and simplicity.

For sorting I’ve chosen this syntax:

?sorting=-category.name,+price

(This would sort by category name ascending, and then by price descending.)

Paging (or pagination?) is simple:

?offset=0&limit=10

And I prefer following way to specify which child objects must be included:

?fields=category,ingredients.photo

If we have few domain models (and few entities) we could write some if-else statements to cover all the condition combinations, but what if there are 20–30 (or more) models and they all are related to each other? For example, you could want to find all the orders that contains products with the specific ingredient name (“salami” for example). Or all the customers that ordered something with salami. What I want to say is that there are too many combinations for such a manual work. You could implement only some of them which are required at the moment, but in this case you will have to add them again and again later as long as you develop your project. So, obviously that must be automated.

Filtering

Thanks to ASP.NET Core model binding system it is easy to combine URL parameters into a filter object like this:

public class ProductFilter
{
public IntegerFilter Id { get; set; }
public CategoryFilter Category { get; set; }
public StringFilter Name { get; set; }
public DecimalFilter Price { get; set; }
public DateTimeFilter Created { get; set; }
}

As you can see, product filter contains category filter and so on, so you can build as deep filter graphs as you need. Filter classes allow to specify which properties can be used for filtering. (So filtering works only using the properties you want.) Sometimes you might need to set some filter properties in controller action to limit API users access. For example, for multitenant applications you can initialize TenantId from the provided JWT token, so user won’t be able to access other’s data.

Instead of using types like string or decimal I use corresponding StringFilter and DecimalFilter ones. This way I can specify value comparison type:

public class DecimalFilter
{
public decimal? Equals { get; set; }
public decimal? From { get; set; }
public decimal? To { get; set; }
}

I only need to add FromQuery attribute to a controller action parameter to tell model binding system to do the rest.

While I prefer 3-layer architecture, there is no need to have different filters for different layers, so I share them between the layers.

The most difficult task was how to transform a filter object into a corresponding SQL query. Sometimes I use Dapper as ORM, but mostly it is Entity Framework. There is a very helpful System.Linq.Dynamic.Core NuGet package. It allows to build LINQ queries from text. I use it.

I wrote the ApplyFiltering generic extension method for IQueryable<TEntity> that gets a filter and translates it into the corresponding LINQ “where” clauses using reflection. (I don’t like that code at the moment, I definitely need to refactor it.)

While LINQ query is constructed using reflection, it is important (at least in current implementation) that filter property names match entity ones. But sometimes we might want to change property path and to specify it manually. Also, we might need to filter by an object in a child collection (for example, filter orders by a product or by a product ingredient). I added the FilterShortcutAttribute for that. It can be used like this:

[FilterShortcut("Products[]")]
public ProductFilter Product { get; set; }
[FilterShortcut("Products[].ProductIngredients[].Ingredient")]
public IngredientFilter Ingredient { get; set; }

In this case entity must have corresponding navigation properties. I would like to use expressions to build property paths, but attribute properties don’t support such complex types.

Sorting

Obviously, we need ability to sort by any property or sub property (or by few of them) and to specify sorting directions. I fixed it using ApplySorting generic extension method for IQueryable<TEntity>. (Now it supports only 3 properties at the same time, but I will rewrite it later.) It uses System.Linq.Dynamic.Core too, I only replace “-” and “+” chars with “DESC” and “ASC” respectively.

Paging

Paging is simple thing. I’ve used LINQ Skip and Take methods for that in my ApplyPaging method.

Children Inclusions

This is one of the most useful features for the REST API GET methods. It is important to make as few HTTP requests as possible. And usually it is much more efficient to get, for example, products with categories with a single query using the JOIN SQL operator on the database side too.

I wrote the ApplyInclusions method, another one extension method for the IQueryable<TEntity>. It receives array of inclusions described by the Inclusion<TEntity> class. This class allows to specify property paths using either an expression or a string. As I get that property paths from an API user (and while Entity Framework needs them to have correct case), I use the AutofixInclusion method to check and fix all of them.

Validation

I really enjoy FluentValidation project. It makes validation very simple and clear. So I use it to validate DTOs before controller actions receive them and to validate models inside the services. I only modified the standard model state output a little to have a better errors handling (from my point of view). (And also I prefer to modify validation error messages either to not include property names, because it is obvious from the validation output which property it is about.)

Usually I use the same validator for POST and PUT requests. FluentValidation allows to have optional rule sets, so it makes it possible to validate IDs only on PUT requests for example.

Controllers, Services, and Repositories

It is important to have everything working automatically and still to have possibility to use manually written code where needed.

I use ASP.NET Core MVC built-in feature to have generic controllers. (There is a great post about it.) I’ve described a special Magicalized attribute to mark DTOs that needs to have the automatically generated controllers. It allows to specify base path for the API resource endpoint. (For example, “/v1/products”.)

I enumerate all the DTOs marked with this attribute and create generic controllers for them using the standard IApplicationFeatureProvider<ControllerFeature> interface implementation and corresponding routes using the IControllerModelConvention interface implementation. So it is possible to manually write controllers.

GET action returns current pagination settings and total number of records using headers and POST action returns created objects (including IDs generated at the database side). Also I use built-in JsonPatchDocument<T> to support PATCH requests.

There is also the AuthorizationRule attribute (applied to DTOs) which can be used to specify authorization policy name that needs to be validated in order to process request with the specified HTTP method. It is possible to provide few of them at the same time. (Usually I prefer to have different authorization policies for different HTTP methods.)

There is the ServiceResolver class which returns whether the generic service or a manually created (and registered inside DI) one for a given model type. It can be used in the manually created controllers, or needed services can be injected directly.

The same with repositories (using the GetRepository<TKey, TEntity, TFilter>() of the IStorage interface). But in case of repositories it is enough just to put concrete IRepository<TKey, TEntity, TFilter> implementation anywhere in your solution and it will be resolved and used automatically (thanks to the underlying ExtCore framework).

Put It All Together

I hope that ideas from this article will save your time and will make your REST API apps better. At some moment working on the sample project for it I realized that I could turn it into the independent project and we could just use it in our team. So, I created Magicalizer. I think this funny name works well for this project where combination of generics and reflection sometimes looks like a real magic.

Currently it is only a version 1.0.0-alpha1, so the codebase is not perfect yet (it is far from being perfect). I decided to release the project as early as possible in order to jointly improve it and receive feedback. Thank you for reading.

Written by

Senior software engineer, technical lead, system architect. Owner and CEO at UBRAINIANS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store