Hey there! I’m gonna write a series of articles to present the features of the library I started to work on some time ago for Asp.Net Core. I started this library out of the necessity of writing a controller as simple as possible but to have CRUD operations implemented out of the box. More than that, I wanted it to have pagination, filtering and sorting. After lots of trial and error, I came up with a library that I perfected along the time and can be found on Nuget.
Contributions to this project are also welcome, so I encourage you to visit this repository hosted with ๐ on Gitlab.
In this article I’m going to show you how to build a REST API that supports CRUD out of the box. It’s very easy to do, but we need to go through some requirements first, so please be patient in the beginning, the rest of it will flow naturally. You can also refer to the project here, it will serve as the codebase for the tutorials I’ll be writing.
Requirements
You’ll need to have a working ASP.NET Core 3.0 project in your solution. The Mindgaze.AspNetCore library makes heavy use of AutoMapper and Entityframework Core, so you’ll need to install these versions (as of 1.5.2 version):
- dotnet add package AutoMapper -v 9.0.0
- dotnet add package Microsoft.EntityFrameworkCore -v 3.0.1
- dotnet add package Mindgaze.AspNetCore -v 1.5.2
Next we need to make sure we have a couple of additional things working in our project:
- Database models
- DTO models
- A working DbContext (with migrations and stuff)
- Automapper profile that defines mappings between DTO and database models
It is necessary to have a DTO for each model that a controller will be working with; you’ll notice as we go through this tutorial. Make sure mappings are defined both ways DTO <-> Model, otherwise your API calls will crash.
Creating the model and database
Let’s define the models we will be using, with a code-first approach. I will define 2 models derived from a base class (along the road this will be useful for me to show you something cool).
public class ProductModel : ModelEntity<int>
{
public string Name { get; set; }
public float Price { get; set; }
public uint Quantity { get; set; }
}
public class KitchenProductModel : ProductModel
{
public short PowerLevel { get; set; }
}
public class BathroomProductModel : ProductModel
{
public bool CleansLimescale { get; set; }
}
Nothing too fancy here, plain C# classes, except that the ProductModel class is derived from ModelEntity<T>. T is referring to the type of the ID key of the entity, which has to be IEquatable (types like int, long, string and Guid are accepted). You don’t have to necessarily derive from ModelEntity class, but it’s encouraged to keep a clean structure in your app.
Moving on to the database context, you can find it below:
public class AspNetCoreDbContext : DbContext
{
public AspNetCoreDbContext(DbContextOptions<AspNetCoreDbContext> options) : base(options)
{
}
public DbSet<BathroomProductModel> BathroomProducts { get; set; }
public DbSet<KitchenProductModel> KitchenProducts { get; set; }
}
DTOs and mappings
Now that we have the database in place (remember to setup connection strings and migrations), let’s move to defining DTOs and mappings for AutoMapper. The DTOs will look the same as our models in our simple case:
public class ProductDto : ModelDto<int>
{
public string Name { get; set; }
public float Price { get; set; }
public uint Quantity { get; set; }
}
public class KitchenProductDto : ProductDto
{
public short PowerLevel { get; set; }
}
public class BathroomProductDto : ProductDto
{
public bool CleansLimescale { get; set; }
}
In this case everything is the same as the models, except that we derive from ModelDto, which needs to have the same type as defined in the model. In more complex scenarios, you may need to alter the DTOs with different properties ๐
It’s now time to have those mappings defined. They look like this:
public class DefaultProfile : Profile
{
public DefaultProfile()
{
CreateMap<ProductDto, ProductModel>()
.IncludeAllDerived()
.ReverseMap()
.IncludeAllDerived()
;
CreateMap<KitchenProductDto, KitchenProductModel>()
.ReverseMap()
;
CreateMap<BathroomProductDto, BathroomProductModel>()
.ReverseMap()
;
}
}
Remember you can always see here the full setup I used when writing this, I won’t paste all the code here for brevity.
The delight: CRUD controller
Ok, let’s now add the mighty CRUD controller. In your Ccontrollers folder, add a BathroomProductsController like this:
public class BathroomProductsController : EntityController<BathroomProductModel, int, AspNetCoreDbContext, BathroomProductDto>
{
public BathroomProductsController(AspNetCoreDbContext dbContext, IMapper mapper) : base(dbContext, mapper)
{
}
protected override Func<AspNetCoreDbContext, DbSet<BathroomProductModel>> EntityDbSetPropertyFunc => throw new NotImplementedException();
protected override Func<BathroomProductModel, int> EntityGetKeyFunc => throw new NotImplementedException();
protected override Action<BathroomProductModel, int> EntitySetKeyAction => throw new NotImplementedException();
protected override Expression<Func<BathroomProductModel, bool>> GenerateKeyEqualityExpression(int key)
{
throw new NotImplementedException();
}
}
It’s not much, but we need to understand those members we need to implement in order to make it work:
- EntityDbSetPropertyFunc – the property from the DbContext that points to your DbSet
- EntityGetKeyFunc – an expression that returns the Id value of the entity
- EntitySetKeyAction – an expression that sets the Id value of the entity
- GenerateKeyEqualityExpression(int key) – returns an expression that represents the Id equality with the key parameter
As these may be confusing, maybe the code that implements those is more explanatory:
protected override Func<AspNetCoreDbContext, DbSet<BathroomProductModel>> EntityDbSetPropertyFunc => db => db.BathroomProducts;
protected override Func<BathroomProductModel, int> EntityGetKeyFunc => e => e.Id;
protected override Action<BathroomProductModel, int> EntitySetKeyAction => (e, key) => e.Id = key;
protected override Expression<Func<BathroomProductModel, bool>> GenerateKeyEqualityExpression(int key)
{
return e => e.Id == key;
}
Looks pretty cool, not that ugly but if it will deliver the requested CRUD functionality, it’s great ๐ At the time of this writing, I didn’t check it, so it’s now time to run it and see if it’s working. Fingers crossed!
Of course it’s not working, I needed to add the [Route(“[controller]”)] so that the route can be found ๐
Testing the operations
Lets call the API by making a GET request to the bathroomProducts endpoint. As we can see in the below Postman screenshot, the request is successful and returns no items:
As we have no data, lets create some entities:
For this we send a POST request which will return the created entity with an assigned id and a 201 response code.
If we try to get the entities, we can see they are returned correctly, also with pagination support (we’ll talk about that in more detail later on):
What’s next?
Did I forget about something? Yes, PUT and DELETE requests. Below you have a snapshot of a PUT request that updates the entity:
On the DELETE side, you have two options:
- Send a delete request to bathroomProducts/{id}
- Send a delete request to bathroomProducts and add the full entity (with id) in the body
In both of the cases you’ll receive a 204 (No Content) status code ๐
Conclusion
Hope you had time and patience to track the article top-bottom. It might be quite long but I hope it will worth your time. The article explains how to create a controller which supports CRUD out of the box with a bit of preparation beforehand.
More than that, the Mindgaze.AspNetCore library supports pagination, filtering, sorting but also JSONPatch, all of which will be explained in future articles. I have added features for Swagger, integration testing or email sending, features I believe are very helpful. Because I developed them to help me in my applications, I believe they will also help you in yours ๐
Allright, that’s quite about it for now, I will take my time to write posts that describe the other features so that you’ll have guidance with this library. Cheers and good luck; don’t hesitate to contact me if you have any trouble!
Thanks for reading, I hope you found this article useful and interesting. If you have any suggestions donโt hesitate toย contact me. If you found my content useful please consider a small donation. Any support is greatly appreciated! Cheersย ย ๐