Core workspace infrastructure providing directory-backed, scoped service containers for user-centric document organization. This library implements the workspace service model, document service pattern, and runner infrastructure that enables isolated, file-based data management.
Key Concept: Workspaces are service scope containers - each workspace gets its own IServiceProvider with workspace-specific IObservableReader/Writer instances pointing to that workspace's directory.
Purpose: Manages the workspace directory location and provides access to workspace references.
Lifecycle: Typically registered as singleton in root DI container, but stores per-user/per-workspace state.
public class UserWorkspacesService
{
// Root service provider
public IServiceProvider ServiceProvider { get; }
// User-scoped services (optional)
public IServiceProvider? UserServices { get; set; }
// Base directory for all workspaces
public IReference? UserWorkspaces { get; set; }
// Filesystem path (convenience accessor)
public string? UserWorkspacesDir { get; set; }
public UserWorkspacesService(IServiceProvider serviceProvider) { }
}Usage:
// Typically configured by UI layer
userWorkspacesService.UserWorkspacesDir = "C:\\Users\\Alice\\Trading\\Workspaces";
// Get specific workspace directory
var workspaceRef = userWorkspacesService.UserWorkspaces.GetChild("MyWorkspace");
// Result: C:\Users\Alice\Trading\Workspaces\MyWorkspaceNotes:
- Stores workspace base path, not individual workspace services
- Individual workspace service providers created on-demand
- Lifetime: Singleton in root container
- Configuration: Usually set by
WorkspaceLayoutVMor similar UI component
Purpose: Stores the list of document types that can live in workspaces.
public class WorkspaceConfiguration
{
// Types registered via AddWorkspaceChildType<T>()
public List<Type> MemberTypes { get; set; } = new();
}Usage:
// Registration (in Program.cs)
services
.AddWorkspaceChildType<BotEntity>() // Adds to MemberTypes
.AddWorkspaceChildType<Portfolio>() // Adds to MemberTypes
.Configure<WorkspaceConfiguration>(config => {
// MemberTypes now contains [typeof(BotEntity), typeof(Portfolio)]
});Used By: WorkspaceTypesConfigurator to know which types to register readers/writers for.
Purpose: Represents a workspace instance (currently minimal, extensible via FlexObject).
public class Workspace : FlexObject, IWorkspace
{
// Currently just a marker type
// Can be extended via FlexData property for workspace-specific settings
}Usage:
// Workspaces are persisted as HJSON files in parent directory
// C:\Users\Alice\Trading\Workspaces\workspace1.hjson
// Access via IObservableReader<string, Workspace>
var workspacesReader = services.GetService<IObservableReader<string, Workspace>>();
var workspace = await workspacesReader.TryGetValue("workspace1");Future: May hold workspace-specific configuration, preferences, or metadata.
Interface (from LionFire.Workspaces.Abstractions):
public interface IWorkspaceServiceConfigurator
{
ValueTask ConfigureWorkspaceServices(
IServiceCollection services,
UserWorkspacesService userWorkspacesService,
string? workspaceId);
}Purpose: Callback interface invoked when a workspace service provider is being built.
Implementation: WorkspaceTypesConfigurator (registers readers/writers for member types).
Usage:
// Register configurator in root services
services.AddSingleton<IWorkspaceServiceConfigurator, WorkspaceTypesConfigurator>();
services.AddSingleton<IWorkspaceServiceConfigurator, MyCustomConfigurator>();
// When workspace is loaded, all configurators are invoked:
foreach (var configurator in serviceProvider.GetServices<IWorkspaceServiceConfigurator>())
{
await configurator.ConfigureWorkspaceServices(workspaceServices, userWorkspacesService, "workspace1");
}Purpose: Main configurator that registers IObservableReader/Writer for each member type.
What It Does:
- Gets workspace directory from
UserWorkspacesService - For each type in
WorkspaceConfiguration.MemberTypes:- Calls
RegisterObservablesInSubDirForType<T>() - Registers
IObservableReader<string, T> - Registers
IObservableWriter<string, T> - Points to
{WorkspaceDir}/{PluralTypeName}/subdirectory
- Calls
Example:
// Given:
// WorkspaceDir: C:\Users\Alice\Trading\Workspaces\workspace1
// MemberTypes: [BotEntity, Portfolio]
// Configurator registers:
// IObservableReader<string, BotEntity>
// -> Points to C:\Users\Alice\Trading\Workspaces\workspace1\Bots\
// IObservableWriter<string, BotEntity>
// -> Points to C:\Users\Alice\Trading\Workspaces\workspace1\Bots\
// IObservableReader<string, Portfolio>
// -> Points to C:\Users\Alice\Trading\Workspaces\workspace1\Portfolios\
// IObservableWriter<string, Portfolio>
// -> Points to C:\Users\Alice\Trading\Workspaces\workspace1\Portfolios\Source (simplified):
public class WorkspaceTypesConfigurator : IWorkspaceServiceConfigurator
{
public WorkspaceConfiguration Options { get; }
public IServiceProvider ServiceProvider { get; }
public async ValueTask ConfigureWorkspaceServices(
IServiceCollection services,
UserWorkspacesService userWorkspacesService,
string? workspaceId)
{
if (userWorkspacesService.UserWorkspaces == null) return;
var workspaceReference = userWorkspacesService.UserWorkspaces.GetChild(workspaceId);
await WorkspaceSchemas.InitFilesystemSchemas(userWorkspacesService.UserWorkspaces);
var dir = new DirectoryReferenceSelector(userWorkspacesService.UserWorkspaces) { Recursive = true };
// For each registered member type
foreach (var type in Options.MemberTypes)
{
// Register IObservableReader/Writer for this type
var method = typeof(FsObservableCollectionFactoryX)
.GetMethod(nameof(FsObservableCollectionFactoryX.RegisterObservablesInSubDirForType))!;
var genericMethod = method.MakeGenericMethod(type);
genericMethod.Invoke(this, [services, ServiceProvider, dir.Path, null]);
}
}
}Purpose: Hosted service that watches workspace documents and invokes runners when documents change.
Inheritance: WorkspaceDocumentService<string, TValue> → IHostedService
What It Does:
- Subscribes to
IObservableReader<string, TValue>.Values.Connect() - When documents are added/modified/removed:
- Finds registered
IWorkspaceDocumentRunner<string, TValue>instances - Creates runner instances per document key
- Calls
runner.OnNext(entity)to notify of changes
- Finds registered
Registration:
services.AddWorkspaceDocumentService<string, BotEntity>();
// Internally registers:
services.AddHostedSingleton<DirectoryWorkspaceDocumentService<BotEntity>>();Hardcoded Directory (temporary):
static string dir = "C:\\ProgramData\\LionFire\\Trading\\Users"; // TEMP HARDCODENote: Currently uses a hardcoded path - should be refactored to use workspace directories.
Purpose: Base class for document services, manages runner lifecycle.
Key Features:
- Subscribes to
IObservableReader.Values.Connect()on startup - Maintains
ConcurrentDictionary<TKey, IObserver<TValue>>per runner type - Creates runner instances on-demand using
ActivatorUtilities - Calls
runner.OnNext(value)when documents change - Disposes subscriptions on shutdown
Flow:
Document Added/Modified (bot1.hjson)
↓
IObservableReader.Values emits changeset
↓
WorkspaceDocumentService.OnValue(key, entity)
↓
For each IWorkspaceDocumentRunner<string, BotEntity>:
Get or create runner instance for "bot1"
Call runner.OnNext(entity)
Purpose: Marker interface for runner services that react to document changes.
public interface IWorkspaceDocumentRunner<TKey, TValue>
{
Type RunnerType { get; }
}Implementation Pattern:
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)
StartBot(bot);
else
StopBot(bot);
}
public void OnError(Exception error) { }
public void OnCompleted() { }
}Registration:
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IWorkspaceDocumentRunner<string, BotEntity>, BotRunner>()
);Result: When bot HJSON files are created/modified, BotRunner.OnNext() is automatically called.
Purpose: Base class helper for implementing runners (optional convenience).
Purpose: Defines directory schemas for workspace organization.
What It Does:
- Defines schema for workspace directory structure
- Initializes schema metadata files in filesystem
- Ensures proper directory conventions
Usage:
// Called when workspace services are configured
await WorkspaceSchemas.InitFilesystemSchemas(userWorkspacesService.UserWorkspaces);
// Creates schema files in workspace directory
// Defines conventions for workspace structureSchema Namespace: https://schemas.lionfire.ca/2025/ui
Methods:
public static IServiceCollection AddWorkspacesModel(this IServiceCollection services)Purpose: Registers workspace model types (currently minimal).
Usage:
services.AddWorkspacesModel();Methods:
public static IServiceCollection AddWorkspaceDocumentService<TKey, TValue>(
this IServiceCollection services)
where TKey : notnull
where TValue : notnullPurpose: Registers DirectoryWorkspaceDocumentService<TValue> as hosted service.
Usage:
services
.AddWorkspaceChildType<BotEntity>() // Declare type
.AddWorkspaceDocumentService<string, BotEntity>(); // Register serviceWhat It Does:
services.AddHostedSingleton<DirectoryWorkspaceDocumentService<TValue>>();var builder = WebApplication.CreateBuilder(args);
builder.Services
// Basic workspace infrastructure
.AddWorkspacesModel()
// Declare document types
.AddWorkspaceChildType<BotEntity>()
.AddWorkspaceChildType<Portfolio>()
// Register document services (hosted services)
.AddWorkspaceDocumentService<string, BotEntity>()
.AddWorkspaceDocumentService<string, Portfolio>()
// Register workspace configurators
.AddSingleton<IWorkspaceServiceConfigurator, WorkspaceTypesConfigurator>()
// Optional: Register runners
.TryAddEnumerable(ServiceDescriptor.Singleton<
IWorkspaceDocumentRunner<string, BotEntity>, BotRunner>());// Set workspace base directory
userWorkspacesService.UserWorkspacesDir = "C:\\Users\\Alice\\Trading\\Workspaces";
// Create workspace-scoped services
var workspaceServices = new ServiceCollection();
// Invoke all configurators
foreach (var configurator in serviceProvider.GetServices<IWorkspaceServiceConfigurator>())
{
await configurator.ConfigureWorkspaceServices(
workspaceServices,
userWorkspacesService,
"workspace1"
);
}
// Build workspace service provider
var workspaceProvider = workspaceServices.BuildServiceProvider();
// Now workspace provider has:
// IObservableReader<string, BotEntity> -> Bots/ subdirectory
// IObservableWriter<string, BotEntity> -> Bots/ subdirectory
// IObservableReader<string, Portfolio> -> Portfolios/ subdirectory
// IObservableWriter<string, Portfolio> -> Portfolios/ subdirectory<CascadingValue Name="WorkspaceServices" Value="@WorkspaceProvider">
@Body
</CascadingValue>
@code {
public IServiceProvider? WorkspaceProvider { get; set; }
}@code {
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
protected override void OnInitialized()
{
var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>();
// Use reader to access bots in this workspace
}
}When set up, workspace directories look like:
C:\Users\Alice\Trading\Workspaces\
├── workspace1.hjson ← Workspace metadata
├── workspace1\ ← Workspace directory
│ ├── Bots\
│ │ ├── bot-alpha.hjson
│ │ └── bot-beta.hjson
│ ├── Portfolios\
│ │ └── portfolio1.hjson
│ └── Strategies\
│ └── strategy1.hjson
└── workspace2.hjson ← Another workspace
└── workspace2\
├── Bots\
└── Portfolios\
Each workspace gets isolated services:
Root Container
├── UserWorkspacesService (singleton)
├── IWorkspaceServiceConfigurator[] (collection)
└── DirectoryWorkspaceDocumentService<T> (hosted service)
Workspace Container (per workspace)
├── IObservableReader<string, BotEntity> ← Workspace-specific
├── IObservableWriter<string, BotEntity> ← Workspace-specific
└── ... (other member types)
Extensible configuration via IWorkspaceServiceConfigurator:
- Core framework provides
WorkspaceTypesConfigurator - Applications can add custom configurators
- All invoked when workspace services are built
Document changes trigger runners:
Document Change → IObservableReader → WorkspaceDocumentService → Runner.OnNext()
DirectoryWorkspaceDocumentService<T> runs as background service:
- Starts when application starts
- Subscribes to document changes
- Manages runner lifecycle
- Stops when application stops
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Options" /><ProjectReference Include="..\LionFire.Workspaces.Abstractions\" />
<ProjectReference Include="..\LionFire.Reactive\" />
<ProjectReference Include="..\LionFire.IO.Reactive.Hjson\" />
<ProjectReference Include="..\LionFire.Referencing\" />
<ProjectReference Include="..\LionFire.FlexObjects\" />
<ProjectReference Include="..\LionFire.Schemas\" />- Workspace Architecture - High-level overview
- Service Scoping Deep Dive - DI and scoping details
- LionFire.Workspaces.Abstractions - Interfaces
- LionFire.Workspaces.UI.Blazor - Blazor UI components
- LionFire.Reactive - Observable readers/writers
LionFire.Workspaces provides the core workspace infrastructure:
✅ Service Scoping: Workspace-scoped service providers ✅ Configuration: Extensible configurator pattern ✅ Document Services: Hosted services watching for changes ✅ Runner Pattern: React to document changes with lifecycle management ✅ Directory Conventions: HJSON file-based storage with subdirectories per type
Use This Library To: Build applications where users organize documents into isolated workspaces with file-based persistence and reactive updates.