A walkthrough of the organization schema for Wrapt projects
Instead of making you learn some custom directory structure, Wrapt projects are organized using the well known and maintainable Clean Architecture
format. If you are not familiar with this structure,
there are some great resources on it by Steve Smith, Uncle Bob, and
Jason Taylor.
In short, Clean Architecture
breaks your project up into logical layers like below. If you want to add something and you're not seeing it here, but think it should be, feel free to
submit a ticket on github for it to be added.
The core layer is split into two projects, the Application
and Domain
layers.
The Domain
project is pretty simple and will capture all of the entities and items directly related to that. This layer should never depend on any other project.
The Application
project is meant to abstract out our specific business rules for our application. It is dependent on the Domain
layer, but has no dependencies on
any other layer or project. This layer defines our interfaces, DTOs, Enums, Exceptions, Mapping Profiles, Validators, Specifications, and Wrappers that can be used by our external layers.
Our infrastructure layer is used for all of our external communication requirements (e.g. database communication). For more control, this layer is split into a Persistence
project as well as a Shared
project. Additional layers like Auth
, Web Api Clients
, File System Accessors
, Logging Adapters
, and Email/SMS Sending
can also be added here.
The Persistence
project will capture our application specific database configuration. The Shared
project will capture any external service requirements that
we may need across our infrastructure layer (e.g. DateTimeService).
Finally, we have our API layer. This is where our WebApi
project will live to provide us access to our API endpoints. This layer depends on both the Core
and Infrastructure
layers,
however, the dependency on Infrastructure
is only to support dependency injection. Therefore only Startup
classes should reference Infrastructure
.
Entities are set up to represent distinct tables in your database. These entities should not be exposed outside of the API and accordingly each have their own read, create, and update data transfer objects (DTOs).
At some point you may want to make a domain entity that is not directly linked to a database table. You can either do this manually or use the add:entity
command
and update the associated files to meet your needs.
Wrapt APIs have a repository built out for each entity that act as the core data access layer (DAL) any time we need to touch the database. You can find the repositories in the
Infrastructure.Persistence
layer.
Wrapt APIs use the magnificent Sieve library for filtering and sorting. All GET collection endpoints will have this capability enabled automatically
and can be used for any attributes that you have this configured for. These properties can be set when creating an entity or
adding a new property, otherwise you'll need to manage filtering and sorting on existing entity properties manually. There are details in the docs, but all you
need to to is manage the Sieve
attribute on any entity properties.
Note that Wrapt APIs do not use Sieve's pagination capabilities. For more information on how Wrapt handles pagination, see the pagination section.
[Sieve(CanFilter = true, CanSort = false)]
To add a filter to your API calls, just add a Filters
query string and the designated filter values you'd like to use. Below is a few examples, but you can find a full list of
operators on the Sieve github page.
http://localhost:5000/api/staff?Filters=firstname@=*al
http://localhost:5000/api/cities?Filters=name==Atlanta
Sorting can also be added using the SortOrder
query string and passing a comma separated list in the order you'd like to sort by. You can use a preceding -
to sort as descending.
http://localhost:5000/api/cities?SortOrder=name
http://localhost:5000/api/cities?Filters=name==Atlanta&SortOrder=name
http://localhost:5000/api/cities?Filters=name==Atlanta&SortOrder=name,-popularityscore
If you'd like to change the pagination and/or sorting, you can updated the ENTITYParametersDto
.
Wrapt APIs use a custom pagination capability to make working with large dataset as easy as possible.
When making a request to your GET collection endpoint, you can pass a PageNumber
and PageSize
query string, or exclude them to use the default values.
To change the default pagination values, go to the ENTITYParametersDto
class that you'd like to modify and create an override parameter for the property you'd like to change. Be sure to set the permission level to internal
to match the inherited permissions.
public class CityParametersDto : BasePaginationParameters
{
internal override int DefaultPageSize { get; set; } = 50;
public string Filters { get; set; }
public string SortOrder { get; set; }
}
Please note that if you make change to the BasePaginationParameters
class, it will affect all classes that inherit from it (which is every one by default)
Getting paginated results is nice, but you'll very likely want to know information about the collection's pagination info as well, especially when you're programming a UI. Wrapt APIs
will automatically return a complete list of pagination metadata in the response header as X-Pagination
. The following fields will be returned:
Metadata | Description |
---|---|
TotalCount | The total record count for the entire collection. |
PageSize | The page size that was requested. |
CurrentPageSize | The current page size. |
CurrentStartIndex | The index of the first record on this page. |
CurrentEndIndex | The index of the last record on this page. |
PageNumber | The current page number in the collection. |
TotalPages | The total page count for the entire collection. |
HasPrevious | A boolean that denotes whether or not there is a previous page. |
HasNext | A boolean that denotes whether or not there is a next page. |
Wrapt APIs are set up to run validation rules on any POST, PUT, and PATCH endpoints using Fluent Validation. No rules are added by default,
but you can add any rules that fit your business needs in Core.Application.Validation
. Validation rules can be assigned to the ENTITYForManipulationDtoValidator
class
if you'd like the rule to be run on both creation (POST calls) and update (PUT, PATCH). If you'd like a rule to just be ran on one or the other, you can add the rule to
the respective validator class.
The OOTB rules for fluent validation are pretty robust, but if you need to make a custom validation rule, something like the below is a good example (source) .
public static class CustomValidators
{
public static IRuleBuilderOptions<T, string> NotStartWithWhiteSpace<T>(this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder
.Must(m => m != null && !m.StartsWith(" "))
.WithMessage("'{PropertyName}' should not start with whitespace");
}
}
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(e => e.FirstName)
.NotEmpty()
.MaximumLength(30)
.NotStartWithWhiteSpace();
RuleFor(e => e.LastName)
.NotEmpty()
.MaximumLength(30)
.NotStartWithWhiteSpace();
}
}
👀 Docs Feedback
See something missing or light in content in the docs? Let me know! I want the Wrapt docs to be as through and helpful as possible!
If you'd like to request a new feature, you can submit a new Craftsman issue.