Related with #90
When we use CQRS the main idea is to segregate commands and queries right?
On this crate we have control over the aggregate_id when we are managing commands, however when we build a view the view_id is populated always with the aggregate_id of an specific aggregate.
This second part for me breaks the main advantage of CQRS, first of all I should be able to use events from 2 different aggregates and secondly (as the problem on issue #90) in many cases the query result (let's call it view record) will not match 1:1 with an aggregate nor have the same id.
![image](https://private-user-images.githubusercontent.com/8631522/325240021-43a51f96-a0aa-454c-8c27-f8873f3bf4d8.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTg1MTI5NzgsIm5iZiI6MTcxODUxMjY3OCwicGF0aCI6Ii84NjMxNTIyLzMyNTI0MDAyMS00M2E1MWY5Ni1hMGFhLTQ1NGMtOGMyNy1mODg3M2YzYmY0ZDgucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDYxNiUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA2MTZUMDQzNzU4WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MGRhNmFkZTgyZDEwMmVmZmRhMmNhOGE2YWM3ZDBjNDg4NTBiMzNkZjhlN2UyYTk3YjhmMWRmNjg2NGMxMGIwYSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.yVJi7naax7zJXVOTpfGZdpcHSFTILfarcsuzRrzNaZU)
You can see in the picture how the Query and Command model are not 1:1 (and they should not)
Example where aggregate id is different from view id:
- Aggregate Portfolio
- View UserPortfoliosOverview --> aggregated view of all portfolios for a specific user
The Portfolio aggregate commands will use a generated id for identity and will add the user_id as part of the metadata.
The view will use user_id as a key, and in every PortfolioCreated event will decide if it updates the view or not.
Example with different aggregates:
- Aggregate Portfolio
- Aggregate Markets
- View PortfolioUsdValueView --> it will update the portfolio value when a NewPriceEvent is emitted by Markets or a NewAssetAdded is emitted by Portfolio
This case can be managed if we consider PortfolioUsdValue as an aggregate, but the reality is just a view (we will never execute a command from users).
We need to be able on this scenario to receive events from Portfolio and Market aggregates.
What I found
When we submit a command we are able to provide 3 parameters:
- aggregate_id
- payload
- metadata
cqrs.execute_with_metadata(aggregate_id, payload, metadata)
However when we write a query we can only update the payload of the view record.
When I see the implementation of execute_with_metadata
I see why the aggregate_id becomes the view_id (this is only a specific case but not the general expected behavior of a query entity)
pub async fn execute_with_metadata(
&self,
aggregate_id: &str,
command: A::Command,
metadata: HashMap<String, String>,
) -> Result<(), AggregateError<A::Error>> {
...
for processor in &self.queries {
let dispatch_events = committed_events.as_slice();
processor.dispatch(aggregate_id, dispatch_events).await;
}
Ok(())
}
Then I see the trait Query has couple of things I'm not fully aligned:
- it expects a Aggregate type (why not a ViewRecord or a QueryEntity type)
- there is no way to specify nor change the view_id
- the aggregate_id is already part of EventEnvelope, so there is no need for it as parameter of
dispatch
function
Current implementation:
#[async_trait]
pub trait Query<A: Aggregate>: Send + Sync {
/// Events will be dispatched here immediately after being committed.
async fn
dispatch(&self, aggregate_id: &str, events: &[EventEnvelope<A>]);
}
Proposal
So maybe we could change this into something like:
#[async_trait]
pub trait Query<A: ViewRecord>: Send + Sync {
/// no need for aggregate_id parameter
async fn dispatch(&self, events: &[EventEnvelope<A>]);
}
#[async_trait]
pub trait ViewRecord: Default + Serialize + DeserializeOwned + Sync + Send {
fn view_record_id() -> String;
fn apply(&mut self, event: Self::Event);
}
And to not disrupt current implementation we can provide From
implementations like:
impl From<Aggregate> for ViewRecord ...
Share your thoughts I may be missing something.