I am using a Razor file inheriting from a base which derives from ComponentBase. My understanding is, these two files should generally be responsible for handling UI related tasks. That being said, should I put my calls to my Data Services in low level components? Or should I keep the calls to them in a higher level component which could orchestrate data services and then simply pass data down to the components to handle the rendering? (When I refer to high or low level, I mean a parent component would be high level and a grandchild would be low level)
As I understand it, the interface injected to handle the data services would hold the same resources (being a singleton). So my question is not concerning the management of resources so much as it is about keep things SOLID. Where should the data services be used? Everywhere or isolated? Thanks!
I'll jump in as a big supporter of isolating services to a base class. The issue I kept running into before I came to this conclusion was that spreading the service calls around everywhere gets confusing as app size and complexity increases. It's very tempting to build each component as an atomic thing that handles everything on it's own and gets it's services injected, but once all those components start composing together and need to start talking to each other it becomes a huge headache. This compounds when you have something like a singleton where any state could be involved, as the underlying state for a component can easily be changed by another component. (sometimes not intentionally - see EF Core and Data Tracking and the fun you can have when the data being tracked is referenced from 2 components - or worse, 2 seperate client connections on Blazor Server) Before you know it, there are simply too many places to look for errors or to make changes when changes need to be made, and tracking down bugs becomes nightmarish.
The second route to component autonomy is to use use cascading parameters, but whenever you do you are coupling your components to a concrete component somewhere up the DOM tree, and avoiding coupling is the whole point of SOLID. It's generally better to have each component represent very simple functionality that can be composed to create richer experiences for the user.
So where I've found success is to isolate services as you mentioned in a base class, and then keep every component down the DOM tree as dumb as possible, which has had a dramatic effect on my output and my ability to find and fix errors. In fact I have one project that I had to scrap twice before I started this approach and now I'm at a functional application and building features out at a good clip. (Thank god it's a hobby project!)
The approach for this isn't very complicated at all. In the base class I'll expose method calls and properties as protected where needed and keep everything else private as much as possible, so the external visibility is at an absolute minimum. All service calls happen within the base class as well and are encapsulated in private methods, and that breaks the connection between the service and the UI. Then I'll pass data down the DOM tree as component parameters, and I'll pass functionality down as parameters of type EventCallback<T>
.
Consider the classic list of orders as an example. I can load a list of orders by a customer ID, and then expose lists of the orders that are open and the orders that are closed simply by using expression bodied members to filter a master list. All this happens in the base class, but I set it up so the only things the UI has access to are the sub-lists and the methods. In the below example I represent service calls through console logs but you'll get the idea, and how you mentioned building things out in your questions is essentially this:
OrdersBase.cs
public class OrdersBase : ComponentBase
{
private List<Order> _orders = new List<Order>();
protected List<Order> OpenOrders => _orders.Where(o => o.IsClosed == false).ToList();
protected List<Order> ClosedOrders => _orders.Where(o => o.IsClosed == true).ToList();
protected void CloseOrder(Order order)
{
_orders.Find(o => o.Id == order.Id).IsClosed = true;
Console.WriteLine($"Service was called to close order #{order.Id}");
}
protected void OpenOrder(Order order)
{
_orders.Find(o => o.Id == order.Id).IsClosed = false;
Console.WriteLine($"Service was called to open order #{order.Id}");
}
protected override async Task OnInitializedAsync()
{
Console.WriteLine("Calling service to fill the orders list for customer #1...");
// quick mock up for a few orders
_orders = new List<Order>()
{
new Order() { Id = 1, OrderName = "Order Number 1", CustomerId = 1 },
new Order() { Id = 2, OrderName = "Order Number 2", CustomerId = 1 },
new Order() { Id = 3, OrderName = "Order Number 3", CustomerId = 1 },
new Order() { Id = 4, OrderName = "Order Number 4", CustomerId = 1 },
new Order() { Id = 5, OrderName = "Order Number 5", CustomerId = 1 },
};
Console.WriteLine("Order list filled");
}
}
Now I can consume the base class in the top level component and I'll have access to the protected and public members only. I can use this high level component to orchestrate how the UI will be arranged and pass out methods for delegates, and that's all it has to do. This is very light as a result.
Orders.razor
@page "/orders"
@inherits OrdersBase
<div>
<h3>Open Orders:</h3>
<OrdersList Orders="OpenOrders" OnOrderClicked="CloseOrder" />
</div>
<div>
<h3>Closed Orders:</h3>
<OrdersList Orders="ClosedOrders" OnOrderClicked="OpenOrder" />
</div>
The OrderList component is then responsible then for rendering a list of OrderItems and passing along a delegate action. Again, just a simple, dumb component.
OrderList.razor
<div>
@foreach (var order in Orders)
{
<OrderItem Order="order" OnOrderClicked="OnOrderClicked.InvokeAsync" />
}
</div>
@code {
[Parameter]
public List<Order> Orders { get; set; }
[Parameter]
public EventCallback<Order> OnOrderClicked { get; set; }
}
Now the OrderItem list can render something about the order and act as a click target, and when the order is clicked, it is invoking the delegate all the way back to the base class and that is where the method runs. The OrderClicked method also checks the EventCallback, so if there isn't a delegate assigned, the click doesn't do anything.
OrderItem.razor
<div @onclick="OrderClicked">
<p>Order Name: @Order.OrderName</p>
</div>
@code {
[Parameter]
public Order Order { get; set; }
[Parameter]
public EventCallback<Order> OnOrderClicked { get; set; }
private void OrderClicked()
{
if(OnOrderClicked.HasDelegate)
{
OnOrderClicked.InvokeAsync(Order);
}
}
}
All this comes together to make component that displays orders, and if you click on an open order it moves the closed order list, and vice versa. All the logic is in the base class, and each component has a simple job to do, which makes reasoning about it much easier.
This will also give me an indicator of when I need to decompose a component into smaller components as well. I'm of the philosophy that not too much should be presented to the user at once, so each page should be concise, simple, and not be expected to do to much. To that end, when I build things like this I can tell that I'm going to far when my base class or parent UI razor files start to bloat out, and that prompts a refactoring of parts of the functionality to another dedicated page. It makes for more files, but it also makes things much easier to build and maintain.
This turned out to be a long answer to a short question. You might agree with me and you might not, but hopefully it helps you decide how to proceed either way.