Overview: Comprehensive guide to the LionFire Workspaces ecosystem - a system for organizing user-centric, file-backed documents with isolated service scopes and reactive updates.
- What Are Workspaces?
- When to Use Workspaces
- Library Ecosystem
- Quick Start
- Common Scenarios
- Best Practices
- Troubleshooting
Workspaces are user-centric, directory-backed containers for heterogeneous documents. Think of them as "project folders" that:
- Store multiple types of related documents (Bots, Portfolios, Strategies, etc.)
- Provide isolated service scopes (each workspace has its own
IObservableReader/Writerinstances) - Persist as HJSON files in subdirectories
- Support reactive updates (file changes automatically propagate to UI)
- Enable multi-workspace scenarios (multiple workspaces can be open simultaneously)
Like Visual Studio Solutions or IntelliJ Projects, but:
- More flexible (any document types, not just code)
- File-based (human-readable HJSON, version-controllable)
- Service-scoped (isolated data access per workspace)
- UI-integrated (common workspace management components)
C:\Users\Alice\Trading\Workspaces\
├── DayTrading\ ← Workspace 1
│ ├── Bots\
│ │ ├── scalper-btc.hjson
│ │ └── momentum-eth.hjson
│ ├── Portfolios\
│ │ └── main-portfolio.hjson
│ └── Strategies\
│ └── mean-reversion.hjson
└── SwingTrading\ ← Workspace 2
├── Bots\
│ └── swing-bot.hjson
└── Portfolios\
└── swing-portfolio.hjson
- User Organization: Users need to group related documents into "projects" or "contexts"
- Multiple Document Types: Application has several entity types that belong together (not just one type)
- File-Based Storage: Want human-readable files that can be version-controlled
- Isolation: Different workspaces should have independent data and services
- Multi-Workspace: Users might work with multiple workspaces (switching or multiple open)
- UI Integration: Want standard workspace management UI (selector, properties, etc.)
Examples:
- Trading Application: Workspaces for different trading strategies, each with bots, portfolios, and configurations
- Content Management: Workspaces for different projects, each with articles, media, and settings
- Game Development: Workspaces for different game projects, each with levels, assets, and configurations
- Data Analysis: Workspaces for different analyses, each with datasets, notebooks, and results
- Single Global Store: Application has one shared data context for all users
- Database-Only: All data lives in a database (consider Ided/Assets pattern instead)
- No User Organization: Data organization is purely technical, not user-driven
- Simple Config Files: Just need to read/write a few config files (use
IObservableReaderdirectly) - No Isolation Needed: All contexts share the same data
Alternatives:
| Need | Alternative | Example |
|---|---|---|
| Database entities | Ided/Assets | Game inventory items |
| Complex virtual filesystem | VOS | Overlay configs from multiple sources |
| Single config file | Direct IObservableReader | App settings |
| Custom storage backend | Persistence Layer | MongoDB, Redis |
┌──────────────────────────────────────────────────────┐
│ LionFire.Workspaces.Abstractions │
│ - IWorkspace, IWorkspaceServiceConfigurator │
│ - UserWorkspacesService │
└─────────────────┬────────────────────────────────────┘
│
↓ implements
┌──────────────────────────────────────────────────────┐
│ LionFire.Workspaces │
│ - WorkspaceTypesConfigurator │
│ - DirectoryWorkspaceDocumentService │
│ - WorkspaceDocumentRunner pattern │
└─────────────────┬────────────────────────────────────┘
│
↓ used by
┌──────────────────────────────────────────────────────┐
│ LionFire.Workspaces.UI │
│ - WorkspaceGridVM (ViewModel) │
└─────────────────┬────────────────────────────────────┘
│
↓ used by
┌──────────────────────────────────────────────────────┐
│ LionFire.Workspaces.UI.Blazor │
│ - WorkspaceLayoutVM (provides WorkspaceServices) │
│ - WorkspaceSelector, WorkspaceGrid │
└──────────────────────────────────────────────────────┘
- LionFire.Reactive: Provides
IObservableReader/Writerinterfaces - LionFire.IO.Reactive.Hjson: HJSON file-based implementations
- LionFire.Data.Async.Mvvm: ViewModels for workspace documents
- LionFire.Blazor.Components.MudBlazor:
ObservableDataViewcomponent
- Workspace Architecture - High-level design
- Service Scoping Deep Dive - Critical for understanding DI
- Blazor MVVM Patterns - UI patterns
- Library References:
dotnet add package LionFire.Workspaces
dotnet add package LionFire.Workspaces.UI.Blazor # If using Blazor
dotnet add package LionFire.Blazor.Components.MudBlazor # For ObservableDataView// Entity: The persisted data
[Alias("Bot")]
public partial class BotEntity : ReactiveObject
{
[Reactive] private string? _name;
[Reactive] private string? _description;
}
// ViewModel: UI-friendly wrapper
public class BotVM : KeyValueVM<string, BotEntity>
{
public BotVM(string key, BotEntity value) : base(key, value) { }
}// In Program.cs
builder.Services
// Core workspace infrastructure
.AddWorkspacesModel()
// Declare document types
.AddWorkspaceChildType<BotEntity>()
.AddWorkspaceChildType<Portfolio>()
// Register document services
.AddWorkspaceDocumentService<string, BotEntity>()
.AddWorkspaceDocumentService<string, Portfolio>()
// Register configurator
.AddSingleton<IWorkspaceServiceConfigurator, WorkspaceTypesConfigurator>();List Page (Bots.razor):
@page "/bots"
<ObservableDataView TKey="string"
TValue="BotEntity"
TValueVM="BotVM"
DataServiceProvider="@WorkspaceServices">
<Columns>
<PropertyColumn Property="x => x.Value.Name" />
<PropertyColumn Property="x => x.Value.Description" />
</Columns>
</ObservableDataView>
@code {
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
}Detail Page (Bot.razor):
@page "/bots/{BotId}"
@using Microsoft.Extensions.DependencyInjection
<MudTextField @bind-Value="VM.Value.Name" Label="Name" />
<MudButton OnClick="Save">Save</MudButton>
@code {
[Parameter]
public string? BotId { get; set; }
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
private ObservableReaderWriterItemVM<string, BotEntity, BotVM>? VM { get; set; }
protected override async Task OnParametersSetAsync()
{
var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>();
var writer = WorkspaceServices.GetService<IObservableWriter<string, BotEntity>>();
VM = new ObservableReaderWriterItemVM<string, BotEntity, BotVM>(reader, writer);
VM.Id = BotId;
}
private async Task Save() => await VM.Write();
}- Application creates workspace directory (e.g.,
C:\ProgramData\MyApp\Users\Alice\Workspaces\workspace1\) - User creates bots via UI ("Add" button in ObservableDataView)
- Files created:
workspace1\Bots\bot-alpha.hjson - User edits bot via detail page
- Changes saved to file
- File watching automatically updates UI in list page
Goal: Add "Strategy" document type to existing workspace application.
Steps:
- Define Entity:
[Alias("Strategy")]
public partial class StrategyEntity : ReactiveObject
{
[Reactive] private string? _name;
[Reactive] private string? _rules;
}- Define ViewModel:
public class StrategyVM : KeyValueVM<string, StrategyEntity>
{
public StrategyVM(string key, StrategyEntity value) : base(key, value) { }
}- Register:
services
.AddWorkspaceChildType<StrategyEntity>()
.AddWorkspaceDocumentService<string, StrategyEntity>();-
Create Pages (same as Bots example above)
-
Done - Strategies now available in workspaces!
Goal: Add application-specific services to workspace scope.
Create Custom Configurator:
public class MyAppWorkspaceConfigurator : IWorkspaceServiceConfigurator
{
public async ValueTask ConfigureWorkspaceServices(
IServiceCollection services,
UserWorkspacesService userWorkspacesService,
string? workspaceId)
{
// Add custom services
services.AddSingleton<IMyAppService>(sp => {
var workspaceDir = userWorkspacesService.UserWorkspaces?.GetChild(workspaceId);
return new MyAppService(workspaceDir);
});
}
}
// Register
services.AddSingleton<IWorkspaceServiceConfigurator, MyAppWorkspaceConfigurator>();Goal: Automatically start/stop bots when documents change.
Create Runner:
public class BotRunner :
IWorkspaceDocumentRunner<string, BotEntity>,
IObserver<BotEntity>
{
public Type RunnerType => typeof(BotRunner);
public void OnNext(BotEntity bot)
{
// React to bot document changes
if (bot.Enabled && !IsRunning(bot))
StartBot(bot);
else if (!bot.Enabled && IsRunning(bot))
StopBot(bot);
}
public void OnError(Exception error) { }
public void OnCompleted() { }
private bool IsRunning(BotEntity bot) { /* ... */ }
private void StartBot(BotEntity bot) { /* ... */ }
private void StopBot(BotEntity bot) { /* ... */ }
}Register:
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IWorkspaceDocumentRunner<string, BotEntity>, BotRunner>()
);Result: When user enables bot in UI, file updates, runner automatically starts bot.
Goal: Allow users to switch between workspaces or have multiple open.
UI Component:
<!-- Workspace selector -->
<MudSelect T="string" @bind-Value="CurrentWorkspaceId" Label="Workspace">
@foreach (var workspace in AvailableWorkspaces)
{
<MudSelectItem Value="@workspace">@workspace</MudSelectItem>
}
</MudSelect>
@code {
private string CurrentWorkspaceId { get; set; }
private List<string> AvailableWorkspaces { get; set; }
protected override void OnInitialized()
{
// Load available workspaces
var workspacesDir = userWorkspacesService.UserWorkspacesDir;
AvailableWorkspaces = Directory.GetDirectories(workspacesDir)
.Select(Path.GetFileName)
.ToList();
}
}Service Management:
// When workspace changes
async Task OnWorkspaceChanged(string workspaceId)
{
// Dispose old workspace services
oldWorkspaceServices?.Dispose();
// Create new workspace services
var services = new ServiceCollection();
foreach (var configurator in workspaceServiceConfigurators)
{
await configurator.ConfigureWorkspaceServices(services, userWorkspacesService, workspaceId);
}
WorkspaceServices = services.BuildServiceProvider();
// Update cascading value
StateHasChanged();
}✅ DO: Use CascadingParameter for workspace services in Blazor
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }❌ DON'T: Try to inject workspace services via @inject
@inject IObservableReader<string, BotEntity> Reader ❌ Won't work!✅ DO: Use kebab-case for file names
bot-alpha.hjson ✅
bot_beta.hjson ✅
BotGamma.hjson ⚠️ Works but inconsistent
❌ DON'T: Use special characters or spaces
bot#1.hjson ❌ Special characters
my bot.hjson ❌ Spaces cause issues
✅ DO: Use ReactiveObject and [Reactive] for change notifications
public partial class BotEntity : ReactiveObject
{
[Reactive] private string? _name; ✅
}❌ DON'T: Use plain properties without change notifications
public class BotEntity
{
public string Name { get; set; } ❌ UI won't update
}✅ DO: Let workspace system create subdirectories automatically
workspace1\
├── Bots\ ← Created automatically
├── Portfolios\ ← Created automatically
└── Strategies\ ← Created automatically
❌ DON'T: Create subdirectories manually or use non-standard names
✅ DO: Check for null services and log errors
if (WorkspaceServices == null)
{
Logger.LogError("WorkspaceServices not available");
return;
}
var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>();
if (reader == null)
{
Logger.LogError("Reader not registered for BotEntity");
return;
}❌ DON'T: Assume services are always available
Symptom: InvalidOperationException when trying to create VM or use workspace services.
Causes:
- Trying to inject from root container instead of workspace services
- Document type not registered with
AddWorkspaceChildType - Workspace services not built yet
Solutions:
- ✅ Use
CascadingParameterand resolve fromWorkspaceServices - ✅ Verify
AddWorkspaceChildType<T>()was called - ✅ Check that workspace layout provides
WorkspaceServices
See: Service Scoping Deep Dive
Causes:
- Wrong directory
- Wrong file extension
- Serialization error
- Not subscribed to observable
Debugging:
var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>();
Logger.LogInformation("Keys: {Keys}", string.Join(", ", reader?.Keys.Items ?? []));
// Expected: bot-alpha, bot-betaCauses:
- Didn't call
VM.Write() - File permissions issue
- Entity not implementing
INotifyPropertyChanged
Solution:
// Ensure you call Write()
await VM.Write();
// Check file permissions on workspace directory
// Ensure entity uses ReactiveObject- Workspaces = Isolated Service Scopes for user-organized documents
- File-Based Storage with HJSON format
- Reactive Updates via
IObservableReader/Writer - Extensible via
IWorkspaceServiceConfigurator - UI-Integrated with Blazor components
- Define entity types (
ReactiveObject) - Define ViewModels (
KeyValueVM) - Register with
AddWorkspaceChildType<T>() - Register document services
- Create Blazor list page (
ObservableDataView) - Create Blazor detail page (manual VM)
- Test with workspace layout providing
WorkspaceServices
- How-To: Create Blazor Workspace Page - Step-by-step tutorial
- Document Types Deep Dive - Advanced document patterns
- Blazor MVVM Patterns - UI implementation details