Architecture Update! (Vertical Slices Experiment) #47
Replies: 5 comments 18 replies
-
Great stuff @jonhilt! Will review in detail this evening and come back with some thoughts. |
Beta Was this translation helpful? Give feedback.
-
WOW @jonhilt ! My head is spinning! Spinning in a good way!!! Yes - a screen cast of your changes and how you would add a simple feature sounds like great thing... wow... Thanks so much to @DotNetDublin and @jonhilt for all this!!! |
Beta Was this translation helpful? Give feedback.
-
A screencast would be good, @jonhilt One crazy solution I've had is to implement domain models as partial classes. For each domain class, one partial is shipped to the client, and the other is kept server-private -- biz logic things that have a direct database connection, etc. This gives you dependency-free domain classes in the client, and heavy domain models on the server -- with seamless compatibility between them. The downside is that on the client side, you must link every domain class individually to the project rather than use a project reference. This is a little clumsy from a solution management perspective, and is a bit tough to conceptualize what's happening if you're new to it. (Frankly true of many things.) I'm still interested to see your suggestion in action via screencast or PowerPoint |
Beta Was this translation helpful? Give feedback.
-
OK here's the promised video showing one way to implement a new feature with this approach. It's a little rough and ready but hopefully helps show the key moving parts :-) https://practicaldotnet.vids.io/videos/709ddbb01f1ee7c2f9/architecture-mp4 |
Beta Was this translation helpful? Give feedback.
-
Just finished watching the video which was really informative. Prior to that I had started watching Tim Corey's video Intro to MediatR - Implementing CQRS and Mediator Patterns so I'll need to finish that over the coming days as well as taking a look at Refit which I hadn't come across before. I can appreciate how the structure shown in the refit-spike branch addresses some of the issues I had thought of while putting the initial code together such as the amount of repeated code in the controllers and services and not exposing the entire TimeEntry model for create requests etc. One question to begin with is that I see that the API project now contains Data and Domain folders which removes the need for the Domain and Repository projects and then in the Shared project there is the Api directory which removes the need for the Services project. I'm wondering if someone could argue that the Api project is doing too much if it is handling database access and controlling the domain? Does that come down to project size or is having that within the Api not an issue? In relation to the Services project in my mind there would be service class such as EmailService.cs as well as a service per object such as TimeEntryService,cs. In the new architecture would the email sending functionality be contained within the Api project and would this be called from within Features/Email or similar within the Api directory of the Shared project? One advantage I thought the Services project held was that it added a layer of abstraction between the ComponentLibrary project (now Shared) and the API in that the component library wouldn't need to be updated if in future you pivoted and had everything server side with direct data access etc. As I type that I realise that's probably similar to when I hear that the repository pattern is great as it covers you when you might switch from SQL Server to CoolKidsDB and I think "well that's never going to happen". There will be more questions but I'm thinking that it will take two to three dog walks before they have formed coherently in my mind. Overall I'm happy to continue going down the road of vertical slices unless anyone else has other thoughts so thanks for taking the time to put this together. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Here's a walkthrough of some of the changes I'm testing out in the refit-spike branch.
Here's a video showing one way to implement a new feature with this approach.
There are a few considerations when building a Blazor project, and targeting Blazor WASM brings its own nuances which I thought it might be useful to discuss.
It's useful to start with some guiding principles for what I think we're aiming for.
Think about what we ship to the client
The first thing that struck me when looking at the initial code for this was that we always have to be careful what we deploy to the client with Blazor.
The first version of the project had a reference to ComponentLibrary which in turn had a reference to the Domain and Services projects.
In practice this means we'd be deploying the following dlls to the client.
This means some server only considerations (like the EF Core Context) are going to end up being shipped to the client. Also, if we push more business logic, rules into the domain, all of this would be shipped to the client too.
However, in practice the client(s) can then only interact with this domain via the web API, so we'd want to try to avoid shipping all of that to the client.
Avoid directly referencing the domain in the client
Moreover, I've learned (the hard way) that using Domain objects directly in the UI for an application can be problematic. Take the
TimeEntry
entity for example.We could use that in our Blazor components like so:
But if we do that we have a couple of potential issues:
Point 2. makes it much harder to iterate the design for how we interact with and store time. Any changes to the domain, or shape of the entities will ripple all the way through to the client. Effectively, what could be an internal refactoring ends up always changing the shape of the API, requiring both the API and the client(s) to be updated and redeployed.
The easiest way around this is is to use different objects (classes) for interacting with the API, effectively creating a public interface which we try to keep fairly consistent, even when refactoring how the API and data persistence is implemented.
In the refit-spike branch I've therefore renamed ComponentLibrary to Shared. Take a look in there at the Api/Features/TimeEntry folder and you'll find a couple of files which represent this public API.
If you then take a look at CreateTimeEntry.cs you'll see a class which includes this
Command
.When thinking about what we need to accept in order to create a time entry, we can be very careful to only add properties to this command which are strictly necessary.
For example, note how it defines a
UserId
but not aCreatedBy
property. We may well want to set theCreatedBy
field to the id of theUser
who's logged in, but that feels like an implementation detail which should be handled in the business 'logic' for this feature (behind the API), rather than requiring any consumers of our API to pass the User Id in twice, once forUserId
and once forCreatedBy
.If we directly interact with
TimeEntry
in the client we have little flexibility to intentionally design our API, whereas this way we can be much more specific about the 'shape' of the API.Commands and Queries
I've found it very useful in recent times to think about these API calls as either Commands (which change data) or Queries (which read data).
In this example:
API/Features/TimeEntry
We have the
CreateTimeEntry
command and then aGetForSelectedDate
query:Note how we define both the query (which accepts
UserId
andSelectedDate
) and then the shape of the date to be returned (the model).Again this is very useful as we build out our API. We can easily add fields to the Model in isolation from changes to our Doman.
Shared Models
Because these models (commands and queries) live in the Shared project we can happily reference them from the Blazor WASM and Server clients.
This could also be a good place to define FluentValidation rules. That's one thing I'd like to explore next!
Refit to reduce boilerplate
Just before we move on from the Shared project there's one more thing I stumbled upon which could be really useful.
In the first version we had a TimeEntryService which acted as an HTTP Client for our API.
These clients are a pain to create because they're pretty much all boilerplate (with just the URIs and Types changing between methods).
That's where Refit comes in, and it looks pretty nice.
Instead of writing all that code manually, you can just define an interface to match the API.
For example, knowing that our API has two methods, one for creating a TimeEntry and one for getting all entries for a selected user/date, we can define that in the Shared project like so:
ITimeEntryApi.cs
Note how we define the URL (to the API) plus re-use the shared types we defined (for our command and query).
Now we can use this in our components (in place of the TimeEntryService) like so:
Refit is configured simply enough Program.cs or Startup.cs in the client projects.
API Implementation
For the actual API implementation itself, I've collapsed some of the layers and used MediatR to create handlers for our command and query.
In the TimeKeeper/Api/Features folder you'll see a TimeEntry folder.
In there you'll see these two files:
CreateTimeEntry
If you're not familiar with MedaitR this may look a bit strange at first.
Effectively we create a class and implement
IRequestHandler<>
for our command (or query).In this case the handler is for our
CreateTimeEntry.Command
.The
Handle
method then contains the logic for this feature.In a case like this, that's likely to be fairly simple, spinning up a new
TimeEntry
object and setting its properties from the incoming command.Note how we set
CreatedBy
tocommand.UserId
. This is an easy way to ensure the correct user is recorded against the record in the DB, but (as discussed earlier) without having to expose that property via the API.You might have heard people talk about vertical slices. These handlers are one way to implement those slices.
Jimmy Bogard has a couple of very useful primers about vertical slices (and how they compare to other approaches:
Why Vertical Slice Architecture is better
How Vertical Slice architecture fulfils clean architecture's broken promises
From my own experience I can only report that this has been the single biggest mindset shift which has positively impacted the projects I've worked on in the last 5-6 years. In every case where we switched to this approach we saw a massive increase in how quickly we could add features, but also a massive reduction in bugs and the ability to make changes without those changes rippling out to other parts of the application.
Summary
Reading this back I realise there's a lot of detail in here which might be hard to decipher/take in!
Although there's quite a few changes, we can break them down as follows (and for thinking about what to adopt going forward).
In terms of moving forwards, I'm thinking I might just knock up a quick screencast showing how I'd add a simple feature with this architecture. Sometimes it's just easier to see it in action than try to understand it from a description 😃
Beta Was this translation helpful? Give feedback.
All reactions