Project philosophy
The general mentality when using vertical slice architecture is all about thinking in endpoints.
Do not think in CRUD. Do not think in resources. Do not think in entities.
Think in features. Think in use cases. Think in endpoints.
Coupling and DRY
Coupling and DRY are always in direct competition and coupling is a WAY bigger problem than "duplicated" code.
Let me indulge in a somewhat lengthy example to illustrate a point about coupling and about DRY.
The most extreme case is when you have some kind of "going from status 1 to status 2 to status 3" type of logic.
Lets say a we are tracking a delivery. It goes through different statuses.
InWarehouse => OnTheRoad => ShippedToCustomer.
I would create 3 endpoints. ArriveAtWarehouse. PickedUpByDriver. ShippedToCustomer.
All 3 endpoints would look like this:
public class ArriveAtWarehouseEndpoint : Endpoint<ArriveAtWarehouseRequest>
{
//...
public override async Task HandleAsync(ArriveAtWarehouseRequest req, CancellationToken ct)
{
var delivery = await context.Delivery.First(x => x.Id == req.DeliveryId);
delivery.Status = DeliveryStatus.ArriveAtWarehouse;
await context.SaveChangesAsync();
}
}
public record ArriveAtWarehouseRequest(Guid DeliveryId);
public class PickedUpByDriverEndpoint : Endpoint<PickedUpByDriverRequest>
{
//...
public override async Task HandleAsync(PickedUpByDriverRequest req, CancellationToken ct)
{
var delivery = await context.Delivery.First(x => x.Id == req.DeliveryId);
delivery.Status = DeliveryStatus.PickedUpByDriver;
await context.SaveChangesAsync();
}
}
public record PickedUpByDriverRequest(Guid DeliveryId);
public class ShippedToCustomerEndpoint : Endpoint<ShippedToCustomerRequest>
{
//...
public override async Task HandleAsync(ShippedToCustomerRequest req, CancellationToken ct)
{
var delivery = await context.Delivery.First(x => x.Id == req.DeliveryId);
delivery.Status = DeliveryStatus.ShippedToCustomer;
await context.SaveChangesAsync();
}
}
public record ShippedToCustomerRequest(Guid DeliveryId);
You feel like an idiot writing these.
Obviously you could just use a UpdateDelivery
or SetDeliveryStatus
endpoint and give it any status you want.
-
But then it turns out when a delivery moves to
ShippedToCustomer
, we send a delivery arrived email. -
And when a delivery goes to
ArriveAtWarehouse
we mark them in some other system. -
And when a delivery goes to
OnTheRoad
we hook that delivery to that trucks GPS tracking. Oh how do we do that? We need theTruckId
or theTrackingId
, but only for this case. -
And how did it ever happen that this delivery went from
ShippedToCustomer
directly toArriveAtWarehouse
?
That should never be possible, but some frontend just calledSetDeliveryStatus
with a DeliveryId and a DeliveryStatus.
When this situation comes up in a UpdateDelivery
endpoint or SetDeliveryStatus
endpoint, you keep adding conditionals, keep adding request parameters that only need to be set for certain status changes and just generally drown in complexity.
Thats coupling getting you and you dont even notice it.
Setting the status to ShippedToCustomer
was actually UNRELATED to setting the status to OnTheRoad
.
When this situation comes up in a vertical slice architecture, nothing happens. You don't even notice it.
You add the email to an endpoint, no issues. You add the exra request parameter to another endpoint, no issues.
That is the beauty of this architecture and the reason I love it. Its incredibly extensible and iterative. You don't even notice the giant headache you just dodged.
It takes restraint and discipline to not go for the premature abstraction. Its also hard to realize that you only got a problem, because you chose the wrong abstraction.
Who the hell questions an UpdateDelivery
endpoint? Who questions the SetDeliveryStatus
method?
Thats just DRY, right? Our system is just complicated, right? The requirements were just unclear, right?
Personally I started writing really repetetive, really dumb code to feel the pain.
I want to be forced into turning these 3 endpoints into one, because im spending soooooo much time writing and maintaining this messy duplicated code. I want to know where the breaking point really is.
Somehow I haven't broken yet.
I want to mention that you can absolutely dodge this problem in any architecture.
You can also absolutely run into this problem using this exact architecture.
This code base, FastEndpoints and this essay gently push you towards the right decision without you even noticing.
Think in endpoints. Forget CRUD.
Abstraction is my enemy
- Dont hide EF Core behind your repository.
- Dont hide your service registration behind 2 other extension methods.
- Dont hide sendgrids API behind your own INotifcationService interface.
Most people, including yourself, wont question your abstraction. They will just use it.
If you don't have a Any()
method in your repository, people will just use FirstOrDefault() == null
.
The odds of you getting it right are basically 0.
The odds of you creating a better database abstraction than Microsoft, while mainly building your AI marketing automation startup are 0.
I like for people to use the actual tools directly. Like SQL. Like EfCore. Like Fastendpoints.
Its easy to look up the documentation for how to do X on EfCore. But if something happens in your internal db abstraction, you get a call. Or people have to wade through your implementation.
If you really want to not use EfCore directly within endpoints, I would not write a classic repository.
I would think of it more like a service. A grouping of a bunch of methods that call EfCore queries and commands.
Dont start abstracting over EfCore. Dont add your own FirstOrDefault
that just wraps EfCores FirstOrDefault
.
Instead just go with DbCalls.GetDelivery()
or DbCalls.GetDeliveryForPageXY
. This way you dont mess up your abstractions, but still have all your database code in one place.
This is usually done for organization and testing, but we are testing differently in this project.
I like going directly through the frontdoor.
I would think of all pulled out code in this static utility way. If you really use the same code in 4 different endpoints, extract it.
But extract it into what is basically a static Utilities class with a bunch of unrelated methods.
Dont try to hard to give it a name. Dont try too hard to give it a concept.
Its just procederal code that needs to be called in 7 endpoints.
I am not a crazy person. I understand in a real project you have to abstract and you will have concepts, abstractions and services in your application.
I do however think that on a spectrum that goes from "abstracting too early" to "abstracting too late" basically every developer is wayyyy down the "too early" side of the spectrum.
If you try to act on these extremes, you will end up on the real middleground and have a good time.
More Tips
Some less structured thoughts about my style. Why I do things the way I do. How I would suggest using this code base.
- Thinking in endpoints makes it easy to plan work or write tickets for someone else to implement.
Its simple to do upfront design, implementation and review when its just "takes in X, does Y, returns Z". - Create endpoints for the frontend in whatever form it needs. The backend serves the frontend.
Dont let someone make 3 http calls in javascript which map to 3 database calls and then transform the results into a new form to display on screen.
Making database calls and transforming data is the backends job. - Never reuse Request types. If an endpoint takes in a very common DTO or entity, make that a property on your RequestType for that endpoint.
- Rarely reuse Response types. I wouldnt say never, but it should be rare.
- Dont spend time thinking about the perfect abstraction or philosophical questions of where your code lives. Does it belong in the Service or the Repository or the Handler? Create an endpoint with a request and a response and just code.
- This setup, much like mediatr, allows some cool patterns for logging, validation and testing. Middleware / Decorator pattern kind of stuff.
- Almost all my reasoning comes back to "because it's simpler".
I find throwing different best practice acronyms at each other shuts off thinking. You hit a "ah but thats not DRY" wall and stop thinking there.
Final Thoughts
All of this is just my style and my philosophy. Nothing in this project stops you from deciding that all your logic is only allowed within Service
classes within the Domain
project.
Nothing stops you from putting your Request-, Endpoint and Response classes in 3 seperate files.
I would heavily encourage you to try it though. Keep your code as unabstracted as possible for as long as possible.
Use EfCore
directly within endpoints.
Skip AutoMapper
and see when you actually go crazy from writing response.Name = entity.Name
.
Wait with the DeliveryHelper.SetDeliveryStatus(delivery)
implementation until you had to touch the endpoints 4 times, because of bug reports.
Wait with your BaseEndpoint
until you can't take it anymore.
Write really stupid looking code until you actually feel the pain.
If your biggest problem in a coding project is that you feel stupid, because everything is so simple and repetitive.
You don't have a problem.
Further resources
- FastEndpoints (opens in a new tab) and its documentation (opens in a new tab).
Read through the entire thing from start to finish once. Its not that much and offers a lot of goodies. - Jimmy Bogard's talk (opens in a new tab). Minute 2:53 to 11:48 should be mandatory viewing for every .net developer.
- CodeOpinion (opens in a new tab) has a lot of great content about vertical slice architecture and in general.