JasperFx / wolverine

Supercharged .NET server side development!
https://wolverinefx.net
MIT License
1.17k stars 128 forks source link

Update aggregates before writing response using HTTP endpoints and the aggregate workflow #658

Open rena0157 opened 6 months ago

rena0157 commented 6 months ago

Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

When using the aggregate workflow and HTTP endpoints it would be nice if the handler wrapper would apply the changes to the aggregate before it is returned.

For example, lets say I have the following http endpoint:

    [Transactional]
    [WolverinePost("/sidekicks/{sidekickId}/rename/{name}")]
    public static (Sidekick, SidekickRenamed) HandleAsync(
        Guid sidekickId, 
        string name, 
        [Aggregate("sidekickId")] Sidekick sidekick
    )
    {
        var renamed = new SidekickRenamed(sidekickId, name);
        return (sidekick, renamed);
    }

Currently this will return the state of the sidekick without the rename event applied. The current work around is to do something like this:


    [Transactional]
    [WolverinePost("/sidekicks/{sidekickId}/rename/{name}")]
    public static (Sidekick, SidekickRenamed) HandleAsync(
        Guid sidekickId, 
        string name, 
        [Aggregate("sidekickId")] Sidekick sidekick
    )
    {
        var renamed = new SidekickRenamed(sidekickId, name);
        return (sidekick with { Name = name }, renamed);
    }

Which is to actually apply the event yourself, which is not ideal.

Note: I have tried using IdentitySessions and it still did not seems to update the aggregate before writing the HTTP response.

Arne-B commented 5 months ago

In general you need to validate if the event actually can be applied. Best way to do this is to actually try. Maybe the sidekick can not be renamed after it choose a superpower? In this case you should not apply the renamed event to the stream.

    [Transactional]
    [WolverinePost("/sidekicks/{sidekickId}/rename/{name}")]
    public static (Sidekick, SidekickRenamed) HandleAsync(
        Guid sidekickId, 
        string name, 
        [Aggregate("sidekickId")] Sidekick sidekick
    )
    {
        var @event = new SidekickRenamed(sidekickId, name);
        sidekick.Apply(@event); // okay, this only works with the projection on the aggregate itself.

        return (sidekick, @event);
    }

This way you keep your sidekick logic in one place. Or even more domain driven and projection independend:

    [Transactional]
    [WolverinePost("/sidekicks/{sidekickId}/rename/{name}")]
    public static (Sidekick, SidekickRenamed) HandleAsync(
        Guid sidekickId, 
        string name, 
        [Aggregate("sidekickId")] Sidekick sidekick
    )
    {
       var @event = sidekick.Rename(name);

        return (sidekick, @event);
    }

public class Sidekick {
   ...  
  public SidekickRenamed Rename(string name) {
     if(name.length < 3) throw InvalidArgumentException(...);

     this.name = name;
     return new SidekickRenamed(this.id, this.name);
   }

  public void Apply(SidekickRenamed @event) {
     this.Rename(@event.name);
  }
}

I personally dont see the point in an integrated auto apply, but maybe there is a clever solution which works out of the box for most cases?

jeremydmiller commented 2 weeks ago

Related to #935, or at least do these things together

jeremydmiller commented 4 days ago

@Arne-B The assumption here is that you would be doing the validation already within the handler method. @rena0157 omitted it in this particular case, but doing the auto-update would not prevent you from doing the validation upfront. If you had to do the validation with the resulting aggregate, then you're out of luck and you'd need to do something explicit instead of the aggregate handler workflow

jeremydmiller commented 3 days ago

Notes

Test Cases