Persistent Storage¶
Persistent storage allows adapters to retain data between restarts.
What is Persistent Storage?¶
+------------------+
| Adapter |
| +------------+ |
| | Settings | | <-- LastEventIndex: 42
| +-----+------+ |
+--------+---------+
|
v
+------------------+
| Persistent |
| Storage | <-- data survives adapter restart
+------------------+
Persistent storage is:
- Persistent - data remains after adapter restart
- Isolated per device - each device has its own data
- Type-safe - you work with C# classes, not raw data
- Automatic - framework handles loading and saving
When Do You Need Persistent Storage?¶
YES - You need to store:
- Index of last processed event (polling devices)
- Synchronization state (what has already been uploaded)
- Device data cache (e.g., user list)
- Any data that must survive a restart
NO - Use other solutions for these purposes:
- Current device state -> use Status
- Admin configuration -> use Properties
- Occurrences -> use Events
Persistent Storage vs Properties¶
| Aspect | Persistent Storage | Properties |
|---|---|---|
| Who sets | Adapter | Admin/user |
| Visibility | Adapter only | Entire system |
| Purpose | Internal adapter state | Device configuration |
| Example | "Last event ID: 42" | "Unlock duration: 5s" |
PERSISTENT STORAGE: "LastEventIndex = 42"
(internal state, no one else sees it)
PROPERTY: "UnlockDuration = 5"
(configuration, admin changes it in UI)
API¶
Defining a Settings Class¶
Create a class representing your data:
public class EventPollerSettings
{
public int LastEventIndex { get; set; }
public DateTime? LastSync { get; set; }
public List<int> ProcessedUserIds { get; set; } = [];
}
Rules:
- Class must have a parameterless constructor
- All properties must have both
getandset - Use default values for initialization
Injecting into a Thing¶
Request IPersistentSettings<T> directly in the Thing constructor:
public partial class MyDevice : IDeviceConnection
{
private readonly IPersistentSettings<EventPollerSettings> _settings;
public MyDevice(IPersistentSettings<EventPollerSettings> settings)
{
_settings = settings;
}
}
This works for root device Things and child Things. Do not inject PersistentSettingsFactory, do not manually call the factory, and do not add optional or parameterless constructor overloads to work around persistence. The generated tree builder uses ActivatorUtilities and the framework resolves IPersistentSettings<T> automatically.
Wrong:
The framework automatically:
- Creates an instance for your device
- Loads existing data (or creates default values)
- Isolates data from other devices
Reading Values¶
public async Task PollEvents(CancellationToken ct)
{
// read current value
var startIndex = _settings.Value.LastEventIndex;
var events = await _device.GetEvents(startIndex, ct);
// ...
}
Saving Values¶
public async Task PollEvents(CancellationToken ct)
{
var startIndex = _settings.Value.LastEventIndex;
var events = await _device.GetEvents(startIndex, ct);
foreach (var evt in events)
{
await ProcessEvent(evt, ct);
// update value
_settings.Value.LastEventIndex = evt.Index;
}
// save changes - DON'T FORGET!
_settings.Save();
}
Important: Changes are not saved automatically. Always call Save() after completing changes.
Complete Example¶
public class EventPollerSettings
{
public int LastEventIndex { get; set; }
public DateTime? LastFullSync { get; set; }
}
public class MyDeviceAdapter : DeviceAdapterBase
{
private readonly IAdapterSettings<EventPollerSettings> _settings;
private readonly IDeviceClient _client;
public MyDeviceAdapter(
IAdapterSettings<EventPollerSettings> settings,
IDeviceClient client)
{
_settings = settings;
_client = client;
}
public async Task PollEvents(CancellationToken ct)
{
var lastIndex = _settings.Value.LastEventIndex;
var events = await _client.GetEventsAfter(lastIndex, ct);
if (events.Count == 0) return;
foreach (var evt in events)
{
await PublishEvent(evt, ct);
_settings.Value.LastEventIndex = evt.Index;
}
_settings.Save();
}
public async Task PerformFullSync(CancellationToken ct)
{
await SyncAllUsers(ct);
_settings.Value.LastFullSync = DateTime.UtcNow;
_settings.Save();
}
}
Data Lifecycle¶
Adapter starts
|
v
+----------------------+
| Framework loads data | <-- from persistent storage
| into _settings.Value | (or creates defaults)
+----------------------+
|
v
+----------------------+
| Adapter works |
| _settings.Value.X=Y | <-- changes in memory only
+----------------------+
|
v
+----------------------+
| _settings.Save() | <-- saves to persistent
| | storage
+----------------------+
|
v
+----------------------+
| Adapter restarts |
+----------------------+
|
v
+----------------------+
| Data is available | <-- values persisted
| again |
+----------------------+
Per-Device Isolation¶
Each device (Thing) has its own isolated data:
Adapter
|
+-- Controller 1 (DeviceId: abc-123)
| |
| +-- IAdapterSettings<EventPollerSettings>
| LastEventIndex: 42
| LastFullSync: 2024-01-15
|
+-- Controller 2 (DeviceId: def-456)
|
+-- IAdapterSettings<EventPollerSettings>
LastEventIndex: 100 <-- different value
LastFullSync: 2024-01-20
When you inject IAdapterSettings<T> into a Thing, the framework automatically returns an instance isolated for that device.
Limitations¶
Data Size¶
- Storage is designed for small data (hundreds of KB)
- Do not store large files, images, or binary data
- For large data, use another solution (filesystem, external DB)
Data Types¶
Supported property types:
- Primitive types:
int,string,bool,DateTime,Guid, etc. - Collections:
List<T>,Dictionary<K,V>(with supported types) - Nested objects (POCO classes)
Not supported:
- Circular references
- Delegates, events
Stream,Span<T>, ref structs
Concurrent Access¶
- Storage is thread-safe for both reading and writing
- Changes from different threads do not merge - last
Save()wins - For complex scenarios, synchronize access yourself
// OK - simple use case
_settings.Value.Counter++;
_settings.Save();
// CAUTION - race condition with parallel access
// If two threads increment simultaneously, one change is lost
Performance¶
Valueis a fast in-memory readSave()performs an I/O operation - don't call unnecessarily often- Batch changes and call
Save()once at the end
// WRONG - unnecessary I/O
foreach (var evt in events)
{
_settings.Value.LastEventIndex = evt.Index;
_settings.Save(); // I/O on each iteration
}
// RIGHT - one Save at the end
foreach (var evt in events)
{
_settings.Value.LastEventIndex = evt.Index;
}
_settings.Save(); // single I/O
Common Mistakes¶
1. Forgotten Save()¶
// WRONG - changes lost on restart
_settings.Value.LastEventIndex = newIndex;
// forgot _settings.Save();
// RIGHT
_settings.Value.LastEventIndex = newIndex;
_settings.Save();
2. Save() in try Block Without Finally¶
// WRONG - data not saved on exception
try
{
_settings.Value.LastEventIndex = evt.Index;
await ProcessEvent(evt, ct);
_settings.Save();
}
catch { /* save not executed */ }
// RIGHT - save before risky operations
_settings.Value.LastEventIndex = evt.Index;
_settings.Save(); // save first
await ProcessEvent(evt, ct); // then process
3. Storing Large Data¶
// WRONG - storage is not for large data
public class BadSettings
{
public byte[] ImageData { get; set; } // could be MB
public List<FullUserRecord> AllUsers { get; set; } // thousands of records
}
// RIGHT - only small metadata
public class GoodSettings
{
public int LastProcessedUserId { get; set; }
public DateTime LastImageSync { get; set; }
}
4. Changing Structure Without Migration¶
// Original version
public class MySettings
{
public int Counter { get; set; }
}
// New version - property renamed
public class MySettings
{
public int EventCounter { get; set; } // CAUTION: Counter data is lost!
}
When changing structure, consider:
- Keeping old properties (backwards compatibility)
- Or initializing new values on startup
Best Practices¶
1. Store the Minimum¶
// RIGHT - only what's necessary
public class EventPollerSettings
{
public int LastEventIndex { get; set; }
}
2. Use Default Values¶
public class SyncSettings
{
public int LastEventIndex { get; set; } = 0; // default value
public DateTime? LastSync { get; set; } // nullable = never happened
}
3. Name Class by Purpose¶
// RIGHT - clear purpose
public class EventPollerSettings { }
public class UserSyncState { }
public class CredentialCacheSettings { }
// WRONG - generic name
public class Settings { }
public class Data { }
4. One Settings Class per Concern¶
// RIGHT - separated concerns
public class EventPollerSettings { public int LastEventIndex { get; set; } }
public class UserSyncSettings { public DateTime? LastSync { get; set; } }
// Inject both
public MyDeviceAdapter(
IAdapterSettings<EventPollerSettings> eventSettings,
IAdapterSettings<UserSyncSettings> syncSettings)
Next: Things - Device hierarchy
- Version: 1.0
- Framework: Pq.Adapters.Framework