REST API with C# and AspNet easily than writing it from scratch over and over in different projects.
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;Create, Update and Delete) on a Controller && interface levelIModelManager interface); Good built-in EntityFramework support (see EfModelManager class). See WeatherControl App which has 2 WEB API projects:
Wissance.WeatherControl.WebApi uses EntityFramework;Wissance.WeatherControl.WebApi.V2 uses EdgeDb.Key concepts:
Controller is a class that handles HTTP-requests to REST Resource.REST Resource is equal to Entity class / Database TableREST Resource produce JSON with DTO as output. We ASSUME to use only one DTO class with all REST methods.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
BasicReadController) contains 2 methods:
GET /api/[controller]/?[page={page}&size={size}&sort={sort}&order={order}] to get PagedDataDto<T>
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. We also pass sort (column name) && order (asc or desc) to manager classes,
EfModelManager allows to sort by any column.
Swagger info to show query parameters usage!!! 1.6.0 it is possible to see all parameters in Swagger and use them.GET /api/[controller]/{id} to get one object by idCRUD controller (BasicCrudController) = basic read controller (BasicReadController) + Create, Update and Delete operations :
POST /api/[controller] - for new object creationPUT /api/[controller]/{id} - for edit object by idDELETE /api/[controller]/{id} - for delete object by idCRUD 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 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)
Managers classes - classes that implements business logic of application
IModelManager - interface that describes basic operationsEfModelManager- is abstract class that contains implementation of Get and Delete operationsEfSoftRemovableModelManager 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:

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.
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 itemsget one item by iddelete item by idFull example is mentioned in section 6 (see below). But if you are starting to build new REST Resource
API you should do following:
model (entity) class implementing IModelIdentifiable<T> and DTO class for it representation (for soft remove also add IModelSoftRemovable implementation), 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;}
}Model to DTO i.e.:public static class BookFactory
{
public static BookDto Create(BookEntity entity)
{
return new BookDto
{
Id = entity.Id,
Title = entity.Title,
Authors = entity.Authors;
};
}
}IModelContext interface that has you BookEntity as 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;}
}ModelContext as a DbContext via DI see Startup classController class and a manager class pair, i.e. consider here full CRUD[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<BookEntity, BookDto, 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; }
}You could find nuget-package here
[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<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
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);
}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;
You could see our articles about Toolkit usage: