Skip to content
Cross platform storage for mobile, web and native apps used in-house at Fifty3North
C#
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
F3N.Hoard
Hoard.Tests
Samples
.gitignore
Hoard.sln
README.md
azure-pipelines.yml
global.json

README.md

F3N.Hoard

Nuget Azure DevOps builds Azure DevOps tests

Cross platform storage for native apps used in-house at Fifty3North.

Based loosely on Redux and generic dispatcher used in Orleankka, views can subscribe to the store and be notified when it updates.

Works anywhere but tested in Blazor and Xamarin.

Uses SQLite and Akavache API to provide persistence

TODOs

  1. Create tests
  2. Make storage module pluggable
  3. Create none Akavache storage example
  4. Provide Xamarin example
  5. Documentation
  6. Publish NuGet package

Works well with:

F3N.YaMVVM

Yet another Model, View, ViewModel framework for Xamarin Forms.

Pre-requisites

Visual Studio 2019 and .Net Core Preview 6 (for Blazor samples)

How to use

You will need a store, a state object to put in your store, a command and an event.

Your store will have command handlers and event handlers.

Command handlers will perform the logic and any communication with external APIs. Depending on the outcome, one or more events will be raised.

Event handlers will respond and update state in the store.

Command

Commands contain a unit of user intent. They contain all the information required to validate the intent and change state.

Commands belong to a store and are typed as such using inheritance from base DomainCommand.

They should be immutable by design but this is not enforced by Hoard.

public class RegisterProduct : DomainCommand<WidgetStore>
{
    public readonly Guid Id;
    public readonly string Title;
    public readonly int IntialStockQuantity;

    public RegisterProduct(Guid id, string title, int initialStockQuantity)
    {
        Id = id;
        Title = title;
        IntialStockQuantity = initialStockQuantity;
    }
}

Event

An event is a fact that has happened within your system. It contains all the information required to change state in your store.

Events always have an Id of type Guid.

public class ProductRegistered : Event
{
    public readonly string Title;
    public readonly int InitialQuantity;

    public ProductRegistered(Guid id, string title, int initialQuantity)
    {
        Id = id;
        Title = title;
        InitialQuantity = initialQuantity;
    }
}

Store

There are two types of store: one for storing a single object, and one for storing a collection of objects.

Single object

public class CounterStore : Store<CounterStore, CounterState> { ... }

Collection

public class WidgetStore : StoreCollection<WidgetStore,WidgetState> { ... }

Command Hanlders

Command handlers examine command and determine which events to raise. This is also where any communication with any external APIs occurs. Based on the result from the API different events can be raised.

public IEnumerable<Event> Handle(Commands.RegisterProduct command)
{
	if (CurrentState.Any(widget => widget.Id == command.Id))
	{
		return new[] { new Events.DuplicateProductIdEncountered(command.Id, command.Title) };
	}
	else
	{
		return new[] { new Events.ProductRegistered(command.Id, command.Title, command.IntialStockQuantity) };
	}
}

Event Handlers

Event handlers take the event and mutate state.

State can be modified in a single object store by accessing the _state object to update properties or can use SetState to replace the entire state.

In a store collection, state is modified using: AddOrReplaceItem(product); and RemoveItem(product); or to replace the whole collection either SetState or Reset to clear state.

public void On(Events.ProductRegistered ev)
{
    var product = new WidgetState(ev.Id, ev.Title, ev.InitialQuantity);

    AddOrReplaceItem(product);
}

State

Your state needs to be serializable to store it in Akavache. If you have a constructor you need to make sure the parameters are named the same as the public properties or fields.

Single object state items implement IStatefulItem and store collection state items implement IStatefulCollectionItem

public class WidgetState : IStatefulCollectionItem
{
    public Guid Id { get; }

    public string Title { get; }

    public int StockQuantity { get; set; }

    public WidgetState(Guid id, string title, int stockQuantity = 0)
    {
        Id = id;
        Title = title;
        StockQuantity = stockQuantity;
    }
}

UI

The UI can either query state directly:

WidgetStore widgetStore = new WidgetStore();
await widgetStore.Initialise();

WidgetState state = widgetStore.CurrentState

Or subsribe to the store to receive updates:

ForecastStore forecastStore = new ForecastStore();
await forecastStore.Initialise();

IDisposable subscription = forecastStore.Observe().Subscribe(state =>
{
    int lookupIndex = forecasts.FirstIndexOf(s => s?.Id == state.Id);

    WeatherForecast preparedViewModel = ToViewModel(state);

    if (lookupIndex == -1)
    {
        forecasts.Insert(preparedViewModel, (a, b) => a.Date > b.Date);
    }
    else
    {
        forecasts[lookupIndex] = preparedViewModel;
    }
});

Or both: load initial state from Store and then keep up to date by subscribing.

There are multiple Observe methods that will deliver events also and can filter based on specific event Ids:

ObserveWithEvents

var subscription = widgetStore.ObserveWithEvents().Subscribe((payload) =>
{
    updatedItem = payload.State;
    if (payload.@event is TestStore.Events.ProductRegistered registeredEvent)
    {
        ev = registeredEvent;
    }
});

ObserveWhere

var subscription = widgetStore.ObserveWhere(ev => ev.Id == product1Command.Id).Subscribe(state =>
{
    products.Add(state);
});

ObserveWhereWithEvents

var subscription = widgetStore.ObserveWhereWithEvents(ev => ev.Id == product1Command.Id).Subscribe((payload) =>
{
    products.Add(payload.State);
    events.Add(payload.@event);
});

Issuing commands from the UI

Take user input and dispatch to the store

ForecastStore forecastStore = new ForecastStore();
await forecastStore.Initialise();

await forecastStore.Dispatch(new Hoard.SampleLogic.Forecast.Commands.RecordObservedTemperature(Guid.NewGuid(), recordedDate, temperatureRecorded));

SQLite

You must initialise Akavache using the following on App start:

LocalStorage.Initialise("Your.App.Name");

The data is stored in %LocalAppData%\Hoard.SampleWeb\BlobCache (c:\users\<username>\Appdata\Local\Hoard.SampleWeb\BlobCache)

You can’t perform that action at this time.