1. A short introduction to how to build Web Applications
REST API is a well-known approach to designing and structuring Web Applications. REST is not a protocol or framework it is a set of concepts. Although there are other approaches i.e. it could be used stateful services with one HTTP method verb (SOAP) or use specific full duplex GRPC or WebSockets connection. REST is a very popular approach: it allows us to use the same methodology for different resources (which usually maps on persistent objects, entities). As engineers, we try to optimize our work: we attempt not to repeat ourselves (DRY), simplify (KISS) and so on. Therefore, today i, as a part of the Wissance
team, would like to share yet another boilerplate to write REST API easily see our WebAPI Toolkit.
2. What is REST
REST is a set of concepts that allows us to build a robust API (for example see this article).
REST operates with the term "resource" which usually has persistence nature i.e. mapped to database table and so on. Let's see if we have a user as a resource.
In REST we have separate HTTP methods to perform different operations:
- CREATE this operation implements with POST HTTP method to resource i.e.
POST {base_url}/api/resource
(i.e. if {base_url} is http://10.0.0.10/myservice and resource isuser
full uri could look like: http://10.0.0.10/myservice/api/user) During creation we are passing user data as DTO in request body and getting back created user with set identifier and other generated on server fields. - READ this operation impelements with GET HTTP method and it usually contains 2 independent endpoints to get collection of entity items and to get one by id i.e. for
user
resource:-
GET {base_url}/api/user
to get a collection -
GET {base_url}/api/user/{id}
to get a single item by {id}
-
- UPDATE this operation implements with PUT HTTP method (in this article PATCH method don't be considered). During a update we are passing resource as DTO through request body i.e. for user resource via url
PUT {base_url}/api/user/{id}
- DELETE this operation implements with DELETE HTTP method i.e for user resource via url
DELETE {base_url}/api/user/{id}
Implementing REST API Service one by one i finally understood what could be made common. We created open source library with this coomon code and it helps us ti significantly reduce amount of any project code. This is our (Wissance LLC) project WebApiToolkit
3. Our approach and concepts
First of all we have following REST application architecture concepts:
- All resources are storing in Entity classes that have required property Id (
IModelIdentifiable<T>
interface in our repo). - Controllers are just
facades
with some preliminary input data processing and validation, all application logic is implementing in appropriateManager
classes (Those we could use the same Managers for i.e.Signalr
too not only for a REST API).Manager
is an entry point to all complicated operation i.e. integration with other Web services and applications, with persistent storages likeDatabases
andQueues
and many others and so on is carrying on Manager classes. Controllers could only set Http Status codes and other things like setting some HTTP headers. - End user interacts with Web API using DTO which could have differences in number of fields in comparison with Entity i.e. we don't like to expose some fields or we do calculation for database fields during processing DTO by Manager.
- We have Factory classes that allow to get DTO from Entity and vise versa.
- All DTO classes are isolated from Entities and others application projects into separate project/assembly this allows us to easily distribute data contract as set of DTO as a Nuget package or directly by Add assembly manually to project.
We analyzed all OUR Net Core projects with REST API and created package with common code. If you like our package please don't forget to give us a star, it is important for us.
4. Let's see how it works in details
Lets consider that we are having a Weather station REST application. A full working code example is here, please don't forget to give a star to a this project too. In this application we have 2 REST resources that associated with 2 database tables:
- weather station (meta info about where measurements were taken)
- measurements (a set of weather control values: temperature, humidity, atmosphere pressure, e.t.c.) with set of sensors and sensors itself. Lets see how our controllers looks like and what could us give the
Wissance.WebToolkit
.
4.1 Controllers
In our Toolkit we have 2 base Controller classes:
- For read-only resources -
BasicReadController
; - For resources that could be changed or deleted (Full CRUD) -
BasicCrudController
; In mentioned upper demo project we usedBasicCrudController
as a base class forWeather station
andMeasurements
:
namespace Wissance.WeatherControl.WebApi.Controllers
{
[ApiController]
public class StationController : BasicCrudController<StationDto, StationEntity, int>
{
public StationController(StationManager manager)
{
Manager = manager; // this is for basic operations
_manager = manager; // this for extended operations
}
private StationManager _manager;
}
}
As a result we have got:
- Lightweight controllers, less code as it is possible;
- We don't care about method declaration, routing, status codes processing;
- Create and Update operations wraps response in
OperationResultDto
which allow to pass message what was wrong; - Get resource collection allowes to use pagination
- We could use different identitifer type (int, string, guid) becuse it passed to controller class as a generic type.
This controller expose 5 endpoints:
-
GET ~/api/station/[?page={page}&size={size}]
to get collection of station wherepage
andsize
are optional parameters if they not provided default values is used (page=1, size=25); -
GET ~/api/station/{id}
to get one station by {id}; -
POST ~/api/station/
to create new station; -
PUT ~/api/station/{id}
to update station with id = {id}; -
DELETE ~/api/station/{id}
to delete station with id = {id};
4.2 Managers
In our library we have base abstract class ModelManager
that implements IModelManager
interface. To work with BaseCrudController
we should use Manager classes (they should implement IModelManager interface) i.e. StationManager looks like:
public class StationManager : ModelManager<StationEntity, StationDto, int>
{
public StationManager(ModelContext modelContext, ILoggerFactory loggerFactory) : base(loggerFactory)
{
_modelContext = modelContext;
}
public override async Task<OperationResultDto<IList<StationDto>>> GetAsync(int page, int size)
{
return await GetAsync<int>(_modelContext.Stations, page, size, null, null, StationFactory.Create);
}
public override async Task<OperationResultDto<StationDto>> GetByIdAsync(int id)
{
return await GetAsync(_modelContext.Stations, id, StationFactory.Create);
}
public override async Task<OperationResultDto<StationDto>> CreateAsync(StationDto data)
{
try
{
StationEntity entity = StationFactory.Create(data);
await _modelContext.Stations.AddAsync(entity);
int result = await _modelContext.SaveChangesAsync();
if (result >= 0)
{
return new OperationResultDto<StationDto>(true, (int)HttpStatusCode.Created, null, StationFactory.Create(entity));
}
return new OperationResultDto<StationDto>(false, (int)HttpStatusCode.InternalServerError, "An unknown error occurred during station creation", null);
}
catch (Exception e)
{
return new OperationResultDto<StationDto>(false, (int)HttpStatusCode.InternalServerError, $"An error occurred during station creation: {e.Message}", null);
}
}
public override async Task<OperationResultDto<StationDto>> UpdateAsync(int id, StationDto data)
{
try
{
StationEntity entity = StationFactory.Create(data);
StationEntity existingEntity = await _modelContext.Stations.FirstOrDefaultAsync(s => s.Id == id);
if (existingEntity == null)
{
return new OperationResultDto<StationDto>(false, (int)HttpStatusCode.NotFound, $"Station with id: {id} does not exists", null);
}
// Copy only name, description and positions, create measurements if necessary from MeasurementsManager
existingEntity.Name = entity.Name;
existingEntity.Description = existingEntity.Description;
existingEntity.Latitude = existingEntity.Latitude;
existingEntity.Longitude = existingEntity.Longitude;
int result = await _modelContext.SaveChangesAsync();
if (result >= 0)
{
return new OperationResultDto<StationDto>(true, (int)HttpStatusCode.OK, null, StationFactory.Create(entity));
}
return new OperationResultDto<StationDto>(false, (int)HttpStatusCode.InternalServerError, "An unknown error occurred during station update", null);
}
catch (Exception e)
{
return new OperationResultDto<StationDto>(false, (int)HttpStatusCode.InternalServerError, $"An error occurred during station update: {e.Message}", null);
}
}
public override async Task<OperationResultDto<bool>> DeleteAsync(int id)
{
return await DeleteAsync(_modelContext, _modelContext.Stations, id);
}
private readonly ModelContext _modelContext;
}
it is not small as Controller class but it still quite small. We also should mention that we have in ModelManager
abstract classes following additional methods:
public async Task<OperationResultDto<TRes>> GetAsync(DbSet<TObj> dbSet, TId id, Func<TObj, TRes> createFunc)
public async Task<OperationResultDto<IList<TRes>>> GetAsync<TKey>(DbSet<TObj> dbSet, int page, int size, Func<TObj, bool> filter, Func<TObj, TKey> sort, Func<TObj, TRes> createFunc)
- public async Task> DeleteAsync(DbContext context, DbSet dbSet, TId id)
Methods that we mentioned above did not used as a IModelContext methods implementations for one particular reason - we cannot implement univesal authorization to perform all sets of operations in the abstract class due to real cases could very complicated, therefore additional methods were created to help us to implement all required operations in any custom manager class like StationManager
.
ModelManager abstract class additional features:
- collection of objects could be filtered using delegate param -
Func<TObj, bool> filter
ofGetAsync
method; - collection of objects could be sorted using delegate param - Func sorted of
GetAsync
method.
We still have what to do (this is not the final version) and in one future release there will be a possibility to pass filter & sort params to appropriate controller and manager methods.
5. Additional Wissance.WebToolkit features
In our example project we did not consider following important things and we are trying to describe them here briefly (if this articles get 50 likes we will make second chapter):
- authentication and authorization;
- additional controller methods that expands controller interface over a just CRUD controller.
5.1 Authentication and authorization
We could use IHttpContextAccessor _httpContext
to check whether user has access to resource or not, we could get Claim value like this where claimTypeId
is a const like Username and so on:
private string GetClaim(string claimTypeId)
{
ClaimsPrincipal principal = _httpContext.HttpContext.User;
Claim claim = principal.Claims.FirstOrDefault(c => c.Type == claimTypeId);
if (claim == null)
return null;
return claim.Value;
}
Don't forget to inject IHttpContextAccessor:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
5.2 Additional controller methods
There is nothing complicated about extending controllers, if i.e. we have to search weather station in pseudo code we should add in controller something like this:
[HttpGet]
[Route("api/[controller]/search")]
public async Task<IList<StationDto>> SearchInTaxServiceAsync([FromQuery]string query, [FromQuery]int page)
{
IList<StationDto> result = await _manager.SearchAsync(query, page);
if (result == null)
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
return result;
}
6. Conclusion
Thank for reading bro, please rise you star on our github projects (WebApiToolkit
&& WeatherControl
) if you find that they are helpful for you. As a conclusion we also would like to say just a fact: we significantly reduce amount of code and copy-paste code using our WebApiToolkit
.
Top comments (0)