Blazor - Where to put your domain logic

Published on

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…

Legacy .NET web apps causing you grief?

Build modern, reliable web applications, faster with .NET and Blazor.

Build better .NET web apps

In this article we’ll:

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.

image-20191127102657670

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:

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:

image-20191127102545354

To Blazor components:

image-20191127102740422

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:

image-20191127111055763

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:

image-20191127110823806

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.

image-20191127112204699

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.

image-20191127115604277

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.

image-20191127120232838

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:

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:

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.

image-20191127151240203

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:

Ultimately pragmatism always wins, just try to avoid accidentally boxing yourself into a corner along the way!

Legacy .NET web apps causing you grief?

Build modern, reliable web applications, faster with .NET and Blazor.

Build better .NET web apps
Next Up
  1. How to learn Blazor when you don't use it for your day job
  2. Shared models - Blazor's (not so) secret super power
  3. But, which flavor of ASP.NET?