Eventuous / eventuous

Event Sourcing library for .NET
https://eventuous.dev
Apache License 2.0
442 stars 70 forks source link

Provide StreamContext in UpdateBuilder<TBuilder>.Configure() for MondoDb #300

Closed iappwebdev closed 9 months ago

iappwebdev commented 9 months ago

Hey,

I would like to use ArrayFilters in a EventHandler for MondoDB. I have a PickupstationDocument with shelves for articles to pick up. My goal is to react on a event ItemQuantityChanged on a Reservation-Stream.

I have to use ArrayFilters because I need two positional operators: one for the shelve with the matching ReservationId and one for the article with the matching ArticeName.

The corresponding event looks like this:

// E.g. new ItemQuantityChanged("Ibuprufen", 5) in Stream "Reservation-123"
public record ItemQuantityChanged(string ArticleName, int NewQuantity);

The problem is that I can specify ArrayFilters in the UpdateBuilder<TBuilder>.Configure()-Method, but I do not have access to the MessageConsumeContext, as it is the case for .Filter() and .Update(). So I cannot dynamically specify the values for ReservationId and ArticeName.

It would be nice to have an additonal method like .ConfigureFromContext. Is there any change to get this working? Maybe it is also considerable to

The PickupstationDocument

[{
  "_id": "d30c58a3-56fd-4aab-a88d-253f76f03e8c",
  "Shelves": [
    {
      "Shelvenumber": 1,
      "Reservation": {
        "Id": "123",
        "Articles": [
          { "ArticleName": "Ibuprufen", "Quantity": 1 },
          { "ArticleName": "Paracetamol", "Quantity": 2
          }
        ]
      }
    },
    {
      "Shelvenumber": 2,
      "Reservation": {
        "Id": "456",
        "Articles": [
          {"ArticleName": "Headache pills","Quantity": 3 }
        ]
      }
    }
  ]
}]

My attempt with ArrayFilters with static values

On<ItemQuantityChanged>(
    builder =>
    {
        return builder
            .UpdateOne
            .Filter(
                (ctx, filter) => filter.ElemMatch(
                    pickupstation => pickupstation.Shelves,
                    Builders<Shelve>.Filter.Eq(a => a.Reservation!.Id, ctx.Stream.GetId())
                )
            )
            .Configure(
                // Here I need the context to get reservationId from the stream name and the article name from the event
                options => options.ArrayFilters = new List<ArrayFilterDefinition>
                {
                    new BsonDocumentArrayFilterDefinition<BsonDocument>(new BsonDocument("shelve.Reservation.Id", "c26f5418-d65e-4165-a211-8f85c4e84ca7")),
                    new BsonDocumentArrayFilterDefinition<BsonDocument>(new BsonDocument("article.Name", "Ibuprufen"))
                }
            )
            .UpdateFromContext(
                (ctx, update) => update.Set("Shelves.$[shelve].Reservation.Articles.$[article].Quantity", ctx.Message.NewQuantity)
            );
    });

The mentioned methods in UpdateOneBuilder

public TBuilder UpdateFromContext(BuildUpdate<TEvent, T> buildUpdate) {
    _buildUpdate = (ctx, update) => new ValueTask<UpdateDefinition<T>>(buildUpdate(ctx, update));

    return Self;
}

public TBuilder Update(BuildUpdateFromEvent<TEvent, T> buildUpdate) {
    _buildUpdate = (ctx, update) => new ValueTask<UpdateDefinition<T>>(buildUpdate(ctx.Message, update));

    return Self;
}

[PublicAPI]
public TBuilder Configure(Action<UpdateOptions> configure) {
    _configureOptions = configure;

    return Self;
}
iappwebdev commented 9 months ago

I found another way doing this. In fact I have all necessary informations in the .UpdateFromContext()-Method. So in there I can perform a standard MongoDb-Update.

The only thing I could not figure out, how to return an "empty" Update operation, so I do an additional call to update.Set(). Maybe someone has a hint how to solve this better?

On<ItemQuantityChanged>(
    builder =>
    {
        return builder
            .UpdateOne
            .Filter(
                (ctx, filter) => filter.ElemMatch(
                    pickupstation => pickupstation.Shelves,
                    Builders<Shelve>.Filter.Eq(a => a.Reservation!.Id, ctx.Stream.GetId())
                )
            )
            .UpdateFromContext(
                async (ctx, update) =>
                {
                    var updateDefinition = Builders<PickupstationDocument>
                        .Update
                        .Set("Shelves.$[shelve].Reservation.Articles.$[article].Quantity", ctx.Message.NewQuantity);

                    var arrayFilterDefinitions = new List<ArrayFilterDefinition>
                    {
                        new BsonDocumentArrayFilterDefinition<BsonDocument>(
                            new BsonDocument("shelve.Reservation.Id", ctx.Stream.GetId())
                        ),
                        new BsonDocumentArrayFilterDefinition<BsonDocument>(
                            new BsonDocument("article.Name", ctx.Message.ArticleName)
                        )
                    };

                    MongoDB.Driver.UpdateResult updateResult = await Collection
                        .UpdateOneAsync(
                            x => x.Shelves.Any(shelve => shelve.Reservation != null && shelve.Reservation!.Id == ctx.Stream.GetId()),
                            updateDefinition,
                            new UpdateOptions
                            {
                                ArrayFilters = arrayFilterDefinitions
                            }
                        )
                        .ConfigureAwait(true);

                    return update.Set(x => x.LastChanged, ctx.Created);
                }
            );
    }
);

Nevertheless the original problem is fixed.