modernice / goes

goes is an event-sourcing framework for Go.
https://goes.modernice.dev
Apache License 2.0
134 stars 12 forks source link

Aggregate Command Handling with Dependencies #159

Open bounoable opened 1 year ago

bounoable commented 1 year ago

Context

The *aggregate.Base type embeds a command.Handler to allow registration of commands in the aggregate constructor, like this:

package example

type Movie struct {
  *aggregate.Base
}

func NewMovie(id uuid.UUID) *Movie {
  m := &Movie{Base: aggregate.New(...))

  command.ApplyWith(m, m.Create, "movie.create")

  return m
}

type CreateParams struct {
  Name string
  Rating int8
}

func (m *Movie) Create(params CreateParams) error {
 // aggregate.Next(...)
 return nil
}

This works well as long as the Create method does not depend on any external dependencies, like it does in the following example (context.Context and RatingService):

package example

type Movie struct {
  *aggregate.Base
}

func NewMovie(id uuid.UUID) *Movie {
  m := &Movie{Base: aggregate.New(...))

  command.ApplyWith(m, func(name string) error {
    // Here, we would need access to a context.Context and RatingService
    return m.Create(???, name, ???)
  }, "movie.create")

  return m
}

type CreateParams struct {
  Name string
  Rating int8
}

type RatingService interface {
  Rating(ctx context.Context, name string) (int8, error)
}

func (m *Movie) Create(ctx context.Context, name string, svc RatingService) error {
  rating, err := svc.Rating(ctx, name)
  if err != nil {
    return err
  }

  aggregate.Next(m, "movie.created", CreateParams{
    Name: name,
    Rating: rating,
  })

 return nil
}

Ideas

a.command.ApplyContextWith helper

Idea: Add a new command.ApplyContextWith helper function to register command handlers that accept a generic command.Context type.

package example

type Movie struct { ... }

func NewMovie() *Movie {
  m := &Movie{...}

  command.ApplyContextWith(m, func(ctx command.Ctx[string]) error {
    name := ctx.Payload()
    return m.Create(ctx, name, ???) // still no RatingService available
  }, "movie.create")

  return m
}

b. Dependency Injection Container

Idea: Provide a DI Container to command.Context to provide dependencies to aggregates when setting up commands.

// commands.go
package example

import "github.com/modernice/goes/command/handler"

func HandleCommands(ctx context.Context, svc RatingService, bus command.Bus, repo aggregate.Repository) <-chan error {
  return handler.New(NewMovie, repo, bus).MustHandle(ctx, command.Provide(svc))
}
// movie.go
package example

func NewMovie() *Movie {
  m := &Movie{...}

  command.ApplyContextWith(m, func(ctx command.Ctx[string]) error {
    name := ctx.Payload()
    ratingSvc := di.Inject[RatingService](ctx)

    return m.Create(ctx, name, ratingSvc)
  }, "movie.create")

  return m
}