Giter VIP home page Giter VIP logo

wissance / webapitoolkit Goto Github PK

View Code? Open in Web Editor NEW
13.0 5.0 2.0 2.14 MB

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)

Home Page: https://wissance.github.io/WebApiToolkit/

License: Apache License 2.0

C# 100.00%
library web-application webapi webapi-core rest-api rest-api-template rest-api-toolkit crud crud-api csharp-library

webapitoolkit's Introduction

Wissance.WebApiToolkit

GitHub code size in bytes GitHub issues GitHub Release Date GitHub release (latest by date)

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

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 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<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. 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

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 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<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;} 
}
  1. Create a factory function (i.e. static function of a static class) that converts 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;
        };
    }
}
  1. Create 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;}
}
  1. Configure to inject ModelContext as a DbContext via DI see Startup class
  2. Create Controller 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; }
}

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, 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

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

webapitoolkit's People

Contributors

evillord666 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

webapitoolkit's Issues

Some issues with examples

Hi Ushakov,

I like your WebApiToolkit. I found a few minor issues on the project home page (https://github.com/Wissance/WebApiToolkit):

  1. In the Requirements section, "labrary" is misspelled.
  2. In the GroupController example, the class constructor name is incorrect.
  3. It'll be better if you can shown how the GroupEntity class implements IModelIdentifiable.

I also have the following comments on WebApiToolkit:

  1. Base Controller: I personally think that passing the Manager object into the base controller via a constructor is easier to understand.
    i.e., public GroupController(GroupManager manager) : base(manager) { }

  2. Order of type parameters is inconsistent between base controller and base manager.
    BasicReadController<GroupDto, GroupEntity, int>
    ModelManager<GroupEntity, GroupDto, int>

Joncheng

Override swagger docs info

We should override swagger information for the following features:

  • Query params usage
  • Disable methods that are not used (`throws NotImplementedException') show

Add typical DB classes

  • Property (Attribute) that used in many-2-many relation
  • RegisteredValue - object that allows to store value with date+time in db
  • History of object changes (think about how this could be implemented)

Pass Manager to Base controllers via constructor

According to #2 :
Base Controller: I personally think that passing the Manager object into the base controller via a constructor is easier to understand.
i.e., public GroupController(GroupManager manager) : base(manager) { }

Make consistent order of generic parameters

According to #2 we have following note:

Order of type parameters is inconsistent between base controller and base manager.
BasicReadController<GroupDto, GroupEntity, int>
ModelManager<GroupEntity, GroupDto, int>

Add EdgeDb support

In future major release upgrade (V2) we should add support to getting result not only from EF but also using edgedb

Add parameters check against SQL-injection

Because we are passing parameter further to Manager classes directly we have to be sure that they do not contain something that could possibly damage database or get secure info

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.