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.
Environment Detection → JavaScript Config → DI Registration → Runtime Services
↓ ↓ ↓ ↓
Local vs Cloud → window.appConfig → ILzHost → API Clients
WASMApp/Program.cs
)window.appConfig
object populated by indexinit.js
Key Flow:
indexinit.js
detects environment and sets window.appConfig
Program.cs
waits for page load via WaitForPageLoad()
GetAppConfigAsync()
retrieves configuration via JavaScript interopMAUIApp/MauiProgram.cs
)appConfig.js
fileDebugger.IsAttached
for local/remote API switchingKey Flow:
AssemblyContent.ReadEmbeddedResource()
reads embedded appConfig.js
ExtractDataFromJs()
uses regex to extract JSON from JavaScriptLocated 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:
appPath: "/admin/"
appPath: "/store/"
appPath: "/consumer/"
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
};
Each client application follows a consistent DI registration pattern:
// 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
))
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 => ...);
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!);
// ...
localhost:5001
) or remote APIsASPNETCORE_ENVIRONMENT
controls behaviorisLocal = hostEnvironment.Environment != "Production"
IStaticAssets
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;
}
}
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 |
Config/Config.csproj
)Each application includes a Config
project that:
*.config.json
)configfiles.json
manifest during buildBuild-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>
Simple static configuration holder in ViewModels:
public static class AppConfig
{
public static string TenantName = string.Empty;
}
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;
}
*.config.json
files in Config projectASPNETCORE_ENVIRONMENT
in launchSettings.json as neededconfig.json
fileThis 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.