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
| Interface | Adds |
IUnitofWork<T> | Core contract: CRUD, Commit, Rollback, Units, events |
INotifyPropertyChanged | WinForms / WPF data binding |
IRevertable | RevertItem, RevertItemAsync |
IBatchCommittable | CommitBatchAsync with progress |
IExportable | ToDataTable, ToJsonAsync, ToCsvAsync |
IImportable | LoadFromJsonAsync, LoadFromCsvAsync |
IAggregatable | Sum, Average, Count |
IUndoable | Undo, Redo, CanUndo, CanRedo |
IMergeable | RefreshAsync with ConflictMode |
IUnitofWorkHistory | GetChangeSummary, GetQueryHistory |
Constructors
| Constructor | Mode | Use 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
| Property | Type | Description |
DMEEditor | IDMEEditor | Read-only. Set by constructor. |
DataSource | IDataSource | Resolved backing datasource. null in list mode. |
DatasourceName | string | Registered datasource name. |
EntityName | string | Entity / table name. |
EntityType | Type | typeof(T). |
EntityStructure | EntityStructure | Schema metadata (fields, keys, relations). |
PrimaryKey | string | Primary key field name. |
GuidKey | string | Optional GUID field name for secondary identity. |
IsIdentity | bool | When true, the DB generates the PK value (no client-side sequencing). |
Sequencer | string | Name of a server-side sequence to call for PK generation. |
State & collection
| Property | Type | Description |
Units | ObservableBindingList<T> | The active collection. Returns FilteredUnits when a filter is applied. |
FilteredUnits | ObservableBindingList<T> | Filter result set. Manages event subscriptions on set. |
CurrentItem | T | Currently navigated item (Units.Current). |
TotalItemCount | int | Units.Count. |
IsDirty | bool | true when any insert/update/delete is pending. |
IsInListMode | bool | Disables datasource I/O; use with in-memory collections. |
IsFilterOn | bool | Internal flag — true while a filter is active. |
DeletedUnits | List<T> | Items removed from the collection awaiting physical delete. |
InsertedKeys | Dictionary<int,string> | Auxiliary insert tracking. |
UpdatedKeys | Dictionary<int,string> | Auxiliary update tracking. |
DeletedKeys | Dictionary<int,string> | Auxiliary delete tracking. |
FilterExpression | string | Free-form filter string for in-memory LINQ filtering. |
Paging
| Property | Type | Default |
PageIndex | int | 0 |
PageSize | int | 10 |
Soft delete
| Property | Type | Description |
SoftDeleteFieldName | string | When set (e.g. "IsDeleted"), Delete() sets this field instead of physically removing the record. |
IncludeDeleted | bool | When true, Get() includes soft-deleted rows. Default false. |
Optimistic concurrency
| Property | Type | Description |
ConcurrencyMode | ConcurrencyMode | None (default), LastWriteWins, ThrowOnConflict. |
ConcurrencyFieldName | string | Field to compare for staleness (e.g. "RowVersion" or "LastModified"). |
Commit control
| Property | Type | Description |
CommitOrder | CommitOrder | Order entities are persisted during Commit(). DeletesFirst (default, FK-safe), InsertsFirst, UpdatesFirst. |
BlockCommitOnValidationError | bool | When true (default), items with Error-severity validation failures are skipped during commit. |
IsAutoValidateEnabled | bool | Validates 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).
| State | When set |
EntityState.Unchanged | Freshly loaded or after AcceptChanges(). |
EntityState.Added | After New() or Add(entity). |
EntityState.Modified | After any property change on a tracked item. |
EntityState.Deleted | After 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
| Method | Signature | Notes |
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
| Method | Signature | Notes |
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
| Method | Signature | Notes |
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
| Method | Signature | Notes |
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
| Property | Type | Description |
Cancel | bool | Set to true to abort the current operation. Only honoured by Pre-events. |
EventAction | EventAction | Identifies which lifecycle step is firing. |
Record | object | The entity involved. Cast to T when needed. |
EntityName | string | Name of the entity type. |
PropertyName | string | Set for PostEdit events — name of the changed property. |
PropertyValue | string | Set for PostEdit events — new value as string. |
Messege | string | Optional cancellation or info message. |
EventAction enum values
| Value | Description |
PreCreate | Before a new entity is added to Units. |
PostCreate | After a new entity has been added to Units. |
PreInsert | Before a new entity is written to the datasource on commit. |
PostInsert | After a new entity has been successfully written to the datasource. |
PreUpdate | Before an entity update is applied. |
PostUpdate | After an entity update has been applied. |
PreDelete | Before an entity is removed from the collection / datasource. |
PostDelete | After an entity has been removed. |
PreQuery | Before Get() fetches data. |
PostQuery | After Get() has loaded data into Units. |
PreCommit | Before the transaction begins. |
PostCommit | After all changes have been successfully written and AcceptChanges() called. |
PostEdit | After 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
| Method | Signature | Description |
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 / Property | Description |
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.
| Member | Description |
IsUndoEnabled | Enable/disable the undo stack. |
MaxUndoDepth | Maximum number of actions to retain. Default 100. Older entries are evicted. |
CanUndo | true when the undo stack is non-empty. |
CanRedo | true 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.
| Method | Description |
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. |
ComputedColumnNames | Read-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
| Feature | API | Description |
| 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.
| Method | Signature | Description |
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
| Method | Signature | Description |
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
| Method | Signature | Description |
AddRange | Task<IErrorsInfo> AddRange(IEnumerable<T>) | Inserts multiple entities. Notifications suppressed for performance; single refresh fired at end. |
UpdateRange | Task<IErrorsInfo> UpdateRange(IEnumerable<T>) | Updates each entity using the standard Update path (includes defaults, events). |
DeleteRange | Task<IErrorsInfo> DeleteRange(IEnumerable<T>) | Deletes each entity. List materialised first to avoid modification-during-enumeration. |
CommitBatchAsync | Task<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.
| Method | Returns | Description |
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)"