This package provides a lightweight mediator pattern implementation with support for request/response handlers, behaviors (middleware), and event handling.
Features
- Type-safe registration using Go generics
- Request/response pattern with typed handlers
- Behavior pipeline (similar to middleware)
- Event/notification pattern with multiple handlers
- Decoupled communication between components
Usage
Basic Request/Response
package main
import (
"context"
"github.com/The127/mediatr"
)
// Define a request type
type GetUserRequest struct {
UserID string
}
// Define a response type
type GetUserResponse struct {
Username string
Email string
}
func main() {
m := mediatr.NewMediator()
// Register a handler for the request
mediatr.RegisterHandler(m, func(ctx context.Context, request GetUserRequest) (GetUserResponse, error) {
// Handle the request
return GetUserResponse{
Username: "john_doe",
Email: "[email protected]",
}, nil
})
// Send a request and receive a response
response, err := mediatr.Send[GetUserResponse](context.Background(), m, GetUserRequest{
UserID: "123",
})
if err != nil {
// Handle error
}
// Use the response
println(response.Username)
}
Behaviors (Middleware)
Behaviors allow you to add cross-cutting concerns like logging, validation, or authorization to your request pipeline:
// Define a behavior for logging
mediatr.RegisterBehaviour(m, func(ctx context.Context, request GetUserRequest, next mediatr.Next) (any, error) {
println("Before handling request")
// Call the next behavior or handler
response, err := next()
println("After handling request")
return response, err
})
// Register the handler
mediatr.RegisterHandler(m, func(ctx context.Context, request GetUserRequest) (GetUserResponse, error) {
return GetUserResponse{Username: "john_doe"}, nil
})
// The behavior will be executed before and after the handler
response, err := mediatr.Send[GetUserResponse](context.Background(), m, GetUserRequest{UserID: "123"})
Behaviors are executed in the order they are registered, forming a pipeline:
Request → Behaviour 1 → Behaviour 2 → Handler → Behaviour 2 → Behaviour 1 → Response
Event Handling
Events allow you to notify multiple handlers about something that has happened:
// Define an event type
type UserCreatedEvent struct {
UserID string
Username string
}
// Register multiple event handlers
mediatr.RegisterEventHandler(m, func(ctx context.Context, evt UserCreatedEvent) error {
println("Sending welcome email to", evt.Username)
return nil
})
mediatr.RegisterEventHandler(m, func(ctx context.Context, evt UserCreatedEvent) error {
println("Creating user profile for", evt.Username)
return nil
})
// Send an event to all registered handlers
err := mediatr.SendEvent(context.Background(), m, UserCreatedEvent{
UserID: "123",
Username: "john_doe",
})
Real-World Example
Here's a complete example showing how handlers, behaviors, and events work together:
package main
import (
"context"
"errors"
"github.com/The127/mediatr"
)
type CreateUserCommand struct {
Username string
Email string
}
type CreateUserResult struct {
UserID string
}
type UserCreatedEvent struct {
UserID string
Username string
}
func main() {
m := mediatr.NewMediator()
// Register a validation behavior
mediatr.RegisterBehaviour(m, func(ctx context.Context, request CreateUserCommand, next mediatr.Next) (any, error) {
if request.Username == "" {
return nil, errors.New("username is required")
}
if request.Email == "" {
return nil, errors.New("email is required")
}
return next()
})
// Register the command handler
mediatr.RegisterHandler(m, func(ctx context.Context, cmd CreateUserCommand) (CreateUserResult, error) {
// Create the user in database
userID := "user-123"
// Publish an event
err := mediatr.SendEvent(ctx, m, UserCreatedEvent{
UserID: userID,
Username: cmd.Username,
})
if err != nil {
return CreateUserResult{}, err
}
return CreateUserResult{UserID: userID}, nil
})
// Register event handlers
mediatr.RegisterEventHandler(m, func(ctx context.Context, evt UserCreatedEvent) error {
println("Sending welcome email to", evt.Username)
return nil
})
// Execute the command
result, err := mediatr.Send[CreateUserResult](context.Background(), m, CreateUserCommand{
Username: "john_doe",
Email: "[email protected]",
})
if err != nil {
panic(err)
}
println("User created with ID:", result.UserID)
}
Key Concepts
Handlers
Handlers process a request and return a response. Each request type can have exactly one handler registered.
type HandlerFunc[TRequest any, TResponse any] func(ctx context.Context, request TRequest) (TResponse, error)
Behaviors
Behaviors are like middleware that wrap around handlers. They can:
- Execute logic before and after the handler
- Short-circuit the pipeline by returning early
- Modify the request or response
- Add cross-cutting concerns (logging, validation, authorization, etc.)
type BehaviourFunc[TRequest any] func(ctx context.Context, request TRequest, next Next) (any, error)
Behaviors are applied to all requests that match their type constraint. If a behavior is registered for a base type or interface, it will be applied to all requests that implement that type.
Events
Events represent notifications that something has happened. Unlike requests:
- Events can have multiple handlers
- Event handlers don't return values (except errors)
- Events are processed sequentially
- If an event handler returns an error, processing stops
type EventHandlerFunc[TEvent any] func(ctx context.Context, evt TEvent) error
Benefits
- Decoupling - Request senders don't need to know about handlers
- Single Responsibility - Each handler has one job
- Open/Closed Principle - Add new handlers without modifying existing code
- Cross-Cutting Concerns - Behaviors handle common logic like validation and logging
- Event-Driven Architecture - Decouple components using events
- Type Safety - Generic implementation ensures compile-time type checking
Thread Safety
The mediator itself is thread-safe for registration operations (handlers, behaviors, events should be registered during application startup).
Handler execution is thread-safe as long as the handlers themselves are thread-safe. The same mediator instance can be used concurrently from multiple goroutines.