Blazor - Where to put your domain logic
November 28, 2019 · 10 minute read · Tags: blazor
It’s fair to say you’re not lacking choices when it comes to choosing how to build the UI part of your web applications these days.
Blazor, Razor Pages, MVC, React.js; the list is endless.
But with all this choice comes “framework fatigue”.
When something like Blazor arrives on the scene, your inner voice is basically saying…
Oh great, now I have to:
a) learn YAF (yet another framework)
b) decide whether to rebuild all my existing applications
c) cross my fingers and hope MS doesn’t pivot in another direction so I have to start from scratch again
Blazor itself takes a little learning.
You need to re-program your brain to think in components, figure out how to make components talk to each other, work out how to retrieve data etc.
It’s a lot to learn, and it takes investment (of time and potentially money).
But, there’s another, more fundamental factor which dictates whether its a) dead simple or b) unbelievably difficult for you to adopt new UI frameworks.
And that’s your application architecture…
In this article we’ll:
- Explore some of the common architectural pitfalls
- Look at some alternatives
- See how to implement these alternatives in your Blazor apps
All mixed up and nowhere to go
Let’s say you’ve got an existing online store application built using MVC.
It’s been built over a number of years, and has grown bigger and bigger, to the point where a lot of important domain/business knowledge is tied up in the code.
Now imagine, for whatever reason, a lot of this business logic is buried in controllers.
You can tell if your controllers are doing too much; open one up and if it’s big (hundreds or even thousands of lines long), there’s probably some business logic mixed up in there!
Business logic is a slippery thing, it has a nasty habit of finding its way into all sorts of weird and wonderful places.
But once it gets mixed up with presentation concerns, your ability to “plug in” different UI frameworks, without expending a lot of time and energy (and taking on a lot of risk) , is severely hampered.
In this scenario, if you were to take the image above and light up the boxes which have business logic, you’d find most of the boxes lit up.
Business logic is pretty much anything which isn’t directly related to which markup to show on the screen, so:
- Calculations, formulae
- Rules
- Auth (permission to perform certain actions, roles etc)
- Queries to retrieve data
- Commands to change (and save) data etc.
In our MVC application, this business logic has permeated every part of our app (controllers, views etc).
Just “swap out the UI”, simple right?
Now imagine you want to adopt Blazor.
The bit you want to “swap out” is the MVC bit; from this:
To Blazor components:
But you can’t! Because there’s important business logic buried in those controllers, possibly even in the views.
At this point, you have little choice but to go an a “business logic hunt” (you’re not scared).
You need to find all the business logic which has found its way into your controllers and views, and move it somewhere else.
Depending on the size of your application, this might take hours or days (or, let’s face it, weeks).
But it’s not just the controllers and views you need to worry about; you also run into…
Massively Distributed Features!
This is 100% a term I just made up, and it might sound like good thing, but it isn’t.
This is when your features: things like viewing products, adding and updating products, fulfilling orders, are broken up into hundreds of tiny pieces and distributed (or, you might say, littered) all around your application.
Want to pull together all the code for checkout, and move it to Blazor? Good luck, the code is split between 15 services, 3 repositories, 2 controllers, 2 views and 6 partials!
If you find yourself wearing out the F12 key on your keyboard trying to understand a feature in your application, you might have massively distributed features.
Here’s a relatively tame example:
All the connections (arrows in the diagram) between controllers, services and repositories, represent a path you have to walk down if you’re going to understand what a feature does.
In any large application this picture is likely to be significantly more complicated and confusing.
Suddenly, migrating your application just got harder.
You have to spend a lot of time finding all the logic for your features in your services, managers and repositories, unpicking and consolidating it into one cohesive piece, just so you can migrate one feature to Blazor.
Improving your architecture
So, we know mixing presentation concerns with business logic, and spreading features around all parts of your codebase makes it harder to manage, maintain and ultimately migrate to a different UI framework.
What’s the alternative? Whether you’re migrating an existing app, or starting with a nice clean fresh Blazor application, there are a few things you can do to keep your application in check.
Keep the UI logic separate
Give yourself a fighting chance by separating UI and business logic concerns.
Ideally, Blazor components, Razor pages and MVC views/controllers will handle:
- Markup
- UI logic (showing, hiding elements etc) markup
- UI interactions, such as button clicks
- Client side validation (because client-side validation provides a better user experience)
- Orchestrating with the business domain (to change, query data etc.)
Beyond that, delegate to the business domain for everything else.
Consolidate your features
When it comes to your domain. Consider organising your code around your features.
I’m a big fan of feature folders, especially when starting on a reasonably small application (such as our online store example).
The key is to really pause and think about the features you need to build.
Talk through the system, identify and document the important domain concepts (products, fulfillment etc) then hone in on the features you need to build e.g. viewing products, increasing stock, checkout.
Once you’ve done that you’re ready to start on a feature and at this point I typically create feature folders.
The feature folder is where your feature takes shape; components, views, controllers all go in here.
Here’s an example in Blazor.
With this approach, it’s trivial to find what you’re looking for (need to work on Checkout? go to the Checkout folder) and easier to build and maintain specific parts of the application.
Use Vertical Slices
Abstractions such as repositories, services, managers etc. tend to spread horizontally.
Once a class has a few methods it has this nasty habit of attracting more, and more, and more, growing bigger and bigger.
As these layers grow, their responsibilities become muddled and it becomes harder to track down what code goes where.
One alternative is to use something which Jimmy Bogard refers to as Vertical Slices.
The gist is, when you’re working on any given feature you’re going to work on all the “layers” of that application: the UI code, the business logic, persistence.
For example, imagine you need to add a new LastSold
field to a product.
You’re going to:
- Add the field to the entity (so it can be persisted and retrieved)
- Map that new field in your business logic (presumably some kind of query, exposed via a service)
- Update your Blazor component (or MVC View etc.) to display it
- Update validation, business rules etc. to accommodate the new field
In a big project built on layers, this can lead to a non-trivial amount of time jumping between all the different parts of your code to make this simple change (and it’s not always easy to find the code you need to change).
With vertical slices, you can avoid creating all those layers and abstractions up front, and instead take a pragmatic approach to building the feature, starting with the least abstractions you can possibly get away with, and crucially, all the code for the feature is one place (after all, if it changes together it belongs together!).
Vertical slices using MediatR
One way to adopt vertical slices is to use something like MediatR to represent each discreet feature in your system.
For example, imagine you want to retrieve a list of products (to show in your Blazor component).
You could create a MediatR query, which returns a model for this specific feature (use case).
public class List
{
public class Query : IRequest<Model>
{
}
public class Model
{
public List<ProductSummary> Products { get; set; }
public class ProductSummary
{
public int Id { get; set; }
public string Name { get; set; }
}
}
}
This represents a very specific query in the application; which returns a list of products including a little summary detail (id
and name
).
You’ll need a handler to actually retrieve the data from a database and map it to ProductSummary
.
public class QueryHandler : IRequestHandler<Query, Model>
{
private readonly ApplicationDbContext dbContext;
public QueryHandler(ApplicationDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<Model> Handle(Query request, CancellationToken cancellationToken)
{
var list = dbContext.Products.Select(x => new Model.ProductSummary
{
Id = x.Id,
Name = x.Name
});
return new Model { Products = list.ToList() };
}
}
This feature is now ready to be used wherever you see fit; including:
From a Blazor component
@using MediatR
@inject IMediator Mediator
<h3>List</h3>
<ul>
@foreach (var product in Model.Products)
{
<li>
<a href="/@product.Id"> @product.Name</a>
</li>
}
</ul>
@code {
protected Queries.List.Model Model;
protected override async Task OnInitializedAsync()
{
Model = await Mediator.Send(new Queries.List.Query());
}
}
From an MVC controller
public class ProductController : Controller
{
private readonly IMediator _mediator;
public Product(IMediator mediator)
{
_mediator = mediator;
}
public async Task<IActionResult> Index()
{
var list = await _mediator.Send(new List.Query());
return View(list);
}
}
And just as easily from an API controller, or a Razor Page.
This single abstraction (the mediator) manages a few things, it:
- Abstracts at your feature level (encapsulate your features)
- Makes your features easily re-usable
- Gives you a common interface for executing your business logic from pretty much UI framework
It works just as well for commands (that change your application’s data).
public class Delete
{
public class Command : IRequest
{
public int Id { get; set; }
}
}
Which you can also call from anywhere:
@using Architecture.Features.Products.Commands
@using MediatR
@inject IMediator Mediator
<h3>List</h3>
<ul>
@foreach (var product in Model.Products)
{
<li>
<a href="/@product.Id"> @product.Name</a>
<button @onclick="()=>Delete(product.Id)">Delete</button>
</li>
}
</ul>
@code {
protected Queries.List.Model Model;
protected override async Task OnInitializedAsync()
{
Model = await Mediator.Send(new Queries.List.Query());
}
protected async Task Delete(int id)
{
await Mediator.Send(new Delete.Command { Id = id });
}
}
Where to put your handlers
You can start by keeping your MediatR classes (queries and commands) in the feature folder.
These then represent the distinct operations you can perform on your business domain: list products, update product etc.
Where next?
You can go a long way with this approach, and for a percentage of many applications you can keep the code in a Mediator handler.
When your app grows, and features accrue more complex business logic, you can always refactor to push your code “down” out of the mediator handlers, ideally into domain objects which represent core truths about your business domain.
But whatever you do, for your own sake and the next developer’s, try to keep domain logic out of your controllers, views and Blazor components!.
In summary:
- Separate your UI and business domain concerns
- Make it easy to call your business domain from any UI framework
- Encapsulate (and organise your code around) features
Ultimately pragmatism always wins, just try to avoid accidentally boxing yourself into a corner along the way!