Skip to content

CRUD Operations

Every document type must have a public Id property of type Guid, int, long, or string. The Id is stored in both the SQLite Id column and inside the JSON blob, so query results always include it.

public class User
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int Age { get; set; }
public string? Email { get; set; }
}
Id CLR TypeDefault ValueAuto-Gen Strategy
GuidGuid.EmptyGuid.NewGuid()
stringnull or ""Guid.NewGuid().ToString("N")
int0MAX(CAST(Id AS INTEGER)) + 1 per TypeName
long0MAX(CAST(Id AS INTEGER)) + 1 per TypeName

When Set is called with a default Id, the store auto-generates one and writes it back to the object. When a non-default Id is provided, it is used as-is.

// Auto-generated ID — written back to the object
var user = new User { Name = "Alice", Age = 25 };
await store.Set(user);
// user.Id is now populated
// Explicit ID
await store.Set(new User { Id = "user-1", Name = "Alice", Age = 25 });

Upsert uses SQLite’s json_patch() (RFC 7396 JSON Merge Patch) to deep-merge a partial patch into an existing document. If the document doesn’t exist, it is inserted as-is. Unlike Set, which replaces the entire document, Upsert only overwrites the fields present in the patch. The document must have a non-default Id.

// Insert a full document
await store.Set(new User { Id = "user-1", Name = "Alice", Age = 25, Email = "alice@test.com" });
// Merge patch — only update Name and Age, preserve Email
await store.Upsert(new User { Id = "user-1", Name = "Alice", Age = 30 });
var user = await store.Get<User>("user-1");
// user.Name == "Alice", user.Age == 30, user.Email == "alice@test.com" (preserved)

How it works:

  • On insert (new ID): the patch is stored as the full document.
  • On conflict (existing ID): json_patch(existing, patch) deep-merges the patch into the stored JSON. Objects are recursively merged; scalars and arrays are replaced.
  • Null properties are excluded from the patch automatically. In C#, unset nullable properties (e.g. string? Email) serialize as null, which would remove the key under RFC 7396. The library strips these so that unset fields are preserved rather than deleted.

SetProperty updates a single scalar field in-place using SQLite’s json_set() — no deserialization, no full document replacement. Returns true if the document was found and updated, false if not found.

// Update a scalar field
await store.SetProperty<User>("user-1", u => u.Age, 31);
// Update a string field
await store.SetProperty<User>("user-1", u => u.Email, "newemail@test.com");
// Set a field to null
await store.SetProperty<User>("user-1", u => u.Email, null);
// Nested property
await store.SetProperty<Order>("order-1", o => o.ShippingAddress.City, "Portland");
// Check if document existed
bool updated = await store.SetProperty<User>("user-1", u => u.Age, 31);

How it works: The expression u => u.Age is resolved to the JSON path $.age (respecting [JsonPropertyName] attributes and naming policies). The SQL executed is:

UPDATE documents
SET Data = json_set(Data, '$.age', json('31')), UpdatedAt = @now
WHERE Id = @id AND TypeName = @typeName;

Supported value types: string, int, long, double, float, decimal, bool, and null. To replace a collection or nested object, use Set (full replacement) or Upsert (merge patch).

RemoveProperty strips a field from the stored JSON using SQLite’s json_remove(). Returns true if the document was found and updated, false if not found. The removed field will have its C# default value on next read.

// Remove a nullable field
await store.RemoveProperty<User>("user-1", u => u.Email);
// Remove a nested property
await store.RemoveProperty<Order>("order-1", o => o.ShippingAddress.City);
// Remove a collection property (removes the entire array)
await store.RemoveProperty<Order>("order-1", o => o.Tags);

Unlike SetProperty, RemoveProperty works on any property type — scalar, nested object, or collection — because it simply removes the key from the JSON.

OperationUse whenScopeCollections
SetPropertyChanging one scalar fieldSingle field via json_setScalar values only
RemovePropertyStripping a field from the documentSingle field via json_removeAny property type
UpsertPatching multiple fields at onceDeep merge via json_patchReplaces arrays (RFC 7396)
SetReplacing the entire documentFull replacementFull control
var user = await store.Get<User>("user-1");
var users = await store.Query<User>().ToList();
// By ID
bool deleted = await store.Remove<User>("user-1");
// Returns number of deleted rows
int deleted = await store.Query<User>().Where(u => u.Age < 18).ExecuteDelete();
// Update a property on matching docs — returns number of updated rows
int updated = await store.Query<User>()
.Where(u => u.Age < 18)
.ExecuteUpdate(u => u.Age, 18);

See Querying for more examples of bulk delete and update with expressions.

int deletedCount = await store.Clear<User>();