ServiceComposer / ServiceComposer.AspNetCore

🧩 ServiceComposer, a ViewModel Composition API Gateway
https://milestone.topics.it/categories/view-model-composition
Apache License 2.0
73 stars 11 forks source link

Question: Any thoughts on where composition should occur when using Blazor? #389

Open markphillips100 opened 2 years ago

markphillips100 commented 2 years ago

I've hit an architectural problem in my use of Blazor webassembly in that I'm not sure where to apply composition of information from difference service boundaries. I'll try to explain with some background info.

I've typically used this library for composing data at an API BFF for several Flutter mobile apps to consume. So from the app client perspective, they don't see the logical separation of data. They make their api calls and use a specific typed model to deserialize into. View "model" composition as opposed to View composition.

With Blazor both the client and the backend are C# so a shared model is possible (although not mandatory). This has meant a single service boundary repo can have code for not only an API controller, but also the view model shared between client and server, and the Blazor razor web components + any state management used to render the models and interact with them. So the service boundary of the repo totally encapsulates the end-to-end components.

Now the issue. Typically we want to compose some information from another boundary. It could be we are rendering a table of information related to a sales location Id but perhaps the title that we want to show in each record is provided by a different boundary. I don't see how this is possible without coupling the boundaries, either at a pure model perspective or even a web component one. The shared client/server model would have to be structured to be aware of the 2nd boundary's data, or we would have to take a reference on another boundaries web component package. Defeats the purpose of boundary separation in my opinion.

I'm thinking the only solution is to put out of my mind the fact Blazor can do shared models and impose a similar approach used for the mobile BFF. In other words, only surface composition handlers/subscribers from the service boundary repos and keep all UI components out of the service repos.

This would then mean the Blazor specific web component code from each service boundary would be refactored into a single Branding repo or even the UI host repo itself, thereby making the UI monolithic - something I was attempting to avoid.

Do you have any thoughts on this sort of use case @mauroservienti ?

mauroservienti commented 2 years ago

Mark, given that I know nothing about Blazor, would you be able to put together a basic sample that I can use to understand what is this about?

markphillips100 commented 2 years ago

Sorry @mauroservienti , it's probably not related to this library as such. More me trying to align which repo (from an ownership point of view) to put UX components that on the outset appear to be logically owned by a single service when perhaps they don't in reality.

In other words, I guess I started out trying to do view composition in addition to the usual viewmodel composition offered by service composer. However, I'm finding that some view components are a composite of other boundaries and so don't belong in a service repo. A discussion for somewhere else other than here I think. :-) Feel free to close.

mauroservienti commented 2 years ago

No need to be sorry at all, Mark. And I'm not sure it's unrelated.

Recently, I've been thinking about composition not in the context of HTTP. My use case is a WPF client application that doesn't need to go through HTTP to retrieve data. Instead, each component can directly access the database and it's hosted in the application itself. That means:

Would Blazor be a similar scenario?

markphillips100 commented 2 years ago

Sounds like you need some other mechanism to compose in the WPF app itself. In process event handling perhaps? Maybe this is what I'm trying to achieve also (without realising it).

The Blazor (wasm) approach is akin to an angular app with say a nodejs backend. SPA with a http backend where both are written in same language. This offers the opportunity for keeping both client UX components and backend API components in the same logical repository. In some ways It's similar to the WPF use case, only without the backend components.

Problem here is how to compose from a view perspective. As an example, lets say a Sales UX component renders a table of Sales data for a Product where each row is keyed on a ProductId. What do we do if we want each row to show the product's name from the Catalog service?

Sales shouldn't be aware of the need for this composition I think. You could say that this table shouldn't be rendering that column of data at all, but the requirement still stands to show both sales and catalog data in each row for each product Id. Admittedly I may have chosen a complex example by using a table but valid nonetheless.

My only solution to this problem is to not make the UX components the responsibility of the service boundaries, and move them to a more monolithic repo that can use models that map to what a backend composition API surfaces. Basically do our usual API composition but the UX component is no longer owned by a logical boundary. Same as I do for Flutter mobile apps basically.

I would really like to work out how to do this from a view composition perspective if possible though, and I imagine whatever works for WPF could possibly work from Blazor wasm too. Have you come up with something that works for your WPF use case?

markphillips100 commented 2 years ago

Just to give clarity from a real world perspective if you don't mind, let's take a look at an Amazon product page as an example but let's assume the app is a SPA rather than server rendered app, and that we are doing view composition within the SPA rather than viewmodel composition at an API level.

A single product's page composes product details, technical details, pricing, ratings, etc. All individual components that theoretically are sourced from their own logical business boundary.

I see 2 separate UI component concerns; A. one set are related to logical business boundaries that can only render their boundary's data. They manage their own state and make their own API calls to obtain data when required. They know nothing about where they will be rendered. Examples are ProductPrice, Ratings. B. the other set govern layout of components and are consumed by the page, both of which are owned/developed from a Branding perspective.

Whilst this page doesn't have a table as such it does render various collections of data, one example being "Products related to this item". Each item template would appear to be a composition of several boundaries; price, ratings, etc. The template itself though is perhaps the responsibility of Branding (the SPA app) rather than any business boundary. Likewise, the HorizontalListComponent (my name for it) used to render the templates would be a Branding component.

I have some questions given points A and B above:

  1. Should any business boundary ever own a page or is that purely the responsibility of the app?
  2. If the answer to 1 is NO, should this then imply all navigation is the responsibility of the app alone, i.e. any component that has navigable links should merely raise component events for pages to capture? Might be a bit off topic but it concerns me that a business boundary would even be aware of URLs, at least beyond relative URLs and only within the component.
  3. What is making the business call to obtain the product Ids data for "Products related to this item"?

I'll try and answer 3 myself: I imagine on initialization of RelatedProductsComponent in whatever the rendering pipeline is, it makes a call to whatever state management it's using to trigger loading of its related product data. In other words, its data access is encapsulated.

From a rendering point of view, templating would be used to separate concerns. This component is not in itself making use of the branding horizontal list component to render how it wants, but instead it is passed a template that can render a list of data of a specific type (Product IDs), and the page/layout component that makes use of RelatedProductsComponent passes the HorizontalListComponent in the template along with the item templates. Basically keep responsibility for how the list is rendered with the app/branding rather than the business component.

If a page has many components each with their own state and http calls, does this mean a really chatty SPA to backend comms? Is there some hybrid approach perhaps?

There aren't many who discuss view(model) composition I find so I really appreciate your insights.

markphillips100 commented 2 years ago

I've re-read your viewmodel composition series and this article seemed to cross common ground with my use case issue.

The code examples under the section "What are they?" describes a component called AvailableProducts from Catalog that makes use of another component AvailableProductPrice from Sales. This would seem to couple a UI component from one service with another service's UI component, at least by type name? Is this okay to do architecturally speaking?

I guess in theory the type name of that component is just configuration so isn't necessarily coupling beyond saying, "I want some Sales stuff to render here".

I possibly could do the same for my Blazor situation as I think they have the concept of dynamic components too. I'll investigate more and pause for a bit - overloaded you enough :-)

markphillips100 commented 2 years ago

@mauroservienti I created a sample application that uses both service composer for viewmodel composition and Blazor's DynamicComponent to support compile-time decoupling of each service's UI components.

This follows the guidance I mention in previous post. I'm not 100% convinced but can't think of any other way of providing composition without moving all service UI components into a monolith app. What are your thoughts?

markphillips100 commented 2 years ago

I added a 2nd blazor web app to the same sample solution to showcase a different approach. I updated the readme to explain but basically it separates layout rendering from item or list components. I wonder if this would be something you can also do architecturally for WPF?

markphillips100 commented 2 years ago

I added a 3rd web app to the sample solution just for clarity and comparison with a monolithic use case.

markphillips100 commented 2 years ago

So I added some more flavours to the sample. This time I can resolve dynamic components from other services by a "placement" contract. I think this is the best approach to allow me to be as decoupled as possible, still retain the majority of ux components with the service that owns them, and give control to the owning service boundary about their page's layout.

mauroservienti commented 1 year ago

I started carving out an InMemory implementation. It doesn't compile yet. I have a couple of unanswered design questions. Can you please chime in @markphillips100? https://github.com/ServiceComposer/ServiceComposer.InMemory/pull/1/files

markphillips100 commented 1 year ago

Yep, ask away

markphillips100 commented 1 year ago

Sorry @mauroservienti just to clarify, is this "in-memory" meant for this issue's concern with UI visual composition, or related to the other issue #392 about composing in a process? Did you see the follow up I did on that one by the way?

markphillips100 commented 1 year ago

FYI, I've been using a copy (and only slightly amended) version of the last "generic context" example mentioned in that thread. InMemoryRequest would have been a far better name than my ObjectRequest though.

It's been working well so far but of course I'd rather not have the technical debt in my codebase.

mauroservienti commented 1 year ago

I don't know, yet.

I did not have time (yet) to dive into the SignalR ideas you raised and described.

Meanwhile, I had another request to run composition in the context of an NServiceBus message handler. I started thinking that probably for that scenario and Blazor having an in memory, simple, implementation would suffice. It might even work for SignalR, but I want to find the time to dive into what you did there before making any decisions.

markphillips100 commented 1 year ago

I kinda discuss the handler integration there and came to the conclusion I didn't want to couple the in-memory behaviour with the outer consumer code. Probably easier if you look at that thread first and then continue from there? Like you said, when you get time. Busy times :-) Always here to help too