Adding User Settings
LightNap includes infrastructure for implementing a flexible user settings system that allows users to store and manage their personal preferences. This article demonstrates how to add a new user setting by walking through the PreferredLanguage setting—a comprehensive example that showcases integration with other services, automatic fallback logic, and reusable UI components.
- Adding User Settings
Understanding the User Settings System
The user settings system in LightNap provides a way to store key-value pairs associated with individual users. These settings are persisted in the database and are accessible across sessions. The system follows the standard LightNap architecture pattern:
- Backend: Settings are stored in the database and managed through a
UserSettingsServiceinLightNap.Core - API Layer: The
MeControllerexposes endpoints under/api/users/me/settingsfor managing settings - Frontend: The
ProfileServicemanages user settings, and components likeUserSettingToggleprovide UI controls for more convenient access
Settings are stored as string key-value pairs, allowing flexibility in what can be stored. The frontend is responsible for converting values to/from appropriate types when reading and writing settings.
Backend Implementation
Understanding the Backend Services
The backend user settings functionality is built around the IUserSettingsService interface in LightNap.Core/UserSettings/Interfaces. Key methods include:
public interface IUserSettingsService
{
// Get a specific setting by key
Task<string?> GetSettingAsync(string userId, string key);
// Get all settings for a user
Task<Dictionary<string, string>> GetAllSettingsAsync(string userId);
// Set or update a setting
Task SetSettingAsync(string userId, string key, string value);
// Delete a setting
Task DeleteSettingAsync(string userId, string key);
}
The service handles all database operations for user settings, and the MeController exposes these methods through REST endpoints that require the user to be authenticated.
Step 1: Define Setting Key Constants
To maintain consistency and avoid typos, create constants for your setting keys. Add them to LightNap.Core/Configuration/Constants.cs:
public static class UserSettingKeys
{
public const string BrowserSettings = "BrowserSettings";
/// <summary>
/// The user's preferred language code for content. Can be empty for auto-detection from browser.
/// </summary>
public const string PreferredLanguage = "PreferredLanguage";
// Add more setting keys as needed
}
The frontend file app/core/backend-api/user-setting-key.ts should be kept up to date with any changes to the backend’s UserSettingKeys for easier reference in frontend code.
Step 2: Define Supported Settings and Defaults
Since user settings can be any key-value pair, LightNap uses a registry to track which keys are valid to avoid database stuffing attacks. Add a new UserSettingDefinition record to UserSettingsConfig._allSettings at LightNap.Core/Configuration/UserSettingsConfig.cs to include new settings in the system:
private static readonly UserSettingDefinition[] _allSettings =
[
new UserSettingDefinition(
Constants.UserSettingKeys.BrowserSettings,
"",
UserSettingAccessLevel.UserReadWrite,
true),
new UserSettingDefinition(
// The key from your constants
Constants.UserSettingKeys.PreferredLanguage,
// The default value in JSON - empty string for auto-detect
"\"\"",
// Who can read and edit this setting
UserSettingAccessLevel.UserReadWrite,
// True if the setting is active, false if archived
true),
];
This approach provides several benefits:
- Centralized Documentation: All settings are documented in one place
- Default Values: Each setting has a clearly defined default value
- Security: Only registered settings can be stored, preventing database stuffing attacks
- API Documentation: Can be exposed through an endpoint for frontend consumption
Step 3: Reading User Settings
To read a user setting in your backend service, inject IUserSettingsService and call the GetUserSettingAsync method. This automatically handles access to the setting definitions to ensure you always get the correct JSON deserialization and default value:
public class MyService(
IUserSettingsService userSettingsService,
IUserContext userContext
)
{
public async Task<string> GetPreferredLanguageAsync()
{
var userId = userContext.GetUserId();
var preferredLanguage = await userSettingsService.GetUserSettingAsync<string>(
userId,
Constants.UserSettingKeys.PreferredLanguage
);
// Empty string means auto-detect from browser
return string.IsNullOrEmpty(preferredLanguage)
? GetBrowserLanguage()
: preferredLanguage;
}
}
Step 4: Writing User Settings
Settings are typically updated by the user through the frontend, but you can also set them programmatically:
public async Task SetPreferredLanguageAsync(string languageCode)
{
var userId = userContext.GetUserId();
await userSettingsService.SetUserSettingAsync(
userId,
new SetUserSettingRequestDto
{
Key = Constants.UserSettingKeys.PreferredLanguage,
Value = languageCode,
}
);
}
Step 5: Using Settings to Control Application Behavior
The PreferredLanguage setting demonstrates a sophisticated use case with automatic fallback logic. The ContentService in the frontend uses this setting to determine which language content to display:
#getPreferredLanguageCode(): Observable<string> {
return this.#identityService.watchLoggedIn$().pipe(
take(1),
switchMap(isLoggedIn => {
if (!isLoggedIn) return of(this.#getBrowserLanguageCode());
return this.#profileService.getSetting<string>(UserSettingKeys.PreferredLanguage, "").pipe(
map(preferredLanguage => {
// If user has set a preference, use it
if (preferredLanguage?.length > 0) return preferredLanguage;
// Otherwise fall back to browser language
return this.#getBrowserLanguageCode();
}),
catchError(() => of(this.#getBrowserLanguageCode()))
);
})
);
}
This pattern demonstrates:
- Graceful Fallbacks: Empty setting falls back to browser detection
- Error Handling: Service failures don’t break the user experience
- Authentication Awareness: Different behavior for logged-in vs anonymous users
Accessing settings should only be done after the initial browser handshake has completed during the app load. This will ensure that the authentication token is available when requesting settings. Use IdentityService.watchLoggedIn$().pipe(take(1)) in cases where you don’t know if that initial token retrieval will have completed yet.
Frontend Implementation
User settings on the frontend are accessed through the ProfileService, which is the main application service for user profile and settings data. The ProfileService is located in app/core/services/profile.service.ts.
Step 1: Define Setting Keys
Keep the frontend setting keys file at app/core/backend-api/user-setting-key.ts up to date with your backend constants:
/*
* Settings keys used in the application.
*/
export type UserSettingKey = "BrowserSettings" | "PreferredLanguage";
export const UserSettingKeys = {
BrowserSettings: "BrowserSettings",
PreferredLanguage: "PreferredLanguage",
} as const;
Step 2: Understanding ProfileService for Settings
The ProfileService provides three key methods for working with user settings:
// Get all settings as a dictionary
getSettings(): Observable<Array<UserSettingDto>>
// Get a specific setting with type safety
getSetting<T>(key: UserSettingKey, defaultValue?: T): Observable<T | null>
// Set a setting value
setSetting<T>(key: UserSettingKey, value: T): Observable<UserSettingDto>
The ProfileService automatically handles type conversion when retrieving values from the backend, but it’s just basic JSON deserialization, so you will need to do additional work for objects like classes and Dates.
Example: Getting typed settings with defaults
// String setting with empty string default (for auto-detect)
this.profileService.getSetting<string>(UserSettingKeys.PreferredLanguage, "")
.subscribe(language => {
this.preferredLanguage = language || this.getBrowserLanguage();
});
// The ProfileService handles JSON deserialization automatically
Step 3: Using Reusable Setting Components
LightNap provides reusable components for common setting UI patterns. For the PreferredLanguage setting, we use UserSettingSelectComponent, which handles select dropdowns with automatic persistence:
Creating a Custom Setting Component
The PreferredLanguageSelectComponent demonstrates how to create a specialized component that wraps UserSettingSelectComponent:
@Component({
selector: "ln-preferred-language-select",
standalone: true,
templateUrl: "./preferred-language-select.component.html",
imports: [CommonModule, UserSettingSelectComponent, ApiResponseComponent],
})
export class PreferredLanguageSelectComponent {
readonly #contentService = inject(ContentService);
readonly supportedLanguages = signal(
this.#contentService
.getSupportedLanguages()
.pipe(map(languages => [
new ListItem("", "Auto-detect"),
...languages.map(lang => new ListItem(lang.languageCode, lang.languageName))
]))
);
}
Template Usage
<ln-api-response [apiResponse]="supportedLanguages()">
<ng-template #success let-supportedLanguages>
<ln-user-setting-select
key="PreferredLanguage"
label="Preferred Language"
[options]="supportedLanguages"
[defaultValue]="null" />
</ng-template>
</ln-api-response>
This pattern demonstrates:
- Service Integration: Fetching options from
ContentService - Dynamic Options: Building option lists with
ListItem - Auto-detect Support: Including an empty value for automatic detection
-
Reusability: The component can be used anywhere in the application
For more details on creating reusable form components like this, see Reusable Form Components.
Additional Resources
Related Documentation
- Solution & Project Structure - Understanding the overall architecture
- API Response Model - Understanding REST API patterns
- Adding Profile Fields - Working with user-specific data
- Adding Entities - Creating database entities for custom settings
Data Flow Pattern
User settings follow the standard LightNap data flow pattern:
graph TD
subgraph Database
DB[(Database)]
end
subgraph Backend
UserSettingsService[UserSettingsService]
MeController[MeController]
end
subgraph Frontend
UserSettingsDataService[UserSettingsDataService]
UserSettingsAppService[UserSettingsService]
Components[Components & UserSettingToggle]
end
UserSettingsService -.-> |Entity Framework| DB
MeController --> |UserSettingsService methods| UserSettingsService
UserSettingsDataService -.-> |GET/POST /api/users/me/settings| MeController
UserSettingsAppService --> UserSettingsDataService
Components --> UserSettingsAppService
See Also
- Reusable Form Components - Creating composable form components
- Adding Entities - Creating database entities
- Adding Profile Fields - User-specific data
- Working With Roles - User permissions
- Solution & Project Structure - Architecture overview