Project Organization

A walkthrough of the organization schema for Wrapt projects

Clean Architecture

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.

Core

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.

Infrastructure

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

API

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.

What Boilerplate Do You Get?

Entities & DTOs

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

Domain Entities

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.

Repositories

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.

Filtering and Sorting

Toggling Filtering and Sorting

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)]

Using Filtering and Sorting

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

Updating Filtering and Sorting

If you'd like to change the pagination and/or sorting, you can updated the ENTITYParametersDto.

Pagination

Wrapt APIs use a custom pagination capability to make working with large dataset as easy as possible.

Pagination Requests

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.

public class CityParametersDto : BasePaginationParameters
{
    public 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)

Pagination Responses

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:

MetadataDescription
TotalCountThe total record count for the entire collection.
PageSizeThe page size that was requested.
CurrentPageSizeThe current page size.
CurrentStartIndexThe index of the first record on this page.
CurrentEndIndexThe index of the last record on this page.
PageNumberThe current page number in the collection.
TotalPagesThe total page count for the entire collection.
HasPreviousA boolean that denotes whether or not there is a previous page.
HasNextA boolean that denotes whether or not there is a next page.

Fluent Validation

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();
    }
}