serverlesstechnology / cqrs

A lightweight, opinionated CQRS and event sourcing framework.
Other
370 stars 40 forks source link

Allow searching for aggregate instance without aggregate ID #90

Closed nanderstabel closed 7 months ago

nanderstabel commented 8 months ago

My team is happily utilizing this crate, however, we're a bit stuck at the moment with the following:

It seems like aggregate instances can only be retrieved from the store by their aggregate IDs(?). For example, if I have a BankAccount Aggregate:

pub struct BankAccount {
    account_id: String,
    first_name: String,
    last_name: String,
    email: String,
    balance: f64,
}

Then I can indeed the specific BankAccount instance using account_id (from https://github.com/serverlesstechnology/cqrs-demo/blob/main/src/route_handler.rs#L11-L26):

pub async fn query_handler(
    Path(account_id): Path<String>,
    State(state): State<ApplicationState>,
) -> Response {
    let view = match state.account_query.load(&account_id).await {
        Ok(view) => view,
        Err(err) => {
            println!("Error: {:#?}\n", err);
            return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response();
        }
    };
    match view {
        None => StatusCode::NOT_FOUND.into_response(),
        Some(account_view) => (StatusCode::OK, Json(account_view)).into_response(),
    }
}

But in some situations I might not have access to the account_id, but I still want to be able to find the BankAccount instance that I need, for example using an email address:

pub async fn query_handler_2(
    email: String,
    State(state): State<ApplicationState>,
) -> Response {
    let view = match state.account_query.find(&email).await {    // <--- use a 'find' method instead of 'load'.
        Ok(view) => view,
        Err(err) => {
            println!("Error: {:#?}\n", err);
            return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response();
        }
    };
    match view {
        None => StatusCode::NOT_FOUND.into_response(),
        Some(account_view) => (StatusCode::OK, Json(account_view)).into_response(),
    }
}

So my main question is, what is the intended way of implementing a way to retrieve an Aggregate instance not by its ID, but rather by something else (email or something else)?

Else would it make sense to add a find/search method to the ViewRepository trait? If so, I would be more than happy to contribute this.

davegarred commented 8 months ago

Hi @nanderstabel,

Thanks for using the package and the feedback.

Just to be clear, I believe we are talking about loading a view using something other than aggregate ID, and not about loading the aggregate. The latter is something you should never need to do while the former is something that you absolutely need in nearly any real world application.

This is a known limitation of the current ViewRepository. One that I'm not happy about but I have not yet found a clean way to extend this functionality while still keeping the package lightweight (or maybe it is time to change our tenets).

For internal applications we have built custom read-only repositories as their construction is rather trivial with the databases supported and, for that matter, they would be implemented that way in most other applications anyway.

nanderstabel commented 7 months ago

Thank you!

Yeah you're right I was confusing view and aggregate ID. In the mean time we got what we wanted just by using a second set of:

This way every time a new account is created, not only the AccountView gets updated, but the EmailView as well.

So in the previous example we are now doing something like this:


pub async fn query_handler(
    Path(email): Path<String>,
    State(state): State<ApplicationState>,
) -> Response {
    // Retrieve the account_id using the email
    let account_id = match state.email_query.load(&email).await {
        Ok(view) => view.account_id,
        Err(err) => {
            println!("Error: {:#?}\n", err);
            return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response();
        }
    };

    // Now the full account view can be retrieved using the account_id
    let view = match state.account_query.load(&account_id).await {
        Ok(view) => view,
        Err(err) => {
            println!("Error: {:#?}\n", err);
            return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response();
        }
    };

    match view {
        None => StatusCode::NOT_FOUND.into_response(),
        Some(account_view) => (StatusCode::OK, Json(account_view)).into_response(),
    }
}

Thanks for building this crate!

If you have nothing else to add please feel free to close this issue :)