Wissance.WebApiToolkit.Ef
5.0.0
See the version list below for details.
dotnet add package Wissance.WebApiToolkit.Ef --version 5.0.0
NuGet\Install-Package Wissance.WebApiToolkit.Ef -Version 5.0.0
<PackageReference Include="Wissance.WebApiToolkit.Ef" Version="5.0.0" />
<PackageVersion Include="Wissance.WebApiToolkit.Ef" Version="5.0.0" />
<PackageReference Include="Wissance.WebApiToolkit.Ef" />
paket add Wissance.WebApiToolkit.Ef --version 5.0.0
#r "nuget: Wissance.WebApiToolkit.Ef, 5.0.0"
#:package Wissance.WebApiToolkit.Ef@5.0.0
#addin nuget:?package=Wissance.WebApiToolkit.Ef&version=5.0.0
#tool nuget:?package=Wissance.WebApiToolkit.Ef&version=5.0.0
Wissance.WebApiToolkit
10 Lines of code = Full CRUD and even BULK with swagger docs
This ultimate lib helps to build REST API with C# and AspNet easier than writing it from scratch over and over in different projects. It helps to build a consistent API (with the same REST routes approach for different controllers) with minimal amount of code: the minimal REST controller contains 10 lines of code with full auto support for all CRUD and BULK operations.
For the easiest way you only need:
- EntityFramework
Entities - DbContext with
DbSets - Inject from DI Manager Class on startup level.
- 1. Key Features
- 2. API Contract
- 3. Requirements
- 4. Toolkit usage algorithm with EntityFramework
- 5. Nuget package
- 6. Examples
- 7. Extending API
- 8. Additional materials
- 9. Contributors
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 parameters;
- support BULK operations with objects (Bulk
Create,UpdateandDelete) on a Controller && interface level - 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
- support writing
GRPCservices with examples (seeWissance.WebApiToolkit.TestAppandWissance.WebApiToolkit.Tests) - Manager classes that support file operation over:
- web folders (folders from mounted devices or just local folders)
- S3 AWS-compatible (tested with
Yandex Object Storageand previously withCloudflare R2andAmazon S3)
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}&sort={sort}&order={order}]to getPagedDataDto<T>now we also have possibility to send ANY number of query params, you just have to pass filter func toEfModelManageror do it in your own way like in WeatherControl example with edgedb. We also pass sort (column name) && order (ascordesc) to manager classes,EfModelManagerallows to sort by any column. <strike> Unfortunately here we have a ONE disadvantage - we should overrideSwaggerinfo to show query parameters usage!!! </strike> Starting from1.6.0it is possible to see all parameters inSwaggerand use them.GET /api/[controller]/{id}to get one object byid
- full
CRUDcontroller (BasicCrudController) = basic read controller (BasicReadController) +Create,UpdateandDeleteoperations :POST /api/[controller]- for new object creationPUT /api/[controller]/{id}- for edit object by idDELETE /api/[controller]/{id}- for delete object by id
- full
CRUDwith Bulk operations (operations over multiple objects at once), Base class -BasicBulkCrudController= basic read controller (BasicReadController) +BulkCreate,BulkUpdateandBulkDeleteoperations:POST /api/bulk/[controller]- for new objects creationPUT /api/bulk/[controller]- for edit objects passing in a request bodyDELETE /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)
- basic read controller (
Managers classes - classes that implements business logic of application
IModelManager- interface that describes basic operationsEfModelManager- is abstract class that contains implementation ofGetandDeleteoperationsEfSoftRemovableModelManageris abstract class that contains implementation ofGetandDeleteoperations with soft removable models (IsDeleted = truemeans model was removed)
Example of how faster Bulk vs Non-Bulk:
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<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 manyitemsget oneitemby iddeleteitemby id
4. Toolkit usage algorithm with EntityFramework
4.1.0 Minimal REST service
What do we need Entity classes and DbContext and init am appropriate Manager class:
services.AddScoped(sp =>
{
return SimplifiedEfBasedManagerFactory.Create<RoleEntity, int>(sp.GetRequiredService<ModelContext>(),
null, sp.GetRequiredService<ILoggerFactory>());
});
and controller class:
public class RoleController : BasicBulkCrudController<RoleEntity, RoleEntity, int, EmptyAdditionalFilters>
{
public RoleController(IModelManager<RoleEntity, RoleEntity, int> manager)
{
Manager = manager;
}
}
4.1.1 REST Services with full Declaration
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 (for soft remove also addIModelSoftRemovableimplementation), 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 aDbSetand it's implementation class that also derives fromDbContext(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, EmptyAdditionalFilters>
{
public BookController(BookManager manager)
{
Manager = manager; // this is for basic operations
_manager = manager; // this for extended operations
}
private BookManager _manager;
}
public class BookManager : EfModelManager<BookDto, BookEntity, int, EmptyAdditionalFilters>
{
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;
}
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.:
public class BooksFilterable : IReadFilterable
{
public IDictionary<string, string> SelectFilters()
{
IDictionary<string, string> additionalFilters = new Dictionary<string, string>();
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; }
}
4.2 GRPC Services
Starting from v3.0.0 it possible to create GRPC Services and we have algorithm for this with example based on same Manager classes with service classes that works as a proxy for generating GRPC-services, here we have 2 type of services:
ROservice with methods for Read data -ResourceBasedDataManageableReadOnlyService(GRPC equivalent toBasicReadController)CRUDservice with methods Read + Create + Update and Delete -ResourceBasedDataManageableCrudService
For building GRPC services based on these service implementation we just need to pass instance of this class to constructor, consider that we are having CodeService
public class CodeGrpcService : CodeService.CodeServiceBase
{
public CodeGrpcService(ResourceBasedDataManageableReadOnlyService<CodeDto, CodeEntity, int, EmptyAdditionalFilters> serviceImpl)
{
_serviceImpl = serviceImpl;
}
// GRPC methods impl
private readonly ResourceBasedDataManageableReadOnlyService<CodeDto, CodeEntity, int, EmptyAdditionalFilters> _serviceImpl;
}
Unfortunately GRPC generates all types Request and therefore we should implement additional mapping to convert DTO to Response, see full example in this solution in the Wissance.WebApiToolkit.TestApp project
5. Nuget package
- Obsolete version (no bug fixes, no support) ⇐
3.x.y, obsolete since4.0.0 - Core interfaces package
- Ef-related implementation of Core
- Cloud AWS S3 Compatible
5.1 Package Versions
Version 4.x.x keep support for old platform (netcoreapp3.1 and net 5.0), since 5.x.x removes platforms older net6.0
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
6.1 REST Service example
[ApiController]
public class StationController : BasicCrudController<StationDto, StationEntity, int, EmptyAdditionalFilters>
{
public StationController(StationManager manager)
{
Manager = manager; // this is for basic operations
_manager = manager; // this for extended operations
}
private StationManager _manager;
}
public class StationManager : EfModelManager<StationDto, StationEntity, 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
6.2 GRPC Service example
For building GRPC service all what we need:
.protofile, consider our CodeService example, we have the following GRPC methods:
service CodeService {
rpc ReadOne(OneItemRequest) returns (CodeOperationResult);
rpc ReadMany(PageDataRequest) returns (CodePagedDataOperationResult);
}
DIfor making service implementation:
private void ConfigureWebServices(IServiceCollection services)
{
services.AddScoped<ResourceBasedDataManageableReadOnlyService<CodeDto, CodeEntity, int, EmptyAdditionalFilters>>(
sp =>
{
return new ResourceBasedDataManageableReadOnlyService<CodeDto, CodeEntity, int, EmptyAdditionalFilters>(sp.GetRequiredService<CodeManager>());
});
}
- GRPC Service that derives from generated service and use as a proxy to
ResourceBasedDataManageableReadOnlyService<CodeDto, CodeEntity, int, EmptyAdditionalFilters>:
public class CodeGrpcService : CodeService.CodeServiceBase
{
public CodeGrpcService(ResourceBasedDataManageableReadOnlyService<CodeDto, CodeEntity, int, EmptyAdditionalFilters> serviceImpl)
{
_serviceImpl = serviceImpl;
}
public override async Task<CodePagedDataOperationResult> ReadMany(PageDataRequest request, ServerCallContext context)
{
OperationResultDto<PagedDataDto<CodeDto>> result = await _serviceImpl.ReadAsync(request.Page, request.Size, request.Sort, request.Order,
new EmptyAdditionalFilters());
context.Status = GrpcErrorCodeHelper.GetGrpcStatus(result.Status, result.Message);
CodePagedDataOperationResult response = new CodePagedDataOperationResult()
{
Success = result.Success,
Message = result.Message ?? String.Empty,
Status = result.Status,
};
if (result.Data != null)
{
response.Data = new CodePagedDataResult()
{
Page = result.Data.Page,
Pages = result.Data.Pages,
Total = result.Data.Total,
Data = {result.Data.Data.Select(c => Convert(c))}
};
}
return response;
}
public override async Task<CodeOperationResult> ReadOne(OneItemRequest request, ServerCallContext context)
{
OperationResultDto<CodeDto> result = await _serviceImpl.ReadByIdAsync(request.Id);
context.Status = GrpcErrorCodeHelper.GetGrpcStatus(result.Status, result.Message);
CodeOperationResult response = new CodeOperationResult()
{
Success = result.Success,
Message = result.Message ?? String.Empty,
Status = result.Status,
Data = Convert(result.Data)
};
return response;
}
private Code Convert(CodeDto dto)
{
if (dto == null)
return null;
return new Code()
{
Id = dto.Id,
Code_ = dto.Code,
Name = dto.Name
};
}
private readonly ResourceBasedDataManageableReadOnlyService<CodeDto, CodeEntity, int, EmptyAdditionalFilters> _serviceImpl;
}
Full example how it all works see in Wissance.WebApiToolkit.TestApp project.
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:
9. Contributors
<a href="https://github.com/Wissance/WebApiToolkit/graphs/contributors"> <img src="https://contrib.rocks/image?repo=Wissance/WebApiToolkit" /> </a>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net6.0
- Microsoft.EntityFrameworkCore (>= 7.0.20)
-
net8.0
- Microsoft.EntityFrameworkCore (>= 7.0.20)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 7.0.20)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
This version contains the new platform net9.0 and libs update