Table of Contents

Which package do I install?

The build publishes a package per language surface, per add-on, and per shared core. Only two of them are ones most people install directly.

The short answer

You write Install And that is it
C# SodaFlow Pulls in SodaFlow.Core and SodaFlow.Functional automatically.
F# SodaFlow.FSharp Pulls in SodaFlow.Core automatically.

Add an async package only if you need to run Task-based work from a stream — see Asynchronous work. Add a bindable package only if you are writing a UI view model and want cells and streams to reach XAML as INotifyPropertyChanged properties and ICommands — see Data binding. Add a collections package only if you have a large keyed collection and want to observe individual items without every observer waking for every edit — see Reactive collections.

You also need Install
MapAsync in C# SodaFlow.Async
MapAsync in F# SodaFlow.FSharp.Async
Data binding in C# SodaFlow.Bindable.ObjectModel
Data binding in F# SodaFlow.FSharp.Bindable.ObjectModel
Reactive collections in C# SodaFlow.Collections
Reactive collections in F# SodaFlow.FSharp.Collections

The full list

Package Assembly Install directly?
SodaFlow SodaFlow Yes, for C#.
SodaFlow.FSharp SodaFlow.FSharp Yes, for F#.
SodaFlow.Core SodaFlow.Core No — a dependency of both of the above.
SodaFlow.Functional SodaFlow.Functional Rarely — a dependency of SodaFlow.
SodaFlow.Async SodaFlow.Async Only if you need MapAsync in C#.
SodaFlow.FSharp.Async SodaFlow.FSharp.Async Only if you need MapAsync in F#.
SodaFlow.Async.Core SodaFlow.Core.Async No — a dependency of both async packages.
SodaFlow.Bindable.ObjectModel SodaFlow.Bindable.ObjectModel Only for data binding in C#.
SodaFlow.FSharp.Bindable.ObjectModel SodaFlow.FSharp.Bindable.ObjectModel Only for data binding in F#.
SodaFlow.Bindable.ObjectModel.Core SodaFlow.Core.Bindable.ObjectModel No — a dependency of both bindable packages.
SodaFlow.Collections SodaFlow.Collections Only for reactive collections in C#.
SodaFlow.FSharp.Collections SodaFlow.FSharp.Collections Only for reactive collections in F#.
SodaFlow.Collections.Core SodaFlow.Core.Collections No — a dependency of both collections packages.

Why SodaFlow and SodaFlow.Core are separate

SodaFlow.Core is the engine. It declares the types you actually hold — Stream<T>, Cell<T>, Behavior<T>, Transaction, the listener interfaces — along with the transaction machinery and dependency graph that make updates atomic. It is language-neutral.

SodaFlow and SodaFlow.FSharp are thin language surfaces over that engine. C# gets extension methods and static factory classes; F# gets modules of curried, pipeline-friendly functions. Both are wrappers around the same core types, which is why a Stream<T> created in a C# library can be consumed by F# code and vice versa.

You never reference SodaFlow.Core directly, because you would get types with no usable operations on them — the core exposes its real work through internal members that the wrappers reach via InternalsVisibleTo.

SodaFlow.Functional is a separate concern: it holds Maybe<T>, Either<T1,T2> and Unit, the small functional vocabulary the C# API needs and C# does not ship with. It has no FRP in it and can be used on its own. F# already has option, Result and unit, which is why SodaFlow.FSharp does not depend on it.

Where optionality lives in the collections packages

SodaFlow.Collections.Core has no optional type of its own, and depends on neither SodaFlow.Functional nor anything else beyond SodaFlow.Core. It answers in TryGetItem, TryGetState, TryGetNewState and an internal index lookup returning -1.

Each language surface puts its own optional type back on top: SodaFlow.Collections adds Maybe<T>, and SodaFlow.FSharp.Collections adds option. That is the rule two paragraphs up applied one layer down — nothing that installs the F# collections package acquires Maybe<T>, which F# has no use for.

The one member where this could have cost something is StateCell, which caches its per-key cells weakly so that N observers of one key share a node. The cache stays in the core and takes the projection from the wrapper, keyed by the projected type as well as the key, so neither surface pays for an extra graph node and neither can be handed the other's cells.

The bindable object model

The bindable packages are the bridge to XAML. A cell or a sink goes in, and what comes out is an object that raises INotifyPropertyChanged for its Value property, or an ICommand, so WPF and Avalonia can bind to a SodaFlow graph without either side knowing about the other.

Every bindable comes from an IBindableFactory, which holds the scheduler that all of them use.

You have You call (C# / F#) You bind to
Cell<T> CreateOneWay / ToOneWay IOneWayBindableValue<T> — read-only
CellSink<T>, or a Cell<T> plus a StreamSink<T> CreateTwoWay / ToTwoWay ITwoWayBindableValue<T> — reads and writes
StreamSink<T> plus an initial value, or a CellSink<T> CreateOneWayToSource / ToOneWayToSource IOneWayToSourceBindableValue<T> — write-only
StreamSink<T> CreateBindableAction / ToBindableAction IBindableAction — an ICommand

Bind to SomeProperty.Value, not SomeProperty: a bindable value is an object whose Value changes, not the value itself. Bindable actions are the exception, being ICommand implementations already.

Updates reach the UI through the IBindingScheduler the factory was built with. Build one factory at startup, on the UI thread, with SynchronizationContextBindingScheduler.Capture(), and hand it to each view model; the thread a view model is built on then changes nothing. There is no process-wide default and no fallback. BindingScheduler.Immediate runs handlers inline, for tests and headless hosts.

Versioning

Each package versions independently from its own git tag prefix, via MinVer. Pushing sodaflow-async-1.1.0 releases SodaFlow.Async at 1.1.0 and leaves every other package exactly where its own last tag put it. Do not expect the version numbers to move together — they are not meant to.

Package Tag prefix
SodaFlow sodaflow-
SodaFlow.Core sodaflow-core-
SodaFlow.Functional sodaflow-functional-
SodaFlow.FSharp sodaflow-fsharp-
SodaFlow.Async sodaflow-async-
SodaFlow.Async.Core sodaflow-async-core-
SodaFlow.FSharp.Async sodaflow-fsharp-async-
SodaFlow.Bindable.ObjectModel sodaflow-bindable-objectmodel-
SodaFlow.Bindable.ObjectModel.Core sodaflow-bindable-objectmodel-core-
SodaFlow.FSharp.Bindable.ObjectModel sodaflow-fsharp-bindable-objectmodel-
SodaFlow.Collections sodaflow-collections-
SodaFlow.Collections.Core sodaflow-collections-core-
SodaFlow.FSharp.Collections sodaflow-fsharp-collections-

A package with no tag on the commit being built keeps the version its last tag gave it, so a release only publishes what actually moved.

Independent numbering does not mean the packages are independently installable, though. Every wrapper — SodaFlow and SodaFlow.FSharp, all three async assemblies, all three bindable ones and all three collections ones — reaches SodaFlow.Core through InternalsVisibleTo, so a change to those internals binds them to one particular core even when no public API moved. When that happens the whole group takes a major version together — the emitted NuGet dependency is a minimum version rather than a range, so nothing but the version number stops an old wrapper being resolved against a newer core.

Supported frameworks

Every package multi-targets net472, net6.0 and netstandard2.0.