LazyMagic

Client-Side Configuration System

Overview

The MagicPets client applications use a multi-layered configuration system that supports both development and production scenarios across Blazor WebAssembly (WASM) and .NET MAUI platforms. The configuration architecture is designed to handle environment detection, API endpoint switching, and multi-tenant asset management.

Configuration Architecture

Configuration Sources Hierarchy

Environment Detection → JavaScript Config → DI Registration → Runtime Services
        ↓                    ↓                 ↓               ↓
   Local vs Cloud →    window.appConfig →   ILzHost   →   API Clients

Platform-Specific Configuration Loading

WASM Applications (WASMApp/Program.cs)

  • JavaScript-based Configuration: Uses window.appConfig object populated by indexinit.js
  • Environment Detection: Automatically detects localhost vs. cloud deployment
  • Async Loading: Waits for page load completion before retrieving configuration
  • Static Configuration: Configuration retrieved via JavaScript interop after page initialization

Key Flow:

  1. indexinit.js detects environment and sets window.appConfig
  2. Program.cs waits for page load via WaitForPageLoad()
  3. GetAppConfigAsync() retrieves configuration via JavaScript interop
  4. Configuration used to register services in DI container

MAUI Applications (MAUIApp/MauiProgram.cs)

  • Embedded Resource Configuration: Uses embedded appConfig.js file
  • Regex Extraction: Parses JavaScript file to extract JSON configuration
  • Platform Detection: Automatically detects Android vs. other platforms
  • Debug Detection: Uses Debugger.IsAttached for local/remote API switching

Key Flow:

  1. AssemblyContent.ReadEmbeddedResource() reads embedded appConfig.js
  2. ExtractDataFromJs() uses regex to extract JSON from JavaScript
  3. Configuration immediately available for service registration

Core Configuration Files

appConfig.js (Per Application)

Located in BlazorUI/wwwroot/appConfig.js for each app:

export const appConfig = {
    appPath: "/admin/",           // Application path (differs per app)
    androidAppUrl: "",            // Android-specific URL (MAUI only)
    remoteApiUrl: "https://uptown.lazymagicdev.click/",  // Production API
    localApiUrl: "https://localhost:5001/",              // Development API
    assetsUrl: "https://uptown.lazymagicdev.click/",     // Static assets URL
    useLocalHostApi: false        // Development flag
}

App-Specific Differences:

  • AdminApp: appPath: "/admin/"
  • StoreApp: appPath: "/store/"
  • ConsumerApp: appPath: "/consumer/"

Configuration Processing in indexinit.js

Environment-aware configuration merging:

// Development (localhost)
window.appConfig = {
    appPath: appConfig.appPath,
    appUrl: window.location.origin,           // localhost:port
    remoteApiUrl: appConfig.remoteApiUrl,     // Cloud API
    localApiUrl: appConfig.localApiUrl,       // Local API
    assetsUrl: appConfig.assetsUrl            // Cloud assets
};

// Production (cloud)
window.appConfig = {
    appPath: appPath,                         // Derived from base href
    appUrl: window.location.origin + "/",     // Cloud origin
    remoteApiUrl: window.location.origin + "/", // Same origin
    assetsUrl: window.location.origin + "/",    // Same origin
    wsUrl: window.location.origin.replace(/^http/, 'ws') + "/"  // WebSocket
};

Dependency Injection Configuration

Service Registration Flow

Each client application follows a consistent DI registration pattern:

  1. Platform-Specific Services (WASM/MAUI-specific)
  2. AddAppViewModels() - Application-specific ViewModels and API clients
  3. AddBlazorUI() - UI components and services

Core Services Registered

HTTP and Host Services

// HTTP Client for assets
.AddSingleton(sp => new HttpClient { 
    BaseAddress = new Uri((string)_appConfig!["assetsUrl"]!) 
})

// Static Assets Service
.AddSingleton<IStaticAssets>(sp => new BlazorStaticAssets(...))

// Host Configuration
.AddSingleton<ILzHost>(sp => new LzHost(
    appPath: (string)_appConfig!["appPath"]!,
    appUrl: (string)_appConfig!["appUrl"]!,
    remoteApiUrl: (string)_appConfig!["remoteApiUrl"]!,
    localApiUrl: (string)_appConfig!["localApiUrl"]!,
    assetsUrl: (string)_appConfig!["assetsUrl"]!,
    isMAUI: false/true,
    isLocal: isLocal,
    useLocalhostApi: useLocalhostApi
))

Application ViewModels (ConfigureViewModels.cs)

// Global application state
services.AddSingleton<ISessionsViewModel, SessionsViewModel>();

// API Client (app-specific)
services.AddSingleton<IAdminApi>(serviceProvider => {
    var lzHttpClient = serviceProvider.GetRequiredService<ILzHttpClient>();
    return new AdminApi.AdminApi(lzHttpClient);
});

// Module Clients (based on API access level)
services.AddSingleton<IAdminModuleClient>(provider => provider.GetRequiredService<IAdminApi>());
services.AddSingleton<IStoreModuleClient>(provider => provider.GetRequiredService<IAdminApi>());
// ... other modules based on app permissions

// Current Session Access
services.AddTransient<ICurrentSessionViewModel>(provider => ...);

Base Application Services (ConfigureBaseAppViewModels.cs)

// Core framework services
services.TryAddSingleton<ILzMessages, LzMessages>();
services.TryAddSingleton<ILzClientConfig, LzClientConfig>();
services.TryAddSingleton<ILzHttpClient, LzHttpClientCognito>();
services.AddLazyMagicAuthCognito();

// Default module client registrations (overridden by apps)
services.TryAddSingleton<IPublicModuleClient>(provider => null!);
services.TryAddSingleton<IConsumerModuleClient>(provider => null!);
// ...

Environment-Specific Behavior

Development Environment

  • API Flexibility: Can use either local (localhost:5001) or remote APIs
  • Asset Loading: Always loads from remote CDN for consistency
  • Environment Variable: ASPNETCORE_ENVIRONMENT controls behavior
  • Local Detection: isLocal = hostEnvironment.Environment != "Production"

Production Environment

  • Same-Origin APIs: All requests go to the same CloudFront distribution
  • Optimized Caching: Service worker handles aggressive caching
  • CDN Assets: All static content served via CloudFront

Multi-Tenant Configuration

Tenant-Aware Services

  • TenantConfigViewModel: Loads tenant-specific configuration from static assets
  • Multi-Tenant Assets: Assets resolved per tenant via IStaticAssets
  • Localization: Multi-language support through ILzMessages

Tenant Configuration Loading:

public class TenantConfigViewModel : LzViewModel
{
    public async Task ReadAsync(string url)
    {
        if(IsLoaded) return;
        var jsonDoc = await staticAssets.ReadContentAsync(url);
        TenantConfig = JsonConvert.DeserializeObject<TenantConfig>(jsonDoc);
        IsLoaded = true;
    }
}

Application-Specific Differences

Aspect AdminApp StoreApp ConsumerApp
API Client IAdminApi IStoreApi IConsumerApi + IPublicApi
App Path /admin/ /store/ /consumer/
Module Access All modules Store + Consumer + Public Consumer + Public
Authentication TenantAuth TenantAuth ConsumerAuth

Configuration Layer Interaction

1. Config Project (Config/Config.csproj)

Each application includes a Config project that:

  • Contains placeholder for runtime configuration files (*.config.json)
  • Generates configfiles.json manifest during build
  • Supports multiple configuration profiles (dev, test, prod)
  • Configuration files are NOT checked into source control

Build-Time Manifest Generation:

<Target Name="GenerateConfigFileManifest" AfterTargets="Build">
    <ItemGroup>
        <ConfigFiles Include="wwwroot\*.config.json" />
    </ItemGroup>
    <PropertyGroup>
        <ConfigFilesJsonPath>$(ProjectDir)wwwroot/configfiles.json</ConfigFilesJsonPath>
        <ConfigFilesJsonContent>[@(ConfigFiles->'"%(Filename)%(Extension)"', ',')]</ConfigFilesJsonContent>
    </PropertyGroup>
    <WriteLinesToFile File="$(ConfigFilesJsonPath)" Lines="$(ConfigFilesJsonContent)" Overwrite="true" />
</Target>

2. Static AppConfig Class

Simple static configuration holder in ViewModels:

public static class AppConfig
{
    public static string TenantName = string.Empty;
}

3. BlazorUI Configuration

Extends base configuration with app-specific settings:

public static IServiceCollection AddBlazorUI(this IServiceCollection services)
{
    services.AddBaseAppBlazorUI();
    return services;
}

public static ILzMessages AddBlazorUIMessages(this ILzMessages lzMessages)
{
    lzMessages.AddBaseAppMessages();
    List<string> messages = [
        "system/{culture}/AdminApp/Messages.json",
    ];
    lzMessages.MessageFiles.AddRange(messages);
    return lzMessages;
}

Configuration Benefits

Development Experience

  • Hot Swapping: Switch between local and remote APIs without rebuilding
  • Environment Detection: Automatic configuration based on deployment context
  • Consistent Assets: Always load from CDN for realistic testing

Production Deployment

  • Single Origin: All resources served from same CloudFront distribution
  • Caching Strategy: Aggressive caching with proper cache invalidation
  • Multi-Tenant: Tenant-specific configuration and assets

Cross-Platform

  • Shared Configuration: Same configuration format for WASM and MAUI
  • Platform Optimization: Platform-specific optimizations (Android detection, etc.)
  • Unified Experience: Consistent behavior across web and mobile

Configuration Workflow

Development Setup

  1. Create appropriate *.config.json files in Config project
  2. Use LazyMagic tools to generate client configuration from Service stack
  3. Set ASPNETCORE_ENVIRONMENT in launchSettings.json as needed
  4. Run LocalWebService for local API development

Production Deployment

  1. Generate production config.json file
  2. Deploy to tenant-specific S3 bucket
  3. CloudFront serves configuration with application
  4. Service worker handles caching and updates

This configuration system provides a robust, flexible foundation that supports the complex requirements of a multi-tenant, multi-platform SaaS application while maintaining developer productivity and deployment simplicity.