ServiceComposer / ServiceComposer.AspNetCore

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

Question: Thoughts on integrating viewmodel composition output with signalr? #392

Open markphillips100 opened 2 years ago

markphillips100 commented 2 years ago

I wasn't sure how to phrase the question to make sense. I'll explain if I may.

I currently publish some SignalR notification messages from NSB handlers for events that are of interest to client apps. These usually don't contain more than an event type and an Id. Their main usage is to inform the client app of an event occurring in the context of some Id, say a cart's OrderId as an example. The client reacts by maybe requesting a reload of its cart using the same OrderId. This would be a query to a viewmodel composition api that spans multiple services. Usual stuff here.

This push-pull approach works fine when only only a single client is likely going to be the only receiver for the message. If many 100s of clients are registered to receive the signalr message then there could be 100s of Http Get requests occurring all roughly synchronized to occur when they received the message. Bad for the backend performance.

I've recently wondered if there's a more efficient approach, architecturally speaking.

Would it be wiser to let the NSB handler use some code to make the cart API call itself and return the viewmodel composition response with the SignalR message?

It would mean the client(s) do not need to query for the data, and as we are still using the same api call the received payload would be 1-to-1 with the regular http calls.

There are some assumptions here:

  1. Max SignalR message payload size is 32Kb.
  2. Can the handler obtain the necessary data to support the route requirements, and also supply the required security context to support the http routes authorization?
  3. We are okay with making a HTTP call from the NSB event handler.

As for point 2 + 3, in some ways it would be nice if there was some other "routing" or middleware mechanism to couple viewmodel composition across service boundaries as in this specific use case the composition could happen in the same process for greater efficiency. Having endpoint routing as the coupling mechanism mandates a remote call (I think).

mauroservienti commented 2 years ago

I've been thinking about that a lot, recently (in the context of Blazor too).

ServiceComposer has two major limitations at the moment:

  1. Handlers selection is based on Routing information, and thus requires (or seems to require) an incoming HTTP request
  2. Every API assumes the surrounding context is HTTP

My gut feeling tells me that we could decouple the composition engine from HTTP by:

  1. Introducing a set of new APIs (e.g., IRequestHandler instead of ICompositionRequestHandler) abstracting away HttpRequest and providing access to the underlying HTTP context through extension methods
  2. Adding an optional set of attributes to replace the various Http* routing attributes where there is probably no need to support any RegEx kind of matching, but just exact string matching is enough

At this point, based on the chosen API ServiceComposer could decide to set up Routing Endpoints, via the endpoint data source, or a simpler in-memory dictionary to hold handlers. Users can drive this via the configuration API.

markphillips100 commented 2 years ago

This sounds like it would fit your WPF scenario you mentioned before too perhaps?

mauroservienti commented 2 years ago

Correct. It would fit any non-http scenario.

I was even thinking that one API option could be something like: ICompositionRequestHandler<TContext>

Where the engine could use the TContext type to determine the environment

markphillips100 commented 2 years ago

Decoupling from HTTP would be nice from my perspective, just from the execution in process point of view.

I was curious what form the request coupling abstraction would take, if not using Http route attributes and model binding that is?

  1. Do you couple requests by having ISubscribeHandler handle an event type and use an event published by an IRequestHandler? Similar to current behaviour
  2. Do you couple IRequestHandler imlementations by a common/shared request type?

At the moment, rightly or wrongly, I might have a subscriber bind to some url query parameters that a related composition request handler is not concerned with. A consumer of the new abstraction would need to know to supply everything that is required across all boundaries. Kinda edges towards point 2 perhaps?

Just seen your response. Is this perhaps what the TContext is?

markphillips100 commented 2 years ago

I still also see value in event subscription too though. So that the boundary that owns the request type can publish related Ids from its domain that the subscription can use to composite an appropriate response. I wouldn't expect the request type to know about this data.

mauroservienti commented 2 years ago

Just seen your response. Is this perhaps what the TContext is?

Yup. The idea is that the TContext thingy would allow to mimic the exact same behavior as today without coupling to HTTP.

Given my experience with the NServiceBus API I don't think we need a strong abstraction, a very thin one would be more than enough. If you're writing a handler, or a subscriber for what matters, I don't expect anyone to be able to use the same handler in different context. The handler knows if it's designed for SignR or HTTP or whatever. It's just the engine that needs to abstract await how handlers are registered and executed.

markphillips100 commented 2 years ago

Awesome. When can we have it? Only kidding.

mauroservienti commented 2 years ago

I tried and I failed 😵‍💫😎 The current implementation has too many dependencies on ASP.Net features or concepts, making it hard to extract a core composition logic.

I think a better option would be to have a separate ServiceComposer.SignalR (and maybe ServiceComposer.Blazor) package with its own implementation. If it turns out there are shared types in the future, it's easy to add a base ServiceComposer package shared among the others.

What do you think?

markphillips100 commented 2 years ago

Thank you for giving it a go, it's a real head-scratcher for sure. I figured those MVC parts would get in the way. Default action results, request binding, etc.

I'm not quite sure there's a use case for separate packages, at least not from my perspective. The "only" :-) issue I see is that the execution of composition is tightly coupled to endpoint routing, in terms of handler/subscriber registration, runtime grouping in terms of an execution context (route template), and implicit pipeline execution (bound to an endpoint delegate).

As for use cases outside of HTTP, I can only think of a use case where some consuming logic might want to call the pipeline direct and in-process, kinda like a mediator send request e.g. serviceComposer.HandleComposableRequest(compositionContext).

As further clarification, I can't imagine wanting to have composition handlers bound (like they are for endpoints) to say an NSB handler, or a SignalR incoming method, or a gRPC service method. I can only think all of those consumers would have their specific method/handler code call directly into the composition pipeline, kinda like your blog example for notifications sends a http request to a composition endpoint and does something with the returned data, but in my (NSB handler->SignalR method) use case it would just call HandleComposableRequest and send the resulting dynamic back through a signalr method, i.e. SC is unaware of how it's consumed. The only real difference between this use case and the http one is that the pipeline is executing in-process with the consumer code.

Had you some other use case in mind, or a different interpretation of what I meant before?

Some thoughts on implementation:

The problem with doing this of course is the tight coupling with endpoint routing, both in terms of type registration and the execution of the CompositionHandler and its HandleComposableRequest needing the types and the HttpRequest. This would need to change I think to take a single CompositionContext but one that is able to understand the context of the requests. So a derived HttpCompositionContext would have a HttpRequest instance and handlers would obtain this to do binding as they wish, set action result on response etc. And any of the original CompositionHandler code would have to be moved to this Http specific composition context.

For non-http, in-process requests we would need a different composition context that only works with a POCO, and has handling logic that does not deal with http requests at all. It would need to support a result of some description so handlers could emulate the non-exception error handling (like SetActionResult). Easier said than done of course :-) I was thinking along the lines of InProcessCompositionContext<TRequest, Result<dynamic>>, and it takes a request object of TRequest and returns a type that can express errors explicitly or data. Of course, all handlers and subscribers are specific to the type of composition context. The two are not interchangeable.

markphillips100 commented 2 years ago

I've forked and am part way through an attempt. Not sure how much time I can spend on it over the next week or so but will report back with something hopefully, if only to say I failed lol.

markphillips100 commented 2 years ago

@mauroservienti I've made an attempt at some sort of separation of concerns in this branch.

I stripped out a lot of what would be obsolete code but also code that potentially should be added back (if this is a valid approach that is), namely factory stuff. I only did this so I could focus better.

I've modified the original unit tests to utilise this new way of using a IHttpCompositionContext, and added a separate test project for the IObjectCompositionContext stuff. Not sure on the naming of course but everything is up for change as this is only a brain-storm :-)

The new object-based approach still makes use of Http route attributes. I didn't want to use a POCO as then there's the concerns of sharing it among different business services. Just a string to couple handlers across domains didn't provide enough data, so it seemed simpler to try and re-use routing, or at least TemplateParser that's built-in to aspnet core.

Each of the 2 context approaches gets their own route-template based, in-memory registry of their own context-based route components, so they are isolated from that perspective.

I only implement HttpGet in the object-based world. I wasn't sure there's a use case for other verbs?

Incidentally, I was able to use a similar model binder as before, just with a DefaultHttpContext inserted where we don't have one. All binds still, well, route params do at least. There's no body support in this example.

Anyway, see what you think.

markphillips100 commented 2 years ago

@mauroservienti I further branched off that branch with a slightly amended implementation to support abstracting away the result type so as for the core not to be dependent on "result" implementations, namely FluentResults in my example (a lib I use a fair bit in projects). An additional project now provides the implementation for a FluentResults.Result result. Tests updated to show example.

Each "result" type implementation gets its own singleton registry so different implementations in-process will get their own set of isolated handlers.

My gut makes me think I can abstract the ObjectRequest type further to obtain a single core to use in both endpoints and non-endpoints use cases. Possible another branch coming :-)

The new branch is at generic-result-context.

markphillips100 commented 2 years ago

@mauroservienti As predicted, I have another variation to consider :-) New branch at generic-context.

This one generalizes composition context, handler and the registry as generic implementations so either endpoint-based (HttpRequest/IActionResult) or object-based (ObjectRequest/some custom result type) can use the same underlying behaviour.

I've separated out the ObjectRequest/FluentResults implementation into its own project to demonstrate how one would implement custom results. The Http endpoint approach supports IActionResult as the result type. No need for any other result types for endpoints.

I think this is closer but I'm worried the viewmodel factory stuff I took out may not be able to re-integrate.

The handler type arguments are a bit long too, so might think about introducing some derived interface to make the handlers easier on the eye like previous branch iterations. This would make the request type implicit in the interface name. As an example:

  1. IHttpCompositionContext : ICompositionContext<HttpRequest, IActionResult> and
  2. IObjectCompositionRequest<TResult> : ICompositionContext<ObjectRequest, TResult>