NDjango Rest Framework makes you focus on business, not on boilerplate code. It's designed to follow the famous Django's slogan "The web framework for perfectionists with deadlines." 🤺
This is a copy of the convention established by Django REST framework, though translated to C# and adapted to the .NET Core framework.
Let's create a CRUD API for a Customer
entity with a CustomerDocument
child entity.
Some characteristics of the entities:
- We should inherit from
BaseModel<TPrimaryKey>
.- The
TPrimaryKey
is the type of the primary key. In this case, we are usingGuid
.
- The
- The
GetFields
method is mandatory. It informs which fields of the entity will be serialized in the API response.- For fields of child or parent entities, we can use
:
to indicate them to be serialized as well. In this case, it is necessary to perform theInclude
with a filter.
- For fields of child or parent entities, we can use
public class Customer : BaseModel<Guid>
{
public string Name { get; set; }
public string CNPJ { get; set; }
public int Age { get; set; }
public ICollection<CustomerDocument> CustomerDocument { get; set; }
public override string[] GetFields()
{
return new[] { "Id", "Name", "CNPJ", "Age", "CustomerDocument", "CustomerDocument:DocumentType", "CustomerDocument:Document" };
}
}
Add the collection to the application's DbContext
:
public class ApplicationDbContext : DbContext
{
public DbSet<Customer> Customer { get; set; }
}
The DTO is required to inherit from BaseDto<TPrimaryKey>
, like the entity.
public class CustomerDto : BaseDto<Guid>
{
public CustomerDto() { }
public string Name { get; set; }
public string CNPJ { get; set; }
public ICollection<CustomerDocumentDto> CustomerDocuments { get; set; }
}
A validation is not mandatory, but it is recommended to ensure that the data is correct. The validation is done using the FluentValidation
library.
public class CustomerDtoValidator : AbstractValidator<CustomerDto>
{
public CustomerDtoValidator(IHttpContextAccessor context)
{
RuleFor(m => m.Name)
.MinimumLength(3)
.WithMessage("Name should have at least 3 characters");
if (context.HttpContext.Request.Method == HttpMethods.Post)
RuleFor(m => m.CNPJ)
.NotEqual("567")
.WithMessage("CNPJ cannot be 567");
}
}
Previously, we included the CustomerDocument
entity in the Customer
entity. Check out the GetFields
method in the Customer
entity.
public class CustomerDocumentIncludeFilter : Filter<Customer>
{
public override IQueryable<Customer> AddFilter(IQueryable<Customer> query, HttpRequest request)
{
return query.Include(x => x.CustomerDocument);
}
}
The CRUD API is created by inheriting from the BaseController
and passing the necessary parameters. Note how AllowedFields
and Filters
are set.
[Route("api/[controller]")]
[ApiController]
public class CustomersController : BaseController<CustomerDto, Customer, Guid, ApplicationDbContext>
{
public CustomersController(
CustomerSerializer serializer,
ApplicationDbContext dbContext,
ILogger<Customer> logger)
: base(
serializer,
dbContext,
logger)
{
AllowedFields = new[] {
nameof(Customer.Id),
nameof(Customer.Name),
nameof(Customer.CNPJ),
nameof(Customer.Age),
};
Filters.Add(new QueryStringFilter<Customer>(AllowedFields));
Filters.Add(new QueryStringSearchFilter<Customer>(AllowedFields));
Filters.Add(new QueryStringIdRangeFilter<Customer, Guid>());
Filters.Add(new CustomerDocumentIncludeFilter());
}
}
In the ListPaged
method, we use the query parameters sort
or sortDesc
to sort by a field. If not specified, we will always use the entity's Id
field for ascending sorting.
Filters are mechanisms applied whenever we try to retrieve entity data in the GetSingle
and ListPaged
methods.
The QueryStringFilter
, perhaps the most relevant, is a filter that matches the fields passed in the query parameters with the fields of the entity whose filter is allowed. All filters are created using the equals (==
) operator.
The QueryStringIdRangeFilter
goal is to filter the entities by Id
based on all the ids
provided in the query parameters.
The QueryStringSearchFilter
is a filter that allows a search
parameter to be provided in the query parameters to search, through a single input, in several fields of the entity, even performing LIKE
on strings.
Given an IQueryable<T>
and an HttpRequest
, you can implement the filter as you prefer. Just inherit from the base class and add it to your controller:
public class MyFilter : AspNetCore.RestFramework.Core.Filters.Filter<Seller>
{
private readonly string _forbiddenName;
public MyFilter(string forbiddenName)
{
_forbiddenName = forbiddenName;
}
public IQueryable<TEntity> AddFilter(IQueryable<TEntity> query, HttpRequest request)
{
return query.Where(m => m.Name != forbiddenName);
}
}
public class SellerController
{
public SellerController(...)
: base(...)
{
Filters.Add(new MyFilter("Example"));
}
}
By default, the BaseController
uses the class PageNumberPagination
. It behaves the same as DRF's PageNumberPagination
. Sample response:
{
"count": 13,
"next": "http://localhost:8000/api/v1/Persons?page=3&page_size=5",
"previous": "http://localhost:8000/api/v1/Persons?page=1&page_size=5",
"results": [
{
"name": "Sal Paradise",
"createdAt": "2024-10-19T19:22:12.0524797",
"id": 6
},
{
"name": "Odulor",
"createdAt": "2024-10-19T19:22:15.4483365",
"id": 7
},
{
"name": "Iago",
"createdAt": "2024-10-19T19:22:18.1077698",
"id": 8
},
{
"name": "Jafar",
"createdAt": "2024-10-19T19:22:21.5425118",
"id": 9
},
{
"name": "Wig",
"createdAt": "2024-10-19T19:22:23.9046811",
"id": 10
}
]
}
The ValidationErrors
and UnexpectedError
might be returned in the BaseController
in case of validation errors or other exceptions.
Implement validators for the DTOs and configure your application with the extension ModelStateValidationExtensions.ConfigureValidationResponseFormat
to ensure that in case of the ModelState
being invalid, a ValidationErrors
is returned. It might be necessary to add the HttpContext
accessor to the services. Check the example below:
services.AddControllers()
// ...
// At the end of AddControllers, add the following:
.AddModelValidationAsyncActionFilter(options =>
{
options.OnlyApiController = true;
})
// ModelStateValidationExtensions
.ConfigureValidationResponseFormat();
// ...
services.AddHttpContextAccessor();
Serializer
is a mechanism used by the BaseController
. Each controller has its own serializer. The serializer's methods can be overridden to add additional or different logic for specific entities. It works more or less similar to the Django REST framework's serializers.
Term | Description |
---|---|
TPrimaryKey |
Type of the primary key of an entity, usually Guid . |
TEntity |
Type of the entity we are talking about in a generic class. |
TOrigin |
In the BaseController , it is the same as TEntity . |
TDestination |
Type of the DTO. |
TContext |
Type of the Entity Framework context. |
This project is still in the early stages of development. We recommend that you do not use it in production environments and check the written tests to understand the current functionality.