How to Add Commit and Rollback to a C# Object Store
An operation that changes several stored objects should either take effect completely or not at all. A FileObjectSpace in Sphere10 Framework writes through a transactional stream, so every change made since the last commit is staged and can be published or discarded as one unit.
This article shows what Save, Commit and Rollback each do to the file, how to roll back a group of changes that spans several dimensions, and what happens to the objects you were holding when a rollback occurs. It uses the store built in
the embedded object database article
.
1. Roll back a failed operation
The code that calls a multi-object operation wraps it and rolls back when anything after the staged writes fails. StageChanges is the sample's method that updates a customer in one dimension and inserts a purchase in another, and a later section shows it in full.
try {
StageChanges(space, 103, "PO-103", 1, 60m, 9);
pendingObserved = space.TryGet((Purchase item) => item.Reference, "PO-103", out var pending);
throw new InvalidOperationException("A later step failed after the writes were staged.");
} catch (InvalidOperationException) {
space.Rollback();
}
The second line finds the staged insert before the failure, which shows that pending changes are visible inside the space that made them. Rollback() then removes both writes together, including the index entries of the inserted object.
2. How the file transaction works
A file-backed ObjectSpace never writes changes straight into its data file. Changed pages are kept in a separate folder, set with KeepUncommittedPagesAt, until the application decides their fate.
Diagram source
stateDiagram-v2
direction TB
[*] --> Committed: Load()
Committed --> Pending: Save or Delete
Pending --> Pending: Save or Delete
Pending --> Committed: Commit()
Pending --> Committed: Rollback()
Pending --> [*]: Dispose()
Each member of the API has one job.
| Call | Effect on a FileObjectSpace |
Save, Delete |
Stage the change. Later reads in the same space see it. |
Flush |
Writes cached state into the pending pages. It does not publish them. |
Commit() |
Flushes, then publishes all pending pages to the data file. |
Rollback() |
Discards all pending pages and reloads the space from the data file. |
Dispose() |
Neither commits nor rolls back. Uncommitted changes are abandoned. |
One ObjectSpace has one transaction. It covers every dimension and every index in the file, so there is nothing to enlist and no way to commit one dimension without the others.
3. Open a transactional store
Choosing the file backend is what makes the store transactional. The builder call below is the relevant part of the sample's factory.
var builder = new ObjectSpaceBuilder()
.UseFile(Path.Combine(directory, "sample.s10"))
.KeepUncommittedPagesAt(Path.Combine(directory, "pending-pages"))
.WithClusterSize(4096)
.WithPageSize(64 * 1024)
.WithMaxMemory(4 * 1024 * 1024)
.WithIndexBTreeOrder(64)
.WithEndianness(Endianness.LittleEndian)
.AutoLoad();
return (FileObjectSpace)SampleSchema.Configure(builder).Build();
Build() returns the base ObjectSpace type. Commit and Rollback are members of FileObjectSpace, which is why the factory casts its result. WithPageSize sets the size of a transaction page, and the staging folder defaults to .txn beside the file when KeepUncommittedPagesAt is omitted.
4. Stage changes across several dimensions
The sample groups two writes into one method. It updates an existing object in one dimension and inserts a new object in another.
// Stages changes in both dimensions. The caller owns commit or rollback.
private static void StageChanges(ObjectSpace space, long purchaseId, string reference, long customerId, decimal total, int region) {
var customer = space.Get((Customer item) => item.Id, customerId);
customer.Region = region;
space.Save(customer);
space.Save(new Purchase { Id = purchaseId, Reference = reference, CustomerId = customerId, Total = total });
}
The method does not commit. It leaves that decision to its caller, which is the usual division of work. A method stages its writes, and the code that knows whether the whole operation succeeded ends the transaction. The try block at the start of this article is such a caller.
5. Reacquire objects after rollback
Rollback() unloads the space and loads it again from the committed file. Every object fetched before the rollback is now detached from the store, including objects that the failed operation never touched.
// Rollback replaces every tracked instance. Fetch objects again before using them.
var restoredCustomer = space.Get((Customer item) => item.Id, 1L);
SampleChecks.Require(!ReferenceEquals(oldCustomer, restoredCustomer), "Rollback kept the old instance.");
SampleChecks.Require(restoredCustomer.Region == 7, "Rollback left the updated object.");
restoredCustomer is a new instance that holds the committed values. Treat a rollback as the end of every reference, query result and UI row you obtained from the space, and fetch them again before continuing.
6. Commit and reopen
A successful operation ends with Commit().
StageChanges(space, 104, "PO-104", 1, 60m, 9);
space.Commit();
The commit flushes the loaded dimensions and publishes the pending pages. Data is durable from this point, and a store opened later on the same file reads it back. The inserts that were rolled back earlier are absent from the reopened file.
7. React to transaction events
FileObjectSpace raises four events around the transaction, which lets other components refresh or release state without being called explicitly.
| Event | Raised |
Committing |
Before pending pages are published. The space flushes here. |
Committed |
After the commit has completed. |
RollingBack |
Before pending pages are discarded. The space unloads here. |
RolledBack |
After the rollback, once the space has reloaded. |
Diagram source
---
config:
sequence:
actorMargin: 8
width: 96
mirrorActors: false
messageMargin: 28
noteMargin: 8
---
sequenceDiagram
participant A as Application
participant S as FileObjectSpace
participant T as Transactional<br/>stream
A->>S: Commit()
S->>T: Commit()
T-->>S: Committing
Note over S: Flush()
Note over T: publish pages
T-->>S: Committed
A->>S: Rollback()
S->>T: Rollback()
T-->>S: RollingBack
Note over S: unload
Note over T: discard pages
T-->>S: RolledBack
Note over S: reload
The handlers take no arguments. The sample counts two of the events.
space.RolledBack += () => rollbacks++;
space.Committed += () => commits++;
A cache or a view model that holds objects from the space can subscribe to RolledBack and reload them.
8. Limits to plan for
-
Dispose does not commit. End every unit of work with an explicit
Commit()orRollback(). -
Synchronous only.
CommitAsync()andRollbackAsync()throwNotSupportedException. -
One thread. Create, use and dispose the space on the same thread, and do not hold an access scope across an
await. -
One store. The transaction covers this file. It cannot undo a message that was sent or a change made in another system.
-
Memory spaces. A
MemoryObjectSpacehas no transaction. It flushes into its stream and has noCommitorRollback.
9. Get the package and run the sample
Use .NET 10 and the Sphere10.Framework NuGet package.
dotnet new console --framework net10.0 --name ArticleSample
cd ArticleSample
dotnet add package Sphere10.Framework --version 3.1.3
The complete MIT-licensed sample contains the project and every file shown here. Extract the download over the generated project and run it against a new directory.
dotnet run -c Release -- ./fresh-transaction-demo
The sample commits an initial set of objects, rolls back one multi-dimension operation, commits a second one and reopens the file to confirm which changes survived.
Rollback discarded the staged update and insert: passed
Tracked object reacquired after rollback: passed
Committed reopen: region 9, 3 purchases
Committed and RolledBack events observed: passed
Commit and rollback checks: passed
The next article shows how a store can describe its own layout . The ObjectSpace guide documents durability, scopes and ownership in full.
The Sphere10 Framework overview describes the rest of the toolkit.
Author: Herman Schoenfeld