Wissance.WebApiToolkit
This lib helps to build REST API with C# and AspNet easily than writing it from scratch over and over in different projects.
1. Key Features
-
REST API Controllerwith fullCRUDcontains only 20 lines of code (~ 10 are imports)-
GETmethods have built-in paging support; -
GETmethods have built-in sorting and filter by query params (coming soon, MORE STARS -> sooner RELEASE);
-
- support to work with any persistent storage (
IModelManagerinterface); Good built-in EntityFramework support (seeEfModelManagerclass). See WeatherControl App which has 2 WEB API projects:-
Wissance.WeatherControl.WebApiusesEntityFramework; -
Wissance.WeatherControl.WebApi.V2usesEdgeDb.
-
Key concepts:
-
Controlleris a class that handlesHTTP-requeststoREST Resource. -
REST Resourceis equal toEntity class / Database Table - Every operation on
REST ResourceproduceJSONwithDTOas output. We ASSUME to use only oneDTOclass with allRESTmethods.
2. API Contract
-
DTOclasses:-
OperationResultDtorepresents result of operation that changes Data in db; -
PagedDataDtorepresents portion (page) of same objects (any type);
-
-
Controllersclasses - abstract classes- basic read controller (
BasicReadController) contains 2 methods:-
GET /api/[controller]/?page={page}&size={size}to get PagedDataDto -
GET /api/[controller]/{id}to get one object by id
-
- full
CRUDcontroller (BasicCrudController) = basic read controller (BasicReadController) +Create,UpdateandDeleteoperations :-
POST /api/[controller]- for new object creation -
PUT /api/[controller]/{id}- for edit object by id -
DELETE /api/[controller]/{id}- for delete object by id
-
Controllers classes expects that all operation will be performed using Manager classes (each controller must have it own manager)
- basic read controller (
-
Managers classes - classes that implements business logic of application
-
IModelManager- interface that describes basic operations -
EfModelManager- is abstract class that contains implementation ofGetandDeleteoperations
-
3. Requirements
There is only ONE requirement: all Entity classes for any Persistence storage that are using with controllers & managers MUST implements IModelIdentifiable<T> from Wissance.WebApiToolkit.Data.Entity.
If this toolkit should be used with EntityFramework you should derive you resource manager from
EfModelManager it have built-in methods for:
- get many
- get one
- delete
4. Toolkit usage algorithm with EntityFramework
Full example is mentioned in section 6 (see below). But if you are starting to build new REST Resource
API you should do following:
- Create a
model(entity) class implementingIModelIdentifiable<T>andDTOclass for it representation, i.e.:
public class BookEntity : IModelIdentifiable<int>
{
public int Id {get; set;}
public string Title {get; set;}
public string Authors {get; set;} // for simplicity
public DateTimeOffset Created {get; set;}
public DateTimeOffset Updated {get; set;}
}
public class BookDto
{
public int Id {get; set;}
public string Title {get; set;}
public string Authors {get; set;}
}
- Create a factory function (i.e. static function of a static class) that converts
ModeltoDTOi.e.:
public static class BookFactory
{
public static BookDto Create(BookEntity entity)
{
return new BookDto
{
Id = entity.Id,
Title = entity.Title,
Authors = entity.Authors;
};
}
}
- Create
IModelContextinterface that has youBookEntityas a DbSet and it's implementation class that also derives from DbContext (Ef abstract class):
public interface IModelContext
{
DbSet<BookEntity> Books {get;set;}
}
public MoidelContext: DbContext<ModelContext>, IModelContext
{
// todo: not mrntioned here constructor, entity mapping and so on
public DbSet<BookEntity> Books {get; set;}
}
- Configure to inject
ModelContextas aDbContextviaDIsee Startup class - Create
Controllerclass and a manager class pair, i.e. consider here fullCRUD
[ApiController]
public class BookController : BasicCrudController<BookDto, BookEntity, int>
{
public BookController(BookManager manager)
{
Manager = manager; // this is for basic operations
_manager = manager; // this for extended operations
}
private BookManager _manager;
}
public class BookManager : EfModelManager<BookEntity, BookDto, int>
{
public BookManager(ModelContext modelContext, ILoggerFactory loggerFactory) : base(modelContext, BookFactory.Create, loggerFactory)
{
_modelContext = modelContext;
}
public override async Task<OperationResultDto<StationDto>> CreateAsync(StationDto data)
{
// todo: implement
}
public override async Task<OperationResultDto<StationDto>> UpdateAsync(int id, StationDto data)
{
// todo: implement
}
private readonly ModelContext _modelContext;
}
5. Nuget package
You could find nuget-package here
6. Examples
Here we consider only Full CRUD controllers because Full CRUD = Read Only + Additional Operations (CREATE, UPDATE, DELETE), a full example = full application created with Wissance.WebApiToolkit could be found here: https://github.com/Wissance/WeatherControl
[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;
}
public class StationManager : EfModelManager<StationEntity, StationDto, int>
{
public StationManager(ModelContext modelContext, ILoggerFactory loggerFactory) : base(modelContext, StationFactory.Create, loggerFactory)
{
_modelContext = modelContext;
}
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);
}
}
private readonly ModelContext _modelContext;
}
JUST 2 VERY SIMPLE CLASSES ^^ USING WebApiToolkit
7. Extending API
7.1 Add new methods to existing controller
Consider we would like to add method search to our controller:
[HttpGet]
[Route("api/[controller]/search")]
public async Task<PagedDataDto<BookDto>>> SearchAsync([FromQuery]string query, [FromQuery]int page, [FromQuery]int size)
{
OperationResultDto<Tuple<IList<BookDto>, long>> result = await Manager.GetAsync(page, size, query);
if (result == null)
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
HttpContext.Response.StatusCode = result.Status;
return new PagedDataDto<TRes>(pageNumber, result.Data.Item2, GetTotalPages(result.Data.Item2, pageSize), result.Data.Item1);
}
7.2 Add security to protect you API
We have additional project to protect API with Keycloak OpenId-Connect.
pass IHttpContextAccessor to Manager class and check something like this: ClaimsPrincipal principal = _httpContext.HttpContext.User;
8. Additional materials
You could see our articles about Toolkit usage:
