UnitofWork<T> — Deep Dive

UnitofWork<T> is the primary transactional data-access component in BeepDM. It wraps an ObservableBindingList<T> (OBL) and a backing IDataSource to provide change-tracking, events, validation, undo/redo, export, and commit/rollback — all in a single, generic unit pattern.

It works in two modes:

  • Connected mode — backed by a live IDataSource (SQL, file, REST, DataView, …). Changes are persisted on Commit().
  • List mode — works purely in-memory on a pre-loaded ObservableBindingList<T>. No datasource I/O.
14 Lifecycle Events
Pre/Post hooks for Create, Insert, Update, Delete, Query, Commit, Edit, and Revert. Every hook supports Cancel.
Undo / Redo
Granular per-action undo/redo stack via OBL. Configurable depth, enable/disable at runtime.
Validation
Data Annotations + EntityStructure-based custom rules. Can block commit when errors exist.
Commit Order
FK-safe ordered commit: DeletesFirst, InsertsFirst, or UpdatesFirst.
Export / Import
DataTable, JSON stream, CSV stream — async, streaming, with optional header row.
Soft Delete & Concurrency
Configurable soft-delete field; optimistic concurrency with LastWriteWins or ThrowOnConflict.

Architecture

UnitofWork<T> is split across multiple partial classes. Each partial owns a clearly scoped concern:

UnitofWork<T>  (partial classes)
│
├── UnitofWork.Core.cs           — properties, constructors, Units/FilteredUnits
├── UnitofWork.Core.Extensions.cs — events declarations, Commit, Rollback, AddRange, Delete(doc)
├── UnitofWork.Core.Utilities.cs — Clear, IsDirty, DocExist, Getindex, ItemPropertyChangedHandler
├── UnitofWork.CRUD.cs           — Get, Get(filters), GetQuery, New, Add, Update, Delete(predicate)
├── UnitofWork.OBLIntegration.cs — passthrough to OBL: Validation, Undo, Computed, Bookmarks,
│                                   ThreadSafe, Freeze, BatchUpdate, Aggregates, Navigation
│
├── Helpers/
│   ├── UnitofWorkEventHelper    — CreateEventParams, RaiseEvent
│   ├── UnitofWorkValidationHelper — ValidateEntity, ValidateForInsert/Update/Delete
│   ├── UnitofWorkStateHelper    — GetEntityState, SetEntityState, MarkFor*
│   ├── UnitofWorkDataHelper     — InsertDoc, UpdateDoc, GetDataInUnits
│   ├── UnitofWorkDefaultsHelper — ApplyInsertDefaults, ApplyUpdateDefaults
│   ├── UnitofWorkCollectionHelper — FilterCollection, pagination helpers
│   ├── UnitofWorkExportHelper   — ToDataTable, ToJsonAsync, ToCsvAsync, LoadFrom*
│   ├── UnitofWorkQueryHistory   — RecordQuery, GetQueryHistory
│   └── UnitofWorkStateHelper    — entity state management
│
├── Models/
│   ├── ChangeSummary            — inserted/updated/deleted counts + per-field summary
│   └── QueryHistoryEntry        — timestamp, filter params, row count
│
└── UnitOfWorkFactory            — runtime-type factory → IUnitOfWorkWrapper

Interfaces implemented

InterfaceAdds
IUnitofWork<T>Core contract: CRUD, Commit, Rollback, Units, events
INotifyPropertyChangedWinForms / WPF data binding
IRevertableRevertItem, RevertItemAsync
IBatchCommittableCommitBatchAsync with progress
IExportableToDataTable, ToJsonAsync, ToCsvAsync
IImportableLoadFromJsonAsync, LoadFromCsvAsync
IAggregatableSum, Average, Count
IUndoableUndo, Redo, CanUndo, CanRedo
IMergeableRefreshAsync with ConflictMode
IUnitofWorkHistoryGetChangeSummary, GetQueryHistory

Constructors

ConstructorModeUse when
UnitofWork() Designer WinForms designer surface. Properties must be set manually before use.
UnitofWork(dMEEditor, datasourceName, entityName) Connected Typical server-backed usage. Opens the datasource and initialises entity structure automatically.
UnitofWork(dMEEditor, datasourceName, entityName, entityStructure) Connected Same as above but supplies a pre-built EntityStructure (avoids a live schema query).
UnitofWork(dMEEditor, isInListMode, ts) List Wraps an existing ObservableBindingList<T> for in-memory operations only.
Primary key (PrimaryKey / GuidKey): If not supplied to the constructor you can set PrimaryKey = "ID" after construction. The helper classes discover it via reflection from the EntityStructure when the datasource is opened.
// Connected — schema detected from datasource
using var uow = new UnitofWork<Customer>(dme, "SalesDB", "Customers");

// Connected — schema supplied (faster, avoids round-trip)
using var uow = new UnitofWork<Customer>(dme, "SalesDB", "Customers", entityStructure);

// List mode — in-memory only
var list = new ObservableBindingList<Customer>(existingCustomers);
using var uow = new UnitofWork<Customer>(dme, true, list);

Core Properties

Identity & data access

PropertyTypeDescription
DMEEditorIDMEEditorRead-only. Set by constructor.
DataSourceIDataSourceResolved backing datasource. null in list mode.
DatasourceNamestringRegistered datasource name.
EntityNamestringEntity / table name.
EntityTypeTypetypeof(T).
EntityStructureEntityStructureSchema metadata (fields, keys, relations).
PrimaryKeystringPrimary key field name.
GuidKeystringOptional GUID field name for secondary identity.
IsIdentityboolWhen true, the DB generates the PK value (no client-side sequencing).
SequencerstringName of a server-side sequence to call for PK generation.

State & collection

PropertyTypeDescription
UnitsObservableBindingList<T>The active collection. Returns FilteredUnits when a filter is applied.
FilteredUnitsObservableBindingList<T>Filter result set. Manages event subscriptions on set.
CurrentItemTCurrently navigated item (Units.Current).
TotalItemCountintUnits.Count.
IsDirtybooltrue when any insert/update/delete is pending.
IsInListModeboolDisables datasource I/O; use with in-memory collections.
IsFilterOnboolInternal flag — true while a filter is active.
DeletedUnitsList<T>Items removed from the collection awaiting physical delete.
InsertedKeysDictionary<int,string>Auxiliary insert tracking.
UpdatedKeysDictionary<int,string>Auxiliary update tracking.
DeletedKeysDictionary<int,string>Auxiliary delete tracking.
FilterExpressionstringFree-form filter string for in-memory LINQ filtering.

Paging

PropertyTypeDefault
PageIndexint0
PageSizeint10

Soft delete

PropertyTypeDescription
SoftDeleteFieldNamestringWhen set (e.g. "IsDeleted"), Delete() sets this field instead of physically removing the record.
IncludeDeletedboolWhen true, Get() includes soft-deleted rows. Default false.

Optimistic concurrency

PropertyTypeDescription
ConcurrencyModeConcurrencyModeNone (default), LastWriteWins, ThrowOnConflict.
ConcurrencyFieldNamestringField to compare for staleness (e.g. "RowVersion" or "LastModified").

Commit control

PropertyTypeDescription
CommitOrderCommitOrderOrder entities are persisted during Commit(). DeletesFirst (default, FK-safe), InsertsFirst, UpdatesFirst.
BlockCommitOnValidationErrorboolWhen true (default), items with Error-severity validation failures are skipped during commit.
IsAutoValidateEnabledboolValidates each item on every property change when true.

Entity States

State is tracked automatically by ObservableBindingList<T>. The UnitofWorkStateHelper provides a facade over OBL's internal GetTrackingItem(entity).

StateWhen set
EntityState.UnchangedFreshly loaded or after AcceptChanges().
EntityState.AddedAfter New() or Add(entity).
EntityState.ModifiedAfter any property change on a tracked item.
EntityState.DeletedAfter Delete(entity).
// Read the state of a specific item
var state = uow._stateHelper.GetEntityState(customer);

// Force-mark as modified (useful when bypassing property change notification)
uow._stateHelper.MarkAsModified(customer);

// Accept all changes (clears dirty flag, resets all states to Unchanged)
uow.Units.AcceptChanges();

Revert a single item

// Revert one item to its original values (before the last AcceptChanges)
bool reverted = uow.RevertItem(customer);

// Async overload
bool reverted = await uow.RevertItemAsync(customer, cts.Token);

CRUD Operations

Create

MethodSignatureNotes
New() void New() Creates a new T(), applies DefaultsManager insert defaults, generates PK sequence if needed, adds to Units. Raises PreCreate → PostCreate.
Add(entity) void Add(T entity) Same as New() but accepts a pre-populated entity. Applies defaults. Raises PreCreate → PostCreate.
AddRange(entities) Task<IErrorsInfo> AddRange(IEnumerable<T>) Suppresses notifications during bulk insert for performance.

Read

MethodSignatureNotes
Get() Task<ObservableBindingList<T>> Get() Loads all rows from the datasource. Raises PreQuery → PostQuery. Cancellable.
Get(filters) Task<ObservableBindingList<T>> Get(List<AppFilter>) Loads rows matching the filter list. In list mode, applies FilterCollection in-memory.
GetQuery(query) Task<ObservableBindingList<T>> GetQuery(string query) Passes a free-form SQL or query string to the datasource.
Get(int key) T Get(int key) Returns the item at index key in the collection.
Get(string pkid) T Get(string pkid) Finds the item whose PrimaryKey field equals pkid. Auto-loads from datasource if collection is empty.
Read(string id) T Read(string id) Alias for index-based read.
Read(predicate) T Read(Func<T,bool>) Units.FirstOrDefault(predicate).
MultiRead(predicate) Task<ObservableBindingList<T>> MultiRead(Func<T,bool>) Returns all matching items as a new list.

Update

MethodSignatureNotes
Update(entity) IErrorsInfo Update(T entity) Applies update defaults, locates the item by PK, replaces it in Units. Raises PreUpdate → PostUpdate.
UpdateRange(entities) Task<IErrorsInfo> UpdateRange(IEnumerable<T>) Iterates and calls Update on each. Returns aggregate result.

Delete

MethodSignatureNotes
Delete(entity) IErrorsInfo Delete(T entity) Finds entity by reference/equality. Raises PreDelete → removes → PostDelete.
Delete(predicate) ErrorsInfo Delete(Func<T,bool>) First match. Raises PreDelete → PostDelete.
Delete() IErrorsInfo Delete() Deletes CurrentItem.
DeleteAsync(doc) Task<IErrorsInfo> DeleteAsync(T doc) Async path — calls datasource DeleteEntity directly.
DeleteRange(entities) Task<IErrorsInfo> DeleteRange(IEnumerable<T>) Materialises to list first to avoid collection-modified exception.
Soft Delete: When SoftDeleteFieldName is set, Delete() sets the specified field to true / current timestamp instead of removing the record. Records with this field set are hidden from Get() unless IncludeDeleted = true.

Events Reference

All events use EventHandler<UnitofWorkParams>. The UnitofWorkParams argument carries the entity, a cancellation flag, and a human-readable message. Setting args.Cancel = true in any Pre- handler aborts the operation.

UnitofWorkParams

PropertyTypeDescription
CancelboolSet to true to abort the current operation. Only honoured by Pre-events.
EventActionEventActionIdentifies which lifecycle step is firing.
RecordobjectThe entity involved. Cast to T when needed.
EntityNamestringName of the entity type.
PropertyNamestringSet for PostEdit events — name of the changed property.
PropertyValuestringSet for PostEdit events — new value as string.
MessegestringOptional cancellation or info message.

EventAction enum values

ValueDescription
PreCreateBefore a new entity is added to Units.
PostCreateAfter a new entity has been added to Units.
PreInsertBefore a new entity is written to the datasource on commit.
PostInsertAfter a new entity has been successfully written to the datasource.
PreUpdateBefore an entity update is applied.
PostUpdateAfter an entity update has been applied.
PreDeleteBefore an entity is removed from the collection / datasource.
PostDeleteAfter an entity has been removed.
PreQueryBefore Get() fetches data.
PostQueryAfter Get() has loaded data into Units.
PreCommitBefore the transaction begins.
PostCommitAfter all changes have been successfully written and AcceptChanges() called.
PostEditAfter a property change on any tracked item. Carries PropertyName and PropertyValue.

Event declarations

public event EventHandler<UnitofWorkParams> PreCreate;
public event EventHandler<UnitofWorkParams> PostCreate;
public event EventHandler<UnitofWorkParams> PreInsert;
public event EventHandler<UnitofWorkParams> PostInsert;
public event EventHandler<UnitofWorkParams> PreUpdate;
public event EventHandler<UnitofWorkParams> PostUpdate;
public event EventHandler<UnitofWorkParams> PreDelete;
public event EventHandler<UnitofWorkParams> PostDelete;
public event EventHandler<UnitofWorkParams> PreQuery;
public event EventHandler<UnitofWorkParams> PostQuery;
public event EventHandler<UnitofWorkParams> PreCommit;
public event EventHandler<UnitofWorkParams> PostCommit;
public event EventHandler<UnitofWorkParams> PostEdit;
public event EventHandler<UnitofWorkParams> OnItemReverted;

Subscribing to events

using var uow = new UnitofWork<Customer>(dme, "SalesDB", "Customers");

// ── Pre-events: inspect the record and optionally cancel ────────────────────

uow.PreCreate += (sender, args) =>
{
    var c = (Customer)args.Record;
    // Validate before the entity is added to Units
    if (string.IsNullOrWhiteSpace(c?.Name))
    {
        args.Cancel = true;
        args.Messege = "Customer name is required.";
    }
};

uow.PreInsert += (sender, args) =>
{
    var c = (Customer)args.Record;
    Console.WriteLine($"About to insert {c.Name} into datasource…");
    // Set args.Cancel = true to abort the INSERT to the datasource
};

uow.PreUpdate += (sender, args) =>
{
    var c = (Customer)args.Record;
    // Example: block updates for archived customers
    if (c.Status == "ARCHIVED")
    {
        args.Cancel = true;
        args.Messege = "Archived records cannot be modified.";
    }
};

uow.PreDelete += (sender, args) =>
{
    var c = (Customer)args.Record;
    Console.WriteLine($"Pre-Delete: {c.Name} (ID={c.ID})");
    // Example: require confirmation for premium customers
};

uow.PreQuery += (sender, args) =>
{
    Console.WriteLine("Loading data…");
    // args.Cancel = true would suppress the datasource round-trip
};

uow.PreCommit += (sender, args) =>
{
    // Final validation gate before any DB write
    if (!IsNetworkAvailable())
    {
        args.Cancel = true;
        args.Messege = "No database connection.";
    }
};

// ── Post-events: receive completed results ──────────────────────────────────

uow.PostCreate += (sender, args) =>
{
    var c = (Customer)args.Record;
    Console.WriteLine($"Entity created in memory: {c.Name}");
};

uow.PostInsert += (sender, args) =>
{
    var c = (Customer)args.Record;
    Console.WriteLine($"Inserted to DB: {c.Name}, new PK = {c.ID}");
};

uow.PostUpdate += (sender, args) =>
{
    var c = (Customer)args.Record;
    AuditLog.Record("UPDATE", c);
};

uow.PostDelete += (sender, args) =>
{
    var c = (Customer)args.Record;
    Console.WriteLine($"Deleted: {c.Name}");
};

uow.PostQuery += (sender, args) =>
{
    // sender is the Units collection after load
    Console.WriteLine($"Loaded {uow.Units.Count} rows.");
};

uow.PostCommit += (sender, args) =>
{
    Console.WriteLine("All changes committed — refreshing UI.");
    RefreshGrid();
};

// ── Property change ─────────────────────────────────────────────────────────
uow.PostEdit += (sender, args) =>
{
    Console.WriteLine($"Field '{args.PropertyName}' changed to '{args.PropertyValue}'");
    // Useful for live audit trails or conditional UI updates
};

// ── Revert notification ─────────────────────────────────────────────────────
uow.OnItemReverted += (sender, args) =>
{
    var c = (Customer)args.Record;
    Console.WriteLine($"Item reverted: {c.Name}");
};

Commit & Rollback

MethodSignatureDescription
Commit() Task<IErrorsInfo> Commit() Persists all pending changes: raises PreCommit, wraps datasource in a transaction, calls CommitAllAsync via OBL (insert→update→delete ordered by CommitOrder), calls PostCommit on success.
Commit(progress, token) Task<IErrorsInfo> Commit(IProgress<PassedArgs>, CancellationToken) Same with progress reporting and cancellation support.
CommitBatchAsync Task<CommitBatchResult> CommitBatchAsync(batchSize, progress, ct) Commits in fixed-size batches. Useful for very large collections to avoid single-transaction timeouts.
Rollback() Task<IErrorsInfo> Rollback() Calls DataSource.EndTransaction() then Units.RejectChanges(). Restores added/deleted items and original property values.

Commit internals

During Commit(), OBL's CommitAllAsync is called with three async lambdas (insert, update, delete). The _defaultsHelper applies audit defaults just before each DB call:

// Simplified internal Commit flow:
// 1. Raise PreCommit — abort if Cancel = true
// 2. DataSource.BeginTransaction()
// 3. OBL.CommitAllAsync(
//        insertAsync: item => { ApplyInsertDefaults(item); InsertDoc(item); },
//        updateAsync: item => { ApplyUpdateDefaults(item); UpdateDoc(item); },
//        deleteAsync: item => { DataSource.DeleteEntity(entityName, item); },
//        CommitOrder)
// 4. If AllSucceeded → DataSource.Commit() → AcceptChanges per item
//    Else            → DataSource.EndTransaction() (rollback)
// 5. Clear InsertedKeys/UpdatedKeys/DeletedKeys
// 6. Raise PostCommit
// Typical commit with cancellable progress
var cts = new CancellationTokenSource();
var progress = new Progress<PassedArgs>(p =>
    Console.WriteLine($"Progress: {p.ParameterString1}"));

var result = await uow.Commit(progress, cts.Token);
if (result.Flag != Errors.Ok)
{
    Console.WriteLine("Commit failed: " + result.Message);
    await uow.Rollback();
}

Validation

Validation is powered by OBL's built-in validation framework, which combines Data Annotations ([Required], [MaxLength], …) with EntityStructure-based rules described in the schema metadata.

Method / PropertyDescription
IsAutoValidateEnabled When true, validation runs after every property change. Default: depends on OBL init.
BlockCommitOnValidationError When true (default), items with ValidationSeverity.Error failures are skipped during commit.
ValidateItem(item) Validates a single item. Returns a ValidationResult with IsValid, Errors, Warnings.
ValidateAll() Validates every item in Units. Returns an aggregate ValidationResult.
GetErrors(item) Returns List<ValidationError> for a specific item.
GetInvalidItems() Returns all items that currently have at least one validation error.
// Enable auto-validate so grid cells show errors live
uow.IsAutoValidateEnabled = true;

// Manual validation before commit
var result = uow.ValidateAll();
if (!result.IsValid)
{
    foreach (var error in result.Errors)
        Console.WriteLine($"{error.FieldName}: {error.Message}");
    return;
}

// Get errors for a specific item
var errors = uow.GetErrors(customer);
foreach (var e in errors)
    Console.WriteLine($"[{e.Severity}] {e.FieldName}: {e.Message}");

// Find all broken items
var invalid = uow.GetInvalidItems();
Console.WriteLine($"{invalid.Count} items have validation errors");

Undo / Redo

Undo/Redo records every property change, Add, and Remove on the collection. It is off by default; enable it after construction.

MemberDescription
IsUndoEnabledEnable/disable the undo stack.
MaxUndoDepthMaximum number of actions to retain. Default 100. Older entries are evicted.
CanUndotrue when the undo stack is non-empty.
CanRedotrue when the redo stack is non-empty.
Undo()Reverts the last recorded action. Returns true if successful.
Redo()Re-applies the last undone action. Returns true if successful.
ClearUndoHistory()Empties both undo and redo stacks.
EnableUndo(bool, int maxDepth)Convenience: sets IsUndoEnabled and MaxUndoDepth in one call.
// Turn on undo at startup
uow.IsUndoEnabled = true;
uow.MaxUndoDepth  = 50;

// Or via the convenience method
uow.EnableUndo(true, maxDepth: 50);

// Edit + undo
uow.CurrentItem.Name = "Wrong Name";
Console.WriteLine($"CanUndo: {uow.CanUndo}"); // true
uow.Undo();
Console.WriteLine($"Name restored: {uow.CurrentItem.Name}");

// Redo the change
uow.Redo();

// Clear history (e.g. after a commit)
uow.ClearUndoHistory();

Computed Columns

Computed columns are evaluated on-demand per item. They are not persisted. Useful for display-only values derived from other fields.

MethodDescription
RegisterComputed(name, func)Registers a Func<T, object> under a display name.
UnregisterComputed(name)Removes a computed column definition.
GetComputed(item, name)Evaluates and returns the computed value for one item.
GetAllComputed(item)Returns Dictionary<string, object> of all computed values for one item.
ComputedColumnNamesRead-only collection of registered computed column names.
// Register a computed full-name column
uow.RegisterComputed("FullLabel", c => $"{c.Name} ({c.Email})");

// Evaluate for a specific item
var label = (string)uow.GetComputed(customer, "FullLabel");

// Get all computed values in one call (for grid display)
foreach (var item in uow.Units)
{
    var computed = uow.GetAllComputed(item);
    Console.WriteLine(computed["FullLabel"]);
}

Bookmarks

Bookmarks save a named cursor position in the collection so you can return quickly (similar to classic desktop record navigation).

// Save position before a search
uow.SetBookmark("SearchStart");

// Navigate through results…
uow.GoToBookmark("SearchStart"); // jumps back

// Remove when done
uow.RemoveBookmark("SearchStart");

// Clear all
uow.ClearBookmarks();

Thread Safety, Freeze & Batch Update

FeatureAPIDescription
Thread-safe mode IsThreadSafe = true All OBL mutations are wrapped in a ReaderWriterLockSlim. Safe for concurrent read from UI thread and write from background thread.
Freeze Freeze() / Unfreeze(), IsFrozen Makes the collection read-only. Mutations throw InvalidOperationException. Useful during UI pagination rendering.
Batch Update BeginBatchUpdate() Returns an IDisposable. While open, CollectionChanged notifications are suppressed. On dispose, a single reset notification is fired. Supports nested scopes.
// Batch insert 10 000 rows without 10 000 individual UI refreshes
using (uow.BeginBatchUpdate())
{
    foreach (var row in largeDataSet)
        uow.Add(row);
} // CollectionChanged fires once here

// Thread-safe cross-thread scenario
uow.IsThreadSafe = true;
Task.Run(() =>
{
    uow.Add(new Customer { Name = "Background Insert" });
    // Safe — writer lock acquired internally
});

Export / Import

Provided by UnitofWorkExportHelper<T>, exposed as interface methods.

MethodSignatureDescription
ToDataTable() DataTable ToDataTable() Materialises the current collection into a DataTable with one column per property. Uses reflection; columns are typed correctly (unwraps Nullable<T>).
ToJsonAsync Task ToJsonAsync(Stream, CancellationToken) Serialises the collection to a JSON array using System.Text.Json.
ToCsvAsync Task ToCsvAsync(Stream, char delimiter, CancellationToken) Writes a header row + data rows to the stream. Values are CSV-escaped. Default delimiter is comma.
LoadFromJsonAsync Task<int> LoadFromJsonAsync(Stream, bool clearFirst, CancellationToken) Deserialises a JSON array into Units. Returns count of records loaded.
LoadFromCsvAsync Task<int> LoadFromCsvAsync(Stream, char delimiter, bool clearFirst, bool hasHeaderRow, CancellationToken) Parses a CSV stream into Units. Maps columns by header name (case-insensitive). Returns count loaded.
// Export to file
using var stream = File.Create("customers.json");
await uow.ToJsonAsync(stream, cts.Token);

// Export to CSV
using var csvStream = File.Create("customers.csv");
await uow.ToCsvAsync(csvStream, delimiter: ',', ct: cts.Token);

// Export to DataTable (e.g. for a WinForms grid)
DataTable dt = uow.ToDataTable();
dataGridView1.DataSource = dt;

// Import from JSON (clears existing data first)
using var jsonIn = File.OpenRead("new_customers.json");
int loaded = await uow.LoadFromJsonAsync(jsonIn, clearFirst: true);
Console.WriteLine($"Imported {loaded} customers");

// Import from CSV (append to existing)
using var csvIn = File.OpenRead("additional.csv");
int added = await uow.LoadFromCsvAsync(csvIn, delimiter: ',', clearFirst: false, hasHeaderRow: true);
Console.WriteLine($"Appended {added} rows from CSV");

Aggregates

MethodSignatureDescription
Sum(fieldName) decimal Sum(string numericFieldName) Sums the values of the named numeric field across all items in Units.
Average(fieldName) decimal Average(string numericFieldName) Arithmetic mean of the named field. Returns 0 if Units is empty.
Count(predicate?) int Count(Func<object,bool> predicate = null) Total item count when predicate is null, otherwise filtered count.
decimal total   = uow.Sum("OrderAmount");
decimal average = uow.Average("OrderAmount");
int     active  = uow.Count(item => ((Order)item).IsActive);

Console.WriteLine($"Total: {total:C}  Average: {average:C}  Active: {active}");

Batch Operations

MethodSignatureDescription
AddRangeTask<IErrorsInfo> AddRange(IEnumerable<T>)Inserts multiple entities. Notifications suppressed for performance; single refresh fired at end.
UpdateRangeTask<IErrorsInfo> UpdateRange(IEnumerable<T>)Updates each entity using the standard Update path (includes defaults, events).
DeleteRangeTask<IErrorsInfo> DeleteRange(IEnumerable<T>)Deletes each entity. List materialised first to avoid modification-during-enumeration.
CommitBatchAsyncTask<CommitBatchResult> CommitBatchAsync(batchSize, progress, ct)Commits in fixed-size chunks. Default batch size 200.
RefreshAsync(filters, conflictMode, ct)Task<bool> RefreshAsync(...)Reloads data from server and merges with local changes. ConflictMode.ServerWins discards local, MergePreferLocal keeps local edits.
// Efficient bulk insert
var newOrders = GenerateOrders(5000);
await uow.AddRange(newOrders);

// Batch commit (200 items per transaction)
var batchResult = await uow.CommitBatchAsync(
    batchSize: 200,
    progress: new Progress<CommitBatchProgress>(p =>
        Console.WriteLine($"Batch {p.BatchNumber}: {p.ItemsCommitted}/{p.TotalItems}")),
    ct: cts.Token);

Console.WriteLine($"All succeeded: {batchResult.AllSucceeded}");

// Server refresh — discard local changes, use server data
bool refreshed = await uow.RefreshAsync(
    filters: null,
    conflictMode: ConflictMode.ServerWins,
    ct: cts.Token);

Factory & Wrapper

Use the UnitOfWorkFactory when the entity type is only known at runtime (e.g. plugin-loaded entities or reflection-driven UI). For compile-time known types, use the generic constructors directly.

MethodReturnsDescription
CreateUnitOfWork(Type, dme, ds, entity, pk?) IUnitOfWorkWrapper Primary factory. Throws on failure.
CreateUnitOfWork(Type, dme, ds, entity, entityStructure, pk?) IUnitOfWorkWrapper Overload with pre-built EntityStructure.
TryCreateUnitOfWork(Type, dme, ds, entity, out wrapper, out error, pk?) bool Non-throwing version.
CreateWrapper(object uow) IUnitOfWorkWrapper Wraps an existing UnitofWork instance in a dynamic runtime wrapper.
CreateUnitOfWorkWrapper(typeName, dme, ds, entity, pk?) IUnitOfWorkWrapper Legacy string-based type name resolution.
// Runtime type (e.g. discovered via plugin)
Type entityType = pluginAssembly.GetType("Acme.Products.Order");

if (UnitOfWorkFactory.TryCreateUnitOfWork(
        entityType, dme, "OrdersDB", "Orders",
        out var wrapper, out string error))
{
    wrapper.Get().GetAwaiter().GetResult();
    Console.WriteLine($"Loaded {wrapper.Units.Count} orders");
}
else
{
    Console.WriteLine("Could not create UOW: " + error);
}

UnitOfWorkWrapper

UnitOfWorkWrapper wraps a dynamic UnitofWork instance and provides safe property access with fallback defaults (GetPropertySafely). It implements IUnitOfWorkWrapper which exposes all core operations through a non-generic contract — suitable for framework-level code that must work with heterogeneous entity types.

Advanced Features

DefaultsManager integration

When DefaultsManager is initialised, _defaultsHelper.ApplyInsertDefaults(entity) is called in New() and Add(), and _defaultsHelper.ApplyUpdateDefaults(entity) is called in Update() and inside the Commit insert/update lambdas. This automatically populates audit fields like CreatedDate, CreatedBy, ModifiedDate, etc.

// Configure once at app startup
DefaultsManager.Initialize(dme);
DefaultsManager.SetColumnDefault(dme, "SalesDB", "Customers", "CreatedDate",  "NOW",      isRule: true);
DefaultsManager.SetColumnDefault(dme, "SalesDB", "Customers", "CreatedBy",    "USERNAME", isRule: true);
DefaultsManager.SetColumnDefault(dme, "SalesDB", "Customers", "ModifiedDate", "NOW",      isRule: true);
DefaultsManager.SetColumnDefault(dme, "SalesDB", "Customers", "IsActive",     "true",     isRule: false);

// Now New()/Add()/Update() automatically fill these fields
uow.New();
var c = uow.CurrentItem;
c.Name = "Alice";
// c.CreatedDate → DateTime.UtcNow, c.CreatedBy → Window user, c.IsActive → true

Change summary & query history

// Get a snapshot of all pending changes
ChangeSummary summary = uow.GetChangeSummary();
Console.WriteLine($"Inserts: {summary.InsertedCount}");
Console.WriteLine($"Updates: {summary.UpdatedCount}");
Console.WriteLine($"Deletes: {summary.DeletedCount}");

// Query history (audit trail of all Get() calls)
var history = uow.GetQueryHistory();
foreach (var entry in history)
    Console.WriteLine($"{entry.Timestamp:u} — {entry.RowCount} rows — filters: {entry.FilterSummary}");

// Clear history
uow.ClearQueryHistory();

Clone an item

// Shallow clone
var copy = (Customer)uow.CloneItem(customer, deep: false);
copy.Name = "Copy of " + customer.Name;
uow.Add(copy);

Soft delete workflow

uow.SoftDeleteFieldName = "IsDeleted";  // marks the record instead of removing it
uow.IncludeDeleted      = false;        // hide soft-deleted records in Get()

uow.Delete(customer);      // sets customer.IsDeleted = true, does NOT remove from collection

// To see all records including deleted:
uow.IncludeDeleted = true;
await uow.Get();

Optimistic concurrency

uow.ConcurrencyMode      = ConcurrencyMode.ThrowOnConflict;
uow.ConcurrencyFieldName = "RowVersion";

// If the server row has been modified since the item was loaded,
// Commit() will throw / return Errors.Failed with a concurrency violation message.

try
{
    var result = await uow.Commit();
    if (result.Flag != Errors.Ok)
        Console.WriteLine("Concurrency conflict: " + result.Message);
}
catch (Exception ex)
{
    Console.WriteLine("Concurrency: " + ex.Message);
    await uow.Rollback();
}

Examples

Example 1 — Basic CRUD with events

using var uow = new UnitofWork<Customer>(dme, "SalesDB", "Customers");

// Hook all lifecycle events
uow.PreCreate  += (_, args) => Console.WriteLine("Creating…");
uow.PostCreate += (_, args) => Console.WriteLine($"Created: {((Customer)args.Record).Name}");
uow.PreInsert  += (_, args) => Console.WriteLine("Writing to DB…");
uow.PostInsert += (_, args) => Console.WriteLine($"Inserted. New PK={((Customer)args.Record).ID}");
uow.PreUpdate  += (_, args) => { /* validate */ };
uow.PostUpdate += (_, args) => AuditLog.Record("UPDATE", args.Record);
uow.PreDelete  += (_, args) =>
{
    if (((Customer)args.Record).IsVIP)
    { args.Cancel = true; args.Messege = "VIP customers cannot be deleted."; }
};
uow.PostDelete += (_, args) => Console.WriteLine("Deleted.");
uow.PostEdit   += (_, args) => Console.WriteLine($"Field '{args.PropertyName}' → '{args.PropertyValue}'");
uow.PreCommit  += (_, args) => Console.WriteLine("Beginning transaction…");
uow.PostCommit += (_, args) => Console.WriteLine("All changes saved.");

// Load
await uow.Get();

// Create
uow.New();
uow.CurrentItem.Name  = "Alice";
uow.CurrentItem.Email = "alice@example.com";

// Update first loaded record
if (uow.Units.Count > 1)
{
    uow.Units[1].Name = "Updated Name";
    uow.Update(uow.Units[1]);
}

// Delete
uow.Delete(uow.Units.FirstOrDefault(c => c.Status == "INACTIVE"));

Console.WriteLine($"Pending changes: {uow.IsDirty}");
var result = await uow.Commit();
Console.WriteLine($"Commit: {result.Flag} — {result.Message}");

Example 2 — Cancelling an operation from a Pre-event

using var uow = new UnitofWork<Order>(dme, "OrdersDB", "Orders");

// Block deletes of confirmed orders
uow.PreDelete += (sender, args) =>
{
    var order = (Order)args.Record;
    if (order.Status == "CONFIRMED")
    {
        args.Cancel  = true;
        args.Messege = $"Order #{order.ID} is confirmed and cannot be deleted.";
    }
};

var confirmed = uow.Units.FirstOrDefault(o => o.Status == "CONFIRMED");
var result    = uow.Delete(confirmed);

if (result.Flag != Errors.Ok)
    MessageBox.Show(result.Message); // "Order #42 is confirmed and cannot be deleted."

Example 3 — Undo / Redo in a WinForms form

public class OrderForm : Form
{
    private UnitofWork<Order> _uow;

    public OrderForm(IDMEEditor dme)
    {
        _uow = new UnitofWork<Order>(dme, "OrdersDB", "Orders");
        _uow.IsUndoEnabled = true;
        _uow.MaxUndoDepth  = 30;
        
        _uow.PostEdit += (_, _) => UpdateUndoButtons();
    }

    private void UpdateUndoButtons()
    {
        btnUndo.Enabled = _uow.CanUndo;
        btnRedo.Enabled = _uow.CanRedo;
    }

    private void btnUndo_Click(object sender, EventArgs e)
    {
        if (_uow.Undo()) UpdateUndoButtons();
    }

    private void btnRedo_Click(object sender, EventArgs e)
    {
        if (_uow.Redo()) UpdateUndoButtons();
    }

    private async void btnSave_Click(object sender, EventArgs e)
    {
        var result = await _uow.Commit();
        if (result.Flag == Errors.Ok)
        {
            _uow.ClearUndoHistory();
            UpdateUndoButtons();
        }
    }
}

Example 4 — Export to JSON and reload

using var uow = new UnitofWork<Product>(dme, "CatalogDB", "Products");
await uow.Get();

// Export snapshot
string path = Path.Combine(Path.GetTempPath(), "products_snapshot.json");
await using (var stream = File.Create(path))
    await uow.ToJsonAsync(stream, CancellationToken.None);

Console.WriteLine($"Exported {uow.Units.Count} products to {path}");

// Later — restore from snapshot
await using (var stream = File.OpenRead(path))
{
    int count = await uow.LoadFromJsonAsync(stream, clearFirst: true);
    Console.WriteLine($"Reloaded {count} products");
}

Example 5 — Large batch import with progress

using var uow = new UnitofWork<Transaction>(dme, "LedgerDB", "Transactions");

// Import from CSV
await using var file = File.OpenRead("import_10k.csv");
int loaded = await uow.LoadFromCsvAsync(file, delimiter: ',', clearFirst: true, hasHeaderRow: true);
Console.WriteLine($"Loaded {loaded} rows for import");

// Commit in batches of 500 with progress
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var progress = new Progress<CommitBatchProgress>(p =>
    Console.WriteLine($"Batch {p.BatchNumber}: {p.ItemsCommitted}/{p.TotalItems} committed"));

var batchResult = await uow.CommitBatchAsync(
    batchSize: 500,
    progress: progress,
    ct: cts.Token);

Console.WriteLine($"Import done. Succeeded: {batchResult.AllSucceeded}, Failed: {batchResult.FailedCount}");

Example 6 — Validation + BlockCommit

using var uow = new UnitofWork<Invoice>(dme, "BillingDB", "Invoices");
uow.IsAutoValidateEnabled      = true;
uow.BlockCommitOnValidationError = true;  // default

await uow.Get();

// Introduce a bad record
uow.New();
uow.CurrentItem.Amount = -500; // violates [Range(0, double.MaxValue)]
uow.CurrentItem.DueDate = DateTime.MinValue;

var allResult = uow.ValidateAll();
if (!allResult.IsValid)
{
    Console.WriteLine("Errors before commit:");
    foreach (var err in allResult.Errors)
        Console.WriteLine($"  {err.FieldName}: {err.Message}");
}

// Commit will skip the invalid record but commit all valid ones
var result = await uow.Commit();
Console.WriteLine(result.Message); // e.g. "Committed 5 of 6 items (1 skipped — validation errors)"

Full API Reference — UnitofWork<T>

Source File Map (20 files)

FileLinesContent
UnitofWork.Core.cs~600Constructors (7), properties (30), events, ChangeSummary, Export/Import, Dispose
UnitofWork.CRUD.cs~400Read/Get/New/Add/Update/Insert/Delete keys — full CRUD lifecycle
UnitofWork.Core.Extensions.cs~350Delete overloads, Commit/Rollback, AddRange/UpdateRange/DeleteRange, ChangeLog, 13 lifecycle events (Pre/Post: Create,Insert,Update,Delete,Query,Commit,Edit), RevertItem, FindAsync, FindManyAsync, CloneItem, Count, Undo/Redo
UnitofWork.Core.Utilities.cs~300Clear, GetIsDirty, DocExist, FindDocIdx, Getindex, GetItemFromCurrentList, navigation (MoveFirst/Next/Previous/Last/MoveTo), GetAddedEntities, GetModifiedEntities, GetDeletedEntities, Delete(id), GetTrackingItem, FilterCollection, GetDataInUnits
UnitofWork.OBLIntegration.cs~300Validation, Undo/Redo, Computed Columns, Bookmarks, Thread Safety (Freeze/Unfreeze/BatchUpdate), Aggregates (Sum/Average/Min/Max/CountWhere/GroupBy/DistinctValues), Navigation (BOF/EOF/IsEmpty/MoveToItem), Virtual/Lazy Loading
IUnitofWorkCapabilities.cs~807 interfaces: IRevertable, IBatchCommittable, IExportable, IImportable, IAggregatable, IUndoable, IMergeable, IUnitofWorkHistory
UnitOfWorkFactory.cs517Static factory — dynamic type creation, caching, validation, batch ops (see unitofwork-factory.html)
UnitOfWorkWrapper.cs1164Late-bound wrapper — all CRUD, events, validation, navigation (see unitofwork-wrapper.html)
UnitOfWorkWrapperExtensions.cs~200ForEachRecordAsync, CreateNewRecord, FindRecord, GetAllRecords, NavigateToRecord, GetPositionInfo, BatchInsertAsync, HasRecords, Get/SetCurrentFieldValue, RecordPositionInfo, BatchOperationResult, BatchProgress
UnitofWorkStateHelper.cs~100Get/SetEntityState, MarkForDeletion, MarkAsModified, MarkAsAdded, ResetState, ClearAllStates, GetIndicesByState, HasPendingChanges
UnitofWorkDataHelper.cs~120CloneEntity, ConvertToEntity, Get/SetEntityValues, CompareEntities (old/new diffs), GetCachedProperties, TypeExtensions (IsNullableType, IsNumericType)
UnitofWorkCollectionHelper.cs~80SynchronizeCollections, FilterCollection, ApplyPaging, SortCollection
UnitofWorkValidationHelper.cs~100ValidateEntity, ValidateForInsert, ValidateForUpdate, ValidateForDelete, ValidatePrimaryKey, ValidateRequiredFields
UnitofWorkEventHelper.cs~80CreateEventParams, CreatePropertyChangeParams, HandlePropertyChanged, RaiseEntityEvent, RaiseEvent
UnitofWorkDefaultsHelper.cs~100ApplyDefaults, ApplyDefaultsAsync, HasDefaults, GetDefaultForField, ApplyInsertDefaults, ApplyUpdateDefaults, ValidateAppliedDefaults
UnitofWorkQueryHistory.cs~30MaxSize (20), Entries (IReadOnlyList), Push, Clear
UnitofWorkExportHelper.cs~80ToDataTable, ToJsonAsync, ToCsvAsync, LoadFromJsonAsync, LoadFromCsvAsync
Models/QueryHistoryEntry.cs—ExecutedAt, Filters, RowCount, Duration, Succeeded, EntityName
Models/ChangeSummary.cs—InsertCount, UpdateCount, DeleteCount, IsDirty, TotalChanges (+ CommitBatchProgress, CommitBatchResult)
Examples/UnitofWorkExamples.cs—BasicUsage, DefaultsManagerIntegration, ListMode, FilteringAndPaging, ErrorHandling, EventHandling, Performance

All 6 Helper Interfaces (from DataManagementModelsStandard)

IUnitofWorkStateHelper<T>

public interface IUnitofWorkStateHelper<T> where T : Entity, new()
{
    EntityState GetEntityState(T entity);
    void SetEntityState(T entity, EntityState state);
    void MarkForDeletion(T entity);
    void MarkAsModified(T entity);
    void MarkAsAdded(T entity);
    void ResetState(T entity);
    void ClearAllStates();
    IEnumerable<int> GetIndicesByState(EntityState state);
    bool HasPendingChanges();
}

IUnitofWorkDataHelper<T>

public interface IUnitofWorkDataHelper<T> where T : Entity, new()
{
    T CloneEntity(T entity);
    T ConvertToEntity(object source);
    Dictionary<string, object> GetEntityValues(T entity);
    void SetEntityValues(T entity, Dictionary<string, object> values);
    Dictionary<string, (object oldValue, object newValue)> CompareEntities(T original, T current);
}

IUnitofWorkCollectionHelper<T>

public interface IUnitofWorkCollectionHelper<T> where T : Entity, new()
{
    void SynchronizeCollections(ObservableBindingList<T> source, ObservableBindingList<T> target);
    ObservableBindingList<T> FilterCollection(ObservableBindingList<T> collection, List<AppFilter> filters);
    ObservableBindingList<T> ApplyPaging(ObservableBindingList<T> collection, int pageIndex, int pageSize);
    ObservableBindingList<T> SortCollection(ObservableBindingList<T> collection, string sortField, bool ascending);
}

IUnitofWorkValidation<T>

public interface IUnitofWorkValidation<T> where T : Entity, new()
{
    IErrorsInfo ValidateEntity(T entity);
    IErrorsInfo ValidateForInsert(T entity);
    IErrorsInfo ValidateForUpdate(T entity);
    IErrorsInfo ValidateForDelete(T entity);
    IErrorsInfo ValidatePrimaryKey(T entity);
    IErrorsInfo ValidateRequiredFields(T entity);
}

IUnitofWorkEventHelper<T>

public interface IUnitofWorkEventHelper<T> where T : Entity, new()
{
    UnitofWorkParams CreateEventParams(T entity, EventAction action);
    UnitofWorkParams CreatePropertyChangeParams(T entity, string propertyName, object newValue);
    void HandlePropertyChanged(T entity, string propertyName, object oldValue, object newValue);
    bool RaiseEntityEvent(T entity, EventAction action, bool isPreEvent);
    bool RaiseEvent(EventHandler<UnitofWorkParams> handler, object sender, T entity, EventAction action);
}

IUnitofWorkDefaults<T>

public interface IUnitofWorkDefaults<T> where T : Entity, new()
{
    void ApplyDefaults(T entity, DefaultValueContext context);
    Task<T> ApplyDefaultsAsync(T entity, DefaultValueContext context);
    bool HasDefaults(string fieldName);
    DefaultValue GetDefaultForField(string fieldName);
    void ApplyInsertDefaults(T entity);
    void ApplyUpdateDefaults(T entity);
    IErrorsInfo ValidateAppliedDefaults(T entity);
}

7 Capability Interfaces (IUnitofWorkCapabilities.cs)

public interface IRevertable
{
    bool RevertItem(object item);
    Task<bool> RevertItemAsync(object item, CancellationToken ct = default);
}

public interface IBatchCommittable
{
    Task<CommitBatchResult> CommitBatchAsync(int batchSize = 200,
        IProgress<CommitBatchProgress> progress = null, CancellationToken ct = default);
}

public interface IExportable
{
    DataTable ToDataTable();
    Task ToJsonAsync(Stream stream, CancellationToken ct = default);
    Task ToCsvAsync(Stream stream, char delimiter = ',', CancellationToken ct = default);
}

public interface IImportable
{
    Task<int> LoadFromJsonAsync(Stream stream, bool clearFirst = true, CancellationToken ct = default);
    Task<int> LoadFromCsvAsync(Stream stream, char delimiter = ',', bool clearFirst = true, bool hasHeaderRow = true, CancellationToken ct = default);
}

public interface IAggregatable
{
    decimal Sum(string numericFieldName);
    decimal Average(string numericFieldName);
    int Count(Func<object, bool> predicate = null);
}

public interface IUndoable
{
    bool CanUndo { get; }
    bool CanRedo { get; }
    bool UndoLastAction();
    bool RedoLastAction();
    void EnableUndo(bool enable, int maxDepth = 50);
}

public interface IMergeable
{
    Task<bool> RefreshAsync(List<AppFilter> filters = null,
        ConflictMode conflictMode = ConflictMode.ServerWins, CancellationToken ct = default);
}

public interface IUnitofWorkHistory
{
    ChangeSummary GetChangeSummary();
    IReadOnlyList<QueryHistoryEntry> GetQueryHistory();
    void ClearQueryHistory();
    object CloneItem(object item, bool deep = false);
}

UnitOfWorkWrapperExtensions — Full API

public static class UnitOfWorkWrapperExtensions
{
    // Iterate all records asynchronously
    public static Task<int> ForEachRecordAsync(this IUnitOfWorkWrapper wrapper,
        Func<dynamic, Task<bool>> processor);

    // Create/Find
    public static dynamic CreateNewRecord(this IUnitOfWorkWrapper wrapper);
    public static bool TryCreateNewRecord(this IUnitOfWorkWrapper wrapper, out dynamic record);
    public static dynamic FindRecord(this IUnitOfWorkWrapper wrapper, Func<dynamic, bool> predicate);
    public static List<dynamic> GetAllRecords(this IUnitOfWorkWrapper wrapper);
    public static bool NavigateToRecord(this IUnitOfWorkWrapper wrapper, string primaryKeyValue);

    // Position info
    public static RecordPositionInfo GetPositionInfo(this IUnitOfWorkWrapper wrapper);

    // Batch insert
    public static Task<BatchOperationResult> BatchInsertAsync(this IUnitOfWorkWrapper wrapper,
        IEnumerable<dynamic> records, IProgress<BatchProgress> progress = null);
    public static Task<BatchOperationResult> BatchInsertAsync(this IUnitOfWorkWrapper wrapper,
        IEnumerable<dynamic> records, IProgress<BatchProgress> progress,
        CancellationToken cancellationToken, bool stopOnFirstFailure = false);

    // State checks
    public static bool HasRecords(this IUnitOfWorkWrapper wrapper);

    // Field value accessors
    public static T GetCurrentFieldValue<T>(this IUnitOfWorkWrapper wrapper,
        string fieldName, T defaultValue = default);
    public static bool SetCurrentFieldValue(this IUnitOfWorkWrapper wrapper,
        string fieldName, object value);
}

// Supporting types
public class RecordPositionInfo
{
    public int CurrentIndex { get; }
    public int TotalCount { get; }
    public bool IsAtFirst { get; }
    public bool IsAtLast { get; }
    public bool HasRecords { get; }
    public dynamic CurrentRecord { get; }
    public string Summary { get; }
}

public class BatchOperationResult
{
    public int TotalRecords { get; }
    public int ProcessedRecords { get; }
    public int SuccessfulRecords { get; }
    public int FailedRecords { get; }
    public List<string> Errors { get; }
    public TimeSpan Duration { get; }
    public bool IsSuccess { get; }
    public double SuccessRate { get; }
}

UnitofWork<T> — Complete Method Index

CategoryMethods
Constructors7 overloads: () | (editor, ds, entity) | (editor, ds, entity, structure) | (editor, isListMode, obl) | (editor, ds, entity, pk) | (editor, ds, entity, structure, pk) | (editor, isListMode, obl, pk)
Read / GetRead(id), Read(pred), MultiRead(pred), Get(), Get(filters), GetQuery(query), Get(key), Get(PrimaryKeyId), FindAsync(pred,ct), FindManyAsync(pred,ct)
Create / AddNew(), Add(entity), AddRange(entities)
UpdateUpdate(pred,entity), Update(entity), Update(id,entity), UpdateAsync(doc), UpdateDoc(doc), UpdateRange(entities)
InsertInsertAsync(doc), InsertDoc(doc)
DeleteDelete(pred), Delete(entity), Delete(), DeleteAsync(doc), DeleteDoc(doc), DeleteRange(entities), Delete(id)
Keys / IDsGetPrimaryKeySequence(doc), GetSeq(name), GetLastIdentity(), SetIDValue(entity,val), GetIDValue(entity)
Commit / RollbackCommit(), Commit(progress,token), Rollback(), CommitBatchAsync(batchSize,progress,ct)
NavigationMoveFirst/MoveNext/MovePrevious/MoveLast/MoveTo/MoveToItem, IsAtBOF/IsAtEOF/IsEmpty
State ManagementClear(), GetIsDirty(), DocExist(doc), DocExistByKey(doc), FindDocIdx(doc), Getindex(id/entity), GetItemFromCurrentList(idx), GetAddedEntities(), GetModifiedEntities(), GetDeletedEntities(), GetTrackingItem(item), GetChangeLog(), RevertItem(item), RevertItemAsync(item,ct), CloneItem(item,deepCopy)
Events (13 lifecycle)PreCreate, PostCreate, PreInsert, PostInsert, PreUpdate, PostUpdate, PreDelete, PostDelete, PreQuery, PostQuery, PreCommit, PostCommit, PostEdit — all EventHandler<UnitofWorkParams>
LoggingIsLogging (bool), UpdateLog (Dict), SaveLog(path)

OBL Integration Features

PhaseFeatureMethods / Properties
3ValidationIsAutoValidateEnabled, BlockCommitOnValidationError, ValidateItem, ValidateAll, GetErrors, GetInvalidItems
4Undo / RedoIsUndoEnabled, MaxUndoDepth, CanUndo, CanRedo, Undo(), Redo(), UndoLastAction(), RedoLastAction(), ClearUndoHistory(), EnableUndo(bool,int)
5Virtual LoadingIsVirtualMode, PageCacheSize, VirtualTotalPages, EnableVirtualMode, DisableVirtualMode, GoToPageAsync, PrefetchAdjacentPagesAsync, InvalidatePageCache
7Computed ColumnsRegisterComputed, UnregisterComputed, GetComputed, GetAllComputed, ComputedColumnNames
8BookmarksSetBookmark, GoToBookmark, RemoveBookmark, ClearBookmarks
9Thread SafetyIsThreadSafe, IsFrozen, Freeze(), Unfreeze(), BeginBatchUpdate()
10AggregatesSum, SumWhere, Average, AverageWhere, Min, Max, CountWhere, GroupBy, DistinctValues
11Navigation ExtsIsAtBOF, IsAtEOF, IsEmpty, MoveToItem