---
title: Save preference
summary: Route a preference save request to delete, update, or insert behavior based on payload content and existing-row state.
status: draft
owner: SAIF Team
last_reviewed: 2026-07-30
library: SAIF.Framework.Preferences
reference_type: package
operation_id: OP-PREF-003
trigger: Caller invokes PreferenceData.SavePreferences
---

# Save preference

This operation is the library's only write entry point. It never inserts, updates, or deletes directly; instead it inspects the supplied text, treats blank content as a delete request, and otherwise chooses update versus insert by checking whether the scoped preference row already exists.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-PREF-003 |
| **Trigger** | Caller invokes `PreferenceData.SavePreferences(appName, pageName, userId, preferenceText)`. |
| **Inputs** | `appName`, `pageName`, `userId`, `preferenceText` |
| **Outputs** | `true` after the delegated write path completes, or an exception if preprocessing/delegated execution fails |
| **Primary components** | `PreferenceData.SavePreferences`, `PreferenceData.DeletePref`, `PreferenceData.DoesPrefExist`, `PreferenceData.UpdatePref`, `PreferenceData.InsertPref` |
| **Entry point** | `SAIF.Framework.Preferences\PreferenceData.cs` |

## Steps

1. Evaluate `preferenceText.Trim()` and pass the result to `string.IsNullOrEmpty(...)` to decide whether the caller supplied usable content ([BR-002](../business-rules.md#br-002-empty-preference-text-triggers-deletion)).
   - On failure: if `preferenceText` is `null`, the immediate `Trim()` call throws `NullReferenceException`; this method does not guard that case locally.
2. If the trimmed text is empty, dispatch to [Delete preference row](delete-preference-row.md), which applies the scoped delete against [USER_PREFERENCE](../database-tables.md#user_preference) using [SQL-006](../sql/SQL-006-delete-preference.md) and the scope rules [BR-001](../business-rules.md#br-001-user-ids-are-normalized-to-lower-case), [BR-003](../business-rules.md#br-003-preference-identity-is-scoped-by-app-user-and-optional-page).
   - The delete path returns `true` even when no row matches the delete criteria.
3. If the trimmed text is not empty, call [Check preference existence](check-preference-existence.md) to count rows in [USER_PREFERENCE](../database-tables.md#user_preference) via [SQL-007](../sql/SQL-007-does-pref-exist.md) under [BR-001](../business-rules.md#br-001-user-ids-are-normalized-to-lower-case) and [BR-003](../business-rules.md#br-003-preference-identity-is-scoped-by-app-user-and-optional-page).
   - If the helper returns `true`, continue to step 4.
   - If the helper returns `false`, continue to step 5.
4. When a scoped row already exists, call [Update preference row](update-preference-row.md), which updates [USER_PREFERENCE](../database-tables.md#user_preference) through [SQL-005](../sql/SQL-005-update-preference.md) and enforces [BR-001](../business-rules.md#br-001-user-ids-are-normalized-to-lower-case), [BR-003](../business-rules.md#br-003-preference-identity-is-scoped-by-app-user-and-optional-page), [BR-004](../business-rules.md#br-004-update-is-skipped-when-stored-value-is-unchanged), and [BR-005](../business-rules.md#br-005-preference-text-is-trimmed-before-storage).
   - Because the update SQL contains the no-op guard from [BR-004](../business-rules.md#br-004-update-is-skipped-when-stored-value-is-unchanged), unchanged text can lead to zero rows updated while this operation still returns `true`.
5. When no scoped row exists, call [Insert preference row](insert-preference-row.md), which allocates identity with [SQL-001](../sql/SQL-001-insert-preference-sequence.md), inserts the row with [SQL-004](../sql/SQL-004-insert-preference.md), and enforces [BR-001](../business-rules.md#br-001-user-ids-are-normalized-to-lower-case), [BR-003](../business-rules.md#br-003-preference-identity-is-scoped-by-app-user-and-optional-page), and [BR-005](../business-rules.md#br-005-preference-text-is-trimmed-before-storage) against [USER_PREFERENCE](../database-tables.md#user_preference).
6. Return `true` to the caller after the selected delete/update/insert helper completes.
   - On failure: exceptions raised by the delegated helper methods are not caught here and propagate to the caller.

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
	Start([Caller invokes SavePreferences]) --> Trim[Trim preferenceText]
	Trim --> Empty{Trimmed text empty?}
	Empty -->|Yes| Delete[Delete preference row]
	Empty -->|No| Exists[Check preference existence]
	Exists -->|Yes| Update[Update preference row]
	Exists -->|No| Insert[Insert preference row]
	Delete --> EndOk([Return true])
	Update --> EndOk
	Insert --> EndOk
```

## Failure and edge-case behavior

- `preferenceText` is `null`: `Trim()` throws before delete/update/insert routing begins.
- Blank or whitespace-only text always routes to delete; this method never persists an empty `PREFERENCE_TEXT` value.
- The return value is a plain `true`/exception signal — it does not distinguish between insert, update, delete, or a no-op update/delete outcome.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [SQL catalog](../sql/index.md)
- [Database tables](../database-tables.md)

## Sources
- SAIF.Framework.Preferences\PreferenceData.cs
