Skip to content
Maurice CGP Peters edited this page Jun 11, 2021 · 10 revisions

[WIP] Basic concepts

Radix' philosophy is centered around the fact that aggregates, just like actors in the actor model, have a stable identifier. In actor speak this is called an address. This address is globally unique and within Radix the address is used as the aggregate id. To be more specific, aggregates are represented in the Radix runtime as actors. To the end user this is totally transparent however as will be shown later.

The actors / aggregates (from hereon plainly called aggregates) have a runtime context and it is only via this context that a reference to an (new) aggregate can be acquired. This reference is represented by an Address. Within Radix the context is aptly named BoundedContext<TCommand, TEvent, TFormat>. TCommand and TEvent respectively represent the top in the hierarchy of message types for commands and events that are valid within the bounded context. TFormat is the message serialization format. Radix provides Json as a type out of the box, but it is a mere alias for a string type.

    BoundedContext<InventoryItemCommand, InventoryItemEvent, Json> context = 
        new BoundedContext<InventoryItemCommand, InventoryItemEvent, Json>(new BoundedContextSettings<InventoryItemEvent, Json>( ... ));
    /// Create an instance of an aggregate. The aggregate is identified by an Address
    Aggregate<InventoryItemCommand, InventoryItemEvent> inventoryItem = 
        context.Create(InventoryItem.Decide, InventoryItem.Update););

Aggregates can only receive command messages. Only valid commands are processed. What valid means is explicitly defined by the programmer. In response to a valid command an aggregate will produce zero or more events. Command messages can only be sent to an aggregate via its Address via its Accept method.

    // Create a validated command
    Validated<InventoryItemCommand> removeItems = RemoveItemsFromInventory.Create(1, 1);
    // Send the command via the Accept method on the Aggregate
    Result<InventoryItemEvent[], Error[]> result = await inventoryItem.Accept(removeItems);
    // Check wetter or not the processing was successful
    switch (result)
    {
        case Ok<InventoryItemEvent[], Error[]>(var events):
            // A valid command was processed and events where created as a result (the deconstructed events variable)
            ...
            break;
        case Error<InventoryItemEvent[], Error[]>(var errors):
            // Errors occured, handle them (the deconstructed errors variable)
            ...
            break;
    }

The state of the aggregate is defined by a plain type, in this example the InventoryItem. Make sure it has read only semantics, like a record with init only properties.

    public record InventoryItem
    {
        
        public InventoryItem()
        {
            Name = "";
            Activated = true;
            Count = 0;
            ReasonForDeactivation = "";
        }

        public InventoryItem(string name, bool activated, int count)
        {
            Name = name;
            Activated = activated;
            Count = count;
            ReasonForDeactivation = "";
        }

        public string Name { get; init; }
        public bool Activated { get; init; }
        public int Count { get; init; }
        public string ReasonForDeactivation { get; init; }
    }

You might have noticed there was no reference to this type directly in the code above. Instances of the state are created by the runtime of Radix. Hence a parameter less constructor is mandatory (and code will not compile of it does not have it). As a convention the empty constructor defines what an empty instance of the state means for that type.

The Create method on the BoundedContext does however take strongly typed instances of 2 delegates, a Decide and an Accept delegate, that have a generic type reference to the type of the state. These 2 delegates define what it means to process a Command (Decide) and how the state should be updated when one or more events were generated as a consequence of processing the command (Update). In general Decide embodies all command handlers and Update the event handlers.

    public static Update<InventoryItem, InventoryItemEvent> Update = (state, events) =>
    {
        return events.
                   Aggregate(state, (item, @event) =>
                   {
                       return @event switch
                       {
                            InventoryItemCreated inventoryItemCreated => 
                                state with { Name = inventoryItemCreated.Name },
                            InventoryItemDeactivated inventoryItemDeactivated => s
                                state with { Activated = true,   ReasonForDeactivation = inventoryItemDeactivated.Reason}, 
                            ItemsCheckedInToInventory itemsCheckedInToInventory => 
                                state with { Count = state.Count + itemsCheckedInToInventory.Amount },
                            ItemsRemovedFromInventory itemsRemovedFromInventory => 
                                state with { Count = state.Count - itemsRemovedFromInventory.Amount },
                            InventoryItemRenamed inventoryItemRenamed => 
                                state with { Name = inventoryItemRenamed.Name },
                            _ => throw new NotSupportedException("Unknown event")
                    };
                });
        };

        public static Decide<InventoryItem, InventoryItemCommand, InventoryItemEvent> Decide = (state, command) =>
        {
            return command switch
            {
                DeactivateInventoryItem deactivateInventoryItem => 
                    Task.FromResult(Ok<InventoryItemEvent[], CommandDecisionError>(
                        new InventoryItemEvent[] 
                        {
                            new InventoryItemDeactivated(deactivateInventoryItem.Reason)
                        })),
                CreateInventoryItem createInventoryItem => 
                    Task.FromResult(Ok<InventoryItemEvent[], CommandDecisionError>(
                        new InventoryItemEvent[]
                        {
                            new InventoryItemCreated(createInventoryItem.Id, createInventoryItem.Name, createInventoryItem.Activated, createInventoryItem.Count)
                        })),
                RenameInventoryItem renameInventoryItem => 
                    Task.FromResult(Ok<InventoryItemEvent[], CommandDecisionError>(
                        new InventoryItemEvent[] 
                        { 
                            new InventoryItemRenamed { Id = renameInventoryItem.Id, Name = renameInventoryItem.Name } 
                        })),
                CheckInItemsToInventory checkInItemsToInventory => 
                    Task.FromResult(Ok<InventoryItemEvent[], CommandDecisionError>(
                        new InventoryItemEvent[] 
                        { 
                            new ItemsCheckedInToInventory { Amount = checkInItemsToInventory.Amount, Id = checkInItemsToInventory.Id } 
                        })),
                RemoveItemsFromInventory removeItemsFromInventory => 
                    Task.FromResult(Ok<InventoryItemEvent[], CommandDecisionError>(
                        new InventoryItemEvent[] 
                        {
                            new ItemsRemovedFromInventory(removeItemsFromInventory.Amount, removeItemsFromInventory.Id)
                        })),
                _ => throw new NotSupportedException("Unknown transientCommand")
            };
        };