Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/wissance/webapitoolkit
A set of useful C# reusable classes and components that could be used with any Net Core Web application IT ALLOW TO REDUCE AMOUNT OF CODE when IMPLEMENTING REST API (see examples in README)
https://github.com/wissance/webapitoolkit
crud crud-api csharp-library csharp-web library rest-api rest-api-template rest-api-toolkit web-application webapi webapi-core
Last synced: about 1 month ago
JSON representation
A set of useful C# reusable classes and components that could be used with any Net Core Web application IT ALLOW TO REDUCE AMOUNT OF CODE when IMPLEMENTING REST API (see examples in README)
- Host: GitHub
- URL: https://github.com/wissance/webapitoolkit
- Owner: Wissance
- License: apache-2.0
- Created: 2021-12-14T10:37:32.000Z (about 3 years ago)
- Default Branch: master
- Last Pushed: 2024-10-14T04:41:55.000Z (3 months ago)
- Last Synced: 2024-12-06T10:17:18.998Z (about 1 month ago)
- Topics: crud, crud-api, csharp-library, csharp-web, library, rest-api, rest-api-template, rest-api-toolkit, web-application, webapi, webapi-core
- Language: C#
- Homepage: https://wissance.github.io/WebApiToolkit/
- Size: 2.1 MB
- Stars: 14
- Watchers: 5
- Forks: 3
- Open Issues: 22
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
## Wissance.WebApiToolkit
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/wissance/WebApiToolkit?style=plastic)
![GitHub issues](https://img.shields.io/github/issues/wissance/WebApiToolkit?style=plastic)
![GitHub Release Date](https://img.shields.io/github/release-date/wissance/WebApiToolkit)
![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/WebApiToolkit/v1.6.0/total?style=plastic)#### This lib helps to build `REST API` with `C#` and `AspNet` easily than writing it from scratch over and over in different projects.
![WebApiToolkit helps to build application easily](/img/cover.png)
### 1. Key Features
* `REST API Controller` with **full `CRUD`** contains ***only 20 lines*** of code (~ 10 are imports)
- `GET` methods have ***built-in paging*** support;
- `GET` methods have ***built-in sorting and filter*** by query parameters;
* support ***BULK operations*** with objects (Bulk `Create`, `Update` and `Delete`) on a Controller && interface level
* support to work with ***any persistent storage*** (`IModelManager` interface); Good built-in EntityFramework support (see `EfModelManager` class). See [WeatherControl App](https://github.com/Wissance/WeatherControl) which has 2 WEB API projects:
- `Wissance.WeatherControl.WebApi` uses `EntityFramework`;
- `Wissance.WeatherControl.WebApi.V2` uses `EdgeDb`.
Key concepts:
1. `Controller` is a class that handles `HTTP-requests` to `REST Resource`.
2. `REST Resource` is equal to `Entity class / Database Table`
3. Every operation on `REST Resource` produce `JSON` with `DTO` as output. We ASSUME to use only one `DTO` class with all `REST` methods.### 2. API Contract
* `DTO` classes:
- `OperationResultDto` represents result of operation that changes Data in db;
- `PagedDataDto` represents portion (page) of same objects (any type);
* `Controllers` classes - abstract classes
- basic read controller (`BasicReadController`) contains 2 methods:
- `GET /api/[controller]/?[page={page}&size={size}&sort={sort}&order={order}]` to get `PagedDataDto`
now we also have possibility to send **ANY number of query params**, you just have to pass filter func to `EfModelManager` or do it in your own way like in [WeatherControl example with edgedb](https://github.com/Wissance/WeatherControl/blob/master/WeatherControl/Wissance.WeatherControl.WebApi.V2/Helpers/EqlResolver.cs). We also pass sort (column name) && order (`asc` or `desc`) to manager classes,
`EfModelManager` allows to sort **by any column**.
Unfortunately here we have a ***ONE disadvantage*** - **we should override `Swagger` info to show query parameters usage!!!** Starting from `1.6.0` it is possible to see all parameters in `Swagger` and use them.
- `GET /api/[controller]/{id}` to get one object by `id`
- full `CRUD` controller (`BasicCrudController`) = basic read controller (`BasicReadController`) + `Create`, `Update` and `Delete` operations :
- `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
- full `CRUD` with **Bulk** operations (operations over multiple objects at once), Base class - `BasicBulkCrudController` = basic read controller (`BasicReadController`) + `BulkCreate`, `BulkUpdate` and `BulkDelete` operations:
- `POST /api/bulk/[controller]` - for new objects creation
- `PUT /api/bulk/[controller]` - for edit objects passing in a request body
- `DELETE /api/bulk/[controller]/{idList}` - for delete multiple objects by id.
Controllers classes expects that all operation will be performed using Manager classes (each controller must have it own manager)
* Managers classes - classes that implements business logic of application
- `IModelManager` - interface that describes basic operations
- `EfModelManager`- is abstract class that contains implementation of `Get` and `Delete` operations
- `EfSoftRemovableModelManager` is abstract class that contains implementation of `Get` and `Delete` operations with soft removable models (`IsDeleted = true` means model was removed)
Example of how faster Bulk vs Non-Bulk:
![Bulk vs Non Bulk](/img/bulk_performance.png)
```
Elapsed time in Non-Bulk REST API with EF is 0.9759984016418457 secs.
Elapsed time in Bulk API with EF is 0.004002094268798828 secs.
```
as a result we got almost ~`250 x` faster `API`.### 3. Requirements
There is **only ONE requirement**: all Entity classes for any Persistence storage that are using with controllers & managers MUST implements `IModelIdentifiable` 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` items
* `get one` item `by id`
* `delete` item `by id`### 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:
1. Create a `model` (`entity`) class implementing `IModelIdentifiable` and `DTO` class for it representation (**for soft remove** also **add** `IModelSoftRemovable` implementation), i.e.:
```csharp
public class BookEntity : IModelIdentifiable
{
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;}
}
```
2. Create a factory function (i.e. static function of a static class) that converts `Model` to `DTO` i.e.:
```csharp
public static class BookFactory
{
public static BookDto Create(BookEntity entity)
{
return new BookDto
{
Id = entity.Id,
Title = entity.Title,
Authors = entity.Authors;
};
}
}
```
3. Create `IModelContext` interface that has you `BookEntity` as a `DbSet` and it's implementation class that also derives from `DbContext` (**Ef abstract class**):
```csharp
public interface IModelContext
{
DbSet Books {get;set;}
}public MoidelContext: DbContext, IModelContext
{
// todo: not mrntioned here constructor, entity mapping and so on
public DbSet Books {get; set;}
}
```
4. Configure to inject `ModelContext` as a `DbContext` via `DI` see [Startup](https://github.com/Wissance/WeatherControl/blob/master/WeatherControl/Wissance.WeatherControl/Startup.cs) class
5. Create `Controller` class and a manager class pair, i.e. consider here full `CRUD`
```csharp
[ApiController]
public class BookController : BasicCrudController
{
public BookController(BookManager manager)
{
Manager = manager; // this is for basic operations
_manager = manager; // this for extended operations
}
private BookManager _manager;
}public class BookManager : EfModelManager
{
public BookManager(ModelContext modelContext, ILoggerFactory loggerFactory) : base(modelContext, BookFactory.Create, loggerFactory)
{
_modelContext = modelContext;
}
public override async Task> CreateAsync(StationDto data)
{
// todo: implement
}
public override async Task> UpdateAsync(int id, StationDto data)
{
// todo: implement
}
private readonly ModelContext _modelContext;
}
```Last generic parameter in above example - `EmptyAdditionalFilters` is a class that holds
additional parameters for search to see in Swagger, just specify a new class implementing
`IReadFilterable` i.e.:```csharp
public class BooksFilterable : IReadFilterable
{
public IDictionary SelectFilters()
{
IDictionary additionalFilters = new Dictionary();
if (!string.IsNullOrEmpty(Title))
{
additionalFilters.Add(FilterParamsNames.TitleParameter, Title);
}if (Authors != null && Authors.Length > 0)
{
additionalFilters.Add(FilterParamsNames.AuthorsParameter, string.Join(",", Authors));
}return additionalFilters;
}
[FromQuery(Name = "title")] public string Title { get; set; }
[FromQuery(Name = "author")] public string[] Authors { get; set; }
}
```
### 5. Nuget package
You could find nuget-package [here](https://www.nuget.org/packages/Wissance.WebApiToolkit)
### 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```csharp
[ApiController]
public class StationController : BasicCrudController
{
public StationController(StationManager manager)
{
Manager = manager; // this is for basic operations
_manager = manager; // this for extended operations
}
private StationManager _manager;
}
```
```csharp
public class StationManager : EfModelManager
{
public StationManager(ModelContext modelContext, ILoggerFactory loggerFactory) : base(modelContext, StationFactory.Create, loggerFactory)
{
_modelContext = modelContext;
}public override async Task> 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(true, (int)HttpStatusCode.Created, null, StationFactory.Create(entity));
}
return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, "An unknown error occurred during station creation", null);}
catch (Exception e)
{
return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, $"An error occurred during station creation: {e.Message}", null);
}
}public override async Task> 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(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(true, (int)HttpStatusCode.OK, null, StationFactory.Create(entity));
}
return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, "An unknown error occurred during station update", null);}
catch (Exception e)
{
return new OperationResultDto(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:
```csharp
[HttpGet]
[Route("api/[controller]/search")]
public async Task>> SearchAsync([FromQuery]string query, [FromQuery]int page, [FromQuery]int size)
{
OperationResultDto, 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(pageNumber, result.Data.Item2, GetTotalPages(result.Data.Item2, pageSize), result.Data.Item1);
}
```#### 7.2 Add security to protect you API
We have [additional project](https://github.com/Wissance/Authorization) 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:
* [Medium article about v1.0.x usage]( https://medium.com/@m-ushakov/how-to-reduce-amount-of-code-when-writing-netcore-rest-api-services-28352edcfca6)
* [Dev.to article about v1.0.x usage]( https://dev.to/wissance/dry-your-web-api-net-core-with-our-toolkit-cbb)### 9. Contributors