HashTableRx 2.1.6

dotnet add package HashTableRx --version 2.1.6
                    
NuGet\Install-Package HashTableRx -Version 2.1.6
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="HashTableRx" Version="2.1.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="HashTableRx" Version="2.1.6" />
                    
Directory.Packages.props
<PackageReference Include="HashTableRx" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add HashTableRx --version 2.1.6
                    
#r "nuget: HashTableRx, 2.1.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package HashTableRx@2.1.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=HashTableRx&version=2.1.6
                    
Install as a Cake Addin
#tool nuget:?package=HashTableRx&version=2.1.6
                    
Install as a Cake Tool

NuGet Stats Nuget

HashTableRx

A reactive hash table that mirrors the structure of an object into a dotted path key/value store you can observe and update.

  • Targets: .NET Standard 2.0, .NET 8, .NET 9
  • Dependencies: System.Reactive

HashTableRx lets you:

  • Reflect an object (even from an unknown assembly) into a hierarchical hash table.
  • Get and set values using dotted path keys like "Process.Temperature.PV.Value".
  • Observe individual variables or all changes as IObservable streams.
  • Toggle case sensitivity via UseUpperCase.

Installation

  • NuGet: dotnet add package HashTableRx
  • Or reference the HashTableRx project in your solution.

Key concepts

  • Dotted variable paths: Nested members are addressed with dot separators, e.g. "A.B.C".
  • Primitive-like members: primitives and string (including arrays of these) are treated as values.
  • Complex members: non-primitive-like types become nested HashTableRx nodes.
  • Case handling: When UseUpperCase is true, all keys are normalized to uppercase for reads/writes/observations.
  • Value API: Reading works with Value<T>(path). Writing with Value(path, value) requires the variable to already exist; otherwise InvalidVariableException is thrown (to prevent silent writes).
  • Indexer API: The string indexer always creates intermediate nodes as needed.

Quick start

using CP.Collections;
using System.Reactive.Linq;

// Create a reactive hash table (case sensitive keys)
var h = new HashTableRx(useUpperCase: false);

// Create a few variables using the string indexer
h["System.Online"] = true;        // bool
h["Process.Temperature.CV"] = 20f; // float

// Read variables
bool online = h.Value<bool>("System.Online") ?? false;
float temp = h.Value<float>("Process.Temperature.CV") ?? 0f;

// Observe changes to an individual variable
var sub1 = h.Observe<float>("Process.Temperature.CV")
            .Subscribe(v => Console.WriteLine($"Temp changed to {v}"));

// Update a value (variable must already exist for Value(..) write)
h.Value("Process.Temperature.CV", 25f);

// Or use the indexer to create and/or set directly (creates if missing)
h["Process.Temperature.SP"] = 30f;

// Observe all changes
var sub2 = h.ObserveAll
            .Subscribe(kv => Console.WriteLine($"{kv.key} => {kv.value}"));

// Cleanup
sub1.Dispose();
sub2.Dispose();

Case sensitivity and UseUpperCase

var h = new HashTableRx(useUpperCase: true);

// All keys are normalized to uppercase internally
h["Rig.Temp.PV"] = 10f;

Console.WriteLine(h.Value<float>("rig.temp.pv")); // 10
Console.WriteLine(h.Value<float>("RIG.TEMP.PV")); // 10

// Observations also normalize
using var sub = h.Observe<float>("rig.temp.pv").Subscribe(v => Console.WriteLine(v));
h.Value("RIG.TEMP.PV", 11f); // Emits 11

Reflecting an unknown assembly (structures)

You can reflect any object instance (e.g., created via reflection from an external assembly) into the hash table.

using System.Reflection;
using CP.Collections;

// Load an external assembly and create an instance
var asm = Assembly.LoadFrom(@"path\to\UnknownLibrary.dll");
var obj = asm.CreateInstance("Namespace.TypeName");

var h = new HashTableRx(useUpperCase: false);

// Populate the hash table from the object's public fields/properties
h.SetStructure(obj!);

// Now you can read values from reflected primitive-like fields/properties
var pv = h.Value<float>("Some.Structured.Path.PV");

// Update values in the hash table
h.Value("Some.Structured.Path.SP", 42.0f);

// Push current hash table values back to the original object
var updated = h.GetStructure();
// 'updated' is the same instance with primitive-like values written back

Notes:

  • Primitive-like = primitive or string (including arrays of those). Complex members become nested nodes.
  • When trimming/AOT, reflection is annotated and may require preserving members. See code attributes for details.

Indexer API vs Value API

  • String indexer h["A.B.C"]:

    • Reading returns the current value (or null if missing).
    • Writing creates intermediate nodes as required and sets the value.
  • Value API:

    • T? Value<T>(string path): typed read, returns default when missing.
    • bool Value<T>(string path, T value): typed write. Throws InvalidVariableException if the variable does not exist.

Example:

var h = new HashTableRx(false);

// Create then write
h["A.B.C"] = 1;
h.Value("A.B.C", 2); // ok

// Attempting to write unknown path throws
Assert.Throws<InvalidVariableException>(() => h.Value("X.Y.Z", 5));

Observing changes

  • Observe<T>(path): emits typed values on change (distinct until changed).
  • ObserveAll: emits (key, object?) for any change (also distinct until changed at the tuple level).
var h = new HashTableRx(false);

// Create initial variable then observe
h["A.B.C"] = 10;
using var sub = h.Observe<int>("A.B.C").Subscribe(v => Console.WriteLine($"A.B.C = {v}"));

h.Value("A.B.C", 10); // may not emit due to DistinctUntilChanged
h.Value("A.B.C", 11); // emits 11

using var subAll = h.ObserveAll.Subscribe(kv => Console.WriteLine($"{kv.key} => {kv.value}"));
h["X.Y"] = 3.14f; // emits ("X.Y", 3.14f)

Working with nested structures

var h = new HashTableRx(false);

// Writes automatically create intermediate nodes
h["Plant.Unit1.Pump.Speed"] = 1200;

// Reads follow the same structure
int? speed = h.Value<int>("Plant.Unit1.Pump.Speed");

// Switching a scalar to a branch via write
h["Plant.Unit1"] = 123;         // was scalar
h["Plant.Unit1.Pump.Speed"] = 900; // Node becomes a nested table to accommodate deeper path

Using the bool indexer for reflection

The bool indexer h[true] is a convenience to reflect the entire object graph to and from the hash table.

var h = new HashTableRx(false);

// Push an object's values into the table
h[true] = myObject;    // equivalent to h.SetStructure(myObject)

// Later, materialize current values back into the object
var updated = h[true]; // equivalent to h.GetStructure()

HashTable base members

HashTableRx derives from a reactive HashTable that is observable as a sequence of (key, value) updates.

  • Add(object key, object? value): Adds a key/value (also notifies observers).
  • Remove(object key): Removes a key (scheduled).
  • Clear(): Clears all entries (scheduled).
  • Get(object key): Returns IObservable<(string key, object value)> that reads the indexer on a scheduler.
var ht = new HashTable();

ht.Add("K", 123);
var (k, v) = ht.Get("K").Wait();  // ("K", 123)

ht.Remove("K");
ht.Clear();

Error handling

  • InvalidVariableException: thrown by Value(path, value) when the variable does not exist.
  • InvalidCastException: thrown by Value(path, value) when the existing variable type is incompatible with value.
try
{
    h.Value("A.B.C", "wrong-type");
}
catch (InvalidCastException ex)
{
    Console.WriteLine(ex.Message);
}

Performance notes

  • Reflection for SetStructure/GetStructure is optimized using compiled expression trees and caching per Type.
  • Primitive-like members read/write via fast delegates, avoiding slow PropertyInfo/FieldInfo.GetValue/SetValue loops.
  • For heavy usage, prefer creating variables once (via indexer or SetStructure) and then use Value(path, value) for updates.

Tips

  • If you need case-insensitive behavior, construct HashTableRx with useUpperCase: true.
  • Prefer Value<T>(path) for typed reads. It returns default(T) when missing instead of throwing.
  • To write to a brand new path, use the indexer (h["A.B.C"] = value) or SetStructure first. Value(path, value) enforces that the variable already exists.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on HashTableRx:

Package Downloads
CP.TwinCATRx

A Reactive TwinCAT PLC Library

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.6 11 8/19/2025
2.1.0 12 8/19/2025
2.0.1 13 8/17/2025
1.3.1 183 7/11/2025
1.3.0 438 1/15/2025
1.2.0 915 6/5/2024
1.1.0 413 12/26/2023
1.0.0 2,922 10/10/2022

Compatability with Net 8 / 9 and netstandard2.0