Project Organization

A walkthrough of the organization schema for Wrapt projects

Choosing an Architecture

Starting Out Clean

Deciding on a particular architecture to organize Wrapt projects was a big deal for me. Project architecture is a foundational aspect that drives a lot of different operations and sets up a projects for maintainable success or a total mess. While I don't think there is a single 'correct' architecture to chose, there are certainly patterns that are better than others.

The first that came to mind was a Clean/Onion Architecture format. It is a well known pattern in the enterprise architecture world and there are some great resources on it by Steve Smith, Uncle Bob, and Jason Taylor.

After a lot of research and a couple of trial projects, I actually decided that this was the architecture I wanted to use for Wrapt and went forward with it through 0.8.2. And then I came across a different pattern...

Onions Actually Stink — Moving to a Vertical Slice Architecture

Enter Vertical Slice Architecture (VSA). I came across one of Jimmy Bogard's talks on VSA and it really spoke to me. I also found out that it has worked really well for others as well, many of which who came from Clean projects.

For those not familiar, the short version is that Clean Architecture aims to separate the business rules from the I/O with designated layers while Vertical Slice Architecture aims to separate the code by features, aiming to minimize code sharing between features.

I'm planning on doing a detailed blog post on why I made this decision and how to work with VSA, but here are some of the benefits of VSA:

  • VSA is feature based. This is how requests come to my teams in real life and it lines up directly with my apps.
  • Because of the feature base, everything is in one spot. Everyone from a senior architect to a new intern knows exactly where to go to find something. This contrasts with Clean/Onion which forces layers on you prematurely in many instances.
  • This structure means that new features are rarely updating existing code and worrying about possible side effects. This allows you to breath easier and work faster.
  • Reusable code is written as the need for them is discovered, and not sooner. Starting with vertical slices essentially means you get adherence to You Ain't Gonna Need It (YAGNI) and Keep It Simple Stupid (KISS). Even if you do miss something that would be better abstracted, duplication is far cheaper than the wrong abstraction.

For those that want to read up more on VSA, here's some links.

What Boilerplate Do You Get?

Entities & DTOs

Entities are set up by default to represent distinct tables in your database (though you can modify the scaffolding to change this when needed). These entities should not be exposed outside of the API and accordingly each have their own read, create, and update data transfer objects (DTOs). There is also a common Manipulation DTO that the update and create DTOs inherit from to share common requirements.

Additionally, These entities will always inherit from a BaseEntity with a Guid Id property that will be set as the primary key as well as CreatedOn, CreatedBy, LastModifiedOn, and LastModifiedBy for auditing and consistency across your domain.

Features

Features are one of the pivotal patterns of a Vertical Slice Architecture. Each feature lives in the Feature folder of the within the Domain directory and will correspond to various actions that can be performed on each of your entities. Out of the box, Wrapt will give you basic CRUD features for each of your entities to get a list, get a record, create a record, update a record, partially update a record, and delete a record.

As your project grows, you can continue to add more features (or update these existing ones) to meet your business needs.

In practice, each controller is essentially acting as a router to a feature and that's all. All the business logic behind the endpoint lives in the feature (or, even better, in the domain entity that is used by the feature). The features themselves are MediatR commands or queries that are fed into a handler that return a response.

Repositories or not?

Repositories are quite a polarizing topic in the .NET world. I've tried approaches both with and without and personally think both cases have their merits. One of the best breakdowns I think I've seen is Derek Comartin's video on it here, but where I've landed is the following:

For Wrapt projects, I've decided to exclude them by default, but depending on your project, you may want to reach for them or similar options for certain use cases

Queries generally don't need repositories as there's no invariant consistency needed here, you just need data. There's a couple reasons you may still be thinking about using a repository for your queries though.

First, You may have common queries that you want to share sometimes and you have a couple options here:

  1. Make an extension class for the entity with a method on an IQueryable and/or DbSet for the common logic
  2. Make a service/repository that can be used for common calls like this (totally reasonable option IMHO)
  3. Integration specifications -- personally, I like the idea of this in theory and played around with ardalis' specification pattern library but had a couple issues and some clunkiness with it.

Any of the above are totally valid and you should choose what makes the most sense for your project and your preferences.

Another common reason is testing your queries. Generally, most of your feature tests should be in the integration test layer where you have a real db to test against. You do NOT want to mock DbContext here or use an in-memory db. It's just extra effort and a false sense of security. If you need to test the db, use an integration test.

With that said, you may have flows or business logic in your features that you want to unit test too. I'd generally recommend abstracting out a method for the logic you want to test so you can focus on that, but if you want to make a service/repository for this, that's totally fine too.

Commands are a bit trickier though. Commands may need a consistency boundary to make sure that you have the proper overarching context for a set of concepts to properly update data. For example, if I try to submit an order, I might need to run a validation check that all the items in the order are in a sellable state or in stock or whatever. I need access to both concepts when doing a state change and I need to know that i have that consistency boundary whenever I'm doing that.

This is where repositories would shine, but as with above, you could also make extension methods to do this as well. Regardless, just make sure that when you're doing transformations, that you are using this root consistency boundary to start.

Filtering and Sorting

Configuring Filtering and Sorting

Wrapt APIs use QueryKit for easy filtering and sorting out of hte box, but feel free to customize your endpoints when needed or modify with a different solution overall. See the QueryKit docs for more details on how it works, but here are some examples of how to use it:

http://localhost:5000/api/staff?Filters=firstname @=* "Al"
http://localhost:5000/api/cities?Filters=name == "Atlanta"&SortOrder=name

Pagination

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

Pagination Requests

When making a request to your GET list 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). This is one of the few classes in Wrapt projects that is shared between multiple features as it is a common base to work from that is easily overridden.

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.

Encapsulated Domain

Generally, we want to push our behaviors down into our domain entity. This means that our CQRS handlers get thinner and thinner and is really just performing data access logic (no need for an extra repository layer) so we can get delegate all the meat of the work in the domain logic. This means we have a fully encapsulated, 'always valid' domain.

Now, before we do all that work in the handler/domain logic, we need to check if we actually can do that thing that we want to do. Enter validation. So we want to center our validation around our requests and our handlers.

Validation

Generally, there's two main places validation would be added into a Wrapt project: 1. inside a handler or service specific to that need or 2. domain specific logic pushed down to the domain entity. Generally, you'll want to try to do the latter to enrich your domain as much as possible, but there are certainly cases where you'll have handler or service specific logic that doesn't need to be at the root of your domain

Value Objects

A ValueObject class is scaffolded to be used in your domain if you would like to use them. Here are a couple examples from other projects:

You can see more on value objects in the value objects section of the docs.

Eventing

In Process Events

If you have an event you want to publish and consume withing the same boundary, you can use an in-process message to do so. This is simple in a wrapt project as you can just add an event to the DomainEvents property on an entity and all the stored events will be published on save.

By default, domain messages will be scaffolded for entity creation and update events and added as part of the factory methods.

To publish messages, just create a message in Domain.[EntityName].DomainEvents and queue it up using the QueueDomainEvent method on your entity.

Out of Process Events

Wrapt projects support scaffolding for event driven features like producers and consumers in an event bus, messages, and more.

Messages are stored in a distinct project at the solution root as they are generally used to communicate across bounded contexts and shouldn't belong to any particular bounded context.

Producers and consumers can both be added as distinct features using the add producer and add consumer commands.

A common requirement is to add a producer to an existing feature. If that's the case, you can add an IPublishEndpoint to your command and publish your message like you usually would. Additionally, you can use the register producer command to set up your registration.

Automated Tests

Wrapt will automatically scaffold out unit, integration and functional tests for each of your bounded contexts using NUnit.

  • Unit tests are meant to confirm that individual operations are working as expected (e.g. PagedList calculations). Additionally, you can test business flows to make sure logic paths are properly tested (e.g. a business process in a feature handler doesn't usually need an expensive integration test). Use these as much as possible for efficiency, but don't over mock things either.

  • Integration tests These are generally the highest value tests in Wrapt projects given the structure of the project and are meant to check that different areas are working together as expected (e.g. our features folder). These tests will spin up a real database in docker and run each of your feature tests in an actual representation of your database along with a full service collection like you would usually have.

  • Functional tests are meant to check an entire slice of functionality with all the code running together. These are generally more involved to write and maintain, but with this project setup, our controllers are essentially just routers to our feature queries and commands, so we have already done the meat of our testing in our integration tests. This means that we can minimize the amount functional tests that we need to write and generally just need to lean on them for a select few use cases. Primary, this will be use cases that need to be tests with the API running and can't just use the service collection. This might be hitting an HTTP endpoint, using a background service like Hangfire, or using a message bus like RabbitMQ.

Shared Kernel

Starting with v0.13, Wrapt projects will support a shared kernel. This means that you can have a class library that can store domain level abstractions.

⚠️ Be careful what you put in here as it will be coupled to all of your boundaries!